mirror of
https://github.com/google/pebble.git
synced 2025-06-19 15:50:36 +00:00
Import of the watch repository from Pebble
This commit is contained in:
commit
3b92768480
10334 changed files with 2564465 additions and 0 deletions
802
src/fw/services/common/accel_manager.c
Normal file
802
src/fw/services/common/accel_manager.c
Normal file
|
@ -0,0 +1,802 @@
|
|||
/*
|
||||
* 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 "accel_manager.h"
|
||||
|
||||
#include "console/prompt.h"
|
||||
#include "drivers/accel.h"
|
||||
#include "drivers/vibe.h"
|
||||
#include "kernel/events.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "mcu/interrupts.h"
|
||||
#include "os/mutex.h"
|
||||
#include "services/common/analytics/analytics.h"
|
||||
#include "services/common/event_service.h"
|
||||
#include "services/common/system_task.h"
|
||||
#include "services/imu/units.h"
|
||||
#include "syscall/syscall.h"
|
||||
#include "syscall/syscall_internal.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
#include "util/math.h"
|
||||
#include "util/shared_circular_buffer.h"
|
||||
|
||||
#include "FreeRTOS.h"
|
||||
#include "queue.h"
|
||||
|
||||
#include <inttypes.h>
|
||||
|
||||
// We use this as an argument to indicate a lookup of the current task
|
||||
#define PEBBLE_TASK_CURRENT PebbleTask_Unknown
|
||||
|
||||
#define US_PER_SECOND (1000 * 1000)
|
||||
|
||||
typedef void (*ProcessDataHandler)(CallbackEventCallback *cb, void *data);
|
||||
|
||||
// We create one of these for each data service subscriber
|
||||
typedef struct AccelManagerState {
|
||||
ListNode list_node; // Entry into the s_data_subscribers linked list
|
||||
|
||||
//! Client pointing into s_buffer
|
||||
SubsampledSharedCircularBufferClient buffer_client;
|
||||
//! The sampling interval we've promised to this client after subsampling.
|
||||
uint32_t sampling_interval_us;
|
||||
//! The requested number of samples needed before calling data_cb_handler
|
||||
uint16_t samples_per_update;
|
||||
|
||||
//! Which task we should call the data_cb_handler on
|
||||
PebbleTask task;
|
||||
CallbackEventCallback data_cb_handler;
|
||||
void* data_cb_context;
|
||||
|
||||
uint64_t timestamp_ms; // timestamp of first item in the buffer
|
||||
AccelRawData *raw_buffer; // raw buffer allocated by subscriber
|
||||
uint8_t num_samples; // number of samples in raw_buffer
|
||||
bool event_posted; // True if we've posted a "data ready" callback event
|
||||
} AccelManagerState;
|
||||
|
||||
typedef struct {
|
||||
AccelRawData rawdata;
|
||||
// The exact time the sample was collected can be recovered by:
|
||||
// time_sample_collected = s_last_empty_timestamp_ms + timestamp_delta_ms
|
||||
uint16_t timestamp_delta_ms;
|
||||
} AccelManagerBufferData;
|
||||
_Static_assert(offsetof(AccelManagerBufferData, rawdata) == 0,
|
||||
"AccelRawData must be first entry in AccelManagerBufferData struct");
|
||||
|
||||
// Statics
|
||||
//! List of all registered consumers of accel data. Points to AccelManagerState objects.
|
||||
static ListNode *s_data_subscribers = NULL;
|
||||
//! Mutex locking all accel_manager state
|
||||
static PebbleRecursiveMutex *s_accel_manager_mutex;
|
||||
|
||||
//! Reference count of how many shake subscribers we have. Used to turn off the feature when not
|
||||
//! in use.
|
||||
static uint8_t s_shake_subscribers_count = 0;
|
||||
//! Reference count of how many double tap subscribers we have. Used to turn off the feature when
|
||||
//! not in use.
|
||||
static uint8_t s_double_tap_subscribers_count = 0;
|
||||
|
||||
//! Circular buffer that raw accel data is written into before being subsampled for each client
|
||||
static SharedCircularBuffer s_buffer;
|
||||
//! Storage for s_buffer
|
||||
//! 1600 bytes (~4s of data at 50Hz)
|
||||
static uint8_t s_buffer_storage[200 * sizeof(AccelManagerBufferData)];
|
||||
|
||||
static uint64_t s_last_empty_timestamp_ms = 0;
|
||||
|
||||
static uint32_t s_accel_samples_collected_count = 0;
|
||||
|
||||
// Accel Idle
|
||||
#define ACCEL_MAX_IDLE_DELTA 100
|
||||
static bool s_is_idle = false;
|
||||
static AccelData s_last_analytics_position;
|
||||
static AccelData s_last_accel_data;
|
||||
|
||||
static void prv_setup_subsampling(uint32_t sampling_interval);
|
||||
|
||||
static void prv_shake_add_subscriber_cb(PebbleTask task) {
|
||||
mutex_lock_recursive(s_accel_manager_mutex);
|
||||
{
|
||||
if (++s_shake_subscribers_count == 1) {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Starting accel shake service");
|
||||
accel_enable_shake_detection(true);
|
||||
prv_setup_subsampling(accel_get_sampling_interval());
|
||||
}
|
||||
}
|
||||
mutex_unlock_recursive(s_accel_manager_mutex);
|
||||
}
|
||||
|
||||
static void prv_shake_remove_subscriber_cb(PebbleTask task) {
|
||||
mutex_lock_recursive(s_accel_manager_mutex);
|
||||
{
|
||||
PBL_ASSERTN(s_shake_subscribers_count > 0);
|
||||
if (--s_shake_subscribers_count == 0) {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Stopping accel shake service");
|
||||
accel_enable_shake_detection(false);
|
||||
prv_setup_subsampling(accel_get_sampling_interval());
|
||||
}
|
||||
}
|
||||
mutex_unlock_recursive(s_accel_manager_mutex);
|
||||
}
|
||||
|
||||
static void prv_double_tap_add_subscriber_cb(PebbleTask task) {
|
||||
mutex_lock_recursive(s_accel_manager_mutex);
|
||||
|
||||
if (++s_double_tap_subscribers_count == 1) {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Starting accel double tap service");
|
||||
accel_enable_double_tap_detection(true);
|
||||
prv_setup_subsampling(accel_get_sampling_interval());
|
||||
}
|
||||
|
||||
mutex_unlock_recursive(s_accel_manager_mutex);
|
||||
}
|
||||
|
||||
static void prv_double_tap_remove_subscriber_cb(PebbleTask task) {
|
||||
mutex_lock_recursive(s_accel_manager_mutex);
|
||||
|
||||
PBL_ASSERTN(s_double_tap_subscribers_count > 0);
|
||||
if (--s_double_tap_subscribers_count == 0) {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Stopping accel double tap service");
|
||||
accel_enable_double_tap_detection(false);
|
||||
prv_setup_subsampling(accel_get_sampling_interval());
|
||||
}
|
||||
|
||||
mutex_unlock_recursive(s_accel_manager_mutex);
|
||||
}
|
||||
|
||||
|
||||
//! Out of all accel subscribers, figures out:
|
||||
//! @param[out] lowest_interval_us - the lowest sampling interval requested (in microseconds)
|
||||
//! @param[out] max_n_samples - the max number of samples requested for batching
|
||||
//! @return The longest amount of samples which can be batched assuming we are
|
||||
//! running at the lowest_sampling_interval
|
||||
//!
|
||||
//! @note currently the longest interval we can batch samples for is computed
|
||||
//! as the minimum of (samples to batch / sample rate) out of all the active
|
||||
//! subscribers. This means that if we have two subscribers, subscriber A at
|
||||
//! 200ms, and subscriber B at 250ms, new samples will become available every
|
||||
//! 200ms, so subscriber B's data buffer would not fill until 400ms, resulting
|
||||
//! in a 150ms latency. This is how the legacy implementation worked as well
|
||||
//! but is potentionally something we could improve in the future if it becomes
|
||||
//! a problem.
|
||||
static uint32_t prv_get_sample_interval_info(uint32_t *lowest_interval_us,
|
||||
uint32_t *max_n_samples) {
|
||||
*lowest_interval_us = (US_PER_SECOND / ACCEL_SAMPLING_10HZ);
|
||||
*max_n_samples = 0;
|
||||
// Tracks which subscriber wants data most frequently. Note this is different than just
|
||||
// lowest_interval_us * max_n_samples as those values can come from 2 different subscribers
|
||||
// where we want to know which one subscriber wants the highest update frequency.
|
||||
uint32_t lowest_us_per_update = UINT32_MAX;
|
||||
|
||||
AccelManagerState *state = (AccelManagerState *)s_data_subscribers;
|
||||
while (state) {
|
||||
*lowest_interval_us = MIN(state->sampling_interval_us, *lowest_interval_us);
|
||||
*max_n_samples = MAX(state->samples_per_update, *max_n_samples);
|
||||
|
||||
if (state->samples_per_update > 0) {
|
||||
uint32_t us_per_update = state->samples_per_update * state->sampling_interval_us;
|
||||
lowest_us_per_update = MIN(lowest_us_per_update, us_per_update);
|
||||
}
|
||||
state = (AccelManagerState *)state->list_node.next;
|
||||
}
|
||||
|
||||
if (lowest_us_per_update == UINT32_MAX) {
|
||||
// No one subscribing or no one who wants updates
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t num_samples = lowest_us_per_update / (*lowest_interval_us);
|
||||
num_samples = MIN(num_samples, ACCEL_MAX_SAMPLES_PER_UPDATE);
|
||||
|
||||
return num_samples;
|
||||
}
|
||||
|
||||
static void prv_setup_subsampling(uint32_t sampling_interval) {
|
||||
// Setup the subsampling numerator and denominators
|
||||
AccelManagerState *state = (AccelManagerState *)s_data_subscribers;
|
||||
while (state) {
|
||||
uint32_t interval_gcd = gcd(sampling_interval,
|
||||
state->sampling_interval_us);
|
||||
uint16_t numerator = sampling_interval / interval_gcd;
|
||||
uint16_t denominator = state->sampling_interval_us / interval_gcd;
|
||||
|
||||
PBL_LOG(LOG_LEVEL_DEBUG,
|
||||
"set subsampling for session %p to %" PRIu16 "/%" PRIu16,
|
||||
state, numerator, denominator);
|
||||
subsampled_shared_circular_buffer_client_set_ratio(
|
||||
&state->buffer_client, numerator, denominator);
|
||||
state = (AccelManagerState *)state->list_node.next;
|
||||
}
|
||||
}
|
||||
|
||||
//! Should be called after any change to a subscriber. Handles re-configuring
|
||||
//! the accel driver to satisfy the requirements of all consumers (i.e setting
|
||||
//! sampling rate and max number of samples which can be batched). If there are no
|
||||
//! subscribers, chooses the lowest power configuration settings
|
||||
static void prv_update_driver_config(void) {
|
||||
// TODO: Add low power support
|
||||
uint32_t lowest_interval_us;
|
||||
uint32_t max_n_samples;
|
||||
uint32_t max_batch = prv_get_sample_interval_info(&lowest_interval_us, &max_n_samples);
|
||||
|
||||
// Configure the driver sampling interval and get the actual interval that the driver is going
|
||||
// to use.
|
||||
uint32_t interval_us = accel_set_sampling_interval(lowest_interval_us);
|
||||
|
||||
prv_setup_subsampling(interval_us);
|
||||
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "setting accel rate:%"PRIu32", num_samples:%"PRIu32,
|
||||
US_PER_SECOND / interval_us, max_batch);
|
||||
|
||||
accel_set_num_samples(max_batch);
|
||||
}
|
||||
|
||||
static bool prv_call_data_callback(AccelManagerState *state) {
|
||||
switch (state->task) {
|
||||
case PebbleTask_App:
|
||||
case PebbleTask_Worker:
|
||||
case PebbleTask_KernelMain: {
|
||||
PebbleEvent event = {
|
||||
.type = PEBBLE_CALLBACK_EVENT,
|
||||
.callback = {
|
||||
.callback = state->data_cb_handler,
|
||||
.data = state->data_cb_context,
|
||||
},
|
||||
};
|
||||
|
||||
QueueHandle_t queue = pebble_task_get_to_queue(state->task);
|
||||
// Note: This call may fail if the queue is full but when a new sample
|
||||
// becomes available from the driver, we will retry anyway
|
||||
return xQueueSendToBack(queue, &event, 0);
|
||||
}
|
||||
case PebbleTask_KernelBackground:
|
||||
return system_task_add_callback(state->data_cb_handler, state->data_cb_context);
|
||||
case PebbleTask_NewTimers:
|
||||
return new_timer_add_work_callback(state->data_cb_handler, state->data_cb_context);
|
||||
default:
|
||||
WTF; // Unsupported task for the accel manager
|
||||
}
|
||||
}
|
||||
|
||||
//! This is called every time new samples arrive from the accel driver & every
|
||||
//! time data has been drained by the accel service. Its responsibility is
|
||||
//! populating subscriber storage with new samples (at the requested sample
|
||||
//! frequency) and generating a callback event on the subscriber's queue when
|
||||
//! the requested number of samples have been batched
|
||||
static void prv_dispatch_data(void) {
|
||||
mutex_lock_recursive(s_accel_manager_mutex);
|
||||
|
||||
AccelManagerState * state = (AccelManagerState *)s_data_subscribers;
|
||||
while (state) {
|
||||
if (!state->raw_buffer) {
|
||||
state = (AccelManagerState *)state->list_node.next;
|
||||
continue;
|
||||
}
|
||||
|
||||
// if subscribed but not looking for any samples then just drop the data
|
||||
if (state->samples_per_update == 0) {
|
||||
uint16_t len = shared_circular_buffer_get_read_space_remaining(
|
||||
&s_buffer, &state->buffer_client.buffer_client);
|
||||
shared_circular_buffer_consume(
|
||||
&s_buffer, &state->buffer_client.buffer_client, len);
|
||||
state = (AccelManagerState *)state->list_node.next;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If buffer has room, read more data
|
||||
uint32_t samples_drained = 0;
|
||||
while (state->num_samples < state->samples_per_update) {
|
||||
// Read available data.
|
||||
AccelManagerBufferData data;
|
||||
if (!shared_circular_buffer_read_subsampled(
|
||||
&s_buffer, &state->buffer_client, sizeof(data), &data, 1)) {
|
||||
// we have drained all available samples
|
||||
break;
|
||||
}
|
||||
|
||||
// Note: the accel_service currently only buffers AccelRawData (i.e it
|
||||
// does not track the timestamp explicitly.) The accel service drains a
|
||||
// buffers worth of data at a time and asks for the starting time
|
||||
// (state->timestamp_ms) of the first sample in that buffer when it
|
||||
// does. Therefore, we provide the real time for the first sample. In
|
||||
// the future, we could phase out legacy accel code and provide the
|
||||
// exact timestamp with every sample
|
||||
if (state->num_samples == 0) {
|
||||
state->timestamp_ms = s_last_empty_timestamp_ms + data.timestamp_delta_ms;
|
||||
}
|
||||
|
||||
memcpy(state->raw_buffer + state->num_samples, &data,
|
||||
sizeof(AccelRawData));
|
||||
state->num_samples++;
|
||||
samples_drained++;
|
||||
}
|
||||
|
||||
// If buffer is full, notify subscriber to process it
|
||||
if (!state->event_posted && state->num_samples >= state->samples_per_update) {
|
||||
// Notify the subscriber that data is available
|
||||
state->event_posted = prv_call_data_callback(state);
|
||||
|
||||
ACCEL_LOG_DEBUG("full set of %d samples for session %p", state->num_samples, state);
|
||||
|
||||
if (!state->event_posted) {
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Failed to post accel event to task: 0x%x", (int) state->task);
|
||||
}
|
||||
}
|
||||
state = (AccelManagerState *)state->list_node.next;
|
||||
}
|
||||
|
||||
mutex_unlock_recursive(s_accel_manager_mutex);
|
||||
}
|
||||
|
||||
#ifdef TEST_KERNEL_SUBSCRIPTION
|
||||
static void prv_kernel_data_subscription_handler(AccelData *accel_data,
|
||||
uint32_t num_samples) {
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Received %" PRIu32 " accel samples for KernelMain.", num_samples);
|
||||
}
|
||||
|
||||
static void prv_kernel_tap_subscription_handler(AccelAxisType axis,
|
||||
int32_t direction) {
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Received a tap event for KernelMain, axis: %d, "
|
||||
"direction: %" PRId32, axis, direction);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Compute and return the device's delta position to help determine movement as idle.
|
||||
static uint32_t prv_compute_delta_pos(AccelData *cur_pos, AccelData *last_pos) {
|
||||
return (abs(last_pos->x - cur_pos->x) + abs(last_pos->y - cur_pos->y) +
|
||||
abs(last_pos->z - cur_pos->z));
|
||||
}
|
||||
|
||||
/*
|
||||
* Exported APIs
|
||||
*/
|
||||
|
||||
// we expect this to get called once by accel_manager_init() so we have a default
|
||||
// starting position.
|
||||
void analytics_external_collect_accel_xyz_delta(void) {
|
||||
AccelData accel_data;
|
||||
|
||||
if (sys_accel_manager_peek(&accel_data) == 0) {
|
||||
uint32_t delta = prv_compute_delta_pos(&accel_data, &s_last_analytics_position);
|
||||
s_is_idle = (delta < ACCEL_MAX_IDLE_DELTA);
|
||||
s_last_analytics_position = accel_data;
|
||||
analytics_set(ANALYTICS_DEVICE_METRIC_ACCEL_XYZ_DELTA, delta, AnalyticsClient_System);
|
||||
}
|
||||
}
|
||||
|
||||
void analytics_external_collect_accel_samples_received(void) {
|
||||
mutex_lock_recursive(s_accel_manager_mutex);
|
||||
uint32_t samps_collected = s_accel_samples_collected_count;
|
||||
s_accel_samples_collected_count = 0;
|
||||
mutex_unlock_recursive(s_accel_manager_mutex);
|
||||
|
||||
analytics_set(ANALYTICS_DEVICE_METRIC_ACCEL_SAMPLE_COUNT, samps_collected,
|
||||
AnalyticsClient_System);
|
||||
}
|
||||
|
||||
void accel_manager_init(void) {
|
||||
s_accel_manager_mutex = mutex_create_recursive();
|
||||
|
||||
shared_circular_buffer_init(&s_buffer, s_buffer_storage,
|
||||
sizeof(s_buffer_storage));
|
||||
|
||||
event_service_init(PEBBLE_ACCEL_SHAKE_EVENT, &prv_shake_add_subscriber_cb,
|
||||
&prv_shake_remove_subscriber_cb);
|
||||
|
||||
event_service_init(PEBBLE_ACCEL_DOUBLE_TAP_EVENT, &prv_double_tap_add_subscriber_cb,
|
||||
&prv_double_tap_remove_subscriber_cb);
|
||||
|
||||
// we always listen for motion events to decide whether or not to enable the backlight
|
||||
// TODO: KernelMain could probably subscribe to the motion service to accomplish this?
|
||||
prv_shake_add_subscriber_cb(PebbleTask_KernelMain);
|
||||
|
||||
analytics_external_collect_accel_xyz_delta();
|
||||
}
|
||||
|
||||
static void prv_copy_accel_sample_to_accel_data(AccelDriverSample const *accel_sample,
|
||||
AccelData *accel_data) {
|
||||
*accel_data = (AccelData) {
|
||||
.x = accel_sample->x,
|
||||
.y = accel_sample->y,
|
||||
.z = accel_sample->z,
|
||||
.timestamp /* ms */ = (accel_sample->timestamp_us / 1000),
|
||||
.did_vibrate = (sys_vibe_get_vibe_strength() != VIBE_STRENGTH_OFF)
|
||||
};
|
||||
}
|
||||
|
||||
static void prv_update_last_accel_data(AccelDriverSample const *data) {
|
||||
prv_copy_accel_sample_to_accel_data(data, &s_last_accel_data);
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(int, sys_accel_manager_peek, AccelData *accel_data) {
|
||||
if (PRIVILEGE_WAS_ELEVATED) {
|
||||
syscall_assert_userspace_buffer(accel_data, sizeof(*accel_data));
|
||||
}
|
||||
|
||||
// bump peek analytics
|
||||
analytics_inc(ANALYTICS_DEVICE_METRIC_ACCEL_PEEK_COUNT, AnalyticsClient_System);
|
||||
PebbleTask task = pebble_task_get_current();
|
||||
if (task == PebbleTask_Worker || task == PebbleTask_App) {
|
||||
analytics_inc(ANALYTICS_APP_METRIC_ACCEL_PEEK_COUNT, AnalyticsClient_CurrentTask);
|
||||
}
|
||||
|
||||
mutex_lock_recursive(s_accel_manager_mutex);
|
||||
|
||||
AccelDriverSample data;
|
||||
int result = accel_peek(&data);
|
||||
if (result == 0 /* success */) {
|
||||
prv_copy_accel_sample_to_accel_data(&data, accel_data);
|
||||
prv_update_last_accel_data(&data);
|
||||
}
|
||||
|
||||
mutex_unlock_recursive(s_accel_manager_mutex);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(AccelManagerState*, sys_accel_manager_data_subscribe,
|
||||
AccelSamplingRate rate, AccelDataReadyCallback data_cb, void* context,
|
||||
PebbleTask handler_task) {
|
||||
AccelManagerState *state;
|
||||
|
||||
mutex_lock_recursive(s_accel_manager_mutex);
|
||||
{
|
||||
state = kernel_malloc_check(sizeof(AccelManagerState));
|
||||
*state = (AccelManagerState) {
|
||||
.task = handler_task,
|
||||
.data_cb_handler = data_cb,
|
||||
.data_cb_context = context,
|
||||
.sampling_interval_us = (US_PER_SECOND / rate),
|
||||
.samples_per_update = ACCEL_MAX_SAMPLES_PER_UPDATE,
|
||||
};
|
||||
|
||||
bool no_subscribers_before = (s_data_subscribers == NULL);
|
||||
s_data_subscribers = list_insert_before(s_data_subscribers, &state->list_node);
|
||||
if (no_subscribers_before) {
|
||||
sys_vibe_history_start_collecting();
|
||||
}
|
||||
|
||||
// Add as a consumer to the accel buffer
|
||||
shared_circular_buffer_add_subsampled_client(
|
||||
&s_buffer, &state->buffer_client, 1, 1);
|
||||
|
||||
// Update the sampling rate and num samples of the driver considering the new
|
||||
// subscriber's request
|
||||
prv_update_driver_config();
|
||||
}
|
||||
mutex_unlock_recursive(s_accel_manager_mutex);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(bool, sys_accel_manager_data_unsubscribe, AccelManagerState *state) {
|
||||
bool event_outstanding;
|
||||
mutex_lock_recursive(s_accel_manager_mutex);
|
||||
{
|
||||
event_outstanding = state->event_posted;
|
||||
// Remove this subscriber and free up its state variables
|
||||
shared_circular_buffer_remove_subsampled_client(
|
||||
&s_buffer, &state->buffer_client);
|
||||
list_remove(&state->list_node, &s_data_subscribers /* &head */, NULL /* &tail */);
|
||||
kernel_free(state);
|
||||
|
||||
if (!s_data_subscribers) {
|
||||
// If no one left using the data subscription, disable it
|
||||
sys_vibe_history_stop_collecting();
|
||||
}
|
||||
|
||||
// reconfig for the common subset of requirements among remaining subscribers
|
||||
prv_update_driver_config();
|
||||
}
|
||||
mutex_unlock_recursive(s_accel_manager_mutex);
|
||||
return event_outstanding;
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(int, sys_accel_manager_set_sampling_rate,
|
||||
AccelManagerState *state, AccelSamplingRate rate) {
|
||||
|
||||
// Make sure the rate is one of our externally supported fixed rates
|
||||
switch (rate) {
|
||||
case ACCEL_SAMPLING_10HZ:
|
||||
case ACCEL_SAMPLING_25HZ:
|
||||
case ACCEL_SAMPLING_50HZ:
|
||||
case ACCEL_SAMPLING_100HZ:
|
||||
break;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
|
||||
mutex_lock_recursive(s_accel_manager_mutex);
|
||||
|
||||
state->sampling_interval_us = (US_PER_SECOND / rate);
|
||||
prv_update_driver_config();
|
||||
|
||||
mutex_unlock_recursive(s_accel_manager_mutex);
|
||||
|
||||
// TODO: doesn't look like our API specifies what this routine should return.
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t accel_manager_set_jitterfree_sampling_rate(AccelManagerState *state,
|
||||
uint32_t min_rate_mHz) {
|
||||
// HACK
|
||||
// We're dumb and don't support anything other than 12.5hz for jitter-free sampling. We chose
|
||||
// this rate because it divides evenly into all the native rates we support right now.
|
||||
// Supporting a wider range of jitter-free rates is harder due to dealing with all the potential
|
||||
// combinations of different subscribers asking for different rates.
|
||||
const uint32_t ONLY_SUPPORTED_JITTERFREE_RATE_MILLIHZ = 12500;
|
||||
PBL_ASSERTN(min_rate_mHz <= ONLY_SUPPORTED_JITTERFREE_RATE_MILLIHZ);
|
||||
|
||||
mutex_lock_recursive(s_accel_manager_mutex);
|
||||
|
||||
state->sampling_interval_us = (US_PER_SECOND * 1000) / ONLY_SUPPORTED_JITTERFREE_RATE_MILLIHZ;
|
||||
prv_update_driver_config();
|
||||
|
||||
mutex_unlock_recursive(s_accel_manager_mutex);
|
||||
|
||||
return ONLY_SUPPORTED_JITTERFREE_RATE_MILLIHZ;
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(int, sys_accel_manager_set_sample_buffer,
|
||||
AccelManagerState *state, AccelRawData *buffer, uint32_t samples_per_update) {
|
||||
if (samples_per_update > ACCEL_MAX_SAMPLES_PER_UPDATE) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (PRIVILEGE_WAS_ELEVATED) {
|
||||
syscall_assert_userspace_buffer(buffer, samples_per_update * sizeof(AccelRawData));
|
||||
}
|
||||
|
||||
mutex_lock_recursive(s_accel_manager_mutex);
|
||||
{
|
||||
state->raw_buffer = buffer;
|
||||
state->samples_per_update = samples_per_update;
|
||||
state->num_samples = 0;
|
||||
prv_update_driver_config();
|
||||
}
|
||||
mutex_unlock_recursive(s_accel_manager_mutex);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(uint32_t, sys_accel_manager_get_num_samples,
|
||||
AccelManagerState *state, uint64_t *timestamp_ms) {
|
||||
|
||||
mutex_lock_recursive(s_accel_manager_mutex);
|
||||
|
||||
uint32_t result = state->num_samples;
|
||||
*timestamp_ms = state->timestamp_ms;
|
||||
|
||||
mutex_unlock_recursive(s_accel_manager_mutex);
|
||||
return result;
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(bool, sys_accel_manager_consume_samples,
|
||||
AccelManagerState *state, uint32_t samples) {
|
||||
bool success = true;
|
||||
mutex_lock_recursive(s_accel_manager_mutex);
|
||||
|
||||
if (samples > state->num_samples) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Consuming more samples than exist %d vs %d!",
|
||||
(int)samples, (int)state->num_samples);
|
||||
success = false;
|
||||
} else if (samples != state->num_samples) {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Dropping %d accel samples", (int)(state->num_samples - samples));
|
||||
success = false;
|
||||
}
|
||||
|
||||
state->event_posted = false;
|
||||
state->num_samples = 0;
|
||||
// Fill it again from circular buffer
|
||||
prv_dispatch_data();
|
||||
|
||||
mutex_unlock_recursive(s_accel_manager_mutex);
|
||||
return success;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* TODO: APIs that still need to be implemented
|
||||
*/
|
||||
|
||||
void accel_manager_enable(bool on) { }
|
||||
|
||||
void accel_manager_exit_low_power_mode(void) { }
|
||||
|
||||
// Return true if we are "idle", defined as seeing no movement in the last hour.
|
||||
bool accel_is_idle(void) {
|
||||
// It was idle recently, see if it's still idle. Note we avoid reading the accel hardware
|
||||
// again here to keep this call as lightweight as possible. Instead we are just comparing the last
|
||||
// read value with the value last captured by analytics (which does so on an hourly heartbeat).
|
||||
return (prv_compute_delta_pos(&s_last_accel_data, &s_last_analytics_position)
|
||||
< ACCEL_MAX_IDLE_DELTA);
|
||||
}
|
||||
|
||||
// The accelerometer should issue a shake/tap event with any slight movements when stationary.
|
||||
// This will allow the watch to immediately return to normal mode, and attempt to reconnect to
|
||||
// the phone.
|
||||
void accel_enable_high_sensitivity(bool high_sensitivity) {
|
||||
mutex_lock_recursive(s_accel_manager_mutex);
|
||||
accel_set_shake_sensitivity_high(high_sensitivity);
|
||||
mutex_unlock_recursive(s_accel_manager_mutex);
|
||||
}
|
||||
|
||||
/*
|
||||
* Driver Callbacks - See accel.h header for more context
|
||||
*/
|
||||
|
||||
static bool prv_shared_buffer_empty(void) {
|
||||
bool empty = true;
|
||||
mutex_lock_recursive(s_accel_manager_mutex);
|
||||
{
|
||||
AccelManagerState *state = (AccelManagerState *)s_data_subscribers;
|
||||
while (state) {
|
||||
int left = shared_circular_buffer_get_read_space_remaining(
|
||||
&s_buffer, &state->buffer_client.buffer_client);
|
||||
if (left != 0) {
|
||||
empty = false;
|
||||
break;
|
||||
}
|
||||
state = (AccelManagerState *)state->list_node.next;
|
||||
}
|
||||
}
|
||||
mutex_unlock_recursive(s_accel_manager_mutex);
|
||||
return empty;
|
||||
}
|
||||
|
||||
void accel_cb_new_sample(AccelDriverSample const *data) {
|
||||
prv_update_last_accel_data(data);
|
||||
|
||||
s_accel_samples_collected_count++;
|
||||
|
||||
if (!s_buffer.clients) {
|
||||
return; // no clients so don't buffer any data
|
||||
}
|
||||
|
||||
AccelManagerBufferData accel_buffer_data;
|
||||
accel_buffer_data.rawdata.x = data->x;
|
||||
accel_buffer_data.rawdata.y = data->y;
|
||||
accel_buffer_data.rawdata.z = data->z;
|
||||
|
||||
if (prv_shared_buffer_empty()) {
|
||||
s_last_empty_timestamp_ms = data->timestamp_us / 1000;
|
||||
}
|
||||
|
||||
// Note: the delta value overflows if the s_buffer is not drained for ~65s,
|
||||
// but there should be more than enough time for it to drain in that window
|
||||
accel_buffer_data.timestamp_delta_ms = ((data->timestamp_us / 1000) -
|
||||
s_last_empty_timestamp_ms);
|
||||
|
||||
// if we have one or more clients who fell behind reading out of the buffer,
|
||||
// we will advance them until there is enough space available for the new data
|
||||
bool rv = shared_circular_buffer_write(&s_buffer, (uint8_t *)&accel_buffer_data,
|
||||
sizeof(accel_buffer_data), false /*advance_slackers*/);
|
||||
if (!rv) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Accel subscriber fell behind, truncating data");
|
||||
rv = shared_circular_buffer_write(&s_buffer, (uint8_t *)&accel_buffer_data,
|
||||
sizeof(accel_buffer_data), true /*advance_slackers*/);
|
||||
}
|
||||
|
||||
PBL_ASSERTN(rv);
|
||||
|
||||
prv_dispatch_data();
|
||||
}
|
||||
|
||||
void accel_cb_shake_detected(IMUCoordinateAxis axis, int32_t direction) {
|
||||
PebbleEvent e = {
|
||||
.type = PEBBLE_ACCEL_SHAKE_EVENT,
|
||||
.accel_tap = {
|
||||
.axis = axis,
|
||||
.direction = direction,
|
||||
},
|
||||
};
|
||||
|
||||
event_put(&e);
|
||||
}
|
||||
|
||||
void accel_cb_double_tap_detected(IMUCoordinateAxis axis, int32_t direction) {
|
||||
PebbleEvent e = {
|
||||
.type = PEBBLE_ACCEL_DOUBLE_TAP_EVENT,
|
||||
.accel_tap = {
|
||||
.axis = axis,
|
||||
.direction = direction,
|
||||
},
|
||||
};
|
||||
|
||||
event_put(&e);
|
||||
}
|
||||
|
||||
static void prv_handle_accel_driver_work_cb(void *data) {
|
||||
// The accel manager is responsible for handling locking
|
||||
mutex_lock_recursive(s_accel_manager_mutex);
|
||||
AccelOffloadCallback cb = data;
|
||||
cb();
|
||||
mutex_unlock_recursive(s_accel_manager_mutex);
|
||||
}
|
||||
|
||||
void accel_offload_work_from_isr(AccelOffloadCallback cb, bool *should_context_switch) {
|
||||
PBL_ASSERTN(mcu_state_is_isr());
|
||||
|
||||
*should_context_switch =
|
||||
new_timer_add_work_callback_from_isr(prv_handle_accel_driver_work_cb, cb);
|
||||
}
|
||||
|
||||
bool accel_manager_run_selftest(void) {
|
||||
mutex_lock_recursive(s_accel_manager_mutex);
|
||||
bool rv = accel_run_selftest();
|
||||
mutex_unlock_recursive(s_accel_manager_mutex);
|
||||
return rv;
|
||||
}
|
||||
|
||||
#if !defined(PLATFORM_SILK)
|
||||
// Note: This selftest is only used for MFG today. When we start to build out a
|
||||
// gyro API, we will need to come up with a more generic way to handle locking
|
||||
// for a gyro only part vs gyro + accel part
|
||||
extern bool gyro_run_selftest(void);
|
||||
bool gyro_manager_run_selftest(void) {
|
||||
mutex_lock_recursive(s_accel_manager_mutex);
|
||||
bool rv = gyro_run_selftest();
|
||||
mutex_unlock_recursive(s_accel_manager_mutex);
|
||||
return rv;
|
||||
}
|
||||
#endif
|
||||
|
||||
void command_accel_peek(void) {
|
||||
AccelData data;
|
||||
|
||||
int result = sys_accel_manager_peek(&data);
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "result: %d", result);
|
||||
|
||||
char buffer[20];
|
||||
prompt_send_response_fmt(buffer, sizeof(buffer), "X: %"PRId16, data.x);
|
||||
prompt_send_response_fmt(buffer, sizeof(buffer), "Y: %"PRId16, data.y);
|
||||
prompt_send_response_fmt(buffer, sizeof(buffer), "Z: %"PRId16, data.z);
|
||||
}
|
||||
|
||||
void command_accel_num_samples(char *num_samples) {
|
||||
int num = atoi(num_samples);
|
||||
mutex_lock_recursive(s_accel_manager_mutex);
|
||||
accel_set_num_samples(num);
|
||||
mutex_unlock_recursive(s_accel_manager_mutex);
|
||||
}
|
||||
|
||||
#if UNITTEST
|
||||
/*
|
||||
* Helper routines strictly for unit tests
|
||||
*/
|
||||
|
||||
void test_accel_manager_get_subsample_info(AccelManagerState *state, uint16_t *num, uint16_t *den,
|
||||
uint16_t *samps_per_update) {
|
||||
*num = state->buffer_client.numerator;
|
||||
*den = state->buffer_client.denominator;
|
||||
*samps_per_update = state->samples_per_update;
|
||||
}
|
||||
|
||||
void test_accel_manager_reset(void) {
|
||||
s_buffer = (SharedCircularBuffer){};
|
||||
AccelManagerState *state = (AccelManagerState *)s_data_subscribers;
|
||||
while (state) {
|
||||
AccelManagerState *free_state = state;
|
||||
state = (AccelManagerState *)state->list_node.next;
|
||||
kernel_free(free_state);
|
||||
}
|
||||
s_data_subscribers = NULL;
|
||||
s_shake_subscribers_count = 0;
|
||||
s_double_tap_subscribers_count = 0;
|
||||
}
|
||||
|
||||
#endif
|
91
src/fw/services/common/accel_manager.h
Normal file
91
src/fw/services/common/accel_manager.h
Normal file
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* 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 "accel_manager_types.h"
|
||||
|
||||
#include "kernel/pebble_tasks.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
|
||||
#define ACCEL_LOG_DEBUG(fmt, args...) PBL_LOG_D(LOG_DOMAIN_ACCEL, LOG_LEVEL_DEBUG, fmt, ## args)
|
||||
|
||||
typedef void (*AccelDataReadyCallback)(void *context);
|
||||
|
||||
typedef struct AccelManagerState AccelManagerState;
|
||||
|
||||
|
||||
static const unsigned int ACCEL_MAX_SAMPLES_PER_UPDATE = 25;
|
||||
|
||||
|
||||
void accel_manager_init(void);
|
||||
void accel_manager_enable(bool on);
|
||||
|
||||
// Peek interface
|
||||
///////////////////////////////////////////////////////////
|
||||
|
||||
int sys_accel_manager_peek(AccelData *accel_data);
|
||||
|
||||
// Callback interface
|
||||
///////////////////////////////////////////////////////////
|
||||
|
||||
//! Subscribe to data events. The supplied callback will be called with the supplied context
|
||||
//! whenever new data is available in the buffer that was previously supplied to
|
||||
//! sys_accel_manager_set_sample_buffer. The callback will be called on the handler_task task.
|
||||
//!
|
||||
//! @return An AccelManagerState object that has been allocated on the kernel heap. You must call
|
||||
//! sys_accel_manager_data_unsubscribe to free this object when you're done.
|
||||
AccelManagerState* sys_accel_manager_data_subscribe(
|
||||
AccelSamplingRate rate, AccelDataReadyCallback data_cb, void* context,
|
||||
PebbleTask handler_task);
|
||||
|
||||
//! @return true if an unprocessed data event is outstanding
|
||||
bool sys_accel_manager_data_unsubscribe(AccelManagerState *state);
|
||||
|
||||
//! Configured an existing subscription to use a given sample rate. Jitter-inducing subsampling
|
||||
//! may be used to accomplish the desired rate.
|
||||
int sys_accel_manager_set_sampling_rate(AccelManagerState *state, AccelSamplingRate rate);
|
||||
|
||||
//! Reconfigure an existing subscription to use a sampling rate that's the lowest the hardware
|
||||
//! can support without introducing jitter and is at least min_rate_hz.
|
||||
//!
|
||||
//! @param min_rate_hz The lowest desired sample rate in millihertz.
|
||||
//! @return The resulting sample rate in millihertz. 0 if it's not possible to get a rate high
|
||||
//! enough.
|
||||
uint32_t accel_manager_set_jitterfree_sampling_rate(AccelManagerState *state,
|
||||
uint32_t min_rate_mHz);
|
||||
|
||||
int sys_accel_manager_set_sample_buffer(AccelManagerState *state, AccelRawData *buffer,
|
||||
uint32_t samples_per_update);
|
||||
|
||||
uint32_t sys_accel_manager_get_num_samples(AccelManagerState *state, uint64_t *timestamp_ms);
|
||||
bool sys_accel_manager_consume_samples(AccelManagerState *state, uint32_t samples);
|
||||
|
||||
// Functions for internal use
|
||||
///////////////////////////////////////////////////////////
|
||||
|
||||
bool accel_manager_run_selftest(void);
|
||||
bool gyro_manager_run_selftest(void);
|
||||
|
||||
// Set whether the accelerometer should be in a sensitive state in order to trigger an accel tap
|
||||
// event from any small movements
|
||||
void accel_enable_high_sensitivity(bool high_sensitivity);
|
||||
|
||||
// lightweight call to determine if the watch is idle
|
||||
bool accel_is_idle(void);
|
59
src/fw/services/common/accel_manager_types.h
Normal file
59
src/fw/services/common/accel_manager_types.h
Normal file
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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>
|
||||
|
||||
//! Valid accelerometer sampling rates, in Hz
|
||||
typedef enum {
|
||||
//! 10 HZ sampling rate
|
||||
ACCEL_SAMPLING_10HZ = 10,
|
||||
//! 25 HZ sampling rate [Default]
|
||||
ACCEL_SAMPLING_25HZ = 25,
|
||||
//! 50 HZ sampling rate
|
||||
ACCEL_SAMPLING_50HZ = 50,
|
||||
//! 100 HZ sampling rate
|
||||
ACCEL_SAMPLING_100HZ = 100,
|
||||
} AccelSamplingRate;
|
||||
|
||||
//! A single accelerometer sample for all three axes
|
||||
typedef struct __attribute__((__packed__)) {
|
||||
//! acceleration along the x axis
|
||||
int16_t x;
|
||||
//! acceleration along the y axis
|
||||
int16_t y;
|
||||
//! acceleration along the z axis
|
||||
int16_t z;
|
||||
} AccelRawData;
|
||||
|
||||
//! A single accelerometer sample for all three axes including timestamp and
|
||||
//! vibration rumble status.
|
||||
typedef struct __attribute__((__packed__)) AccelData {
|
||||
//! acceleration along the x axis
|
||||
int16_t x;
|
||||
//! acceleration along the y axis
|
||||
int16_t y;
|
||||
//! acceleration along the z axis
|
||||
int16_t z;
|
||||
|
||||
//! true if the watch vibrated when this sample was collected
|
||||
bool did_vibrate;
|
||||
|
||||
//! timestamp, in milliseconds
|
||||
uint64_t timestamp;
|
||||
} AccelData;
|
116
src/fw/services/common/analytics/analytics.h
Normal file
116
src/fw/services/common/analytics/analytics.h
Normal file
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* 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 "kernel/pebble_tasks.h"
|
||||
#include "util/uuid.h"
|
||||
#include "analytics_metric_table.h"
|
||||
#include "analytics_event.h"
|
||||
|
||||
|
||||
#define ANALYTICS_LOG_DEBUG(fmt, args...) \
|
||||
PBL_LOG_D(LOG_DOMAIN_ANALYTICS, LOG_LEVEL_DEBUG, fmt, ## args)
|
||||
|
||||
//! Possible values for the client argument when setting/updating a metric. This tells the
|
||||
//! analytics code under which "blob" to put the metric. For device metrics, the client argument
|
||||
//! is ignored, but passing in AnalyticsClient_System is basically good documentation.
|
||||
//! For app metrics, the client can be AnalyticsClient_App, AnalyticsClient_Worker or
|
||||
//! AnalyticsClient_CurrentTask
|
||||
typedef enum AnalyticsClient {
|
||||
AnalyticsClient_System, //! Put in the "device" blob. Illegal if the metric is an app metric.
|
||||
AnalyticsClient_App, //! Put in the "app" blob with the UUID of the current foreground app
|
||||
AnalyticsClient_Worker, //! Put in the "app" blob with the UUID of the current background
|
||||
//! worker
|
||||
AnalyticsClient_CurrentTask, //! Put in the "app" blob with the UUID of the current task (either
|
||||
//! app or worker)
|
||||
AnalyticsClient_Ignore, //! For internal use by the analytics module only
|
||||
} AnalyticsClient;
|
||||
|
||||
|
||||
void analytics_init(void);
|
||||
|
||||
//! Set a scalar metric
|
||||
//! @param metric The metric to set
|
||||
//! @param val The new value
|
||||
//! @param client If the metric is an app metric, this logs it to the app blob using the UUID of the given client.
|
||||
//! If the metric is a device metric, client must be AnalyticsClient_System
|
||||
void analytics_set(AnalyticsMetric metric, int64_t val, AnalyticsClient client);
|
||||
|
||||
//! Keeps val if it's larger than the previous measurement
|
||||
//! @param metric The metric to set
|
||||
//! @param val The value of the new measurement
|
||||
//! @param client If the metric is an app metric, this logs it to the app blob using the UUID of the given client.
|
||||
//! If the metric is a device metric, client must be AnalyticsClient_System
|
||||
void analytics_max(AnalyticsMetric metric, int64_t val, AnalyticsClient client);
|
||||
|
||||
//! Set a scalar metric for an app blob by UUID
|
||||
//! @param metric The metric to set. This should be an app metric
|
||||
//! @param val The new value
|
||||
//! @param uuid The uuid of the app blob
|
||||
void analytics_set_for_uuid(AnalyticsMetric metric, int64_t val, const Uuid *uuid);
|
||||
|
||||
//! Set an array metric
|
||||
//! @param metric The metric to set
|
||||
//! @param data The new data array
|
||||
//! @param client If the metric is an app metric, this logs it to the app blob using the UUID of the given client.
|
||||
//! If the metric is a device metric, client must be AnalyticsClient_System
|
||||
// TODO: Remove this, and add analytics_append_array or something. See PBL-5333
|
||||
void analytics_set_entire_array(AnalyticsMetric metric, const void *data, AnalyticsClient client);
|
||||
|
||||
//! Increment a metric by 1
|
||||
//! @param metric The metric to increment
|
||||
//! @param client If the metric is an app metric, this logs it to the app blob using the UUID of the given client.
|
||||
//! If the metric is a device metric, client must be AnalyticsClient_System
|
||||
void analytics_inc(AnalyticsMetric metric, AnalyticsClient client);
|
||||
|
||||
//! Increment an app metric for an app with the given UUID by 1
|
||||
//! @param metric The metric to increment. This should be an app metric
|
||||
//! @param uuid The uuid of the app blob
|
||||
void analytics_inc_for_uuid(AnalyticsMetric metric, const Uuid *uuid);
|
||||
|
||||
//! Increment a metric
|
||||
//! @param metric The metric to increment
|
||||
//! @param amount The amount to increment by
|
||||
//! @param client If the metric is an app metric, this logs it to the app blob using the UUID of the given client.
|
||||
//! If the metric is a device metric, client must be AnalyticsClient_System
|
||||
void analytics_add(AnalyticsMetric metric, int64_t amount, AnalyticsClient client);
|
||||
|
||||
//! Increment an app metric for an app with the given UUID
|
||||
//! @param metric The metric to increment. This should be an app metric
|
||||
//! @param amount The amount to increment by
|
||||
//! @param uuid The uuid of the app blob
|
||||
void analytics_add_for_uuid(AnalyticsMetric metric, int64_t amount, const Uuid *uuid);
|
||||
|
||||
//! Starts a stopwatch that integrates a "rate of things" over time.
|
||||
//! @param metric The metric of the stopwatch to start
|
||||
//! @param client If the metric is an app metric, this logs it to the app blob using the UUID of the given client.
|
||||
//! If the metric is a device metric, client must be AnalyticsClient_System
|
||||
void analytics_stopwatch_start(AnalyticsMetric metric, AnalyticsClient client);
|
||||
|
||||
//! Starts a stopwatch that integrates a "rate of things" over time.
|
||||
//! @param metric The metric for which to start the stopwatch.
|
||||
//! @param count_per_second The rate in number of things per second to count.
|
||||
//! @param client If the metric is an app metric, this logs it to the app blob using the UUID of the given client.
|
||||
//! If the metric is a device metric, client must be AnalyticsClient_System
|
||||
//! For example, if you want to measure "bytes transferred" over time and know the transfer speed is 1024 bytes per
|
||||
//! second, then you would pass in 1024 as count_per_second.
|
||||
void analytics_stopwatch_start_at_rate(AnalyticsMetric metric, uint32_t count_per_second, AnalyticsClient client);
|
||||
|
||||
//! Stops a stopwatch
|
||||
//! @param metric The metric of the stopwatch
|
||||
void analytics_stopwatch_stop(AnalyticsMetric metric);
|
518
src/fw/services/common/analytics/analytics_event.h
Normal file
518
src/fw/services/common/analytics/analytics_event.h
Normal file
|
@ -0,0 +1,518 @@
|
|||
/*
|
||||
* 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/pebble_process_info.h"
|
||||
#include "services/common/comm_session/session_analytics.h"
|
||||
#include "services/normal/activity/activity_insights.h"
|
||||
#include "services/normal/timeline/item.h"
|
||||
#include "util/attributes.h"
|
||||
#include "util/build_id.h"
|
||||
#include "util/time/time.h"
|
||||
#include "util/uuid.h"
|
||||
|
||||
#if !PLATFORM_TINTIN
|
||||
#include "services/normal/vibes/vibe_score_info.h"
|
||||
#endif
|
||||
|
||||
// Every analytics blob we send out (device blob, app blob, or event blob) starts out with
|
||||
// an 8-bit AnalyticsBlobKind followed by a 16-bit version. Here we define the format of the
|
||||
// event blob. The device and app blobs are defined in analytics_metric_table.h.
|
||||
// The ANALYTICS_EVENT_BLOB_VERSION value defined here will need to bumped whenever the format of
|
||||
// the AnalyticsEventBlob structure changes - this includes if ANY of the unions inside of it
|
||||
// change or a new AnalyticsEvent enum is added.
|
||||
// Please do not cherrypick any change here into a release branch without first checking
|
||||
// with Katharine, or something is very likely to break.
|
||||
#define ANALYTICS_EVENT_BLOB_VERSION 32
|
||||
|
||||
|
||||
//! Types of events that can be logged outside of a heartbeat using analytics_logging_log_event()
|
||||
typedef enum {
|
||||
AnalyticsEvent_AppLaunch,
|
||||
AnalyticsEvent_PinOpen,
|
||||
AnalyticsEvent_PinAction,
|
||||
AnalyticsEvent_CannedReponseSent,
|
||||
AnalyticsEvent_CannedReponseFailed,
|
||||
AnalyticsEvent_VoiceTranscriptionAccepted,
|
||||
AnalyticsEvent_VoiceTranscriptionRejected,
|
||||
AnalyticsEvent_PinAppLaunch,
|
||||
AnalyticsEvent_BtClassicDisconnect,
|
||||
AnalyticsEvent_BtLeDisconnect,
|
||||
AnalyticsEvent_Crash,
|
||||
AnalyticsEvent_LocalBtDisconnect,
|
||||
AnalyticsEvent_BtLockupError,
|
||||
AnalyticsEvent_BtClassicConnectionComplete,
|
||||
AnalyticsEvent_BtLeConnectionComplete,
|
||||
AnalyticsEvent_PinCreated,
|
||||
AnalyticsEvent_PinUpdated,
|
||||
AnalyticsEvent_BtLeAMS,
|
||||
AnalyticsEvent_VoiceTranscriptionAutomaticallyAccepted,
|
||||
AnalyticsEvent_StationaryModeSwitch,
|
||||
AnalyticsEvent_HealthLegacySleep,
|
||||
AnalyticsEvent_HealthLegacyActivity,
|
||||
AnalyticsEvent_PutByteTime,
|
||||
AnalyticsEvent_HealthInsightCreated,
|
||||
AnalyticsEvent_HealthInsightResponse,
|
||||
AnalyticsEvent_AppCrash,
|
||||
AnalyticsEvent_VibeAccess,
|
||||
AnalyticsEvent_HealthActivitySession, // Deprecated
|
||||
AnalyticsEvent_BtAppLaunchError,
|
||||
AnalyticsEvent_BtLePairingError,
|
||||
AnalyticsEvent_BtClassicPairingError,
|
||||
AnalyticsEvent_PebbleProtocolSystemSessionEnd,
|
||||
AnalyticsEvent_PebbleProtocolAppSessionEnd,
|
||||
AnalyticsEvent_AlarmCreated,
|
||||
AnalyticsEvent_AlarmTriggered,
|
||||
AnalyticsEvent_AlarmDismissed,
|
||||
AnalyticsEvent_PPoGATTDisconnect,
|
||||
AnalyticsEvent_BtChipBoot,
|
||||
AnalyticsEvent_GetBytesStats,
|
||||
AnalyticsEvent_RockyAppCrash,
|
||||
AnalyticsEvent_AppOOMNative,
|
||||
AnalyticsEvent_AppOOMRocky,
|
||||
AnalyticsEvent_BtLeMicError,
|
||||
AnalyticsEvent_BleHrmEvent,
|
||||
} AnalyticsEvent;
|
||||
|
||||
// AnalyticsEvent_BleHrmEvent
|
||||
typedef enum {
|
||||
BleHrmEventSubtype_SharingAccepted,
|
||||
BleHrmEventSubtype_SharingDeclined,
|
||||
BleHrmEventSubtype_SharingRevoked,
|
||||
BleHrmEventSubtype_SharingTimeoutPopupPresented,
|
||||
} BleHrmEventSubtype;
|
||||
|
||||
typedef struct PACKED {
|
||||
BleHrmEventSubtype subtype:8;
|
||||
} AnalyticsEventBleHrmEvent;
|
||||
|
||||
// AnalyticsEvent_AppLaunch event
|
||||
typedef struct PACKED {
|
||||
Uuid uuid;
|
||||
} AnalyticsEventAppLaunch;
|
||||
|
||||
// AnalyticsEvent_PinOpen/Create/Update events
|
||||
typedef struct PACKED {
|
||||
uint32_t time_utc; // pin utc time
|
||||
Uuid parent_id; // owner app UUID
|
||||
} AnalyticsEventPinOpenCreateUpdate;
|
||||
|
||||
// AnalyticsEvent_PinAction events
|
||||
typedef struct PACKED {
|
||||
uint32_t time_utc; // pin utc time
|
||||
Uuid parent_id; // owner app UUID
|
||||
uint8_t type; // action type
|
||||
} AnalyticsEventPinAction;
|
||||
|
||||
// AnalyticsEvent_PinAppLaunch event
|
||||
typedef struct PACKED {
|
||||
uint32_t time_utc; // pins utc time
|
||||
Uuid parent_id; // owner app UUID
|
||||
} AnalyticsEventPinAppLaunch;
|
||||
|
||||
// AnalyticsEvent_PinCreated events
|
||||
typedef struct PACKED {
|
||||
uint32_t time_utc; // pin utc time
|
||||
Uuid parent_id; // owner app UUID
|
||||
} AnalyticsEventPinCreated;
|
||||
|
||||
// AnalyticsEvent_PinUpdated events
|
||||
typedef struct PACKED {
|
||||
uint32_t time_utc; // pin utc time
|
||||
Uuid parent_id; // owner app UUID
|
||||
} AnalyticsEventPinUpdated;
|
||||
|
||||
// AnalyticsEvent_CannedResponse.* events.
|
||||
typedef struct PACKED {
|
||||
uint8_t response_size_bytes;
|
||||
} AnalyticsEventCannedResponse;
|
||||
|
||||
// AnalyticsEvent_VoiceResponse.* events.
|
||||
typedef struct PACKED {
|
||||
uint8_t num_sessions;
|
||||
uint8_t error_count;
|
||||
uint16_t response_size_bytes;
|
||||
uint16_t response_len_chars;
|
||||
uint32_t response_len_ms;
|
||||
Uuid app_uuid;
|
||||
} AnalyticsEventVoiceResponse;
|
||||
|
||||
typedef struct PACKED {
|
||||
uint8_t reason; // the connection status / reason we disconnected
|
||||
} AnalyticsEventBtConnectionDisconnection;
|
||||
|
||||
typedef struct PACKED {
|
||||
uint8_t reason; // The reason we disconnected
|
||||
uint8_t remote_bt_version;
|
||||
uint16_t remote_bt_company_id;
|
||||
uint16_t remote_bt_subversion_number;
|
||||
uint16_t remote_features_supported; // placeholder for supported features
|
||||
} AnalyticsEventBleDisconnection;
|
||||
|
||||
typedef struct CommSession CommSession;
|
||||
|
||||
typedef struct PACKED {
|
||||
CommSessionCloseReason close_reason:8;
|
||||
uint16_t duration_minutes;
|
||||
} AnalyticsEventPebbleProtocolCommonSessionClose;
|
||||
|
||||
typedef struct PACKED {
|
||||
AnalyticsEventPebbleProtocolCommonSessionClose common;
|
||||
} AnalyticsEventPebbleProtocolSystemSessionClose;
|
||||
|
||||
typedef struct PACKED {
|
||||
AnalyticsEventPebbleProtocolCommonSessionClose common;
|
||||
Uuid app_uuid;
|
||||
} AnalyticsEventPebbleProtocolAppSessionClose;
|
||||
|
||||
typedef struct PACKED {
|
||||
uint32_t error_code;
|
||||
} AnalyticsEventBtError;
|
||||
|
||||
typedef struct PACKED {
|
||||
uint8_t crash_code;
|
||||
uint32_t link_register;
|
||||
} AnalyticsEventCrash;
|
||||
|
||||
typedef struct PACKED {
|
||||
uint32_t lr;
|
||||
uint16_t conn_handle;
|
||||
} AnalyticsEvent_LocalBTDisconnect;
|
||||
|
||||
typedef struct PACKED {
|
||||
uint8_t type;
|
||||
int32_t aux_info;
|
||||
} AnalyticsEvent_AMSData;
|
||||
|
||||
typedef struct PACKED {
|
||||
time_t timestamp;
|
||||
uint8_t state_change;
|
||||
} AnalyticsEvent_StationaryStateChangeData;
|
||||
|
||||
typedef struct PACKED {
|
||||
uint16_t start_minute; // minute of day when sleep started (midnight is minute 0)
|
||||
uint16_t wake_minute; // minute of day when sleep ended
|
||||
uint16_t total_minutes; // total minutes of sleep
|
||||
uint16_t deep_minutes; // deep minutes of sleep
|
||||
} AnalyticsEvent_HealthLegacySleepData;
|
||||
|
||||
typedef struct PACKED {
|
||||
uint16_t duration_minutes; // duration in minutes
|
||||
uint16_t steps; // # of steps
|
||||
} AnalyticsEvent_HealthLegacyActivityData;
|
||||
|
||||
typedef struct PACKED {
|
||||
uint8_t insight_type; // numerical id of insight
|
||||
uint8_t activity_type; // activity type, one of ActivitySessionType
|
||||
uint8_t response_id; // numerical id of response
|
||||
uint32_t time_utc; // insight utc time, activity start UTC if activity type is not none
|
||||
} AnalyticsEvent_HealthInsightResponseData;
|
||||
|
||||
typedef struct PACKED {
|
||||
uint8_t insight_type; // numerical id of insight
|
||||
uint32_t time_utc; // insight utc time
|
||||
uint8_t percent_tier; // above average / below average
|
||||
} AnalyticsEvent_HealthInsightCreatedData;
|
||||
|
||||
typedef struct PACKED {
|
||||
bool ppogatt; // true if transport is PPOGATT, else SPP
|
||||
uint8_t conn_intvl_1_25ms; // if ppogatt, the connection interval at end of FW update
|
||||
bool crc_good; // true if calculated CRC matches expected CRC
|
||||
uint8_t type; // see PutBytesObjectType
|
||||
uint32_t bytes_transferred;
|
||||
uint32_t elapsed_time_ms;
|
||||
uint32_t conn_events;
|
||||
uint16_t sync_errors;
|
||||
uint16_t skip_errors;
|
||||
uint16_t other_errors;
|
||||
} AnalyticsEvent_PutByteTimeData;
|
||||
|
||||
//! Used for both AnalyticsEvent_AppCrash and AnalyticsEvent_RockyAppCrash event types!
|
||||
typedef struct PACKED {
|
||||
Uuid uuid;
|
||||
uint32_t pc;
|
||||
uint32_t lr;
|
||||
uint8_t build_id_slice[4];
|
||||
} AnalyticsEvent_AppCrashData;
|
||||
|
||||
typedef enum VibePatternFeature {
|
||||
VibePatternFeature_Notifications = 1 << 0,
|
||||
VibePatternFeature_PhoneCalls = 1 << 1,
|
||||
VibePatternFeature_Alarms = 1 << 2,
|
||||
} VibePatternFeature;
|
||||
|
||||
typedef struct PACKED {
|
||||
uint8_t feature;
|
||||
uint8_t vibe_pattern_id;
|
||||
} AnalyticsEvent_VibeAcessData;
|
||||
|
||||
typedef struct PACKED {
|
||||
uint16_t activity_type; // activity type, one of ActivitySessionType
|
||||
uint32_t start_utc; // start time of activity, in UTC seconds
|
||||
uint32_t elapsed_sec; // length of activity in seconds.
|
||||
} AnalyticsEvent_HealthActivitySessionData; // Deprecated
|
||||
|
||||
typedef struct PACKED {
|
||||
uint8_t hour;
|
||||
uint8_t minute;
|
||||
bool is_smart;
|
||||
uint8_t kind;
|
||||
uint8_t scheduled_days[DAYS_PER_WEEK];
|
||||
} AnalyticsEvent_AlarmData;
|
||||
|
||||
typedef struct PACKED AnalyticsEvent_BtChipBootData {
|
||||
uint8_t build_id[BUILD_ID_EXPECTED_LEN];
|
||||
uint32_t crash_lr;
|
||||
uint32_t reboot_reason;
|
||||
} AnalyticsEvent_BtChipBootData;
|
||||
|
||||
typedef struct PACKED AnalyticsEvent_PPoGATTDisconnectData {
|
||||
bool successful_reconnect;
|
||||
uint32_t time_utc; // utc time
|
||||
} AnalyticsEvent_PPoGATTDisconnectData;
|
||||
|
||||
typedef struct PACKED AnalyticsEvent_GetBytesStatsData {
|
||||
bool ppogatt; // true if transport is PPOGATT, else SPP
|
||||
uint8_t conn_intvl_1_25ms; // if ppogatt, the connection interval at end of FW update
|
||||
uint8_t type; // see GetBytesObjectType
|
||||
uint32_t bytes_transferred;
|
||||
uint32_t elapsed_time_ms;
|
||||
uint32_t conn_events;
|
||||
uint16_t sync_errors;
|
||||
uint16_t skip_errors;
|
||||
uint16_t other_errors;
|
||||
} AnalyticsEvent_GetBytesStatsData;
|
||||
|
||||
typedef struct PACKED AnalyticsEvent_AppOomData {
|
||||
Uuid app_uuid;
|
||||
uint32_t requested_size;
|
||||
uint32_t total_size;
|
||||
uint16_t total_free;
|
||||
uint16_t largest_free_block;
|
||||
} AnalyticsEvent_AppOomData;
|
||||
|
||||
typedef struct PACKED {
|
||||
uint8_t kind; // set to ANALYTICS_BLOB_KIND_EVENT
|
||||
uint16_t version; // set to ANALYTICS_EVENT_BLOB_VERSION
|
||||
AnalyticsEvent event:8; // type of event
|
||||
uint32_t timestamp;
|
||||
union PACKED {
|
||||
AnalyticsEventBtError bt_error;
|
||||
AnalyticsEventAppLaunch app_launch;
|
||||
AnalyticsEventPinOpenCreateUpdate pin_open_create_update;
|
||||
AnalyticsEventPinAction pin_action;
|
||||
AnalyticsEventPinAppLaunch pin_app_launch;
|
||||
AnalyticsEventCannedResponse canned_response;
|
||||
AnalyticsEventVoiceResponse voice_response;
|
||||
AnalyticsEventBtConnectionDisconnection bt_connection_disconnection;
|
||||
AnalyticsEventBleDisconnection ble_disconnection;
|
||||
AnalyticsEventCrash crash_report;
|
||||
AnalyticsEvent_LocalBTDisconnect local_bt_disconnect;
|
||||
AnalyticsEvent_AMSData ams;
|
||||
AnalyticsEvent_StationaryStateChangeData sd;
|
||||
AnalyticsEvent_HealthLegacySleepData health_sleep;
|
||||
AnalyticsEvent_HealthLegacyActivityData health_activity;
|
||||
AnalyticsEvent_PutByteTimeData pb_time;
|
||||
AnalyticsEvent_HealthInsightCreatedData health_insight_created;
|
||||
AnalyticsEvent_HealthInsightResponseData health_insight_response;
|
||||
AnalyticsEvent_AppCrashData app_crash_report;
|
||||
AnalyticsEvent_VibeAcessData vibe_access_data;
|
||||
AnalyticsEvent_HealthActivitySessionData health_activity_session;
|
||||
AnalyticsEventPebbleProtocolCommonSessionClose pp_common_session_close;
|
||||
AnalyticsEventPebbleProtocolSystemSessionClose pp_system_session_close;
|
||||
AnalyticsEventPebbleProtocolAppSessionClose pp_app_session_close;
|
||||
AnalyticsEvent_AlarmData alarm;
|
||||
AnalyticsEvent_BtChipBootData bt_chip_boot;
|
||||
AnalyticsEvent_PPoGATTDisconnectData ppogatt_disconnect;
|
||||
AnalyticsEvent_GetBytesStatsData get_bytes_stats;
|
||||
AnalyticsEvent_AppOomData app_oom;
|
||||
AnalyticsEventBleHrmEvent ble_hrm;
|
||||
};
|
||||
} AnalyticsEventBlob;
|
||||
|
||||
//! @param type AnalyticsEvent_AppOOMNative or AnalyticsEvent_AppOOMRocky
|
||||
//! @param total_free Sum of free bytes
|
||||
//! @param largest_free_block The largest, contiguous, free block of memory.
|
||||
//! @note Intended to be called from the app/worker task (calls sys_analytics_logging_log_event).
|
||||
void analytics_event_app_oom(AnalyticsEvent type,
|
||||
uint32_t requested_size, uint32_t total_size,
|
||||
uint32_t total_free, uint32_t largest_free_block);
|
||||
|
||||
//! Log an app launch event to analytics
|
||||
//! @param uuid app's UUID
|
||||
void analytics_event_app_launch(const Uuid *uuid);
|
||||
|
||||
//! Log a pin open event to analytics
|
||||
//! @param timestamp the UTC timestamp of the pin
|
||||
//! @param parent_id UUID of the owner of the pin
|
||||
void analytics_event_pin_open(time_t timestamp, const Uuid *parent_id);
|
||||
|
||||
//! Log a generic pin action event (i.e. not an app launch) to analytics
|
||||
//! @param timestamp the UTC timestamp of the pin
|
||||
//! @param parent_id UUID of the owner of the pin
|
||||
//! @param action_title the title of the action
|
||||
void analytics_event_pin_action(time_t timestamp, const Uuid *parent_id,
|
||||
TimelineItemActionType action_type);
|
||||
|
||||
//! Log a pin launch app event to analytics
|
||||
//! @param timestamp the UTC timestamp of the pin
|
||||
//! @param parent_id UUID of the owner of the pin
|
||||
void analytics_event_pin_app_launch(time_t timestamp, const Uuid *parent_id);
|
||||
|
||||
//! Log a pin created event to analytics
|
||||
//! @param timestamp the UTC timestamp of the pin
|
||||
//! @param parent_id UUID of the owner of the pin
|
||||
void analytics_event_pin_created(time_t timestamp, const Uuid *parent_id);
|
||||
|
||||
//! Log a pin updated event to analytics
|
||||
//! @param timestamp the UTC timestamp of the pin
|
||||
//! @param parent_id UUID of the owner of the pin
|
||||
void analytics_event_pin_updated(time_t timestamp, const Uuid *parent_id);
|
||||
|
||||
//! Log a canned response event
|
||||
//! @param response pointer to response text
|
||||
//! @param successfully_sent true if successfully sent, false if a failure occurred
|
||||
void analytics_event_canned_response(const char *response, bool successfully_sent);
|
||||
|
||||
//! Log voice transcription event
|
||||
//! @param event_type event type - must be one of \ref AnalyticsEvent_VoiceTranscriptionAccepted,
|
||||
//! \ref AnalyticsEvent_VoiceTranscriptionRejected, or
|
||||
//! \ref AnalyticsEvent_VoiceTranscriptionAutomaticallyAccepted
|
||||
//! @param response_size_bytes accepted response size in number of bytes
|
||||
//! @param response_len_chars accepted response length in number of unicode characters
|
||||
//! @param response_len_ms accepted response time in ms
|
||||
//! @param error_count number of errors that occurred
|
||||
//! @param num_sessions number of transcription sessions initiated to get the accepted user response
|
||||
//! or before the user exited the UI
|
||||
//! @param app_uuid pointer to app or system UUID
|
||||
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);
|
||||
|
||||
//! Log BLE HRM event
|
||||
void analytics_event_ble_hrm(BleHrmEventSubtype subtype);
|
||||
|
||||
//! Log bluetooth disconnection event
|
||||
//! @param type - AnalyticsEvent_BtLeConnectionComplete, AnalyticsEvent_BtClassicDisconnect, etc
|
||||
//! @param reason - The HCI Error code representing the disconnect reason. (See
|
||||
//! "OVERVIEW OF ERROR CODES" in BT Core Specification or the HCI_ERROR_CODEs in HCITypes.h
|
||||
void analytics_event_bt_connection_or_disconnection(AnalyticsEvent type, uint8_t reason);
|
||||
|
||||
//! Log bluetooth le disconnection event
|
||||
//! @param reason - The HCI Error code associated with the disconnect
|
||||
//! remote_bt_version, remote_bt_company_id & remote_bt_subversion come from the version
|
||||
//! response received from the remote device
|
||||
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);
|
||||
|
||||
//! Log bluetooth error
|
||||
void analytics_event_bt_error(AnalyticsEvent type, uint32_t error);
|
||||
|
||||
//! Log when the CC2564x BT chip becomes unresponsive
|
||||
void analytics_event_bt_cc2564x_lockup_error(void);
|
||||
|
||||
//! Log when app_launch trigger failed.
|
||||
void analytics_event_bt_app_launch_error(uint8_t 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);
|
||||
|
||||
//! Log crash event to analytics
|
||||
//! @param crash_code Reboot reason (see RebootReasonCode in reboot_reason.h)
|
||||
//! @param link_register Last running function before crash
|
||||
void analytics_event_crash(uint8_t crash_code, uint32_t link_register);
|
||||
|
||||
//! Log the reason we disconnect locally as an event. (Mainly interested in
|
||||
//! seeing how often Bluetopia kills us due to unexpected L2CAP errors)
|
||||
//! @param handle - the handle being disconnected
|
||||
//! @param lr - the LR of the function which invoked the disconnect
|
||||
void analytics_event_local_bt_disconnect(uint16_t conn_handle, uint32_t lr);
|
||||
|
||||
//! Log an Apple Media Service event.
|
||||
//! @param type See AMSAnalyticsEvent
|
||||
//! @param aux_info Additional information specific to the type of event
|
||||
void analytics_event_ams(uint8_t type, int32_t aux_info);
|
||||
|
||||
void analytics_event_stationary_state_change(time_t timestamp, uint8_t state_change_reason);
|
||||
|
||||
//! Log a health insight created event
|
||||
//! @param timestamp the UTC timestamp of the insight
|
||||
//! @param insight_type numerical id (from \ref ActivityInsightType enum) of the insight
|
||||
//! @param pct_tier numerical id (from \ref PercentageTier enum) of the percent tier from
|
||||
//! average for the metric the insight is about.
|
||||
void analytics_event_health_insight_created(time_t timestamp,
|
||||
ActivityInsightType insight_type,
|
||||
PercentTier pct_tier);
|
||||
|
||||
//! Log a health insight response event
|
||||
//! @param timestamp the UTC timestamp of the insight
|
||||
//! @param insight_type numerical id (from \ref ActivityInsightType enum) of the insight
|
||||
//! @param activity_type type of activity, one of ActivitySessionType
|
||||
//! @param response_id numerical id (from \ref ActivityInsightResponseType enum) of response
|
||||
void analytics_event_health_insight_response(time_t timestamp, ActivityInsightType insight_type,
|
||||
ActivitySessionType activity_type,
|
||||
ActivityInsightResponseType response_id);
|
||||
|
||||
//! Tracks duration of time it takes to recieve byte transfers over putbytes
|
||||
//! and statistics on the type of transfer and whether the data stored was valid
|
||||
//! @param session the session used to transfer the data
|
||||
//! @param crc_good whether or not the CRC for the blob transferred is valid
|
||||
//! @param type the PutBytesObjectType that was transferred
|
||||
//! @param bytes_transferred the number of bytes transferred
|
||||
//! @param elapsed_time_ms the amount of time spent transmitting the bytes
|
||||
//! @param conn_events, sync_errors, other_errors LE connection event statistics (if available)
|
||||
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);
|
||||
|
||||
//! Log an App Crash event to analytics
|
||||
//! @param uuid app's UUID
|
||||
//! @param pc Current running function before crash
|
||||
//! @param lr Last running function before crash
|
||||
//! @param build_id Pointer to the build_id buffer of the application (can be NULL)
|
||||
void analytics_event_app_crash(const Uuid *uuid, uint32_t pc, uint32_t lr, const uint8_t *build_id,
|
||||
bool is_rocky_app);
|
||||
|
||||
#if !PLATFORM_TINTIN
|
||||
//! Log the user's vibration pattern
|
||||
//! @param VibePatternFeature Notifications, Phone Calls, or Alarms
|
||||
void analytics_event_vibe_access(VibePatternFeature vibe_feature, VibeScoreId pattern_id);
|
||||
#endif
|
||||
|
||||
typedef struct AlarmInfo AlarmInfo;
|
||||
|
||||
//! Sends an analytic event about an alarm event
|
||||
//! @param even_type The type of alarm analytic event that occurred
|
||||
//! @param info Information about the alarm
|
||||
void analytics_event_alarm(AnalyticsEvent event_type, const AlarmInfo *info);
|
||||
|
||||
//! Sends an analytic event about an alarm event
|
||||
//! @param even_type The type of alarm analytic event that occurred
|
||||
//! @param info Information about the alarm
|
||||
void analytics_event_bt_chip_boot(uint8_t build_id[BUILD_ID_EXPECTED_LEN],
|
||||
uint32_t crash_lr, uint32_t reboot_reason_code);
|
||||
|
||||
//! Log forced PPoGATT disconnection caused by too many resets
|
||||
void analytics_event_PPoGATT_disconnect(time_t timestamp, bool successful_reconnect);
|
||||
|
||||
//! Log out to analytics stats about a GetBytes transfer
|
||||
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);
|
50
src/fw/services/common/analytics/analytics_external.h
Normal file
50
src/fw/services/common/analytics/analytics_external.h
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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 "drivers/rtc.h"
|
||||
|
||||
//! Request other modules to update their analytics fields
|
||||
void analytics_external_update(void);
|
||||
|
||||
extern void analytics_external_collect_battery(void);
|
||||
extern void analytics_external_collect_accel_xyz_delta(void);
|
||||
extern void analytics_external_collect_app_cpu_stats(void);
|
||||
extern void analytics_external_collect_app_flash_read_stats(void);
|
||||
extern void analytics_external_collect_cpu_stats(void);
|
||||
extern void analytics_external_collect_stop_inhibitor_stats(RtcTicks now_ticks);
|
||||
extern void analytics_external_collect_bt_parameters(void);
|
||||
extern void analytics_external_collect_bt_pairing_info(void);
|
||||
extern void analytics_external_collect_ble_parameters(void);
|
||||
extern void analytics_external_collect_ble_pairing_info(void);
|
||||
extern void analytics_external_collect_backlight_settings(void);
|
||||
extern void analytics_external_collect_notification_settings(void);
|
||||
extern void analytics_external_collect_system_theme_settings(void);
|
||||
extern void analytics_external_collect_ancs_info(void);
|
||||
extern void analytics_external_collect_dls_stats(void);
|
||||
extern void analytics_external_collect_i2c_stats(void);
|
||||
extern void analytics_external_collect_system_flash_statistics(void);
|
||||
extern void analytics_external_collect_stack_free(void);
|
||||
extern void analytics_external_collect_alerts_preferences(void);
|
||||
extern void analytics_external_collect_timeline_pin_stats(void);
|
||||
extern void analytics_external_collect_display_offset(void);
|
||||
extern void analytics_external_collect_pfs_stats(void);
|
||||
extern void analytics_external_collect_chip_specific_parameters(void);
|
||||
extern void analytics_external_collect_bt_chip_heartbeat(void);
|
||||
extern void analytics_external_collect_kernel_heap_stats(void);
|
||||
extern void analytics_external_collect_accel_samples_received(void);
|
63
src/fw/services/common/analytics/analytics_heartbeat.h
Normal file
63
src/fw/services/common/analytics/analytics_heartbeat.h
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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 <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include "util/uuid.h"
|
||||
|
||||
#include "analytics_metric_table.h"
|
||||
|
||||
typedef enum {
|
||||
ANALYTICS_HEARTBEAT_KIND_DEVICE = 0,
|
||||
ANALYTICS_HEARTBEAT_KIND_APP = 1,
|
||||
} AnalyticsHeartbeatKind;
|
||||
|
||||
typedef struct {
|
||||
// Note that the first byte of data[] is also the kind of the heartbeat.
|
||||
// We could merge these into one, but I'm not sure what kind of code gcc
|
||||
// will generate when so many fields are unaligned, and I don't really want
|
||||
// to risk the codesize.
|
||||
AnalyticsHeartbeatKind kind;
|
||||
uint8_t data[0];
|
||||
} AnalyticsHeartbeat;
|
||||
|
||||
uint32_t analytics_heartbeat_kind_data_size(AnalyticsHeartbeatKind kind);
|
||||
|
||||
void analytics_heartbeat_set(AnalyticsHeartbeat *heartbeat, AnalyticsMetric metric, int64_t val);
|
||||
void analytics_heartbeat_set_array(AnalyticsHeartbeat *heartbeat, AnalyticsMetric metric, uint32_t index, int64_t val);
|
||||
void analytics_heartbeat_set_entire_array(AnalyticsHeartbeat *heartbeat, AnalyticsMetric metric, const void* data);
|
||||
|
||||
int64_t analytics_heartbeat_get(AnalyticsHeartbeat *heartbeat, AnalyticsMetric metric);
|
||||
int64_t analytics_heartbeat_get_array(AnalyticsHeartbeat *heartbeat, AnalyticsMetric metric, uint32_t index);
|
||||
const Uuid *analytics_heartbeat_get_uuid(AnalyticsHeartbeat *heartbeat);
|
||||
|
||||
AnalyticsHeartbeat *analytics_heartbeat_device_create();
|
||||
AnalyticsHeartbeat *analytics_heartbeat_app_create(const Uuid *uuid);
|
||||
void analytics_heartbeat_clear(AnalyticsHeartbeat *heartbeat);
|
||||
|
||||
// Turning this on is helpful when debugging analytics subsystems. It changes the heartbeat
|
||||
// to run once every 10 seconds instead of once every hour and also prints out the value of
|
||||
// each metric. Also helpful is to change LOG_DOMAIN_ANALYTICS from 0 to 1 to enable extra
|
||||
// logging messages (found in core/system/logging.h).
|
||||
// Another useful debugging tip is that doing a long-select on any item in the launcher menu
|
||||
// will trigger data logging to do an immediate flush of logged data to the phone.
|
||||
// #define ANALYTICS_DEBUG
|
||||
|
||||
void analytics_heartbeat_print(AnalyticsHeartbeat *heartbeat);
|
31
src/fw/services/common/analytics/analytics_logging.h
Normal file
31
src/fw/services/common/analytics/analytics_logging.h
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 "analytics_event.h"
|
||||
|
||||
typedef enum {
|
||||
ANALYTICS_BLOB_KIND_DEVICE_HEARTBEAT,
|
||||
ANALYTICS_BLOB_KIND_APP_HEARTBEAT,
|
||||
ANALYTICS_BLOB_KIND_EVENT
|
||||
} AnalyticsBlobKind;
|
||||
|
||||
void analytics_logging_init(void);
|
||||
|
||||
//! Used internally to log raw analytics events
|
||||
void analytics_logging_log_event(AnalyticsEventBlob *event_blob);
|
||||
|
||||
//! Exposed for unit testing only
|
||||
void analytics_logging_system_task_cb(void *ignored);
|
54
src/fw/services/common/analytics/analytics_metric.h
Normal file
54
src/fw/services/common/analytics/analytics_metric.h
Normal file
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 <stdbool.h>
|
||||
|
||||
#include "analytics_metric_table.h"
|
||||
|
||||
typedef enum {
|
||||
ANALYTICS_METRIC_KIND_DEVICE,
|
||||
ANALYTICS_METRIC_KIND_APP,
|
||||
ANALYTICS_METRIC_KIND_MARKER,
|
||||
ANALYTICS_METRIC_KIND_UNKNOWN,
|
||||
} AnalyticsMetricKind;
|
||||
|
||||
typedef enum {
|
||||
ANALYTICS_METRIC_ELEMENT_TYPE_NIL,
|
||||
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,
|
||||
} AnalyticsMetricElementType;
|
||||
|
||||
void analytics_metric_init(void);
|
||||
|
||||
AnalyticsMetricElementType analytics_metric_element_type(AnalyticsMetric metric);
|
||||
|
||||
uint32_t analytics_metric_num_elements(AnalyticsMetric metric);
|
||||
uint32_t analytics_metric_element_size(AnalyticsMetric metric);
|
||||
uint32_t analytics_metric_size(AnalyticsMetric metric);
|
||||
|
||||
bool analytics_metric_is_array(AnalyticsMetric metric);
|
||||
bool analytics_metric_is_unsigned(AnalyticsMetric metric);
|
||||
|
||||
uint32_t analytics_metric_offset(AnalyticsMetric metric);
|
||||
|
||||
AnalyticsMetricKind analytics_metric_kind(AnalyticsMetric metric);
|
333
src/fw/services/common/analytics/analytics_metric_table.h
Normal file
333
src/fw/services/common/analytics/analytics_metric_table.h
Normal file
|
@ -0,0 +1,333 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
// Increment when adding fields or changing the types of fields & tell Katharine what
|
||||
// you changed so the new definition is in sync with the server
|
||||
// Please do not cherrypick any change here into a release branch without first checking
|
||||
// with Katharine, or something is very likely to break.
|
||||
|
||||
#define ANALYTICS_APP_HEARTBEAT_BLOB_VERSION 11
|
||||
#define ANALYTICS_DEVICE_HEARTBEAT_BLOB_VERSION 69
|
||||
|
||||
|
||||
// Note that every analytics blob we send out (device blob, app blob, or event blob) starts out with
|
||||
// an 8-bit AnalyticsBlobKind followed by a 16-bit version. This header defines the format of the
|
||||
// device and app blobs. The format of an event blob is defined in analytics_logging.c
|
||||
|
||||
// The device and application heartbeats. Defining them like this allows
|
||||
// us to also use one table for defining the enum, creating the enum<->type
|
||||
// table and to generate names for debugging (when enabled).
|
||||
// DO NOT MOVE ELEMENTS AROUND, THIS WILL CHANGE THE BINARY FORMAT!
|
||||
#define ANALYTICS_METRIC_TABLE(MARKER, DEVICE, APP, UINT8, UINT16, UINT32, INT8, INT16, INT32) \
|
||||
MARKER(ANALYTICS_METRIC_INVALID) \
|
||||
MARKER(ANALYTICS_METRIC_START) \
|
||||
MARKER(ANALYTICS_DEVICE_METRIC_START) \
|
||||
\
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BLOB_KIND, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BLOB_VERSION, UINT16) \
|
||||
\
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_TIMESTAMP, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_TIME_INTERVAL, UINT32) \
|
||||
\
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_DEVICE_UP_TIME, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_SYSTEM_CRASH_CODE, UINT32) \
|
||||
\
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BATTERY_VOLTAGE, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BATTERY_VOLTAGE_DELTA, INT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BATTERY_PERCENT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BATTERY_PERCENT_DELTA, INT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BATTERY_CHARGE_TIME, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BATTERY_PLUGGED_TIME, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BATTERY_SAMPLE_SKIP_COUNT_EXCEEDED, UINT8) \
|
||||
\
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_STATIONARY_TIME_MINUTES, UINT8) \
|
||||
\
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_SYSTEM_CRASH_LR, UINT32) \
|
||||
\
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_CPU_RUNNING_TIME, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_CPU_STOP_TIME, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_CPU_SLEEP_TIME, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_CPU_NOSTOP_MAIN_TIME, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_CPU_NOSTOP_BUTTON_TIME, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_CPU_NOSTOP_BLUETOOTH_TIME, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_CPU_NOSTOP_DISPLAY_TIME, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_CPU_NOSTOP_BACKLIGHT_TIME, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_CPU_NOSTOP_COMM_TIME, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_CPU_NOSTOP_FLASH_TIME, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_CPU_NOSTOP_I2C1_TIME, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_CPU_NOSTOP_ACCESSORY, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_CPU_NOSTOP_MIC, UINT32) \
|
||||
\
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_PHONE_APP_INFO_COUNT, UINT8) \
|
||||
\
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_PHONE_CALL_INCOMING_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_PHONE_CALL_OUTGOING_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_PHONE_CALL_ANSWER_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_PHONE_CALL_DECLINE_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_PHONE_CALL_POP_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_PHONE_CALL_START_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_PHONE_CALL_END_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_PHONE_CALL_TIME, UINT32) \
|
||||
\
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BT_AIRPLANE_MODE_QUICK_TOGGLE_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BT_PAIRING_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BT_PAIRING_RECORDS_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BT_PAIRING_FORGET_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BT_CONNECT_COUNT, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BT_CONNECT_TIME, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BT_ACTIVE_TIME, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BT_PRIVATE_BYTE_IN_COUNT, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BT_PRIVATE_BYTE_OUT_COUNT, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BT_PUBLIC_BYTE_IN_COUNT, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BT_PUBLIC_BYTE_OUT_COUNT, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BT_PEBBLE_SPP_APP_TIME, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BT_PEBBLE_PPOGATT_APP_TIME, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BT_PEBBLE_APP_LAUNCH_SUCCESS_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BT_SYSTEM_SESSION_OPEN_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BT_IOS_IBEACON_WAKEUP_TIMEOUT_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BT_SNIFF_INTERVAL, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BT_AFH_CHAN_USE_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BT_ROLE, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BT_MODE_CHANGE_COUNT, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BT_SNIFF_ENTER_REQ, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BT_SNIFF_EXIT_REQ, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BT_HCILL_CNT, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BT_NUM_SDP_REQ, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BT_UART_BYTES_IN, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BT_UART_BYTES_OUT, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BT_OFF_TIME, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BT_DISCONNECT_NOT_PAIRABLE_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BT_DISCONNECT_MFI_FAILURE_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BT_DISCONNECT_IAP_PACKET_FAILURE_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BT_DISCONNECT_IAP_WATCHDOG_FAILURE_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BT_ZERO_ACL_CREDITS_MAX_DURATION_TICKS, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BT_COMM_SESSION_SEND_DATA_FAIL_COUNT, UINT16) \
|
||||
\
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BLE_CONNECT_COUNT, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BLE_CONNECT_TIME, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BLE_CONNECT_ENCRYPTED_TIME, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BLE_CONNECT_NO_INTENT_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BLE_GATT_DROPPED_NOTIFICATIONS_COUNT, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BLE_GATT_STALLED_NOTIFICATIONS_COUNT, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BLE_GATT_UNHANDLED1_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BLE_GATT_UNHANDLED2_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BLE_GATT_UNHANDLED3_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BLE_ESTIMATED_BYTES_ADVERTISED_COUNT, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BLE_CHAN_USE_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BLE_CONN_PARAMS, UINT16, 2) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BLE_CONN_PARAM_UPDATE_FAILED_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BLE_CONN_EVENT_COUNT, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BLE_PAIRING_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BLE_PAIRING_RECORDS_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BLE_PAIRING_FORGET_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BLE_LINK_QUALITY_SUM, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BLE_RSSI_SUM, UINT16) \
|
||||
\
|
||||
DEVICE(ANALYTICS_DEVICE_BT_PERSISTENT_STORAGE_UPDATES, UINT8) \
|
||||
\
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_MFI_RESET_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_MFI_ON_TIME, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_I2C_MAX_TRANSFER_DURATION_TICKS, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_I2C_ERROR_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BUTTON_PRESSED_COUNT, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_ACCEL_SHAKE_COUNT, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_ACCEL_DOUBLE_TAP_COUNT, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_ACCEL_PEEK_COUNT, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_ACCEL_SAMPLE_COUNT, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_ACCEL_XYZ_DELTA, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_ACCEL_FIFO_OVERRUN_COUNT, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_ACCEL_RESET_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_ALARM_SOUNDED_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BACKLIGHT_ON_COUNT, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BACKLIGHT_ON_TIME, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_VIBRATOR_ON_COUNT, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_VIBRATOR_ON_TIME, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_FPGA_REPROGRAM_COUNT, UINT8) \
|
||||
\
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_NOTIFICATION_RECEIVED_COUNT, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_NOTIFICATION_RECEIVED_DND_COUNT, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_NOTIFICATION_DISMISSED_COUNT, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_NOTIFICATION_DISMISS_ALL_COUNT, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_NOTIFICATION_CLOSED_COUNT, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_NOTIFICATION_BYTE_IN_COUNT, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_NOTIFICATION_JUMBOJI_COUNT, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_NOTIFICATION_ANCS_COUNT, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_NOTIFICATION_ANCS_DS_COUNT, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_NOTIFICATION_ANCS_NS_COUNT, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_NOTIFICATION_ANCS_PARSE_ERROR_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_NOTIFICATION_ANCS_DISCOVERED_COUNT, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_NOTIFICATION_ANCS_DS_SUBSCRIBE_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_NOTIFICATION_ANCS_NS_SUBSCRIBE_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_NOTIFICATION_ANCS_DS_SUBSCRIBE_FAIL_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_NOTIFICATION_ANCS_NS_SUBSCRIBE_FAIL_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_NOTIFICATION_ANCS_CONNECT_TIME, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_NOTIFICATION_ANCS_NS_FLAGS_BITSET, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_NOTIFICATION_ANCS_SMS_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_NOTIFICATION_ANCS_GROUP_SMS_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_NOTIFICATION_ANCS_MUTED_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_NOTIFICATION_ANCS_FILTERED_BECAUSE_MUTED_COUNT, UINT8) \
|
||||
\
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_REMINDER_RECEIVED_COUNT, UINT16) \
|
||||
\
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_APP_ROCKY_LAUNCH_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_APP_ROCKY_CRASHED_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_APP_USER_LAUNCH_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_APP_CRASHED_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_APP_THROTTLED_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_APP_NOTIFIED_DISCONNECTED_COUNT, UINT8) \
|
||||
\
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_DATA_LOGGING_FLUSH_COUNT, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_DATA_LOGGING_REALLOC_COUNT, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_DATA_LOGGING_MAX_SPOOLED_BYTES, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_DATA_LOGGING_ENDPOINT_SENDS, UINT16) \
|
||||
\
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_PING_SENT_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_PONG_RECEIVED_COUNT, UINT8) \
|
||||
\
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_APP_QUICK_LAUNCH_COUNT, UINT8) \
|
||||
\
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_SETTING_BACKLIGHT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_SETTING_SHAKE_TO_LIGHT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_SETTING_BACKLIGHT_INTENSITY_PCT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_SETTING_BACKLIGHT_TIMEOUT_SEC, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_SETTING_VIBRATION_STRENGTH, UINT8) \
|
||||
\
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_ALERTS_MASK, UINT8) \
|
||||
\
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_ALERTS_DND_ACTIVE_TIME, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_ALERTS_DND_PREFS_BITMASK, UINT8) \
|
||||
\
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_SYSTEM_THEME_TEXT_STYLE, UINT8) \
|
||||
\
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_WATCH_ONLY_TIME, UINT32) \
|
||||
\
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_TIMELINE_PAST_LAUNCH_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_TIMELINE_FUTURE_LAUNCH_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_TIMELINE_PAST_NAVIGATION_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_TIMELINE_FUTURE_NAVIGATION_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_TIMELINE_PINS_VISIBLE_CALENDAR_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_TIMELINE_PINS_VISIBLE_OTHER_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_TIMELINE_PINS_HOURLY_CALENDAR_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_TIMELINE_PINS_HOURLY_OTHER_COUNT, UINT8) \
|
||||
\
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BLOB_DB_EVENT_COUNT, UINT16) \
|
||||
\
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_FLASH_READ_BYTES_COUNT, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_FLASH_WRITE_BYTES_COUNT, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_FLASH_ERASE_COUNT, UINT8) \
|
||||
\
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_KERNEL_HEAP_MIN_HEADROOM_BYTES, UINT16) \
|
||||
\
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_STACK_FREE_KERNEL_MAIN, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_STACK_FREE_KERNEL_BACKGROUND, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_STACK_FREE_BLUETOPIA_BIG, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_STACK_FREE_BLUETOPIA_MEDIUM, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_STACK_FREE_BLUETOPIA_SMALL, UINT16) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_STACK_FREE_NEWTIMERS, UINT16) \
|
||||
\
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_ACTION_INVOKED_FROM_TIMELINE_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_ACTION_INVOKED_FROM_MODAL_NOTIFICATION_COUNT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_ACTION_INVOKED_FROM_NOTIFICATION_APP_COUNT, UINT8) \
|
||||
\
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_HEALTH_CURRENT_STEP_COUNT__DEPRECATED, UINT16) \
|
||||
\
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_DISPLAY_UPDATES_PER_HOUR, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_DISPLAY_OFFSET_X, INT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_DISPLAY_OFFSET_Y, INT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_DISPLAY_OFFSET_MODIFIED_COUNT, UINT8) \
|
||||
\
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_PFS_SPACE_FREE_KB, UINT16) \
|
||||
\
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_HRM_ACCEL_DATA_MISSING, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_HRM_ON_TIME, UINT32) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_HRM_WATCHDOG_TIMEOUT, UINT8) \
|
||||
DEVICE(ANALYTICS_DEVICE_METRIC_BLE_HRM_SHARING_TIME, UINT32) \
|
||||
\
|
||||
MARKER(ANALYTICS_DEVICE_METRIC_END) \
|
||||
\
|
||||
\
|
||||
MARKER(ANALYTICS_APP_METRIC_START) \
|
||||
\
|
||||
APP(ANALYTICS_APP_METRIC_BLOB_KIND, UINT8) \
|
||||
APP(ANALYTICS_APP_METRIC_BLOB_VERSION, UINT16) \
|
||||
APP(ANALYTICS_APP_METRIC_TIMESTAMP, UINT32) \
|
||||
\
|
||||
APP(ANALYTICS_APP_METRIC_TIME_INTERVAL, UINT32) \
|
||||
APP(ANALYTICS_APP_METRIC_UUID, UINT8, 16) \
|
||||
APP(ANALYTICS_APP_METRIC_SDK_MAJOR_VERSION, UINT8) \
|
||||
APP(ANALYTICS_APP_METRIC_SDK_MINOR_VERSION, UINT8) \
|
||||
APP(ANALYTICS_APP_METRIC_APP_MAJOR_VERSION, UINT8) \
|
||||
APP(ANALYTICS_APP_METRIC_APP_MINOR_VERSION, UINT8) \
|
||||
APP(ANALYTICS_APP_METRIC_RESOURCE_TIMESTAMP, UINT32) \
|
||||
\
|
||||
APP(ANALYTICS_APP_METRIC_LAUNCH_COUNT, UINT8) \
|
||||
APP(ANALYTICS_APP_METRIC_USER_LAUNCH_COUNT, UINT8) \
|
||||
APP(ANALYTICS_APP_METRIC_QUICK_LAUNCH_COUNT, UINT8) \
|
||||
APP(ANALYTICS_APP_METRIC_FRONT_MOST_TIME, UINT32) \
|
||||
APP(ANALYTICS_APP_METRIC_CRASHED_COUNT, UINT8) \
|
||||
APP(ANALYTICS_APP_METRIC_ROCKY_LAUNCH_COUNT, UINT8) \
|
||||
APP(ANALYTICS_APP_METRIC_ROCKY_CRASHED_COUNT, UINT8) \
|
||||
\
|
||||
APP(ANALYTICS_APP_METRIC_CPU_RUNNING_TIME, UINT32) \
|
||||
APP(ANALYTICS_APP_METRIC_CPU_SLEEP_TIME, UINT32) \
|
||||
APP(ANALYTICS_APP_METRIC_CPU_STOP_TIME, UINT32) \
|
||||
\
|
||||
APP(ANALYTICS_APP_METRIC_MEM_NATIVE_HEAP_SIZE, UINT32) \
|
||||
APP(ANALYTICS_APP_METRIC_MEM_NATIVE_HEAP_PEAK, UINT32) \
|
||||
APP(ANALYTICS_APP_METRIC_MEM_ROCKY_HEAP_PEAK, UINT32) \
|
||||
APP(ANALYTICS_APP_METRIC_MEM_ROCKY_HEAP_WASTE, UINT32) \
|
||||
APP(ANALYTICS_APP_METRIC_MEM_ROCKY_RECURSIVE_MEMORYPRESSURE_EVENT_COUNT, UINT8) \
|
||||
\
|
||||
APP(ANALYTICS_APP_METRIC_BG_CPU_RUNNING_TIME, UINT32) \
|
||||
APP(ANALYTICS_APP_METRIC_BG_CPU_SLEEP_TIME, UINT32) \
|
||||
APP(ANALYTICS_APP_METRIC_BG_CPU_STOP_TIME, UINT32) \
|
||||
APP(ANALYTICS_APP_METRIC_BG_CRASHED_COUNT, UINT8) \
|
||||
\
|
||||
APP(ANALYTICS_APP_METRIC_BUTTONS_PRESSED_COUNT, UINT32) \
|
||||
APP(ANALYTICS_APP_METRIC_ACCEL_SHAKE_COUNT, UINT32) \
|
||||
APP(ANALYTICS_APP_METRIC_ACCEL_PEEK_COUNT, UINT32) \
|
||||
APP(ANALYTICS_APP_METRIC_ACCEL_SAMPLE_COUNT, UINT32) \
|
||||
APP(ANALYTICS_APP_METRIC_BACKLIGHT_ON_COUNT, UINT32) \
|
||||
APP(ANALYTICS_APP_METRIC_BACKLIGHT_ON_TIME, UINT32) \
|
||||
APP(ANALYTICS_APP_METRIC_VIBRATOR_ON_COUNT, UINT32) \
|
||||
APP(ANALYTICS_APP_METRIC_VIBRATOR_ON_TIME, UINT32) \
|
||||
APP(ANALYTICS_APP_METRIC_DISPLAY_WRITE_TIME, UINT32) \
|
||||
\
|
||||
APP(ANALYTICS_APP_METRIC_MSG_IN_COUNT, UINT32) \
|
||||
APP(ANALYTICS_APP_METRIC_MSG_BYTE_IN_COUNT, UINT32) \
|
||||
APP(ANALYTICS_APP_METRIC_MSG_OUT_COUNT, UINT32) \
|
||||
APP(ANALYTICS_APP_METRIC_MSG_BYTE_OUT_COUNT, UINT32) \
|
||||
APP(ANALYTICS_APP_METRIC_MSG_DROP_COUNT, UINT16) \
|
||||
\
|
||||
APP(ANALYTICS_APP_METRIC_LOG_OUT_COUNT, UINT32) \
|
||||
APP(ANALYTICS_APP_METRIC_LOG_BYTE_OUT_COUNT, UINT32) \
|
||||
\
|
||||
APP(ANALYTICS_APP_METRIC_FLASH_READ_COUNT, UINT32) \
|
||||
APP(ANALYTICS_APP_METRIC_FLASH_READ_BYTES_COUNT, UINT32) \
|
||||
APP(ANALYTICS_APP_METRIC_FLASH_WRITE_BYTES_COUNT, UINT32) \
|
||||
APP(ANALYTICS_APP_METRIC_FLASH_SUBSECTOR_ERASE_COUNT, UINT32) \
|
||||
\
|
||||
MARKER(ANALYTICS_APP_METRIC_END) \
|
||||
MARKER(ANALYTICS_METRIC_END)
|
||||
|
||||
|
||||
#define ENUM(name, ...) name,
|
||||
typedef enum {
|
||||
ANALYTICS_METRIC_TABLE(ENUM, ENUM, ENUM,,,,,,)
|
||||
} AnalyticsMetric;
|
||||
#undef ENUM
|
43
src/fw/services/common/analytics/analytics_storage.h
Normal file
43
src/fw/services/common/analytics/analytics_storage.h
Normal file
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 "analytics.h"
|
||||
#include "kernel/pebble_tasks.h"
|
||||
#include "analytics_heartbeat.h"
|
||||
|
||||
typedef struct {
|
||||
ListNode node;
|
||||
AnalyticsHeartbeat *heartbeat;
|
||||
} AnalyticsHeartbeatList;
|
||||
|
||||
typedef void (*AnalyticsHeartbeatCallback)(AnalyticsHeartbeat *heartbeat, void *data);
|
||||
|
||||
void analytics_storage_init(void);
|
||||
|
||||
void analytics_storage_take_lock(void);
|
||||
bool analytics_storage_has_lock(void);
|
||||
void analytics_storage_give_lock(void);
|
||||
|
||||
// Must hold the lock before using any of the functions below this marker.
|
||||
AnalyticsHeartbeat *analytics_storage_hijack_device_heartbeat();
|
||||
AnalyticsHeartbeatList *analytics_storage_hijack_app_heartbeats();
|
||||
|
||||
AnalyticsHeartbeat *analytics_storage_find(AnalyticsMetric metric, const Uuid *uuid,
|
||||
AnalyticsClient client);
|
||||
|
||||
const Uuid *analytics_uuid_for_client(AnalyticsClient client);
|
146
src/fw/services/common/animation_service.c
Normal file
146
src/fw/services/common/animation_service.c
Normal file
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
* 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/ui/animation_private.h"
|
||||
#include "applib/app_logging.h"
|
||||
|
||||
#include "kernel/events.h"
|
||||
#include "kernel/kernel_applib_state.h"
|
||||
|
||||
#include "process_management/process_manager.h"
|
||||
#include "process_state/app_state/app_state.h"
|
||||
|
||||
#include "services/common/new_timer/new_timer.h"
|
||||
|
||||
#include "system/passert.h"
|
||||
|
||||
#include "syscall/syscall.h"
|
||||
#include "syscall/syscall_internal.h"
|
||||
|
||||
|
||||
// The timer ID used for each task that we support
|
||||
static TimerID s_kernel_main_timer_id = TIMER_INVALID_ID;
|
||||
static TimerID s_app_timer_id = TIMER_INVALID_ID;
|
||||
|
||||
static bool s_kernel_main_event_pending;
|
||||
static bool s_app_event_pending;
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
void animation_service_cleanup(PebbleTask task) {
|
||||
PBL_ASSERT_TASK(PebbleTask_KernelMain);
|
||||
|
||||
if (task == PebbleTask_KernelMain) {
|
||||
if (s_kernel_main_timer_id != TIMER_INVALID_ID) {
|
||||
new_timer_delete(s_kernel_main_timer_id);
|
||||
s_kernel_main_timer_id = TIMER_INVALID_ID;
|
||||
}
|
||||
s_kernel_main_event_pending = false;
|
||||
} else if (task == PebbleTask_App) {
|
||||
if (s_app_timer_id != TIMER_INVALID_ID) {
|
||||
new_timer_delete(s_app_timer_id);
|
||||
s_app_timer_id = TIMER_INVALID_ID;
|
||||
}
|
||||
s_app_event_pending = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
static void prv_timer_callback(void * context) {
|
||||
PebbleTask task = (PebbleTask)context;
|
||||
|
||||
PebbleEvent e = {
|
||||
.type = PEBBLE_CALLBACK_EVENT,
|
||||
.callback = {
|
||||
.callback = animation_private_timer_callback
|
||||
}
|
||||
};
|
||||
|
||||
switch (task) {
|
||||
case PebbleTask_KernelMain:
|
||||
if (!s_kernel_main_event_pending) {
|
||||
s_kernel_main_event_pending = true;
|
||||
e.callback.data = (void *)kernel_applib_get_animation_state();
|
||||
event_put(&e);
|
||||
}
|
||||
break;
|
||||
case PebbleTask_App:
|
||||
if (!s_app_event_pending) {
|
||||
e.callback.data = (void *)app_state_get_animation_state();
|
||||
s_app_event_pending = process_manager_send_event_to_process(task, &e);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
PBL_CROAK("Invalid task %s", pebble_task_get_name(pebble_task_get_current()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
DEFINE_SYSCALL(void, animation_service_timer_event_received, void) {
|
||||
PebbleTask task = pebble_task_get_current();
|
||||
|
||||
if (task == PebbleTask_KernelMain) {
|
||||
s_kernel_main_event_pending = false;
|
||||
} else if (task == PebbleTask_App) {
|
||||
s_app_event_pending = false;
|
||||
} else {
|
||||
if (PRIVILEGE_WAS_ELEVATED) {
|
||||
syscall_failed();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
DEFINE_SYSCALL(void, animation_service_timer_schedule, uint32_t ms) {
|
||||
PebbleTask task = pebble_task_get_current();
|
||||
TimerID *timer_id;
|
||||
|
||||
if (task == PebbleTask_KernelMain) {
|
||||
timer_id = &s_kernel_main_timer_id;
|
||||
} else if (task == PebbleTask_App) {
|
||||
timer_id = &s_app_timer_id;
|
||||
} else {
|
||||
if (PRIVILEGE_WAS_ELEVATED) {
|
||||
syscall_failed();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Need to create the timer?
|
||||
bool success = false;
|
||||
if (*timer_id == TIMER_INVALID_ID) {
|
||||
*timer_id = new_timer_create();
|
||||
}
|
||||
|
||||
// Schedule/reschedule it
|
||||
if (*timer_id != TIMER_INVALID_ID) {
|
||||
success = new_timer_start(*timer_id, ms, prv_timer_callback, (void *)(uintptr_t)task,
|
||||
0 /*flags */);
|
||||
}
|
||||
if (!success) {
|
||||
APP_LOG(APP_LOG_LEVEL_ERROR, "Error scheduling timer");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Used for unit tests only
|
||||
TimerID animation_service_test_get_timer_id(void) {
|
||||
return s_kernel_main_timer_id;
|
||||
}
|
35
src/fw/services/common/animation_service.h
Normal file
35
src/fw/services/common/animation_service.h
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include "kernel/pebble_tasks.h"
|
||||
|
||||
//! @file animation_service.h
|
||||
//! Manage the system resources used by the applib/animation module.
|
||||
|
||||
//! Register the timer to fire in N ms. When it fires, the animation_private_timer_callback()
|
||||
//! will be called and passed the AnimationState for that task.
|
||||
void animation_service_timer_schedule(uint32_t ms);
|
||||
|
||||
//! Acknowledge that we received an event sent by the animation timer
|
||||
void animation_service_timer_event_received(void);
|
||||
|
||||
//! Destroy the animation resoures used by the given task. Called by the process_manager when a
|
||||
// process exits
|
||||
void animation_service_cleanup(PebbleTask task);
|
325
src/fw/services/common/battery/battery_curve.c
Normal file
325
src/fw/services/common/battery/battery_curve.c
Normal file
|
@ -0,0 +1,325 @@
|
|||
/*
|
||||
* 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/battery/battery_curve.h"
|
||||
|
||||
#include "board/board.h"
|
||||
#include "system/logging.h"
|
||||
#include "util/math.h"
|
||||
#include "util/ratio.h"
|
||||
#include "util/size.h"
|
||||
|
||||
typedef struct VoltagePoint {
|
||||
uint8_t percent;
|
||||
uint16_t voltage;
|
||||
} VoltagePoint;
|
||||
|
||||
// TODO: Move these curves somewhere else. Related: PBL-21049
|
||||
|
||||
#ifdef PLATFORM_TINTIN
|
||||
// When the voltage drops below these (mV), the watch will start heading for standby (after delay)
|
||||
#define BATTERY_CRITICAL_VOLTAGE_CHARGING 3200
|
||||
#define BATTERY_CRITICAL_VOLTAGE_DISCHARGING 3100
|
||||
// Battery Tables for non-Snowy
|
||||
static VoltagePoint discharge_curve[] = {
|
||||
{0, BATTERY_CRITICAL_VOLTAGE_DISCHARGING},
|
||||
{2, 3410},
|
||||
{5, 3600},
|
||||
{10, 3670},
|
||||
{20, 3710},
|
||||
{30, 3745},
|
||||
{40, 3775},
|
||||
{50, 3810},
|
||||
{60, 3860},
|
||||
{70, 3925},
|
||||
{80, 4000},
|
||||
{90, 4080},
|
||||
{100, 4120},
|
||||
};
|
||||
|
||||
static const VoltagePoint charge_curve[] = {
|
||||
{0, BATTERY_CRITICAL_VOLTAGE_CHARGING},
|
||||
{5, 3725},
|
||||
{10, 3750},
|
||||
{20, 3790},
|
||||
{30, 3830},
|
||||
{40, 3845},
|
||||
{50, 3870},
|
||||
{60, 3905},
|
||||
{70, 3970},
|
||||
{80, 4025},
|
||||
{90, 4090},
|
||||
{100, 4130},
|
||||
};
|
||||
#elif BOARD_SNOWY_S3 || PLATFORM_ROBERT
|
||||
// When the voltage drops below these (mV), the watch will start heading for standby (after delay)
|
||||
#define BATTERY_CRITICAL_VOLTAGE_CHARGING 3700
|
||||
#define BATTERY_CRITICAL_VOLTAGE_DISCHARGING 3300
|
||||
// Battery Tables for Bobby Smiles
|
||||
static VoltagePoint discharge_curve[] = {
|
||||
{0, BATTERY_CRITICAL_VOLTAGE_DISCHARGING},
|
||||
{2, 3465},
|
||||
{5, 3615},
|
||||
{10, 3685},
|
||||
{20, 3725},
|
||||
{30, 3760},
|
||||
{40, 3795},
|
||||
{50, 3830},
|
||||
{60, 3885},
|
||||
{70, 3955},
|
||||
{80, 4065},
|
||||
{90, 4160},
|
||||
{100, 4250},
|
||||
};
|
||||
|
||||
static const VoltagePoint charge_curve[] = {
|
||||
{0, BATTERY_CRITICAL_VOLTAGE_CHARGING},
|
||||
{2, 3850},
|
||||
{5, 3935},
|
||||
{10, 4000},
|
||||
{20, 4040},
|
||||
{30, 4090},
|
||||
{40, 4145},
|
||||
{50, 4175},
|
||||
{60, 4225},
|
||||
{70, 4250},
|
||||
};
|
||||
#elif PLATFORM_SNOWY || PLATFORM_CALCULUS
|
||||
// When the voltage drops below these (mV), the watch will start heading for standby (after delay)
|
||||
#define BATTERY_CRITICAL_VOLTAGE_CHARGING 3500
|
||||
#define BATTERY_CRITICAL_VOLTAGE_DISCHARGING 3300
|
||||
// Battery Tables for Snowy
|
||||
static VoltagePoint discharge_curve[] = {
|
||||
{0, BATTERY_CRITICAL_VOLTAGE_DISCHARGING},
|
||||
{2, 3500},
|
||||
{5, 3600},
|
||||
{10, 3640},
|
||||
{20, 3690},
|
||||
{30, 3730},
|
||||
{40, 3750},
|
||||
{50, 3790},
|
||||
{60, 3840},
|
||||
{70, 3910},
|
||||
{80, 4000},
|
||||
{90, 4120},
|
||||
{100, 4250},
|
||||
};
|
||||
|
||||
static const VoltagePoint charge_curve[] = {
|
||||
{0, BATTERY_CRITICAL_VOLTAGE_CHARGING},
|
||||
{10, 3970},
|
||||
{20, 4020},
|
||||
{30, 4060},
|
||||
{40, 4090},
|
||||
{50, 4130},
|
||||
{60, 4190},
|
||||
{70, 4250},
|
||||
};
|
||||
#elif PLATFORM_SPALDING
|
||||
// When the voltage drops below these (mV), the watch will start heading for standby (after delay)
|
||||
#define BATTERY_CRITICAL_VOLTAGE_CHARGING 3700
|
||||
#define BATTERY_CRITICAL_VOLTAGE_DISCHARGING 3300
|
||||
// Battery Tables for Spalding
|
||||
static VoltagePoint discharge_curve[] = {
|
||||
{0, BATTERY_CRITICAL_VOLTAGE_DISCHARGING},
|
||||
{2, 3470},
|
||||
{5, 3600},
|
||||
{10, 3680},
|
||||
{20, 3720},
|
||||
{30, 3760},
|
||||
{40, 3790},
|
||||
{50, 3830},
|
||||
{60, 3875},
|
||||
{70, 3950},
|
||||
{80, 4050},
|
||||
{90, 4130},
|
||||
{100, 4250},
|
||||
};
|
||||
|
||||
static const VoltagePoint charge_curve[] = {
|
||||
{0, BATTERY_CRITICAL_VOLTAGE_CHARGING},
|
||||
{10, 3950},
|
||||
{20, 3990},
|
||||
{30, 4030},
|
||||
{40, 4090},
|
||||
{50, 4180},
|
||||
{60, 4230},
|
||||
{70, 4250},
|
||||
};
|
||||
|
||||
#elif PLATFORM_SILK
|
||||
// When the voltage drops below these (mV), the watch will start heading for standby (after delay)
|
||||
#define BATTERY_CRITICAL_VOLTAGE_CHARGING 3550
|
||||
#define BATTERY_CRITICAL_VOLTAGE_DISCHARGING 3300
|
||||
// Battery Tables for Silk
|
||||
static VoltagePoint discharge_curve[] = {
|
||||
{0, BATTERY_CRITICAL_VOLTAGE_DISCHARGING},
|
||||
{2, 3490},
|
||||
{5, 3615},
|
||||
{10, 3655},
|
||||
{20, 3700},
|
||||
{30, 3735},
|
||||
{40, 3760},
|
||||
{50, 3800},
|
||||
{60, 3855},
|
||||
{70, 3935},
|
||||
{80, 4025},
|
||||
{90, 4120},
|
||||
{100, 4230}
|
||||
};
|
||||
|
||||
static const VoltagePoint charge_curve[] = {
|
||||
{0, BATTERY_CRITICAL_VOLTAGE_CHARGING},
|
||||
{2, 3570},
|
||||
{5, 3600},
|
||||
{10, 3645},
|
||||
{20, 3730},
|
||||
{30, 3800},
|
||||
{40, 3860},
|
||||
{50, 3915},
|
||||
{60, 3970},
|
||||
{70, 4030},
|
||||
{80, 4095},
|
||||
{90, 4175},
|
||||
{100, 4260}
|
||||
};
|
||||
|
||||
#else
|
||||
#error "No battery curve for platform!"
|
||||
#endif
|
||||
|
||||
static const uint8_t NUM_DISCHARGE_POINTS = ARRAY_LENGTH(discharge_curve);
|
||||
static const uint8_t NUM_CHARGE_POINTS = ARRAY_LENGTH(charge_curve);
|
||||
|
||||
static int s_battery_compensation_values[BATTERY_CURVE_COMPENSATE_COUNT];
|
||||
|
||||
// Shifts the 100% reference on the discharge curve, as long as it
|
||||
// doesn't drop below the next highest point.
|
||||
void battery_curve_set_full_voltage(uint16_t voltage) {
|
||||
voltage = MAX(voltage, discharge_curve[NUM_DISCHARGE_POINTS-2].voltage + 1);
|
||||
discharge_curve[NUM_DISCHARGE_POINTS-1].voltage = voltage;
|
||||
}
|
||||
|
||||
static uint32_t prv_lookup_percent_by_voltage(
|
||||
int battery_mv, bool is_charging, uint32_t scaling_factor) {
|
||||
VoltagePoint const * const battery_curve = (is_charging) ? charge_curve : discharge_curve;
|
||||
uint8_t const num_curve_points = (is_charging) ? NUM_CHARGE_POINTS : NUM_DISCHARGE_POINTS;
|
||||
|
||||
// Constrain the voltage between the min and max points of the curve
|
||||
if (battery_mv <= battery_curve[0].voltage) {
|
||||
return battery_curve[0].percent * scaling_factor;
|
||||
} else if (battery_mv >= battery_curve[num_curve_points-1].voltage) {
|
||||
return battery_curve[num_curve_points-1].percent * scaling_factor;
|
||||
}
|
||||
|
||||
// search through the curves for the next charge level...
|
||||
uint32_t charge_index;
|
||||
for (charge_index = num_curve_points-2;
|
||||
(charge_index > 0) && (battery_mv < battery_curve[charge_index].voltage);
|
||||
--charge_index) {
|
||||
}
|
||||
|
||||
// linearly interpolate between battery_curve[charge_index] and battery_curve[charge_index+1]
|
||||
uint32_t delta_mv = (battery_mv - battery_curve[charge_index].voltage);
|
||||
uint32_t delta_next_mv = (battery_curve[charge_index + 1].voltage -
|
||||
battery_curve[charge_index].voltage);
|
||||
uint32_t delta_next_percent = (battery_curve[charge_index + 1].percent -
|
||||
battery_curve[charge_index].percent);
|
||||
|
||||
uint32_t start_percent = battery_curve[charge_index].percent * scaling_factor;
|
||||
delta_next_percent *= scaling_factor;
|
||||
|
||||
uint32_t charge_percent =
|
||||
(((uint64_t)delta_next_percent * delta_mv) / delta_next_mv) + start_percent;
|
||||
return charge_percent;
|
||||
}
|
||||
|
||||
uint32_t battery_curve_lookup_percent_by_voltage(uint32_t battery_mv, bool is_charging) {
|
||||
return prv_lookup_percent_by_voltage(battery_mv, is_charging, 1);
|
||||
}
|
||||
|
||||
static uint32_t prv_sample_scaled_charge_percent(
|
||||
uint32_t battery_mv, bool is_charging, uint32_t scaling_factor) {
|
||||
int compensate = 0; // We compensate 5% during rounding, so don't do here
|
||||
|
||||
for (int i = 0; i < BATTERY_CURVE_COMPENSATE_COUNT; ++i) {
|
||||
compensate += s_battery_compensation_values[i];
|
||||
}
|
||||
|
||||
return prv_lookup_percent_by_voltage(battery_mv + compensate, is_charging, scaling_factor);
|
||||
}
|
||||
|
||||
uint32_t battery_curve_sample_ratio32_charge_percent(uint32_t battery_mv, bool is_charging) {
|
||||
const uint32_t scaling_factor = ratio32_from_percent(100) / 100 + 1;
|
||||
return prv_sample_scaled_charge_percent(battery_mv, is_charging, scaling_factor);
|
||||
}
|
||||
|
||||
void battery_curve_set_compensation(BatteryCurveVoltageCompensationKey key, int mv) {
|
||||
s_battery_compensation_values[key] = mv;
|
||||
}
|
||||
|
||||
// This is used by unit tests and QEMU
|
||||
uint32_t battery_curve_lookup_voltage_by_percent(uint32_t percent, bool is_charging) {
|
||||
VoltagePoint const * const battery_curve = (is_charging) ? charge_curve : discharge_curve;
|
||||
uint32_t const num_curve_points = (is_charging) ? NUM_CHARGE_POINTS : NUM_DISCHARGE_POINTS;
|
||||
|
||||
// Clip if above curve upper bound
|
||||
if (percent > battery_curve[num_curve_points-1].percent) {
|
||||
return battery_curve[num_curve_points-1].voltage;
|
||||
}
|
||||
|
||||
// search through the curves for the next charge level...
|
||||
uint32_t charge_index;
|
||||
for (charge_index = num_curve_points-2;
|
||||
(charge_index > 0) && (percent < battery_curve[charge_index].percent);
|
||||
--charge_index) {
|
||||
}
|
||||
|
||||
// linearly interpolate between battery_curve[charge_index] and battery_curve[charge_index+1]
|
||||
uint32_t delta_next_mv = (battery_curve[charge_index+1].voltage -
|
||||
battery_curve[charge_index].voltage);
|
||||
uint32_t delta_next_percent = (battery_curve[charge_index+1].percent -
|
||||
battery_curve[charge_index].percent);
|
||||
uint32_t battery_mv = battery_curve[charge_index].voltage +
|
||||
((percent - battery_curve[charge_index].percent) * delta_next_mv) /
|
||||
delta_next_percent;
|
||||
return battery_mv;
|
||||
}
|
||||
|
||||
//! This call is used internally with PreciseBatteryChargeState
|
||||
//! So must remove low_power_threshold to get correct remaining hours
|
||||
//! before low_power_mode is triggered
|
||||
uint32_t battery_curve_get_hours_remaining(uint32_t percent_remaining) {
|
||||
if (percent_remaining <= BOARD_CONFIG_POWER.low_power_threshold) {
|
||||
return 0;
|
||||
}
|
||||
percent_remaining -= BOARD_CONFIG_POWER.low_power_threshold;
|
||||
return ((BOARD_CONFIG_POWER.battery_capacity_hours * percent_remaining) / 100);
|
||||
}
|
||||
|
||||
//! This call is used internally with PreciseBatteryChargeState
|
||||
//! So must add low_power_threshold to get percentage in terms of
|
||||
//! PreciseBatteryChargeState (which includes low_power_mode)
|
||||
uint32_t battery_curve_get_percent_remaining(uint32_t hours) {
|
||||
return ((hours * 100) / BOARD_CONFIG_POWER.battery_capacity_hours) +
|
||||
BOARD_CONFIG_POWER.low_power_threshold;
|
||||
}
|
||||
|
||||
// for unit tests and analytics
|
||||
int32_t battery_curve_lookup_percent_with_scaling_factor(
|
||||
int battery_mv, bool is_charging, uint32_t scaling_factor) {
|
||||
return (prv_lookup_percent_by_voltage(battery_mv, is_charging, scaling_factor));
|
||||
}
|
48
src/fw/services/common/battery/battery_curve.h
Normal file
48
src/fw/services/common/battery/battery_curve.h
Normal file
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// Handles battery mV <-> % conversion
|
||||
|
||||
typedef enum {
|
||||
BATTERY_CURVE_COMPENSATE_STATUS_LED,
|
||||
BATTERY_CURVE_COMPENSATE_COUNT
|
||||
} BatteryCurveVoltageCompensationKey;
|
||||
|
||||
//! Set compensation value to be applied to battery voltage when calculating percentage charge.
|
||||
//! For example, if an LED is constantly on, the voltage being measured is going to drop due to the
|
||||
//! internal resistance of the battery.
|
||||
void battery_curve_set_compensation(BatteryCurveVoltageCompensationKey key, int mv);
|
||||
|
||||
void battery_curve_set_full_voltage(uint16_t voltage);
|
||||
|
||||
//! Returns the corresponding battery percentage as a ratio32.
|
||||
uint32_t battery_curve_sample_ratio32_charge_percent(uint32_t battery_mv, bool is_charging);
|
||||
|
||||
uint32_t battery_curve_lookup_percent_by_voltage(uint32_t battery_mv, bool is_charging);
|
||||
|
||||
int32_t battery_curve_lookup_percent_with_scaling_factor(
|
||||
int battery_mv, bool is_charging, uint32_t scaling_factor);
|
||||
|
||||
uint32_t battery_curve_get_hours_remaining(uint32_t percent_remaining);
|
||||
|
||||
uint32_t battery_curve_get_percent_remaining(uint32_t hours);
|
||||
|
||||
// This is used by unit tests and QEMU
|
||||
uint32_t battery_curve_lookup_voltage_by_percent(uint32_t percent, bool is_charging);
|
236
src/fw/services/common/battery/battery_monitor.c
Normal file
236
src/fw/services/common/battery/battery_monitor.c
Normal file
|
@ -0,0 +1,236 @@
|
|||
/*
|
||||
* 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/battery/battery_monitor.h"
|
||||
|
||||
#include "board/board.h"
|
||||
#include "kernel/low_power.h"
|
||||
#include "kernel/util/standby.h"
|
||||
#include "services/common/firmware_update.h"
|
||||
#include "services/common/new_timer/new_timer.h"
|
||||
#include "services/common/system_task.h"
|
||||
#include "system/logging.h"
|
||||
#include "util/ratio.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define BATT_LOG_COLOR LOG_COLOR_YELLOW
|
||||
|
||||
// State machine stuff
|
||||
|
||||
typedef void (*Action)(void);
|
||||
|
||||
typedef struct PowerState {
|
||||
Action enter;
|
||||
Action exit;
|
||||
} PowerState;
|
||||
|
||||
typedef enum {
|
||||
PowerStateGood,
|
||||
PowerStateLowPower,
|
||||
PowerStateCritical,
|
||||
PowerStateStandby
|
||||
} PowerStateID;
|
||||
|
||||
static void prv_enter_lpm(void);
|
||||
static void prv_exit_lpm(void);
|
||||
static void prv_begin_standby_timer(void);
|
||||
static void prv_enter_standby(void);
|
||||
static void prv_exit_critical(void);
|
||||
|
||||
static const PowerState power_states[] = {
|
||||
[PowerStateGood] = { 0 },
|
||||
[PowerStateLowPower] = { .enter = prv_enter_lpm, .exit = prv_exit_lpm },
|
||||
[PowerStateCritical] = { .enter = prv_begin_standby_timer, .exit = prv_exit_critical },
|
||||
[PowerStateStandby] = { .enter = prv_enter_standby }
|
||||
};
|
||||
|
||||
////////////////////////
|
||||
// Business logic
|
||||
static TimerID s_standby_timer_id = TIMER_INVALID_ID;
|
||||
T_STATIC PowerStateID s_power_state;
|
||||
static bool s_low_on_first_run;
|
||||
static bool s_first_run;
|
||||
|
||||
static void prv_transition(PowerStateID next_state) {
|
||||
if (next_state == s_power_state) {
|
||||
return;
|
||||
}
|
||||
PowerStateID old_state = s_power_state;
|
||||
s_power_state = next_state;
|
||||
if (power_states[old_state].exit) {
|
||||
power_states[old_state].exit();
|
||||
}
|
||||
if (power_states[next_state].enter) {
|
||||
power_states[next_state].enter();
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_enter_lpm(void) {
|
||||
#ifndef BATTERY_DEBUG
|
||||
if (!firmware_update_is_in_progress()) {
|
||||
low_power_enter();
|
||||
}
|
||||
#endif
|
||||
PBL_LOG_COLOR(LOG_LEVEL_INFO, BATT_LOG_COLOR, "Battery low: enter low power mode");
|
||||
}
|
||||
|
||||
static void prv_resume_normal_operation(void) {
|
||||
low_power_exit();
|
||||
PBL_LOG_COLOR(LOG_LEVEL_INFO, BATT_LOG_COLOR, "Battery good: resume normal operation");
|
||||
}
|
||||
|
||||
static void prv_exit_critical(void) {
|
||||
// Checking the state here is a bit of a hack because the state machine does not have proper
|
||||
// transition actions, only entry/exit actions.
|
||||
// We check that the state is PowerStateGood because the state machine does not transition through
|
||||
// all states in between the new and old states in a transition.
|
||||
if (s_power_state == PowerStateGood) {
|
||||
prv_resume_normal_operation();
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_exit_lpm(void) {
|
||||
// Checking the state here is a bit of a hack because the state machine does not have proper
|
||||
// transition actions, only entry/exit actions
|
||||
if (s_power_state == PowerStateGood) {
|
||||
prv_resume_normal_operation();
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_standby_timer_callback(void* data) {
|
||||
// FIXME This is so broken: battery_state_force_update schedules a new timer callback to execute
|
||||
// immediately, which then pends a background task callback to perform the update, so this will
|
||||
// never update before we check the power_state.
|
||||
battery_state_force_update();
|
||||
if (s_power_state == PowerStateCritical) {
|
||||
// Still critical after timeout, transition to standby
|
||||
prv_transition(PowerStateStandby);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_begin_standby_timer(void) {
|
||||
PBL_LOG_COLOR(LOG_LEVEL_INFO, BATT_LOG_COLOR, "Battery critical: begin standby timer");
|
||||
// If the watch was already running, give them 30s, otherwise just 2s.
|
||||
uint32_t standby_timeout = (s_first_run) ? 2000: 30000;
|
||||
new_timer_start(s_standby_timer_id, standby_timeout,
|
||||
prv_standby_timer_callback, NULL, 0 /*flags*/);
|
||||
}
|
||||
|
||||
static void system_task_handle_battery_critical(void* data) {
|
||||
PBL_LOG_COLOR(LOG_LEVEL_INFO, BATT_LOG_COLOR, "Battery critical: go to standby mode");
|
||||
if (low_power_is_active()) {
|
||||
low_power_standby();
|
||||
} else {
|
||||
enter_standby(RebootReasonCode_LowBattery);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_enter_standby(void) {
|
||||
system_task_add_callback(system_task_handle_battery_critical, NULL);
|
||||
}
|
||||
|
||||
static void prv_log_battery_state(PreciseBatteryChargeState state) {
|
||||
const uint16_t k_min_percent_diff = 5;
|
||||
const uint16_t percent = ratio32_to_percent(state.charge_percent);
|
||||
|
||||
union LoggingBattState{
|
||||
struct {
|
||||
uint16_t is_charging:1;
|
||||
uint16_t is_plugged:1;
|
||||
uint16_t percent:14;
|
||||
};
|
||||
uint16_t all;
|
||||
};
|
||||
static union LoggingBattState s_prev_batt_state;
|
||||
|
||||
union LoggingBattState new_batt_state = {
|
||||
.percent = percent / k_min_percent_diff,
|
||||
.is_charging = state.is_charging,
|
||||
.is_plugged = state.is_plugged,
|
||||
};
|
||||
|
||||
if ((percent < BOARD_CONFIG_POWER.low_power_threshold) ||
|
||||
(s_prev_batt_state.all != new_batt_state.all) ||
|
||||
s_first_run) {
|
||||
s_prev_batt_state.all = new_batt_state.all;
|
||||
PBL_LOG_COLOR(LOG_LEVEL_INFO, BATT_LOG_COLOR, "Percent: %d Charging: %d Plugged: %d",
|
||||
percent, state.is_charging, state.is_plugged);
|
||||
}
|
||||
}
|
||||
|
||||
void battery_monitor_handle_state_change_event(PreciseBatteryChargeState state) {
|
||||
// Update Critical/Low Power Mode
|
||||
|
||||
// Standby behaviour, as gleaned from the previous implementation:
|
||||
// Once the battery voltage falls below exactly 0%, the standby lockout is displayed.
|
||||
// If the USB cable is disconnected, the standby timer starts. This standby delay is 2s
|
||||
// (if at first start), otherwise it is 30s (if the watch was already running).
|
||||
// The shutdown can be averted if the watch is plugged in before the timer expires.
|
||||
// Similarly, if the battery voltage has rebounded when the timer expires, the shutdown
|
||||
// will not occur.
|
||||
|
||||
bool critical = (state.charge_percent == 0) && !state.is_charging;
|
||||
|
||||
#ifndef RECOVERY_FW
|
||||
const uint32_t LOW_POWER_PERCENT = ratio32_from_percent(BOARD_CONFIG_POWER.low_power_threshold);
|
||||
|
||||
bool low_power = !state.is_charging && (state.charge_percent <= LOW_POWER_PERCENT);
|
||||
s_low_on_first_run = s_low_on_first_run || (low_power && s_first_run);
|
||||
#else
|
||||
const uint32_t PRF_LOW_POWER_THRESHOLD_PERCENT = ratio32_from_percent(5);
|
||||
|
||||
// We want to keep the LPM UI up until we've hit 10% regardless of charging
|
||||
bool low_power = state.charge_percent < PRF_LOW_POWER_THRESHOLD_PERCENT;
|
||||
s_low_on_first_run = false;
|
||||
#endif
|
||||
|
||||
PowerStateID new_state;
|
||||
|
||||
if (critical || s_low_on_first_run) {
|
||||
new_state = PowerStateCritical;
|
||||
} else if (low_power) {
|
||||
new_state = PowerStateLowPower;
|
||||
} else {
|
||||
new_state = PowerStateGood;
|
||||
}
|
||||
|
||||
// All state transitions are valid in this state machine.
|
||||
prv_transition(new_state);
|
||||
|
||||
prv_log_battery_state(state);
|
||||
|
||||
s_first_run = false;
|
||||
}
|
||||
|
||||
void battery_monitor_init(void) {
|
||||
s_standby_timer_id = new_timer_create();
|
||||
s_power_state = PowerStateGood;
|
||||
s_low_on_first_run = false;
|
||||
s_first_run = true;
|
||||
|
||||
// Initialize driver interface
|
||||
battery_state_init();
|
||||
}
|
||||
|
||||
bool battery_monitor_critical_lockout(void) {
|
||||
// critical or low on first run
|
||||
return s_power_state == PowerStateCritical;
|
||||
}
|
||||
|
||||
TimerID battery_monitor_get_standby_timer_id(void) {
|
||||
return s_standby_timer_id;
|
||||
}
|
33
src/fw/services/common/battery/battery_monitor.h
Normal file
33
src/fw/services/common/battery/battery_monitor.h
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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/common/battery/battery_state.h"
|
||||
#include "services/common/new_timer/new_timer.h"
|
||||
#include <stdbool.h>
|
||||
|
||||
// The battery monitor handles power state and associated service control, in response to battery
|
||||
// state changes. This includes low power and critical modes.
|
||||
|
||||
void battery_monitor_init(void);
|
||||
void battery_monitor_handle_state_change_event(PreciseBatteryChargeState state);
|
||||
|
||||
// Use the battery state to determine if UI elements should be locked out
|
||||
// because the battery is too low
|
||||
bool battery_monitor_critical_lockout(void);
|
||||
|
||||
// For unit tests
|
||||
TimerID battery_monitor_get_standby_timer_id(void);
|
381
src/fw/services/common/battery/battery_state.c
Normal file
381
src/fw/services/common/battery/battery_state.c
Normal file
|
@ -0,0 +1,381 @@
|
|||
/*
|
||||
* 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/battery/battery_state.h"
|
||||
|
||||
#include "board/board.h"
|
||||
#include "debug/power_tracking.h"
|
||||
#include "drivers/battery.h"
|
||||
#include "kernel/events.h"
|
||||
#include "kernel/util/stop.h"
|
||||
#include "services/common/analytics/analytics.h"
|
||||
#include "services/common/battery/battery_curve.h"
|
||||
#include "services/common/battery/battery_monitor.h"
|
||||
#include "services/common/new_timer/new_timer.h"
|
||||
#include "services/common/system_task.h"
|
||||
#include "syscall/syscall_internal.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
#include "util/math.h"
|
||||
#include "util/ratio.h"
|
||||
|
||||
#ifdef DEBUG_BATTERY_STATE
|
||||
#define BATTERY_SAMPLE_RATE_MS 1000
|
||||
#else
|
||||
#define BATTERY_SAMPLE_RATE_MS (60 * 1000)
|
||||
#endif
|
||||
|
||||
typedef void (*EntryFunc)(void);
|
||||
|
||||
typedef enum {
|
||||
ConnectionStateInvalid,
|
||||
ConnectionStateChargingPlugged,
|
||||
ConnectionStateDischargingPlugged,
|
||||
ConnectionStateDischargingUnplugged
|
||||
} ConnectionStateID;
|
||||
|
||||
typedef struct ConnectionState {
|
||||
EntryFunc enter;
|
||||
} ConnectionState;
|
||||
|
||||
static void prv_update_plugged_change(void);
|
||||
static void prv_update_done_charging(void);
|
||||
|
||||
static const ConnectionState s_transitions[] = {
|
||||
[ConnectionStateChargingPlugged] = { .enter = prv_update_plugged_change },
|
||||
[ConnectionStateDischargingPlugged] = { .enter = prv_update_done_charging },
|
||||
[ConnectionStateDischargingUnplugged] = { .enter = prv_update_plugged_change }
|
||||
};
|
||||
|
||||
typedef struct BatteryState {
|
||||
uint64_t init_time;
|
||||
uint32_t percent;
|
||||
uint16_t voltage;
|
||||
uint8_t skip_count;
|
||||
ConnectionStateID connection;
|
||||
} BatteryState;
|
||||
|
||||
static BatteryState s_last_battery_state;
|
||||
static TimerID s_periodic_timer_id = TIMER_INVALID_ID;
|
||||
static int s_analytics_previous_mv = 0;
|
||||
|
||||
static void prv_schedule_update(uint32_t delay, bool force_update);
|
||||
PreciseBatteryChargeState prv_get_precise_charge_state(const BatteryState *state);
|
||||
|
||||
static void prv_transition(BatteryState *state, ConnectionStateID next_state) {
|
||||
state->connection = next_state;
|
||||
s_transitions[state->connection].enter();
|
||||
}
|
||||
|
||||
static void prv_update_plugged_change(void) {
|
||||
// If the connection state changed or we finished charging, reset the filter since we're
|
||||
// probably switching to a new curve.
|
||||
battery_state_reset_filter();
|
||||
|
||||
|
||||
bool is_charging = battery_charge_controller_thinks_we_are_charging();
|
||||
if (is_charging) {
|
||||
analytics_stopwatch_start(ANALYTICS_DEVICE_METRIC_BATTERY_CHARGE_TIME, AnalyticsClient_System);
|
||||
} else {
|
||||
analytics_stopwatch_stop(ANALYTICS_DEVICE_METRIC_BATTERY_CHARGE_TIME);
|
||||
}
|
||||
|
||||
bool is_plugged = battery_is_usb_connected();
|
||||
if (is_plugged) {
|
||||
analytics_stopwatch_start(ANALYTICS_DEVICE_METRIC_BATTERY_PLUGGED_TIME, AnalyticsClient_System);
|
||||
} else {
|
||||
analytics_stopwatch_stop(ANALYTICS_DEVICE_METRIC_BATTERY_PLUGGED_TIME);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_update_done_charging(void) {
|
||||
prv_update_plugged_change();
|
||||
|
||||
// Amount in mV to drop the "Full" voltage by to briefly stay at 100% once unplugged
|
||||
const uint16_t BATTERY_FULL_FUDGE_AMOUNT = 10;
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Done charging - Updating curve");
|
||||
battery_curve_set_full_voltage(s_last_battery_state.voltage - BATTERY_FULL_FUDGE_AMOUNT);
|
||||
}
|
||||
|
||||
static void battery_state_put_change_event(PreciseBatteryChargeState state) {
|
||||
PebbleEvent e = {
|
||||
.type = PEBBLE_BATTERY_STATE_CHANGE_EVENT,
|
||||
.battery_state = {
|
||||
.new_state = state,
|
||||
},
|
||||
};
|
||||
event_put(&e);
|
||||
}
|
||||
|
||||
void battery_state_reset_filter(void) {
|
||||
s_last_battery_state.voltage = battery_get_millivolts();
|
||||
// Reset the stablization timer in case we encountered a current spike during the reset
|
||||
s_last_battery_state.init_time = rtc_get_ticks();
|
||||
}
|
||||
|
||||
static uint32_t prv_filter_voltage(uint32_t avg_mv, uint32_t battery_mv) {
|
||||
// Basic low-pass filter - See PBL-23637
|
||||
const uint8_t VOLTAGE_FILTER_BETA = 2;
|
||||
uint32_t avg = (avg_mv << VOLTAGE_FILTER_BETA);
|
||||
avg -= avg_mv;
|
||||
avg += battery_mv;
|
||||
return avg >> VOLTAGE_FILTER_BETA;
|
||||
}
|
||||
|
||||
static bool prv_is_stable(const BatteryState *state) {
|
||||
// After a reboot, we typically source a lot of current which can drastically impact
|
||||
// our mV readings due to the internal resistance of the battery. We use the
|
||||
// system_likely_stabilized flag as an indicator of how trustworthy our readings are
|
||||
const uint64_t STABLE_TICKS = 3 * 60 * RTC_TICKS_HZ;
|
||||
uint64_t elapsed_ticks = rtc_get_ticks() - state->init_time;
|
||||
return elapsed_ticks > STABLE_TICKS;
|
||||
}
|
||||
|
||||
static ConnectionStateID prv_get_connection_state(void) {
|
||||
const bool charging = battery_charge_controller_thinks_we_are_charging();
|
||||
const bool plugged_in = battery_is_usb_connected();
|
||||
|
||||
if (plugged_in) {
|
||||
if (charging) {
|
||||
return ConnectionStateChargingPlugged;
|
||||
} else {
|
||||
return ConnectionStateDischargingPlugged;
|
||||
}
|
||||
} else {
|
||||
if (charging) {
|
||||
// Since we can't be charging and disconnected,
|
||||
// just log a warning and pretend we aren't charging.
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "PMIC reported charging while unplugged - ignoring");
|
||||
}
|
||||
return ConnectionStateDischargingUnplugged;
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_update_state(void *force_update) {
|
||||
const uint8_t MAX_SAMPLE_SKIPS = 5;
|
||||
bool forced = (bool)force_update;
|
||||
// Large current draws will cause the voltage supplied by the battery to
|
||||
// droop. We try to only sample the battery when there is minimal
|
||||
// activity. We look to see if stop mode is allowed because this is a good
|
||||
// indicator that no peripherals are in use (i.e vibe, backlight, etc)
|
||||
if ((s_last_battery_state.skip_count < MAX_SAMPLE_SKIPS) &&
|
||||
!forced && !stop_mode_is_allowed()) {
|
||||
s_last_battery_state.skip_count++;
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_last_battery_state.skip_count == MAX_SAMPLE_SKIPS) {
|
||||
analytics_inc(ANALYTICS_DEVICE_METRIC_BATTERY_SAMPLE_SKIP_COUNT_EXCEEDED,
|
||||
AnalyticsClient_System);
|
||||
}
|
||||
s_last_battery_state.skip_count = 0;
|
||||
|
||||
// Driver communication
|
||||
|
||||
bool state_changed = false;
|
||||
ConnectionStateID next_state = prv_get_connection_state();
|
||||
if (s_last_battery_state.connection != next_state) {
|
||||
// Do not allow DischargingPlugged -> ChargingPlugged state
|
||||
if ((s_last_battery_state.connection != ConnectionStateDischargingPlugged) ||
|
||||
(next_state != ConnectionStateChargingPlugged)) {
|
||||
prv_transition(&s_last_battery_state, next_state);
|
||||
state_changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
s_last_battery_state.voltage = prv_filter_voltage(s_last_battery_state.voltage,
|
||||
battery_get_millivolts());
|
||||
bool charging = (s_last_battery_state.connection == ConnectionStateChargingPlugged);
|
||||
|
||||
// Update Percent & Filtering
|
||||
|
||||
const uint32_t ALWAYS_UPDATE_THRESHOLD = ratio32_from_percent(10);
|
||||
bool likely_stable = prv_is_stable(&s_last_battery_state);
|
||||
|
||||
uint32_t new_charge_percent =
|
||||
battery_curve_sample_ratio32_charge_percent(s_last_battery_state.voltage, charging);
|
||||
#ifndef TARGET_QEMU
|
||||
// If QEMU, allow updates to always occur for ease of testing otherwise
|
||||
// Allow updates iff:
|
||||
// - We are charging
|
||||
// - We are discharging and:
|
||||
// - The readings have stabilized and the battery percent did not go up
|
||||
// - The readings have not yet stablized
|
||||
// TL;DR: Allow updates unless we're stable and discharging but the % went up.
|
||||
if (!charging && likely_stable &&
|
||||
new_charge_percent > s_last_battery_state.percent) {
|
||||
// It's okay to return early since any connection/plugged changes will reset the filter,
|
||||
// so we won't catch those.
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
s_last_battery_state.percent = new_charge_percent;
|
||||
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "mV Raw: %"PRIu16" Ratio: %"PRIu32" Percent: %"PRIu32,
|
||||
s_last_battery_state.voltage, s_last_battery_state.percent,
|
||||
ratio32_to_percent(s_last_battery_state.percent));
|
||||
|
||||
PWR_TRACK_BATT(charging ? "CHARGING" : "DISCHARGING", s_last_battery_state.voltage);
|
||||
|
||||
if (forced || likely_stable || s_last_battery_state.percent <= ALWAYS_UPDATE_THRESHOLD ||
|
||||
charging || state_changed) {
|
||||
battery_state_put_change_event(prv_get_precise_charge_state(&s_last_battery_state));
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_update_callback(void *data) {
|
||||
// Running the battery monitor on the timer task is not a good idea because
|
||||
// we could be sampling right in the middle of a flash erase, etc. Therefore,
|
||||
// dispatch to a lower priority task
|
||||
system_task_add_callback(prv_update_state, data);
|
||||
|
||||
// Reschedule ourselves again so we create a loop
|
||||
prv_schedule_update(BATTERY_SAMPLE_RATE_MS, false);
|
||||
}
|
||||
|
||||
static void prv_schedule_update(uint32_t delay, bool force_update) {
|
||||
bool success = new_timer_start(s_periodic_timer_id, delay, prv_update_callback,
|
||||
(void *)force_update, 0 /*flags*/);
|
||||
PBL_ASSERTN(success);
|
||||
}
|
||||
|
||||
void battery_state_force_update(void) {
|
||||
// Fire off our periodic timer. Note that we rely on the callback to reschedule the timer
|
||||
// for 1 minute intervals rather than create it as a repeating timer. This is because
|
||||
// we occasionally want the callback to get triggered immediately
|
||||
// (in response to the charging cable being plugged in). In these instances, we reschedule it
|
||||
// from the main task.
|
||||
prv_schedule_update(0, true);
|
||||
}
|
||||
|
||||
void battery_state_init(void) {
|
||||
s_periodic_timer_id = new_timer_create();
|
||||
|
||||
s_last_battery_state = (BatteryState) { .connection = ConnectionStateDischargingUnplugged };
|
||||
battery_state_reset_filter();
|
||||
battery_state_force_update();
|
||||
|
||||
s_analytics_previous_mv = s_last_battery_state.voltage;
|
||||
}
|
||||
|
||||
void battery_state_handle_connection_event(bool is_connected) {
|
||||
static const uint32_t RECONNECTION_DELAY_MS = 1000;
|
||||
|
||||
PBL_LOG_VERBOSE("USB Connected:%d", is_connected);
|
||||
|
||||
// Trigger a reset update to the state machine. Delay the update to allow the battery voltage to
|
||||
// settle and to debounce reconnection events
|
||||
prv_schedule_update(RECONNECTION_DELAY_MS, true);
|
||||
}
|
||||
|
||||
PreciseBatteryChargeState prv_get_precise_charge_state(const BatteryState *state) {
|
||||
PreciseBatteryChargeState event_state = {
|
||||
.charge_percent = state->percent,
|
||||
.is_charging = (s_last_battery_state.connection == ConnectionStateChargingPlugged),
|
||||
.is_plugged = (s_last_battery_state.connection != ConnectionStateDischargingUnplugged)
|
||||
};
|
||||
return event_state;
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(BatteryChargeState, sys_battery_get_charge_state, void) {
|
||||
return battery_get_charge_state();
|
||||
}
|
||||
|
||||
BatteryChargeState battery_get_charge_state(void) {
|
||||
bool is_plugged = (s_last_battery_state.connection != ConnectionStateDischargingUnplugged);
|
||||
|
||||
int32_t percent = ratio32_to_percent(s_last_battery_state.percent);
|
||||
|
||||
// subtract low power reserve, so developer will see 0% when we're approaching low power mode
|
||||
int32_t percent_normalized = MAX((percent - BOARD_CONFIG_POWER.low_power_threshold
|
||||
+ percent / (100 / BOARD_CONFIG_POWER.low_power_threshold)), 0);
|
||||
|
||||
// massage rounding factor so that between 100% to 50% charge the SOC reported is biased to a
|
||||
// higher charge percent bin.
|
||||
int32_t rounding_factor = 5 + MAX(((percent - 50) / 10), 0);
|
||||
BatteryChargeState state = {
|
||||
.charge_percent = MIN(10 * ((percent_normalized + rounding_factor) / 10), 100),
|
||||
.is_charging = is_plugged && percent_normalized < 100,
|
||||
.is_plugged = is_plugged,
|
||||
};
|
||||
return state;
|
||||
}
|
||||
|
||||
// For unit tests
|
||||
TimerID battery_state_get_periodic_timer_id(void) {
|
||||
return s_periodic_timer_id;
|
||||
}
|
||||
|
||||
uint16_t battery_state_get_voltage(void) {
|
||||
return s_last_battery_state.voltage;
|
||||
}
|
||||
|
||||
|
||||
#include "console/prompt.h"
|
||||
void command_print_battery_status(void) {
|
||||
char buffer[32];
|
||||
PreciseBatteryChargeState state = prv_get_precise_charge_state(&s_last_battery_state);
|
||||
prompt_send_response_fmt(buffer, 32, "%"PRIu16" mV", s_last_battery_state.voltage);
|
||||
prompt_send_response_fmt(buffer, 32,
|
||||
"batt_percent: %"PRIu32"%%", ratio32_to_percent(state.charge_percent));
|
||||
prompt_send_response_fmt(buffer, 32, "plugged: %s", state.is_plugged ? "YES" : "NO");
|
||||
prompt_send_response_fmt(buffer, 32, "charging: %s", state.is_charging ? "YES" : "NO");
|
||||
}
|
||||
|
||||
/////////////////
|
||||
// Analytics
|
||||
|
||||
// Note that this is run on a different thread than battery_state!
|
||||
void analytics_external_collect_battery(void) {
|
||||
// This should not be called for an hour after bootup
|
||||
|
||||
int battery_mv = s_last_battery_state.voltage;
|
||||
|
||||
int d_mv = battery_mv - s_analytics_previous_mv;
|
||||
analytics_set(ANALYTICS_DEVICE_METRIC_BATTERY_VOLTAGE, battery_mv, AnalyticsClient_System);
|
||||
analytics_set(ANALYTICS_DEVICE_METRIC_BATTERY_VOLTAGE_DELTA, d_mv, AnalyticsClient_System);
|
||||
|
||||
int scaling_factor = INT32_MAX / 100; // we want to cover -100 to 100 percent
|
||||
// Note: we assume that the watch was not charging during the hour.
|
||||
int32_t start_percent = battery_curve_lookup_percent_with_scaling_factor(s_analytics_previous_mv,
|
||||
false, scaling_factor);
|
||||
int32_t curr_percent = battery_curve_lookup_percent_with_scaling_factor(battery_mv, false,
|
||||
scaling_factor);
|
||||
int32_t d_percent = curr_percent - start_percent;
|
||||
|
||||
s_analytics_previous_mv = battery_mv;
|
||||
analytics_set(ANALYTICS_DEVICE_METRIC_BATTERY_PERCENT_DELTA, d_percent, AnalyticsClient_System);
|
||||
|
||||
analytics_set(ANALYTICS_DEVICE_METRIC_BATTERY_PERCENT,
|
||||
ratio32_to_percent(s_last_battery_state.percent),
|
||||
AnalyticsClient_System);
|
||||
}
|
||||
|
||||
static void prv_set_forced_charge_state(bool is_charging) {
|
||||
battery_force_charge_enable(is_charging);
|
||||
|
||||
// Trigger an immediate update to the state machine: may trigger an event
|
||||
battery_state_force_update();
|
||||
}
|
||||
|
||||
void command_battery_charge_option(const char* option) {
|
||||
if (!strcmp("disable", option)) {
|
||||
prv_set_forced_charge_state(false);
|
||||
} else if (!strcmp("enable", option)) {
|
||||
prv_set_forced_charge_state(true);
|
||||
}
|
||||
}
|
77
src/fw/services/common/battery/battery_state.h
Normal file
77
src/fw/services/common/battery/battery_state.h
Normal file
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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/common/new_timer/new_timer.h"
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// Handles all battery-related driver communication, filters out events
|
||||
|
||||
//! @addtogroup Foundation
|
||||
//! @{
|
||||
//! @addtogroup Battery Battery
|
||||
//! \brief Functions related to getting the battery status
|
||||
//!
|
||||
//! This module contains the functions necessary to find the current charge status.
|
||||
//! @note Battery charge state is a complex topic; our modelling of the charge
|
||||
//! that is exposed by these functions represents a very simplified model based
|
||||
//! mostly on empirically derived charge and discharge voltage curves. As
|
||||
//! such, you should expect that the output will not have a high degree of
|
||||
//! accuracy.
|
||||
//! @{
|
||||
|
||||
//! Structure for retrieval of the battery charge state
|
||||
typedef struct {
|
||||
//! A percentage (0-100) of how full the battery is
|
||||
uint8_t charge_percent;
|
||||
//! True if the battery is currently being charged. False if not.
|
||||
bool is_charging;
|
||||
//! True if the charger cable is connected. False if not.
|
||||
bool is_plugged;
|
||||
} BatteryChargeState;
|
||||
|
||||
//! @internal
|
||||
//! Structure for retrieval of the exact battery charge state
|
||||
typedef struct {
|
||||
//! The battery's percentage as a ratio32
|
||||
uint32_t charge_percent;
|
||||
//! WARNING: This maps to @see battery_charge_controller_thinks_we_are_charging as opposed to
|
||||
//! the user-facing defintion of whether we're charging (100% battery).
|
||||
bool is_charging;
|
||||
bool is_plugged;
|
||||
} PreciseBatteryChargeState;
|
||||
|
||||
//! Function to get the current battery charge state
|
||||
//! @returns a \ref BatteryChargeState struct with the current charge state
|
||||
BatteryChargeState battery_get_charge_state(void);
|
||||
|
||||
//! @}
|
||||
//! @}
|
||||
|
||||
void battery_state_force_update(void);
|
||||
|
||||
void battery_state_init(void);
|
||||
|
||||
void battery_state_handle_connection_event(bool is_connected);
|
||||
|
||||
void battery_state_reset_filter(void);
|
||||
|
||||
// Get the last recorded voltage
|
||||
uint16_t battery_state_get_voltage(void);
|
||||
|
||||
// For unit tests
|
||||
TimerID battery_state_get_periodic_timer_id(void);
|
69
src/fw/services/common/bluetooth/ble_root_keys.c
Normal file
69
src/fw/services/common/bluetooth/ble_root_keys.c
Normal file
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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/rng.h"
|
||||
#include "services/common/bluetooth/ble_root_keys.h"
|
||||
#include "services/common/bluetooth/bluetooth_persistent_storage.h"
|
||||
#include "system/hexdump.h"
|
||||
#include "system/logging.h"
|
||||
|
||||
#include <bluetooth/sm_types.h>
|
||||
|
||||
static void prv_generate_root_keys(SM128BitKey *keys_out) {
|
||||
uint8_t *rand_buffer = (uint8_t *)keys_out;
|
||||
|
||||
int tries_left = 20;
|
||||
// rng_rand generates only 4 bytes of random data at a time. Loop to fill up the whole array:
|
||||
for (uint32_t i = 0; i < ((sizeof(SM128BitKey) * SMRootKeyTypeNum) / sizeof(uint32_t)); ++i) {
|
||||
while (tries_left) {
|
||||
const bool success = rng_rand((uint32_t *) &rand_buffer[i * sizeof(uint32_t)]);
|
||||
if (success) {
|
||||
break;
|
||||
}
|
||||
--tries_left;
|
||||
}
|
||||
}
|
||||
if (tries_left == 0) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "rng_rand() failed too many times, falling back to rand()");
|
||||
// Fall back to rand():
|
||||
for (uint32_t i = 0; i < (sizeof(SM128BitKey) * SMRootKeyTypeNum); ++i) {
|
||||
rand_buffer[i] = rand();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ble_root_keys_get_and_generate_if_needed(SM128BitKey *keys_out) {
|
||||
SM128BitKey *enc_key = &keys_out[SMRootKeyTypeEncryption];
|
||||
SM128BitKey *id_key = &keys_out[SMRootKeyTypeIdentity];
|
||||
|
||||
bool is_existing = false;
|
||||
if (bt_persistent_storage_get_root_key(SMRootKeyTypeIdentity, id_key) &&
|
||||
bt_persistent_storage_get_root_key(SMRootKeyTypeEncryption, enc_key)) {
|
||||
is_existing = true;
|
||||
goto finally;
|
||||
}
|
||||
|
||||
prv_generate_root_keys(keys_out);
|
||||
|
||||
finally:
|
||||
#ifndef RELEASE
|
||||
PBL_LOG(LOG_LEVEL_INFO, "BLE Root Keys (existing=%u):", is_existing);
|
||||
PBL_HEXDUMP(LOG_LEVEL_INFO, (const uint8_t *)keys_out, 2 * sizeof(SM128BitKey));
|
||||
#endif
|
||||
if (!is_existing) {
|
||||
bt_persistent_storage_set_root_keys(keys_out);
|
||||
}
|
||||
}
|
23
src/fw/services/common/bluetooth/ble_root_keys.h
Normal file
23
src/fw/services/common/bluetooth/ble_root_keys.h
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <bluetooth/sm_types.h>
|
||||
|
||||
//! @param[out] keys_out Array of at least SMRootKeyTypeNum elements in which the root keys should
|
||||
//! be copied.
|
||||
void ble_root_keys_get_and_generate_if_needed(SM128BitKey *keys_out);
|
286
src/fw/services/common/bluetooth/bluetooth_ctl.c
Normal file
286
src/fw/services/common/bluetooth/bluetooth_ctl.c
Normal file
|
@ -0,0 +1,286 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#define FILE_LOG_COLOR LOG_COLOR_BLUE
|
||||
#include "system/logging.h"
|
||||
|
||||
#include "console/dbgserial.h"
|
||||
#include "comm/ble/gap_le.h"
|
||||
#include "comm/ble/gatt_client_subscriptions.h"
|
||||
#include "drivers/clocksource.h"
|
||||
|
||||
#include "kernel/events.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "kernel/util/stop.h"
|
||||
#include "pebble_errors.h"
|
||||
#include "services/common/analytics/analytics.h"
|
||||
#include "services/common/bluetooth/ble_root_keys.h"
|
||||
#include "services/common/bluetooth/bluetooth_ctl.h"
|
||||
#include "services/common/bluetooth/bluetooth_persistent_storage.h"
|
||||
#include "services/common/bluetooth/dis.h"
|
||||
#include "services/common/bluetooth/local_addr.h"
|
||||
#include "services/common/bluetooth/local_id.h"
|
||||
#include "services/common/bluetooth/pairability.h"
|
||||
#include "services/common/regular_timer.h"
|
||||
#include "services/common/system_task.h"
|
||||
#include "services/normal/bluetooth/ble_hrm.h"
|
||||
#include "os/mutex.h"
|
||||
|
||||
#include <bluetooth/init.h>
|
||||
|
||||
#include <string.h>
|
||||
|
||||
static bool s_comm_initialized = false;
|
||||
static bool s_comm_airplane_mode_on = false;
|
||||
static bool s_comm_enabled = false;
|
||||
static bool s_comm_is_running = false;
|
||||
static bool s_comm_state_change_eval_is_scheduled;
|
||||
static BtCtlModeOverride s_comm_override = BtCtlModeOverrideNone;
|
||||
static PebbleMutex *s_comm_state_change_mutex;
|
||||
|
||||
bool bt_ctl_is_airplane_mode_on(void) {
|
||||
return s_comm_airplane_mode_on;
|
||||
}
|
||||
|
||||
bool bt_ctl_is_bluetooth_active(void) {
|
||||
if (s_comm_enabled) {
|
||||
if (s_comm_override == BtCtlModeOverrideRun) {
|
||||
return true;
|
||||
} else if (s_comm_override == BtCtlModeOverrideNone && !s_comm_airplane_mode_on) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool bt_ctl_is_bluetooth_running(void) {
|
||||
return s_comm_is_running;
|
||||
}
|
||||
|
||||
static void prv_put_disconnection_event(void) {
|
||||
PebbleEvent event = (PebbleEvent) {
|
||||
.type = PEBBLE_BT_CONNECTION_EVENT,
|
||||
.bluetooth.connection = {
|
||||
.is_ble = true,
|
||||
.state = PebbleBluetoothConnectionEventStateDisconnected,
|
||||
}
|
||||
};
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "New BT Conn change event, We are now disconnected");
|
||||
event_put(&event);
|
||||
}
|
||||
|
||||
static void prv_comm_start(void) {
|
||||
if (s_comm_is_running) {
|
||||
return;
|
||||
}
|
||||
stop_mode_disable(InhibitorCommMode);
|
||||
// Heap allocated to reduce stack usage
|
||||
BTDriverConfig *config = kernel_zalloc_check(sizeof(BTDriverConfig));
|
||||
ble_root_keys_get_and_generate_if_needed(config->root_keys);
|
||||
dis_get_info(&config->dis_info);
|
||||
#if CAPABILITY_HAS_BUILTIN_HRM
|
||||
config->is_hrm_supported_and_enabled = ble_hrm_is_supported_and_enabled();
|
||||
PBL_LOG(LOG_LEVEL_INFO, "BLE HRM sharing prefs: is_enabled=%u",
|
||||
config->is_hrm_supported_and_enabled);
|
||||
#endif
|
||||
s_comm_is_running = bt_driver_start(config);
|
||||
kernel_free(config);
|
||||
|
||||
if (s_comm_is_running) {
|
||||
bt_local_addr_init();
|
||||
bt_persistent_storage_register_existing_ble_bondings();
|
||||
gap_le_init();
|
||||
bt_local_id_configure_driver();
|
||||
#if CAPABILITY_HAS_BUILTIN_HRM
|
||||
ble_hrm_init();
|
||||
#endif
|
||||
bt_pairability_init();
|
||||
analytics_stopwatch_stop(ANALYTICS_DEVICE_METRIC_BT_OFF_TIME);
|
||||
} else {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "BT driver failed to start!");
|
||||
// FIXME: PBL-36163 -- handle this better
|
||||
}
|
||||
|
||||
stop_mode_enable(InhibitorCommMode);
|
||||
}
|
||||
|
||||
static void prv_comm_stop(void) {
|
||||
if (!s_comm_is_running) {
|
||||
return;
|
||||
}
|
||||
stop_mode_disable(InhibitorCommMode);
|
||||
#if CAPABILITY_HAS_BUILTIN_HRM
|
||||
ble_hrm_deinit();
|
||||
#endif
|
||||
gap_le_deinit();
|
||||
|
||||
// Should be the last thing to happen that touches the Bluetooth controller directly
|
||||
bt_driver_stop();
|
||||
stop_mode_enable(InhibitorCommMode);
|
||||
analytics_stopwatch_start(ANALYTICS_DEVICE_METRIC_BT_OFF_TIME, AnalyticsClient_System);
|
||||
s_comm_is_running = false;
|
||||
|
||||
// This is a legacy event used to update the Settings app.
|
||||
prv_put_disconnection_event();
|
||||
}
|
||||
|
||||
static void prv_send_state_change_event(void) {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "----> Sending a BT state event");
|
||||
PebbleEvent event = {
|
||||
.type = PEBBLE_BT_STATE_EVENT,
|
||||
.bluetooth = {
|
||||
.state = {
|
||||
.airplane = s_comm_airplane_mode_on,
|
||||
.enabled = s_comm_enabled,
|
||||
.override = s_comm_override,
|
||||
},
|
||||
},
|
||||
};
|
||||
event_put(&event);
|
||||
}
|
||||
|
||||
static void prv_comm_state_change(void *context) {
|
||||
static bool s_first_run = true;
|
||||
mutex_lock(s_comm_state_change_mutex);
|
||||
s_comm_state_change_eval_is_scheduled = false;
|
||||
bool is_active_mode = bt_ctl_is_bluetooth_active();
|
||||
if (is_active_mode != s_comm_is_running) {
|
||||
if (is_active_mode) {
|
||||
prv_comm_start();
|
||||
} else {
|
||||
prv_comm_stop();
|
||||
}
|
||||
// Only send event if state changed successfully:
|
||||
if (is_active_mode == s_comm_is_running) {
|
||||
prv_send_state_change_event();
|
||||
}
|
||||
} else if (!s_comm_is_running && s_first_run) {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Shutting down the BT stack on boot");
|
||||
bt_driver_power_down_controller_on_boot();
|
||||
}
|
||||
|
||||
s_first_run = false;
|
||||
mutex_unlock(s_comm_state_change_mutex);
|
||||
}
|
||||
|
||||
void bt_ctl_set_enabled(bool enabled) {
|
||||
if (!s_comm_initialized) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Error: Bluetooth isn't initialized yet");
|
||||
return;
|
||||
}
|
||||
mutex_lock(s_comm_state_change_mutex);
|
||||
s_comm_enabled = enabled;
|
||||
mutex_unlock(s_comm_state_change_mutex);
|
||||
prv_comm_state_change(NULL);
|
||||
}
|
||||
|
||||
void bt_ctl_set_override_mode(BtCtlModeOverride override) {
|
||||
if (!s_comm_initialized) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Error: Bluetooth isn't initialized yet");
|
||||
return;
|
||||
}
|
||||
mutex_lock(s_comm_state_change_mutex);
|
||||
s_comm_override = override;
|
||||
mutex_unlock(s_comm_state_change_mutex);
|
||||
prv_comm_state_change(NULL);
|
||||
}
|
||||
|
||||
static void prv_track_quick_airplane_mode_toggles(bool is_airplane_mode_currently_on) {
|
||||
// Track when coming out of airplane mode and we've gone into airplane mode less than 30 secs ago:
|
||||
static RtcTicks s_airplane_mode_last_toggle_ticks;
|
||||
const RtcTicks now_ticks = rtc_get_ticks();
|
||||
const uint64_t max_interval_secs = 30;
|
||||
if (((now_ticks - s_airplane_mode_last_toggle_ticks) < (max_interval_secs * RTC_TICKS_HZ)) &&
|
||||
is_airplane_mode_currently_on) {
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Quick airplane mode toggle detected!");
|
||||
analytics_inc(ANALYTICS_DEVICE_METRIC_BT_AIRPLANE_MODE_QUICK_TOGGLE_COUNT,
|
||||
AnalyticsClient_System);
|
||||
}
|
||||
s_airplane_mode_last_toggle_ticks = now_ticks;
|
||||
}
|
||||
|
||||
void bt_ctl_set_airplane_mode_async(bool enabled) {
|
||||
if (!s_comm_initialized) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Error: Bluetooth isn't initialized yet");
|
||||
return;
|
||||
}
|
||||
mutex_lock(s_comm_state_change_mutex);
|
||||
prv_track_quick_airplane_mode_toggles(!enabled);
|
||||
bt_persistent_storage_set_airplane_mode_enabled(enabled);
|
||||
s_comm_airplane_mode_on = enabled;
|
||||
bool should_schedule_eval = false;
|
||||
if (!s_comm_state_change_eval_is_scheduled) {
|
||||
should_schedule_eval = true;
|
||||
s_comm_state_change_eval_is_scheduled = true;
|
||||
}
|
||||
mutex_unlock(s_comm_state_change_mutex);
|
||||
if (should_schedule_eval) {
|
||||
system_task_add_callback(prv_comm_state_change, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
void bt_ctl_init(void) {
|
||||
s_comm_state_change_mutex = mutex_create();
|
||||
|
||||
s_comm_airplane_mode_on = bt_persistent_storage_get_airplane_mode_enabled();
|
||||
s_comm_initialized = true;
|
||||
analytics_stopwatch_start(ANALYTICS_DEVICE_METRIC_BT_OFF_TIME, AnalyticsClient_System);
|
||||
|
||||
gatt_client_subscription_boot();
|
||||
}
|
||||
|
||||
static void prv_bt_ctl_reset_bluetooth_callback(void *context) {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Resetting Bluetooth");
|
||||
mutex_lock(s_comm_state_change_mutex);
|
||||
|
||||
bool was_already_running = s_comm_is_running;
|
||||
|
||||
prv_comm_stop();
|
||||
prv_comm_start();
|
||||
|
||||
// It's possible a reset was triggered because the stack failed to boot up
|
||||
// correctly in which case we have never generated an event about the stack
|
||||
// booting up. Don't bother sending events if we are just returning the stack
|
||||
// to the state it is already in
|
||||
if (!was_already_running && s_comm_is_running) {
|
||||
prv_send_state_change_event();
|
||||
}
|
||||
mutex_unlock(s_comm_state_change_mutex);
|
||||
}
|
||||
|
||||
void bt_ctl_reset_bluetooth(void) {
|
||||
if (bt_ctl_is_bluetooth_active()) {
|
||||
system_task_add_callback(prv_bt_ctl_reset_bluetooth_callback, NULL);
|
||||
} else {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Bluetooth is disabled, reset aborted");
|
||||
}
|
||||
}
|
||||
|
||||
void command_bt_airplane_mode(const char* new_mode) {
|
||||
// as tests run using command_bt_airplane_mode, will retain nomenclature
|
||||
// but work as override mode change
|
||||
BtCtlModeOverride override = BtCtlModeOverrideStop;
|
||||
if (strcmp(new_mode, "exit") == 0) {
|
||||
override = BtCtlModeOverrideNone;
|
||||
}
|
||||
bt_ctl_set_override_mode(override);
|
||||
bool new_state = bt_ctl_is_bluetooth_active();
|
||||
if (!new_state) {
|
||||
dbgserial_putstr("Entered airplane mode");
|
||||
} else {
|
||||
dbgserial_putstr("Left airplane mode");
|
||||
}
|
||||
}
|
52
src/fw/services/common/bluetooth/bluetooth_ctl.h
Normal file
52
src/fw/services/common/bluetooth/bluetooth_ctl.h
Normal file
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 {
|
||||
BtCtlModeOverrideNone,
|
||||
BtCtlModeOverrideStop,
|
||||
BtCtlModeOverrideRun
|
||||
} BtCtlModeOverride;
|
||||
|
||||
void bt_ctl_init(void);
|
||||
|
||||
//! returns the airplane mode state
|
||||
bool bt_ctl_is_airplane_mode_on(void);
|
||||
|
||||
//! Returns whether the bluetooth stack is supposed to be up and running (but might not because it's
|
||||
//! still starting or in the middle of resetting).
|
||||
bool bt_ctl_is_bluetooth_active(void);
|
||||
|
||||
//! Returns whether the bluetooth stack is up and running or not.
|
||||
bool bt_ctl_is_bluetooth_running(void);
|
||||
|
||||
// The following three functions are used for setting the flags that define the state of
|
||||
// the bluetooth stack.
|
||||
|
||||
//! Sets the airplane mode flag. The flag is persisted across reboots
|
||||
void bt_ctl_set_airplane_mode_async(bool enabled);
|
||||
|
||||
//! Sets enable flag (used by the runlevel system).
|
||||
void bt_ctl_set_enabled(bool enabled);
|
||||
|
||||
//! Sets the override mode used to stop and start the bluetooth independent of the airplane mode.
|
||||
void bt_ctl_set_override_mode(BtCtlModeOverride override);
|
||||
|
||||
//! Reset bluetoosh using sequential calls to comm_stop() and comm_start()
|
||||
void bt_ctl_reset_bluetooth(void);
|
203
src/fw/services/common/bluetooth/bluetooth_persistent_storage.h
Normal file
203
src/fw/services/common/bluetooth/bluetooth_persistent_storage.h
Normal file
|
@ -0,0 +1,203 @@
|
|||
/*
|
||||
* 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/common/comm_session/session_remote_version.h"
|
||||
|
||||
#include <bluetooth/bluetooth_types.h>
|
||||
#include <bluetooth/id.h>
|
||||
#include <bluetooth/sm_types.h>
|
||||
|
||||
//! @file bluetooth_persistent_storage.h
|
||||
//! Future file-based database for Bluetooth related credentials, cached data, etc.
|
||||
//! The idea is to replace the deprecated, registry-based "remote_prefs.c" and "known_device_list.c"
|
||||
//! For now this module contains temporary implementations that use the legacy registry.
|
||||
|
||||
typedef enum {
|
||||
BtPersistBondingOpInvalid = -1,
|
||||
BtPersistBondingOpDidAdd,
|
||||
BtPersistBondingOpDidChange,
|
||||
BtPersistBondingOpWillDelete,
|
||||
} BtPersistBondingOp;
|
||||
|
||||
typedef enum {
|
||||
BtPersistBondingTypeBTClassic,
|
||||
BtPersistBondingTypeBLE,
|
||||
BtPersistBondingNumTypes
|
||||
} BtPersistBondingType;
|
||||
|
||||
|
||||
//! Signature of function that handles changes in the pairing database
|
||||
typedef void (*BtPersistBondingChangeHandler)(BTBondingID affected_bonding,
|
||||
BtPersistBondingOp operation);
|
||||
|
||||
typedef void (*BtPersistBondingDBEachBLE)(BTDeviceInternal *device, SMIdentityResolvingKey *irk,
|
||||
const char *name, BTBondingID *id, void *context);
|
||||
|
||||
typedef void (*BtPersistBondingDBEachBTClassic)(BTDeviceAddress *addr, SM128BitKey *link_key,
|
||||
const char *name, uint8_t *platform_bits, void *context);
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//! BLE Pairing Info
|
||||
|
||||
bool bt_persistent_storage_has_pinned_ble_pairings(void);
|
||||
|
||||
bool bt_persistent_storage_set_ble_pinned_address(const BTDeviceAddress *address);
|
||||
|
||||
bool bt_persistent_storage_get_ble_pinned_address(BTDeviceAddress *address_out);
|
||||
|
||||
BTBondingID bt_persistent_storage_store_ble_pairing(const SMPairingInfo *pairing_info,
|
||||
bool is_gateway, const char *device_name,
|
||||
bool requires_address_pinning,
|
||||
uint8_t flags);
|
||||
|
||||
bool bt_persistent_storage_update_ble_device_name(BTBondingID bonding, const char *device_name);
|
||||
|
||||
void bt_persistent_storage_delete_ble_pairing_by_id(BTBondingID);
|
||||
|
||||
bool bt_persistent_storage_get_ble_pairing_by_id(BTBondingID bonding,
|
||||
SMIdentityResolvingKey *IRK_out,
|
||||
BTDeviceInternal *device_out,
|
||||
char name[BT_DEVICE_NAME_BUFFER_SIZE]);
|
||||
|
||||
bool bt_persistent_storage_get_ble_pairing_by_addr(const BTDeviceInternal *device,
|
||||
SMIdentityResolvingKey *IRK_out,
|
||||
char name_out[BT_DEVICE_NAME_BUFFER_SIZE]);
|
||||
|
||||
//! Returns the first ANCS supported bonding that is found
|
||||
//! The case of having multiple supported ANCS bondings isn't handled well yet.
|
||||
//! When this happens this could easily be changed to a for_each_ancs_supported_bonding(cb)
|
||||
BTBondingID bt_persistent_storage_get_ble_ancs_bonding(void);
|
||||
|
||||
//! Returns true if the bondings is BLE and supports ANCS
|
||||
bool bt_persistent_storage_is_ble_ancs_bonding(BTBondingID bonding);
|
||||
|
||||
//! Returns true if there exists a BLE bonding which supports ANCS
|
||||
bool bt_persistent_storage_has_ble_ancs_bonding(void);
|
||||
|
||||
//! Returns true if the active gateway uses BLE
|
||||
//! [PG]: This will currently always return false until PPoGATT is supported
|
||||
bool bt_persistent_storage_has_active_ble_gateway_bonding(void);
|
||||
|
||||
//! Runs the callback for each BLE pairing
|
||||
//! The callback is NOT allowed to aquire the bt_lock() (or we could deadlock).
|
||||
void bt_persistent_storage_for_each_ble_pairing(BtPersistBondingDBEachBLE cb, void *context);
|
||||
|
||||
//! Registers all the existing BLE bondings with the BT driver lib.
|
||||
void bt_persistent_storage_register_existing_ble_bondings(void);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//! BT Classic Pairing Info
|
||||
|
||||
BTBondingID bt_persistent_storage_store_bt_classic_pairing(BTDeviceAddress *address,
|
||||
SM128BitKey *key, char *name,
|
||||
uint8_t *platform_bits);
|
||||
|
||||
void bt_persistent_storage_delete_bt_classic_pairing_by_id(BTBondingID bonding);
|
||||
|
||||
void bt_persistent_storage_delete_bt_classic_pairing_by_addr(const BTDeviceAddress *bd_addr);
|
||||
|
||||
bool bt_persistent_storage_get_bt_classic_pairing_by_id(BTBondingID bonding,
|
||||
BTDeviceAddress *address_out,
|
||||
SM128BitKey *link_key_out,
|
||||
char *name_out,
|
||||
uint8_t *platform_bits_out);
|
||||
|
||||
BTBondingID bt_persistent_storage_get_bt_classic_pairing_by_addr(BTDeviceAddress *addr_in,
|
||||
SM128BitKey *link_key_out,
|
||||
char *name_out,
|
||||
uint8_t *platform_bits_out);
|
||||
|
||||
//! Returns true if the active gateway uses BT Classic
|
||||
bool bt_persistent_storage_has_active_bt_classic_gateway_bonding(void);
|
||||
|
||||
//! Runs the callback for each BT Classic pairing
|
||||
//! The callback is NOT allowed to aquire the bt_lock() (or we could deadlock).
|
||||
void bt_persistent_storage_for_each_bt_classic_pairing(BtPersistBondingDBEachBTClassic cb,
|
||||
void *context);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//! Local Device Info
|
||||
|
||||
//! Updates the active gateway (the gateway which implements PP)
|
||||
//! This bonding is used for BT Classic reconnection as well
|
||||
//! @param bonding The desired active gateway
|
||||
void bt_persistent_storage_set_active_gateway(BTBondingID bonding);
|
||||
|
||||
//! Returns false if no active gateway exists, true if one does exist
|
||||
//! bonding_out and type_out are only valid when this function returns true;
|
||||
bool bt_persistent_storage_get_active_gateway(BTBondingID *bonding_out,
|
||||
BtPersistBondingType *type_out);
|
||||
|
||||
//! Returns true when the active gateway is changed until a sync happens
|
||||
bool bt_persistent_storage_is_unfaithful(void);
|
||||
|
||||
//! Marks the device as being unfaithful
|
||||
void bt_persistent_storage_set_unfaithful(bool is_unfaithful);
|
||||
|
||||
//! Copies the BLE Encryption Root (ER) or Identity Root (IR) keys out of storage
|
||||
//! @param key_out Storage into which ER or IR should be copied.
|
||||
//! @param key_type The type of key to copy
|
||||
//! @return true if ER and IR are copied, false if there are no keys have been found to copy.
|
||||
bool bt_persistent_storage_get_root_key(SMRootKeyType key_type, SM128BitKey *key_out);
|
||||
|
||||
//! Stores new BLE Encryption Root (ER) and Identity Root (IR) keys
|
||||
void bt_persistent_storage_set_root_keys(SM128BitKey *keys_in);
|
||||
|
||||
//! @param local_device_name_out Storage for the local device name.
|
||||
//! @param max_size Size of the local_device_name_out buffer
|
||||
//! @return true if there is a valid local device name stored, otherwise false (a zero-length string
|
||||
bool bt_persistent_storage_get_local_device_name(char *local_device_name_out, size_t max_size);
|
||||
|
||||
//! Stores the customized local device name
|
||||
//! @param local_device_name The device name to store
|
||||
//! @param size The size of the string
|
||||
void bt_persistent_storage_set_local_device_name(char *local_device_name, size_t max_size);
|
||||
|
||||
//! Retrieve the airplane mode setting
|
||||
//! @return the stored airplane mode flag
|
||||
bool bt_persistent_storage_get_airplane_mode_enabled(void);
|
||||
|
||||
//! Store the airplane mode setting
|
||||
//! @param the airplane mode state to be saved
|
||||
void bt_persistent_storage_set_airplane_mode_enabled(bool enable);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//! Remote Device Info
|
||||
|
||||
//! Retrieve the most recent system session capabilities
|
||||
//! @param capabilities_out Storage for system session capabilities
|
||||
//! @note It's preferable to use \ref comm_session_get_capabilities when possible
|
||||
void bt_persistent_storage_get_cached_system_capabilities(
|
||||
PebbleProtocolCapabilities *capabilities_out);
|
||||
|
||||
//! Store the most recent system session capabilities
|
||||
//! @param capabilities The capability flags to be saved (cache will be cleared if NULL)
|
||||
void bt_persistent_storage_set_cached_system_capabilities(
|
||||
const PebbleProtocolCapabilities *capabilities);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//! Common
|
||||
|
||||
void bt_persistent_storage_init(void);
|
||||
|
||||
//! This will not delete the local device info, only pairings
|
||||
void bt_persistent_storage_delete_all_pairings(void);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//! Unit testing
|
||||
int bt_persistent_storage_get_raw_data(const void *key, size_t key_len,
|
||||
void *data_out, size_t buf_len);
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* 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_persistent_storage_debug.h"
|
||||
|
||||
#include "console/prompt.h"
|
||||
#include "services/common/shared_prf_storage/shared_prf_storage_debug.h"
|
||||
#include "system/hexdump.h"
|
||||
#include "system/logging.h"
|
||||
#include "util/string.h"
|
||||
|
||||
#include <bluetooth/bluetooth_types.h>
|
||||
#include <bluetooth/features.h>
|
||||
#include <bluetooth/sm_types.h>
|
||||
#include <btutil/bt_device.h>
|
||||
#include <btutil/sm_util.h>
|
||||
|
||||
//
|
||||
// Strictly for debug. Pretty-prints most of the pairing information saved
|
||||
// in the gap bonding db and shared PRF.
|
||||
//
|
||||
|
||||
void bluetooth_persistent_storage_debug_dump_ble_pairing_info(
|
||||
char *display_buf, const SMPairingInfo *info) {
|
||||
prompt_send_response(" Local Encryption Info: ");
|
||||
PBL_HEXDUMP_D_PROMPT(LOG_LEVEL_DEBUG,
|
||||
(uint8_t *)&info->local_encryption_info,
|
||||
sizeof(info->local_encryption_info));
|
||||
|
||||
prompt_send_response(" Remote Encryption Info: ");
|
||||
PBL_HEXDUMP_D_PROMPT(LOG_LEVEL_DEBUG,
|
||||
(uint8_t *)&info->remote_encryption_info,
|
||||
sizeof(info->remote_encryption_info));
|
||||
|
||||
prompt_send_response(" SMIdentityResolvingKey: ");
|
||||
PBL_HEXDUMP_D_PROMPT(LOG_LEVEL_DEBUG,
|
||||
(uint8_t *)&info->irk,
|
||||
sizeof(info->irk));
|
||||
|
||||
prompt_send_response(" BTDeviceInternal: ");
|
||||
PBL_HEXDUMP_D_PROMPT(LOG_LEVEL_DEBUG,
|
||||
(uint8_t *)&info->identity,
|
||||
sizeof(BTDeviceInternal));
|
||||
|
||||
prompt_send_response(" SMConnectionSignatureResolvingKey: ");
|
||||
PBL_HEXDUMP_D_PROMPT(LOG_LEVEL_DEBUG,
|
||||
(uint8_t *)&info->csrk,
|
||||
sizeof(SMConnectionSignatureResolvingKey));
|
||||
|
||||
prompt_send_response_fmt(display_buf, DISPLAY_BUF_LEN,
|
||||
" local encryption valid: %s\n"
|
||||
" remote encryption valid: %s\n"
|
||||
" remote identity valid: %s\n"
|
||||
" remote signature valid: %s\n",
|
||||
bool_to_str(info->is_local_encryption_info_valid),
|
||||
bool_to_str(info->is_remote_encryption_info_valid),
|
||||
bool_to_str(info->is_remote_encryption_info_valid),
|
||||
bool_to_str(info->is_remote_signing_info_valid));
|
||||
}
|
||||
|
||||
void bluetooth_persistent_storage_debug_dump_classic_pairing_info(
|
||||
char *display_buf, BTDeviceAddress *addr, char *device_name, SM128BitKey *link_key,
|
||||
uint8_t platform_bits) {
|
||||
prompt_send_response(" Link Key:");
|
||||
PBL_HEXDUMP_D_PROMPT(LOG_LEVEL_DEBUG, (uint8_t *)link_key, sizeof(SM128BitKey));
|
||||
prompt_send_response_fmt(display_buf, DISPLAY_BUF_LEN, " BT ADDR: " BD_ADDR_FMT,
|
||||
BT_DEVICE_ADDRESS_XPLODE(*addr));
|
||||
prompt_send_response_fmt(display_buf, DISPLAY_BUF_LEN, " Name: %s",
|
||||
device_name);
|
||||
prompt_send_response_fmt(display_buf, DISPLAY_BUF_LEN, " Platform Bits: 0x%x",
|
||||
(int)platform_bits);
|
||||
}
|
||||
|
||||
|
||||
void bluetooth_persistent_storage_debug_dump_root_keys(SM128BitKey *irk, SM128BitKey *erk) {
|
||||
prompt_send_response("Root Key hexdumps:");
|
||||
|
||||
prompt_send_response(" IRK:");
|
||||
if (irk) {
|
||||
PBL_HEXDUMP_D_PROMPT(LOG_LEVEL_DEBUG, (uint8_t *)irk, sizeof(SM128BitKey));
|
||||
} else {
|
||||
prompt_send_response(" None");
|
||||
};
|
||||
|
||||
prompt_send_response(" ERK:");
|
||||
if (erk) {
|
||||
PBL_HEXDUMP_D_PROMPT(LOG_LEVEL_DEBUG, (uint8_t *)erk, sizeof(SM128BitKey));
|
||||
} else {
|
||||
prompt_send_response(" None");
|
||||
};
|
||||
}
|
||||
|
||||
extern void bluetooth_persistent_storage_dump_contents(void);
|
||||
void command_gapdb_dump(void) {
|
||||
#if !RECOVERY_FW
|
||||
bluetooth_persistent_storage_dump_contents();
|
||||
#endif
|
||||
shared_prf_storage_dump_contents();
|
||||
}
|
|
@ -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 <bluetooth/bluetooth_types.h>
|
||||
#include <bluetooth/sm_types.h>
|
||||
#include <btutil/sm_util.h>
|
||||
|
||||
#define DISPLAY_BUF_LEN 160
|
||||
|
||||
void bluetooth_persistent_storage_debug_dump_ble_pairing_info(
|
||||
char *display_buf, const SMPairingInfo *info);
|
||||
|
||||
void bluetooth_persistent_storage_debug_dump_classic_pairing_info(
|
||||
char *display_buf, BTDeviceAddress *addr, char *device_name, SM128BitKey *link_key,
|
||||
uint8_t platform_bits);
|
||||
|
||||
void bluetooth_persistent_storage_debug_dump_root_keys(SM128BitKey *irk, SM128BitKey *erk);
|
134
src/fw/services/common/bluetooth/bluetooth_prompt.c
Normal file
134
src/fw/services/common/bluetooth/bluetooth_prompt.c
Normal file
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* 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 "comm/ble/gap_le_connection.h"
|
||||
#include "comm/bt_lock.h"
|
||||
|
||||
#include "console/prompt.h"
|
||||
#include "services/common/bluetooth/bluetooth_ctl.h"
|
||||
#include "services/common/bluetooth/bluetooth_persistent_storage.h"
|
||||
#include "services/common/bluetooth/bt_compliance_tests.h"
|
||||
#include "services/common/bluetooth/local_id.h"
|
||||
#include "services/common/bluetooth/pairability.h"
|
||||
#include "services/common/shared_prf_storage/shared_prf_storage.h"
|
||||
#include "util/string.h"
|
||||
|
||||
#include <bluetooth/bluetooth_types.h>
|
||||
#include <bluetooth/bt_test.h>
|
||||
#include <bluetooth/classic_connect.h>
|
||||
#include <bluetooth/id.h>
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
void command_bt_print_mac(void) {
|
||||
char addr_hex_str[BT_ADDR_FMT_BUFFER_SIZE_BYTES];
|
||||
bt_local_id_copy_address_hex_string(addr_hex_str);
|
||||
prompt_send_response(addr_hex_str);
|
||||
}
|
||||
|
||||
//! Overrides the BD ADDR of the Bluetooth controller for test automation purposes
|
||||
//! @param bd_addr String of 12 hex characters (6 bytes) of the Bluetooth device address
|
||||
//! @note To undo the change, call this with all zeroes.
|
||||
//! @note The change will take effect when the Bluetooth is (re)enabled.
|
||||
void command_bt_set_addr(const char *bd_addr_str) {
|
||||
char buffer[32];
|
||||
BTDeviceAddress bd_addr;
|
||||
if (convert_bt_addr_hex_str_to_bd_addr(bd_addr_str, (uint8_t *) &bd_addr, sizeof(bd_addr))) {
|
||||
bt_driver_test_set_spoof_address(&bd_addr);
|
||||
prompt_send_response_fmt(buffer, 32, BT_DEVICE_ADDRESS_FMT, BT_DEVICE_ADDRESS_XPLODE(bd_addr));
|
||||
} else {
|
||||
prompt_send_response("?");
|
||||
}
|
||||
}
|
||||
|
||||
//! @param bt_name A custom Bluetooth device name.
|
||||
void command_bt_set_name(const char *bt_name) {
|
||||
bt_local_id_set_device_name(bt_name);
|
||||
}
|
||||
|
||||
// BT FCC tests
|
||||
void command_bt_test_start(void) {
|
||||
// take down the BT stack and put the OS in a mode where it will not
|
||||
// interfere with the BT testing.
|
||||
bt_test_start();
|
||||
}
|
||||
|
||||
void command_bt_test_stop(void) {
|
||||
// restore the watch to normal operation
|
||||
bt_test_stop();
|
||||
}
|
||||
|
||||
void command_bt_test_hci_passthrough(void) {
|
||||
bt_test_enter_hci_passthrough();
|
||||
}
|
||||
|
||||
void command_bt_test_bt_sig_rf_mode(void) {
|
||||
if (bt_test_bt_sig_rf_test_mode()) {
|
||||
prompt_send_response("BT SIG RF Test Mode Enabled");
|
||||
} else {
|
||||
prompt_send_response("Failed to enter BT SIG RF Test Mode");
|
||||
}
|
||||
}
|
||||
|
||||
void command_bt_prefs_wipe(void) {
|
||||
bt_driver_classic_disconnect(NULL);
|
||||
bt_persistent_storage_delete_all_pairings();
|
||||
}
|
||||
|
||||
void command_bt_sprf_nuke(void) {
|
||||
shared_prf_storage_wipe_all();
|
||||
#if RECOVERY_FW
|
||||
// Reset system to get caches (in s_intents, s_connections and controller-side caches) in sync.
|
||||
extern void factory_reset_set_reason_and_reset(void);
|
||||
factory_reset_set_reason_and_reset();
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef RECOVERY_FW
|
||||
void command_bt_status(void) {
|
||||
char buffer[64];
|
||||
|
||||
prompt_send_response_fmt(buffer, sizeof(buffer), "Alive: %s",
|
||||
bt_ctl_is_bluetooth_running() ? "yes" : "no");
|
||||
|
||||
const char *prefix = "BT Chip Info: ";
|
||||
size_t prefix_length = strlen(prefix);
|
||||
strncpy(buffer, prefix, sizeof(buffer));
|
||||
bt_driver_id_copy_chip_info_string(buffer + prefix_length,
|
||||
sizeof(buffer) - prefix_length);
|
||||
prompt_send_response(buffer);
|
||||
|
||||
char name[BT_DEVICE_NAME_BUFFER_SIZE];
|
||||
bt_lock();
|
||||
bool connected = bt_driver_classic_copy_connected_device_name(name);
|
||||
if (!connected) {
|
||||
// Try LE:
|
||||
GAPLEConnection *connection = gap_le_connection_any();
|
||||
if (connection) {
|
||||
const char *device_name = connection->device_name ?: "<Unknown>";
|
||||
strncpy(name, device_name, BT_DEVICE_NAME_BUFFER_SIZE);
|
||||
name[BT_DEVICE_NAME_BUFFER_SIZE - 1] = '\0';
|
||||
connected = true;
|
||||
}
|
||||
}
|
||||
bt_unlock();
|
||||
|
||||
prompt_send_response_fmt(buffer, sizeof(buffer), "Connected: %s", connected ? "yes" : "no");
|
||||
if (connected) {
|
||||
prompt_send_response_fmt(buffer, sizeof(buffer), "Device: %s", name);
|
||||
}
|
||||
}
|
||||
#endif // RECOVERY_FW
|
67
src/fw/services/common/bluetooth/bonding.c
Normal file
67
src/fw/services/common/bluetooth/bonding.c
Normal file
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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 "comm/ble/gap_le_connection.h"
|
||||
#include "comm/ble/gap_le_device_name.h"
|
||||
#include "comm/bt_lock.h"
|
||||
|
||||
#include "services/common/bluetooth/bluetooth_persistent_storage.h"
|
||||
#include "services/common/bluetooth/local_addr.h"
|
||||
#include "system/logging.h"
|
||||
|
||||
#include <bluetooth/bonding_sync.h>
|
||||
#include <bluetooth/bluetooth_types.h>
|
||||
#include <bluetooth/sm_types.h>
|
||||
|
||||
void bt_driver_cb_handle_create_bonding(const BleBonding *bonding,
|
||||
const BTDeviceAddress *addr) {
|
||||
#if !defined(PLATFORM_TINTIN)
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Creating new bonding for "BT_DEVICE_ADDRESS_FMT,
|
||||
BT_DEVICE_ADDRESS_XPLODE(bonding->pairing_info.identity.address));
|
||||
#endif
|
||||
const bool should_pin_address = bonding->should_pin_address;
|
||||
if (should_pin_address) {
|
||||
bt_local_addr_pin(&bonding->pinned_address);
|
||||
}
|
||||
const uint8_t flags = bonding->flags;
|
||||
if (flags) {
|
||||
PBL_LOG(LOG_LEVEL_INFO, "flags: 0x02%x", flags);
|
||||
}
|
||||
BTBondingID bonding_id = bt_persistent_storage_store_ble_pairing(&bonding->pairing_info,
|
||||
bonding->is_gateway, NULL,
|
||||
should_pin_address,
|
||||
flags);
|
||||
bt_lock();
|
||||
{
|
||||
GAPLEConnection *connection = gap_le_connection_by_addr(addr);
|
||||
if (connection) {
|
||||
// Associate the connection with the bonding:
|
||||
connection->bonding_id = bonding_id;
|
||||
connection->is_gateway = bonding->is_gateway;
|
||||
|
||||
if (!connection->is_gateway) {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "New bonding is not gateway?");
|
||||
}
|
||||
|
||||
// Request device name. iOS returns an "anonymized" device name before encryption, like
|
||||
// "iPhone" and only returns the real name i.e. "Martijn's iPhone" after encryption is set up.
|
||||
gap_le_device_name_request(&connection->device);
|
||||
} else {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Couldn't find connection for bonding!");
|
||||
}
|
||||
}
|
||||
bt_unlock();
|
||||
}
|
64
src/fw/services/common/bluetooth/bt_compliance_tests.c
Normal file
64
src/fw/services/common/bluetooth/bt_compliance_tests.c
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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 "console/console_internal.h"
|
||||
#include "console/prompt.h"
|
||||
|
||||
#include "services/common/bluetooth/bluetooth_ctl.h"
|
||||
#include "services/common/bluetooth/bt_compliance_tests.h"
|
||||
|
||||
#include "kernel/util/stop.h"
|
||||
|
||||
#include <bluetooth/bt_test.h>
|
||||
|
||||
static bool s_test_mode_enabled = false;
|
||||
|
||||
void bt_test_start(void) {
|
||||
if (s_test_mode_enabled) {
|
||||
prompt_send_response("Invalid operation: Run 'bt test stop' first");
|
||||
return;
|
||||
}
|
||||
s_test_mode_enabled = true;
|
||||
bt_ctl_set_override_mode(BtCtlModeOverrideStop);
|
||||
stop_mode_disable(InhibitorBluetooth);
|
||||
|
||||
bt_driver_test_start();
|
||||
}
|
||||
|
||||
bool bt_test_bt_sig_rf_test_mode(void) {
|
||||
return bt_driver_test_enter_rf_test_mode();
|
||||
}
|
||||
|
||||
void bt_test_enter_hci_passthrough(void) {
|
||||
// redirect all communications to the BT module
|
||||
serial_console_set_state(SERIAL_CONSOLE_STATE_HCI_PASSTHROUGH);
|
||||
|
||||
bt_driver_test_enter_hci_passthrough();
|
||||
}
|
||||
|
||||
void bt_test_stop(void) {
|
||||
if (!s_test_mode_enabled) {
|
||||
prompt_send_response("Invalid operation: Run 'bt test start' first");
|
||||
return;
|
||||
}
|
||||
|
||||
bt_driver_test_stop();
|
||||
stop_mode_enable(InhibitorBluetooth);
|
||||
|
||||
// Bring the normal BT stack back up - airplane mode makes this simple
|
||||
bt_ctl_set_override_mode(BtCtlModeOverrideNone);
|
||||
s_test_mode_enabled = false;
|
||||
}
|
26
src/fw/services/common/bluetooth/bt_compliance_tests.h
Normal file
26
src/fw/services/common/bluetooth/bt_compliance_tests.h
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
void bt_test_start(void);
|
||||
void bt_test_stop(void);
|
||||
|
||||
bool bt_test_bt_sig_rf_test_mode(void);
|
||||
void bt_test_enter_hci_passthrough(void);
|
67
src/fw/services/common/bluetooth/dis.c
Normal file
67
src/fw/services/common/bluetooth/dis.c
Normal file
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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/dis.h>
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "process_management/pebble_process_info.h"
|
||||
#include "mfg/mfg_info.h"
|
||||
#include "mfg/mfg_serials.h"
|
||||
#include "system/version.h"
|
||||
|
||||
#define MANUFACTURER_STR "Pebble Technology"
|
||||
|
||||
_Static_assert(MODEL_NUMBER_LEN >= MFG_HW_VERSION_SIZE + 1, "Size mismatch");
|
||||
_Static_assert(MANUFACTURER_LEN >= sizeof(MANUFACTURER_STR), "Size mismatch");
|
||||
_Static_assert(SERIAL_NUMBER_LEN >= MFG_SERIAL_NUMBER_SIZE + 1, "Size mismatch");
|
||||
_Static_assert(FW_REVISION_LEN >= sizeof(TINTIN_METADATA.version_tag), "Size mismatch");
|
||||
|
||||
static void prv_set_model_number(DisInfo *info) {
|
||||
mfg_info_get_hw_version(info->model_number, MODEL_NUMBER_LEN);
|
||||
}
|
||||
|
||||
static void prv_set_manufacturer_name(DisInfo *info) {
|
||||
strncpy(info->manufacturer, MANUFACTURER_STR, MANUFACTURER_LEN);
|
||||
}
|
||||
|
||||
static void prv_set_serial_number(DisInfo *info) {
|
||||
mfg_info_get_serialnumber(info->serial_number, SERIAL_NUMBER_LEN);
|
||||
}
|
||||
|
||||
static void prv_set_firmware_revision(DisInfo *info) {
|
||||
strncpy(info->fw_revision, (char*)TINTIN_METADATA.version_tag, FW_REVISION_LEN);
|
||||
}
|
||||
|
||||
static void prv_set_software_revision(DisInfo *info) {
|
||||
// Fmt: xx.xx\0
|
||||
char sdk_version[SW_REVISION_LEN];
|
||||
sniprintf(sdk_version, SW_REVISION_LEN, "%2u.%02u",
|
||||
PROCESS_INFO_CURRENT_SDK_VERSION_MAJOR,
|
||||
PROCESS_INFO_CURRENT_SDK_VERSION_MINOR);
|
||||
strncpy(info->sw_revision, sdk_version, SW_REVISION_LEN);
|
||||
}
|
||||
|
||||
void dis_get_info(DisInfo *info) {
|
||||
prv_set_model_number(info);
|
||||
prv_set_manufacturer_name(info);
|
||||
prv_set_serial_number(info);
|
||||
prv_set_firmware_revision(info);
|
||||
prv_set_software_revision(info);
|
||||
}
|
21
src/fw/services/common/bluetooth/dis.h
Normal file
21
src/fw/services/common/bluetooth/dis.h
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 struct DisInfo DisInfo;
|
||||
|
||||
void dis_get_info(DisInfo *info);
|
130
src/fw/services/common/bluetooth/local_addr.c
Normal file
130
src/fw/services/common/bluetooth/local_addr.c
Normal file
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* 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 "local_addr.h"
|
||||
|
||||
#include "comm/bt_lock.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
|
||||
#include <bluetooth/bluetooth_types.h>
|
||||
#include <bluetooth/id.h>
|
||||
#include <btutil/bt_device.h>
|
||||
|
||||
static uint32_t s_pra_cycling_pause_count;
|
||||
static BTDeviceAddress s_pinned_addr;
|
||||
static bool s_cycling_paused_due_to_dependent_bondings;
|
||||
|
||||
static void prv_allow_cycling(bool allow_cycling) {
|
||||
bt_driver_set_local_address(allow_cycling, allow_cycling ? NULL : &s_pinned_addr);
|
||||
}
|
||||
|
||||
void bt_local_addr_pause_cycling(void) {
|
||||
bt_lock();
|
||||
{
|
||||
if (s_pra_cycling_pause_count == 0) {
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Pausing address cycling (pinned_addr="BT_DEVICE_ADDRESS_FMT")",
|
||||
BT_DEVICE_ADDRESS_XPLODE(s_pinned_addr));
|
||||
prv_allow_cycling(false);
|
||||
}
|
||||
++s_pra_cycling_pause_count;
|
||||
}
|
||||
bt_unlock();
|
||||
}
|
||||
|
||||
void bt_local_addr_resume_cycling(void) {
|
||||
bt_lock();
|
||||
{
|
||||
PBL_ASSERTN(s_pra_cycling_pause_count);
|
||||
--s_pra_cycling_pause_count;
|
||||
if (s_pra_cycling_pause_count == 0) {
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Resuming address cycling (pinned_addr="BT_DEVICE_ADDRESS_FMT")",
|
||||
BT_DEVICE_ADDRESS_XPLODE(s_pinned_addr));
|
||||
prv_allow_cycling(true);
|
||||
}
|
||||
}
|
||||
bt_unlock();
|
||||
}
|
||||
|
||||
void bt_local_addr_pin(const BTDeviceAddress *addr) {
|
||||
// In a previous version of the code, the main FW would not know yet what address would be used
|
||||
// for pinning until the BT driver would give the address to pin when a pairing was added.
|
||||
// A single, persistent pinned address is now generated up front in bt_local_addr_init().
|
||||
// Getting the address back in this call from the BT driver currently only serves as a
|
||||
// consistency check.
|
||||
// It is possible that the addresses do not match in the following scenario:
|
||||
// 1. No bondings that require pinning present. Cycling address 'C' is used.
|
||||
// 2. Device A is connected.
|
||||
// 3. Become discoverable: cycling is requested to be paused at address 'P' but can't be granted
|
||||
// yet because device A is still connected.
|
||||
// 4. Device B connects (using 'C' as connection address)
|
||||
// 5. Device B requests pin + pairs => the remote bonding is stored with 'C' as key instead of 'P'
|
||||
// 6. We'll print here there's a mismatch.
|
||||
// 7. Once Device A & B disconnect, device B won't be able to recognize us because 'P' is used...
|
||||
|
||||
bt_lock();
|
||||
bool addresses_match = bt_device_address_equal(addr, &s_pinned_addr);
|
||||
bt_unlock();
|
||||
|
||||
PBL_LOG(LOG_LEVEL_INFO,
|
||||
"Requested to pin address to "BT_DEVICE_ADDRESS_FMT " match=%u",
|
||||
BT_DEVICE_ADDRESS_XPLODE_PTR(addr), addresses_match);
|
||||
}
|
||||
|
||||
void bt_local_addr_handle_bonding_change(BTBondingID bonding, BtPersistBondingOp op) {
|
||||
bool has_pinned_ble_pairings = bt_persistent_storage_has_pinned_ble_pairings();
|
||||
if (has_pinned_ble_pairings != s_cycling_paused_due_to_dependent_bondings) {
|
||||
if (has_pinned_ble_pairings) {
|
||||
bt_local_addr_pause_cycling();
|
||||
} else {
|
||||
bt_local_addr_resume_cycling();
|
||||
}
|
||||
s_cycling_paused_due_to_dependent_bondings = has_pinned_ble_pairings;
|
||||
}
|
||||
}
|
||||
|
||||
void bt_local_addr_init(void) {
|
||||
s_pra_cycling_pause_count = 0;
|
||||
s_cycling_paused_due_to_dependent_bondings = false;
|
||||
|
||||
// Load pinned address from settings file or generate one if it hasn't happened before:
|
||||
if (!bt_persistent_storage_get_ble_pinned_address(&s_pinned_addr)) {
|
||||
if (bt_driver_id_generate_private_resolvable_address(&s_pinned_addr)) {
|
||||
bt_persistent_storage_set_ble_pinned_address(&s_pinned_addr);
|
||||
} else {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Failed to generate PRA... :(");
|
||||
}
|
||||
}
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Pinned address: " BT_DEVICE_ADDRESS_FMT,
|
||||
BT_DEVICE_ADDRESS_XPLODE(s_pinned_addr));
|
||||
|
||||
if (bt_persistent_storage_has_pinned_ble_pairings()) {
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Bonding that requires address pinning exists, applying pinned addr!");
|
||||
bt_local_addr_pause_cycling();
|
||||
s_cycling_paused_due_to_dependent_bondings = true;
|
||||
} else {
|
||||
#if RECOVERY_FW
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Pausing address cycling because PRF!");
|
||||
bt_local_addr_pause_cycling();
|
||||
#elif BT_CONTROLLER_CC2564X && !RELEASE
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Pausing address cycling because cc2564x and beta build!");
|
||||
bt_local_addr_pause_cycling();
|
||||
#else
|
||||
PBL_LOG(LOG_LEVEL_INFO, "No bondings found that require address pinning!");
|
||||
bt_driver_set_local_address(true /* allow_cycling */, NULL);
|
||||
#endif
|
||||
}
|
||||
}
|
42
src/fw/services/common/bluetooth/local_addr.h
Normal file
42
src/fw/services/common/bluetooth/local_addr.h
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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/common/bluetooth/bluetooth_persistent_storage.h"
|
||||
|
||||
typedef struct BTDeviceAddress BTDeviceAddress;
|
||||
|
||||
//! Pauses cycling of local Private Resolvable Address (ref counted).
|
||||
//! As long as the cycling is paused, the address that is used "on air" will be stable for the
|
||||
//! duration that the BT stack is up (so the address can be expected to have changed after rebooting
|
||||
//! or resetting the stack).
|
||||
//! In case the local address is currently pinned, this function will be a no-op.
|
||||
void bt_local_addr_pause_cycling(void);
|
||||
|
||||
//! Resumes cycling of local Private Resolvable Address (ref counted).
|
||||
//! In case the local address is currently pinned, this function will be a no-op.
|
||||
void bt_local_addr_resume_cycling(void);
|
||||
|
||||
//! Called by BT driver to indicate what the local address was that was used during the pairing
|
||||
//! and pinning was requested. See comment in the implementation for more details.
|
||||
void bt_local_addr_pin(const BTDeviceAddress *addr);
|
||||
|
||||
//! Handler for bonding changes (deletions primarily).
|
||||
void bt_local_addr_handle_bonding_change(BTBondingID bonding, BtPersistBondingOp op);
|
||||
|
||||
//! Called during the BT stack initialization.
|
||||
void bt_local_addr_init(void);
|
158
src/fw/services/common/bluetooth/local_id.c
Normal file
158
src/fw/services/common/bluetooth/local_id.c
Normal file
|
@ -0,0 +1,158 @@
|
|||
/*
|
||||
* 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 "local_id.h"
|
||||
|
||||
#include "mfg/mfg_serials.h"
|
||||
#include "services/common/bluetooth/bluetooth_persistent_storage.h"
|
||||
#include "util/attributes.h"
|
||||
#include "util/hash.h"
|
||||
#include "util/size.h"
|
||||
#include "util/string.h"
|
||||
|
||||
#include <bluetooth/features.h>
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
// Caches of the local address and device name.
|
||||
// Some clients (i.e. Settings app) make a lot of calls to this module. By caching this info,
|
||||
// we avoid having to reach out to the BT driver every time.
|
||||
static BTDeviceAddress s_local_address;
|
||||
static char s_local_device_name[BT_DEVICE_NAME_BUFFER_SIZE];
|
||||
static char s_local_le_device_name[BT_DEVICE_NAME_BUFFER_SIZE];
|
||||
|
||||
static void prv_populate_name(char name[BT_DEVICE_NAME_BUFFER_SIZE], const char *name_fmt) {
|
||||
sprintf(name, name_fmt, s_local_address.octets[1], s_local_address.octets[0]);
|
||||
}
|
||||
|
||||
static void prv_set_default_device_name(void) {
|
||||
#if (PLATFORM_SNOWY || PLATFORM_SPALDING || PLATFORM_ROBERT)
|
||||
const char *s_local_default_device_name_format = "Pebble Time %02X%02X";
|
||||
const char *s_local_default_le_device_name_format = "Pebble Time LE %02X%02X";
|
||||
#else
|
||||
const char *s_local_default_device_name_format = "Pebble %02X%02X";
|
||||
const char *s_local_default_le_device_name_format = "Pebble-LE %02X%02X";
|
||||
#endif
|
||||
|
||||
// Pebble + hex last 2 bytes of the device address:
|
||||
prv_populate_name(s_local_device_name, s_local_default_device_name_format);
|
||||
prv_populate_name(s_local_le_device_name, s_local_default_le_device_name_format);
|
||||
}
|
||||
|
||||
static bool prv_has_device_name(void) {
|
||||
return (s_local_device_name[0] != '\0');
|
||||
}
|
||||
|
||||
static void prv_configure_device_name(void) {
|
||||
bt_driver_id_set_local_device_name(s_local_device_name);
|
||||
}
|
||||
|
||||
void bt_local_id_configure_driver(void) {
|
||||
// Request the local address from the BT driver and cache it:
|
||||
bt_driver_id_copy_local_identity_address(&s_local_address);
|
||||
|
||||
if (!prv_has_device_name()) {
|
||||
if (!bt_persistent_storage_get_local_device_name(s_local_device_name,
|
||||
sizeof(s_local_device_name))) {
|
||||
prv_set_default_device_name();
|
||||
}
|
||||
}
|
||||
|
||||
prv_configure_device_name();
|
||||
}
|
||||
|
||||
void bt_local_id_set_device_name(const char *device_name) {
|
||||
strncpy(s_local_device_name, device_name, sizeof(s_local_device_name));
|
||||
s_local_device_name[sizeof(s_local_device_name) - 1] = '\0';
|
||||
prv_configure_device_name();
|
||||
}
|
||||
|
||||
void bt_local_id_copy_device_name(char name_out[BT_DEVICE_NAME_BUFFER_SIZE], bool is_le) {
|
||||
char *name = (is_le && bt_driver_supports_bt_classic()) ? s_local_le_device_name :
|
||||
s_local_device_name;
|
||||
strncpy(name_out, name, BT_DEVICE_NAME_BUFFER_SIZE);
|
||||
}
|
||||
|
||||
void bt_local_id_copy_address(BTDeviceAddress *addr_out) {
|
||||
*addr_out = s_local_address;
|
||||
}
|
||||
|
||||
void bt_local_id_copy_address_hex_string(char addr_hex_str_out[BT_ADDR_FMT_BUFFER_SIZE_BYTES]) {
|
||||
static const BTDeviceAddress null_addr = {};
|
||||
if (0 != memcmp(&null_addr, &s_local_address, sizeof(s_local_address))) {
|
||||
sniprintf(addr_hex_str_out, BT_DEVICE_ADDRESS_FMT_BUFFER_SIZE,
|
||||
BD_ADDR_FMT, BT_DEVICE_ADDRESS_XPLODE(s_local_address));
|
||||
} else {
|
||||
sniprintf(addr_hex_str_out, BT_DEVICE_ADDRESS_FMT_BUFFER_SIZE, "Unknown");
|
||||
}
|
||||
}
|
||||
|
||||
void bt_local_id_copy_address_mac_string(char addr_mac_str_out[BT_DEVICE_ADDRESS_FMT_BUFFER_SIZE]) {
|
||||
sniprintf(addr_mac_str_out, BT_DEVICE_ADDRESS_FMT_BUFFER_SIZE,
|
||||
BT_DEVICE_ADDRESS_FMT, BT_DEVICE_ADDRESS_XPLODE(s_local_address));
|
||||
}
|
||||
|
||||
T_STATIC void prv_generate_address(BTDeviceAddress *addr_out) {
|
||||
const char *serial = mfg_get_serial_number();
|
||||
const uint32_t full_len = strlen(serial);
|
||||
const uint32_t half_len = (full_len / 2);
|
||||
|
||||
// Hash of the normal serial
|
||||
const uint32_t serial_hash = hash((uint8_t *)serial, full_len);
|
||||
|
||||
// Hash of the serial reversed
|
||||
char tmp[full_len + 1];
|
||||
strncpy(tmp, serial, sizeof(tmp));
|
||||
string_reverse(tmp);
|
||||
const uint32_t reverse_hash = hash((uint8_t *)tmp, full_len);
|
||||
|
||||
struct PACKED {
|
||||
union {
|
||||
BTDeviceAddress bt_addr;
|
||||
struct PACKED {
|
||||
uint16_t a;
|
||||
uint32_t b;
|
||||
};
|
||||
};
|
||||
} addr = {
|
||||
.a = (uint16_t) reverse_hash,
|
||||
.b = (serial_hash ^ reverse_hash),
|
||||
};
|
||||
|
||||
*addr_out = addr.bt_addr;
|
||||
}
|
||||
|
||||
void bt_local_id_generate_address_from_serial(BTDeviceAddress *addr_out) {
|
||||
prv_generate_address(addr_out);
|
||||
addr_out->octets[ARRAY_LENGTH(addr_out->octets) - 1] |= 0b11000000;
|
||||
|
||||
// Addresses with all 0's or 1's
|
||||
const BTDeviceAddress zero_addr = {.octets = {0x00, 0x00, 0x00, 0x00, 0x00, 0xC0}};
|
||||
const BTDeviceAddress one_addr = {.octets = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff}};
|
||||
// NOTE: It already has the two most sig. bits set.
|
||||
const BTDeviceAddress fallback_addr = {.octets = {0x3c, 0x08, 0x55, 0xaf, 0xd3, 0xc4}};
|
||||
|
||||
// Compare (the first 5 bytes) the generated one with the invalid ones. If they are equal,
|
||||
// fall back to this address.
|
||||
if (!memcmp(addr_out, &zero_addr, sizeof(BTDeviceAddress))
|
||||
|| !memcmp(addr_out, &one_addr, sizeof(BTDeviceAddress))) {
|
||||
*addr_out = fallback_addr;
|
||||
}
|
||||
|
||||
memcpy(addr_out, (uint8_t *)addr_out, sizeof(*addr_out));
|
||||
}
|
47
src/fw/services/common/bluetooth/local_id.h
Normal file
47
src/fw/services/common/bluetooth/local_id.h
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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/bluetooth_types.h>
|
||||
#include <bluetooth/id.h>
|
||||
|
||||
//! Called by bl_ctl right after the stack starts, to configure the local device name and address.
|
||||
void bt_local_id_configure_driver(void);
|
||||
|
||||
//! Sets a new device name, overriding the existing (default) one.
|
||||
//! The name will be truncated to BT_DEVICE_NAME_BUFFER_SIZE - 1 characters.
|
||||
void bt_local_id_set_device_name(const char *device_name);
|
||||
|
||||
//! Copies the name of the local device into the given buffer.
|
||||
//! @param is_le Only consumed if the device used is dual mode. If so,
|
||||
// this changes the name returned
|
||||
void bt_local_id_copy_device_name(char name_out[BT_DEVICE_NAME_BUFFER_SIZE], bool is_le);
|
||||
|
||||
//! Copies the address of the local device.
|
||||
void bt_local_id_copy_address(BTDeviceAddress *addr_out);
|
||||
|
||||
//! Copies a hex-formatted string representation ("0x000000000000") of the device address into the
|
||||
//! given buffer. The buffer should be at least BT_ADDR_FMT_BUFFER_SIZE_BYTES bytes in size to fit
|
||||
//! the address string.
|
||||
//! If there is no local address known, the string "Unknown" will be copied into the buffer.
|
||||
void bt_local_id_copy_address_hex_string(char addr_hex_str_out[BT_ADDR_FMT_BUFFER_SIZE_BYTES]);
|
||||
|
||||
//! Copies a MAC-formatted string representation ("00:00:00:00:00:00") of the device address into
|
||||
//! the given buffer. The buffer should be at least BT_ADDR_FMT_BUFFER_SIZE_BYTES bytes in size to
|
||||
//! fit the address string.
|
||||
void bt_local_id_copy_address_mac_string(char addr_mac_str_out[BT_DEVICE_ADDRESS_FMT_BUFFER_SIZE]);
|
||||
|
||||
//! Generates a BTDeviceAddress from the serial number of the watch
|
||||
void bt_local_id_generate_address_from_serial(BTDeviceAddress *addr_out);
|
155
src/fw/services/common/bluetooth/pairability.c
Normal file
155
src/fw/services/common/bluetooth/pairability.c
Normal file
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#define FILE_LOG_COLOR LOG_COLOR_BLUE
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
|
||||
#include "comm/ble/gap_le_slave_discovery.h"
|
||||
#include "kernel/pebble_tasks.h"
|
||||
#include "services/common/bluetooth/bluetooth_ctl.h"
|
||||
#include "services/common/bluetooth/bluetooth_persistent_storage.h"
|
||||
#include "services/common/bluetooth/local_addr.h"
|
||||
#include "services/common/bluetooth/pairability.h"
|
||||
#include "services/common/regular_timer.h"
|
||||
#include "services/common/system_task.h"
|
||||
|
||||
#include <bluetooth/connectability.h>
|
||||
#include <bluetooth/features.h>
|
||||
#include <bluetooth/pairability.h>
|
||||
|
||||
static void prv_pairability_timer_cb(void *unused);
|
||||
|
||||
static int s_allow_bt_pairing_refcount = 0;
|
||||
static int s_allow_ble_pairing_refcount = 0;
|
||||
|
||||
static RegularTimerInfo s_pairability_timer_info = {
|
||||
.cb = prv_pairability_timer_cb,
|
||||
};
|
||||
|
||||
static void evaluate_pairing_refcount(void *data) {
|
||||
PBL_ASSERT_TASK(PebbleTask_KernelBackground);
|
||||
|
||||
if (!bt_ctl_is_bluetooth_running()) {
|
||||
return;
|
||||
}
|
||||
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Pairabilty state: LE=%u, Classic=%u",
|
||||
s_allow_ble_pairing_refcount, s_allow_bt_pairing_refcount);
|
||||
|
||||
bool is_ble_pairable_and_discoverable = (s_allow_ble_pairing_refcount > 0);
|
||||
|
||||
bt_driver_le_pairability_set_enabled(is_ble_pairable_and_discoverable);
|
||||
if (gap_le_slave_is_discoverable() != is_ble_pairable_and_discoverable) {
|
||||
if (is_ble_pairable_and_discoverable) {
|
||||
bt_local_addr_pause_cycling();
|
||||
} else {
|
||||
bt_local_addr_resume_cycling();
|
||||
}
|
||||
gap_le_slave_set_discoverable(is_ble_pairable_and_discoverable);
|
||||
}
|
||||
|
||||
if (bt_driver_supports_bt_classic()) {
|
||||
bt_driver_classic_pairability_set_enabled((s_allow_bt_pairing_refcount > 0));
|
||||
bt_driver_classic_update_connectability();
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_schedule_evaluation(void) {
|
||||
// We used to sparingly schedule the evaluation and had a bug because of this:
|
||||
// https://pebbletechnology.atlassian.net/browse/PBL-22884
|
||||
// Because this pretty much only happens in response to user input, don't bother limiting this,
|
||||
// and always evaluate, even though the state might not have changed:
|
||||
system_task_add_callback(evaluate_pairing_refcount, NULL);
|
||||
}
|
||||
|
||||
void bt_pairability_use(void) {
|
||||
++s_allow_bt_pairing_refcount;
|
||||
++s_allow_ble_pairing_refcount;
|
||||
prv_schedule_evaluation();
|
||||
}
|
||||
|
||||
void bt_pairability_use_bt(void) {
|
||||
++s_allow_bt_pairing_refcount;
|
||||
prv_schedule_evaluation();
|
||||
}
|
||||
|
||||
void bt_pairability_use_ble(void) {
|
||||
++s_allow_ble_pairing_refcount;
|
||||
prv_schedule_evaluation();
|
||||
}
|
||||
|
||||
static void prv_pairability_timer_cb(void *unused) {
|
||||
regular_timer_remove_callback(&s_pairability_timer_info);
|
||||
bt_pairability_release_ble();
|
||||
}
|
||||
|
||||
void bt_pairability_use_ble_for_period(uint16_t duration_secs) {
|
||||
if (!regular_timer_is_scheduled(&s_pairability_timer_info)) {
|
||||
// If this function is called multiple times before the timer is unscheduled, limit to calling
|
||||
// "use" only once:
|
||||
bt_pairability_use_ble();
|
||||
}
|
||||
// Always reschedule, even if the duration is shorter than the one that might already be
|
||||
// scheduled:
|
||||
regular_timer_add_multisecond_callback(&s_pairability_timer_info, duration_secs);
|
||||
}
|
||||
|
||||
void bt_pairability_release(void) {
|
||||
PBL_ASSERT(s_allow_bt_pairing_refcount != 0 && s_allow_ble_pairing_refcount != 0, "");
|
||||
--s_allow_bt_pairing_refcount;
|
||||
--s_allow_ble_pairing_refcount;
|
||||
prv_schedule_evaluation();
|
||||
}
|
||||
|
||||
void bt_pairability_release_bt(void) {
|
||||
PBL_ASSERT(s_allow_bt_pairing_refcount != 0, "");
|
||||
--s_allow_bt_pairing_refcount;
|
||||
prv_schedule_evaluation();
|
||||
}
|
||||
|
||||
void bt_pairability_release_ble(void) {
|
||||
PBL_ASSERT(s_allow_ble_pairing_refcount != 0, "");
|
||||
--s_allow_ble_pairing_refcount;
|
||||
prv_schedule_evaluation();
|
||||
}
|
||||
|
||||
//! Call this whenever we modify the number of saved bondings we have.
|
||||
void bt_pairability_update_due_to_bonding_change(void) {
|
||||
static bool s_pairable_due_to_no_gateway_bondings = false;
|
||||
const bool has_classic_bonding =
|
||||
(bt_driver_supports_bt_classic() &&
|
||||
bt_persistent_storage_has_active_bt_classic_gateway_bonding());
|
||||
|
||||
if (!has_classic_bonding &&
|
||||
!bt_persistent_storage_has_active_ble_gateway_bonding() &&
|
||||
!bt_persistent_storage_has_ble_ancs_bonding()) {
|
||||
if (!s_pairable_due_to_no_gateway_bondings) {
|
||||
bt_pairability_use();
|
||||
s_pairable_due_to_no_gateway_bondings = true;
|
||||
}
|
||||
} else {
|
||||
if (s_pairable_due_to_no_gateway_bondings) {
|
||||
bt_pairability_release();
|
||||
s_pairable_due_to_no_gateway_bondings = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void bt_pairability_init(void) {
|
||||
bt_pairability_update_due_to_bonding_change();
|
||||
prv_schedule_evaluation();
|
||||
}
|
45
src/fw/services/common/bluetooth/pairability.h
Normal file
45
src/fw/services/common/bluetooth/pairability.h
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
//! Reference counted request to allow us to be discovered and paired with over BT Classic & LE.
|
||||
void bt_pairability_use(void);
|
||||
|
||||
//! Reference counted request to allow us to be discovered and paired with over BT Classic.
|
||||
void bt_pairability_use_bt(void);
|
||||
|
||||
//! Reference counted request to allow us to be discovered and paired with over BLE.
|
||||
void bt_pairability_use_ble(void);
|
||||
|
||||
//! Reference counted request to allow us to be discovered and paired with over BLE for a specific
|
||||
//! period, after which bt_pairability_release_ble will be called automatically.
|
||||
void bt_pairability_use_ble_for_period(uint16_t duration_secs);
|
||||
|
||||
//! Reference counted request to disallow us to be discovered and paired with over BT Classic & LE.
|
||||
void bt_pairability_release(void);
|
||||
|
||||
//! Reference counted request to disallow us to be discovered and paired with over BT Classic.
|
||||
void bt_pairability_release_bt(void);
|
||||
|
||||
//! Reference counted request to disallow us to be discovered and paired with over BLE.
|
||||
void bt_pairability_release_ble(void);
|
||||
|
||||
//! Evaluates whether there are any bondings to gateways. If there are none, make the system
|
||||
//! discoverable and pairable.
|
||||
void bt_pairability_update_due_to_bonding_change(void);
|
||||
|
||||
void bt_pairability_init(void);
|
130
src/fw/services/common/bluetooth/pebble_pairing_service.c
Normal file
130
src/fw/services/common/bluetooth/pebble_pairing_service.c
Normal file
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* 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/pebble_pairing_service.h>
|
||||
|
||||
#include "comm/ble/gap_le_connect_params.h"
|
||||
#include "comm/ble/gap_le_connection.h"
|
||||
#include "comm/ble/kernel_le_client/app_launch/app_launch.h"
|
||||
#include "comm/bt_conn_mgr.h"
|
||||
#include "comm/bt_lock.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "system/logging.h"
|
||||
|
||||
extern void gap_le_connect_params_re_evaluate(GAPLEConnection *connection);
|
||||
|
||||
static void prv_convert_pps_request_params(const PebblePairingServiceConnParamSet *pps_params_in,
|
||||
GAPLEConnectRequestParams *params_out) {
|
||||
const uint16_t min_1_25ms = pps_params_in->interval_min_1_25ms;
|
||||
params_out->connection_interval_min_1_25ms = min_1_25ms;
|
||||
params_out->connection_interval_max_1_25ms =
|
||||
min_1_25ms + pps_params_in->interval_max_delta_1_25ms;
|
||||
#if RECOVERY_FW || BT_CONTROLLER_DA14681
|
||||
if (pps_params_in->slave_latency_events != 0) {
|
||||
# if RECOVERY_FW
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Overriding requested slave latency with 0 because PRF");
|
||||
# else
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Overriding requested slave latency with 0 because Dialog");
|
||||
# endif
|
||||
}
|
||||
params_out->slave_latency_events = 0;
|
||||
#else
|
||||
params_out->slave_latency_events = pps_params_in->slave_latency_events;
|
||||
#endif
|
||||
params_out->supervision_timeout_10ms = pps_params_in->supervision_timeout_30ms * 3;
|
||||
}
|
||||
|
||||
static void prv_handle_set_remote_param_mgmt_settings(GAPLEConnection *connection,
|
||||
const PebblePairingServiceRemoteParamMgmtSettings *settings, size_t settings_length) {
|
||||
bool is_remote_device_managing_connection_parameters =
|
||||
settings->is_remote_device_managing_connection_parameters;
|
||||
connection->is_remote_device_managing_connection_parameters =
|
||||
is_remote_device_managing_connection_parameters;
|
||||
PBL_LOG(LOG_LEVEL_INFO, "PPS: is_remote_mgmt=%u",
|
||||
is_remote_device_managing_connection_parameters);
|
||||
|
||||
if (settings_length >= PEBBLE_PAIRING_SERVICE_REMOTE_PARAM_MGTM_SETTINGS_SIZE_WITH_PARAM_SETS) {
|
||||
if (!connection->connection_parameter_sets) {
|
||||
const size_t size = sizeof(GAPLEConnectRequestParams) * NumResponseTimeState;
|
||||
connection->connection_parameter_sets =
|
||||
(GAPLEConnectRequestParams *) kernel_zalloc_check(size);
|
||||
}
|
||||
for (ResponseTimeState s = ResponseTimeMax; s < NumResponseTimeState; ++s) {
|
||||
const PebblePairingServiceConnParamSet *pps_params =
|
||||
&settings->connection_parameter_sets[s];
|
||||
GAPLEConnectRequestParams *params = &connection->connection_parameter_sets[s];
|
||||
prv_convert_pps_request_params(pps_params, params);
|
||||
PBL_LOG(LOG_LEVEL_INFO,
|
||||
"PPS: Updated param set %u: %u-%u, slave lat: %u, supervision timeout: %u",
|
||||
s, params->connection_interval_min_1_25ms, params->connection_interval_max_1_25ms,
|
||||
params->slave_latency_events, params->supervision_timeout_10ms);
|
||||
}
|
||||
}
|
||||
|
||||
// Always just re-evaluate, should be idempotent:
|
||||
gap_le_connect_params_re_evaluate(connection);
|
||||
}
|
||||
|
||||
static void prv_handle_set_remote_desired_state(GAPLEConnection *connection,
|
||||
const PebblePairingServiceRemoteDesiredState *desired_state) {
|
||||
const ResponseTimeState remote_desired_state = (ResponseTimeState)desired_state->state;
|
||||
PBL_LOG(LOG_LEVEL_INFO, "PPS: desired_state=%u", remote_desired_state);
|
||||
|
||||
// "As a safety measure, the watch will reset it back to ResponseTimeMax after 5 minutes."
|
||||
const uint16_t max_period_secs = 5 * 60;
|
||||
conn_mgr_set_ble_conn_response_time(connection, BtConsumerPebblePairingServiceRemoteDevice,
|
||||
remote_desired_state, max_period_secs);
|
||||
}
|
||||
|
||||
void bt_driver_cb_pebble_pairing_service_handle_connection_parameter_write(
|
||||
const BTDeviceInternal *device,
|
||||
const PebblePairingServiceConnParamsWrite *conn_params,
|
||||
size_t conn_params_length) {
|
||||
bt_lock();
|
||||
{
|
||||
GAPLEConnection *connection = gap_le_connection_by_device(device);
|
||||
if (!connection) {
|
||||
goto unlock;
|
||||
}
|
||||
const size_t length = (conn_params_length - offsetof(PebblePairingServiceConnParamsWrite,
|
||||
remote_desired_state));
|
||||
switch (conn_params->cmd) {
|
||||
case PebblePairingServiceConnParamsWriteCmd_SetRemoteParamMgmtSettings:
|
||||
prv_handle_set_remote_param_mgmt_settings(connection,
|
||||
&conn_params->remote_param_mgmt_settings, length);
|
||||
break;
|
||||
|
||||
case PebblePairingServiceConnParamsWriteCmd_SetRemoteDesiredState:
|
||||
prv_handle_set_remote_desired_state(connection, &conn_params->remote_desired_state);
|
||||
break;
|
||||
case PebblePairingServiceConnParamsWriteCmd_EnablePacketLengthExtension:
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Enabling BLE Packet Length Extension");
|
||||
break;
|
||||
case PebblePairingServiceConnParamsWriteCmd_InhibitBLESleep:
|
||||
PBL_LOG(LOG_LEVEL_INFO, "BLE Sleep Mode inhibited!");
|
||||
break;
|
||||
default:
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Unknown write_cmd %d", conn_params->cmd);
|
||||
break;
|
||||
}
|
||||
}
|
||||
unlock:
|
||||
bt_unlock();
|
||||
}
|
||||
|
||||
void bt_driver_cb_pebble_pairing_service_handle_ios_app_termination_detected(void) {
|
||||
app_launch_trigger();
|
||||
}
|
72
src/fw/services/common/bluetooth/pp_ble_control.c
Normal file
72
src/fw/services/common/bluetooth/pp_ble_control.c
Normal file
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#define FILE_LOG_COLOR LOG_COLOR_BLUE
|
||||
|
||||
#include "pp_ble_control.h"
|
||||
#include "services/common/bluetooth/pairability.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
#include "util/attributes.h"
|
||||
|
||||
typedef enum {
|
||||
// Values 0 - 3 are deprecated, do not use.
|
||||
BLEControlCommandTypeSetDiscoverablePairable = 4,
|
||||
} BLEControlCommandType;
|
||||
|
||||
typedef struct PACKED {
|
||||
uint8_t opcode;
|
||||
bool discoverable_pairable;
|
||||
uint16_t duration;
|
||||
} BLEControlCommandSetDiscoverablePairable;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
//! Handler for the "Set Discoverable & Pairable" command
|
||||
static void prv_handle_set_discoverable_pairable(
|
||||
BLEControlCommandSetDiscoverablePairable *cmd_data) {
|
||||
bt_pairability_use_ble_for_period(cmd_data->duration);
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Set Discoverable Pairable: %u, %u",
|
||||
cmd_data->discoverable_pairable, cmd_data->duration);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
//! Pebble protocol handler for the BLE control endpoint
|
||||
void pp_ble_control_protocol_msg_callback(
|
||||
CommSession* session, const uint8_t *data, unsigned int length) {
|
||||
PBL_ASSERT_RUNNING_FROM_EXPECTED_TASK(PebbleTask_KernelBackground);
|
||||
|
||||
if (length < sizeof(BLEControlCommandSetDiscoverablePairable)) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Invalid pp_ble_control_protocol_msg_callback message: %d", length);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t opcode = *(const uint8_t *) data;
|
||||
switch (opcode) {
|
||||
case 0 ... 3:
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Deprecated & unsupported opcode: %u", opcode);
|
||||
break;
|
||||
|
||||
case BLEControlCommandTypeSetDiscoverablePairable: {
|
||||
BLEControlCommandSetDiscoverablePairable *cmd_data =
|
||||
(BLEControlCommandSetDiscoverablePairable *)data;
|
||||
prv_handle_set_discoverable_pairable(cmd_data);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Unknown opcode %u", opcode);
|
||||
break;
|
||||
}
|
||||
}
|
23
src/fw/services/common/bluetooth/pp_ble_control.h
Normal file
23
src/fw/services/common/bluetooth/pp_ble_control.h
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "services/common/comm_session/session.h"
|
||||
|
||||
void pp_ble_control_protocol_msg_callback(CommSession* session,
|
||||
const uint8_t *data,
|
||||
unsigned int length);
|
994
src/fw/services/common/clock.c
Normal file
994
src/fw/services/common/clock.c
Normal file
|
@ -0,0 +1,994 @@
|
|||
/*
|
||||
* 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/clock.h"
|
||||
|
||||
#include "console/prompt.h"
|
||||
#include "drivers/rtc.h"
|
||||
#include "kernel/events.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "services/common/comm_session/session.h"
|
||||
#include "services/common/i18n/i18n.h"
|
||||
#include "services/common/regular_timer.h"
|
||||
#include "services/normal/alarms/alarm.h"
|
||||
#include "services/normal/timezone_database.h"
|
||||
#include "services/normal/wakeup.h"
|
||||
#include "shell/prefs.h"
|
||||
#include "syscall/syscall.h"
|
||||
#include "syscall/syscall_internal.h"
|
||||
#include "system/logging.h"
|
||||
#include "util/attributes.h"
|
||||
#include "util/math.h"
|
||||
#include "util/net.h"
|
||||
#include "util/size.h"
|
||||
#include "util/string.h"
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
// NOTE: There are RECOVERY_FW ifdefs in this file because PRF does not have
|
||||
// timezone support
|
||||
|
||||
#define UNKNOWN_TIMEZONE_ID (-1)
|
||||
|
||||
static const uint16_t protocol_time_endpoint_id = 11;
|
||||
|
||||
static RegularTimerInfo s_dst_checker;
|
||||
|
||||
//! Migrations for services that use timezone info
|
||||
static void prv_migrate_timezone_info(int utc_diff) {
|
||||
#ifndef RECOVERY_FW
|
||||
// Since all migrations are to UTC time, we are passed the relative offset from UTC
|
||||
notifications_migrate_timezone(utc_diff);
|
||||
wakeup_migrate_timezone(utc_diff);
|
||||
#endif
|
||||
}
|
||||
|
||||
static time_t prv_migrate_local_time_to_UTC(time_t local_time) {
|
||||
return time_local_to_utc(local_time);
|
||||
}
|
||||
|
||||
// Should only called by prv_update_time_info_and_generate_event()!
|
||||
static void prv_handle_timezone_set(TimezoneInfo *tz_info) {
|
||||
// Check if the timezone is set before setting it. This ensures that this
|
||||
// will only be false once as needed for us to migrate.
|
||||
bool timezone_migration_needed = !clock_is_timezone_set();
|
||||
|
||||
time_util_update_timezone(tz_info);
|
||||
|
||||
// Update the RTC registers with the latest timezone info
|
||||
rtc_set_timezone(tz_info);
|
||||
|
||||
// We are pivoting to UTC from localtime for the first time
|
||||
if (timezone_migration_needed) {
|
||||
time_t t = rtc_get_time();
|
||||
t = prv_migrate_local_time_to_UTC(t);
|
||||
rtc_sanitize_time_t(&t);
|
||||
rtc_set_time(t); // Pivot RTC from localtime to UTC
|
||||
prv_migrate_timezone_info(tz_info->tm_gmtoff);
|
||||
}
|
||||
}
|
||||
|
||||
typedef struct PACKED {
|
||||
// This struct is packed because it mirrors the endpoint definition:
|
||||
// https://pebbletechnology.atlassian.net/wiki/pages/viewpage.action?pageId=491698#PebbleProtocol(BluetoothSerial)-0xb(11)-Time/Clock(bigendian)
|
||||
time_t utc_time; // UTC timestamp
|
||||
int16_t utc_offset_min; // local timestamp - UTC timestamp in mins
|
||||
int8_t region_name_len; // timezone name length
|
||||
char region_name[TIMEZONE_NAME_LENGTH]; // timezone name string
|
||||
} TimezoneCBData;
|
||||
|
||||
#ifndef UNITTEST
|
||||
_Static_assert(sizeof(time_t) == 4, "Sizeof time_t does not match endpoint definition");
|
||||
#endif
|
||||
|
||||
#if !defined(RECOVERY_FW)
|
||||
static time_t prv_clock_dstrule_to_timestamp(
|
||||
bool is_end, const TimezoneInfo *tz_info, const TimezoneDSTRule *rule, int year) {
|
||||
|
||||
struct tm time_tm = {
|
||||
.tm_min = rule->minute,
|
||||
.tm_hour = rule->hour,
|
||||
.tm_mday = rule->mday,
|
||||
.tm_mon = rule->month,
|
||||
.tm_year = year,
|
||||
.tm_gmtoff = 0,
|
||||
.tm_isdst = 0,
|
||||
};
|
||||
// A few countries actually have their DST rule on the midnight AFTER a day
|
||||
// This is subtly different from the midnight OF a day.
|
||||
if (rule->hour >= HOURS_PER_DAY) {
|
||||
time_tm.tm_hour %= HOURS_PER_DAY;
|
||||
}
|
||||
// Brazil delays DST end by one week every 3 years for elections
|
||||
if (tz_info->dst_id == DSTID_BRAZIL && (((TM_YEAR_ORIGIN + year) % 3) == 2) && is_end) {
|
||||
time_tm.tm_mday += DAYS_PER_WEEK;
|
||||
}
|
||||
time_t uxtime = mktime(&time_tm);
|
||||
gmtime_r(&uxtime, &time_tm);
|
||||
|
||||
for (int i = 0; i < DAYS_PER_WEEK; i++) { // max is DAYS_PER_WEEK to find a day_of_week
|
||||
// we also have to check month here, as leap-year case puts us 1 day past feb
|
||||
#define DSTRULE_WDAY_ANY (255)
|
||||
if ((time_tm.tm_wday == rule->wday || rule->wday == DSTRULE_WDAY_ANY) &&
|
||||
time_tm.tm_mon == rule->month) {
|
||||
break;
|
||||
}
|
||||
time_tm.tm_mday += (rule->flag & TIMEZONE_FLAG_DAY_DECREMENT) ? -1 : 1;
|
||||
uxtime = mktime(&time_tm);
|
||||
gmtime_r(&uxtime, &time_tm);
|
||||
}
|
||||
|
||||
if (rule->hour >= HOURS_PER_DAY) {
|
||||
time_tm.tm_mday += rule->hour / HOURS_PER_DAY;
|
||||
uxtime = mktime(&time_tm);
|
||||
gmtime_r(&uxtime, &time_tm);
|
||||
}
|
||||
|
||||
if (rule->flag & TIMEZONE_FLAG_STANDARD_TIME) { // Standard time (not wall time)
|
||||
time_tm.tm_gmtoff = tz_info->tm_gmtoff;
|
||||
time_tm.tm_isdst = 0;
|
||||
} else if (rule->flag & TIMEZONE_FLAG_UTC_TIME) { // UTC
|
||||
time_tm.tm_gmtoff = 0;
|
||||
time_tm.tm_isdst = 0;
|
||||
} else { // Wall time
|
||||
time_tm.tm_gmtoff = tz_info->tm_gmtoff;
|
||||
time_tm.tm_isdst = is_end;
|
||||
}
|
||||
// Lord Howe Island has a half-hour DST
|
||||
if (tz_info->dst_id == DSTID_LORDHOWE) {
|
||||
uxtime -= time_tm.tm_isdst ? SECONDS_PER_HOUR / 2 : 0;
|
||||
} else {
|
||||
uxtime -= time_tm.tm_isdst ? SECONDS_PER_HOUR : 0;
|
||||
}
|
||||
uxtime -= time_tm.tm_gmtoff;
|
||||
return uxtime;
|
||||
}
|
||||
#endif // RECOVERY_FW
|
||||
|
||||
T_STATIC void prv_update_dstrule_timestamps_by_dstzone_id(TimezoneInfo *tz_info, time_t utc_time) {
|
||||
if (tz_info->dst_id == 0) {
|
||||
tz_info->dst_start = 0;
|
||||
tz_info->dst_end = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
#if defined(RECOVERY_FW)
|
||||
return;
|
||||
#else
|
||||
|
||||
// Load the pair of TimezoneDSTRule objects from the timezone database
|
||||
|
||||
TimezoneDSTRule dst_rule_begin;
|
||||
TimezoneDSTRule dst_rule_end;
|
||||
|
||||
if (!timezone_database_load_dst_rule(tz_info->dst_id, &dst_rule_begin, &dst_rule_end)) {
|
||||
// No DST rule or invalid DST ID. Either way just clear the DST information.
|
||||
tz_info->dst_start = 0;
|
||||
tz_info->dst_end = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
struct tm current_tm;
|
||||
gmtime_r(&utc_time, ¤t_tm);
|
||||
|
||||
// Calculate the timestamps of the start and ends of DST for the previous year, the current
|
||||
// year, and the next year.
|
||||
|
||||
#define DST_YEARS_RANGE 3
|
||||
#define DST_YEARS_OFFSET 1
|
||||
time_t dst_start_stamps[DST_YEARS_RANGE];
|
||||
time_t dst_end_stamps[DST_YEARS_RANGE];
|
||||
for (int i = 0; i < DST_YEARS_RANGE; i++) {
|
||||
const int year = current_tm.tm_year + (i - DST_YEARS_OFFSET);
|
||||
|
||||
dst_start_stamps[i] =
|
||||
prv_clock_dstrule_to_timestamp(false, tz_info, &dst_rule_begin, year);
|
||||
dst_end_stamps[i] =
|
||||
prv_clock_dstrule_to_timestamp(true, tz_info, &dst_rule_end, year);
|
||||
}
|
||||
|
||||
// Figure out which timestamps are relevant to us
|
||||
|
||||
int start_idx = DST_YEARS_OFFSET;
|
||||
int end_idx = DST_YEARS_OFFSET;
|
||||
|
||||
if (dst_start_stamps[start_idx] > dst_end_stamps[end_idx]) {
|
||||
start_idx--;
|
||||
}
|
||||
|
||||
if (dst_start_stamps[start_idx] < utc_time && dst_end_stamps[end_idx] < utc_time) {
|
||||
start_idx++;
|
||||
end_idx++;
|
||||
}
|
||||
|
||||
tz_info->dst_start = dst_start_stamps[start_idx];
|
||||
tz_info->dst_end = dst_end_stamps[end_idx];
|
||||
|
||||
#endif // RECOVERY_FW
|
||||
}
|
||||
|
||||
static void prv_clock_get_timezone_info_from_region_id(
|
||||
int16_t region_id, time_t utc_time, TimezoneInfo *tz_info) {
|
||||
|
||||
#ifdef RECOVERY_FW
|
||||
*tz_info = (TimezoneInfo) { .dst_id = 0 };
|
||||
#else
|
||||
timezone_database_load_region_info(region_id, tz_info);
|
||||
prv_update_dstrule_timestamps_by_dstzone_id(tz_info, utc_time);
|
||||
#endif
|
||||
}
|
||||
|
||||
static TimezoneInfo prv_get_timezone_info_from_data(TimezoneCBData *tz_data) {
|
||||
int region_id = -1;
|
||||
|
||||
if (tz_data->region_name_len) {
|
||||
region_id = timezone_database_find_region_by_name(tz_data->region_name,
|
||||
tz_data->region_name_len);
|
||||
}
|
||||
|
||||
if (region_id != -1) {
|
||||
// We have a valid region!
|
||||
TimezoneInfo tz_info;
|
||||
prv_clock_get_timezone_info_from_region_id(region_id, tz_data->utc_time, &tz_info);
|
||||
return tz_info;
|
||||
}
|
||||
|
||||
// Else, we couldn't find find the specified timezone.
|
||||
#ifndef RECOVERY_FW
|
||||
TimezoneInfo tz_info = {
|
||||
.dst_id = 0,
|
||||
.timezone_id = UNKNOWN_TIMEZONE_ID,
|
||||
.tm_gmtoff = tz_data->utc_offset_min * SECONDS_PER_MINUTE,
|
||||
.dst_start = 0,
|
||||
.dst_end = 0,
|
||||
};
|
||||
|
||||
// I was hoping to fill the name with something like UTC-10 or UTC+4.25 but we only get 5 chars
|
||||
strncpy(tz_info.tm_zone, "N/A", TZ_LEN - 1);
|
||||
return tz_info;
|
||||
#else
|
||||
return (TimezoneInfo) {};
|
||||
#endif
|
||||
}
|
||||
|
||||
// This routine is solely responsible for setting the time and/or timezone for
|
||||
// the system RTC. After the time is changed, it generates an event for
|
||||
// consumers interested in time changes
|
||||
T_STATIC void prv_update_time_info_and_generate_event(time_t *t, TimezoneInfo *tz_info) {
|
||||
int orig_gmt_offset = time_get_gmtoffset();
|
||||
time_t orig_utc_time = rtc_get_time();
|
||||
TimezoneInfo tz_adjust_info = {{0}};
|
||||
|
||||
if (clock_is_timezone_set()) { // We'll need to update timezone stamps.
|
||||
time_t tz_adjust_time;
|
||||
// Get the time that we need to adjust for.
|
||||
if (t) {
|
||||
tz_adjust_time = *t;
|
||||
} else {
|
||||
tz_adjust_time = orig_utc_time;
|
||||
}
|
||||
if (tz_info) { // Adjust the DST rule timestamps of the provided tz_info
|
||||
prv_update_dstrule_timestamps_by_dstzone_id(tz_info, tz_adjust_time);
|
||||
} else if (clock_get_timezone_region_id() != UNKNOWN_TIMEZONE_ID) {
|
||||
// If we have a timezone _actually_ set, update our own.
|
||||
int region_id = clock_get_timezone_region_id();
|
||||
prv_clock_get_timezone_info_from_region_id(region_id, tz_adjust_time, &tz_adjust_info);
|
||||
tz_info = &tz_adjust_info; // We need to set timezone info so point to the new info.
|
||||
}
|
||||
}
|
||||
|
||||
// Note: update the timezone before setting the utc time. (If we set the utc
|
||||
// time first we could wind up accidentally applying the timezone correction
|
||||
// to that value in the case where no timezone data previously existed
|
||||
// ... such as a migration from legacy firmware or after the RTC backup
|
||||
// domain had completely powered down)
|
||||
if (tz_info) {
|
||||
prv_handle_timezone_set(tz_info);
|
||||
}
|
||||
|
||||
if (t) {
|
||||
rtc_set_time(*t);
|
||||
}
|
||||
|
||||
int new_gmt_offset = time_get_gmtoffset();
|
||||
int new_utc_time = rtc_get_time();
|
||||
|
||||
PebbleEvent e = {
|
||||
.type = PEBBLE_SET_TIME_EVENT,
|
||||
.set_time_info = {
|
||||
.utc_time_delta = new_utc_time - orig_utc_time,
|
||||
.gmt_offset_delta = new_gmt_offset - orig_gmt_offset,
|
||||
.dst_changed = false,
|
||||
}
|
||||
};
|
||||
event_put(&e);
|
||||
}
|
||||
|
||||
static void prv_handle_set_utc_and_timezone_msg(TimezoneCBData *tz_data) {
|
||||
tz_data->utc_time = ntohl(tz_data->utc_time);
|
||||
tz_data->utc_offset_min = ntohs(tz_data->utc_offset_min);
|
||||
|
||||
const char *region_name = tz_data->region_name;
|
||||
if (tz_data->region_name_len == 0) {
|
||||
region_name = "[N/A]";
|
||||
}
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "set_timezone utc_time: %u offset: %d region_name: %s",
|
||||
(int) tz_data->utc_time, (int) tz_data->utc_offset_min, region_name);
|
||||
|
||||
TimezoneInfo tz_info = prv_get_timezone_info_from_data(tz_data);
|
||||
shell_prefs_set_automatic_timezone_id(tz_info.timezone_id);
|
||||
if (clock_timezone_source_is_manual()) {
|
||||
prv_update_time_info_and_generate_event(&tz_data->utc_time, NULL);
|
||||
} else {
|
||||
prv_update_time_info_and_generate_event(&tz_data->utc_time, &tz_info);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_handle_set_time_msg(time_t new_time) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Mobile app calling deprecated API, time = %d",
|
||||
(int)new_time);
|
||||
if (clock_is_timezone_set()) {
|
||||
new_time = prv_migrate_local_time_to_UTC(new_time);
|
||||
}
|
||||
|
||||
prv_update_time_info_and_generate_event(&new_time, NULL);
|
||||
}
|
||||
|
||||
void clock_protocol_msg_callback(CommSession *session, const uint8_t* data, unsigned int length) {
|
||||
char sub_command = *data++;
|
||||
|
||||
switch (sub_command) {
|
||||
// Get time request:
|
||||
case 0x00: {
|
||||
time_t t = rtc_get_time();
|
||||
|
||||
// Send Get time response (0x01):
|
||||
const unsigned int response_buffer_length = 1 + 4;
|
||||
uint8_t response_buffer[response_buffer_length];
|
||||
|
||||
response_buffer[0] = 0x01;
|
||||
|
||||
*(uint32_t*)(response_buffer + 1) = htonl(t);
|
||||
|
||||
comm_session_send_data(session, protocol_time_endpoint_id, response_buffer,
|
||||
response_buffer_length, COMM_SESSION_DEFAULT_TIMEOUT);
|
||||
PBL_LOG_VERBOSE("protocol_time_callback called, responding with current time: %"PRIu32,
|
||||
(uint32_t)t);
|
||||
break;
|
||||
}
|
||||
// Set time:
|
||||
case 0x02: {
|
||||
time_t new_time = ntohl(*(uint32_t*)data);
|
||||
prv_handle_set_time_msg(new_time);
|
||||
break;
|
||||
}
|
||||
// Set timezone:
|
||||
case 0x03: {
|
||||
// Verify that the message length is correct
|
||||
const size_t header_size = offsetof(TimezoneCBData, region_name);
|
||||
const uint8_t *timezone_length_ptr = data + offsetof(TimezoneCBData, region_name_len);
|
||||
if (length != (sizeof(uint8_t) + header_size + *timezone_length_ptr)) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Set timezone message invalid length");
|
||||
return;
|
||||
}
|
||||
|
||||
TimezoneCBData *timezone_data = (TimezoneCBData *)data;
|
||||
prv_handle_set_utc_and_timezone_msg(timezone_data);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Invalid message received. First byte is %u", data[0]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Using a regular timer is pretty gross...
|
||||
static void prv_watch_dst(void* user) {
|
||||
const bool was_dst = (bool)user;
|
||||
const bool is_dst = time_get_isdst(rtc_get_time());
|
||||
if (is_dst != was_dst) {
|
||||
PebbleEvent e = {
|
||||
.type = PEBBLE_SET_TIME_EVENT,
|
||||
.set_time_info = {
|
||||
.utc_time_delta = 0,
|
||||
.gmt_offset_delta = 0,
|
||||
.dst_changed = true,
|
||||
}
|
||||
};
|
||||
event_put(&e);
|
||||
s_dst_checker.cb_data = (void*)is_dst;
|
||||
}
|
||||
}
|
||||
|
||||
void clock_init(void) {
|
||||
if (clock_is_timezone_set()) {
|
||||
TimezoneInfo tz_info;
|
||||
rtc_get_timezone(&tz_info);
|
||||
time_util_update_timezone(&tz_info);
|
||||
}
|
||||
// TODO: Using a regular timer is pretty gross...
|
||||
s_dst_checker = (RegularTimerInfo) {
|
||||
.cb = prv_watch_dst,
|
||||
.cb_data = (void*)time_get_isdst(rtc_get_time()),
|
||||
};
|
||||
regular_timer_add_seconds_callback(&s_dst_checker);
|
||||
}
|
||||
|
||||
void clock_get_time_tm(struct tm* time_tm) {
|
||||
rtc_get_time_tm(time_tm);
|
||||
}
|
||||
|
||||
size_t clock_format_time(char *buffer, uint8_t size, int16_t hours, int16_t minutes,
|
||||
bool add_space) {
|
||||
if (size == 0 || buffer == NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool is24h = clock_is_24h_style();
|
||||
const char *format;
|
||||
|
||||
// [INTL] you want to have layout resources that specify time formatting,
|
||||
// and be able to set a default one for each locale.
|
||||
if (is24h) {
|
||||
format = "%u:%02u";
|
||||
} else {
|
||||
if (hours < 12) {
|
||||
format = add_space ? "%u:%02u AM" : "%u:%02uAM";
|
||||
} else {
|
||||
format = add_space ? "%u:%02u PM" : "%u:%02uPM";
|
||||
}
|
||||
}
|
||||
return sniprintf(buffer, size, format, time_util_get_num_hours(hours, is24h), minutes);
|
||||
}
|
||||
|
||||
size_t clock_copy_time_string_timestamp(char *buffer, uint8_t size, time_t timestamp) {
|
||||
struct tm time;
|
||||
sys_localtime_r(×tamp, &time);
|
||||
return clock_format_time(buffer, size, time.tm_hour, time.tm_min, true);
|
||||
}
|
||||
|
||||
void clock_copy_time_string(char *buffer, uint8_t size) {
|
||||
time_t t = sys_get_time();
|
||||
clock_copy_time_string_timestamp(buffer, size, t);
|
||||
}
|
||||
|
||||
static size_t prv_format_time(char *buffer, int buf_size, const char *format, time_t timestamp) {
|
||||
struct tm time_tm;
|
||||
localtime_r(×tamp, &time_tm);
|
||||
const size_t ret_val = strftime(buffer, buf_size, i18n_get(format, buffer), &time_tm);
|
||||
i18n_free(format, buffer);
|
||||
return ret_val;
|
||||
}
|
||||
|
||||
size_t clock_get_time_number(char *number_buffer, size_t number_buffer_size, time_t timestamp) {
|
||||
const size_t written =
|
||||
prv_format_time(number_buffer, number_buffer_size,
|
||||
(clock_is_24h_style() ? i18n_noop("%R") : i18n_noop("%l:%M")), timestamp);
|
||||
const char *number_buffer_ptr = string_strip_leading_whitespace(number_buffer);
|
||||
memmove(number_buffer,
|
||||
number_buffer_ptr,
|
||||
number_buffer_size - (number_buffer_ptr - number_buffer));
|
||||
return written - (number_buffer_ptr - number_buffer);
|
||||
}
|
||||
|
||||
size_t clock_get_time_word(char *buffer, size_t buffer_size, time_t timestamp) {
|
||||
if (clock_is_24h_style()) {
|
||||
buffer[0] = '\0';
|
||||
return 0;
|
||||
} else {
|
||||
return prv_format_time(buffer, buffer_size, i18n_noop("%p"), timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_copy_time_string_timestamp(char *number_buffer, uint8_t number_buffer_size,
|
||||
char *word_buffer, uint8_t word_buffer_size, time_t timestamp) {
|
||||
clock_get_time_number(number_buffer, number_buffer_size, timestamp);
|
||||
clock_get_time_word(word_buffer, word_buffer_size, timestamp);
|
||||
}
|
||||
|
||||
static void prv_get_relative_all_day_string(char *buffer, int buffer_size, time_t timestamp) {
|
||||
time_t today = time_util_get_midnight_of(rtc_get_time());
|
||||
if (time_util_get_midnight_of(timestamp) == today) {
|
||||
i18n_get_with_buffer("Today", buffer, buffer_size);
|
||||
} else {
|
||||
i18n_get_with_buffer("All day", buffer, buffer_size);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_copy_relative_time_string(char *number_buffer, uint8_t number_buffer_size,
|
||||
char *word_buffer, uint8_t word_buffer_size, time_t timestamp, time_t end_time) {
|
||||
time_t now = rtc_get_time();
|
||||
// average without overflows since time_t might be signed and now ~1.4 billion, so 2*now > INT_MAX
|
||||
time_t midtime = timestamp / 2 + end_time / 2;
|
||||
if (midtime > now) { // future
|
||||
time_t difference = timestamp - now;
|
||||
if (timestamp < now || difference < SECONDS_PER_MINUTE) {
|
||||
i18n_get_with_buffer("Now", word_buffer, word_buffer_size);
|
||||
strncpy(number_buffer, "", number_buffer_size);
|
||||
} else if (difference <= SECONDS_PER_HOUR) {
|
||||
snprintf(number_buffer, number_buffer_size, "%ld", difference / SECONDS_PER_MINUTE);
|
||||
i18n_get_with_buffer(" MIN. TO", word_buffer, word_buffer_size);
|
||||
} else {
|
||||
prv_copy_time_string_timestamp(number_buffer, number_buffer_size, word_buffer,
|
||||
word_buffer_size, timestamp);
|
||||
}
|
||||
} else { // past
|
||||
time_t difference = now - timestamp;
|
||||
if (now < timestamp || difference < SECONDS_PER_MINUTE) {
|
||||
i18n_get_with_buffer("Now", word_buffer, word_buffer_size);
|
||||
strncpy(number_buffer, "", number_buffer_size);
|
||||
} else {
|
||||
prv_copy_time_string_timestamp(number_buffer, number_buffer_size, word_buffer,
|
||||
word_buffer_size, timestamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// number: 10
|
||||
// word: min to
|
||||
void clock_get_event_relative_time_string(char *number_buffer, int number_buffer_size,
|
||||
char *word_buffer, int word_buffer_size, time_t timestamp, uint16_t duration,
|
||||
time_t current_day, bool all_day) {
|
||||
time_t end_time = timestamp + duration * SECONDS_PER_MINUTE;
|
||||
if (all_day) {
|
||||
// all day event, multiday or single day
|
||||
prv_get_relative_all_day_string(word_buffer, word_buffer_size, current_day);
|
||||
strncpy(number_buffer, "", number_buffer_size);
|
||||
} else if (time_util_get_midnight_of(timestamp) == current_day) {
|
||||
// first day of multiday event or only day
|
||||
prv_copy_relative_time_string(number_buffer, number_buffer_size, word_buffer,
|
||||
word_buffer_size, timestamp, end_time);
|
||||
} else if (time_util_get_midnight_of(end_time) == current_day) {
|
||||
// last day of multiday event
|
||||
prv_copy_relative_time_string(number_buffer, number_buffer_size, word_buffer,
|
||||
word_buffer_size, end_time, end_time);
|
||||
} else {
|
||||
// middle day of non-all day multiday event
|
||||
prv_get_relative_all_day_string(word_buffer, word_buffer_size, current_day);
|
||||
strncpy(number_buffer, "", number_buffer_size);
|
||||
}
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(bool, clock_is_24h_style, void) {
|
||||
return shell_prefs_get_clock_24h_style();
|
||||
}
|
||||
|
||||
void clock_set_24h_style(bool is_24h_style) {
|
||||
shell_prefs_set_clock_24h_style(is_24h_style);
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(bool, clock_is_timezone_set, void) {
|
||||
return rtc_is_timezone_set(); // If timezone abbr isn't set
|
||||
}
|
||||
|
||||
bool clock_timezone_source_is_manual(void) {
|
||||
return shell_prefs_is_timezone_source_manual();
|
||||
}
|
||||
|
||||
void clock_set_manual_timezone_source(bool manual) {
|
||||
shell_prefs_set_timezone_source_manual(manual);
|
||||
}
|
||||
|
||||
time_t clock_to_timestamp(WeekDay day, int hour, int minute) {
|
||||
time_t t = sys_get_time();
|
||||
struct tm cal;
|
||||
sys_localtime_r(&t, &cal);
|
||||
|
||||
if (day != TODAY) {
|
||||
// If difference between WeekDay and current day, always in the future
|
||||
day -= 1; // cal_wday is 0-6
|
||||
int day_offset = (day > cal.tm_wday) ? (day - cal.tm_wday) : (day - cal.tm_wday + 7);
|
||||
cal.tm_mday += day_offset; // normalized by mktime
|
||||
} else if ((hour < cal.tm_hour) || (hour == cal.tm_hour && minute <= cal.tm_min)){
|
||||
// Always return a future timestamp, so if day was today, and
|
||||
// minutes and hours already occurred, just make it tomorrow
|
||||
cal.tm_mday++; // normalized by mktime
|
||||
}
|
||||
|
||||
cal.tm_hour = hour;
|
||||
cal.tm_min = minute;
|
||||
|
||||
return mktime(&cal);
|
||||
}
|
||||
|
||||
void command_timezone_clear(void) {
|
||||
rtc_timezone_clear();
|
||||
}
|
||||
|
||||
void command_get_time(void) {
|
||||
char buffer[80];
|
||||
char time_buffer[26];
|
||||
prompt_send_response_fmt(buffer, 80, "Time is now <%s>", rtc_get_time_string(time_buffer));
|
||||
}
|
||||
|
||||
void command_set_time(const char *arg) {
|
||||
time_t t = atoi(arg);
|
||||
if (t == 0) {
|
||||
prompt_send_response("Invalid length");
|
||||
return;
|
||||
}
|
||||
|
||||
prv_update_time_info_and_generate_event(&t, NULL);
|
||||
|
||||
char buffer[80];
|
||||
char time_buffer[26];
|
||||
prompt_send_response_fmt(buffer, 80, "Time is now <%s>", rtc_get_time_string(time_buffer));
|
||||
}
|
||||
|
||||
void clock_get_timezone_region(char* region_name, const size_t buffer_size) {
|
||||
if (!region_name) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (clock_is_timezone_set()) {
|
||||
int region_id = clock_get_timezone_region_id();
|
||||
if (region_id != UNKNOWN_TIMEZONE_ID) {
|
||||
timezone_database_load_region_name(region_id, region_name);
|
||||
} else {
|
||||
// Show something like UTC-4 or UTC-10.25
|
||||
// This will typically happen in the emulator when we know the UTC offset, but not
|
||||
// the timezone (fallback case).
|
||||
int gmt_offset_m = time_get_gmtoffset() / SECONDS_PER_MINUTE;
|
||||
int hour_offset = gmt_offset_m / MINUTES_PER_HOUR;
|
||||
|
||||
char min_buf[4] = {0};
|
||||
int min_offset_percent = ((ABS(gmt_offset_m) % MINUTES_PER_HOUR) * 100) / MINUTES_PER_HOUR;
|
||||
if (min_offset_percent) {
|
||||
snprintf(min_buf, sizeof(min_buf), ".%d", min_offset_percent);
|
||||
}
|
||||
snprintf(region_name, buffer_size, "UTC%+d%s", hour_offset, min_buf);
|
||||
}
|
||||
} else {
|
||||
strncpy(region_name, "---", buffer_size);
|
||||
}
|
||||
}
|
||||
|
||||
int16_t clock_get_timezone_region_id(void) {
|
||||
return rtc_get_timezone_id();
|
||||
}
|
||||
|
||||
void clock_set_timezone_by_region_id(uint16_t region_id) {
|
||||
TimezoneInfo tz_info;
|
||||
prv_clock_get_timezone_info_from_region_id(region_id, rtc_get_time(), &tz_info);
|
||||
prv_update_time_info_and_generate_event(NULL, &tz_info);
|
||||
}
|
||||
|
||||
void clock_get_friendly_date(char *buffer, int buf_size, time_t timestamp) {
|
||||
const time_t now = rtc_get_time();
|
||||
|
||||
const time_t midnight = time_util_get_midnight_of(timestamp);
|
||||
const time_t today_midnight = time_util_get_midnight_of(now);
|
||||
|
||||
if (midnight == today_midnight) {
|
||||
i18n_get_with_buffer("Today", buffer, buf_size);
|
||||
} else if (midnight == (today_midnight - SECONDS_PER_DAY)) {
|
||||
i18n_get_with_buffer("Yesterday", buffer, buf_size);
|
||||
} else if (midnight == (today_midnight + SECONDS_PER_DAY)) {
|
||||
i18n_get_with_buffer("Tomorrow", buffer, buf_size);
|
||||
} else if (midnight <= (today_midnight + (5 * SECONDS_PER_DAY))) {
|
||||
// Use weekday name up to 5 days in the future, aka "Sunday"
|
||||
prv_format_time(buffer, buf_size, i18n_noop("%A"), timestamp);
|
||||
} else {
|
||||
// Otherwise use "Month Day", aka "June 21"
|
||||
prv_format_time(buffer, buf_size, i18n_noop("%B %d"), timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
enum {
|
||||
RoundTypeHalfUp,
|
||||
RoundTypeHalfDown,
|
||||
RoundTypeAlwaysUp,
|
||||
RoundTypeAlwaysDown,
|
||||
};
|
||||
|
||||
static time_t prv_round(time_t round_me, time_t multiple, int round_type) {
|
||||
switch (round_type) {
|
||||
case RoundTypeHalfDown:
|
||||
return ((round_me + multiple / 2 - 1) / multiple) * multiple;
|
||||
case RoundTypeAlwaysUp:
|
||||
return ((round_me + multiple - 1) / multiple) * multiple;
|
||||
case RoundTypeAlwaysDown:
|
||||
return (round_me / multiple) * multiple;
|
||||
case RoundTypeHalfUp:
|
||||
default:
|
||||
return ((round_me + multiple / 2) / multiple) * multiple;
|
||||
}
|
||||
}
|
||||
|
||||
enum {
|
||||
FullStyleLower12h,
|
||||
FullStyleCapital12h,
|
||||
FullStyleLower24h,
|
||||
FullStyleCapital24h,
|
||||
};
|
||||
|
||||
static void prv_clock_get_full_relative_time(char *buffer, int buf_size, time_t timestamp,
|
||||
bool capitalized, bool with_fulltime) {
|
||||
time_t today_midnight = time_util_get_midnight_of(rtc_get_time());
|
||||
time_t timestamp_midnight = time_util_get_midnight_of(timestamp);
|
||||
time_t yesterday_midnight = time_util_get_midnight_of(rtc_get_time() - SECONDS_PER_DAY);
|
||||
time_t last_week_midnight = time_util_get_midnight_of(rtc_get_time() - SECONDS_PER_WEEK);
|
||||
time_t next_week_midnight = time_util_get_midnight_of(rtc_get_time() + SECONDS_PER_WEEK);
|
||||
|
||||
const char *time_fmt = NULL;
|
||||
int style;
|
||||
if (clock_is_24h_style()) {
|
||||
if (capitalized) {
|
||||
style = FullStyleCapital24h;
|
||||
} else {
|
||||
style = FullStyleLower24h;
|
||||
}
|
||||
} else {
|
||||
if (capitalized) {
|
||||
style = FullStyleCapital12h;
|
||||
} else {
|
||||
style = FullStyleLower12h;
|
||||
}
|
||||
}
|
||||
|
||||
if (timestamp_midnight == today_midnight) {
|
||||
switch (style) {
|
||||
case FullStyleLower12h:
|
||||
case FullStyleCapital12h:
|
||||
time_fmt = i18n_noop("%l:%M %p");
|
||||
break;
|
||||
case FullStyleLower24h:
|
||||
case FullStyleCapital24h:
|
||||
time_fmt = i18n_noop("%R");
|
||||
break;
|
||||
}
|
||||
} else if (timestamp_midnight == yesterday_midnight) {
|
||||
switch (style) {
|
||||
case FullStyleLower12h:
|
||||
case FullStyleCapital12h:
|
||||
if (with_fulltime) {
|
||||
time_fmt = i18n_noop("Yesterday, %l:%M %p");
|
||||
} else {
|
||||
time_fmt = i18n_noop("Yesterday");
|
||||
}
|
||||
break;
|
||||
case FullStyleLower24h:
|
||||
case FullStyleCapital24h:
|
||||
if (with_fulltime) {
|
||||
time_fmt = i18n_noop("Yesterday, %R");
|
||||
} else {
|
||||
time_fmt = i18n_noop("Yesterday");
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else if (timestamp_midnight <= last_week_midnight || timestamp_midnight >= next_week_midnight) {
|
||||
switch (style) {
|
||||
case FullStyleLower12h:
|
||||
case FullStyleCapital12h:
|
||||
if (with_fulltime) {
|
||||
time_fmt = i18n_noop("%b %e, %l:%M %p");
|
||||
} else {
|
||||
time_fmt = i18n_noop("%B %e");
|
||||
}
|
||||
break;
|
||||
case FullStyleLower24h:
|
||||
case FullStyleCapital24h:
|
||||
if (with_fulltime) {
|
||||
time_fmt = i18n_noop("%b %e, %R");
|
||||
} else {
|
||||
time_fmt = i18n_noop("%B %e");
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
switch (style) {
|
||||
case FullStyleLower12h:
|
||||
case FullStyleCapital12h:
|
||||
if (with_fulltime) {
|
||||
time_fmt = i18n_noop("%a, %l:%M %p");
|
||||
} else {
|
||||
time_fmt = i18n_noop("%A");
|
||||
}
|
||||
break;
|
||||
case FullStyleLower24h:
|
||||
case FullStyleCapital24h:
|
||||
if (with_fulltime) {
|
||||
time_fmt = i18n_noop("%a, %R");
|
||||
} else {
|
||||
time_fmt = i18n_noop("%A");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
prv_format_time(buffer, buf_size, time_fmt, timestamp);
|
||||
}
|
||||
|
||||
static void prv_clock_get_relative_time_string(char *buffer, int buf_size, time_t timestamp,
|
||||
bool capitalized, int max_relative_hrs,
|
||||
bool with_fulltime) {
|
||||
time_t difference = rtc_get_time() - timestamp;
|
||||
|
||||
time_t today_midnight = time_util_get_midnight_of(rtc_get_time());
|
||||
time_t timestamp_midnight = time_util_get_midnight_of(timestamp);
|
||||
|
||||
if (today_midnight != timestamp_midnight) {
|
||||
prv_clock_get_full_relative_time(buffer, buf_size, timestamp, capitalized, with_fulltime);
|
||||
|
||||
} else if (difference >= (SECONDS_PER_HOUR * max_relative_hrs)) {
|
||||
prv_clock_get_full_relative_time(buffer, buf_size, timestamp, capitalized, with_fulltime);
|
||||
|
||||
} else if (difference >= SECONDS_PER_HOUR) {
|
||||
const int num_hrs =
|
||||
prv_round(difference, SECONDS_PER_HOUR, RoundTypeHalfUp) / SECONDS_PER_HOUR;
|
||||
|
||||
const char *str_fmt;
|
||||
if (capitalized) {
|
||||
str_fmt = i18n_noop("%lu H AGO");
|
||||
} else if (num_hrs == 1) {
|
||||
str_fmt = i18n_noop("An hour ago");
|
||||
} else {
|
||||
str_fmt = i18n_noop("%lu hours ago");
|
||||
}
|
||||
snprintf(buffer, buf_size, i18n_get(str_fmt, buffer), num_hrs);
|
||||
|
||||
} else if (difference >= SECONDS_PER_MINUTE) {
|
||||
const int num_minutes =
|
||||
prv_round(difference, SECONDS_PER_MINUTE, RoundTypeAlwaysDown) / SECONDS_PER_MINUTE;
|
||||
|
||||
const char *str_fmt;
|
||||
if (capitalized) {
|
||||
str_fmt = i18n_noop("%lu MIN AGO");
|
||||
} else if (num_minutes == 1) {
|
||||
str_fmt = i18n_noop("%lu minute ago");
|
||||
} else {
|
||||
str_fmt = i18n_noop("%lu minutes ago");
|
||||
}
|
||||
snprintf(buffer, buf_size, i18n_get(str_fmt, buffer), num_minutes);
|
||||
|
||||
} else if (difference >= 0) {
|
||||
strncpy(buffer, capitalized ? i18n_get("NOW", buffer) : i18n_get("Now", buffer), buf_size);
|
||||
|
||||
} else if (difference >= -(SECONDS_PER_HOUR - SECONDS_PER_MINUTE)) {
|
||||
const int num_minutes =
|
||||
prv_round(-difference, SECONDS_PER_MINUTE, RoundTypeAlwaysUp) / SECONDS_PER_MINUTE;
|
||||
|
||||
const char *str_fmt;
|
||||
if (capitalized) {
|
||||
str_fmt = i18n_noop("IN %lu MIN");
|
||||
} else if (num_minutes == 1) {
|
||||
str_fmt = i18n_noop("In %lu minute");
|
||||
} else {
|
||||
str_fmt = i18n_noop("In %lu minutes");
|
||||
}
|
||||
snprintf(buffer, buf_size, i18n_get(str_fmt, buffer), num_minutes);
|
||||
|
||||
} else if (difference >= -(SECONDS_PER_HOUR * max_relative_hrs)) {
|
||||
const int num_hrs =
|
||||
prv_round(-difference, SECONDS_PER_HOUR, RoundTypeHalfDown) / SECONDS_PER_HOUR;
|
||||
|
||||
const char *str_fmt;
|
||||
if (capitalized) {
|
||||
str_fmt = i18n_noop("IN %lu H");
|
||||
} else if (num_hrs == 1) {
|
||||
str_fmt = i18n_noop("In %lu hour");
|
||||
} else {
|
||||
str_fmt = i18n_noop("In %lu hours");
|
||||
}
|
||||
snprintf(buffer, buf_size, i18n_get(str_fmt, buffer), num_hrs);
|
||||
|
||||
} else {
|
||||
prv_clock_get_full_relative_time(buffer, buf_size, timestamp, capitalized, with_fulltime);
|
||||
}
|
||||
i18n_free_all(buffer);
|
||||
}
|
||||
|
||||
size_t clock_get_date(char *buffer, int buf_size, time_t timestamp) {
|
||||
return prv_format_time(buffer, buf_size, i18n_noop("%m/%d"), timestamp);
|
||||
}
|
||||
|
||||
size_t clock_get_day_date(char *buffer, int buf_size, time_t timestamp) {
|
||||
return prv_format_time(buffer, buf_size, i18n_noop("%d"), timestamp);
|
||||
}
|
||||
|
||||
static size_t prv_clock_get_month_named_date(char *buffer, size_t buffer_size, time_t timestamp,
|
||||
bool abbrev) {
|
||||
const char *format = abbrev ? i18n_noop("%b ") : i18n_noop("%B ");
|
||||
const size_t month_size = prv_format_time(buffer, buffer_size, format, timestamp);
|
||||
char *day_buffer = buffer + month_size;
|
||||
const size_t day_buffer_size = buffer_size - month_size;
|
||||
size_t day_size = prv_format_time(day_buffer, day_buffer_size, i18n_noop("%e"), timestamp);
|
||||
const char *day_buffer_ptr = string_strip_leading_whitespace(day_buffer);
|
||||
memmove(day_buffer, day_buffer_ptr, day_buffer_size - (day_buffer_ptr - day_buffer));
|
||||
return month_size + day_size;
|
||||
}
|
||||
|
||||
size_t clock_get_month_named_date(char *buffer, size_t buffer_size, time_t timestamp) {
|
||||
const bool abbrev = false;
|
||||
return prv_clock_get_month_named_date(buffer, buffer_size, timestamp, abbrev);
|
||||
}
|
||||
|
||||
size_t clock_get_month_named_abbrev_date(char *buffer, size_t buffer_size, time_t timestamp) {
|
||||
const bool abbrev = true;
|
||||
return prv_clock_get_month_named_date(buffer, buffer_size, timestamp, abbrev);
|
||||
}
|
||||
|
||||
void clock_get_since_time(char *buffer, int buf_size, time_t timestamp) {
|
||||
const time_t now = rtc_get_time();
|
||||
const time_t clamped_timestamp = MIN(now, timestamp);
|
||||
prv_clock_get_relative_time_string(buffer, buf_size, clamped_timestamp, false, HOURS_PER_DAY,
|
||||
true);
|
||||
}
|
||||
|
||||
void clock_get_until_time(char *buffer, int buf_size, time_t timestamp,
|
||||
int max_relative_hrs) {
|
||||
prv_clock_get_relative_time_string(buffer, buf_size, timestamp, false, max_relative_hrs, true);
|
||||
}
|
||||
|
||||
void clock_get_until_time_capitalized(char *buffer, int buf_size, time_t timestamp,
|
||||
int max_relative_hrs) {
|
||||
prv_clock_get_relative_time_string(buffer, buf_size, timestamp, true, max_relative_hrs, true);
|
||||
}
|
||||
|
||||
void clock_get_until_time_without_fulltime(char *buffer, int buf_size, time_t timestamp,
|
||||
int max_relative_hrs) {
|
||||
prv_clock_get_relative_time_string(buffer, buf_size, timestamp, true, max_relative_hrs, false);
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(void, sys_clock_get_timezone, char *timezone, const size_t buffer_size) {
|
||||
if (PRIVILEGE_WAS_ELEVATED) {
|
||||
syscall_assert_userspace_buffer(timezone, TIMEZONE_NAME_LENGTH);
|
||||
}
|
||||
clock_get_timezone_region(timezone, buffer_size);
|
||||
}
|
||||
|
||||
typedef struct daypart_message {
|
||||
const uint32_t hour_offset; // hours from 12am of current day
|
||||
const char* const message; // text containing daypart
|
||||
} daypart_message;
|
||||
|
||||
static const daypart_message daypart_messages[] = {
|
||||
{0, i18n_noop("this morning")}, // anything before 12pm of the current day
|
||||
{12, i18n_noop("this afternoon")}, // 12pm today
|
||||
{18, i18n_noop("this evening")}, // 6pm today
|
||||
{21, i18n_noop("tonight")}, // 9pm today
|
||||
{33, i18n_noop("tomorrow morning")}, // 9am tomorrow
|
||||
{36, i18n_noop("tomorrow afternoon")}, // 12pm tomorrow
|
||||
{42, i18n_noop("tomorrow evening")}, // 6pm tomorrow
|
||||
{45, i18n_noop("tomorrow night")}, // 9pm tomorrow
|
||||
{57, i18n_noop("the day after tomorrow")}, // starting 9am 2 days from now
|
||||
{72, i18n_noop("the day after tomorrow")}, // ends midnight 2 days from now
|
||||
{73, i18n_noop("the foreseeable future")}, // Catchall for beyond 3 days
|
||||
};
|
||||
|
||||
//! Daypart string is used internally for battery popups
|
||||
//! and is a minimum threshold, ie. "Powered 'til at least"...
|
||||
const char *clock_get_relative_daypart_string(time_t current_timestamp,
|
||||
uint32_t hours_in_the_future) {
|
||||
struct tm current_tm;
|
||||
const char *message = NULL;
|
||||
localtime_r(¤t_timestamp, ¤t_tm);
|
||||
|
||||
// Look for the furthest time in the future that we are "above"
|
||||
for (int i = ARRAY_LENGTH(daypart_messages) - 1; i >= 0; i--) {
|
||||
if ((current_tm.tm_hour + hours_in_the_future) >= daypart_messages[i].hour_offset) {
|
||||
message = daypart_messages[i].message;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
void clock_hour_and_minute_add(int *hour, int *minute, int delta_minutes) {
|
||||
const int new_minutes = positive_modulo(*hour * MINUTES_PER_HOUR + *minute + delta_minutes,
|
||||
MINUTES_PER_DAY);
|
||||
*hour = new_minutes / MINUTES_PER_HOUR;
|
||||
*minute = new_minutes % MINUTES_PER_HOUR;
|
||||
}
|
208
src/fw/services/common/clock.h
Normal file
208
src/fw/services/common/clock.h
Normal file
|
@ -0,0 +1,208 @@
|
|||
/*
|
||||
* 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 <inttypes.h>
|
||||
#include "util/time/time.h"
|
||||
|
||||
//! @addtogroup Foundation
|
||||
//! @{
|
||||
//! @addtogroup WallTime Wall Time
|
||||
//! \brief Functions, data structures and other things related to wall clock time.
|
||||
//!
|
||||
//! This module contains utilities to get the current time and create strings with formatted
|
||||
//! dates and times.
|
||||
//! @{
|
||||
|
||||
//! The maximum length for a timezone full name (e.g. America/Chicago)
|
||||
#define TIMEZONE_NAME_LENGTH 32
|
||||
// Large enough for common usages like "Wednesday" or "30 minutes ago"
|
||||
#define TIME_STRING_REQUIRED_LENGTH 20
|
||||
// Large enough for time. e.g. 14:20
|
||||
#define TIME_STRING_TIME_LENGTH 10
|
||||
// Large enough for day/mo e.g. 04/27
|
||||
#define TIME_STRING_DATE_LENGTH 10
|
||||
// Large enough for day e.g. 27
|
||||
#define TIME_STRING_DAY_DATE_LENGTH 3
|
||||
|
||||
//! Weekday values
|
||||
typedef enum {
|
||||
TODAY = 0, //!< Today
|
||||
SUNDAY, //!< Sunday
|
||||
MONDAY, //!< Monday
|
||||
TUESDAY, //!< Tuesday
|
||||
WEDNESDAY, //!< Wednesday
|
||||
THURSDAY, //!< Thursday
|
||||
FRIDAY, //!< Friday
|
||||
SATURDAY, //!< Saturday
|
||||
} WeekDay;
|
||||
|
||||
//! @internal
|
||||
//! Initialize clock service
|
||||
void clock_init(void);
|
||||
|
||||
//! @internal
|
||||
void clock_get_time_tm(struct tm* time_tm);
|
||||
|
||||
//! @internal
|
||||
//! @param add_space whether to add a space between the time and AM/PM
|
||||
size_t clock_format_time(char *buffer, uint8_t size, int16_t hours, int16_t minutes,
|
||||
bool add_space);
|
||||
|
||||
//! Same as \ref clock_copy_time_string, but with a supplied timestamp
|
||||
size_t clock_copy_time_string_timestamp(char *buffer, uint8_t size, time_t timestamp);
|
||||
|
||||
//! Copies a time string into the buffer, formatted according to the user's time display preferences (such as 12h/24h
|
||||
//! time).
|
||||
//! Example results: "7:30" or "15:00".
|
||||
//! @note AM/PM are also outputted with the time if the user's preference is 12h time.
|
||||
//! @param[out] buffer A pointer to the buffer to copy the time string into
|
||||
//! @param size The maximum size of buffer
|
||||
void clock_copy_time_string(char *buffer, uint8_t size);
|
||||
|
||||
//! Gets the time formatted as "7:30" or "15:00" depending on the user's 12/24h clock setting
|
||||
//! @note AM/PM is not outputted. Use in combination with \ref clock_get_time_word.
|
||||
size_t clock_get_time_number(char *buffer, size_t buffer_size, time_t timestamp);
|
||||
|
||||
//! Gets AM/PM or sets the first character to '\0' depending on the user's 12/24h clock setting
|
||||
//! @note Use in combination with \ref clock_get_time_number to get a full hour minute timestamp
|
||||
size_t clock_get_time_word(char *buffer, size_t buffer_size, time_t timestamp);
|
||||
|
||||
//! Get the relative time string of an event, e.g. "10 min. ago", with "10" and " min ago"
|
||||
//! copied into separate buffers so they can be rendered in different fonts
|
||||
void clock_get_event_relative_time_string(char *number_buffer, int number_buffer_size,
|
||||
char *word_buffer, int word_buffer_size, time_t timestamp, uint16_t duration,
|
||||
time_t current_day, bool all_day);
|
||||
|
||||
//! Gets the user's 12/24h clock style preference.
|
||||
//! @return `true` if the user prefers 24h-style time display or `false` if the
|
||||
//! user prefers 12h-style time display.
|
||||
bool clock_is_24h_style(void);
|
||||
|
||||
//! @internal
|
||||
//! Sets the user's time display style.
|
||||
//! @param is_24h_style True means 24h style, false means 12h style.
|
||||
void clock_set_24h_style(bool is_24h_style);
|
||||
|
||||
//! Checks if timezone is currently set, otherwise gmtime == localtime.
|
||||
//! @return `true` if timezone has been set, false otherwise
|
||||
bool clock_is_timezone_set(void);
|
||||
|
||||
//! @internal
|
||||
//! Checks the timezone source. If the source is manual, the user must select the timezone from
|
||||
//! the settings menu, if the source is automatic the timezone will be set by the phone.
|
||||
//! @return true if the user has a manual timezone set, false if the timezone is set by the phone
|
||||
bool clock_timezone_source_is_manual(void);
|
||||
|
||||
//! @internal
|
||||
//! Sets the timezone source. If the source is manual, the user must select the timezone from
|
||||
//! the settings menu, if the source is automatic the timezone will be set by the phone.
|
||||
//! @param manual True means a manually selected timezone will be used,
|
||||
//! false means the phone's timezone will be used
|
||||
void clock_set_manual_timezone_source(bool manual);
|
||||
|
||||
//! @internal
|
||||
//! If timezone is set, copies the current timezone long name (e.g. America/Chicago)
|
||||
//! to buffer region_name.
|
||||
//! @param timezone A pointer to the buffer to copy the timezone long name into
|
||||
//! @param buffer_size Size of the allocated buffer to copy the timezone long name into
|
||||
//! @note region_name size should be at least TIMEZONE_NAME_LENGTH bytes
|
||||
void clock_get_timezone_region(char *region_name, const size_t buffer_size);
|
||||
|
||||
//! Function to retrieve the current timezone's region_id
|
||||
//! @return the index of the current timezone in terms of the timezone database
|
||||
int16_t clock_get_timezone_region_id(void);
|
||||
|
||||
//! Function to set the watch to the selected timezone region_id
|
||||
//! @param region_id the index of the selected timezone in terms of the timezone database
|
||||
void clock_set_timezone_by_region_id(uint16_t region_id);
|
||||
|
||||
//! Converts a (day, hour, minute) specification to a UTC timestamp occurring in the future
|
||||
//! Always returns a timestamp for the next occurring instance,
|
||||
//! example: specifying TODAY@14:30 when it is 14:40 will return a timestamp for 7 days from
|
||||
//! now at 14:30
|
||||
//! @note This function does not support Daylight Saving Time (DST) changes, events scheduled
|
||||
//! during a DST change will be off by an hour.
|
||||
//! @param day WeekDay day of week including support for specifying TODAY
|
||||
//! @param hour hour specified in 24-hour format [0-23]
|
||||
//! @param minute minute [0-59]
|
||||
time_t clock_to_timestamp(WeekDay day, int hour, int minute);
|
||||
|
||||
//! Get a friendly date out of a timestamp (e.g. "Today", "Tomorrow")
|
||||
//! @param buffer buffer to output the friendly date into
|
||||
//! @param buf_size size of the buffer
|
||||
//! @param timestamp timestamp to get a friendly date for
|
||||
void clock_get_friendly_date(char *buffer, int buf_size, time_t timestamp);
|
||||
|
||||
//! Get a friendly "time since" out of a timestamp (e.g. "Just now", "5 minutes ago")
|
||||
//! @param buffer buffer to output the friendly time into
|
||||
//! @param buf_size size of the buffer
|
||||
//! @param timestamp timestamp to get a friendly time for
|
||||
void clock_get_since_time(char *buffer, int buf_size, time_t timestamp);
|
||||
|
||||
//! Get a friendly "time to" out of a timestamp (e.g. "Now", "In 5 hours")
|
||||
//! @param buffer buffer to output the friendly time into
|
||||
//! @param buf_size size of the buffer
|
||||
//! @param timestamp timestamp to get a friendly time for
|
||||
//! @param max_relative_hrs how many hours for which it should show "IN X HOURS"
|
||||
void clock_get_until_time(char *buffer, int buf_size, time_t timestamp,
|
||||
int max_relative_hrs);
|
||||
|
||||
//! Get a friendly "time to" out of a timestamp, without ever writing the real time
|
||||
//! @param buffer buffer to output the friendly time into
|
||||
//! @param buf_size size of the buffer
|
||||
//! @param timestamp timestamp to get a friendly time for
|
||||
//! @param max_relative_hrs how many hours for which it should show "IN X HOURS"
|
||||
void clock_get_until_time_without_fulltime(char *buffer, int buf_size, time_t timestamp,
|
||||
int max_relative_hrs);
|
||||
|
||||
//! Get the date in MM/DD format
|
||||
size_t clock_get_date(char *buffer, int buf_size, time_t timestamp);
|
||||
|
||||
//! Get the day date in DD format
|
||||
size_t clock_get_day_date(char *buffer, int buf_size, time_t timestamp);
|
||||
|
||||
//! Get the date in Month DD format (e.g. "July 16")
|
||||
size_t clock_get_month_named_date(char *buffer, size_t buffer_size, time_t timestamp);
|
||||
|
||||
//! Get the date in Mon DD format (e.g. "Jul 16")
|
||||
size_t clock_get_month_named_abbrev_date(char *buffer, size_t buffer_size, time_t timestamp);
|
||||
|
||||
//! Get a friendly capitalized "time to" out of a timestamp (e.g. "NOW", "IN 5 HOURS")
|
||||
//! @param buffer buffer to output the friendly time into
|
||||
//! @param buf_size size of the buffer
|
||||
//! @param timestamp timestamp to get a friendly time for
|
||||
//! @param max_relative_hrs how many hours for which it should show "IN X HOURS"
|
||||
void clock_get_until_time_capitalized(char *buffer, int buf_size, time_t timestamp,
|
||||
int max_relative_hrs);
|
||||
|
||||
//! @} // end addtogroup WallTime
|
||||
//! @} // end addtogroup Foundation
|
||||
|
||||
//! Get a textual daypart message out of a timestamp and time offset (in the future)
|
||||
//! Example: current_time + 5 hours returns -> "this evening"
|
||||
//! Note: This function was provided to provide daypart message for today and tomorrow
|
||||
//! and provides a single phrase for the day after as well as a catchall beyond that
|
||||
//! @param current_timestamp timestamp for the current time
|
||||
//! @param hours_in_the_future hours after current_timestamp used to select the daypart message
|
||||
//! @return const text containing daypart phrase
|
||||
const char *clock_get_relative_daypart_string(time_t current_timestamp,
|
||||
uint32_t hours_in_the_future);
|
||||
|
||||
//! Adds minutes to wall clock time, wrapping around 24 hours.
|
||||
void clock_hour_and_minute_add(int *hour, int *minute, int delta_minutes);
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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/common/comm_session/session.h"
|
||||
#include "util/uuid.h"
|
||||
|
||||
//! @param capability The capability to check for.
|
||||
//! @returns True if the session for the current application supports the capability of interest.
|
||||
//! If the session is currently not connected, it will use cached data. If no cache exists
|
||||
//! and the session is not connected, false will be returned.
|
||||
bool comm_session_current_app_session_cache_has_capability(CommSessionCapability capability);
|
||||
|
||||
//! Removes the cached app session capabilities for app with specified uuid.
|
||||
void comm_session_app_session_capabilities_evict(const Uuid *app_uuid);
|
||||
|
||||
void comm_session_app_session_capabilities_init(void);
|
149
src/fw/services/common/comm_session/default_kernel_receiver.c
Normal file
149
src/fw/services/common/comm_session/default_kernel_receiver.c
Normal file
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* 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 "session_receive_router.h"
|
||||
|
||||
#include "kernel/event_loop.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "services/common/comm_session/session.h"
|
||||
#include "services/common/system_task.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
|
||||
#include <inttypes.h>
|
||||
|
||||
//! Default option for the kernel receiver, execute the endpoint handler on KernelBG.
|
||||
const PebbleTask g_default_kernel_receiver_opt_bg = PebbleTask_KernelBackground;
|
||||
|
||||
//! If the endpoint handler puts events onto the KernelMain queue *and* it is important that
|
||||
//! PEBBLE_COMM_SESSION_EVENT and your endpoint's events are handled in order, use this
|
||||
//! receiver option in the protocol_endpoints_table.json:
|
||||
const PebbleTask g_default_kernel_receiver_opt_main = PebbleTask_KernelMain;
|
||||
|
||||
// A common pattern for endpoint handlers it to:
|
||||
// 1) Kernel malloc a buffer & copy Pebble Protocol payload to it
|
||||
// 2) Schedule a callback on KernelBG/Main to run the code that decodes the payload
|
||||
// (because the handler runs from BT02, a high priority thread
|
||||
// 3) Free malloc'ed buffer
|
||||
// Leaving this up to each individual endpoint wastes code and creates more
|
||||
// opportunity for memory leaks. This file contains an implementation that
|
||||
// different endpoints can use to achieve this pattern.
|
||||
//
|
||||
// Note: Since the buffer is malloc'ed on the kernel heap, the expected consumer
|
||||
// for this receiver is the system (not an app). However, it might be
|
||||
// receiving messages *from* a PebbleKit app that the system is supposed
|
||||
// to handle. For example, app run state commands (i.e. "app launch") are
|
||||
// sent by PebbleKit apps, but get handled by the system.
|
||||
|
||||
typedef struct {
|
||||
CommSession *session;
|
||||
const PebbleProtocolEndpoint *endpoint;
|
||||
size_t total_payload_size;
|
||||
int curr_pos;
|
||||
bool handler_scheduled;
|
||||
bool should_use_kernel_main;
|
||||
uint8_t payload[];
|
||||
} DefaultReceiverImpl;
|
||||
|
||||
static Receiver *prv_default_kernel_receiver_prepare(
|
||||
CommSession *session, const PebbleProtocolEndpoint *endpoint,
|
||||
size_t total_payload_size) {
|
||||
if (total_payload_size == 0) {
|
||||
return NULL; // Ignore zero-length messages
|
||||
}
|
||||
|
||||
size_t size_needed = sizeof(DefaultReceiverImpl) + total_payload_size;
|
||||
DefaultReceiverImpl *receiver = kernel_zalloc(size_needed);
|
||||
|
||||
if (!receiver) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Could not allocate receiver, handler:%p size:%d",
|
||||
endpoint->handler, (int)size_needed);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const bool should_use_kernel_main =
|
||||
(endpoint->receiver_opt == &g_default_kernel_receiver_opt_main);
|
||||
*receiver = (DefaultReceiverImpl) {
|
||||
.session = session,
|
||||
.endpoint = endpoint,
|
||||
.total_payload_size = total_payload_size,
|
||||
.should_use_kernel_main = should_use_kernel_main,
|
||||
.curr_pos = 0
|
||||
};
|
||||
|
||||
return (Receiver *)receiver;
|
||||
}
|
||||
|
||||
static void prv_default_kernel_receiver_write(
|
||||
Receiver *receiver, const uint8_t *data, size_t length) {
|
||||
DefaultReceiverImpl *impl = (DefaultReceiverImpl *)receiver;
|
||||
|
||||
PBL_ASSERTN((impl->curr_pos + length) <= impl->total_payload_size);
|
||||
memcpy(impl->payload + impl->curr_pos, data, length);
|
||||
|
||||
impl->curr_pos += length;
|
||||
}
|
||||
|
||||
static void prv_wipe_receiver_data(DefaultReceiverImpl *receiver) {
|
||||
*receiver = (DefaultReceiverImpl) { };
|
||||
}
|
||||
|
||||
static void prv_default_kernel_receiver_cb(void *data) {
|
||||
DefaultReceiverImpl *impl = (DefaultReceiverImpl *)data;
|
||||
PBL_ASSERTN(impl && impl->handler_scheduled && impl->session);
|
||||
|
||||
impl->endpoint->handler(impl->session, impl->payload, impl->total_payload_size);
|
||||
|
||||
prv_wipe_receiver_data(impl);
|
||||
kernel_free(impl);
|
||||
}
|
||||
|
||||
static void prv_default_kernel_receiver_finish(Receiver *receiver) {
|
||||
DefaultReceiverImpl *impl = (DefaultReceiverImpl *)receiver;
|
||||
impl->handler_scheduled = true;
|
||||
|
||||
if ((int)impl->total_payload_size != impl->curr_pos) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Got fewer bytes than expected for handler %p",
|
||||
impl->endpoint->handler);
|
||||
}
|
||||
|
||||
// Note: At the moment we unconditionally spawn a new callback upon
|
||||
// completion of each individual payload. If we are getting a flood of
|
||||
// events, this may generate too many CBs and overflow the queue. We could keep a list
|
||||
// of pending receiver events and only schedule the CB if there isn't one
|
||||
// already pending
|
||||
if (impl->should_use_kernel_main) {
|
||||
launcher_task_add_callback(prv_default_kernel_receiver_cb, receiver);
|
||||
} else {
|
||||
system_task_add_callback(prv_default_kernel_receiver_cb, receiver);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_default_kernel_receiver_cleanup(Receiver *receiver) {
|
||||
DefaultReceiverImpl *impl = (DefaultReceiverImpl *)receiver;
|
||||
if (impl->handler_scheduled) {
|
||||
return; // the kernel BG/main callback will free the data
|
||||
}
|
||||
prv_wipe_receiver_data(impl);
|
||||
kernel_free(impl);
|
||||
}
|
||||
|
||||
const ReceiverImplementation g_default_kernel_receiver_implementation = {
|
||||
.prepare = prv_default_kernel_receiver_prepare,
|
||||
.write = prv_default_kernel_receiver_write,
|
||||
.finish = prv_default_kernel_receiver_finish,
|
||||
.cleanup = prv_default_kernel_receiver_cleanup,
|
||||
};
|
309
src/fw/services/common/comm_session/default_kernel_sender.c
Normal file
309
src/fw/services/common/comm_session/default_kernel_sender.c
Normal file
|
@ -0,0 +1,309 @@
|
|||
/*
|
||||
* 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 "comm/bt_lock.h"
|
||||
#include "drivers/rtc.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "services/common/analytics/analytics.h"
|
||||
#include "services/common/comm_session/protocol.h"
|
||||
#include "services/common/comm_session/session_send_buffer.h"
|
||||
#include "services/common/comm_session/session_send_queue.h"
|
||||
#include "system/logging.h"
|
||||
#include "util/attributes.h"
|
||||
#include "util/likely.h"
|
||||
#include "util/math.h"
|
||||
#include "util/net.h"
|
||||
|
||||
#include "FreeRTOS.h"
|
||||
#include "semphr.h"
|
||||
|
||||
typedef struct SendBuffer {
|
||||
//! Save some memory by making this a union.
|
||||
//! @note This is the first field, so that we can just cast between
|
||||
//! (SendBuffer *) and (SessionSendQueueJob *)
|
||||
union {
|
||||
//! The targeted session, this field is valid until ..._write_end has been called.
|
||||
CommSession *session;
|
||||
|
||||
//! This fields is valid after ...write_end has returned.
|
||||
SessionSendQueueJob queue_job;
|
||||
};
|
||||
|
||||
//! Length of payload[] in bytes
|
||||
size_t payload_buffer_length;
|
||||
|
||||
//! It's tempting to use header.length, but this is big endian... :(
|
||||
size_t written_length;
|
||||
|
||||
//! Number of bytes that have been consumed so far
|
||||
size_t consumed_length;
|
||||
|
||||
struct PACKED {
|
||||
//! The remainder of this struct is the Pebble Protocol message (header + payload):
|
||||
PebbleProtocolHeader header;
|
||||
uint8_t payload[];
|
||||
};
|
||||
} SendBuffer;
|
||||
|
||||
#define DEFAULT_KERNEL_SENDER_MAX_PAYLOAD_SIZE ((size_t)1024)
|
||||
|
||||
//! @note This does not include sizeof(SendBuffer) by design, to avoid letting the implementation
|
||||
//! affect the maximum number of (smaller) Pebble Protocol messages can be allocated. For example,
|
||||
//! the Audio endpoint likes to send out a stream of small Pebble Protocol messages. We don't want
|
||||
//! to accidentally cut the max number when sizeof(SendBuffer) would increase for whatever reason.
|
||||
//!
|
||||
//! @note We leave it up to the caller of the exported comm_session_send_* APIs to implement a
|
||||
//! retry mechanism when we are OOM. A lot of callers just implicitly assume things will work and
|
||||
//! the payload get dropped on the floor.
|
||||
#if BT_CONTROLLER_DA14681
|
||||
#define DEFAULT_KERNEL_SENDER_MAX_BYTES_ALLOCATED \
|
||||
((sizeof(PebbleProtocolHeader) + DEFAULT_KERNEL_SENDER_MAX_PAYLOAD_SIZE) * 4)
|
||||
#else
|
||||
//! TODO: I don't know where we stand heap wise on older platforms like spalding. We don't really
|
||||
//! have any analytics in place to track this. Before changing the behavior, let's back it with
|
||||
//! some data. For now ... live and let live
|
||||
#define DEFAULT_KERNEL_SENDER_MAX_BYTES_ALLOCATED \
|
||||
((sizeof(PebbleProtocolHeader) + DEFAULT_KERNEL_SENDER_MAX_PAYLOAD_SIZE))
|
||||
#endif
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
//! Semaphore that is signaled when data has been consumed by the transport,
|
||||
//! when it calls to comm_default_kernel_sender_consume(). This semaphore is used to block calls to
|
||||
//! comm_session_send_buffer_begin_write() in case there is not enough space left.
|
||||
//! @note This semaphore *must never* be taken when bt_lock() is held or deadlock will happen!
|
||||
//! Giving the semaphore when bt_lock() is held is fine though.
|
||||
static SemaphoreHandle_t s_default_kernel_sender_write_semaphore;
|
||||
|
||||
//! Total number of bytes worth of Pebble Protocol messages (incl. header) allocated by this module.
|
||||
//! This excludes sizeof(SendBuffer), see comment with DEFAULT_KERNEL_SENDER_MAX_BYTES_ALLOCATED.
|
||||
static size_t s_default_kernel_sender_bytes_allocated;
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
extern bool comm_session_is_current_task_send_next_task(CommSession *session);
|
||||
extern bool comm_session_is_valid(const CommSession *session);
|
||||
extern void comm_session_send_next_immediately(CommSession *session);
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
//! To be called once at boot
|
||||
void comm_default_kernel_sender_init(void) {
|
||||
s_default_kernel_sender_write_semaphore = xSemaphoreCreateBinary();
|
||||
}
|
||||
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
|
||||
static uint32_t prv_remaining_ms(uint32_t timeout_ms_in, RtcTicks start_ticks) {
|
||||
const RtcTicks now = rtc_get_ticks();
|
||||
const uint32_t elapsed_ms = (((now - start_ticks) * 1000) / RTC_TICKS_HZ);
|
||||
if (timeout_ms_in > elapsed_ms) {
|
||||
return timeout_ms_in - elapsed_ms;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static SendBuffer *prv_create_send_buffer(CommSession *session, uint16_t endpoint_id,
|
||||
size_t payload_buffer_length) {
|
||||
bt_lock_assert_held(true /* assert_is_held */);
|
||||
const size_t num_bytes_allocated_after = (s_default_kernel_sender_bytes_allocated +
|
||||
sizeof(PebbleProtocolHeader) + payload_buffer_length);
|
||||
if (num_bytes_allocated_after > DEFAULT_KERNEL_SENDER_MAX_BYTES_ALLOCATED) {
|
||||
return NULL;
|
||||
}
|
||||
const size_t allocation_size = (sizeof(SendBuffer) + payload_buffer_length);
|
||||
s_default_kernel_sender_bytes_allocated = num_bytes_allocated_after;
|
||||
// Use ...alloc_check() here. If this appears to be an issue, we could consider giving this
|
||||
// module its own Heap:
|
||||
SendBuffer *sb = (SendBuffer *)kernel_zalloc_check(allocation_size);
|
||||
*sb = (const SendBuffer) {
|
||||
.payload_buffer_length = payload_buffer_length,
|
||||
.consumed_length = 0,
|
||||
.session = session,
|
||||
.header = {
|
||||
.endpoint_id = htons(endpoint_id),
|
||||
.length = 0,
|
||||
},
|
||||
};
|
||||
return sb;
|
||||
}
|
||||
|
||||
static void prv_destroy_send_buffer(SendBuffer *sb) {
|
||||
bt_lock_assert_held(true /* assert_is_held */);
|
||||
s_default_kernel_sender_bytes_allocated -= (sizeof(PebbleProtocolHeader)
|
||||
+ sb->payload_buffer_length);
|
||||
kernel_free(sb);
|
||||
xSemaphoreGive(s_default_kernel_sender_write_semaphore);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Interfaces towards Send Queue:
|
||||
|
||||
static size_t prv_get_remaining_length(const SendBuffer *sb) {
|
||||
return (sizeof(PebbleProtocolHeader) + sb->written_length - sb->consumed_length);
|
||||
}
|
||||
|
||||
static const uint8_t *prv_get_read_pointer(const SendBuffer *sb) {
|
||||
return ((const uint8_t *)&sb->header + sb->consumed_length);
|
||||
}
|
||||
|
||||
static size_t prv_send_job_impl_get_length(const SessionSendQueueJob *send_job) {
|
||||
return prv_get_remaining_length((SendBuffer *)send_job);
|
||||
}
|
||||
|
||||
static size_t prv_send_job_impl_copy(const SessionSendQueueJob *send_job, int start_offset,
|
||||
size_t length, uint8_t *data_out) {
|
||||
SendBuffer *sb = (SendBuffer *)send_job;
|
||||
const size_t length_remaining = prv_get_remaining_length(sb);
|
||||
const size_t length_after_offset = (length_remaining - start_offset);
|
||||
const size_t length_to_copy = MIN(length_after_offset, length);
|
||||
memcpy(data_out, prv_get_read_pointer(sb) + start_offset, length_to_copy);
|
||||
return length_to_copy;
|
||||
}
|
||||
|
||||
static size_t prv_send_job_impl_get_read_pointer(const SessionSendQueueJob *send_job,
|
||||
const uint8_t **data_out) {
|
||||
SendBuffer *sb = (SendBuffer *)send_job;
|
||||
*data_out = prv_get_read_pointer(sb);
|
||||
return prv_get_remaining_length(sb);
|
||||
}
|
||||
|
||||
static void prv_send_job_impl_consume(const SessionSendQueueJob *send_job, size_t length) {
|
||||
SendBuffer *sb = (SendBuffer *)send_job;
|
||||
sb->consumed_length += length;
|
||||
}
|
||||
|
||||
static void prv_send_job_impl_free(SessionSendQueueJob *send_job) {
|
||||
prv_destroy_send_buffer((SendBuffer *)send_job);
|
||||
}
|
||||
|
||||
T_STATIC const SessionSendJobImpl s_default_kernel_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 subsystems that need to send data out
|
||||
|
||||
size_t comm_session_send_buffer_get_max_payload_length(const CommSession *session) {
|
||||
size_t max_length = 0;
|
||||
bt_lock();
|
||||
{
|
||||
if (comm_session_is_valid(session)) {
|
||||
max_length = DEFAULT_KERNEL_SENDER_MAX_PAYLOAD_SIZE;
|
||||
}
|
||||
}
|
||||
bt_unlock();
|
||||
return max_length;
|
||||
}
|
||||
|
||||
SendBuffer * comm_session_send_buffer_begin_write(CommSession *session, uint16_t endpoint_id,
|
||||
size_t required_payload_length,
|
||||
uint32_t timeout_ms) {
|
||||
if (!session) {
|
||||
return NULL;
|
||||
}
|
||||
if (required_payload_length > DEFAULT_KERNEL_SENDER_MAX_PAYLOAD_SIZE) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING,
|
||||
"Message for endpoint_id %u exceeds maximum length (length=%"PRIu32")",
|
||||
endpoint_id, (uint32_t)required_payload_length);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
RtcTicks start_ticks = rtc_get_ticks();
|
||||
SendBuffer *sb = NULL;
|
||||
|
||||
while (true) {
|
||||
bool is_timeout = false;
|
||||
bool is_current_task_send_next_task;
|
||||
|
||||
bt_lock();
|
||||
{
|
||||
if (!comm_session_is_valid(session)) {
|
||||
bt_unlock();
|
||||
return NULL;
|
||||
}
|
||||
sb = prv_create_send_buffer(session, endpoint_id, required_payload_length);
|
||||
is_current_task_send_next_task = comm_session_is_current_task_send_next_task(session);
|
||||
}
|
||||
bt_unlock();
|
||||
|
||||
if (sb) {
|
||||
return sb;
|
||||
}
|
||||
|
||||
// Check for timeout
|
||||
uint32_t remaining_ms = prv_remaining_ms(timeout_ms, start_ticks);
|
||||
if (remaining_ms == 0) {
|
||||
is_timeout = true;
|
||||
} else {
|
||||
if (is_current_task_send_next_task) {
|
||||
// If there is no space and this is called from the task that performs the sending,
|
||||
// the "send_next" callback is waiting in the task queue after this callback.
|
||||
// Therefore, data will never get sent out unless it's done right now:
|
||||
comm_session_send_next_immediately(session);
|
||||
} else {
|
||||
// Wait for the sending process to free up some space in the send buffer:
|
||||
is_timeout = (xSemaphoreTake(s_default_kernel_sender_write_semaphore,
|
||||
remaining_ms) == pdFALSE);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_timeout) {
|
||||
analytics_inc(ANALYTICS_DEVICE_METRIC_BT_COMM_SESSION_SEND_DATA_FAIL_COUNT,
|
||||
AnalyticsClient_System);
|
||||
|
||||
PBL_LOG(LOG_LEVEL_WARNING,
|
||||
"Failed to get send buffer (bytes=%"PRIu32", endpoint_id=%"PRIu16", to=%"PRIu32")",
|
||||
(uint32_t)required_payload_length, endpoint_id, (uint32_t)is_timeout);
|
||||
return NULL;
|
||||
}
|
||||
} // while(true)
|
||||
}
|
||||
|
||||
bool comm_session_send_buffer_write(SendBuffer *sb, const uint8_t *data, size_t length) {
|
||||
if (UNLIKELY((sb->payload_buffer_length - sb->written_length) < length)) {
|
||||
return false;
|
||||
}
|
||||
memcpy(sb->payload + sb->header.length + sb->written_length, data, length);
|
||||
sb->written_length += length;
|
||||
return true;
|
||||
}
|
||||
|
||||
void comm_session_send_buffer_end_write(SendBuffer *sb) {
|
||||
CommSession *session = sb->session;
|
||||
// Clear out the ListNode and set impl:
|
||||
sb->queue_job = (const SessionSendQueueJob) {
|
||||
.impl = &s_default_kernel_send_job_impl,
|
||||
};
|
||||
sb->header.length = ntohs(sb->written_length);
|
||||
comm_session_send_queue_add_job(session, (SessionSendQueueJob **)&sb);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Interfaces for testing
|
||||
|
||||
SemaphoreHandle_t comm_session_send_buffer_write_semaphore(void) {
|
||||
return s_default_kernel_sender_write_semaphore;
|
||||
}
|
||||
|
||||
void comm_default_kernel_sender_deinit(void) {
|
||||
vSemaphoreDelete(s_default_kernel_sender_write_semaphore);
|
||||
s_default_kernel_sender_write_semaphore = NULL;
|
||||
}
|
19
src/fw/services/common/comm_session/default_kernel_sender.h
Normal file
19
src/fw/services/common/comm_session/default_kernel_sender.h
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
void comm_default_kernel_sender_init(void);
|
58
src/fw/services/common/comm_session/meta_endpoint.c
Normal file
58
src/fw/services/common/comm_session/meta_endpoint.c
Normal file
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "meta_endpoint.h"
|
||||
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "services/common/system_task.h"
|
||||
#include "system/logging.h"
|
||||
#include "util/net.h"
|
||||
|
||||
static const uint16_t META_ENDPOINT_ID = 0;
|
||||
|
||||
static void prv_send_meta_response_kernelbg_cb(void *data) {
|
||||
MetaResponseInfo *meta_response_info_heap_copy = data;
|
||||
|
||||
// Swap endpoint_id bytes to be Big-Endian:
|
||||
meta_response_info_heap_copy->payload.endpoint_id =
|
||||
htons(meta_response_info_heap_copy->payload.endpoint_id);
|
||||
|
||||
uint16_t payload_size;
|
||||
if (meta_response_info_heap_copy->payload.error_code == MetaResponseCodeCorruptedMessage) {
|
||||
payload_size = sizeof(meta_response_info_heap_copy->payload.error_code);
|
||||
} else {
|
||||
payload_size = sizeof(meta_response_info_heap_copy->payload);
|
||||
}
|
||||
|
||||
comm_session_send_data(meta_response_info_heap_copy->session, META_ENDPOINT_ID,
|
||||
(const uint8_t *)&meta_response_info_heap_copy->payload,
|
||||
payload_size, COMM_SESSION_DEFAULT_TIMEOUT);
|
||||
|
||||
kernel_free(meta_response_info_heap_copy);
|
||||
}
|
||||
|
||||
void meta_endpoint_send_response_async(const MetaResponseInfo *meta_response_info) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Meta protocol error: 0x%x (endpoint=%u)",
|
||||
meta_response_info->payload.error_code, meta_response_info->payload.endpoint_id);
|
||||
|
||||
MetaResponseInfo *meta_response_info_heap_copy = kernel_zalloc_check(sizeof(*meta_response_info));
|
||||
memcpy(meta_response_info_heap_copy, meta_response_info, sizeof(*meta_response_info));
|
||||
system_task_add_callback(prv_send_meta_response_kernelbg_cb, meta_response_info_heap_copy);
|
||||
}
|
||||
|
||||
void meta_protocol_msg_callback(CommSession *session, const uint8_t* data, size_t length) {
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Meta endpoint callback called");
|
||||
}
|
43
src/fw/services/common/comm_session/meta_endpoint.h
Normal file
43
src/fw/services/common/comm_session/meta_endpoint.h
Normal file
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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/common/comm_session/session.h"
|
||||
#include "util/attributes.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef enum {
|
||||
MetaResponseCodeNoError = 0x0,
|
||||
MetaResponseCodeCorruptedMessage = 0xd0,
|
||||
MetaResponseCodeDisallowed = 0xdd,
|
||||
MetaResponseCodeUnhandled = 0xdc,
|
||||
} MetaResponseCode;
|
||||
|
||||
typedef struct MetaResponseInfo {
|
||||
CommSession *session;
|
||||
struct PACKED {
|
||||
//! @see MetaResponseCode
|
||||
uint8_t error_code;
|
||||
uint16_t endpoint_id;
|
||||
} payload;
|
||||
} MetaResponseInfo;
|
||||
|
||||
//! Sends out a response for the "meta" endpoint, asynchronously on KernelBG.
|
||||
//! @note The endpoint_id must be set in Little Endian byte order. This function will take care
|
||||
//! of swapping it to the correct endianness.
|
||||
void meta_endpoint_send_response_async(const MetaResponseInfo *meta_response_info);
|
30
src/fw/services/common/comm_session/protocol.h
Normal file
30
src/fw/services/common/comm_session/protocol.h
Normal file
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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>
|
||||
|
||||
typedef struct PACKED {
|
||||
uint16_t length;
|
||||
uint16_t endpoint_id;
|
||||
} PebbleProtocolHeader;
|
||||
|
||||
#define COMM_PRIVATE_MAX_INBOUND_PAYLOAD_SIZE 2044
|
||||
#define COMM_PUBLIC_MAX_INBOUND_PAYLOAD_SIZE 144
|
||||
// TODO: If we have memory to spare, let's crank this up to improve data spooling
|
||||
#define COMM_MAX_OUTBOUND_PAYLOAD_SIZE 656
|
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"comments": [
|
||||
"Only endpoints that need to be accessible from PRF (Recovery) ",
|
||||
"should be added to 'prf_and_normal_fw'. ",
|
||||
"The first column is the endpoint ID as decimal number. ",
|
||||
"The second column is the endpoint ID as hex. It's just for ",
|
||||
"reference, though it will be asserted to match. ",
|
||||
"The third column is the access setting: ",
|
||||
"Use 'any' only if 3rd party mobile apps (using PebbleKit) are ",
|
||||
"allowed to talk directly to the endpoint, or 'private' if only ",
|
||||
"the Pebble mobile app is allowed to talk to it. ",
|
||||
"The fourth column is the callback function handling inbound ",
|
||||
"messages for the endpoint. ",
|
||||
"The fifth column is the ReceiverImplementation that should be ",
|
||||
"used with the endpoint. Specify `null` to select the default.",
|
||||
"The sixth column is the context for that will be passed along when the ",
|
||||
"ReceiverImplementation is called. This provides a way to tweak the ",
|
||||
"behavior of a receiver slightly on a per-endpoint basis. See notes ",
|
||||
"in default_kernel_receiver.c on the default options."
|
||||
],
|
||||
"prf_and_normal_fw": [
|
||||
[ 0, "0x0000", "any", "meta_protocol_msg_callback", null, null ],
|
||||
[ 16, "0x0010", "any", "system_version_protocol_msg_callback", null, "g_default_kernel_receiver_opt_main" ],
|
||||
[ 17, "0x0011", "any", "session_remote_version_protocol_msg_callback", null, "g_default_kernel_receiver_opt_main" ],
|
||||
[ 18, "0x0012", "private", "sys_msg_protocol_msg_callback", null, "g_default_kernel_receiver_opt_main" ],
|
||||
[ 2002, "0x07d2", "private", "dump_log_protocol_msg_callback", null, null ],
|
||||
[ 2003, "0x07d3", "private", "reset_protocol_msg_callback", null, null ],
|
||||
[ 5001, "0x1389", "private", "factory_registry_protocol_msg_callback", null, null ],
|
||||
[ 9000, "0x2328", "private", "get_bytes_protocol_msg_callback", null, null ],
|
||||
[ 48879, "0xbeef", "private", null, "g_put_bytes_receiver_impl", null ]
|
||||
],
|
||||
"normal_fw_only": [
|
||||
[ 11, "0x000b", "private", "clock_protocol_msg_callback", null, null ],
|
||||
[ 32, "0x0020", "private", "music_protocol_msg_callback", null, "g_default_kernel_receiver_opt_main" ],
|
||||
[ 33, "0x0021", "private", "phone_protocol_msg_callback", null, "g_default_kernel_receiver_opt_main" ],
|
||||
[ 48, "0x0030", "any", null, "g_app_message_receiver_implementation", null ],
|
||||
[ 49, "0x0031", "any", "launcher_app_message_protocol_msg_callback_deprecated", null, null ],
|
||||
[ 50, "0x0032", "any", "customizable_app_protocol_msg_callback", null, null ],
|
||||
[ 51, "0x0033", "private", "pp_ble_control_protocol_msg_callback", null, null ],
|
||||
[ 52, "0x0034", "any", "app_run_state_protocol_msg_callback", null, null ],
|
||||
[ 911, "0x038f", "private", "health_sync_protocol_msg_callback", null, null ],
|
||||
[ 2001, "0x07d1", "any", "ping_protocol_msg_callback", null, null ],
|
||||
[ 2006, "0x07d6", "private", "app_log_protocol_msg_callback", null, null ],
|
||||
[ 6001, "0x1771", "private", "app_fetch_protocol_msg_callback", null, null ],
|
||||
[ 6778, "0x1a7a", "private", "data_logging_protocol_msg_callback", null, null ],
|
||||
[ 8000, "0x1f40", "private", "screenshot_protocol_msg_callback", null, null ],
|
||||
[ 10000, "0x2710", "private", "audio_endpoint_protocol_msg_callback", null, null ],
|
||||
[ 11000, "0x2af8", "private", "voice_endpoint_protocol_msg_callback", null, null ],
|
||||
[ 11440, "0x2cb0", "private", "timeline_action_endpoint_protocol_msg_callback", null, null ],
|
||||
[ 43981, "0xabcd", "private", "app_order_protocol_msg_callback", null, null ],
|
||||
[ 45531, "0xb1db", "private", "blob_db_protocol_msg_callback", null, null ],
|
||||
[ 45787, "0xb2db", "private", "blob_db2_protocol_msg_callback", null, null ],
|
||||
[ 51966, "0xcafe", "any", "comm_poll_remote_protocol_msg_callback", null, null ]
|
||||
]
|
||||
}
|
582
src/fw/services/common/comm_session/session.c
Normal file
582
src/fw/services/common/comm_session/session.c
Normal file
|
@ -0,0 +1,582 @@
|
|||
/*
|
||||
* 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/bt_driver_comm.h>
|
||||
|
||||
#include "services/common/comm_session/session.h"
|
||||
#include "session_analytics.h"
|
||||
#include "session_internal.h"
|
||||
#include "session_transport.h"
|
||||
|
||||
#include "applib/app_comm.h"
|
||||
#include "comm/ble/kernel_le_client/app_launch/app_launch.h"
|
||||
#include "comm/bt_lock.h"
|
||||
#include "services/common/bluetooth/bluetooth_persistent_storage.h"
|
||||
#include "services/common/comm_session/protocol.h"
|
||||
#include "services/common/comm_session/session_remote_version.h"
|
||||
#include "services/common/comm_session/session_send_buffer.h"
|
||||
|
||||
#include "kernel/events.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
|
||||
#include "process_management/app_manager.h"
|
||||
|
||||
#include "services/common/system_task.h"
|
||||
#include "services/normal/data_logging/dls_private.h"
|
||||
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
#include "syscall/syscall_internal.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Static variables
|
||||
|
||||
//! The list of open Pebble Protocol sessions.
|
||||
//! @note bt_lock() must be held when accessing this list.
|
||||
static CommSession *s_session_head;
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Defined in session_send_buffer.c
|
||||
|
||||
extern SendBuffer * comm_session_send_buffer_create(TransportDestination destination);
|
||||
extern void comm_session_send_buffer_destroy(SendBuffer *sb);
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Defined in session_receive_router.c
|
||||
extern void comm_session_receive_router_cleanup(CommSession *session);
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Defined in session_send_queue.c
|
||||
extern void comm_session_send_queue_cleanup(CommSession *session);
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
static void prv_put_comm_session_event(bool is_open, bool is_system) {
|
||||
PebbleEvent event = {
|
||||
.type = PEBBLE_COMM_SESSION_EVENT,
|
||||
.bluetooth.comm_session_event.is_open = is_open,
|
||||
.bluetooth.comm_session_event
|
||||
.is_system = is_system,
|
||||
};
|
||||
event_put(&event);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Extern'd interface for session_send_buffer.c and session_remote_version.c
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
//! bt_lock() is expected to be taken by the caller!
|
||||
bool comm_session_is_valid(const CommSession *session) {
|
||||
return list_contains((ListNode *) s_session_head, &session->node);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
//! Extern'd interface for protocol.c
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
bool comm_session_has_capability(CommSession *session, CommSessionCapability capability) {
|
||||
bool rv = false;
|
||||
bt_lock();
|
||||
if (comm_session_is_valid(session)) {
|
||||
rv = (session->protocol_capabilities & capability) != 0;
|
||||
}
|
||||
bt_unlock();
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
CommSessionCapability comm_session_get_capabilities(CommSession *session) {
|
||||
CommSessionCapability capabilities = 0;
|
||||
bt_lock();
|
||||
if (comm_session_is_valid(session)) {
|
||||
capabilities = session->protocol_capabilities;
|
||||
}
|
||||
bt_unlock();
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
void comm_session_set_capabilities(CommSession *session, CommSessionCapability capability_flags) {
|
||||
bt_lock();
|
||||
if (comm_session_is_valid(session)) {
|
||||
session->protocol_capabilities = capability_flags;
|
||||
}
|
||||
bt_unlock();
|
||||
|
||||
if (comm_session_is_system(session)) {
|
||||
const PebbleProtocolCapabilities capabilities = { .flags = capability_flags };
|
||||
bt_persistent_storage_set_cached_system_capabilities(&capabilities);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//! Resets the session (close and attempt re-opening the session)
|
||||
//! @note If the underlying transport is iAP, this will end up closing all the sessions on top of
|
||||
//! the transport, since we don't really have the ability to close a single iAP session.
|
||||
void comm_session_reset(CommSession *session) {
|
||||
bt_lock();
|
||||
{
|
||||
if (!comm_session_is_valid(session)) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Already closed!");
|
||||
goto unlock;
|
||||
}
|
||||
session->transport_imp->reset(session->transport);
|
||||
}
|
||||
unlock:
|
||||
bt_unlock();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Interfaces towards Transport (reading from the send buffer to actually transmit the data):
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
static const Uuid *prv_get_uuid(const CommSession *session) {
|
||||
if (session->transport_imp->get_uuid) {
|
||||
return session->transport_imp->get_uuid(session->transport);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static const char *prv_string_for_destination(TransportDestination destination) {
|
||||
switch (destination) {
|
||||
case TransportDestinationSystem: return "S";
|
||||
case TransportDestinationApp: return "A";
|
||||
case TransportDestinationHybrid: return "H";
|
||||
default:
|
||||
WTF;
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_log_session_event(CommSession *session, bool is_open) {
|
||||
char uuid_string[UUID_STRING_BUFFER_LENGTH];
|
||||
uuid_to_string(prv_get_uuid(session), uuid_string);
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Session event: is_open=%d, destination=%s, app_uuid=%s",
|
||||
is_open, prv_string_for_destination(session->destination), uuid_string);
|
||||
}
|
||||
|
||||
static bool prv_is_transport_type(Transport *transport,
|
||||
const TransportImplementation *implementation,
|
||||
CommSessionTransportType expected_transport_type) {
|
||||
CommSessionTransportType transport_type = implementation->get_type(transport);
|
||||
return transport_type == expected_transport_type;
|
||||
}
|
||||
|
||||
//! bt_lock() is expected to be taken by the caller!
|
||||
CommSession * comm_session_open(Transport *transport, const TransportImplementation *implementation,
|
||||
TransportDestination destination) {
|
||||
|
||||
const bool is_system = (destination != TransportDestinationApp);
|
||||
if (is_system) {
|
||||
CommSession *existing_system_session = comm_session_get_system_session();
|
||||
if (existing_system_session) {
|
||||
// Allow PULSE transport to be opened alongside any other transport
|
||||
// Actually using PULSE at the same time as another transport may cause
|
||||
// undesirable behaviour however.
|
||||
if (!prv_is_transport_type(existing_system_session->transport,
|
||||
existing_system_session->transport_imp,
|
||||
CommSessionTransportType_PULSE)
|
||||
&& !prv_is_transport_type(transport, implementation, CommSessionTransportType_PULSE)) {
|
||||
if (!existing_system_session->transport_imp->close) {
|
||||
// iAP sessions cannot be closed from the watch' side :(
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "System session already exists and cannot be closed");
|
||||
return NULL;
|
||||
}
|
||||
// Last system session to connect wins:
|
||||
// This is to work-around a race condition that happens when iOS still has the PPoGATT service
|
||||
// registered (the app has crashed / jettisoned) and iSPP is connected but the system session
|
||||
// is running over PPoGATT. If the app launches again, it will have no state of what was the
|
||||
// previously used transport was, prior to getting killed. Often, iAP ends up winning.
|
||||
// However, to the firmware, PPoGATT still appears connected, so we'd end up here.
|
||||
PBL_LOG(LOG_LEVEL_INFO, "System session already exists, closing it now");
|
||||
existing_system_session->transport_imp->close(existing_system_session->transport);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CommSession *session = kernel_malloc(sizeof(CommSession));
|
||||
if (!session) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Not enough memory for new CommSession");
|
||||
return NULL;
|
||||
}
|
||||
*session = (const CommSession) {
|
||||
.transport = transport,
|
||||
.transport_imp = implementation,
|
||||
.destination = destination,
|
||||
};
|
||||
|
||||
s_session_head = (CommSession *) list_prepend((ListNode *) s_session_head, &session->node);
|
||||
|
||||
prv_log_session_event(session, true /* is_open */);
|
||||
|
||||
// Request capabilities for both the Pebble app and 3rd party companion apps:
|
||||
session_remote_version_start_requests(session);
|
||||
|
||||
comm_session_analytics_open_session(session);
|
||||
|
||||
prv_put_comm_session_event(true, is_system);
|
||||
|
||||
if (is_system && (session->destination == TransportDestinationHybrid)) {
|
||||
// For Android, if the app is connected, PebbleKit should be
|
||||
// working as well
|
||||
prv_put_comm_session_event(true, false);
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
//! bt_lock() is expected to be taken by the caller!
|
||||
void comm_session_close(CommSession *session, CommSessionCloseReason reason) {
|
||||
PBL_ASSERTN(comm_session_is_valid(session));
|
||||
|
||||
prv_log_session_event(session, false /* is_open */);
|
||||
|
||||
comm_session_analytics_close_session(session, reason);
|
||||
|
||||
const bool is_system = (session->destination != TransportDestinationApp);
|
||||
if (is_system) {
|
||||
|
||||
// Only relevant for iOS + BLE, otherwise this is a no-op:
|
||||
app_launch_trigger();
|
||||
|
||||
// TODO: PBL-1771: find a more graceful way to handle this
|
||||
#ifndef RECOVERY_FW
|
||||
system_task_add_callback(dls_private_handle_disconnect, NULL);
|
||||
#endif
|
||||
}
|
||||
|
||||
prv_put_comm_session_event(false, is_system);
|
||||
|
||||
if (is_system && (session->destination == TransportDestinationHybrid)) {
|
||||
prv_put_comm_session_event(true, false);
|
||||
}
|
||||
|
||||
// Cleanup:
|
||||
comm_session_receive_router_cleanup(session);
|
||||
comm_session_send_queue_cleanup(session);
|
||||
list_remove(&session->node, (ListNode **) &s_session_head, NULL);
|
||||
kernel_free(session);
|
||||
}
|
||||
|
||||
void comm_session_set_responsiveness(
|
||||
CommSession *session, BtConsumer consumer, ResponseTimeState state,
|
||||
uint16_t max_period_secs) {
|
||||
comm_session_set_responsiveness_ext(session, consumer, state, max_period_secs, NULL);
|
||||
}
|
||||
|
||||
void comm_session_set_responsiveness_ext(CommSession *session, BtConsumer consumer,
|
||||
ResponseTimeState state, uint16_t max_period_secs,
|
||||
ResponsivenessGrantedHandler granted_handler) {
|
||||
if (session) {
|
||||
bt_lock();
|
||||
if (comm_session_is_valid(session)) {
|
||||
session->transport_imp->set_connection_responsiveness(session->transport, consumer,
|
||||
state, max_period_secs,
|
||||
granted_handler);
|
||||
}
|
||||
bt_unlock();
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
bool comm_session_is_current_task_send_next_task(CommSession *session) {
|
||||
if (session->transport_imp->schedule) {
|
||||
return session->transport_imp->is_current_task_schedule_task(session->transport);
|
||||
}
|
||||
return bt_driver_comm_is_current_task_send_next_task();
|
||||
}
|
||||
|
||||
void prv_send_next(CommSession *session, bool is_callback) {
|
||||
bt_lock();
|
||||
{
|
||||
if (!comm_session_is_valid(session)) {
|
||||
// Session closed in the mean time
|
||||
goto unlock;
|
||||
}
|
||||
// Flip the flag before the send_next callback, so it can schedule again if needed.
|
||||
// Only flip the flag, if this called as a thread callback, to avoid getting more
|
||||
// of these callbacks scheduled.
|
||||
if (is_callback) {
|
||||
session->is_send_next_call_pending = false;
|
||||
}
|
||||
|
||||
// Kick the transport to send out the next bytes it has prepared. It's possible these bytes
|
||||
// are not in the send queue (i.e PPoGATT Acks) so we leave it up to the transport to check
|
||||
// that
|
||||
session->transport_imp->send_next(session->transport);
|
||||
}
|
||||
unlock:
|
||||
bt_unlock();
|
||||
}
|
||||
|
||||
void bt_driver_run_send_next_job(CommSession *session, bool is_callback) {
|
||||
prv_send_next(session, is_callback);
|
||||
}
|
||||
|
||||
//! bt_lock() is expected to be taken by the caller!
|
||||
void comm_session_send_next(CommSession *session) {
|
||||
if (session->is_send_next_call_pending) {
|
||||
return;
|
||||
}
|
||||
|
||||
TransportSchedule schedule_func = session->transport_imp->schedule;
|
||||
if (!schedule_func) {
|
||||
schedule_func = bt_driver_comm_schedule_send_next_job;
|
||||
}
|
||||
|
||||
if (schedule_func(session)) {
|
||||
session->is_send_next_call_pending = true;
|
||||
} else {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Failed to schedule comm_session_send_next callback");
|
||||
}
|
||||
}
|
||||
|
||||
//! extern'd for session_send_buffer.c
|
||||
void comm_session_send_next_immediately(CommSession *session) {
|
||||
prv_send_next(session, false /* is_callback */);
|
||||
}
|
||||
|
||||
//! For unit test
|
||||
//! bt_lock() is expected to be taken by the caller!
|
||||
bool comm_session_send_next_is_scheduled(CommSession *session) {
|
||||
return session->is_send_next_call_pending;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Interface towards the system / subsystems that need to receive and send data
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
bool comm_session_send_data(CommSession *session, uint16_t endpoint_id,
|
||||
const uint8_t *data, size_t length, uint32_t timeout_ms) {
|
||||
if (!session) {
|
||||
return false;
|
||||
}
|
||||
SendBuffer *sb = comm_session_send_buffer_begin_write(session, endpoint_id, length, timeout_ms);
|
||||
if (!sb) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Could not acquire send buffer for %x", endpoint_id);
|
||||
return false;
|
||||
}
|
||||
comm_session_send_buffer_write(sb, data, length);
|
||||
comm_session_send_buffer_end_write(sb);
|
||||
return true;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
typedef struct {
|
||||
const Uuid *app_uuid;
|
||||
CommSession *fallback_session;
|
||||
} FindByAppUUIDContext;
|
||||
|
||||
static bool prv_find_session_by_app_uuid_comparator(ListNode *found_node, void *data) {
|
||||
CommSession *session = (CommSession *)found_node;
|
||||
FindByAppUUIDContext *ctx = data;
|
||||
const Uuid *session_uuid = prv_get_uuid(session);
|
||||
if (uuid_equal(session_uuid, ctx->app_uuid)) {
|
||||
// Match on UUID found!
|
||||
return true;
|
||||
}
|
||||
// If there is no valid UUID, it means we don't know what app UUID is associated with the
|
||||
// transport, consider it as a fallback option:
|
||||
const bool is_unknown_app_session = (session->destination == TransportDestinationApp &&
|
||||
uuid_is_invalid(session_uuid));
|
||||
const bool is_hybrid_session = (session->destination == TransportDestinationHybrid);
|
||||
if (is_hybrid_session || is_unknown_app_session) {
|
||||
// On Android + SPP, we can expect one Hybrid session, so we assume that the found session is
|
||||
// the hybrid one.
|
||||
// On iOS + iAP, we can expect at most one App session, so we assume that the found session is
|
||||
// the app one.
|
||||
if (ctx->fallback_session) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Fallback session already set!?");
|
||||
}
|
||||
ctx->fallback_session = session;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static CommSession *prv_get_app_session(void) {
|
||||
const Uuid *app_uuid = &app_manager_get_current_app_md()->uuid;
|
||||
if (uuid_is_system(app_uuid) || uuid_is_invalid(app_uuid)) {
|
||||
return NULL;
|
||||
}
|
||||
FindByAppUUIDContext ctx = (FindByAppUUIDContext) {
|
||||
.app_uuid = app_uuid,
|
||||
};
|
||||
// Try most specific first:
|
||||
CommSession *session = (CommSession *) list_find((ListNode *) s_session_head,
|
||||
prv_find_session_by_app_uuid_comparator, &ctx);
|
||||
if (!session) {
|
||||
return ctx.fallback_session;
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
static bool prv_find_session_is_system_filter(ListNode *found_node, void *data) {
|
||||
CommSession *session = (CommSession *) found_node;
|
||||
const TransportDestination destination = session->destination;
|
||||
return (destination == TransportDestinationSystem || destination == TransportDestinationHybrid)
|
||||
&& !prv_is_transport_type(session->transport, session->transport_imp,
|
||||
CommSessionTransportType_QEMU)
|
||||
&& !prv_is_transport_type(session->transport, session->transport_imp,
|
||||
CommSessionTransportType_PULSE);
|
||||
}
|
||||
|
||||
static bool prv_find_session_is_type_filter(ListNode *found_node, void *data) {
|
||||
CommSession *session = (CommSession *) found_node;
|
||||
CommSessionTransportType required_session_type = (CommSessionTransportType) data;
|
||||
return prv_is_transport_type(session->transport, session->transport_imp, required_session_type);
|
||||
}
|
||||
|
||||
static CommSession *prv_find_session_by_type(CommSessionTransportType session_type) {
|
||||
return (CommSession *) list_find((ListNode *) s_session_head,
|
||||
prv_find_session_is_type_filter, (void*)session_type);
|
||||
}
|
||||
|
||||
static CommSession *prv_get_system_session(void) {
|
||||
// Attempt to explicitly find and return a session that isn't QEMU or PULSE
|
||||
CommSession *session = (CommSession *) list_find((ListNode *) s_session_head,
|
||||
prv_find_session_is_system_filter, NULL);
|
||||
if (session) {
|
||||
return session;
|
||||
}
|
||||
|
||||
// If we don't find one, try to find a PULSE session
|
||||
session = prv_find_session_by_type(CommSessionTransportType_PULSE);
|
||||
if (session) {
|
||||
return session;
|
||||
}
|
||||
|
||||
// If we don't find one, try to find a QEMU session as a last resort
|
||||
return prv_find_session_by_type(CommSessionTransportType_QEMU);
|
||||
}
|
||||
|
||||
static CommSession *prv_get_session_by_type(CommSessionType type) {
|
||||
switch (type) {
|
||||
case CommSessionTypeSystem:
|
||||
return prv_get_system_session();
|
||||
case CommSessionTypeApp:
|
||||
return prv_get_app_session();
|
||||
case CommSessionTypeInvalid:
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
const Uuid *comm_session_get_uuid(const CommSession *session) {
|
||||
bt_lock_assert_held(true);
|
||||
return prv_get_uuid(session);
|
||||
}
|
||||
|
||||
CommSession *comm_session_get_by_type(CommSessionType type) {
|
||||
CommSession *session;
|
||||
bt_lock();
|
||||
{
|
||||
session = prv_get_session_by_type(type);
|
||||
}
|
||||
bt_unlock();
|
||||
return session;
|
||||
}
|
||||
|
||||
CommSession *comm_session_get_system_session(void) {
|
||||
return comm_session_get_by_type(CommSessionTypeSystem);
|
||||
}
|
||||
|
||||
CommSession *comm_session_get_current_app_session(void) {
|
||||
if (app_manager_get_current_app_md()->allow_js) {
|
||||
return comm_session_get_system_session();
|
||||
}
|
||||
return comm_session_get_by_type(CommSessionTypeApp);
|
||||
}
|
||||
|
||||
void comm_session_sanitize_app_session(CommSession **session_in_out) {
|
||||
CommSession *permitted_session = comm_session_get_current_app_session();
|
||||
if (!permitted_session) {
|
||||
// No session connected that can serve the currently running app
|
||||
*session_in_out = NULL;
|
||||
return;
|
||||
}
|
||||
if (*session_in_out == NULL) {
|
||||
// NULL means "auto select" the session
|
||||
*session_in_out = permitted_session;
|
||||
return;
|
||||
}
|
||||
if (*session_in_out != permitted_session) {
|
||||
// Don't allow the app to send data to any arbitrary session, this can happen if the session
|
||||
// got disconnected in the mean time.
|
||||
*session_in_out = NULL;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
CommSessionType comm_session_get_type(const CommSession *session) {
|
||||
CommSessionType type = CommSessionTypeInvalid;
|
||||
bt_lock();
|
||||
{
|
||||
if (comm_session_is_valid(session)) {
|
||||
type = (session->destination == TransportDestinationApp) ? CommSessionTypeApp :
|
||||
CommSessionTypeSystem;
|
||||
}
|
||||
}
|
||||
bt_unlock();
|
||||
return type;
|
||||
}
|
||||
|
||||
bool comm_session_is_system(CommSession* session) {
|
||||
return (comm_session_get_type(session) == CommSessionTypeSystem);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
//! Must (only) be called when going out of airplane mode (enabling Bluetooth).
|
||||
void comm_session_init(void) {
|
||||
PBL_ASSERTN(s_session_head == NULL);
|
||||
}
|
||||
|
||||
//! Must (only) be called when going into airplane mode (disabling Bluetooth).
|
||||
void comm_session_deinit(void) {
|
||||
// If this assert fires, it means a Transport has not cleaned up properly after itself by closing
|
||||
// all the CommSessions it has opened.
|
||||
PBL_ASSERTN(s_session_head == NULL);
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(void, sys_app_comm_set_responsiveness, SniffInterval interval) {
|
||||
|
||||
CommSession *comm_session = comm_session_get_current_app_session();
|
||||
switch (interval) {
|
||||
case SNIFF_INTERVAL_REDUCED:
|
||||
comm_session_set_responsiveness(comm_session, BtConsumerApp,
|
||||
ResponseTimeMiddle, MAX_PERIOD_RUN_FOREVER);
|
||||
return;
|
||||
case SNIFF_INTERVAL_NORMAL:
|
||||
comm_session_set_responsiveness(comm_session, BtConsumerApp,
|
||||
ResponseTimeMax, 0);
|
||||
return;
|
||||
}
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Invalid sniff interval");
|
||||
syscall_failed();
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(bool, sys_system_pp_has_capability, CommSessionCapability capability) {
|
||||
CommSession *session = comm_session_get_system_session();
|
||||
return comm_session_has_capability(session, capability);
|
||||
}
|
137
src/fw/services/common/comm_session/session.h
Normal file
137
src/fw/services/common/comm_session/session.h
Normal file
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
* 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 "comm/bt_conn_mgr.h"
|
||||
|
||||
//! CommSession represents a Pebble Protocol communication session. It attempts to abstract away the
|
||||
//! differences in the underlying data Transport types (iAP over SPP for iOS, plain SPP for Android,
|
||||
//! PPoGATT for BLE, QEMU ...)
|
||||
//! There are two types of sessions: the system session and the app session.
|
||||
//! The system session must be used when communicating to the Pebble app. There can only be one
|
||||
//! system session at a time. On Android, the system session uses a "hybrid" transport, which means
|
||||
//! that it also connects to PebbleKit apps (via the Pebble Android app).
|
||||
//! With iAP/PPoGATT, an app session is a dedicated Pebble Protocol session, connecting directly to
|
||||
//! a 3rd party phone app. With PPoGATT, there can be multiple transports and thus multiple app
|
||||
//! sessions at a time. With iAP transport, it's different. There can only be one iAP-based app
|
||||
//! session.
|
||||
|
||||
typedef struct CommSession CommSession;
|
||||
|
||||
typedef enum {
|
||||
CommSessionTypeInvalid = -1,
|
||||
CommSessionTypeSystem = 0,
|
||||
CommSessionTypeApp = 1,
|
||||
NumCommSessions,
|
||||
} CommSessionType;
|
||||
|
||||
// Note: The FW packs the capabilities it supports in the PebbleProtocolCapabilities struct
|
||||
typedef enum {
|
||||
CommSessionRunState = 1 << 0,
|
||||
CommSessionInfiniteLogDumping = 1 << 1,
|
||||
CommSessionExtendedMusicService = 1 << 2,
|
||||
CommSessionExtendedNotificationService = 1 << 3,
|
||||
CommSessionLanguagePackSupport = 1 << 4,
|
||||
CommSessionAppMessage8kSupport = 1 << 5,
|
||||
CommSessionActivityInsightsSupport = 1 << 6,
|
||||
CommSessionVoiceApiSupport = 1 << 7,
|
||||
CommSessionSendTextSupport = 1 << 8,
|
||||
CommSessionNotificationFilteringSupport = 1 << 9,
|
||||
CommSessionUnreadCoredumpSupport = 1 << 10,
|
||||
CommSessionWeatherAppSupport = 1 << 11,
|
||||
CommSessionRemindersAppSupport = 1 << 12,
|
||||
CommSessionWorkoutAppSupport = 1 << 13,
|
||||
CommSessionSmoothFwInstallProgressSupport = 1 << 14,
|
||||
CommSessionOutOfRange
|
||||
} CommSessionCapability;
|
||||
|
||||
#define COMM_SESSION_DEFAULT_TIMEOUT (4000)
|
||||
|
||||
//! @return whether the specified capability is supported by the session provided
|
||||
bool comm_session_has_capability(CommSession *session, CommSessionCapability capability);
|
||||
|
||||
//! @return Capabilities bitset by the provided session.
|
||||
CommSessionCapability comm_session_get_capabilities(CommSession *session);
|
||||
|
||||
//! @return a reference to the system (Pebble app) communication session, or NULL if the session
|
||||
//! does not exist (is not connected).
|
||||
//! @note It is possible that the session becomes disconnected at any point in time.
|
||||
CommSession *comm_session_get_system_session(void);
|
||||
|
||||
//! @return a reference to the the third party app communication session for the *currently running*
|
||||
//! watch app, or NULL if the session does not exist (is not connected).
|
||||
//! @note It is possible that the session becomes disconnected at any point in time.
|
||||
CommSession *comm_session_get_current_app_session(void);
|
||||
|
||||
//! @param session_in_out[in, out] Pass in a pointer to session pointer to sanitize it. The current
|
||||
//! *session value can be NULL, to "auto-select" the session for the currently running app.
|
||||
//! After returning, if *session_in_out was non-NULL when passed in, it will be unchanged if the app
|
||||
//! is permitted to use it. If not, it *session_in_out will be set to NULL.
|
||||
//! If *session_in_out was NULL when passed, but there is no session available, it will stay NULL.
|
||||
void comm_session_sanitize_app_session(CommSession **session_in_out);
|
||||
|
||||
//! @return the type of the given session
|
||||
CommSessionType comm_session_get_type(const CommSession *session);
|
||||
|
||||
//! @return the session of the requested type, or NULL if the session does not exist.
|
||||
//! @note If CommSessionTypeApp is passed in, the *currently running app* session will be returned,
|
||||
//! if it exists.
|
||||
CommSession *comm_session_get_by_type(CommSessionType type);
|
||||
|
||||
//! @returns a pointer to the UUID of the session, or NULL if the UUID is not known.
|
||||
//! @note The caller is expected to hold bt_lock! After bt_unlock(), the pointer should no longer
|
||||
//! be used!
|
||||
const Uuid *comm_session_get_uuid(const CommSession *session);
|
||||
|
||||
//! @return True if the session is the system session
|
||||
bool comm_session_is_system(CommSession *session);
|
||||
|
||||
//! Resets the session (close and attempt re-opening the session)
|
||||
//! @note If the underlying transport is iAP, this will end up closing all the sessions on top of
|
||||
//! the transport, since we don't really have the ability to close a single iAP session.
|
||||
void comm_session_reset(CommSession *session);
|
||||
|
||||
//! Convenience function to send data to session for given endpoint id.
|
||||
//! Note, this is implemented by calling comm_session_send_buffer_begin_write(),
|
||||
//! comm_session_send_buffer_write() and comm_session_send_buffer_end_write().
|
||||
//! This will allocate a buffer on the kernel-heap to store the message. If you want to avoid
|
||||
//! this, use comm_session_send_queue_add_job() directly.
|
||||
//! If you want to write parts of a Pebble Protocol message piece-meal, it's probably better to use
|
||||
//! these functions directly instead of this.
|
||||
//! @param session The session to use
|
||||
//! @param endpoint_id Which endpoint to send the pebble protocol message to.
|
||||
//! @param data Pointer to the buffer with data to send
|
||||
//! @param length The length of the data
|
||||
//! @param timeout The duration for how long the call is allowed to block. If the send buffer does
|
||||
//! not have enough space available to enqueue the data, this function will block up to timeout_ms.
|
||||
//! @return true if the data was successfully queued up for sending.
|
||||
bool comm_session_send_data(CommSession *session, uint16_t endpoint_id,
|
||||
const uint8_t *data, size_t length, uint32_t timeout_ms);
|
||||
|
||||
//! See bt_conn_mgr.h for more details on the parameters
|
||||
void comm_session_set_responsiveness(
|
||||
CommSession *session, BtConsumer consumer, ResponseTimeState state, uint16_t max_period_secs);
|
||||
|
||||
//! See bt_conn_mgr.h for more details on the parameters
|
||||
void comm_session_set_responsiveness_ext(CommSession *session, BtConsumer consumer,
|
||||
ResponseTimeState state, uint16_t max_period_secs,
|
||||
ResponsivenessGrantedHandler granted_handler);
|
||||
|
||||
void comm_session_init(void);
|
92
src/fw/services/common/comm_session/session_analytics.c
Normal file
92
src/fw/services/common/comm_session/session_analytics.c
Normal file
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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 "session_analytics.h"
|
||||
|
||||
#include "drivers/rtc.h"
|
||||
|
||||
#include "services/common/comm_session/session_internal.h"
|
||||
#include "services/common/analytics/analytics.h"
|
||||
#include "services/common/ping.h"
|
||||
#include "util/time/time.h"
|
||||
|
||||
//! returns the analytic timer id we want to use
|
||||
static int prv_get_analytic_id_for_session(CommSession *session) {
|
||||
if (comm_session_analytics_get_transport_type(session) == CommSessionTransportType_PPoGATT) {
|
||||
return ANALYTICS_DEVICE_METRIC_BT_PEBBLE_PPOGATT_APP_TIME;
|
||||
} else {
|
||||
return ANALYTICS_DEVICE_METRIC_BT_PEBBLE_SPP_APP_TIME;
|
||||
}
|
||||
}
|
||||
|
||||
CommSessionTransportType comm_session_analytics_get_transport_type(CommSession *session) {
|
||||
return session->transport_imp->get_type(session->transport);
|
||||
}
|
||||
|
||||
void comm_session_analytics_open_session(CommSession *session) {
|
||||
const bool is_system = (session->destination != TransportDestinationApp);
|
||||
if (is_system) {
|
||||
int analytic_id = prv_get_analytic_id_for_session(session);
|
||||
analytics_stopwatch_start(analytic_id, AnalyticsClient_System);
|
||||
analytics_inc(ANALYTICS_DEVICE_METRIC_BT_SYSTEM_SESSION_OPEN_COUNT, AnalyticsClient_System);
|
||||
}
|
||||
session->open_ticks = rtc_get_ticks();
|
||||
}
|
||||
|
||||
void comm_session_analytics_close_session(CommSession *session, CommSessionCloseReason reason) {
|
||||
const bool is_system = (session->destination != TransportDestinationApp);
|
||||
if (is_system) {
|
||||
int analytic_id = prv_get_analytic_id_for_session(session);
|
||||
analytics_stopwatch_stop(analytic_id);
|
||||
}
|
||||
|
||||
const RtcTicks duration_ticks = (rtc_get_ticks() - session->open_ticks);
|
||||
const uint16_t duration_mins = ((duration_ticks / RTC_TICKS_HZ) / SECONDS_PER_MINUTE);
|
||||
const Uuid *optional_app_uuid = comm_session_get_uuid(session);
|
||||
|
||||
analytics_event_session_close(is_system, optional_app_uuid, reason, duration_mins);
|
||||
}
|
||||
|
||||
//! Increment "bytes sent" counter and perform app ping if due
|
||||
void comm_session_analytics_inc_bytes_sent(CommSession *session, uint16_t length) {
|
||||
CommSessionType type = comm_session_get_type(session);
|
||||
AnalyticsMetric metric;
|
||||
switch (type) {
|
||||
case CommSessionTypeSystem:
|
||||
metric = ANALYTICS_DEVICE_METRIC_BT_PRIVATE_BYTE_OUT_COUNT;
|
||||
|
||||
// We know that bluetooth is already active. If we just sent a message to the Pebble mobile
|
||||
// app, this is a good time to see if we should send our ping out to it as well.
|
||||
ping_send_if_due();
|
||||
break;
|
||||
|
||||
case CommSessionTypeApp:
|
||||
metric = ANALYTICS_DEVICE_METRIC_BT_PUBLIC_BYTE_OUT_COUNT;
|
||||
break;
|
||||
|
||||
case CommSessionTypeInvalid:
|
||||
default:
|
||||
return;
|
||||
}
|
||||
analytics_add(metric, length, AnalyticsClient_System);
|
||||
}
|
||||
|
||||
void comm_session_analytics_inc_bytes_received(CommSession *session, uint16_t length) {
|
||||
const AnalyticsMetric metric = (comm_session_get_type(session) == CommSessionTypeSystem) ?
|
||||
ANALYTICS_DEVICE_METRIC_BT_PRIVATE_BYTE_IN_COUNT :
|
||||
ANALYTICS_DEVICE_METRIC_BT_PUBLIC_BYTE_IN_COUNT;
|
||||
analytics_add(metric, length, AnalyticsClient_System);
|
||||
}
|
48
src/fw/services/common/comm_session/session_analytics.h
Normal file
48
src/fw/services/common/comm_session/session_analytics.h
Normal file
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct CommSession CommSession;
|
||||
|
||||
typedef enum {
|
||||
CommSessionCloseReason_UnderlyingDisconnection = 0,
|
||||
CommSessionCloseReason_ClosedRemotely = 1,
|
||||
CommSessionCloseReason_ClosedLocally = 2,
|
||||
CommSessionCloseReason_TransportSpecificBegin = 100,
|
||||
CommSessionCloseReason_TransportSpecificEnd = 255,
|
||||
} CommSessionCloseReason;
|
||||
|
||||
typedef enum {
|
||||
CommSessionTransportType_PlainSPP = 0,
|
||||
CommSessionTransportType_iAP = 1,
|
||||
CommSessionTransportType_PPoGATT = 2,
|
||||
CommSessionTransportType_QEMU = 3,
|
||||
CommSessionTransportType_PULSE = 4,
|
||||
} CommSessionTransportType;
|
||||
|
||||
//! Assumes bt_lock() is held by the caller.
|
||||
CommSessionTransportType comm_session_analytics_get_transport_type(CommSession *session);
|
||||
|
||||
void comm_session_analytics_open_session(CommSession *session);
|
||||
|
||||
void comm_session_analytics_close_session(CommSession *session, CommSessionCloseReason reason);
|
||||
|
||||
void comm_session_analytics_inc_bytes_sent(CommSession *session, uint16_t length);
|
||||
|
||||
void comm_session_analytics_inc_bytes_received(CommSession *session, uint16_t length);
|
62
src/fw/services/common/comm_session/session_internal.h
Normal file
62
src/fw/services/common/comm_session/session_internal.h
Normal file
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "drivers/rtc.h"
|
||||
|
||||
#include "session_receive_router.h"
|
||||
#include "session_transport.h"
|
||||
|
||||
#include "services/common/regular_timer.h"
|
||||
#include "util/list.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef struct SessionSendQueueJob SessionSendQueueJob;
|
||||
|
||||
//! Data structure representing a Pebble Protocol communication session.
|
||||
//! There can be multiple. For example, with the iAP transport, the Pebble app has a session and
|
||||
//! 3rd party apps share another separate session as well. With PPoGATT, the Pebble app has its own
|
||||
//! session, but each 3rd party app has its own session as well.
|
||||
typedef struct CommSession {
|
||||
ListNode node;
|
||||
|
||||
//! The underlying transport responsible for actually sending and receiving the Pebble Protocol
|
||||
//! data. This can be SPP, iAP (see ispp.c), PPoGATT (see ppogatt.c) or QEMU (qemu_transport.c).
|
||||
Transport *transport;
|
||||
|
||||
//! Set of function pointers that the session uses to call back to the transport.
|
||||
const TransportImplementation *transport_imp;
|
||||
|
||||
//! True if a Kernel BG callback has been scheduled to call transport_imp->send_next()
|
||||
bool is_send_next_call_pending;
|
||||
|
||||
//! True if the session is a system session (connected to the Pebble mobile app).
|
||||
TransportDestination destination;
|
||||
|
||||
// Extensions supported by the mobile endpoint, see
|
||||
// https://pebbletechnology.atlassian.net/wiki/pages/viewpage.action?pageId=491698
|
||||
CommSessionCapability protocol_capabilities;
|
||||
|
||||
//! The send queue of this session. See session_send_queue.c
|
||||
SessionSendQueueJob *send_queue_head;
|
||||
|
||||
ReceiveRouter recv_router;
|
||||
|
||||
//! Absolute number of ticks since session opened.
|
||||
RtcTicks open_ticks;
|
||||
} CommSession;
|
231
src/fw/services/common/comm_session/session_receive_router.c
Normal file
231
src/fw/services/common/comm_session/session_receive_router.c
Normal file
|
@ -0,0 +1,231 @@
|
|||
/*
|
||||
* 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 "session_receive_router.h"
|
||||
|
||||
#include "services/common/comm_session/meta_endpoint.h"
|
||||
#include "services/common/comm_session/session_analytics.h"
|
||||
#include "services/common/comm_session/session_internal.h"
|
||||
#include "system/hexdump.h"
|
||||
#include "system/logging.h"
|
||||
#include "util/math.h"
|
||||
#include "util/net.h"
|
||||
#include "util/size.h"
|
||||
|
||||
// Generated table of endpoint handler (s_protocol_endpoints):
|
||||
#include "services/common/comm_session/protocol_endpoints_table.auto.h"
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Static helper functions
|
||||
|
||||
static const PebbleProtocolEndpoint* prv_find_endpoint(uint16_t endpoint_id) {
|
||||
for (size_t i = 0; i < ARRAY_LENGTH(s_protocol_endpoints); ++i) {
|
||||
const PebbleProtocolEndpoint* endpoint = &s_protocol_endpoints[i];
|
||||
if (!endpoint || endpoint->endpoint_id > endpoint_id) {
|
||||
break;
|
||||
}
|
||||
if (endpoint->endpoint_id == endpoint_id) {
|
||||
return endpoint;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static bool prv_is_endpoint_allowed_with_session(const PebbleProtocolEndpoint* endpoint,
|
||||
CommSession *session) {
|
||||
PebbleProtocolAccess granted_access_bitset = PebbleProtocolAccessNone;
|
||||
switch (comm_session_get_type(session)) {
|
||||
case CommSessionTypeSystem:
|
||||
granted_access_bitset = PebbleProtocolAccessPrivate; // Pebble app
|
||||
break;
|
||||
case CommSessionTypeApp:
|
||||
granted_access_bitset = PebbleProtocolAccessPublic; // 3rd party PebbleKit app
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return (endpoint->access_mask & granted_access_bitset);
|
||||
}
|
||||
|
||||
static MetaResponseCode prv_error_for_endpoint(const PebbleProtocolEndpoint* endpoint,
|
||||
CommSession *session) {
|
||||
if (!endpoint) {
|
||||
return MetaResponseCodeUnhandled;
|
||||
}
|
||||
if (!prv_is_endpoint_allowed_with_session(endpoint, session)) {
|
||||
return MetaResponseCodeDisallowed;
|
||||
}
|
||||
return MetaResponseCodeNoError;
|
||||
}
|
||||
|
||||
static void prv_cleanup_router(ReceiveRouter *rtr) {
|
||||
memset(rtr, 0, sizeof(*rtr));
|
||||
}
|
||||
|
||||
static bool prv_copy_header(ReceiveRouter *rtr, size_t *data_size_p, const uint8_t **data_p) {
|
||||
// New message or still gathering the header of the message
|
||||
const uint16_t header_bytes_missing = sizeof(PebbleProtocolHeader) - rtr->bytes_received;
|
||||
const uint16_t header_bytes_to_copy = MIN(header_bytes_missing, *data_size_p);
|
||||
memcpy(rtr->header_buffer + rtr->bytes_received, *data_p, header_bytes_to_copy);
|
||||
*data_size_p -= header_bytes_to_copy;
|
||||
*data_p += header_bytes_to_copy;
|
||||
rtr->bytes_received += header_bytes_to_copy;
|
||||
|
||||
if (rtr->bytes_received < sizeof(PebbleProtocolHeader)) {
|
||||
// Incomplete header, wait for more data to come.
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool prv_handle_endpoint_error_and_skip_message_if_needed(CommSession *session,
|
||||
const PebbleProtocolEndpoint *endpoint,
|
||||
const uint16_t endpoint_id) {
|
||||
MetaResponseInfo meta_response_info;
|
||||
meta_response_info.payload.error_code = prv_error_for_endpoint(endpoint, session);
|
||||
if (MetaResponseCodeNoError != meta_response_info.payload.error_code) {
|
||||
meta_response_info.payload.endpoint_id = endpoint_id;
|
||||
meta_response_info.session = session;
|
||||
meta_endpoint_send_response_async(&meta_response_info);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void prv_skip_message(ReceiveRouter *rtr, const uint32_t payload_length) {
|
||||
rtr->bytes_to_ignore = payload_length;
|
||||
rtr->bytes_received = 0;
|
||||
}
|
||||
|
||||
static bool prv_ignore_skipped_message_if_needed(const uint8_t **data_p, size_t *data_size_p,
|
||||
ReceiveRouter *rtr) {
|
||||
// Eat any bytes from an ignored, previous message:
|
||||
if (rtr->bytes_to_ignore) {
|
||||
const uint32_t num_ignored_bytes = MIN(*data_size_p, rtr->bytes_to_ignore);
|
||||
rtr->bytes_to_ignore -= num_ignored_bytes;
|
||||
*data_size_p -= num_ignored_bytes;
|
||||
if (*data_size_p == 0) {
|
||||
return true; // we're done
|
||||
}
|
||||
*data_p += num_ignored_bytes;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool prv_prepare_receiver(const uint32_t payload_length,
|
||||
const PebbleProtocolEndpoint *endpoint, const uint16_t endpoint_id,
|
||||
CommSession *session, ReceiveRouter *rtr) {
|
||||
Receiver *receiver = endpoint->receiver_imp->prepare(session, endpoint,
|
||||
payload_length);
|
||||
if (!receiver) {
|
||||
// If no receiver could be provided (buffers full?), ignore the message:
|
||||
|
||||
// TODO: What to do here?
|
||||
// - Look into SPP flow control
|
||||
// - With PPoGATT: drop packet and rely on automatic retransmission?
|
||||
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "No receiver for endpoint=%"PRIu16" len=%"PRIu32,
|
||||
endpoint_id, payload_length);
|
||||
prv_skip_message(rtr, payload_length);
|
||||
return true;
|
||||
}
|
||||
|
||||
rtr->receiver = receiver;
|
||||
rtr->msg_payload_length = payload_length;
|
||||
rtr->receiver_imp = endpoint->receiver_imp;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static void prv_write_payload_to_receiver(ReceiveRouter *rtr, size_t *data_size_p,
|
||||
const uint8_t **data_p) {
|
||||
// Write the (partial) payload bytes to the Receiver:
|
||||
const uint32_t num_payload_bytes_left_to_receive =
|
||||
rtr->msg_payload_length + sizeof(PebbleProtocolHeader) - rtr->bytes_received;
|
||||
const uint32_t num_payload_bytes_received = MIN(*data_size_p, num_payload_bytes_left_to_receive);
|
||||
rtr->bytes_received += num_payload_bytes_received;
|
||||
rtr->receiver_imp->write(rtr->receiver, *data_p, num_payload_bytes_received);
|
||||
*data_p += num_payload_bytes_received;
|
||||
*data_size_p -= num_payload_bytes_received;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Exported functions
|
||||
|
||||
void comm_session_receive_router_write(CommSession *session,
|
||||
const uint8_t *data, size_t data_size) {
|
||||
comm_session_analytics_inc_bytes_received(session, data_size);
|
||||
|
||||
PBL_LOG_D_VERBOSE(LOG_DOMAIN_COMM, "Received packet from BT");
|
||||
PBL_HEXDUMP_D(LOG_DOMAIN_COMM, LOG_LEVEL_DEBUG_VERBOSE, data, data_size);
|
||||
|
||||
ReceiveRouter *rtr = &session->recv_router;
|
||||
|
||||
while (data_size) {
|
||||
if (prv_ignore_skipped_message_if_needed(&data, &data_size, rtr)) {
|
||||
return; // we're done
|
||||
}
|
||||
|
||||
// Deal with the header:
|
||||
if (rtr->bytes_received < sizeof(PebbleProtocolHeader)) {
|
||||
if (prv_copy_header(rtr, &data_size, &data)) {
|
||||
return; // Incomplete header, wait for more data to come.
|
||||
}
|
||||
|
||||
// Complete header received!
|
||||
const PebbleProtocolHeader *header_big_endian = (PebbleProtocolHeader *)rtr->header_buffer;
|
||||
const uint16_t endpoint_id = ntohs(header_big_endian->endpoint_id);
|
||||
|
||||
const PebbleProtocolEndpoint* endpoint = prv_find_endpoint(endpoint_id);
|
||||
const uint32_t payload_length = ntohs(header_big_endian->length);
|
||||
|
||||
if (prv_handle_endpoint_error_and_skip_message_if_needed(session, endpoint, endpoint_id)) {
|
||||
prv_skip_message(rtr, payload_length);
|
||||
continue; // while (data_size)
|
||||
}
|
||||
|
||||
PBL_LOG_D(LOG_DOMAIN_COMM, LOG_LEVEL_DEBUG,
|
||||
"Receiving message: endpoint_id 0x%"PRIx16" (%"PRIu16"), payload_length %"PRIu32,
|
||||
endpoint_id, endpoint_id, payload_length);
|
||||
|
||||
if (prv_prepare_receiver(payload_length, endpoint, endpoint_id, session, rtr)) {
|
||||
continue; // while (data_size)
|
||||
}
|
||||
}
|
||||
|
||||
prv_write_payload_to_receiver(rtr, &data_size, &data);
|
||||
|
||||
// If the message payload is completed, call the Receiver to process it:
|
||||
if (rtr->bytes_received == (sizeof(PebbleProtocolHeader) + rtr->msg_payload_length)) {
|
||||
rtr->receiver_imp->finish(rtr->receiver);
|
||||
|
||||
// Wipe it, to avoid confusing ourselves when looking at core dumps:
|
||||
prv_cleanup_router(rtr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void comm_session_receive_router_cleanup(CommSession *session) {
|
||||
ReceiveRouter *rtr = &session->recv_router;
|
||||
|
||||
if (rtr->receiver_imp) {
|
||||
rtr->receiver_imp->cleanup(rtr->receiver);
|
||||
}
|
||||
|
||||
// Wipe it, to avoid confusing ourselves when looking at core dumps:
|
||||
prv_cleanup_router(rtr);
|
||||
}
|
113
src/fw/services/common/comm_session/session_receive_router.h
Normal file
113
src/fw/services/common/comm_session/session_receive_router.h
Normal file
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* 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/common/comm_session/protocol.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct CommSession CommSession;
|
||||
|
||||
//! Pebble Protocol endpoint handler.
|
||||
//! @see protocol_endpoints_table.json
|
||||
typedef void (*PebbleProtocolEndpointHandler)(CommSession *session, const uint8_t *data,
|
||||
size_t length);
|
||||
|
||||
typedef enum {
|
||||
PebbleProtocolAccessPublic = 1 << 0, // reserved for 3rd party phone apps
|
||||
PebbleProtocolAccessPrivate = 1 << 1, // reserved for Pebble phone app
|
||||
PebbleProtocolAccessAny = ~0, // anyone is allowed
|
||||
PebbleProtocolAccessNone = 0,
|
||||
} PebbleProtocolAccess;
|
||||
|
||||
typedef struct ReceiverImplementation ReceiverImplementation;
|
||||
|
||||
//! The info associated with a single Pebble Protocol endpoint.
|
||||
//! @see protocol_endpoints_table.json
|
||||
typedef struct PebbleProtocolEndpoint {
|
||||
uint16_t endpoint_id;
|
||||
PebbleProtocolEndpointHandler handler;
|
||||
PebbleProtocolAccess access_mask;
|
||||
const ReceiverImplementation *receiver_imp;
|
||||
const void *receiver_opt;
|
||||
} PebbleProtocolEndpoint;
|
||||
|
||||
//! Opaque type, can be anything, up to ReceiverImplementation what it actually contains.
|
||||
//! Receiver is the context associated with a messages that is currently being received and only
|
||||
//! that one message. At any time a message is being received by a CommSession, there is a
|
||||
//! one-to-one relationship between that CommSession and the Receiver, because messages cannot be
|
||||
//! interleaved inside one Pebble Protocol data stream.
|
||||
//! @see ReceiverImplementation
|
||||
typedef struct Receiver Receiver;
|
||||
|
||||
|
||||
//! A ReceiverImplementation is responsible for creating a Receiver context (see "prepare"),
|
||||
//! buffering inbound message payload data (see "write") and finally scheduling the execution of
|
||||
//! the endpoint handler (see "finish").
|
||||
//! A ReceiverImplementation can be specific to the endpoint, for example, Put Bytes has a special
|
||||
//! receiver implementation, because of the big buffer it requires.
|
||||
//! However, a ReceiverImplementation can also be shared amongst multiple endpoints, which makes
|
||||
//! sense if the buffering needs for a set of endpoints are equal or very similar.
|
||||
//! @note There can be multiple CommSessions writing (partial) messages concurrently.
|
||||
//! The receiver is responsible for dealing with this. So if the messages are collected in one big
|
||||
//! circular buffer, it will have to take special measures to allow another CommSession to start
|
||||
//! writing something while another CommSession's message has not been fully received yet.
|
||||
//! @note All functions must be implemented, none of the function pointers can point to NULL.
|
||||
typedef struct ReceiverImplementation {
|
||||
//! Prepares a Receiver context.
|
||||
//! If there is not enough space left to be able to buffer the complete payload, NULL can be
|
||||
//! returned to drop/ignore the message.
|
||||
//! @param receiver_opt Optional per-endpoint configuration for the receiver, assigned through
|
||||
//! protocol_endpoints_table.json.
|
||||
Receiver * (*prepare)(CommSession *session, const PebbleProtocolEndpoint *endpoint,
|
||||
size_t total_payload_length);
|
||||
|
||||
//! Writes payload data of the current message to the Receiver context.
|
||||
void (*write)(Receiver *receiver, const uint8_t *data, size_t length);
|
||||
|
||||
//! Indicates the complete payload data of the current message has been written.
|
||||
//! When "finish" is called, execution of the endpoint handler should be scheduled by the
|
||||
//! implementation. The implementation should also take care of cleaning up the Receiver context.
|
||||
void (*finish)(Receiver *receiver);
|
||||
|
||||
//! Called when the session is closed, to clean up the Receiver context.
|
||||
//! The message will be discarded and not be delivered to the endpoint handler.
|
||||
void (*cleanup)(Receiver *receiver);
|
||||
} ReceiverImplementation;
|
||||
|
||||
|
||||
//! ReceiveRouter contains the state associated with parsing the Pebble Protocol header.
|
||||
//! This module will call the ReceiverImplementation to buffer and process the message payload.
|
||||
typedef struct ReceiveRouter {
|
||||
//! Total number of bytes received for the current message so far, including the header.
|
||||
uint16_t bytes_received;
|
||||
|
||||
//! Number of inbound bytes that should be ignored after the current point.
|
||||
uint16_t bytes_to_ignore;
|
||||
|
||||
//! Expected payload length of the current message in bytes.
|
||||
uint16_t msg_payload_length;
|
||||
|
||||
//! In case the number of bytes received was less than the length of the header,
|
||||
//! this buffer will be used to store those few bytes that were received.
|
||||
uint8_t header_buffer[sizeof(PebbleProtocolHeader)];
|
||||
|
||||
//! Receiver of the current message.
|
||||
const ReceiverImplementation *receiver_imp;
|
||||
Receiver *receiver;
|
||||
} ReceiveRouter;
|
30
src/fw/services/common/comm_session/session_remote_os.h
Normal file
30
src/fw/services/common/comm_session/session_remote_os.h
Normal file
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 {
|
||||
RemoteBitmaskOS = 0x7, // bits 0 - 2
|
||||
} RemoteBitmask;
|
||||
|
||||
typedef enum {
|
||||
RemoteOSUnknown = 0,
|
||||
RemoteOSiOS = 1,
|
||||
RemoteOSAndroid = 2,
|
||||
RemoteOSX = 3,
|
||||
RemoteOSLinux = 4,
|
||||
RemoteOSWindows = 5,
|
||||
} RemoteOS;
|
184
src/fw/services/common/comm_session/session_remote_version.c
Normal file
184
src/fw/services/common/comm_session/session_remote_version.c
Normal file
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "session_remote_version.h"
|
||||
|
||||
#include "comm/bt_lock.h"
|
||||
|
||||
#include "kernel/events.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
|
||||
#include "services/common/bluetooth/bluetooth_persistent_storage.h"
|
||||
#include "services/common/comm_session/session_internal.h"
|
||||
#include "services/common/comm_session/session_remote_os.h"
|
||||
#include "kernel/event_loop.h"
|
||||
|
||||
#include "util/attributes.h"
|
||||
#include "util/net.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/hexdump.h"
|
||||
|
||||
#include <bluetooth/reconnect.h>
|
||||
|
||||
extern bool comm_session_is_valid(const CommSession *session);
|
||||
|
||||
#define MAX_REQUEST_RETRIES (3)
|
||||
|
||||
//! Defined in session.c. We should only be setting the capabilities here
|
||||
extern void comm_session_set_capabilities(
|
||||
CommSession *session, CommSessionCapability capability_flags);
|
||||
|
||||
typedef enum {
|
||||
CommSessionVersionCommandRequest = 0x00,
|
||||
CommSessionVersionCommandResponse = 0x01,
|
||||
} CommSessionVersionCommand;
|
||||
|
||||
// The 1.x mobile app response
|
||||
struct PACKED VersionsPhoneResponseV1 {
|
||||
uint32_t pebble_library_version;
|
||||
uint32_t session_capabilities_bitfield;
|
||||
uint32_t platform_bitfield;
|
||||
};
|
||||
|
||||
// The 2.x mobile apps return a longer response than the 1.x apps do.
|
||||
struct PACKED VersionsPhoneResponseV2 {
|
||||
uint32_t pebble_library_version;
|
||||
uint32_t session_capabilities_bitfield;
|
||||
uint32_t platform_bitfield;
|
||||
uint8_t response_version; // Set to 2 in this format of the response.
|
||||
uint8_t major_version; // major version number of the mobile app, i.e. 2
|
||||
uint8_t minor_version; // minfo version number of the mobile app, i.e. 0
|
||||
uint8_t bugfix_version; // bugfix version number of the mobile app, i.e. 1
|
||||
};
|
||||
|
||||
// The 3.x mobile apps return a longer response than the 2.x apps do
|
||||
struct PACKED VersionsPhoneResponseV3 {
|
||||
uint32_t pebble_library_version_deprecated; // Deprecated as of v3.x
|
||||
uint32_t session_capabilities_bitfield; // Deprecated as of v3.x
|
||||
uint32_t platform_bitfield;
|
||||
uint8_t response_version; // Set to 2 in this format of the response.
|
||||
uint8_t major_version; // major version number of the mobile app, i.e. 2
|
||||
uint8_t minor_version; // minfo version number of the mobile app, i.e. 0
|
||||
uint8_t bugfix_version; // bugfix version number of the mobile app, i.e. 1
|
||||
|
||||
//! Pebble Protocol capabilities that the other side supports
|
||||
CommSessionCapability protocol_capabilities;
|
||||
};
|
||||
|
||||
static const uint16_t SESSION_REMOTE_VERSION_ENDPOINT_ID = 0x0011;
|
||||
|
||||
static void prv_comm_session_perform_version_request_bg_cb(void *data) {
|
||||
CommSession *session = (CommSession *) data;
|
||||
const uint8_t command = CommSessionVersionCommandRequest;
|
||||
// No need to check validity of session here, comm_session_send_data already does this
|
||||
comm_session_send_data(session, SESSION_REMOTE_VERSION_ENDPOINT_ID,
|
||||
&command, sizeof(command), COMM_SESSION_DEFAULT_TIMEOUT);
|
||||
}
|
||||
|
||||
static void prv_schedule_request(CommSession *session) {
|
||||
bt_lock();
|
||||
if (!comm_session_is_valid(session)) {
|
||||
session = NULL;
|
||||
goto unlock;
|
||||
}
|
||||
unlock:
|
||||
bt_unlock();
|
||||
|
||||
if (session) {
|
||||
launcher_task_add_callback(prv_comm_session_perform_version_request_bg_cb, session);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_handle_phone_versions_response(CommSession *session,
|
||||
const uint8_t *data, size_t length) {
|
||||
int request_version = 0;
|
||||
|
||||
// Check which version of the response we are being given based on the length of the
|
||||
// message that the callback was given.
|
||||
if (length >= sizeof(struct VersionsPhoneResponseV3)) {
|
||||
request_version = 3;
|
||||
} else if (length >= sizeof(struct VersionsPhoneResponseV2)) {
|
||||
request_version = 2;
|
||||
} else if (length >= sizeof(struct VersionsPhoneResponseV1)) {
|
||||
request_version = 1;
|
||||
} else {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Invalid version request");
|
||||
return;
|
||||
}
|
||||
|
||||
struct VersionsPhoneResponseV3 *response = (struct VersionsPhoneResponseV3 *)data;
|
||||
|
||||
CommSessionCapability capability_flags = 0;
|
||||
|
||||
// If this is an old V1 response, use defaults for new fields introduced since V1
|
||||
// NOTE: The 1.X Android mobile app has a bug which causes it to send double length responses -
|
||||
// where the response message is repeated twice. This results in the
|
||||
// CommSessionVersionCommandResponse byte (value of 1) for the second copy landing in the
|
||||
// response_version field. That is why we only accept when this field is exactly 2, otherwise we
|
||||
// treat it as a V1 response.
|
||||
if (request_version >= 2) {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Connected to Mobile App %"PRIu8 ".%"PRIu8 "-%"PRIu8,
|
||||
response->major_version, response->minor_version, response->bugfix_version);
|
||||
|
||||
// For 3.X mobile applications, they will return additional bits in their response to correspond
|
||||
// to supporting certain endpoints over their deprecated counterparts, so assign them here after
|
||||
// the check for 2.X.
|
||||
if (request_version >= 3) {
|
||||
capability_flags = response->protocol_capabilities;
|
||||
}
|
||||
}
|
||||
|
||||
comm_session_set_capabilities(session, capability_flags);
|
||||
const uint32_t platform_bits = ntohl(response->platform_bitfield);
|
||||
bt_driver_reconnect_notify_platform_bitfield(platform_bits);
|
||||
|
||||
const bool is_system = comm_session_is_system(session);
|
||||
PBL_LOG(LOG_LEVEL_INFO,
|
||||
"Phone app: is_system=%u, plf=0x%"PRIx32", capabilities=0x%"PRIx32,
|
||||
is_system, platform_bits, (uint32_t)capability_flags);
|
||||
|
||||
// Only emit for the Pebble app, not 3rd party companion apps:
|
||||
if (is_system) {
|
||||
PebbleEvent event = {
|
||||
.type = PEBBLE_REMOTE_APP_INFO_EVENT,
|
||||
.bluetooth.app_info_event = {
|
||||
.os = (platform_bits & RemoteBitmaskOS),
|
||||
},
|
||||
};
|
||||
event_put(&event);
|
||||
}
|
||||
}
|
||||
|
||||
void session_remote_version_protocol_msg_callback(CommSession *session_ref,
|
||||
const uint8_t *data, size_t length) {
|
||||
switch (data[0]) {
|
||||
case CommSessionVersionCommandResponse: {
|
||||
prv_handle_phone_versions_response(session_ref, data + 1, length - 1);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
PBL_LOG_D(LOG_DOMAIN_COMM, LOG_LEVEL_ERROR,
|
||||
"Invalid message received. First byte is %u", data[0]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//! bt_lock() is expected to be taken by the caller!
|
||||
void session_remote_version_start_requests(CommSession *session) {
|
||||
// Ask for the phone's version + capabilities:
|
||||
prv_schedule_request(session);
|
||||
}
|
58
src/fw/services/common/comm_session/session_remote_version.h
Normal file
58
src/fw/services/common/comm_session/session_remote_version.h
Normal file
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "util/attributes.h"
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct CommSession CommSession;
|
||||
|
||||
// Capabilities are a bitfield set by passing the capabilities character array in
|
||||
// system_versions.c. The corresponding mobile applications return an integer
|
||||
// field indicating which endpoints it has support for over the deprecated ones.
|
||||
typedef struct PACKED {
|
||||
union {
|
||||
struct PACKED {
|
||||
bool run_state_support:1;
|
||||
bool infinite_log_dumping_support:1;
|
||||
bool extended_music_service:1;
|
||||
bool extended_notification_service:1;
|
||||
bool lang_pack_support:1;
|
||||
bool app_message_8k_support:1;
|
||||
bool activity_insights_support:1;
|
||||
bool voice_api_support:1;
|
||||
bool send_text_support:1;
|
||||
bool notification_filtering_support:1;
|
||||
bool unread_coredump_support:1;
|
||||
bool weather_app_support:1;
|
||||
bool reminders_app_support:1;
|
||||
bool workout_app_support:1;
|
||||
bool smooth_fw_install_progress_support:1;
|
||||
// Workaround the fact that we inadvertently injected some padding when we added a 5 bit
|
||||
// field (PBL-37933) Since the padded bits are all getting 0'ed out today, we are free to use
|
||||
// them in the future!
|
||||
uint8_t padded_bits:1;
|
||||
uint8_t javascript_bytecode_version_appended: 1;
|
||||
uint8_t more_padded_bits:4;
|
||||
bool continue_fw_install_across_disconnect_support: 1;
|
||||
};
|
||||
uint64_t flags;
|
||||
};
|
||||
} PebbleProtocolCapabilities;
|
||||
|
||||
void session_remote_version_start_requests(CommSession *session);
|
64
src/fw/services/common/comm_session/session_send_buffer.h
Normal file
64
src/fw/services/common/comm_session/session_send_buffer.h
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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/common/comm_session/session.h"
|
||||
|
||||
typedef struct SendBuffer SendBuffer;
|
||||
|
||||
//! @return The maximum number of bytes that a client can copy into a CommSessionSendBuffer, or
|
||||
//! zero if the session is invalid (e.g. disconnected in the mean time).
|
||||
size_t comm_session_send_buffer_get_max_payload_length(const CommSession *session);
|
||||
|
||||
//! Creates a kernel-heap allocated buffer for outbound messages.
|
||||
//! This will block if the required space is not yet available.
|
||||
//! If you want to avoid the allocation on the kernel-heap, use comm_session_send_queue_add_job()
|
||||
//! directly.
|
||||
//! @see comm_session_send_data for a simpler, one-liner interface to send data.
|
||||
//! @note Remember to call comm_session_send_buffer_end_write when you're done.
|
||||
//! @note bt_lock() MUST NOT be held when making the call. For ..._write and ..._end_write it
|
||||
//! is fine if bt_lock() is held.
|
||||
//! @param session The session to which the message should be sent.
|
||||
//! @param endpoint_id The Pebble Protocol endpoint ID to send the message to.
|
||||
//! @param required_free_length The number of bytes of free space the caller needs at minumum. Once
|
||||
//! the function returns with `true`, the amount of space (or more) is guaranteed to be available.
|
||||
//! @param timeout_ms The maximum duration to wait for the send buffer to become available with the
|
||||
//! required number of bytes of free space.
|
||||
//! @return True if the "writer access" was sucessfully acquired, false otherwise.
|
||||
SendBuffer * comm_session_send_buffer_begin_write(CommSession *session, uint16_t endpoint_id,
|
||||
size_t required_free_length,
|
||||
uint32_t timeout_ms);
|
||||
|
||||
//! Copies data into the send buffer of the session.
|
||||
//! @note The caller must have called comm_session_send_buffer_begin_write() first.
|
||||
//! @note bt_lock() may be held when making the call.
|
||||
//! @param session The session for which to enqueue data
|
||||
//! @param data Pointer to the data to enqueue
|
||||
//! @param length Length of the data to enqueue
|
||||
//! @return true if the data was successfully queued up for sending, or false if there was not
|
||||
//! enough space left in the send buffer to enqueue the data. Note that the `required_free_length`
|
||||
//! as passed into comm_session_send_buffer_begin_write() is guaranteed. Nonetheless, callers can
|
||||
//! try to stash in more data than `required_free_length` but will need to handle the return value
|
||||
//! of this function when it does attempt to write more than `required_free_length`.
|
||||
bool comm_session_send_buffer_write(SendBuffer *send_buffer, const uint8_t *data, size_t length);
|
||||
|
||||
//! Finish writing to the send buffer. Any enqueued data will be transmitted after this call,
|
||||
//! to the session that was passed in the ..._begin_write() call.
|
||||
//! @note The caller must have called comm_session_send_buffer_begin_write() first.
|
||||
//! @note bt_lock() may be held when making the call.
|
||||
//! @param session The session for which to release the send buffer.
|
||||
void comm_session_send_buffer_end_write(SendBuffer *send_buffer);
|
128
src/fw/services/common/comm_session/session_send_queue.c
Normal file
128
src/fw/services/common/comm_session/session_send_queue.c
Normal file
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* 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 "comm/bt_lock.h"
|
||||
#include "services/common/comm_session/session_analytics.h"
|
||||
#include "services/common/comm_session/session_internal.h"
|
||||
#include "services/common/comm_session/session_send_queue.h"
|
||||
#include "system/passert.h"
|
||||
#include "util/math.h"
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
extern bool comm_session_is_valid(const CommSession *session);
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Interface towards CommSession
|
||||
|
||||
void comm_session_send_queue_cleanup(CommSession *session) {
|
||||
SessionSendQueueJob *job = session->send_queue_head;
|
||||
while (job) {
|
||||
SessionSendQueueJob *next = (SessionSendQueueJob *) job->node.next;
|
||||
job->impl->free(job);
|
||||
job = next;
|
||||
}
|
||||
session->send_queue_head = NULL;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Interface towards Senders
|
||||
|
||||
void comm_session_send_queue_add_job(CommSession *session, SessionSendQueueJob **job_ptr_ptr) {
|
||||
bt_lock();
|
||||
{
|
||||
SessionSendQueueJob *job = *job_ptr_ptr;
|
||||
if (!comm_session_is_valid(session)) {
|
||||
job->impl->free(job);
|
||||
*job_ptr_ptr = NULL;
|
||||
goto unlock;
|
||||
}
|
||||
ListNode *head = (ListNode *)session->send_queue_head;
|
||||
PBL_ASSERTN(!list_contains(head, (const ListNode *)job));
|
||||
if (head) {
|
||||
list_append(head, (ListNode *)job);
|
||||
} else {
|
||||
session->send_queue_head = job;
|
||||
}
|
||||
// Schedule to let the transport to send the enqueued data:
|
||||
comm_session_send_next(session);
|
||||
}
|
||||
unlock:
|
||||
bt_unlock();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Interface towards Transport
|
||||
// bt_lock is assumed to be taken by the caller of each of the below functions:
|
||||
|
||||
size_t comm_session_send_queue_get_length(const CommSession *session) {
|
||||
size_t length = 0;
|
||||
const SessionSendQueueJob *job = session->send_queue_head;
|
||||
while (job) {
|
||||
length += job->impl->get_length(job);
|
||||
job = (const SessionSendQueueJob *)job->node.next;
|
||||
}
|
||||
return length;
|
||||
}
|
||||
|
||||
size_t comm_session_send_queue_copy(CommSession *session, uint32_t start_offset,
|
||||
size_t length, uint8_t *data_out) {
|
||||
size_t remaining_length = length;
|
||||
const SessionSendQueueJob *job = session->send_queue_head;
|
||||
while (job && remaining_length) {
|
||||
const size_t job_length = job->impl->get_length(job);
|
||||
if (job_length <= start_offset) {
|
||||
start_offset -= job_length;
|
||||
} else {
|
||||
const size_t copied_length = job->impl->copy(job, start_offset, remaining_length, data_out);
|
||||
remaining_length -= copied_length;
|
||||
data_out += copied_length;
|
||||
start_offset = 0;
|
||||
}
|
||||
job = (SessionSendQueueJob *)job->node.next;
|
||||
}
|
||||
return (length - remaining_length);
|
||||
}
|
||||
|
||||
size_t comm_session_send_queue_get_read_pointer(const CommSession *session,
|
||||
const uint8_t **data_out) {
|
||||
if (!session->send_queue_head) {
|
||||
return 0;
|
||||
}
|
||||
const SessionSendQueueJob *job = session->send_queue_head;
|
||||
return job->impl->get_read_pointer(job, data_out);
|
||||
}
|
||||
|
||||
void comm_session_send_queue_consume(CommSession *session, size_t remaining_length) {
|
||||
// The data has sucessfully been sent out at this point
|
||||
comm_session_analytics_inc_bytes_sent(session, remaining_length);
|
||||
|
||||
PBL_ASSERTN(session->send_queue_head);
|
||||
SessionSendQueueJob *job = session->send_queue_head;
|
||||
while (job && remaining_length) {
|
||||
const size_t job_length = job->impl->get_length(job);
|
||||
const size_t consume_length = MIN(remaining_length, job_length);
|
||||
job->impl->consume(job, consume_length);
|
||||
SessionSendQueueJob *next = (SessionSendQueueJob *)job->node.next;
|
||||
if (job_length == consume_length) {
|
||||
// job's done
|
||||
list_remove((ListNode *)job, (ListNode **)&session->send_queue_head, NULL);
|
||||
job->impl->free(job);
|
||||
}
|
||||
remaining_length -= consume_length;
|
||||
job = next;
|
||||
}
|
||||
}
|
72
src/fw/services/common/comm_session/session_send_queue.h
Normal file
72
src/fw/services/common/comm_session/session_send_queue.h
Normal file
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "services/common/comm_session/session.h"
|
||||
#include "util/list.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct SessionSendQueueJob SessionSendQueueJob;
|
||||
|
||||
//! @note bt_lock() is expected to be taken by the caller of any of these functions!
|
||||
typedef struct {
|
||||
//! @return The size of the message(s) of this job in bytes.
|
||||
size_t (*get_length)(const SessionSendQueueJob *send_job);
|
||||
|
||||
//! Copies bytes from the message(s) into another buffer.
|
||||
//! @param start_off The offset into the send buffer
|
||||
//! @param length The number of bytes to copy
|
||||
//! @param[out] data_out Pointer to the buffer into which to copy the data
|
||||
//! @return The number of bytes copied
|
||||
//! @note The caller will ensure there is enough data available.
|
||||
size_t (*copy)(const SessionSendQueueJob *send_job, int start_offset,
|
||||
size_t length, uint8_t *data_out);
|
||||
|
||||
//! Gets a read pointer and the number of bytes that can be read from the read pointer.
|
||||
//! @note The implementation might use a non-contiguous buffer, so it is possible
|
||||
//! that there is more data to read. To access the entire message data, call this function
|
||||
//! and consume() repeatedly until it returns zero.
|
||||
//! @param data_out Pointer to the pointer to assign the read pointer to.
|
||||
//! @return The number of bytes that can be read starting at the read pointer.
|
||||
size_t (*get_read_pointer)(const SessionSendQueueJob *send_job,
|
||||
const uint8_t **data_out);
|
||||
|
||||
//! Indicates that `length` bytes have been consumed and sent out by the transport.
|
||||
void (*consume)(const SessionSendQueueJob *send_job, size_t length);
|
||||
|
||||
//! Called when the send queue is done consuming the job, or when the session is disconnected
|
||||
//! and the job should clean itself up.
|
||||
void (*free)(SessionSendQueueJob *send_job);
|
||||
} SessionSendJobImpl;
|
||||
|
||||
//! Structure representing a job to send one or more complete Pebble Protocol messages.
|
||||
//! Future possibility: add a priority level, so the jobs can be sent out in priority order.
|
||||
typedef struct SessionSendQueueJob {
|
||||
ListNode node;
|
||||
|
||||
//! Job implementation
|
||||
const SessionSendJobImpl *impl;
|
||||
|
||||
//! The creator of the job can potentially tack more context fields to the end here.
|
||||
} SessionSendQueueJob;
|
||||
|
||||
//! The caller is responsible for keeping around the job until impl->free() is called.
|
||||
//! @note If the session has been closed in the mean time, impl->free() will be called before
|
||||
//! returning from this function. In that case, job will be set to NULL.
|
||||
//! bt_lock() does not have to be held by the caller.
|
||||
void comm_session_send_queue_add_job(CommSession *session, SessionSendQueueJob **job);
|
165
src/fw/services/common/comm_session/session_transport.h
Normal file
165
src/fw/services/common/comm_session/session_transport.h
Normal file
|
@ -0,0 +1,165 @@
|
|||
/*
|
||||
* 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/common/comm_session/session.h"
|
||||
#include "services/common/comm_session/session_analytics.h"
|
||||
|
||||
#include "util/uuid.h"
|
||||
|
||||
#include "comm/bt_conn_mgr.h"
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Types and functions that a transport should use to manage the session:
|
||||
|
||||
//! Opaque type (can be anything)
|
||||
typedef struct Transport Transport;
|
||||
|
||||
//! Pointer to function implementing the sending of data that is enqueued in the send buffer
|
||||
typedef void (*TransportSendNext)(Transport *transport);
|
||||
|
||||
//! Pointer to function implementing the closing of the transport
|
||||
//! @note This is called by session.c in case there is a conflict: multiple transports for the
|
||||
//! 'system' destination. In this case, the older one will be closed. The transport MUST call
|
||||
//! comm_session_close() before returning from this call.
|
||||
typedef void (*TransportClose)(Transport *transport);
|
||||
|
||||
//! Pointer to function implementing the resetting of the transport.
|
||||
typedef void (*TransportReset)(Transport *transport);
|
||||
|
||||
//! Pointer to function which calls the appropriate connection speed API
|
||||
//! exported by bt_conn_mgr
|
||||
typedef void (*TransportSetConnectionResponsiveness)(Transport *transport,
|
||||
BtConsumer consumer,
|
||||
ResponseTimeState state,
|
||||
uint16_t max_period_secs,
|
||||
ResponsivenessGrantedHandler granted_handler);
|
||||
|
||||
//! Pointer to function which returns the UUID of the app that the transport connects to.
|
||||
typedef const Uuid *(*TransportGetUUID)(Transport *transport);
|
||||
|
||||
typedef CommSessionTransportType (*TransportGetType)(Transport *transport);
|
||||
|
||||
//! Pointer to function that schedules a callback to send data over the transport.
|
||||
typedef bool (*TransportSchedule)(CommSession *session);
|
||||
|
||||
typedef bool (*TransportScheduleTask)(Transport *transport);
|
||||
|
||||
//! Set of function pointers that the session can use to call back to the transport
|
||||
typedef struct TransportImplementation {
|
||||
//! Pointer to function of that will trigger the transport to send out any newly enqueued data
|
||||
//! from the send buffer. bt_lock() is held when this call is made. The implementation must be
|
||||
//! able to handle send_next() getting called but having no data in the send buffer. (This is to
|
||||
//! allow some implementations to flush out other types of data during the call)
|
||||
TransportSendNext send_next;
|
||||
|
||||
TransportClose close;
|
||||
TransportReset reset;
|
||||
TransportSetConnectionResponsiveness set_connection_responsiveness;
|
||||
|
||||
//! This field is allowed to be NULL if the transport is not UUID-aware.
|
||||
TransportGetUUID get_uuid;
|
||||
|
||||
TransportGetType get_type;
|
||||
|
||||
//! Pointer to function that schedules a callback to send data over the transport.
|
||||
//! When left NULL, bt_driver_comm_schedule_send_next_job() will be used instead.
|
||||
//! @note When providing a function, .schedule_task must be provided as well!
|
||||
TransportSchedule schedule;
|
||||
TransportScheduleTask is_current_task_schedule_task;
|
||||
} TransportImplementation;
|
||||
|
||||
//! The "destination" of the transport
|
||||
typedef enum TransportDestination {
|
||||
//! The transport carries Pebble Protocol solely for the "system", for example:
|
||||
//! iSPP/iAP with Pebble iOS App.
|
||||
TransportDestinationSystem,
|
||||
|
||||
//! The transport carries Pebble Protocol solely for a Pebble app, for example:
|
||||
//! iSPP/iAP with 3rd party native iOS App and PebbleKit iOS.
|
||||
TransportDestinationApp,
|
||||
|
||||
//! The transport carries Pebble Protocol for both the "system" and "app", for example:
|
||||
//! Plain SPP with Pebble Android App.
|
||||
TransportDestinationHybrid,
|
||||
} TransportDestination;
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Open & Close
|
||||
|
||||
//! Called by a transport to open/create a Pebble Protocol session for it.
|
||||
//! @param transport Opaque reference to the underlying serial transport
|
||||
//! @param send_next Function pointer to the implementation for the transport to send data
|
||||
//! @param is_system True if the transport is connected to the Pebble App (either using
|
||||
//! "com.getpebble.private" iAP protocol identifier on iOS, or directly connected to the Android
|
||||
//! Pebble App), false if it was directly connected to a 3rd party application.
|
||||
//! @return True if the session was opened successfully, false if not
|
||||
//! bt_lock() is expected to be taken by the caller!
|
||||
CommSession * comm_session_open(Transport *transport, const TransportImplementation *implementation,
|
||||
TransportDestination destination);
|
||||
|
||||
//! Called by the transport to indicate that the session associated with the given transport needs
|
||||
//! to be closed and cleaned up.
|
||||
//! bt_lock() is expected to be taken by the caller!
|
||||
//! @param reason For analytics tracking.
|
||||
void comm_session_close(CommSession *session, CommSessionCloseReason reason);
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Receiving
|
||||
|
||||
//! Called by the transport to copy received data from a given buffer into the receive buffer.
|
||||
//! @note bt_lock() is expected to be taken by the caller!
|
||||
void comm_session_receive_router_write(CommSession *session,
|
||||
const uint8_t *data, size_t data_size);
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Sending
|
||||
|
||||
//! @note bt_lock() is expected to be taken by the caller!
|
||||
//! @return The total size in bytes, of all the messages in the queue.
|
||||
size_t comm_session_send_queue_get_length(const CommSession *session);
|
||||
|
||||
//! Copies bytes from the send buffer into another buffer.
|
||||
//! @param start_off The offset into the send buffer
|
||||
//! @param length The number of bytes to copy
|
||||
//! @param[out] data_out Pointer to the buffer into which to copy the data
|
||||
//! @return The number of bytes copied
|
||||
//! @note To avoid making a copy, consider using comm_session_send_queue_get_read_pointer().
|
||||
//! @note The caller must ensure there is enough data available, for example by getting the length
|
||||
//! by calling comm_session_send_queue_get_length().
|
||||
//! @note bt_lock() is expected to be taken by the caller!
|
||||
size_t comm_session_send_queue_copy(CommSession *session, uint32_t start_offset,
|
||||
size_t length, uint8_t *data_out);
|
||||
|
||||
//! Gets a read pointer and the number of bytes that can be read from the read pointer.
|
||||
//! @note Internally, a non-contiguous buffer is used, so it is possible that there is more data
|
||||
//! to read. To access the entire contents, call this function and comm_session_send_queue_consume()
|
||||
//! repeatedly until it returns zero.
|
||||
//! @param data_out Pointer to the pointer to assign the read pointer to.
|
||||
//! @return The number of bytes that can be read starting at the read pointer.
|
||||
size_t comm_session_send_queue_get_read_pointer(const CommSession *session,
|
||||
const uint8_t **data_out);
|
||||
|
||||
//! @note bt_lock() is expected to be taken by the caller!
|
||||
void comm_session_send_queue_consume(CommSession *session, size_t length);
|
||||
|
||||
//! Schedule a KernelBG callback to the send_next function of the transport, if needed.
|
||||
//! In case a callback is already pending, this function is a no-op.
|
||||
//! If, by the time the callback executes, the send buffer is empty, no callback to send_next will
|
||||
//! be made either.
|
||||
//! @note bt_lock() is expected to be taken by the caller!
|
||||
void comm_session_send_next(CommSession *session);
|
97
src/fw/services/common/comm_session/wscript
Normal file
97
src/fw/services/common/comm_session/wscript
Normal file
|
@ -0,0 +1,97 @@
|
|||
import json
|
||||
|
||||
|
||||
def configure(conf):
|
||||
pass
|
||||
|
||||
|
||||
def build(bld):
|
||||
in_node = bld.path.find_node('protocol_endpoints_table.json')
|
||||
out_node = bld.path.get_bld().make_node('protocol_endpoints_table.auto.h')
|
||||
|
||||
def generate_endpoints_table(task):
|
||||
in_node = task.inputs[0]
|
||||
out_node = task.outputs[0]
|
||||
endpoints = []
|
||||
definition = {}
|
||||
with open(in_node.abspath(), 'r') as f_in:
|
||||
definition.update(json.load(f_in))
|
||||
|
||||
endpoints.extend(definition['prf_and_normal_fw'])
|
||||
if bld.variant != 'prf':
|
||||
endpoints.extend(definition['normal_fw_only'])
|
||||
endpoints.sort()
|
||||
|
||||
def get_access_enum(access_str):
|
||||
if access_str == "private":
|
||||
return "PebbleProtocolAccessPrivate"
|
||||
elif access_str == "any":
|
||||
return "PebbleProtocolAccessAny"
|
||||
elif access_str == "any":
|
||||
return "PebbleProtocolAccessPublic"
|
||||
else:
|
||||
raise ValueError("Unknown value: %s" % access_str)
|
||||
|
||||
with open(out_node.abspath(), 'w') as f_out:
|
||||
f_out.write("// GENERATED -- DO NOT EDIT\n\n")
|
||||
f_out.write("#include \"kernel/pebble_tasks.h\"\n\n")
|
||||
|
||||
DEFAULT_SYSTEM_RECV_IMPL = \
|
||||
"g_default_kernel_receiver_implementation"
|
||||
recv_imp_set = set([DEFAULT_SYSTEM_RECV_IMPL])
|
||||
|
||||
DEFAULT_SYSTEM_RECV_OPT = \
|
||||
"g_default_kernel_receiver_opt_bg"
|
||||
recv_opt_set = set([DEFAULT_SYSTEM_RECV_OPT])
|
||||
|
||||
for (eid, eid_str, access_str, cb_str,
|
||||
recv_imp, recv_opt) in endpoints:
|
||||
if recv_imp:
|
||||
recv_imp_set.add(recv_imp)
|
||||
if recv_opt:
|
||||
recv_opt_set.add(recv_opt)
|
||||
if cb_str:
|
||||
f_out.write("extern void {cb_str}(CommSession *session, "
|
||||
"const uint8_t* data, "
|
||||
"size_t length);\n".format(cb_str=cb_str))
|
||||
|
||||
f_out.write("\n\n")
|
||||
for recv_imp in recv_imp_set:
|
||||
fmt = "extern ReceiverImplementation {recv_imp};\n"
|
||||
f_out.write(fmt.format(recv_imp=recv_imp))
|
||||
|
||||
f_out.write("\n\n")
|
||||
for recv_opt in recv_opt_set:
|
||||
fmt = "extern const PebbleTask {recv_opt};\n"
|
||||
f_out.write(fmt.format(recv_opt=recv_opt))
|
||||
|
||||
f_out.write("\n\nstatic const PebbleProtocolEndpoint "
|
||||
"s_protocol_endpoints[] = {\n")
|
||||
for (eid, eid_str, access_str, cb_str,
|
||||
recv_imp, recv_opt) in endpoints:
|
||||
if int(eid_str, base=16) != eid:
|
||||
raise ValueError("Endpoint IDs need to match: %i vs %s" %
|
||||
(eid, eid_str))
|
||||
if not cb_str:
|
||||
cb_str = "NULL"
|
||||
if not recv_imp:
|
||||
recv_imp = DEFAULT_SYSTEM_RECV_IMPL
|
||||
if not recv_opt:
|
||||
recv_opt = DEFAULT_SYSTEM_RECV_OPT
|
||||
if not recv_opt:
|
||||
recv_opt = "NULL"
|
||||
else:
|
||||
recv_opt = "&" + recv_opt
|
||||
fmt = (" {{ {eid}, {cb_str}, {access_enum}, &{recv_imp},"
|
||||
" {recv_opt} }},\n")
|
||||
f_out.write(fmt.format(eid=eid,
|
||||
cb_str=cb_str,
|
||||
access_enum=get_access_enum(access_str),
|
||||
recv_imp=recv_imp,
|
||||
recv_opt=recv_opt,
|
||||
))
|
||||
f_out.write("};\n\n")
|
||||
|
||||
bld(rule=generate_endpoints_table,
|
||||
source=[in_node],
|
||||
target=out_node)
|
572
src/fw/services/common/compositor/compositor.c
Normal file
572
src/fw/services/common/compositor/compositor.c
Normal file
|
@ -0,0 +1,572 @@
|
|||
/*
|
||||
* 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 "compositor.h"
|
||||
#include "compositor_dma.h"
|
||||
#include "compositor_display.h"
|
||||
|
||||
#include "applib/graphics/bitblt.h"
|
||||
#include "applib/graphics/framebuffer.h"
|
||||
#include "applib/graphics/gcontext.h"
|
||||
#include "applib/graphics/gtypes.h"
|
||||
#include "applib/ui/animation.h"
|
||||
#include "applib/ui/animation_private.h"
|
||||
#include "drivers/display/display.h"
|
||||
#include "kernel/event_loop.h"
|
||||
#include "kernel/kernel_applib_state.h"
|
||||
#include "kernel/ui/kernel_ui.h"
|
||||
#include "kernel/ui/modals/modal_manager.h"
|
||||
#include "mcu/cache.h"
|
||||
#include "popups/timeline/peek.h"
|
||||
#include "process_management/app_manager.h"
|
||||
#include "process_management/process_manager.h"
|
||||
#include "process_state/app_state/app_state.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
#include "system/profiler.h"
|
||||
#include "util/size.h"
|
||||
|
||||
// The number of pixels for a given row which get set to black to round the corner. These numbers
|
||||
// are for the top-left corner, but can easily be translated to the other corners. This is used by
|
||||
// bezel mode to replicate the behavior of the FPGA.
|
||||
static const uint32_t s_rounded_corner_width[6] = { 6, 4, 3, 2, 1, 1 };
|
||||
|
||||
//! This is our root framebuffer that everything gets composited into.
|
||||
static FrameBuffer DMA_BSS s_framebuffer;
|
||||
|
||||
typedef enum {
|
||||
//! Render the app with no transparent modals straight through
|
||||
CompositorState_App,
|
||||
//! Render the opaque modal straight through
|
||||
CompositorState_Modal,
|
||||
//! Render the app with transparent modals straight through
|
||||
CompositorState_AppAndModal,
|
||||
//!< Waiting for the app to render itself so we can start the transition
|
||||
CompositorState_AppTransitionPending,
|
||||
//!< Compositor is running a transition animation
|
||||
CompositorState_Transitioning,
|
||||
} CompositorState;
|
||||
|
||||
//! Deferred render struct is used to handle a render event initiated while a display update is in
|
||||
//! progress and the update is non-blocking on the platform (ie. snowy/bobby smiles).
|
||||
typedef struct {
|
||||
struct {
|
||||
bool pending;
|
||||
AnimationProgress progress;
|
||||
} animation;
|
||||
struct {
|
||||
bool pending;
|
||||
} transition_complete;
|
||||
struct {
|
||||
bool pending;
|
||||
} app;
|
||||
struct {
|
||||
bool pending;
|
||||
const CompositorTransition *compositor_animation;
|
||||
} transition_start;
|
||||
} DeferredRender;
|
||||
|
||||
typedef struct {
|
||||
Animation *animation;
|
||||
const CompositorTransition *impl;
|
||||
GPoint modal_offset;
|
||||
} CompositorTransitionState;
|
||||
|
||||
static CompositorState s_state;
|
||||
|
||||
static DeferredRender s_deferred_render;
|
||||
|
||||
static CompositorTransitionState s_animation_state;
|
||||
|
||||
static bool s_framebuffer_frozen;
|
||||
|
||||
//! Animation .update function for the AnimationImplementation we use to drive our transitions.
|
||||
//! Wraps the .update function of the current CompositorTransition.
|
||||
static void prv_animation_update(Animation *animation, const AnimationProgress distance_normalized);
|
||||
|
||||
//! Call this function whenever a transition completes to change the state to one of the stable
|
||||
//! states (CompositorState_App or CompositorState_Modal).
|
||||
static void prv_finish_transition(void);
|
||||
|
||||
void compositor_init(void) {
|
||||
#if CAPABILITY_COMPOSITOR_USES_DMA && !TARGET_QEMU && !UNITTEST
|
||||
compositor_dma_init();
|
||||
#endif
|
||||
|
||||
const GSize fb_size = GSize(DISP_COLS, DISP_ROWS);
|
||||
framebuffer_init(&s_framebuffer, &fb_size);
|
||||
framebuffer_clear(&s_framebuffer);
|
||||
|
||||
s_state = CompositorState_App;
|
||||
|
||||
s_deferred_render = (DeferredRender) {
|
||||
.animation.pending = false,
|
||||
.app.pending = false
|
||||
};
|
||||
|
||||
s_animation_state = (CompositorTransitionState) { 0 };
|
||||
|
||||
s_framebuffer_frozen = false;
|
||||
}
|
||||
|
||||
// Helper functions to make implementing transitions easier
|
||||
///////////////////////////////////////////////////////////
|
||||
|
||||
void compositor_app_framebuffer_fill_callback(GContext *ctx, int16_t y,
|
||||
Fixed_S16_3 x_range_begin, Fixed_S16_3 x_range_end,
|
||||
Fixed_S16_3 delta_begin, Fixed_S16_3 delta_end,
|
||||
void *user_data) {
|
||||
const GPoint *offset = user_data ?: &GPointZero; // User data has left the building
|
||||
|
||||
GBitmap src_bitmap = compositor_get_app_framebuffer_as_bitmap();
|
||||
src_bitmap.bounds = GRect(x_range_begin.integer - offset->x, y - offset->y,
|
||||
x_range_end.integer - x_range_begin.integer, 1);
|
||||
|
||||
GBitmap dest_bitmap = compositor_get_framebuffer_as_bitmap();
|
||||
|
||||
bitblt_bitmap_into_bitmap(&dest_bitmap, &src_bitmap, GPoint(x_range_begin.integer, y),
|
||||
GCompOpAssign, GColorWhite);
|
||||
}
|
||||
|
||||
static int prv_get_rounded_corner_width(int row_index, int num_rows) {
|
||||
if (row_index >= num_rows) {
|
||||
return 0;
|
||||
}
|
||||
if (row_index < (int)ARRAY_LENGTH(s_rounded_corner_width)) {
|
||||
return s_rounded_corner_width[row_index];
|
||||
} else if (num_rows - row_index - 1 < (int)ARRAY_LENGTH(s_rounded_corner_width)) {
|
||||
return s_rounded_corner_width[num_rows - row_index - 1];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void compositor_set_modal_transition_offset(GPoint modal_offset) {
|
||||
s_animation_state.modal_offset = modal_offset;
|
||||
}
|
||||
|
||||
void compositor_render_app(void) {
|
||||
PBL_ASSERT_TASK(PebbleTask_KernelMain);
|
||||
|
||||
PROFILER_NODE_START(compositor);
|
||||
|
||||
// Don't trust the size field within the app framebuffer as the app could modify it.
|
||||
GSize app_framebuffer_size;
|
||||
app_manager_get_framebuffer_size(&app_framebuffer_size);
|
||||
const FrameBuffer *app_framebuffer = app_state_get_framebuffer();
|
||||
|
||||
if (gsize_equal(&app_framebuffer_size, &s_framebuffer.size)) {
|
||||
#if CAPABILITY_COMPOSITOR_USES_DMA && !TARGET_QEMU && !UNITTEST
|
||||
compositor_dma_run(s_framebuffer.buffer, app_framebuffer->buffer, FRAMEBUFFER_SIZE_BYTES);
|
||||
#else
|
||||
GBitmap src_bitmap = compositor_get_app_framebuffer_as_bitmap();
|
||||
GBitmap dest_bitmap = compositor_get_framebuffer_as_bitmap();
|
||||
|
||||
bitblt_bitmap_into_bitmap(&dest_bitmap, &src_bitmap, GPointZero, GCompOpAssign, GColorWhite);
|
||||
#endif
|
||||
} else {
|
||||
// On Robert, we support running older apps which have a smaller framebuffer in "bezel mode"
|
||||
// where we center them and draw a black bezel around them. Using memset to set the bezel to
|
||||
// black and using memcpy to copy the app framebuffer into the center is the fastest method
|
||||
// (significantly faster than DMA even). We only support the app framebuffer being smaller than
|
||||
// the system framebuffer and we assume the system framebuffer is always DISP_COLS x DISP_ROWS.
|
||||
|
||||
const int16_t app_width = app_framebuffer_size.w;
|
||||
const int16_t app_height = app_framebuffer_size.h;
|
||||
const int16_t bezel_width = (DISP_COLS - app_width) / 2;
|
||||
const int16_t bezel_height = (DISP_ROWS - app_height) / 2;
|
||||
const int16_t app_peek_offset_y = timeline_peek_get_origin_y() - app_height;
|
||||
const int16_t app_offset_y = CLIP(app_peek_offset_y, 0, bezel_height);
|
||||
PBL_ASSERTN((bezel_width > 0) && (bezel_height > 0));
|
||||
|
||||
uint8_t *dst = (uint8_t *)s_framebuffer.buffer;
|
||||
uint8_t *app_buffer = (uint8_t *)app_framebuffer->buffer;
|
||||
|
||||
// Set all the black pixels from the start, which is the sum of the following:
|
||||
// - app_offset_y * DISP_COLS - the top part of the bezel
|
||||
// - bezel_width - the left bezel for the first row of the app
|
||||
// - corner_pixels - the top-left corner for the first row
|
||||
const int top_bezel_length =
|
||||
app_offset_y * DISP_COLS + bezel_width + s_rounded_corner_width[0];
|
||||
memset(dst, GColorBlack.argb, top_bezel_length);
|
||||
dst += top_bezel_length;
|
||||
|
||||
// Starting from the origin for the app (bezel_width, bezel_height), copy one row of the app
|
||||
// framebuffer and set two bezel_width's worth of pixels to black. This will set the right-most
|
||||
// bezel pixels of the current row to black, and the left-most bezel pixels of the next row to
|
||||
// black.
|
||||
int corner_width = prv_get_rounded_corner_width(0, app_height);
|
||||
for (int app_row = 0; app_row < app_height; ++app_row) {
|
||||
const int row_width = app_width - corner_width * 2;
|
||||
// Copy the row of the app framebuffer (advance past the corner pixels on the left)
|
||||
const uint8_t *src = &app_buffer[app_row * app_width + corner_width];
|
||||
memcpy(dst, src, row_width);
|
||||
dst += row_width;
|
||||
|
||||
// Set the right-side corner and bezel of this row and left-size corner and bezel of the next.
|
||||
const int next_corner_width = prv_get_rounded_corner_width(app_row + 1, app_height);
|
||||
const int bezel_length = corner_width + bezel_width * 2 + next_corner_width;
|
||||
memset(dst, GColorBlack.argb, bezel_length);
|
||||
dst += bezel_length;
|
||||
corner_width = next_corner_width;
|
||||
}
|
||||
|
||||
// Set the remaining pixels to black.
|
||||
const int bottom_bezel_length = (uintptr_t)&s_framebuffer.buffer[DISP_ROWS * DISP_COLS] -
|
||||
(uintptr_t)dst;
|
||||
memset(dst, GColorBlack.argb, bottom_bezel_length);
|
||||
}
|
||||
|
||||
if (s_state == CompositorState_AppAndModal) {
|
||||
compositor_render_modal();
|
||||
}
|
||||
|
||||
PROFILER_NODE_STOP(compositor);
|
||||
|
||||
framebuffer_dirty_all(&s_framebuffer);
|
||||
}
|
||||
|
||||
void compositor_render_modal(void) {
|
||||
|
||||
GContext *ctx = kernel_ui_get_graphics_context();
|
||||
|
||||
// We make this GDrawState static to save stack space, thus the declaration and init must be
|
||||
// performed on two separate lines because the initializer value is not constant
|
||||
static GDrawState prev_state;
|
||||
prev_state = ctx->draw_state;
|
||||
|
||||
gpoint_add_eq(&ctx->draw_state.drawing_box.origin, s_animation_state.modal_offset);
|
||||
|
||||
modal_manager_render(ctx);
|
||||
|
||||
ctx->draw_state = prev_state;
|
||||
}
|
||||
|
||||
// Compositor implementation
|
||||
///////////////////////////////////////////////////////////
|
||||
|
||||
T_STATIC void prv_handle_display_update_complete(void) {
|
||||
if (s_deferred_render.transition_complete.pending) {
|
||||
s_deferred_render.transition_complete.pending = false;
|
||||
prv_finish_transition();
|
||||
}
|
||||
if (s_deferred_render.animation.pending) {
|
||||
s_deferred_render.animation.pending = false;
|
||||
prv_animation_update(s_animation_state.animation, s_deferred_render.animation.progress);
|
||||
}
|
||||
if (s_deferred_render.app.pending) {
|
||||
s_deferred_render.app.pending = false;
|
||||
compositor_app_render_ready();
|
||||
}
|
||||
if (s_deferred_render.transition_start.pending) {
|
||||
s_deferred_render.transition_start.pending = false;
|
||||
compositor_transition(s_deferred_render.transition_start.compositor_animation);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_compositor_flush(void) {
|
||||
PBL_ASSERT_TASK(PebbleTask_KernelMain);
|
||||
|
||||
// Stop the framebuffer_prepare performance timer. This timer was started when the client
|
||||
// first posted the render event to the system.
|
||||
compositor_display_update(prv_handle_display_update_complete);
|
||||
}
|
||||
|
||||
static void prv_send_did_focus_event(bool in_focus) {
|
||||
PebbleEvent event = {
|
||||
.type = PEBBLE_APP_DID_CHANGE_FOCUS_EVENT,
|
||||
.app_focus = {
|
||||
.in_focus = in_focus,
|
||||
},
|
||||
};
|
||||
event_put(&event);
|
||||
}
|
||||
|
||||
static bool prv_should_render(void) {
|
||||
return !(compositor_display_update_in_progress() || s_framebuffer_frozen);
|
||||
}
|
||||
|
||||
static void prv_release_app_framebuffer(void) {
|
||||
// Inform the app that the render is complete and it is safe to write into its framebuffer again.
|
||||
PebbleEvent event = {
|
||||
.type = PEBBLE_RENDER_FINISHED_EVENT,
|
||||
};
|
||||
process_manager_send_event_to_process(PebbleTask_App, &event);
|
||||
}
|
||||
|
||||
void compositor_app_render_ready(void) {
|
||||
if (!prv_should_render()) {
|
||||
s_deferred_render.app.pending = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_state == CompositorState_AppTransitionPending) {
|
||||
// Huzzah, the app sent us the first frame!
|
||||
if (s_animation_state.animation) {
|
||||
// We have an animation to run, run it.
|
||||
s_state = CompositorState_Transitioning;
|
||||
animation_schedule(s_animation_state.animation);
|
||||
|
||||
// Don't release the app framebuffer yet, we'll do this once the transition completes. This
|
||||
// way the app won't update its frame buffer while we're transitioning to it.
|
||||
return;
|
||||
} else {
|
||||
// No animation was used, immediately say that the app is now fully focused.
|
||||
const ModalProperty properties = modal_manager_get_properties();
|
||||
s_state = ((properties & ModalProperty_Exists) && (properties & ModalProperty_Transparent)) ?
|
||||
CompositorState_AppAndModal : CompositorState_App;
|
||||
prv_send_did_focus_event(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the app framebuffer if in the App state
|
||||
if (s_state == CompositorState_App || s_state == CompositorState_AppAndModal) {
|
||||
// compositor_render_app also renders modals if the CompositorState_AppAndModal as that state
|
||||
// indicates that there are transparent modals that allow the app framebuffer to show through
|
||||
compositor_render_app();
|
||||
prv_compositor_flush();
|
||||
}
|
||||
|
||||
// Draw the modal if in the Modal state
|
||||
if (s_state == CompositorState_Modal) {
|
||||
compositor_render_modal();
|
||||
prv_compositor_flush();
|
||||
}
|
||||
|
||||
prv_release_app_framebuffer();
|
||||
}
|
||||
|
||||
static void prv_send_app_render_request(void) {
|
||||
PebbleEvent event = {
|
||||
.type = PEBBLE_RENDER_REQUEST_EVENT,
|
||||
};
|
||||
process_manager_send_event_to_process(PebbleTask_App, &event);
|
||||
}
|
||||
|
||||
void compositor_modal_render_ready(void) {
|
||||
if ((s_state == CompositorState_Transitioning) || !prv_should_render()) {
|
||||
// Don't let the modal redraw itself when the redraw loop is being currently driven by an
|
||||
// animation or if a display update is in progress.
|
||||
return;
|
||||
}
|
||||
|
||||
if ((s_state == CompositorState_AppTransitionPending) &&
|
||||
(modal_manager_get_properties() & ModalProperty_Transparent)) {
|
||||
// Don't render if modals are transparent while the app is not ready yet
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_state == CompositorState_Modal) {
|
||||
compositor_render_modal();
|
||||
prv_compositor_flush();
|
||||
} else if (s_state == CompositorState_AppAndModal) {
|
||||
prv_send_app_render_request();
|
||||
}
|
||||
}
|
||||
|
||||
void compositor_transition_render(CompositorTransitionUpdateFunc func, Animation *animation,
|
||||
const AnimationProgress distance_normalized) {
|
||||
if (!prv_should_render()) {
|
||||
if (!s_deferred_render.transition_complete.pending) {
|
||||
s_deferred_render.animation.pending = true;
|
||||
s_deferred_render.animation.progress = distance_normalized;
|
||||
}
|
||||
return;
|
||||
}
|
||||
GContext *ctx = kernel_ui_get_graphics_context();
|
||||
|
||||
// Save the draw state in a static to save stack space
|
||||
static GDrawState prev_state;
|
||||
prev_state = ctx->draw_state;
|
||||
|
||||
func(ctx, animation, distance_normalized);
|
||||
|
||||
ctx->draw_state = prev_state;
|
||||
|
||||
if (!s_animation_state.impl->skip_modal_render_after_update) {
|
||||
compositor_render_modal();
|
||||
}
|
||||
|
||||
prv_compositor_flush();
|
||||
}
|
||||
|
||||
static void prv_animation_update(Animation *animation,
|
||||
const AnimationProgress distance_normalized) {
|
||||
PBL_ASSERT_TASK(PebbleTask_KernelMain);
|
||||
// Since we might be running this animation update as part of a deferred render, we must
|
||||
// update the kernel animation state's .current_animation to point to this animation;
|
||||
// otherwise if the animation specified any custom spacial interpolation (e.g. moook), it would
|
||||
// be ignored
|
||||
AnimationPrivate *animation_private = animation_private_animation_find(animation);
|
||||
AnimationState *kernel_animation_state = kernel_applib_get_animation_state();
|
||||
PBL_ASSERTN(animation_private && kernel_animation_state && kernel_animation_state->aux);
|
||||
AnimationPrivate *saved_current_animation = kernel_animation_state->aux->current_animation;
|
||||
|
||||
kernel_animation_state->aux->current_animation = animation_private;
|
||||
compositor_transition_render(s_animation_state.impl->update, animation, distance_normalized);
|
||||
kernel_animation_state->aux->current_animation = saved_current_animation;
|
||||
}
|
||||
|
||||
static void prv_finish_transition(void) {
|
||||
const ModalProperty properties = modal_manager_get_properties();
|
||||
if (properties & ModalProperty_Exists) {
|
||||
s_state = (properties & ModalProperty_Transparent) ? CompositorState_AppAndModal :
|
||||
CompositorState_Modal;
|
||||
compositor_modal_render_ready();
|
||||
|
||||
// Force the app framebuffer to be released. We hold it during transitions to keep the app
|
||||
// framebuffer from changing while it's being animated but now that we're done we want to make
|
||||
// sure it's always available to the app. This is only needed when we're finishing to a modal
|
||||
// since compositor_app_render_ready will also release the framebuffer.
|
||||
prv_release_app_framebuffer();
|
||||
} else {
|
||||
s_state = CompositorState_App;
|
||||
compositor_app_render_ready();
|
||||
}
|
||||
|
||||
prv_send_did_focus_event(properties & ModalProperty_Unfocused);
|
||||
}
|
||||
|
||||
static void prv_animation_teardown(Animation *animation) {
|
||||
if (s_animation_state.impl->teardown) {
|
||||
s_animation_state.impl->teardown(animation);
|
||||
}
|
||||
s_animation_state = (CompositorTransitionState) { 0 };
|
||||
|
||||
s_deferred_render.animation.pending = false;
|
||||
if (!prv_should_render()) {
|
||||
s_deferred_render.transition_complete.pending = true;
|
||||
return;
|
||||
}
|
||||
|
||||
prv_finish_transition();
|
||||
}
|
||||
|
||||
void compositor_transition(const CompositorTransition *compositor_animation) {
|
||||
if (s_animation_state.animation != NULL) {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Animation <%u> in progress, cancelling",
|
||||
(int) s_animation_state.animation);
|
||||
|
||||
animation_destroy(s_animation_state.animation);
|
||||
s_animation_state = (CompositorTransitionState) { 0 };
|
||||
|
||||
s_deferred_render.animation.pending = false;
|
||||
s_deferred_render.transition_complete.pending = false;
|
||||
}
|
||||
|
||||
if (!prv_should_render() || s_deferred_render.animation.pending) {
|
||||
if (s_deferred_render.app.pending) {
|
||||
s_deferred_render.app.pending = false;
|
||||
prv_release_app_framebuffer();
|
||||
}
|
||||
|
||||
s_deferred_render.transition_start.pending = true;
|
||||
s_deferred_render.transition_start.compositor_animation = compositor_animation;
|
||||
return;
|
||||
}
|
||||
|
||||
if (compositor_animation) {
|
||||
// Set up our animation state and schedule it
|
||||
|
||||
s_animation_state = (CompositorTransitionState) {
|
||||
.animation = animation_create(),
|
||||
.impl = compositor_animation
|
||||
};
|
||||
|
||||
static const AnimationImplementation s_compositor_animation_impl = {
|
||||
.update = prv_animation_update,
|
||||
.teardown = prv_animation_teardown,
|
||||
};
|
||||
animation_set_implementation(s_animation_state.animation, &s_compositor_animation_impl);
|
||||
|
||||
compositor_animation->init(s_animation_state.animation);
|
||||
}
|
||||
|
||||
const ModalProperty properties = modal_manager_get_properties();
|
||||
const bool is_modal_existing = (properties & ModalProperty_Exists);
|
||||
const bool is_modal_transparent = (properties & ModalProperty_Transparent);
|
||||
if (((s_state == CompositorState_Modal) && !is_modal_existing) || is_modal_transparent) {
|
||||
// Modal to App or Any to Transparent Modal
|
||||
|
||||
// We can't say for sure whether or not the app framebuffer is in a reasonable state, as the
|
||||
// app could be redrawing itself right now. Since we can't query this, instead trigger the
|
||||
// app to redraw itself. This way we will cause an PEBBLE_RENDER_READY_EVENT in the very near
|
||||
// future, regardless of the app's state.
|
||||
prv_send_app_render_request();
|
||||
|
||||
// Now wait for the ready event.
|
||||
s_state = CompositorState_AppTransitionPending;
|
||||
|
||||
} else if (is_modal_existing && !is_modal_transparent) {
|
||||
// Modal to Modal or App to Modal
|
||||
|
||||
// We can start animating immediately if we're going to a modal window. This is because
|
||||
// modal window content is drawn on demand so it's always available.
|
||||
if (compositor_animation) {
|
||||
s_state = CompositorState_Transitioning;
|
||||
animation_schedule(s_animation_state.animation);
|
||||
} else {
|
||||
prv_finish_transition();
|
||||
}
|
||||
|
||||
} else {
|
||||
// App to App
|
||||
|
||||
// We have to wait for the app to populate its framebuffer
|
||||
s_state = CompositorState_AppTransitionPending;
|
||||
}
|
||||
}
|
||||
|
||||
FrameBuffer *compositor_get_framebuffer(void) {
|
||||
return &s_framebuffer;
|
||||
}
|
||||
|
||||
GBitmap compositor_get_framebuffer_as_bitmap(void) {
|
||||
return framebuffer_get_as_bitmap(&s_framebuffer, &s_framebuffer.size);
|
||||
}
|
||||
|
||||
GBitmap compositor_get_app_framebuffer_as_bitmap(void) {
|
||||
// Get the app framebuffer state based on the size it should be to prevent a malicious app from
|
||||
// changing it and causing issues.
|
||||
GSize app_framebuffer_size;
|
||||
app_manager_get_framebuffer_size(&app_framebuffer_size);
|
||||
return framebuffer_get_as_bitmap(app_state_get_framebuffer(), &app_framebuffer_size);
|
||||
}
|
||||
|
||||
bool compositor_is_animating(void) {
|
||||
return s_state == CompositorState_AppTransitionPending ||
|
||||
s_state == CompositorState_Transitioning;
|
||||
}
|
||||
|
||||
void compositor_transition_cancel(void) {
|
||||
if (animation_is_scheduled(s_animation_state.animation)) {
|
||||
animation_unschedule(s_animation_state.animation);
|
||||
}
|
||||
}
|
||||
|
||||
void compositor_freeze(void) {
|
||||
s_framebuffer_frozen = true;
|
||||
}
|
||||
|
||||
static void prv_compositor_unfreeze_cb(void *ignored) {
|
||||
// Run deferred draws
|
||||
prv_handle_display_update_complete();
|
||||
}
|
||||
|
||||
void compositor_unfreeze(void) {
|
||||
s_framebuffer_frozen = false;
|
||||
|
||||
launcher_task_add_callback(prv_compositor_unfreeze_cb, NULL);
|
||||
}
|
135
src/fw/services/common/compositor/compositor.h
Normal file
135
src/fw/services/common/compositor/compositor.h
Normal file
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* 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/property_animation.h"
|
||||
#include "applib/ui/window.h"
|
||||
|
||||
//! @file kernel/services/compositor.h
|
||||
//! This file manages what's currently shown on the screen of your Pebble!
|
||||
//! There are two main things that are managed by the compositor...
|
||||
//!
|
||||
//! <h3>The App Framebuffer</h3>
|
||||
//! This is the framebuffer the app is currently drawing into. The compositor
|
||||
//! handles animating between app framebuffers when the app changes and window
|
||||
//! animations requested by the window stack. The compositor will also draw in
|
||||
//! the status bar when the app is in fullscreen, and the app will adjust its
|
||||
//! framebuffer's destination frame vertically. The framebuffer is simply
|
||||
//! bitblt'ed into the appropriate position whenever compositor_flush is called.
|
||||
//!
|
||||
//! @see \ref compositor_get_child_entity
|
||||
//! @see \ref compositor_set_entity_framebuffer
|
||||
//! @see \ref compositor_transition
|
||||
//! @see \ref window_stack_animation_schedule
|
||||
//! @see \ref app_emit_render_ready_event
|
||||
//!
|
||||
//! <h3>Modal Window</h3>
|
||||
//! A modal window is a Window that can be rendered on top of an app without
|
||||
//! interrupting it. The modal window can only be supplied by the kernel, so
|
||||
//! we can trust its contents. The modal window is animated up and down the
|
||||
//! screen when its pushed and popped. Since the window doesn't have a framebuffer
|
||||
//! of its own, we render it to the main framebuffer on top of everything else
|
||||
//! whenever compositor_flush is called.
|
||||
//!
|
||||
|
||||
//! Transition direction, from the current position to the next.
|
||||
//! For example, Up is a transition to some item that is upwards of the current screen.
|
||||
typedef enum {
|
||||
CompositorTransitionDirectionUp,
|
||||
CompositorTransitionDirectionDown,
|
||||
CompositorTransitionDirectionLeft,
|
||||
CompositorTransitionDirectionRight,
|
||||
CompositorTransitionDirectionNone,
|
||||
} CompositorTransitionDirection;
|
||||
|
||||
typedef void (*CompositorTransitionInitFunc)(Animation *animation);
|
||||
|
||||
// TODO: PBL-31460 Change compositor transitions to use AnimationProgress
|
||||
// This would enable time-based bounce back transitions
|
||||
typedef void (*CompositorTransitionUpdateFunc)(GContext *ctx, Animation *animation,
|
||||
uint32_t distance_normalized);
|
||||
|
||||
typedef void (*CompositorTransitionTeardownFunc)(Animation *animation);
|
||||
|
||||
typedef struct CompositorTransition {
|
||||
CompositorTransitionInitFunc init; //!< Mandatory initialization function
|
||||
CompositorTransitionUpdateFunc update; //!< Mandatory update function
|
||||
CompositorTransitionTeardownFunc teardown; //!< Optional teardown function
|
||||
//! If false, modals are rendered after the update function, otherwise they are skipped
|
||||
bool skip_modal_render_after_update;
|
||||
} CompositorTransition;
|
||||
|
||||
typedef struct FrameBuffer FrameBuffer;
|
||||
|
||||
void compositor_init(void);
|
||||
|
||||
//! Kick off a transition using the given CompositorTransition implementation. If a transition is
|
||||
//! already underway the transition will be immediately cancelled and this one will be scheduled
|
||||
//! in its place.
|
||||
//!
|
||||
//! For modal windows the new app we're animating to should already be on top of the modal window
|
||||
//! stack. For apps the new app we're animating to should always be running. For apps, the
|
||||
//! animation won't begin until the app has already started rendering itself.
|
||||
void compositor_transition(const CompositorTransition *impl);
|
||||
|
||||
//! Perform the compositor transition rendering steps for a given update function.
|
||||
//! Normally you will not call this, as the assigned transition animation automatically runs this.
|
||||
//! However, if an animation needs to be scheduled inside of the compositor transition animation,
|
||||
//! the new animation will need to call this in order to properly render.
|
||||
//! A good use-case for this is when the transition needs to use an animation_sequence.
|
||||
//!
|
||||
//! For an example of this being used, check out compositor_shutter_transitions.c
|
||||
void compositor_transition_render(CompositorTransitionUpdateFunc func, Animation *animation,
|
||||
const AnimationProgress distance_normalized);
|
||||
|
||||
//! Writes the app framebuffer to either the system framebuffer or display directly.
|
||||
//! Calls compositor_render_modal if all modals are transparent as well.
|
||||
void compositor_render_app(void);
|
||||
|
||||
//! Renders modals using the kernel graphics context
|
||||
void compositor_render_modal(void);
|
||||
|
||||
//! The modal needs to redraw its buffer to the display.
|
||||
void compositor_modal_render_ready(void);
|
||||
|
||||
//! The app needs to copy its framebuffer to the display.
|
||||
void compositor_app_render_ready(void);
|
||||
|
||||
FrameBuffer* compositor_get_framebuffer(void);
|
||||
|
||||
GBitmap compositor_get_framebuffer_as_bitmap(void);
|
||||
|
||||
//! Gets the app framebuffer as a bitmap. The bounds of the bitmap will be set based on
|
||||
//! app_manager_get_framebuffer_size() rather than the app's framebuffer size to protect against
|
||||
//! malicious apps changing it.
|
||||
GBitmap compositor_get_app_framebuffer_as_bitmap(void);
|
||||
|
||||
//! @return True if we're currently mid-animation between apps or modal windows
|
||||
bool compositor_is_animating(void);
|
||||
|
||||
//! Sets the modal draw offset for transitions that redraw the modal
|
||||
void compositor_set_modal_transition_offset(GPoint modal_offset);
|
||||
|
||||
//! Stops an existing transition in its tracks.
|
||||
void compositor_transition_cancel(void);
|
||||
|
||||
//! Don't allow new frames to be pushed to the compostor from either the app or the modal.
|
||||
void compositor_freeze(void);
|
||||
|
||||
//! Resuming allowing new frames to be pushed to the compositor, undoes the effects of
|
||||
//! compositor_freeze.
|
||||
void compositor_unfreeze(void);
|
98
src/fw/services/common/compositor/compositor_display.c
Normal file
98
src/fw/services/common/compositor/compositor_display.c
Normal file
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "compositor.h"
|
||||
|
||||
#include "applib/graphics/framebuffer.h"
|
||||
#include "applib/graphics/gtypes.h"
|
||||
#include "util/bitset.h"
|
||||
#include "util/math.h"
|
||||
#include "util/size.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
//! This variable is used when we are flushing s_framebuffer out to the display driver.
|
||||
//! It's set to the current row index that we are DMA'ing out to the display.
|
||||
static uint8_t s_current_flush_line;
|
||||
|
||||
static void (*s_update_complete_handler)(void);
|
||||
|
||||
#if PLATFORM_SILK
|
||||
static const uint8_t s_corner_shape[] = { 3, 1, 1 };
|
||||
static uint8_t s_line_buffer[FRAMEBUFFER_BYTES_PER_ROW];
|
||||
#endif
|
||||
|
||||
//! display_update get next line callback
|
||||
static bool prv_flush_get_next_line_cb(DisplayRow* row) {
|
||||
FrameBuffer *fb = compositor_get_framebuffer();
|
||||
|
||||
s_current_flush_line = MAX(s_current_flush_line, fb->dirty_rect.origin.y);
|
||||
const uint8_t y_end = fb->dirty_rect.origin.y + fb->dirty_rect.size.h;
|
||||
if (s_current_flush_line < y_end) {
|
||||
row->address = s_current_flush_line;
|
||||
void *fb_line = framebuffer_get_line(fb, s_current_flush_line);
|
||||
#if PLATFORM_SILK
|
||||
// Draw rounded corners onto the screen without modifying the
|
||||
// system framebuffer.
|
||||
if (s_current_flush_line < ARRAY_LENGTH(s_corner_shape) ||
|
||||
s_current_flush_line >= DISP_ROWS - ARRAY_LENGTH(s_corner_shape)) {
|
||||
memcpy(s_line_buffer, fb_line, FRAMEBUFFER_BYTES_PER_ROW);
|
||||
uint8_t corner_idx =
|
||||
(s_current_flush_line < ARRAY_LENGTH(s_corner_shape))?
|
||||
s_current_flush_line : DISP_ROWS - s_current_flush_line - 1;
|
||||
uint8_t corner_width = s_corner_shape[corner_idx];
|
||||
for (uint8_t pixel = 0; pixel < corner_width; ++pixel) {
|
||||
bitset8_clear(s_line_buffer, pixel);
|
||||
bitset8_clear(s_line_buffer, DISP_COLS - pixel - 1);
|
||||
}
|
||||
row->data = s_line_buffer;
|
||||
} else {
|
||||
row->data = fb_line;
|
||||
}
|
||||
#else
|
||||
row->data = fb_line;
|
||||
#endif
|
||||
s_current_flush_line++;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//! display_update complete callback
|
||||
static void prv_flush_complete_cb(void) {
|
||||
s_current_flush_line = 0;
|
||||
framebuffer_reset_dirty(compositor_get_framebuffer());
|
||||
|
||||
if (s_update_complete_handler) {
|
||||
s_update_complete_handler();
|
||||
}
|
||||
}
|
||||
|
||||
void compositor_display_update(void (*handle_update_complete_cb)(void)) {
|
||||
if (!framebuffer_is_dirty(compositor_get_framebuffer())) {
|
||||
return;
|
||||
}
|
||||
s_update_complete_handler = handle_update_complete_cb;
|
||||
s_current_flush_line = 0;
|
||||
|
||||
display_update(&prv_flush_get_next_line_cb, &prv_flush_complete_cb);
|
||||
}
|
||||
|
||||
bool compositor_display_update_in_progress(void) {
|
||||
return display_update_in_progress();
|
||||
}
|
||||
|
25
src/fw/services/common/compositor/compositor_display.h
Normal file
25
src/fw/services/common/compositor/compositor_display.h
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
//! @file compositor_display.h
|
||||
//!
|
||||
//! This module handles copying the framebuffer content to the display driver.
|
||||
|
||||
void compositor_display_update(void (*handle_update_complete_cb)(void));
|
||||
|
||||
bool compositor_display_update_in_progress(void);
|
53
src/fw/services/common/compositor/compositor_dma.c
Normal file
53
src/fw/services/common/compositor/compositor_dma.c
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 "board/board.h"
|
||||
#include "drivers/dma.h"
|
||||
#include "kernel/util/stop.h"
|
||||
#include "system/logging.h"
|
||||
|
||||
#include "FreeRTOS.h"
|
||||
#include "semphr.h"
|
||||
|
||||
#if CAPABILITY_COMPOSITOR_USES_DMA && !TARGET_QEMU && !UNITTEST
|
||||
|
||||
static SemaphoreHandle_t s_dma_in_progress;
|
||||
|
||||
void compositor_dma_init(void) {
|
||||
s_dma_in_progress = xSemaphoreCreateBinary();
|
||||
dma_request_init(COMPOSITOR_DMA);
|
||||
}
|
||||
|
||||
static bool prv_dma_complete_handler(DMARequest *transfer, void *context) {
|
||||
signed portBASE_TYPE should_context_switch = false;
|
||||
xSemaphoreGiveFromISR(s_dma_in_progress, &should_context_switch);
|
||||
return should_context_switch != pdFALSE;
|
||||
}
|
||||
|
||||
void compositor_dma_run(void *to, const void *from, uint32_t size) {
|
||||
stop_mode_disable(InhibitorCompositor);
|
||||
dma_request_start_direct(COMPOSITOR_DMA, to, from, size, prv_dma_complete_handler, NULL);
|
||||
|
||||
if (xSemaphoreTake(s_dma_in_progress, 10) != pdTRUE) {
|
||||
PBL_LOG_SYNC(LOG_LEVEL_ERROR, "DMA Compositing never completed.");
|
||||
// TODO: This should never be hit, but do we want to queue up a new render
|
||||
// event so that there is no visible breakage in low-fps situations?
|
||||
dma_request_stop(COMPOSITOR_DMA);
|
||||
}
|
||||
stop_mode_enable(InhibitorCompositor);
|
||||
}
|
||||
|
||||
#endif // CAPABILITY_COMPOSITOR_USES_DMA && !TARGET_QEMU && !UNITTEST
|
22
src/fw/services/common/compositor/compositor_dma.h
Normal file
22
src/fw/services/common/compositor/compositor_dma.h
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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>
|
||||
|
||||
void compositor_dma_init(void);
|
||||
void compositor_dma_run(void *to, const void *from, uint32_t size);
|
35
src/fw/services/common/compositor/compositor_private.h
Normal file
35
src/fw/services/common/compositor/compositor_private.h
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "applib/graphics/graphics.h"
|
||||
|
||||
//! @file compositor_private.h
|
||||
//!
|
||||
//! Useful helpful function to help out implementing compositor animations
|
||||
|
||||
//! Trigger the app framebuffer to be copied to the system framebuffer
|
||||
void compositor_render_app(void);
|
||||
|
||||
//! Trigger the modal window to be rendered to the system framebuffer
|
||||
void compositor_render_modal(void);
|
||||
|
||||
//! A GPathDrawFilledCallback that can be used to fill pixels with the app's framebuffer
|
||||
void compositor_app_framebuffer_fill_callback(GContext *ctx, int16_t y,
|
||||
Fixed_S16_3 x_range_begin, Fixed_S16_3 x_range_end,
|
||||
Fixed_S16_3 delta_begin, Fixed_S16_3 delta_end,
|
||||
void *user_data);
|
239
src/fw/services/common/compositor/compositor_transitions.c
Normal file
239
src/fw/services/common/compositor/compositor_transitions.c
Normal file
|
@ -0,0 +1,239 @@
|
|||
/*
|
||||
* 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 "compositor_transitions.h"
|
||||
#include "compositor_private.h"
|
||||
|
||||
#include "applib/graphics/framebuffer.h"
|
||||
#include "applib/graphics/gdraw_command_transforms.h"
|
||||
#include "applib/graphics/gpath.h"
|
||||
#include "applib/graphics/graphics_private_raw.h"
|
||||
#include "applib/graphics/graphics_private_raw_mask.h"
|
||||
#include "applib/ui/animation_interpolate.h"
|
||||
#include "kernel/ui/modals/modal_manager.h"
|
||||
#include "system/passert.h"
|
||||
#include "util/math.h"
|
||||
|
||||
bool compositor_transition_app_to_app_should_be_skipped(void) {
|
||||
// App-to-app compositor transitions should only be visible if there are no opaque modal windows
|
||||
return !(modal_manager_get_properties() & ModalProperty_Transparent);
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
GDrawCommandProcessor draw_command_processor;
|
||||
GContext *ctx;
|
||||
GColor stroke_color; // replace red to this
|
||||
GColor key_color; // replace this color with overdraw_color
|
||||
GColor overdraw_color; // replace key_color with this
|
||||
GColor app_fb_key_color; // replace with app framebuffer, use GColorClear to skip
|
||||
GPoint framebuffer_offset; // displacement for the app framebuffer when drawing
|
||||
} CompositorColorReplacementProcessor;
|
||||
|
||||
static void prv_compositor_replace_colors_processor(GDrawCommandProcessor *processor,
|
||||
GDrawCommand *processed_command,
|
||||
size_t processed_command_max_size,
|
||||
const GDrawCommandList* list,
|
||||
const GDrawCommand *command) {
|
||||
CompositorColorReplacementProcessor *p = (CompositorColorReplacementProcessor *)processor;
|
||||
|
||||
// fill with app framebuffer (only if a app_fb_key_color != clear was passed)
|
||||
if (!gcolor_is_invisible(p->app_fb_key_color) &&
|
||||
gcolor_equal(gdraw_command_get_fill_color(processed_command), p->app_fb_key_color)) {
|
||||
gdraw_command_set_hidden(processed_command, true);
|
||||
const uint16_t num_points = gdraw_command_get_num_points(processed_command);
|
||||
GPoint points[num_points];
|
||||
if (sizeof(points) == gdraw_command_copy_points(processed_command, points, sizeof(points))) {
|
||||
GPath path = {
|
||||
.num_points = num_points,
|
||||
.points = points
|
||||
};
|
||||
gpath_draw_filled_with_cb(p->ctx, &path,
|
||||
compositor_app_framebuffer_fill_callback,
|
||||
&p->framebuffer_offset);
|
||||
}
|
||||
} else {
|
||||
// Original SVGs use Red for the stroke, replace it here
|
||||
const GColor8 key_stroke_color = GColorRed;
|
||||
gdraw_command_replace_color(processed_command, key_stroke_color, p->stroke_color);
|
||||
// replace surrounding color
|
||||
gdraw_command_replace_color(processed_command, p->key_color, p->overdraw_color);
|
||||
}
|
||||
}
|
||||
|
||||
void compositor_transition_pdcs_animation_update(
|
||||
GContext *ctx, GDrawCommandSequence *sequence, uint32_t distance_normalized,
|
||||
GColor chroma_key_color, GColor stroke_color, GColor overdraw_color, bool inner,
|
||||
const GPoint *framebuffer_offset) {
|
||||
if (!sequence) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32_t total_duration = gdraw_command_sequence_get_total_duration(sequence);
|
||||
const uint32_t elapsed = interpolate_uint32(distance_normalized, 0, total_duration);
|
||||
GDrawCommandFrame *frame = gdraw_command_sequence_get_frame_by_elapsed(sequence, elapsed);
|
||||
|
||||
if (!frame) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Original SVGs use Islamic Green for the overdraw color, if we are filling the inner ring
|
||||
// if it's the outer ring (inner == false) this "key color" is GColorGreen,
|
||||
// also we don't render the app's framebuffer in this case
|
||||
// it's a bit weird but just they way how these SVGs have been designed
|
||||
const GColor8 key_color = inner ? GColorIslamicGreen : GColorGreen;
|
||||
CompositorColorReplacementProcessor processor = {
|
||||
.draw_command_processor.command = prv_compositor_replace_colors_processor,
|
||||
.ctx = ctx,
|
||||
.stroke_color = stroke_color,
|
||||
.key_color = key_color,
|
||||
.overdraw_color = overdraw_color,
|
||||
.app_fb_key_color = inner ? chroma_key_color : GColorClear,
|
||||
};
|
||||
|
||||
gdraw_command_frame_draw_processed(ctx, sequence, frame, GPointZero,
|
||||
&processor.draw_command_processor);
|
||||
}
|
||||
|
||||
//! Copy horizontal lines from the app framebuffer to the provided framebuffer
|
||||
//! This is basically duplicated from prv_assign_horizontal_line_raw() in graphics_private_raw.c
|
||||
void prv_app_fb_fill_assign_horizontal_line(GContext *ctx, int16_t y, Fixed_S16_3 x1,
|
||||
Fixed_S16_3 x2, GColor color) {
|
||||
PBL_ASSERTN(ctx);
|
||||
GBitmap *framebuffer = &ctx->dest_bitmap;
|
||||
PBL_ASSERTN(framebuffer->bounds.origin.x == 0 && framebuffer->bounds.origin.y == 0);
|
||||
|
||||
// Clip the line to the bitmap data row's range, taking into account fractions
|
||||
const GBitmapDataRowInfo destination_data_row_info = gbitmap_get_data_row_info(framebuffer, y);
|
||||
x1.raw_value = MAX(x1.raw_value, destination_data_row_info.min_x << FIXED_S16_3_PRECISION);
|
||||
x2.raw_value = MIN(x2.raw_value, destination_data_row_info.max_x << FIXED_S16_3_PRECISION);
|
||||
if (x1.integer > x2.integer) {
|
||||
return;
|
||||
}
|
||||
|
||||
GBitmap app_framebuffer = compositor_get_app_framebuffer_as_bitmap();
|
||||
// We only check the destination data rows above (and not also the source data rows) because we
|
||||
// assume that both source and destination are framebuffers using the native bitmap format
|
||||
PBL_ASSERTN(app_framebuffer.info.format == framebuffer->info.format);
|
||||
PBL_ASSERTN(app_framebuffer.data_row_infos == framebuffer->data_row_infos);
|
||||
|
||||
const GBitmapDataRowInfo source_data_row_info = gbitmap_get_data_row_info(&app_framebuffer, y);
|
||||
GColor8 *input = (GColor8 *)(source_data_row_info.data + x1.integer);
|
||||
GColor8 *output = (GColor8 *)(destination_data_row_info.data + x1.integer);
|
||||
|
||||
// First pixel with blending if fraction is different than 0
|
||||
const uint16_t data_row_offset =
|
||||
(uint16_t)(destination_data_row_info.data - (uint8_t *)framebuffer->addr);
|
||||
if (x1.fraction != 0) {
|
||||
graphics_private_raw_blend_color_factor(ctx, output, data_row_offset, *input, x1.integer,
|
||||
(uint8_t)(FIXED_S16_3_ONE.raw_value - x1.fraction));
|
||||
input++;
|
||||
output++;
|
||||
x1.integer++;
|
||||
}
|
||||
|
||||
// Middle pixels
|
||||
const int16_t width = x2.integer - x1.integer + 1;
|
||||
if (width > 0) {
|
||||
#if CAPABILITY_HAS_MASKING
|
||||
const GDrawMask *mask = ctx->draw_state.draw_mask;
|
||||
for (int x = x1.integer; x < x1.integer + width; x++) {
|
||||
graphics_private_raw_mask_apply(output, mask, data_row_offset, x1.integer, 1, *input);
|
||||
input++;
|
||||
output++;
|
||||
}
|
||||
#else
|
||||
memcpy((uint8_t *)output, (uint8_t *)input, width);
|
||||
input += width;
|
||||
output += width;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Last pixel with blending (don't render first AND last pixel if line length is 1)
|
||||
if (x2.fraction != 0) {
|
||||
graphics_private_raw_blend_color_factor(ctx, output, data_row_offset, *input, x2.integer,
|
||||
(uint8_t)x2.fraction);
|
||||
}
|
||||
}
|
||||
|
||||
//! Copy vertical lines from the app framebuffer to the provided framebuffer
|
||||
//! This is basically duplicated from prv_assign_vertical_line_raw() in graphics_private_raw.c
|
||||
void prv_app_fb_fill_assign_vertical_line(GContext *ctx, int16_t x, Fixed_S16_3 y1,
|
||||
Fixed_S16_3 y2, GColor color) {
|
||||
PBL_ASSERTN(ctx);
|
||||
GBitmap *framebuffer = &ctx->dest_bitmap;
|
||||
PBL_ASSERTN(framebuffer->bounds.origin.x == 0 && framebuffer->bounds.origin.y == 0);
|
||||
|
||||
GBitmap app_framebuffer = compositor_get_app_framebuffer_as_bitmap();
|
||||
// We assume that both source and destination are framebuffers using the native bitmap format
|
||||
PBL_ASSERTN(app_framebuffer.info.format == framebuffer->info.format);
|
||||
PBL_ASSERTN(app_framebuffer.data_row_infos == framebuffer->data_row_infos);
|
||||
|
||||
GBitmapDataRowInfo source_data_row_info = gbitmap_get_data_row_info(&app_framebuffer, y1.integer);
|
||||
GColor8 *input = (GColor8 *)(source_data_row_info.data + x);
|
||||
|
||||
GBitmapDataRowInfo destination_data_row_info = gbitmap_get_data_row_info(framebuffer, y1.integer);
|
||||
GColor8 *output = (GColor8 *)(destination_data_row_info.data + x);
|
||||
|
||||
// first pixel with blending
|
||||
const uint16_t data_row_offset =
|
||||
(uint16_t)(destination_data_row_info.data - (uint8_t *)framebuffer->addr);
|
||||
if (y1.fraction != 0) {
|
||||
// Only draw the pixel if its within the bitmap data row range
|
||||
if (WITHIN(x, destination_data_row_info.min_x, destination_data_row_info.max_x)) {
|
||||
graphics_private_raw_blend_color_factor(ctx, output, data_row_offset, *input, x,
|
||||
(uint8_t)(FIXED_S16_3_ONE.raw_value - y1.fraction));
|
||||
}
|
||||
y1.integer++;
|
||||
source_data_row_info = gbitmap_get_data_row_info(&app_framebuffer, y1.integer);
|
||||
input = (GColor8 *)(source_data_row_info.data + x);
|
||||
destination_data_row_info = gbitmap_get_data_row_info(framebuffer, y1.integer);
|
||||
output = (GColor8 *)(destination_data_row_info.data + x);
|
||||
}
|
||||
|
||||
// middle pixels
|
||||
while (y1.integer <= y2.integer) {
|
||||
// Only draw the pixel if its within the bitmap data row range
|
||||
if (WITHIN(x, destination_data_row_info.min_x, destination_data_row_info.max_x)) {
|
||||
#if CAPABILITY_HAS_MASKING
|
||||
const GDrawMask *mask = ctx->draw_state.draw_mask;
|
||||
graphics_private_raw_mask_apply(output, mask, data_row_offset, x, 1, *input);
|
||||
#else
|
||||
*output = *input;
|
||||
#endif
|
||||
}
|
||||
y1.integer++;
|
||||
source_data_row_info = gbitmap_get_data_row_info(&app_framebuffer, y1.integer);
|
||||
input = (GColor8 *)(source_data_row_info.data + x);
|
||||
destination_data_row_info = gbitmap_get_data_row_info(framebuffer, y1.integer);
|
||||
output = (GColor8 *)(destination_data_row_info.data + x);
|
||||
}
|
||||
|
||||
// last pixel with blending (don't render first *and* last pixel if line length is 1)
|
||||
if (y2.fraction != 0) {
|
||||
// Only draw the pixel if its within the bitmap data row range
|
||||
if (WITHIN(x, destination_data_row_info.min_x, destination_data_row_info.max_x)) {
|
||||
graphics_private_raw_blend_color_factor(ctx, output, data_row_offset, *input, x,
|
||||
(uint8_t)y2.fraction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const GDrawRawImplementation g_compositor_transitions_app_fb_draw_implementation = {
|
||||
.assign_horizontal_line = prv_app_fb_fill_assign_horizontal_line,
|
||||
.assign_vertical_line = prv_app_fb_fill_assign_vertical_line,
|
||||
// If you ever experience a crash during compositor transitions (e.g. in an integration tests)
|
||||
// then it's likely that you need to provide additional draw handlers here
|
||||
};
|
64
src/fw/services/common/compositor/compositor_transitions.h
Normal file
64
src/fw/services/common/compositor/compositor_transitions.h
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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 "default/compositor_dot_transitions.h"
|
||||
#include "default/compositor_launcher_app_transitions.h"
|
||||
#include "default/compositor_slide_transitions.h"
|
||||
#include "default/compositor_shutter_transitions.h"
|
||||
#include "legacy/compositor_app_slide_transitions.h"
|
||||
#if PLATFORM_SILK
|
||||
# include "legacy/compositor_modal_slide_transitions.h"
|
||||
#else
|
||||
# include "default/compositor_modal_transitions.h"
|
||||
# include "default/compositor_port_hole_transitions.h"
|
||||
# include "default/compositor_round_flip_transitions.h"
|
||||
#endif
|
||||
#if CAPABILITY_HAS_TIMELINE_PEEK
|
||||
#include "default/compositor_peek_transitions.h"
|
||||
#endif
|
||||
|
||||
#include "applib/graphics/gdraw_command_sequence.h"
|
||||
|
||||
//! @return Whether an app-to-app compositor animation should be skipped (e.g. if a modal is
|
||||
//! being displayed)
|
||||
bool compositor_transition_app_to_app_should_be_skipped(void);
|
||||
|
||||
//! Return a new normalized distance that represents the provided distance as a new normalized
|
||||
//! distance between the new start and end. `normalized` must be between start_distance and
|
||||
//! end_distance if you want a valid result.
|
||||
AnimationProgress animation_timing_scaled(AnimationProgress time_normalized,
|
||||
AnimationProgress interval_start,
|
||||
AnimationProgress interval_end);
|
||||
|
||||
//! Draw the next frame of the provided PDC sequence using the given options
|
||||
//! @param ctx The graphics context to use to draw the frame
|
||||
//! @param sequence The PDC sequence whose frame you want to draw
|
||||
//! @param distance_normalized The normalized distance for the current moment in the animation
|
||||
//! @param chroma_key_color The color to replace with the app's frame buffer
|
||||
//! @param stroke_color The color to use when drawing the stroke of the ring in the frame
|
||||
//! @param overdraw_color The color to use when "overdrawing" areas of the frame with no app content
|
||||
//! (e.g. flip/flop animations need to draw the right color beyond the edges of the app face)
|
||||
//! @param inner If true, draw the app frame buffer inside the ring, otherwise outside
|
||||
//! @param framebuffer_offset Visual offset of the app frame buffer
|
||||
void compositor_transition_pdcs_animation_update(
|
||||
GContext *ctx, GDrawCommandSequence *sequence, uint32_t distance_normalized,
|
||||
GColor chroma_key_color, GColor stroke_color, GColor overdraw_color, bool inner,
|
||||
const GPoint *framebuffer_offset);
|
||||
|
||||
//! Draw implementation that can be used to fill lines with the contents of the app framebuffer
|
||||
extern const GDrawRawImplementation g_compositor_transitions_app_fb_draw_implementation;
|
|
@ -0,0 +1,403 @@
|
|||
/*
|
||||
* 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 "compositor_dot_transitions.h"
|
||||
|
||||
#include "services/common/compositor/compositor_private.h"
|
||||
#include "services/common/compositor/compositor_transitions.h"
|
||||
|
||||
#include "apps/system_apps/launcher/launcher_app.h"
|
||||
#include "apps/system_apps/timeline/timeline_common.h"
|
||||
#include "applib/ui/animation_interpolate.h"
|
||||
#include "applib/ui/animation_timing.h"
|
||||
#include "applib/graphics/bitblt.h"
|
||||
#include "applib/graphics/framebuffer.h"
|
||||
#include "applib/graphics/gtypes.h"
|
||||
#include "applib/graphics/graphics.h"
|
||||
#include "applib/graphics/gpath.h"
|
||||
#include "applib/graphics/gdraw_command_transforms.h"
|
||||
#include "util/trig.h"
|
||||
#include "system/passert.h"
|
||||
#include "util/attributes.h"
|
||||
#include "util/math.h"
|
||||
#include "util/size.h"
|
||||
|
||||
static CompositorTransitionDirection prv_flip_transition_direction(
|
||||
CompositorTransitionDirection direction) {
|
||||
switch (direction) {
|
||||
case CompositorTransitionDirectionUp:
|
||||
return CompositorTransitionDirectionDown;
|
||||
case CompositorTransitionDirectionDown:
|
||||
return CompositorTransitionDirectionUp;
|
||||
case CompositorTransitionDirectionLeft:
|
||||
return CompositorTransitionDirectionRight;
|
||||
case CompositorTransitionDirectionRight:
|
||||
return CompositorTransitionDirectionLeft;
|
||||
default:
|
||||
return CompositorTransitionDirectionNone;
|
||||
}
|
||||
}
|
||||
|
||||
//! linear interpolation between two GPoints, supports delay and clamping
|
||||
//! @param delay value to postpone interpolation (in range 0..ANIMATION_NORMALIZED_MAX)
|
||||
static GPoint prv_gpoint_interpolate(int32_t delay, int32_t normalized,
|
||||
const GPoint from, const GPoint to) {
|
||||
normalized = CLIP(normalized - delay, 0, ANIMATION_NORMALIZED_MAX);
|
||||
normalized = animation_timing_curve(normalized, AnimationCurveEaseInOut);
|
||||
GPoint result;
|
||||
result.x = interpolate_int16(normalized, from.x, to.x);
|
||||
result.y = interpolate_int16(normalized, from.y, to.y);
|
||||
return result;
|
||||
}
|
||||
|
||||
//! Returns a new point halfway between two provided points
|
||||
static GPoint prv_gpoint_mid(const GPoint a, const GPoint b) {
|
||||
return GPoint((a.x + b.x) / 2, (a.y + b.y) / 2);
|
||||
}
|
||||
|
||||
//! Draw the "collapse" portion of the animation. This function can either work by drawing
|
||||
//! an outer ring using the fill_cb, or by drawing and expanding inner portion using the fill_cb.
|
||||
//! This behaviour is configured by the inner bool.
|
||||
static void prv_collapse_animation(GContext *ctx, uint32_t distance_normalized, bool inner,
|
||||
GPathDrawFilledCallback ring_fill_cb) {
|
||||
|
||||
const GRect bounds = ctx->draw_state.clip_box;
|
||||
PBL_ASSERTN(bounds.origin.x == 0 && bounds.origin.y == 0);
|
||||
|
||||
int32_t rel_p = distance_normalized;
|
||||
|
||||
// calculate dynamic positions for top-left (tl), top-right (tr), bottom-right (br), etc.
|
||||
// offset by stroke_width (sw) makes sure stroke is completely invisible at beginning/end
|
||||
const uint8_t sw = interpolate_int16(rel_p, 11, DOT_ANIMATION_STROKE_WIDTH);
|
||||
const GSize size = bounds.size;
|
||||
// outer points
|
||||
const GPoint tl = GPoint(0, 0);
|
||||
const GPoint tr = GPoint(size.w, 0);
|
||||
const GPoint br = GPoint(size.w, size.h);
|
||||
const GPoint bl = GPoint(0, size.h);
|
||||
|
||||
const GPoint center = GPoint(size.w / 2, size.h / 2);
|
||||
|
||||
// inner points
|
||||
// these magic numbers are nominators/denominators (e.g. 7) to reflect the visual effect of the
|
||||
// provided video
|
||||
const int32_t d = ANIMATION_NORMALIZED_MAX / 7;
|
||||
// pause at the end/beginning to create a total pause of 2*pause
|
||||
int32_t pause = 0;
|
||||
rel_p = rel_p * (7 + 4 + pause) / 7;
|
||||
// delays for each point between collapsing and expanding - hand-tweaked
|
||||
const GPoint scaled_tl = prv_gpoint_interpolate(0 * d, rel_p, tl, center);
|
||||
const GPoint scaled_tr = prv_gpoint_interpolate(1 * d, rel_p, tr, center);
|
||||
const GPoint scaled_bl = prv_gpoint_interpolate(3 * d, rel_p, bl, center);
|
||||
const GPoint scaled_br = prv_gpoint_interpolate(4 * d, rel_p, br, center);
|
||||
|
||||
const GPoint scaled_l = prv_gpoint_mid(scaled_tl, scaled_bl);
|
||||
const GPoint l = GPoint(-sw, scaled_l.y);
|
||||
|
||||
if (inner) {
|
||||
// gpath that creates the inner section
|
||||
GPoint path_points[] = { scaled_bl, scaled_br, scaled_tr, scaled_tl, };
|
||||
|
||||
GPath path = {
|
||||
.num_points = ARRAY_LENGTH(path_points),
|
||||
.points = path_points
|
||||
};
|
||||
|
||||
gpath_draw_filled_with_cb(ctx, &path, ring_fill_cb, NULL);
|
||||
} else {
|
||||
// gpath that creates a solid "ring"
|
||||
GPoint path_points[] = {
|
||||
tl, tr, br, bl,
|
||||
l, scaled_l,
|
||||
scaled_bl, scaled_br, scaled_tr, scaled_tl,
|
||||
scaled_l, l,
|
||||
};
|
||||
|
||||
GPath path = {
|
||||
.num_points = ARRAY_LENGTH(path_points),
|
||||
.points = path_points
|
||||
};
|
||||
|
||||
gpath_draw_filled_with_cb(ctx, &path, ring_fill_cb, NULL);
|
||||
}
|
||||
|
||||
ctx->draw_state.stroke_width = sw;
|
||||
|
||||
graphics_draw_line(ctx, scaled_tl, scaled_tr);
|
||||
graphics_draw_line(ctx, scaled_tr, scaled_br);
|
||||
graphics_draw_line(ctx, scaled_br, scaled_bl);
|
||||
graphics_draw_line(ctx, scaled_bl, scaled_tl);
|
||||
}
|
||||
|
||||
//! Callback to be used with prv_collapse_animation to fill with the current fill_color
|
||||
static void prv_gpath_draw_filled_cb(GContext *ctx, int16_t y,
|
||||
Fixed_S16_3 x_range_begin, Fixed_S16_3 x_range_end,
|
||||
Fixed_S16_3 delta_begin, Fixed_S16_3 delta_end,
|
||||
void *user_data) {
|
||||
|
||||
const GRect fill_rect = GRect(x_range_begin.integer + 1, y,
|
||||
x_range_end.integer - x_range_begin.integer - 1, 1);
|
||||
graphics_fill_rect(ctx, &fill_rect);
|
||||
}
|
||||
|
||||
//! Draw a dumb dot at the supplied position with the supplied color
|
||||
static void prv_draw_dot(GContext *ctx, GPoint pos, GColor color) {
|
||||
ctx->draw_state.stroke_width = DOT_ANIMATION_STROKE_WIDTH;
|
||||
graphics_context_set_stroke_color(ctx, color);
|
||||
graphics_draw_line(ctx, pos, pos);
|
||||
}
|
||||
|
||||
//! Packed so we can squeeze this into a void* as the animation context
|
||||
typedef struct PACKED {
|
||||
union {
|
||||
struct {
|
||||
//! Whether or not to collapse the starting screen of the animation to a dot
|
||||
bool collapse_starting_animation:1;
|
||||
//! The direction the animation is moving
|
||||
CompositorTransitionDirection direction:3;
|
||||
//! The animation's dot color after collapsing
|
||||
GColor collapse_dot_color;
|
||||
//! The animation's final dot color
|
||||
GColor final_dot_color;
|
||||
//! The background color during the animation
|
||||
GColor background_color;
|
||||
};
|
||||
void *data;
|
||||
};
|
||||
} DotTransitionAnimationConfiguration;
|
||||
|
||||
_Static_assert(sizeof(DotTransitionAnimationConfiguration) == sizeof(void *), "");
|
||||
|
||||
static void prv_collapse_animation_update_rect(GContext *ctx,
|
||||
DotTransitionAnimationConfiguration config,
|
||||
uint32_t distance_normalized) {
|
||||
GPathDrawFilledCallback draw_filled_cb;
|
||||
bool inner;
|
||||
|
||||
ctx->draw_state.fill_color = config.background_color;
|
||||
|
||||
if (config.collapse_starting_animation) {
|
||||
// Don't blank here because this intended to be an "in place" operation. The data the makes up
|
||||
// the center of the collapse is only present in the system framebuffer at this point so we
|
||||
// need to be careful not to wipe it all out.
|
||||
|
||||
// Draw in an outer ring that expands of the background color.
|
||||
draw_filled_cb = prv_gpath_draw_filled_cb;
|
||||
inner = false;
|
||||
} else {
|
||||
// First blank out any left overs from a previous frame to make sure we have a solid color
|
||||
// background.
|
||||
graphics_fill_rect(ctx, &ctx->draw_state.clip_box);
|
||||
|
||||
// Draw in an expanding inner ring of the incoming app framebuffer.
|
||||
// Note that this only expands because we're running the animation backwards.
|
||||
draw_filled_cb = compositor_app_framebuffer_fill_callback;
|
||||
inner = true;
|
||||
}
|
||||
graphics_context_set_stroke_color(ctx, config.collapse_dot_color);
|
||||
|
||||
prv_collapse_animation(ctx, distance_normalized, inner, draw_filled_cb);
|
||||
}
|
||||
|
||||
void compositor_dot_transitions_collapsing_ring_animation_update(GContext *ctx,
|
||||
uint32_t distance_normalized,
|
||||
GColor outer_ring_color,
|
||||
GColor inner_ring_color) {
|
||||
const int16_t dot_radius = DOT_ANIMATION_STROKE_WIDTH / 2;
|
||||
const GRect bounds = ctx->draw_state.clip_box;
|
||||
const GPoint center = grect_center_point(&bounds);
|
||||
|
||||
// Calculate the inner/outer radii for the outer radial and the inner radial
|
||||
const int16_t outer_radial_outer_radius = (bounds.size.w / 2) + (dot_radius * 2);
|
||||
const int16_t outer_radial_inner_radius_from = (bounds.size.w / 2) + dot_radius;
|
||||
const int16_t outer_radial_inner_radius_to = dot_radius;
|
||||
const int16_t interpolated_outer_radial_inner_radius = interpolate_int16(distance_normalized,
|
||||
outer_radial_inner_radius_from, outer_radial_inner_radius_to);
|
||||
const int16_t inner_radial_outer_radius = interpolated_outer_radial_inner_radius;
|
||||
const int16_t inner_radial_inner_radius = inner_radial_outer_radius - dot_radius;
|
||||
|
||||
// Draw an outer ring to show the collapsing/expanding to/from a dot
|
||||
graphics_context_set_stroke_color(ctx, outer_ring_color);
|
||||
graphics_context_set_fill_color(ctx, outer_ring_color);
|
||||
|
||||
graphics_fill_radial_internal(ctx, center, interpolated_outer_radial_inner_radius,
|
||||
outer_radial_outer_radius, 0, TRIG_MAX_ANGLE);
|
||||
|
||||
// The outer ring also has a small inner ring with a radial width equal to the dot radius
|
||||
graphics_context_set_stroke_color(ctx, inner_ring_color);
|
||||
graphics_context_set_fill_color(ctx, inner_ring_color);
|
||||
graphics_fill_radial_internal(ctx, center, inner_radial_inner_radius, inner_radial_outer_radius,
|
||||
0,
|
||||
TRIG_MAX_ANGLE);
|
||||
}
|
||||
|
||||
static void prv_collapse_animation_update_round(GContext *ctx,
|
||||
DotTransitionAnimationConfiguration config,
|
||||
uint32_t distance_normalized) {
|
||||
// If we're expanding, blit the app framebuffer into the system framebuffer (so below the ring)
|
||||
if (!config.collapse_starting_animation) {
|
||||
GBitmap src_bitmap = compositor_get_app_framebuffer_as_bitmap();
|
||||
GBitmap dest_bitmap = compositor_get_framebuffer_as_bitmap();
|
||||
bitblt_bitmap_into_bitmap(&dest_bitmap, &src_bitmap, GPointZero, GCompOpAssign, GColorWhite);
|
||||
}
|
||||
|
||||
compositor_dot_transitions_collapsing_ring_animation_update(ctx, distance_normalized,
|
||||
config.background_color,
|
||||
config.collapse_dot_color);
|
||||
}
|
||||
|
||||
static void prv_collapse_animation_update(GContext *ctx,
|
||||
DotTransitionAnimationConfiguration config,
|
||||
uint32_t distance_normalized) {
|
||||
PBL_IF_RECT_ELSE(prv_collapse_animation_update_rect,
|
||||
prv_collapse_animation_update_round)(ctx, config, distance_normalized);
|
||||
}
|
||||
|
||||
static void prv_static_dot_transition_animation_update(
|
||||
GContext *ctx, Animation *animation, uint32_t distance_normalized) {
|
||||
DotTransitionAnimationConfiguration config = {
|
||||
.data = animation_get_context(animation)
|
||||
};
|
||||
|
||||
const uint32_t COLLAPSE_END_DISTANCE = 7 * (ANIMATION_NORMALIZED_MAX / 8);
|
||||
const GRect bounds = ctx->draw_state.clip_box;
|
||||
const GPoint center = grect_center_point(&bounds);
|
||||
|
||||
if (distance_normalized < COLLAPSE_END_DISTANCE) {
|
||||
const uint32_t local_distance = animation_timing_scaled(distance_normalized,
|
||||
0,
|
||||
COLLAPSE_END_DISTANCE);
|
||||
prv_collapse_animation_update(ctx, config, local_distance);
|
||||
} else {
|
||||
prv_draw_dot(ctx, center, config.collapse_dot_color);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_configure_dot_transition_animation(Animation *animation,
|
||||
GColor collapse_dot_color,
|
||||
GColor final_dot_color,
|
||||
GColor background_color,
|
||||
CompositorTransitionDirection direction,
|
||||
uint32_t duration,
|
||||
bool collapse_starting_animation) {
|
||||
// Flip the direction and dot colors if we aren't starting with a collapsing animation
|
||||
// because we reverse the animation below
|
||||
if (!collapse_starting_animation) {
|
||||
direction = prv_flip_transition_direction(direction);
|
||||
|
||||
GColor swap_collapse_dot_color = collapse_dot_color;
|
||||
collapse_dot_color = final_dot_color;
|
||||
final_dot_color = swap_collapse_dot_color;
|
||||
}
|
||||
|
||||
DotTransitionAnimationConfiguration config = {
|
||||
.collapse_starting_animation = collapse_starting_animation,
|
||||
.collapse_dot_color = collapse_dot_color,
|
||||
.final_dot_color = final_dot_color,
|
||||
.background_color = background_color,
|
||||
.direction = direction
|
||||
};
|
||||
|
||||
animation_set_curve(animation, AnimationCurveLinear);
|
||||
animation_set_duration(animation, duration);
|
||||
animation_set_handlers(animation, (AnimationHandlers) { 0 }, config.data);
|
||||
animation_set_reverse(animation, !collapse_starting_animation);
|
||||
}
|
||||
|
||||
static void prv_dot_transition_to_timeline_past_animation_init(Animation *animation) {
|
||||
prv_configure_dot_transition_animation(animation, TIMELINE_DOT_COLOR,
|
||||
TIMELINE_DOT_COLOR, TIMELINE_PAST_COLOR,
|
||||
CompositorTransitionDirectionUp,
|
||||
STATIC_DOT_ANIMATION_DURATION_MS, false);
|
||||
}
|
||||
|
||||
static void prv_dot_transition_from_timeline_past_animation_init(Animation *animation) {
|
||||
prv_configure_dot_transition_animation(animation, TIMELINE_DOT_COLOR,
|
||||
TIMELINE_DOT_COLOR, TIMELINE_PAST_COLOR,
|
||||
CompositorTransitionDirectionDown,
|
||||
STATIC_DOT_ANIMATION_DURATION_MS, true);
|
||||
}
|
||||
|
||||
static void prv_dot_transition_to_timeline_future_animation_init(Animation *animation) {
|
||||
prv_configure_dot_transition_animation(animation, TIMELINE_DOT_COLOR,
|
||||
TIMELINE_DOT_COLOR, TIMELINE_FUTURE_COLOR,
|
||||
CompositorTransitionDirectionUp,
|
||||
STATIC_DOT_ANIMATION_DURATION_MS, true);
|
||||
}
|
||||
|
||||
static void prv_dot_transition_from_timeline_future_animation_init(Animation *animation) {
|
||||
prv_configure_dot_transition_animation(animation, TIMELINE_DOT_COLOR,
|
||||
TIMELINE_DOT_COLOR, TIMELINE_FUTURE_COLOR,
|
||||
CompositorTransitionDirectionDown,
|
||||
STATIC_DOT_ANIMATION_DURATION_MS, false);
|
||||
}
|
||||
|
||||
static void prv_dot_transition_from_app_fetch_animation_init(Animation *animation) {
|
||||
prv_configure_dot_transition_animation(animation, GColorWhite,
|
||||
GColorWhite, GColorLightGray,
|
||||
CompositorTransitionDirectionNone,
|
||||
STATIC_DOT_ANIMATION_DURATION_MS, false);
|
||||
}
|
||||
|
||||
|
||||
const CompositorTransition* compositor_dot_transition_timeline_get(bool timeline_is_future,
|
||||
bool timeline_is_destination) {
|
||||
if (compositor_transition_app_to_app_should_be_skipped()) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (timeline_is_future) {
|
||||
if (timeline_is_destination) {
|
||||
static const CompositorTransition s_impl = {
|
||||
.init = prv_dot_transition_to_timeline_future_animation_init,
|
||||
.update = prv_static_dot_transition_animation_update,
|
||||
};
|
||||
return &s_impl;
|
||||
} else {
|
||||
static const CompositorTransition s_impl = {
|
||||
.init = prv_dot_transition_from_timeline_future_animation_init,
|
||||
.update = prv_static_dot_transition_animation_update,
|
||||
};
|
||||
return &s_impl;
|
||||
}
|
||||
} else {
|
||||
if (timeline_is_destination) {
|
||||
static const CompositorTransition s_impl = {
|
||||
.init = prv_dot_transition_from_timeline_past_animation_init,
|
||||
.update = prv_static_dot_transition_animation_update,
|
||||
};
|
||||
return &s_impl;
|
||||
} else {
|
||||
static const CompositorTransition s_impl = {
|
||||
.init = prv_dot_transition_to_timeline_past_animation_init,
|
||||
.update = prv_static_dot_transition_animation_update,
|
||||
};
|
||||
return &s_impl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const CompositorTransition* compositor_dot_transition_app_fetch_get(void) {
|
||||
if (compositor_transition_app_to_app_should_be_skipped()) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static const CompositorTransition s_impl = {
|
||||
.init = prv_dot_transition_from_app_fetch_animation_init,
|
||||
.update = prv_static_dot_transition_animation_update,
|
||||
};
|
||||
return &s_impl;
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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/common/compositor/compositor.h"
|
||||
|
||||
// These numbers approximate the visuals shown in the videos from the design team
|
||||
#define STATIC_DOT_ANIMATION_DURATION_MS 233
|
||||
#define DOT_ANIMATION_STROKE_WIDTH 12
|
||||
|
||||
void compositor_dot_transitions_collapsing_ring_animation_update(GContext *ctx,
|
||||
uint32_t distance_normalized,
|
||||
GColor outer_ring_color,
|
||||
GColor inner_ring_color);
|
||||
|
||||
const CompositorTransition* compositor_dot_transition_timeline_get(bool timeline_is_future,
|
||||
bool timeline_is_destination);
|
||||
|
||||
const CompositorTransition* compositor_dot_transition_app_fetch_get(void);
|
|
@ -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 "compositor_launcher_app_transitions.h"
|
||||
|
||||
#include "applib/graphics/bitblt.h"
|
||||
#include "applib/graphics/framebuffer.h"
|
||||
#include "applib/graphics/graphics_private.h"
|
||||
#include "apps/system_apps/launcher/default/launcher_app.h"
|
||||
#include "services/common/compositor/compositor_transitions.h"
|
||||
#include "system/passert.h"
|
||||
#include "util/attributes.h"
|
||||
#include "util/math.h"
|
||||
#include "util/size.h"
|
||||
|
||||
typedef struct CompositorLauncherAppTransitionData {
|
||||
bool app_is_destination;
|
||||
LauncherDrawState launcher_draw_state;
|
||||
int16_t prev_delta_x_before_cut;
|
||||
} CompositorLauncherAppTransitionData;
|
||||
|
||||
static CompositorLauncherAppTransitionData s_data;
|
||||
|
||||
// This custom moook curve was created with iterative feedback from the Design team
|
||||
static const int32_t s_custom_moook_frames_in[] = {0, 1, 2, 4, 12, 24, 48};
|
||||
static const int32_t s_custom_moook_frames_out[] = {12, 6, 3, 2, 1, 0};
|
||||
static const MoookConfig s_custom_moook_config = {
|
||||
.frames_in = s_custom_moook_frames_in,
|
||||
.num_frames_in = ARRAY_LENGTH(s_custom_moook_frames_in),
|
||||
.frames_out = s_custom_moook_frames_out,
|
||||
.num_frames_out = ARRAY_LENGTH(s_custom_moook_frames_out),
|
||||
};
|
||||
|
||||
static void prv_move_region_of_bitmap_horizontally(GBitmap *bitmap, const GRect *region,
|
||||
int16_t delta_x) {
|
||||
if (!bitmap || !region) {
|
||||
return;
|
||||
}
|
||||
GBitmap region_sub_bitmap;
|
||||
gbitmap_init_as_sub_bitmap(®ion_sub_bitmap, bitmap, *region);
|
||||
graphics_private_move_pixels_horizontally(®ion_sub_bitmap, delta_x, true /* patch_garbage */);
|
||||
}
|
||||
|
||||
static void prv_copy_app_fb_patching_garbage(int16_t dest_origin_x) {
|
||||
const GBitmap src_bitmap = compositor_get_app_framebuffer_as_bitmap();
|
||||
GBitmap dest_bitmap = compositor_get_framebuffer_as_bitmap();
|
||||
|
||||
// Patch garbage pixels using the first/last column, if necessary
|
||||
if (dest_origin_x != 0) {
|
||||
const bool offscreen_left = (dest_origin_x < 0);
|
||||
const int16_t first_column_x = 0;
|
||||
const int16_t last_column_x = DISP_COLS - 1;
|
||||
const GRect column_to_replicate = (GRect) {
|
||||
.origin = GPoint((offscreen_left ? last_column_x : first_column_x), 0),
|
||||
.size = GSize(1, DISP_ROWS),
|
||||
};
|
||||
GBitmap column_to_replicate_sub_bitmap;
|
||||
gbitmap_init_as_sub_bitmap(&column_to_replicate_sub_bitmap, &src_bitmap, column_to_replicate);
|
||||
const int16_t from = (int16_t)(offscreen_left ? (dest_origin_x + DISP_COLS) : first_column_x);
|
||||
const int16_t to = (int16_t)(offscreen_left ? last_column_x : dest_origin_x - 1);
|
||||
for (int16_t x = from; x <= to; x++) {
|
||||
bitblt_bitmap_into_bitmap(&dest_bitmap, &column_to_replicate_sub_bitmap,
|
||||
GPoint(x, 0), GCompOpAssign, GColorWhite);
|
||||
}
|
||||
}
|
||||
|
||||
bitblt_bitmap_into_bitmap(&dest_bitmap, &src_bitmap, GPoint(dest_origin_x, 0), GCompOpAssign,
|
||||
GColorWhite);
|
||||
}
|
||||
|
||||
static void prv_manipulate_launcher_in_system_framebuffer(GContext *ctx,
|
||||
const GRect *selection_rect,
|
||||
int16_t delta, GColor selection_color) {
|
||||
if (!ctx || !selection_rect || (delta == 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Move the selection rectangle
|
||||
prv_move_region_of_bitmap_horizontally(&ctx->dest_bitmap, selection_rect, delta);
|
||||
|
||||
const int16_t abs_delta = (int16_t)ABS(delta);
|
||||
|
||||
// Move everything above the selection rectangle (if there is anything) and stretch the selection
|
||||
// color up
|
||||
const int16_t area_above_selection_rect_height = selection_rect->origin.y;
|
||||
if (area_above_selection_rect_height > 0) {
|
||||
const GRect area_above_selection_rect = GRect(-selection_rect->origin.x, 0, DISP_COLS,
|
||||
area_above_selection_rect_height);
|
||||
prv_move_region_of_bitmap_horizontally(&ctx->dest_bitmap, &area_above_selection_rect, -delta);
|
||||
|
||||
const GRect stretch_rect_above_selection_rect =
|
||||
GRect(0, selection_rect->origin.y - abs_delta, DISP_COLS, abs_delta);
|
||||
graphics_context_set_fill_color(ctx, selection_color);
|
||||
graphics_fill_rect(ctx, &stretch_rect_above_selection_rect);
|
||||
}
|
||||
|
||||
// Move everything below the selection rectangle (if there is anything) and stretch the selection
|
||||
// color down
|
||||
const int16_t row_below_selection_rect_bottom = grect_get_max_y(selection_rect);
|
||||
const int16_t area_below_selection_rect_height =
|
||||
(int16_t)DISP_ROWS - row_below_selection_rect_bottom;
|
||||
if (area_below_selection_rect_height > 0) {
|
||||
const GRect area_below_selection_rect = GRect(-selection_rect->origin.x,
|
||||
row_below_selection_rect_bottom, DISP_COLS,
|
||||
area_below_selection_rect_height);
|
||||
prv_move_region_of_bitmap_horizontally(&ctx->dest_bitmap, &area_below_selection_rect, -delta);
|
||||
|
||||
const GRect stretch_rect_below_selection_rect = GRect(0, row_below_selection_rect_bottom,
|
||||
DISP_COLS, abs_delta);
|
||||
graphics_context_set_fill_color(ctx, selection_color);
|
||||
graphics_fill_rect(ctx, &stretch_rect_below_selection_rect);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_launcher_app_transition_animation_update(GContext *ctx, Animation *UNUSED animation,
|
||||
uint32_t distance_normalized) {
|
||||
const bool is_right = s_data.app_is_destination;
|
||||
const GRangeVertical selection_vertical_range =
|
||||
s_data.launcher_draw_state.selection_vertical_range;
|
||||
const GColor selection_color = s_data.launcher_draw_state.selection_background_color;
|
||||
|
||||
const int16_t start = 0;
|
||||
const int16_t end = DISP_COLS;
|
||||
const int16_t delta_x_before_cut = interpolate_int16(distance_normalized, start, end);
|
||||
const int16_t delta_x_before_cut_diff = delta_x_before_cut - s_data.prev_delta_x_before_cut;
|
||||
const int16_t delta_x_after_cut = interpolate_int16(distance_normalized, -end, start);
|
||||
|
||||
// This rect specifies where the launcher's selected row currently is in the system framebuffer
|
||||
const GRect selection_rect = GRect((is_right ? s_data.prev_delta_x_before_cut : start),
|
||||
selection_vertical_range.origin_y, DISP_COLS,
|
||||
selection_vertical_range.size_h);
|
||||
|
||||
// We know we're before the moook cut if our delta for after the cut hasn't "moooked" beyond
|
||||
// where we will finish the animation
|
||||
const bool before_cut = (delta_x_after_cut < start);
|
||||
if (before_cut) {
|
||||
if (is_right) {
|
||||
// Manipulate the launcher's pixels in the system framebuffer so the selection moves from its
|
||||
// starting point right and everything else moves left
|
||||
prv_manipulate_launcher_in_system_framebuffer(ctx, &selection_rect, delta_x_before_cut_diff,
|
||||
selection_color);
|
||||
} else {
|
||||
// Move the system framebuffer's pixels from its starting point right
|
||||
const bool patch_garbage = true;
|
||||
graphics_private_move_pixels_horizontally(&ctx->dest_bitmap, delta_x_before_cut_diff,
|
||||
patch_garbage);
|
||||
}
|
||||
|
||||
// Save the delta we used so we can calculate the diff for the next frame
|
||||
s_data.prev_delta_x_before_cut = delta_x_before_cut;
|
||||
} else {
|
||||
const int16_t dest_origin_x = (is_right ? -delta_x_after_cut : start);
|
||||
// Copy the entire app framebuffer (containing the launcher) to the compositor framebuffer
|
||||
prv_copy_app_fb_patching_garbage(dest_origin_x);
|
||||
if (!is_right) {
|
||||
// Manipulate the launcher's pixels in the system framebuffer so the selection moves right
|
||||
// and everything else moves left so everything comes to rest at its final position
|
||||
prv_manipulate_launcher_in_system_framebuffer(ctx, &selection_rect, -delta_x_after_cut,
|
||||
selection_color);
|
||||
}
|
||||
}
|
||||
|
||||
// Technically the whole framebuffer may not be dirty after each frame (and thus not need to be
|
||||
// marked as such so we don't flush every scan line to the display), but let's make it easy and
|
||||
// just dirty the whole framebuffer on each frame anyway since most pixels do change
|
||||
framebuffer_dirty_all(compositor_get_framebuffer());
|
||||
}
|
||||
|
||||
static int64_t prv_launcher_app_transition_custom_moook(int32_t progress, int64_t from,
|
||||
int64_t to) {
|
||||
return interpolate_moook_custom(progress, from, to, &s_custom_moook_config);
|
||||
}
|
||||
|
||||
static uint32_t prv_launcher_app_transition_custom_moook_duration(void) {
|
||||
return interpolate_moook_custom_duration(&s_custom_moook_config);
|
||||
}
|
||||
|
||||
static void prv_launcher_app_transition_animation_init(Animation *animation) {
|
||||
// Grab the draw state now that the launcher has had a chance to save its state before closing
|
||||
const LauncherDrawState *launcher_draw_state = launcher_app_get_draw_state();
|
||||
PBL_ASSERTN(launcher_draw_state);
|
||||
s_data.launcher_draw_state = *launcher_draw_state;
|
||||
|
||||
animation_set_custom_interpolation(animation, prv_launcher_app_transition_custom_moook);
|
||||
animation_set_duration(animation, prv_launcher_app_transition_custom_moook_duration());
|
||||
}
|
||||
|
||||
const CompositorTransition *compositor_launcher_app_transition_get(bool app_is_destination) {
|
||||
if (compositor_transition_app_to_app_should_be_skipped()) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
s_data = (CompositorLauncherAppTransitionData) {
|
||||
.app_is_destination = app_is_destination,
|
||||
};
|
||||
|
||||
static const CompositorTransition s_impl = {
|
||||
.init = prv_launcher_app_transition_animation_init,
|
||||
.update = prv_launcher_app_transition_animation_update,
|
||||
};
|
||||
return &s_impl;
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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/common/compositor/compositor.h"
|
||||
|
||||
#include "apps/system_apps/launcher/launcher_app.h"
|
||||
|
||||
//! @file compositor_launcher_app_transitions.h
|
||||
//! Allows a user to create and configure compositor transition animations between the launcher
|
||||
//! and apps.
|
||||
|
||||
//! @param app_is_destination Whether the animation should reflect the app as the destination
|
||||
//! @return \ref CompositorTransition for the resulting animation
|
||||
const CompositorTransition *compositor_launcher_app_transition_get(bool app_is_destination);
|
|
@ -0,0 +1,229 @@
|
|||
/*
|
||||
* 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 "compositor_modal_transitions.h"
|
||||
|
||||
#include "services/common/compositor/compositor_transitions.h"
|
||||
#include "services/common/compositor/compositor_private.h"
|
||||
|
||||
#include "applib/graphics/framebuffer.h"
|
||||
#include "util/trig.h"
|
||||
#include "applib/ui/animation_interpolate.h"
|
||||
#include "applib/graphics/gdraw_command_sequence.h"
|
||||
#include "apps/system_apps/timeline/timeline_common.h"
|
||||
#include "kernel/ui/kernel_ui.h"
|
||||
#include "kernel/ui/modals/modal_manager.h"
|
||||
#include "resource/resource_ids.auto.h"
|
||||
|
||||
|
||||
// No animations will be shown on the following platforms
|
||||
#if defined(RECOVERY_FW)
|
||||
#define MODAL_CONTRACT_TO_MODAL_ANIMATION (RESOURCE_ID_INVALID)
|
||||
#define MODAL_CONTRACT_FROM_MODAL_ANIMATION (RESOURCE_ID_INVALID)
|
||||
#define MODAL_EXPAND_TO_APP_ANIMATION (RESOURCE_ID_INVALID)
|
||||
#else
|
||||
#define MODAL_CONTRACT_TO_MODAL_ANIMATION (RESOURCE_ID_MODAL_CONTRACT_TO_MODAL_SEQUENCE)
|
||||
#define MODAL_CONTRACT_FROM_MODAL_ANIMATION (RESOURCE_ID_MODAL_CONTRACT_FROM_MODAL_SEQUENCE)
|
||||
#define MODAL_EXPAND_TO_APP_ANIMATION (RESOURCE_ID_MODAL_EXPAND_TO_APP_SEQUENCE)
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
GColor outer_color;
|
||||
bool modal_is_destination;
|
||||
bool expanding;
|
||||
GDrawCommandSequence *animation_sequence;
|
||||
} CompositorModalTransitionData;
|
||||
|
||||
static CompositorModalTransitionData s_data;
|
||||
|
||||
static void prv_modal_transition_animation_teardown_rect(Animation *animation);
|
||||
|
||||
static void prv_modal_transition_animation_init_sequence(const uint32_t resource_id) {
|
||||
prv_modal_transition_animation_teardown_rect(NULL);
|
||||
s_data.animation_sequence = gdraw_command_sequence_create_with_resource(resource_id);
|
||||
}
|
||||
|
||||
static void prv_modal_transition_fill_update(GContext *ctx,
|
||||
uint32_t distance_normalized,
|
||||
bool inner) {
|
||||
const GColor replace_color = GColorGreen;
|
||||
const GColor stroke_color = TIMELINE_DOT_COLOR;
|
||||
compositor_transition_pdcs_animation_update(
|
||||
ctx, s_data.animation_sequence, distance_normalized, replace_color, stroke_color,
|
||||
s_data.outer_color /* overdraw color */, inner, NULL);
|
||||
}
|
||||
|
||||
void prv_render_modal_if_necessary(void) {
|
||||
// Since modal windows don't have a framebuffer that we can use in the compositor animation,
|
||||
// draw the modal now (if one exists) so the modal compositor animations can draw on top of it,
|
||||
// revealing the relevant parts of the modal window throughout the animation
|
||||
if (s_data.modal_is_destination) {
|
||||
compositor_render_modal();
|
||||
}
|
||||
}
|
||||
|
||||
static NOINLINE void prv_render_transition_rect(GContext *ctx, Animation *animation,
|
||||
uint32_t distance_normalized) {
|
||||
// If the modal is the destination, just draw the frame and fill its inner ring with the app's
|
||||
// frame buffer
|
||||
if (s_data.modal_is_destination) {
|
||||
prv_modal_transition_fill_update(ctx, distance_normalized, true /* fill inner */);
|
||||
return;
|
||||
}
|
||||
|
||||
// For the first half of the animation where the app is the destination, draw the "contract
|
||||
// from modal" frame and fill its outer ring with the background color specified by
|
||||
// s_data.outer_color
|
||||
const uint32_t contract_to_dot_distance = ANIMATION_NORMALIZED_MAX / 2;
|
||||
if (distance_normalized < contract_to_dot_distance) {
|
||||
// Switch to the "contract from modal" animation if necessary (e.g. if the animation was
|
||||
// reversed in the future)
|
||||
if (s_data.expanding) {
|
||||
prv_modal_transition_animation_init_sequence(MODAL_CONTRACT_FROM_MODAL_ANIMATION);
|
||||
s_data.expanding = false;
|
||||
}
|
||||
distance_normalized = animation_timing_scaled(distance_normalized,
|
||||
0,
|
||||
contract_to_dot_distance);
|
||||
prv_modal_transition_fill_update(ctx, distance_normalized, false /* fill outer */);
|
||||
} else {
|
||||
// For the second half of the animation where the app is the destination, draw the "expand to
|
||||
// app" frame and fill its inner ring with the app's frame buffer
|
||||
// Switch to the "expand to app" animation if necessary
|
||||
if (!s_data.expanding) {
|
||||
prv_modal_transition_animation_init_sequence(MODAL_EXPAND_TO_APP_ANIMATION);
|
||||
s_data.expanding = true;
|
||||
}
|
||||
distance_normalized = animation_timing_scaled(distance_normalized,
|
||||
contract_to_dot_distance,
|
||||
ANIMATION_NORMALIZED_MAX);
|
||||
prv_modal_transition_fill_update(ctx, distance_normalized, true /* fill inner */);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_modal_transition_animation_update_rect(GContext *ctx, Animation *animation,
|
||||
uint32_t distance_normalized) {
|
||||
prv_render_modal_if_necessary();
|
||||
prv_render_transition_rect(ctx, animation, distance_normalized);
|
||||
}
|
||||
|
||||
static NOINLINE void prv_render_transition_round(GContext *ctx, Animation *animation,
|
||||
uint32_t distance_normalized) {
|
||||
const int16_t dot_radius = DOT_ANIMATION_STROKE_WIDTH / 2;
|
||||
const GRect display_bounds = ctx->draw_state.clip_box;
|
||||
const GPoint circle_center = grect_center_point(&display_bounds);
|
||||
|
||||
// Calculate the inner/outer radii for the colored radial and the dot radial
|
||||
const int16_t dot_ring_outer_radius_from = (display_bounds.size.w / 2) + (dot_radius * 2);
|
||||
const int16_t dot_ring_outer_radius_to = dot_radius;
|
||||
const int16_t interpolated_dot_ring_outer_radius = interpolate_int16(distance_normalized,
|
||||
dot_ring_outer_radius_from,
|
||||
dot_ring_outer_radius_to);
|
||||
const int16_t dot_ring_inner_radius = interpolated_dot_ring_outer_radius - dot_radius;
|
||||
|
||||
// Draw the dot ring
|
||||
graphics_context_set_fill_color(ctx, TIMELINE_DOT_COLOR);
|
||||
graphics_fill_radial_internal(ctx, circle_center, dot_ring_inner_radius,
|
||||
interpolated_dot_ring_outer_radius, 0, TRIG_MAX_ANGLE);
|
||||
|
||||
// Save a reference to the existing draw implementation
|
||||
const GDrawRawImplementation *saved_draw_implementation = ctx->draw_state.draw_implementation;
|
||||
|
||||
// Replace the draw implementation with one that fills horizontal lines using the app framebuffer
|
||||
ctx->draw_state.draw_implementation = &g_compositor_transitions_app_fb_draw_implementation;
|
||||
|
||||
// Fill the inside of the dot ring with the app framebuffer
|
||||
graphics_fill_circle(ctx, circle_center, dot_ring_inner_radius);
|
||||
|
||||
// Restore the saved draw implementation
|
||||
ctx->draw_state.draw_implementation = saved_draw_implementation;
|
||||
}
|
||||
|
||||
static void prv_modal_push_transition_animation_update_round(GContext *ctx, Animation *animation,
|
||||
uint32_t distance_normalized) {
|
||||
prv_render_modal_if_necessary();
|
||||
prv_render_transition_round(ctx, animation, distance_normalized);
|
||||
}
|
||||
|
||||
static void prv_modal_transition_animation_init_rect(Animation *animation) {
|
||||
const uint32_t resource_id = s_data.modal_is_destination ?
|
||||
MODAL_CONTRACT_TO_MODAL_ANIMATION :
|
||||
MODAL_CONTRACT_FROM_MODAL_ANIMATION;
|
||||
prv_modal_transition_animation_init_sequence(resource_id);
|
||||
|
||||
// Tweaked from observations by the design team
|
||||
const uint32_t duration = s_data.modal_is_destination ? 310 : 800;
|
||||
if (s_data.animation_sequence) {
|
||||
animation_set_duration(animation, duration);
|
||||
animation_set_curve(animation, AnimationCurveLinear);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_modal_push_transition_animation_init_round(Animation *animation) {
|
||||
const uint32_t duration_ms = 8 * ANIMATION_TARGET_FRAME_INTERVAL_MS;
|
||||
animation_set_duration(animation, duration_ms);
|
||||
animation_set_curve(animation, AnimationCurveLinear);
|
||||
}
|
||||
|
||||
static void prv_modal_transition_animation_teardown_rect(Animation *animation) {
|
||||
if (s_data.animation_sequence) {
|
||||
gdraw_command_sequence_destroy(s_data.animation_sequence);
|
||||
s_data.animation_sequence = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
const CompositorTransition *prv_modal_transition_get_rect(bool modal_is_destination) {
|
||||
// NOTE: This initialization will set .expanding to false so we default to contracting to a dot
|
||||
s_data = (CompositorModalTransitionData) {
|
||||
.modal_is_destination = modal_is_destination,
|
||||
// TODO: PBL-19849
|
||||
// Decide on the logistics of the background color of the modal pop animation, including
|
||||
// providing a setter for it that apps can use
|
||||
.outer_color = GColorLightGray,
|
||||
};
|
||||
|
||||
static const CompositorTransition s_impl = {
|
||||
.init = prv_modal_transition_animation_init_rect,
|
||||
.update = prv_modal_transition_animation_update_rect,
|
||||
.teardown = prv_modal_transition_animation_teardown_rect,
|
||||
.skip_modal_render_after_update = true, // This transition renders the modal itself
|
||||
};
|
||||
|
||||
return &s_impl;
|
||||
}
|
||||
|
||||
const CompositorTransition *prv_modal_transition_get_round(bool modal_is_destination) {
|
||||
s_data = (CompositorModalTransitionData) {
|
||||
.modal_is_destination = modal_is_destination,
|
||||
};
|
||||
|
||||
if (!modal_is_destination) {
|
||||
return compositor_round_flip_transition_get(false /* flip_to_the_right */);
|
||||
} else {
|
||||
static const CompositorTransition s_impl = {
|
||||
.init = prv_modal_push_transition_animation_init_round,
|
||||
.update = prv_modal_push_transition_animation_update_round,
|
||||
.skip_modal_render_after_update = true, // This transition renders the modal itself
|
||||
};
|
||||
|
||||
return &s_impl;
|
||||
}
|
||||
}
|
||||
|
||||
const CompositorTransition* compositor_modal_transition_to_modal_get(bool modal_is_destination) {
|
||||
return PBL_IF_RECT_ELSE(prv_modal_transition_get_rect,
|
||||
prv_modal_transition_get_round)(modal_is_destination);
|
||||
}
|
|
@ -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
|
||||
|
||||
#include "services/common/compositor/compositor.h"
|
||||
|
||||
//! @file compositor_modal_transitions.h
|
||||
//! Allows a user to create and configure compositor transition animations for modals.
|
||||
|
||||
//! @param modal_is_destination Whether the animation should animate to the modal or not
|
||||
//! @return \ref CompositorTransition for the requested modal animation
|
||||
const CompositorTransition* compositor_modal_transition_to_modal_get(bool modal_is_destination);
|
|
@ -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 "compositor_peek_transitions.h"
|
||||
|
||||
#include "applib/graphics/bitblt.h"
|
||||
#include "applib/graphics/framebuffer.h"
|
||||
#include "applib/graphics/gtypes.h"
|
||||
#include "applib/ui/animation_interpolate.h"
|
||||
#include "apps/system_apps/timeline/timeline_common.h"
|
||||
#include "popups/timeline/peek.h"
|
||||
#include "popups/timeline/peek_animations.h"
|
||||
#include "services/common/compositor/compositor_private.h"
|
||||
#include "services/common/compositor/compositor_transitions.h"
|
||||
|
||||
#define NUM_FRAMES (3)
|
||||
|
||||
typedef struct CompositorPeekTransitionData {
|
||||
int offset_y;
|
||||
} CompositorPeekTransitionData;
|
||||
|
||||
static CompositorPeekTransitionData s_data;
|
||||
|
||||
static void prv_update_peek_transition_animation(GContext *ctx, Animation *animation,
|
||||
uint32_t distance_normalized) {
|
||||
const AnimationProgress progress = distance_normalized;
|
||||
|
||||
GRect box = TIMELINE_PEEK_FRAME_VISIBLE;
|
||||
const int16_t initial_offset_y = -4;
|
||||
const int16_t final_offset_y = 7;
|
||||
box.origin.y = interpolate_int16(progress, box.origin.y + initial_offset_y, final_offset_y);
|
||||
|
||||
if (progress > (ANIMATION_NORMALIZED_MAX / NUM_FRAMES)) {
|
||||
timeline_peek_draw_background(ctx, &DISP_FRAME, 0);
|
||||
peek_animations_draw_compositor_background_speed_lines(
|
||||
ctx, GPoint(PEEK_ANIMATIONS_SPEED_LINES_OFFSET_X, 0));
|
||||
}
|
||||
const unsigned int num_concurrent = 3;
|
||||
timeline_peek_draw_background(ctx, &box, num_concurrent);
|
||||
|
||||
const unsigned int concurrent_height = timeline_peek_get_concurrent_height(num_concurrent);
|
||||
const int16_t foreground_speed_line_offset_y = 2;
|
||||
const GPoint offset =
|
||||
gpoint_add(box.origin, GPoint(PEEK_ANIMATIONS_SPEED_LINES_OFFSET_X,
|
||||
concurrent_height + foreground_speed_line_offset_y));
|
||||
graphics_context_set_fill_color(ctx, GColorBlack);
|
||||
peek_animations_draw_compositor_foreground_speed_lines(ctx, offset);
|
||||
}
|
||||
|
||||
static void prv_init_peek_transition_animation(Animation *animation) {
|
||||
animation_set_curve(animation, AnimationCurveLinear);
|
||||
animation_set_duration(animation, NUM_FRAMES * ANIMATION_TARGET_FRAME_INTERVAL_MS);
|
||||
}
|
||||
|
||||
const CompositorTransition *compositor_peek_transition_timeline_get(void) {
|
||||
s_data = (CompositorPeekTransitionData) {};
|
||||
static const CompositorTransition s_impl = {
|
||||
.init = prv_init_peek_transition_animation,
|
||||
.update = prv_update_peek_transition_animation,
|
||||
};
|
||||
return &s_impl;
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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/common/compositor/compositor_transitions.h"
|
||||
|
||||
const CompositorTransition *compositor_peek_transition_timeline_get(void);
|
|
@ -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.
|
||||
*/
|
||||
|
||||
#include "compositor_port_hole_transitions.h"
|
||||
|
||||
#include "applib/graphics/bitblt.h"
|
||||
#include "applib/graphics/framebuffer.h"
|
||||
#include "applib/graphics/graphics_private.h"
|
||||
#include "util/trig.h"
|
||||
#include "services/common/compositor/compositor_transitions.h"
|
||||
|
||||
#include "resource/resource_ids.auto.h"
|
||||
#include "system/logging.h"
|
||||
|
||||
typedef struct {
|
||||
CompositorTransitionDirection direction;
|
||||
int16_t animation_offset_px;
|
||||
} CompositorPortHoleTransitionData;
|
||||
|
||||
static CompositorPortHoleTransitionData s_data;
|
||||
|
||||
void compositor_port_hole_transition_draw_outer_ring(GContext *ctx, int16_t thickness,
|
||||
GColor ring_color) {
|
||||
const uint16_t overdraw = 2;
|
||||
graphics_context_set_fill_color(ctx, ring_color);
|
||||
graphics_fill_radial(ctx, grect_inset(DISP_FRAME, GEdgeInsets(-overdraw)),
|
||||
GOvalScaleModeFitCircle, thickness + overdraw, 0, TRIG_MAX_ANGLE);
|
||||
}
|
||||
|
||||
// piecewise interpolator between 0 and to for the first half of ANIMATION_NORMALIZED_MAX
|
||||
// and between -to and 0 for the second half
|
||||
static int16_t prv_interpolate_two_ways_int16(AnimationProgress normalized_progress,
|
||||
int32_t discontinuity_progress, int16_t to) {
|
||||
if (normalized_progress < discontinuity_progress) {
|
||||
return interpolate_int16(animation_timing_scaled(normalized_progress, 0,
|
||||
discontinuity_progress), 0, to);
|
||||
} else {
|
||||
return interpolate_int16(animation_timing_scaled(normalized_progress, discontinuity_progress,
|
||||
ANIMATION_NORMALIZED_MAX), -to, 0);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_port_hole_transition_animation_init(Animation *animation) {
|
||||
animation_set_duration(animation, PORT_HOLE_TRANSITION_DURATION_MS);
|
||||
s_data.animation_offset_px = 0;
|
||||
}
|
||||
|
||||
static void prv_port_hole_transition_animation_update(GContext *ctx,
|
||||
Animation *animation,
|
||||
uint32_t distance_normalized) {
|
||||
const uint32_t transition_progress_threshold = ANIMATION_NORMALIZED_MAX / 2;
|
||||
const int32_t ring_max_thickness = 40;
|
||||
const bool direction_negative = ((s_data.direction == CompositorTransitionDirectionRight) ||
|
||||
(s_data.direction == CompositorTransitionDirectionDown));
|
||||
const bool direction_vertical = ((s_data.direction == CompositorTransitionDirectionDown) ||
|
||||
(s_data.direction == CompositorTransitionDirectionUp));
|
||||
|
||||
const int16_t current_offset_px =
|
||||
prv_interpolate_two_ways_int16(distance_normalized, transition_progress_threshold,
|
||||
direction_negative ? ring_max_thickness :
|
||||
-ring_max_thickness);
|
||||
|
||||
if (distance_normalized > transition_progress_threshold) {
|
||||
// Second half of the transition
|
||||
const GBitmap app_bitmap = compositor_get_app_framebuffer_as_bitmap();
|
||||
GBitmap sys_bitmap = compositor_get_framebuffer_as_bitmap();
|
||||
const GPoint point = direction_vertical ? GPoint(0, -current_offset_px) :
|
||||
GPoint(-current_offset_px, 0);
|
||||
// the framebuffer is already wiped at the beginning, so we can use GColorWhite as a fill color
|
||||
// without filling it ourselves
|
||||
bitblt_bitmap_into_bitmap(&sys_bitmap, &app_bitmap, point, GCompOpAssign, GColorWhite);
|
||||
} else {
|
||||
// First half of the transition
|
||||
const int16_t diff = s_data.animation_offset_px - current_offset_px;
|
||||
if (direction_vertical) {
|
||||
graphics_private_move_pixels_vertically(&ctx->dest_bitmap, diff);
|
||||
} else {
|
||||
graphics_private_move_pixels_horizontally(&ctx->dest_bitmap, diff,
|
||||
false /* patch_garbage */);
|
||||
}
|
||||
}
|
||||
|
||||
compositor_port_hole_transition_draw_outer_ring(ctx, ABS(current_offset_px), GColorBlack);
|
||||
s_data.animation_offset_px = current_offset_px;
|
||||
}
|
||||
|
||||
const CompositorTransition *compositor_port_hole_transition_app_get(
|
||||
CompositorTransitionDirection direction) {
|
||||
if (compositor_transition_app_to_app_should_be_skipped()) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
s_data.direction = direction;
|
||||
|
||||
static const CompositorTransition s_impl = {
|
||||
.init = prv_port_hole_transition_animation_init,
|
||||
.update = prv_port_hole_transition_animation_update,
|
||||
};
|
||||
|
||||
return &s_impl;
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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/common/compositor/compositor.h"
|
||||
|
||||
#define PORT_HOLE_TRANSITION_DURATION_MS (6 * ANIMATION_TARGET_FRAME_INTERVAL_MS)
|
||||
|
||||
const CompositorTransition *compositor_port_hole_transition_app_get(
|
||||
CompositorTransitionDirection direction);
|
||||
|
||||
void compositor_port_hole_transition_draw_outer_ring(GContext *ctx, int16_t pixels,
|
||||
GColor ring_color);
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* 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 "compositor_round_flip_transitions.h"
|
||||
|
||||
#include "services/common/compositor/compositor_transitions.h"
|
||||
|
||||
#include "applib/graphics/graphics_private_raw.h"
|
||||
#include "applib/graphics/framebuffer.h"
|
||||
#include "util/attributes.h"
|
||||
#include "util/trig.h"
|
||||
#include "system/passert.h"
|
||||
|
||||
//! Packed so we can squeeze this into a void* as the animation context
|
||||
typedef struct PACKED {
|
||||
union {
|
||||
struct {
|
||||
//! The direction of the animation of the visual elements
|
||||
CompositorTransitionDirection direction:3;
|
||||
};
|
||||
void *data;
|
||||
};
|
||||
} RoundFlipTransitionAnimationConfiguration;
|
||||
|
||||
_Static_assert(sizeof(RoundFlipTransitionAnimationConfiguration) == sizeof(void *), "");
|
||||
|
||||
void compositor_round_flip_transitions_flip_animation_update(GContext *ctx,
|
||||
uint32_t distance_normalized,
|
||||
CompositorTransitionDirection dir,
|
||||
GColor flip_lid_color) {
|
||||
graphics_context_set_fill_color(ctx, flip_lid_color);
|
||||
|
||||
const int16_t circle_radius = DISP_COLS * 3 / 4;
|
||||
const GPoint display_center = GPoint(DISP_COLS / 2, DISP_ROWS / 2);
|
||||
// The flip overlap region is the intersection of the two large circles (think of a Venn diagram)
|
||||
const uint16_t flip_overlap_region_width = DISP_COLS / 4;
|
||||
|
||||
// Flip halfway through the animation
|
||||
const uint32_t flip_distance = ANIMATION_NORMALIZED_MAX / 2;
|
||||
if (distance_normalized < flip_distance) {
|
||||
const int16_t flip_boundary_from_x = DISP_COLS;
|
||||
const int16_t flip_boundary_to_x = display_center.x - (flip_overlap_region_width / 2);
|
||||
const int16_t current_flip_boundary_x = interpolate_int16(distance_normalized,
|
||||
flip_boundary_from_x,
|
||||
flip_boundary_to_x);
|
||||
|
||||
const GPoint circle_center = GPoint(current_flip_boundary_x - circle_radius + 1,
|
||||
display_center.y);
|
||||
if (dir == CompositorTransitionDirectionLeft) {
|
||||
graphics_fill_radial_internal(ctx, circle_center, circle_radius,
|
||||
DISP_COLS - circle_center.x + 1, 0, TRIG_MAX_ANGLE);
|
||||
} else {
|
||||
graphics_fill_circle(ctx, circle_center, circle_radius);
|
||||
}
|
||||
} else {
|
||||
const int16_t flip_boundary_from_x = display_center.x + (flip_overlap_region_width / 2);
|
||||
const int16_t flip_boundary_to_x = 0;
|
||||
const int16_t current_flip_boundary_x = interpolate_int16(distance_normalized,
|
||||
flip_boundary_from_x,
|
||||
flip_boundary_to_x);
|
||||
const GPoint circle_center = GPoint(current_flip_boundary_x + circle_radius - 1,
|
||||
display_center.y);
|
||||
if (dir == CompositorTransitionDirectionLeft) {
|
||||
graphics_fill_circle(ctx, circle_center, circle_radius);
|
||||
} else {
|
||||
graphics_fill_radial_internal(ctx, circle_center, circle_radius, circle_center.x + 1, 0,
|
||||
TRIG_MAX_ANGLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_round_flip_transition_animation_update(GContext *ctx, Animation *animation,
|
||||
uint32_t distance_normalized) {
|
||||
// Unwrap our animation configuration from the context
|
||||
RoundFlipTransitionAnimationConfiguration config = {
|
||||
.data = animation_get_context(animation)
|
||||
};
|
||||
|
||||
// Save a reference to the existing draw implementation
|
||||
const GDrawRawImplementation *saved_draw_implementation = ctx->draw_state.draw_implementation;
|
||||
|
||||
// Replace the draw implementation with one that fills horizontal lines using the app framebuffer
|
||||
ctx->draw_state.draw_implementation = &g_compositor_transitions_app_fb_draw_implementation;
|
||||
|
||||
// Note that the flip_lid_color here doesn't matter because we've replaced the draw implementation
|
||||
// However, we do have to specify a color that isn't invisible, otherwise nothing will be drawn
|
||||
compositor_round_flip_transitions_flip_animation_update(ctx, distance_normalized,
|
||||
config.direction,
|
||||
GColorBlack /* flip_lid_color */);
|
||||
|
||||
// Restore the saved draw implementation
|
||||
ctx->draw_state.draw_implementation = saved_draw_implementation;
|
||||
}
|
||||
|
||||
//! The transition direction here is the direction of the visual elements, not the motion
|
||||
static void prv_configure_round_flip_transition_animation(Animation *animation,
|
||||
CompositorTransitionDirection direction) {
|
||||
RoundFlipTransitionAnimationConfiguration config = {
|
||||
.direction = direction,
|
||||
};
|
||||
|
||||
animation_set_curve(animation, AnimationCurveLinear);
|
||||
animation_set_duration(animation, ROUND_FLIP_ANIMATION_DURATION_MS);
|
||||
animation_set_handlers(animation, (AnimationHandlers) { 0 }, config.data);
|
||||
// If the visual elements will move to the right, we will just play the left animation backwards
|
||||
const bool should_animate_backwards = (direction == CompositorTransitionDirectionRight);
|
||||
animation_set_reverse(animation, should_animate_backwards);
|
||||
}
|
||||
|
||||
static void prv_round_flip_transition_from_launcher_animation_init(Animation *animation) {
|
||||
prv_configure_round_flip_transition_animation(animation, CompositorTransitionDirectionRight);
|
||||
}
|
||||
|
||||
static void prv_round_flip_transition_to_launcher_animation_init(Animation *animation) {
|
||||
prv_configure_round_flip_transition_animation(animation, CompositorTransitionDirectionLeft);
|
||||
}
|
||||
|
||||
const CompositorTransition *compositor_round_flip_transition_get(bool flip_to_the_right) {
|
||||
if (compositor_transition_app_to_app_should_be_skipped()) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (flip_to_the_right) {
|
||||
static const CompositorTransition s_impl = {
|
||||
.init = prv_round_flip_transition_to_launcher_animation_init,
|
||||
.update = prv_round_flip_transition_animation_update,
|
||||
};
|
||||
return &s_impl;
|
||||
} else {
|
||||
static const CompositorTransition s_impl = {
|
||||
.init = prv_round_flip_transition_from_launcher_animation_init,
|
||||
.update = prv_round_flip_transition_animation_update,
|
||||
};
|
||||
return &s_impl;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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/common/compositor/compositor.h"
|
||||
|
||||
// Animation in design video lasts this many frames
|
||||
#define ROUND_FLIP_ANIMATION_DURATION_MS (6 * ANIMATION_TARGET_FRAME_INTERVAL_MS)
|
||||
|
||||
void compositor_round_flip_transitions_flip_animation_update(GContext *ctx,
|
||||
uint32_t distance_normalized,
|
||||
CompositorTransitionDirection dir,
|
||||
GColor flip_lid_color);
|
||||
|
||||
const CompositorTransition *compositor_round_flip_transition_get(bool flip_to_the_right);
|
|
@ -0,0 +1,322 @@
|
|||
/*
|
||||
* 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 "compositor_shutter_transitions.h"
|
||||
|
||||
#include "services/common/compositor/compositor_transitions.h"
|
||||
|
||||
#include "applib/graphics/bitblt.h"
|
||||
#include "applib/graphics/framebuffer.h"
|
||||
#include "applib/graphics/gpath.h"
|
||||
#include "applib/graphics/graphics.h"
|
||||
#include "applib/graphics/graphics_private.h"
|
||||
#include "system/passert.h"
|
||||
|
||||
typedef struct {
|
||||
CompositorTransitionDirection direction;
|
||||
GColor color;
|
||||
GColor sampled_color;
|
||||
GPath path;
|
||||
bool is_first_half;
|
||||
int16_t animation_offset_px;
|
||||
Animation *first_anim;
|
||||
Animation *second_anim;
|
||||
} CompositorShutterTransitionData;
|
||||
|
||||
static CompositorShutterTransitionData s_data;
|
||||
|
||||
#define PATH_WEDGE_POINTS 3
|
||||
#define PATH_QUAD_POINTS 4
|
||||
|
||||
static GPathInfo s_path_wedge = {
|
||||
.num_points = PATH_WEDGE_POINTS,
|
||||
.points = (GPoint[PATH_WEDGE_POINTS]) {
|
||||
// These are just placeholders to allocate the needed space.
|
||||
// They will be set to the proper values during the animation.
|
||||
{0, 0}, {0, 0}, {0, 0},
|
||||
},
|
||||
};
|
||||
|
||||
static GPathInfo s_path_quad = {
|
||||
.num_points = PATH_QUAD_POINTS,
|
||||
.points = (GPoint[PATH_QUAD_POINTS]) {
|
||||
// These are just placeholders to allocate the needed space.
|
||||
// They will be set to the proper values during the animation.
|
||||
{0, 0}, {0, 0}, {0, 0}, {0, 0},
|
||||
},
|
||||
};
|
||||
|
||||
typedef struct PathInterpDefinition {
|
||||
GPoint start, end;
|
||||
} PathInterpDefinition;
|
||||
|
||||
typedef struct PathDefinition {
|
||||
PathInterpDefinition wedge_verts[PATH_WEDGE_POINTS];
|
||||
PathInterpDefinition quad_verts[PATH_QUAD_POINTS];
|
||||
} PathDefinition;
|
||||
|
||||
#define PATH_INTERP_DEF_TL_CORNER \
|
||||
{ { 0, 0 }, \
|
||||
{ 0, 0 }, }
|
||||
#define PATH_INTERP_DEF_TR_CORNER \
|
||||
{ { DISP_COLS, 0 }, \
|
||||
{ DISP_COLS, 0 }, }
|
||||
#define PATH_INTERP_DEF_BL_CORNER \
|
||||
{ { 0, DISP_ROWS }, \
|
||||
{ 0, DISP_ROWS }, }
|
||||
#define PATH_INTERP_DEF_BR_CORNER \
|
||||
{ { DISP_COLS, DISP_ROWS }, \
|
||||
{ DISP_COLS, DISP_ROWS }, }
|
||||
|
||||
|
||||
// These factors are based on getting pixel coordinates from the videos, then dividing them by
|
||||
// the designed screen size (144x168). By being ratios instead of pixel counts, these can work
|
||||
// out of the box on Robert.
|
||||
static const PathDefinition s_path_defs[4] = {
|
||||
[CompositorTransitionDirectionUp] = {
|
||||
.wedge_verts = {
|
||||
// BL: 0,M -> 0,109 (0.65)
|
||||
{ { 0, DISP_ROWS },
|
||||
{ 0, DISP_ROWS * 0.65 }, },
|
||||
// BM: 72,M (0.5) -> 115,M (0.8)
|
||||
{ { DISP_COLS * 0.5, DISP_ROWS },
|
||||
{ DISP_COLS * 0.8, DISP_ROWS }, },
|
||||
PATH_INTERP_DEF_BL_CORNER,
|
||||
},
|
||||
.quad_verts = {
|
||||
// TR: M,0 -> M,52 (0.31)
|
||||
{ { DISP_COLS, 0 },
|
||||
{ DISP_COLS, DISP_ROWS * 0.31 }, },
|
||||
// TL: 0,0 -> 0,30 (0.18)
|
||||
{ { 0, 0 },
|
||||
{ 0, DISP_ROWS * 0.18 }, },
|
||||
PATH_INTERP_DEF_TL_CORNER,
|
||||
PATH_INTERP_DEF_TR_CORNER,
|
||||
},
|
||||
},
|
||||
// We don't have one for Left or Down because the shutter will not be drawn on those.
|
||||
[CompositorTransitionDirectionRight] = {
|
||||
.wedge_verts = {
|
||||
// TL: 0,0 -> 50,0 (0.35)
|
||||
{ { 0, 0 },
|
||||
{ DISP_COLS * 0.35, 0 }, },
|
||||
// ML: 0,50 (0.3) -> 0,117 (0.7)
|
||||
{ { 0, DISP_ROWS * 0.3 },
|
||||
{ 0, DISP_ROWS * 0.7 }, },
|
||||
PATH_INTERP_DEF_TL_CORNER,
|
||||
},
|
||||
.quad_verts = {
|
||||
// BR: M,M -> 93,M (0.65)
|
||||
{ { DISP_COLS, DISP_ROWS },
|
||||
{ DISP_COLS * 0.65, DISP_ROWS }, },
|
||||
// TR: M,0 -> 119,0 (0.83)
|
||||
{ { DISP_COLS, 0 },
|
||||
{ DISP_COLS * 0.83, 0 }, },
|
||||
PATH_INTERP_DEF_TR_CORNER,
|
||||
PATH_INTERP_DEF_BR_CORNER,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Creates a gpoint from a PathInterpDefinition and animation progress.
|
||||
static void prv_gpoint_interpolate(GPoint *result, int32_t normalized,
|
||||
const PathInterpDefinition *def) {
|
||||
result->x = interpolate_int16(normalized, def->start.x, def->end.x);
|
||||
result->y = interpolate_int16(normalized, def->start.y, def->end.y);
|
||||
}
|
||||
|
||||
// piecewise interpolator between 0 and to for the first half of ANIMATION_NORMALIZED_MAX
|
||||
// and between -to and 0 for the second half
|
||||
static int16_t prv_interpolate_two_ways(AnimationProgress normalized_progress, int16_t end) {
|
||||
const int16_t from = s_data.is_first_half ? 0 : -end;
|
||||
const int16_t to = s_data.is_first_half ? end : 0;
|
||||
return interpolate_int16(normalized_progress, from, to);
|
||||
}
|
||||
|
||||
// Draws the shutters based on the path definitions
|
||||
static void prv_draw_shutter(GContext *ctx, uint32_t distance, bool vertical) {
|
||||
graphics_context_set_antialiased(ctx, true);
|
||||
graphics_context_set_stroke_width(ctx, 1);
|
||||
graphics_context_set_stroke_color(ctx, s_data.color);
|
||||
graphics_context_set_fill_color(ctx, s_data.color);
|
||||
|
||||
for (int i = 0; i < PATH_WEDGE_POINTS; i++) {
|
||||
prv_gpoint_interpolate(&s_path_wedge.points[i], distance,
|
||||
&s_path_defs[s_data.direction].wedge_verts[i]);
|
||||
}
|
||||
gpath_init(&s_data.path, &s_path_wedge);
|
||||
gpath_draw_outline(ctx, &s_data.path);
|
||||
gpath_draw_filled(ctx, &s_data.path);
|
||||
|
||||
for (int i = 0; i < PATH_QUAD_POINTS; i++) {
|
||||
prv_gpoint_interpolate(&s_path_quad.points[i], distance,
|
||||
&s_path_defs[s_data.direction].quad_verts[i]);
|
||||
}
|
||||
gpath_init(&s_data.path, &s_path_quad);
|
||||
gpath_draw_outline(ctx, &s_data.path);
|
||||
gpath_draw_filled(ctx, &s_data.path);
|
||||
}
|
||||
|
||||
// Moves the current framebuffer around
|
||||
static int16_t prv_move_in(GContext *ctx, int move_size, uint32_t distance, bool vertical) {
|
||||
const int16_t current_offset_px = prv_interpolate_two_ways(distance, move_size);
|
||||
const int16_t diff = s_data.animation_offset_px - current_offset_px;
|
||||
if (vertical) {
|
||||
graphics_private_move_pixels_vertically(&ctx->dest_bitmap, -diff);
|
||||
} else {
|
||||
graphics_private_move_pixels_horizontally(&ctx->dest_bitmap, diff, true /* patch_garbage */);
|
||||
}
|
||||
s_data.animation_offset_px = current_offset_px;
|
||||
framebuffer_dirty_all(compositor_get_framebuffer());
|
||||
return current_offset_px;
|
||||
}
|
||||
|
||||
// Draws in the new application's framebuffer and any transparent modal
|
||||
static void prv_draw_in(GContext *ctx, int move_size, uint32_t distance, bool vertical,
|
||||
bool invert) {
|
||||
const int16_t current_offset_px = prv_interpolate_two_ways(distance, invert ? -move_size :
|
||||
move_size);
|
||||
const GBitmap app_bitmap = compositor_get_app_framebuffer_as_bitmap();
|
||||
GBitmap sys_bitmap = compositor_get_framebuffer_as_bitmap();
|
||||
const GPoint point = vertical ? GPoint(0, -current_offset_px) :
|
||||
GPoint(-current_offset_px, 0);
|
||||
|
||||
// Make sure the undrawn areas are the shutter color
|
||||
graphics_context_set_fill_color(ctx, s_data.sampled_color);
|
||||
graphics_fill_rect(ctx, &DISP_FRAME);
|
||||
|
||||
bitblt_bitmap_into_bitmap(&sys_bitmap, &app_bitmap, point, GCompOpAssign, GColorWhite);
|
||||
|
||||
const GPoint drawing_box_origin = ctx->draw_state.drawing_box.origin;
|
||||
gpoint_add_eq(&ctx->draw_state.drawing_box.origin, point);
|
||||
compositor_render_modal();
|
||||
ctx->draw_state.drawing_box.origin = drawing_box_origin;
|
||||
}
|
||||
|
||||
const int32_t s_small_movement_size = DISP_COLS * 0.042; // 6 on snowy
|
||||
const int32_t s_large_movement_size = DISP_COLS * 0.14; // 20 on snowy
|
||||
const int32_t s_upwards_movement_size = DISP_ROWS * 0.18; // 30 on snowy
|
||||
|
||||
static void prv_transition_animation_update(GContext *ctx, Animation *animation,
|
||||
uint32_t progress) {
|
||||
const bool direction_negative = ((s_data.direction == CompositorTransitionDirectionRight) ||
|
||||
(s_data.direction == CompositorTransitionDirectionUp));
|
||||
const bool direction_vertical = ((s_data.direction == CompositorTransitionDirectionDown) ||
|
||||
(s_data.direction == CompositorTransitionDirectionUp));
|
||||
|
||||
const bool draw_shutter = s_data.is_first_half ? direction_negative : !direction_negative;
|
||||
|
||||
static GDrawState prev_state;
|
||||
prev_state = ctx->draw_state;
|
||||
|
||||
if (s_data.is_first_half) {
|
||||
const int32_t movement_size = (s_data.direction == CompositorTransitionDirectionUp) ?
|
||||
s_upwards_movement_size : s_large_movement_size;
|
||||
prv_move_in(ctx, draw_shutter ? movement_size : -movement_size, progress, direction_vertical);
|
||||
} else {
|
||||
prv_draw_in(ctx, s_small_movement_size, progress, direction_vertical,
|
||||
/*invert*/ draw_shutter ? direction_vertical : true);
|
||||
}
|
||||
|
||||
// We don't draw a shutter during the Down or Left transitions
|
||||
if (draw_shutter && direction_negative) {
|
||||
prv_draw_shutter(ctx, progress, direction_vertical);
|
||||
}
|
||||
|
||||
ctx->draw_state = prev_state;
|
||||
}
|
||||
|
||||
static void prv_transition_animation_first_update(Animation *animation,
|
||||
const AnimationProgress progress) {
|
||||
s_data.is_first_half = true;
|
||||
compositor_transition_render(prv_transition_animation_update, animation, progress);
|
||||
}
|
||||
|
||||
static void prv_transition_animation_second_update(Animation *animation,
|
||||
const AnimationProgress progress) {
|
||||
if (s_data.is_first_half) {
|
||||
// This needs to be sampled here instead of in init because apparently the app framebuffer
|
||||
// hasn't been drawn at all during init.
|
||||
const GBitmap app_bitmap = compositor_get_app_framebuffer_as_bitmap();
|
||||
s_data.sampled_color = graphics_private_sample_line_color(&app_bitmap,
|
||||
(GColorSampleEdge)s_data.direction,
|
||||
GColorBlack);
|
||||
// Force the sampled color to be completely opaque, because we're using this to fill the
|
||||
// framebuffer background when moving the new app into focus.
|
||||
s_data.sampled_color.a = 3;
|
||||
}
|
||||
s_data.is_first_half = false;
|
||||
compositor_transition_render(prv_transition_animation_update, animation, progress);
|
||||
}
|
||||
|
||||
static void prv_transition_animation_init(Animation *animation) {
|
||||
Animation *anim_array[2] = {NULL};
|
||||
size_t anim_count = 0;
|
||||
uint32_t duration = 0;
|
||||
|
||||
s_data.first_anim = animation_create();
|
||||
if (s_data.first_anim) {
|
||||
anim_array[anim_count++] = s_data.first_anim;
|
||||
static const AnimationImplementation s_first_animation_impl = {
|
||||
.update = prv_transition_animation_first_update,
|
||||
};
|
||||
animation_set_implementation(s_data.first_anim, &s_first_animation_impl);
|
||||
animation_set_duration(s_data.first_anim, SHUTTER_TRANSITION_FIRST_DURATION_MS);
|
||||
duration += SHUTTER_TRANSITION_FIRST_DURATION_MS;
|
||||
animation_set_curve(s_data.first_anim, AnimationCurveEaseIn);
|
||||
}
|
||||
|
||||
s_data.second_anim = animation_create();
|
||||
if (s_data.second_anim) {
|
||||
anim_array[anim_count++] = s_data.second_anim;
|
||||
static const AnimationImplementation s_second_animation_impl = {
|
||||
.update = prv_transition_animation_second_update,
|
||||
};
|
||||
animation_set_implementation(s_data.second_anim, &s_second_animation_impl);
|
||||
animation_set_duration(s_data.second_anim, SHUTTER_TRANSITION_SECOND_DURATION_MS);
|
||||
duration += SHUTTER_TRANSITION_SECOND_DURATION_MS;
|
||||
animation_set_curve(s_data.second_anim, AnimationCurveEaseOut);
|
||||
}
|
||||
|
||||
PBL_ASSERTN(animation_sequence_init_from_array(animation, anim_array, anim_count));
|
||||
animation_set_duration(animation, duration);
|
||||
animation_set_curve(animation, AnimationCurveLinear);
|
||||
|
||||
s_data.animation_offset_px = 0;
|
||||
}
|
||||
|
||||
static void prv_transition_animation_update_stub(GContext *ctx,
|
||||
Animation *animation,
|
||||
uint32_t progress) {
|
||||
}
|
||||
|
||||
const CompositorTransition *compositor_shutter_transition_get(
|
||||
CompositorTransitionDirection direction, GColor color) {
|
||||
if (compositor_transition_app_to_app_should_be_skipped()) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
s_data = (CompositorShutterTransitionData){};
|
||||
s_data.color = color;
|
||||
s_data.direction = direction;
|
||||
|
||||
static const CompositorTransition s_impl = {
|
||||
.init = prv_transition_animation_init,
|
||||
.update = prv_transition_animation_update_stub,
|
||||
.skip_modal_render_after_update = true,
|
||||
};
|
||||
return &s_impl;
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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/common/compositor/compositor.h"
|
||||
|
||||
// The length of first "section" of the animation, where the old app is moved off of the screen.
|
||||
#define SHUTTER_TRANSITION_FIRST_DURATION_MS (2 * ANIMATION_TARGET_FRAME_INTERVAL_MS)
|
||||
// The length of second "section" of the animation, where the new app is moved in.
|
||||
#define SHUTTER_TRANSITION_SECOND_DURATION_MS (4 * ANIMATION_TARGET_FRAME_INTERVAL_MS)
|
||||
// Total length of the animation.
|
||||
#define SHUTTER_TRANSITION_DURATION_MS (SHUTTER_TRANSITION_FIRST_DURATION_MS + \
|
||||
SHUTTER_TRANSITION_SECOND_DURATION_MS)
|
||||
|
||||
const CompositorTransition *compositor_shutter_transition_get(
|
||||
CompositorTransitionDirection direction, GColor color);
|
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
* 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 "compositor_slide_transitions.h"
|
||||
|
||||
#include "services/common/compositor/compositor_private.h"
|
||||
#include "services/common/compositor/compositor_transitions.h"
|
||||
|
||||
#include "apps/system_apps/timeline/timeline_common.h"
|
||||
#include "applib/graphics/bitblt.h"
|
||||
#include "applib/graphics/framebuffer.h"
|
||||
#include "applib/graphics/gtypes.h"
|
||||
#include "applib/ui/animation_interpolate.h"
|
||||
#include "popups/timeline/peek.h"
|
||||
|
||||
// TODO: PBL-31388 Factor out vertical compositor slide animations
|
||||
// This does a similar transition to the legacy modal slide transition
|
||||
// With a few tweaks, this compositor animation can drive both
|
||||
|
||||
typedef struct {
|
||||
int16_t offset_y;
|
||||
bool slide_up;
|
||||
bool timeline_is_destination;
|
||||
bool timeline_is_empty;
|
||||
GColor fill_color;
|
||||
} CompositorSlideTransitionData;
|
||||
|
||||
static CompositorSlideTransitionData s_data;
|
||||
|
||||
static void prv_copy_framebuffer_rows(GBitmap *dest_bitmap, GBitmap *src_bitmap, int16_t start_row,
|
||||
int16_t end_row, int16_t dupe_row, int16_t shift_amount) {
|
||||
const int16_t delta = start_row > end_row ? -1 : 1;
|
||||
for (int16_t dest_row = start_row; dest_row != end_row; dest_row += delta) {
|
||||
src_bitmap->bounds = (GRect) {
|
||||
.origin.y = (dupe_row >= 0 ? dupe_row : dest_row) - shift_amount,
|
||||
.size = { DISP_COLS, 1 },
|
||||
};
|
||||
bitblt_bitmap_into_bitmap(dest_bitmap, src_bitmap, GPoint(0, dest_row), GCompOpAssign,
|
||||
GColorWhite);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_shift_framebuffer_rows(GBitmap *dest_bitmap, int16_t start_row, int16_t end_row,
|
||||
int16_t shift_amount) {
|
||||
GBitmap src_bitmap = *dest_bitmap;
|
||||
prv_copy_framebuffer_rows(dest_bitmap, &src_bitmap, start_row, end_row, -1, shift_amount);
|
||||
}
|
||||
|
||||
static void prv_duplicate_framebuffer_row(GBitmap *dest_bitmap, int16_t start_row, int16_t end_row,
|
||||
GBitmap *src_bitmap, int16_t dupe_row) {
|
||||
prv_copy_framebuffer_rows(dest_bitmap, src_bitmap, start_row, end_row, dupe_row, 0);
|
||||
}
|
||||
|
||||
static void prv_slide_transition_animation_update(GContext *ctx, Animation *animation,
|
||||
uint32_t distance_normalized) {
|
||||
const AnimationProgress progress = distance_normalized;
|
||||
const int last_offset_y = s_data.offset_y;
|
||||
const int delta_rows = s_data.slide_up ? -DISP_ROWS : DISP_ROWS;
|
||||
s_data.offset_y = interpolate_int16(progress, 0, delta_rows);
|
||||
// Advance one line to give movement from the very start only for the full curve
|
||||
const int advance = s_data.timeline_is_destination ? 0 : 1;
|
||||
|
||||
// Whether shifting should occur. offset_y starts at 0, and |delta_rows| is DISP_ROWS
|
||||
const bool should_shift =
|
||||
s_data.slide_up ? (s_data.offset_y > delta_rows) : (s_data.offset_y < delta_rows);
|
||||
// Horizontal framebuffer lines are shifted vertically, these indicate the start and end of the
|
||||
// entire region to be shifted
|
||||
int shift_start_row;
|
||||
int shift_end_row;
|
||||
// Lines that shift with no replacement framebuffer line are filled if we are entering timeline
|
||||
int fill_offset_y;
|
||||
int fill_height;
|
||||
// Otherwise they are overdrawn with lines from the app framebuffer
|
||||
int app_offset_y;
|
||||
// When the app framebuffer overshoots, the line nearest the overshoot needs to be duplicated
|
||||
bool app_should_dupe;
|
||||
int app_dupe_row;
|
||||
|
||||
if (s_data.slide_up) {
|
||||
s_data.offset_y -= advance;
|
||||
shift_start_row = 0;
|
||||
shift_end_row = DISP_ROWS + s_data.offset_y;
|
||||
fill_offset_y = shift_end_row;
|
||||
fill_height = DISP_ROWS - shift_end_row;
|
||||
app_offset_y = shift_end_row;
|
||||
app_should_dupe = (app_offset_y < 0);
|
||||
app_dupe_row = DISP_ROWS - 1;
|
||||
} else {
|
||||
s_data.offset_y += advance;
|
||||
shift_start_row = DISP_ROWS;
|
||||
shift_end_row = s_data.offset_y;
|
||||
fill_offset_y = 0;
|
||||
fill_height = shift_end_row;
|
||||
app_offset_y = shift_end_row - DISP_ROWS + 1;
|
||||
app_should_dupe = (app_offset_y > 0);
|
||||
app_dupe_row = 0;
|
||||
}
|
||||
|
||||
GBitmap dest_bitmap = compositor_get_framebuffer_as_bitmap();
|
||||
if (should_shift) {
|
||||
const int shift_amount = s_data.offset_y - last_offset_y;
|
||||
prv_shift_framebuffer_rows(&dest_bitmap, shift_start_row, shift_end_row, shift_amount);
|
||||
}
|
||||
if (s_data.timeline_is_destination) {
|
||||
#if CAPABILITY_HAS_TIMELINE_PEEK
|
||||
if (!s_data.timeline_is_empty) {
|
||||
graphics_context_set_fill_color(ctx, GColorWhite);
|
||||
const int content_width = DISP_COLS - TIMELINE_PEEK_ICON_BOX_WIDTH;
|
||||
graphics_fill_rect(ctx, &GRect(0, fill_offset_y, content_width, fill_height));
|
||||
graphics_context_set_fill_color(ctx, s_data.fill_color);
|
||||
graphics_fill_rect(ctx, &GRect(content_width, fill_offset_y, TIMELINE_PEEK_ICON_BOX_WIDTH,
|
||||
fill_height));
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
graphics_context_set_fill_color(ctx, s_data.fill_color);
|
||||
graphics_fill_rect(ctx, &GRect(0, fill_offset_y, DISP_COLS, fill_height));
|
||||
}
|
||||
} else {
|
||||
GBitmap app_bitmap = compositor_get_app_framebuffer_as_bitmap();
|
||||
bitblt_bitmap_into_bitmap(&dest_bitmap, &app_bitmap, GPoint(0, app_offset_y),
|
||||
GCompOpAssign, GColorWhite);
|
||||
if (app_should_dupe) {
|
||||
prv_duplicate_framebuffer_row(&dest_bitmap, app_dupe_row, app_offset_y + app_dupe_row,
|
||||
&app_bitmap, app_dupe_row);
|
||||
}
|
||||
}
|
||||
framebuffer_dirty_all(compositor_get_framebuffer());
|
||||
|
||||
// Update modal position for transparent modals
|
||||
compositor_set_modal_transition_offset(GPoint(0, app_offset_y));
|
||||
}
|
||||
|
||||
static void prv_slide_transition_animation_init(Animation *animation) {
|
||||
// Give a regular moook more time to stretch the anticipation
|
||||
const uint32_t duration = s_data.timeline_is_destination ? interpolate_moook_in_duration() :
|
||||
interpolate_moook_duration();
|
||||
const InterpolateInt64Function interpolation =
|
||||
s_data.timeline_is_destination ? interpolate_moook_in_only : interpolate_moook;
|
||||
animation_set_duration(animation, duration);
|
||||
animation_set_custom_interpolation(animation, interpolation);
|
||||
}
|
||||
|
||||
const CompositorTransition *prv_slide_transition_get(void) {
|
||||
static const CompositorTransition s_impl = {
|
||||
.init = prv_slide_transition_animation_init,
|
||||
.update = prv_slide_transition_animation_update,
|
||||
};
|
||||
return &s_impl;
|
||||
}
|
||||
|
||||
const CompositorTransition *compositor_slide_transition_timeline_get(
|
||||
bool timeline_is_future, bool timeline_is_destination, bool timeline_is_empty) {
|
||||
s_data = (CompositorSlideTransitionData) {
|
||||
.slide_up = timeline_is_future ^ !timeline_is_destination,
|
||||
.fill_color = timeline_is_future ? TIMELINE_FUTURE_COLOR : TIMELINE_PAST_COLOR,
|
||||
.timeline_is_destination = timeline_is_destination,
|
||||
.timeline_is_empty = timeline_is_empty,
|
||||
};
|
||||
return prv_slide_transition_get();
|
||||
}
|
|
@ -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
|
||||
|
||||
#include "services/common/compositor/compositor.h"
|
||||
|
||||
const CompositorTransition *compositor_slide_transition_timeline_get(bool timeline_is_future,
|
||||
bool timeline_is_destination,
|
||||
bool timeline_is_empty);
|
|
@ -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.
|
||||
*/
|
||||
|
||||
#include "compositor_app_slide_transitions.h"
|
||||
|
||||
#include "services/common/compositor/compositor_transitions.h"
|
||||
|
||||
#include "applib/graphics/bitblt.h"
|
||||
#include "applib/graphics/framebuffer.h"
|
||||
#include "applib/graphics/graphics_private_raw.h"
|
||||
#include "applib/graphics/graphics.h"
|
||||
#include "util/trig.h"
|
||||
#include "system/passert.h"
|
||||
|
||||
//! Packed so we can squeeze this into a void* as the animation context
|
||||
typedef struct {
|
||||
union {
|
||||
//! The direction of the animation of the visual elements
|
||||
CompositorTransitionDirection direction;
|
||||
void *data;
|
||||
};
|
||||
} AppSlideTransitionAnimationConfiguration;
|
||||
|
||||
_Static_assert(sizeof(AppSlideTransitionAnimationConfiguration) == sizeof(void *), "");
|
||||
|
||||
void compositor_app_slide_transition_animation_update(GContext *ctx,
|
||||
uint32_t distance_normalized,
|
||||
CompositorTransitionDirection dir) {
|
||||
const bool is_right = (dir == CompositorTransitionDirectionRight);
|
||||
const int16_t from = (int16_t)((is_right) ? -DISP_COLS : DISP_COLS);
|
||||
const int16_t to = 0;
|
||||
const int16_t app_fb_origin_x = interpolate_int16(distance_normalized, from, to);
|
||||
|
||||
// When the window is past its destination (due to the moook), fill in the remaining pixels
|
||||
// with black
|
||||
if ((is_right && (app_fb_origin_x > to)) || (!is_right && (app_fb_origin_x < to))) {
|
||||
graphics_context_set_fill_color(ctx, GColorBlack);
|
||||
graphics_fill_rect(ctx, &DISP_FRAME);
|
||||
}
|
||||
|
||||
const GPoint dest_bitmap_blit_offset = GPoint(app_fb_origin_x, 0);
|
||||
|
||||
GBitmap src_bitmap = compositor_get_app_framebuffer_as_bitmap();
|
||||
GBitmap dest_bitmap = compositor_get_framebuffer_as_bitmap();
|
||||
bitblt_bitmap_into_bitmap(&dest_bitmap, &src_bitmap, dest_bitmap_blit_offset, GCompOpAssign,
|
||||
GColorWhite);
|
||||
framebuffer_dirty_all(compositor_get_framebuffer());
|
||||
|
||||
// Update modal position for transparent modals
|
||||
compositor_set_modal_transition_offset(dest_bitmap_blit_offset);
|
||||
}
|
||||
|
||||
static void prv_transition_animation_update(GContext *ctx,
|
||||
Animation *animation,
|
||||
uint32_t distance_normalized) {
|
||||
// Unwrap our animation configuration from the context
|
||||
AppSlideTransitionAnimationConfiguration config = {
|
||||
.data = animation_get_context(animation)
|
||||
};
|
||||
|
||||
compositor_app_slide_transition_animation_update(ctx,
|
||||
distance_normalized,
|
||||
config.direction);
|
||||
}
|
||||
|
||||
//! The transition direction here is the direction of the visual elements, not the motion
|
||||
static void prv_configure_transition_animation(Animation *animation,
|
||||
CompositorTransitionDirection direction) {
|
||||
AppSlideTransitionAnimationConfiguration config = {
|
||||
.direction = direction,
|
||||
};
|
||||
|
||||
animation_set_handlers(animation, (AnimationHandlers) { 0 }, config.data);
|
||||
animation_set_custom_interpolation(animation, interpolate_moook);
|
||||
animation_set_duration(animation, interpolate_moook_duration());
|
||||
}
|
||||
|
||||
static void prv_transition_from_launcher_animation_init(Animation *animation) {
|
||||
prv_configure_transition_animation(animation, CompositorTransitionDirectionRight);
|
||||
}
|
||||
|
||||
static void prv_transition_to_launcher_animation_init(Animation *animation) {
|
||||
prv_configure_transition_animation(animation, CompositorTransitionDirectionLeft);
|
||||
}
|
||||
|
||||
const CompositorTransition *compositor_app_slide_transition_get(bool flip_to_the_right) {
|
||||
if (compositor_transition_app_to_app_should_be_skipped()) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (flip_to_the_right) {
|
||||
static const CompositorTransition s_impl = {
|
||||
.init = prv_transition_to_launcher_animation_init,
|
||||
.update = prv_transition_animation_update,
|
||||
};
|
||||
return &s_impl;
|
||||
} else {
|
||||
static const CompositorTransition s_impl = {
|
||||
.init = prv_transition_from_launcher_animation_init,
|
||||
.update = prv_transition_animation_update,
|
||||
};
|
||||
return &s_impl;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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/common/compositor/compositor.h"
|
||||
|
||||
void compositor_app_slide_transitions_animation_update(GContext *ctx,
|
||||
uint32_t distance_normalized,
|
||||
CompositorTransitionDirection dir);
|
||||
|
||||
const CompositorTransition *compositor_app_slide_transition_get(bool flip_to_the_right);
|
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
* 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 "compositor_modal_slide_transitions.h"
|
||||
|
||||
#include "services/common/compositor/compositor_private.h"
|
||||
|
||||
#include "applib/graphics/bitblt.h"
|
||||
#include "applib/graphics/framebuffer.h"
|
||||
#include "applib/graphics/graphics.h"
|
||||
#include "applib/ui/animation_interpolate.h"
|
||||
#include "kernel/ui/kernel_ui.h"
|
||||
#include "util/math.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
// the y offset of the modal currently within the display
|
||||
int32_t cur_modal_offset_y;
|
||||
bool modal_is_destination;
|
||||
} CompositorModalSlideTransitionData;
|
||||
|
||||
static CompositorModalSlideTransitionData s_data;
|
||||
|
||||
static const int32_t DISP_ROWS_LAST_INDEX = DISP_ROWS - 1;
|
||||
|
||||
static void prv_modal_transition_push_update(GContext *ctx, uint32_t distance_normalized) {
|
||||
const int16_t new_modal_offset_y = interpolate_int16(distance_normalized,
|
||||
DISP_ROWS_LAST_INDEX,
|
||||
0);
|
||||
|
||||
// The modal overshoots its destination by a few pixels. When this happens, fill in the pixels
|
||||
// at the bottom of the screen with black.
|
||||
if (new_modal_offset_y < 0) {
|
||||
graphics_fill_rect(
|
||||
ctx, &GRect(0, DISP_ROWS + new_modal_offset_y, DISP_COLS, -new_modal_offset_y));
|
||||
}
|
||||
|
||||
gpoint_add_eq(&ctx->draw_state.drawing_box.origin, GPoint(0, new_modal_offset_y));
|
||||
compositor_render_modal();
|
||||
}
|
||||
|
||||
static void prv_modal_transition_pop_update(GContext *ctx, uint32_t distance_normalized) {
|
||||
FrameBuffer *sys_frame_buffer = compositor_get_framebuffer();
|
||||
|
||||
// This is the offset where the modal is to be drawn after the operations below.
|
||||
// NOTE: It has to be clipped since our moook interpolate function goes past the destination
|
||||
// and would cause us to write into an invalid memory address in the framebuffer.
|
||||
const int32_t new_modal_offset_y = MIN(DISP_ROWS_LAST_INDEX,
|
||||
interpolate_int16(distance_normalized,
|
||||
0,
|
||||
DISP_ROWS_LAST_INDEX));
|
||||
// This is the delta between the new offset and the previous offset.
|
||||
const int32_t modal_offset_delta_y = new_modal_offset_y - s_data.cur_modal_offset_y;
|
||||
|
||||
if (modal_offset_delta_y == 0) {
|
||||
// if we aren't going to move the modal, just bail
|
||||
return;
|
||||
}
|
||||
|
||||
// Start from the bottom of the display (last row index) and copy rows from above to the
|
||||
// current line. If we did this the other way, we would lose data from the framebuffer.
|
||||
// This produces a sliding down effect.
|
||||
const uint32_t start_row = DISP_ROWS_LAST_INDEX;
|
||||
const uint32_t end_row = new_modal_offset_y;
|
||||
for (unsigned int dest_row = start_row; dest_row >= end_row; dest_row--) {
|
||||
int32_t fetch_row = dest_row - modal_offset_delta_y;
|
||||
if (fetch_row > DISP_ROWS_LAST_INDEX) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// copy a row from above and paste it into the destination.
|
||||
uint32_t *src_line = framebuffer_get_line(sys_frame_buffer, fetch_row);
|
||||
uint32_t *dest_line = framebuffer_get_line(sys_frame_buffer, dest_row);
|
||||
memcpy(dest_line, src_line, FRAMEBUFFER_BYTES_PER_ROW);
|
||||
}
|
||||
|
||||
// update the current offset of the modal after all lines have been copied.
|
||||
s_data.cur_modal_offset_y = new_modal_offset_y;
|
||||
|
||||
// As we move the modal down, we need to show the app that is underneath it.
|
||||
// We do this by copying the rows from the app's framebuffer into the system's.
|
||||
uint32_t *app_buffer = compositor_get_app_framebuffer_as_bitmap().addr;
|
||||
uint32_t *sys_buffer = sys_frame_buffer->buffer;
|
||||
memcpy(sys_buffer, app_buffer, FRAMEBUFFER_BYTES_PER_ROW * s_data.cur_modal_offset_y);
|
||||
|
||||
// Render transparent modals over only the revealed app portion
|
||||
ctx->draw_state.clip_box.size.h = s_data.cur_modal_offset_y;
|
||||
compositor_render_modal();
|
||||
|
||||
framebuffer_dirty_all(sys_frame_buffer);
|
||||
}
|
||||
|
||||
static void prv_transition_animation_update(GContext *ctx, Animation *animation,
|
||||
uint32_t distance_normalized) {
|
||||
if (s_data.modal_is_destination) {
|
||||
prv_modal_transition_push_update(ctx, distance_normalized);
|
||||
} else {
|
||||
prv_modal_transition_pop_update(ctx, distance_normalized);
|
||||
}
|
||||
}
|
||||
|
||||
#define NUM_MOOOK_FRAMES_MID 1
|
||||
|
||||
static int64_t prv_interpolate_moook_soft(int32_t normalized, int64_t from, int64_t to) {
|
||||
return interpolate_moook_soft(normalized, from, to, NUM_MOOOK_FRAMES_MID);
|
||||
}
|
||||
|
||||
static void prv_transition_animation_init(Animation *animation) {
|
||||
// Tweaked from observations by the design team
|
||||
animation_set_custom_interpolation(animation, prv_interpolate_moook_soft);
|
||||
animation_set_duration(animation, interpolate_moook_soft_duration(NUM_MOOOK_FRAMES_MID));
|
||||
}
|
||||
|
||||
const CompositorTransition* compositor_modal_transition_to_modal_get(bool modal_is_destination) {
|
||||
// Performs different operations on whether the modal is being pushed or popped.
|
||||
s_data = (CompositorModalSlideTransitionData) {
|
||||
.modal_is_destination = modal_is_destination,
|
||||
};
|
||||
|
||||
static const CompositorTransition s_impl = {
|
||||
.init = prv_transition_animation_init,
|
||||
.update = prv_transition_animation_update,
|
||||
.skip_modal_render_after_update = true, // This transition renders the modal itself
|
||||
};
|
||||
|
||||
return &s_impl;
|
||||
}
|
|
@ -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
|
||||
|
||||
#include "services/common/compositor/compositor.h"
|
||||
|
||||
//! @file compositor_modal_transitions.h
|
||||
//! Allows a user to create and configure compositor transition animations for modals.
|
||||
|
||||
//! @param modal_is_destination Whether the animation should animate to the modal or not
|
||||
//! @return \ref CompositorTransition for the requested modal animation
|
||||
const CompositorTransition* compositor_modal_transition_to_modal_get(bool modal_is_destination);
|
243
src/fw/services/common/compositor/screenshot_pp.c
Normal file
243
src/fw/services/common/compositor/screenshot_pp.c
Normal file
|
@ -0,0 +1,243 @@
|
|||
/*
|
||||
* 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 "screenshot_pp.h"
|
||||
|
||||
#include "compositor.h"
|
||||
|
||||
#include "applib/graphics/framebuffer.h"
|
||||
#include "kernel/event_loop.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "services/common/comm_session/session_send_buffer.h"
|
||||
#include "services/common/system_task.h"
|
||||
#include "system/logging.h"
|
||||
#include "util/attributes.h"
|
||||
#include "util/net.h"
|
||||
|
||||
static const uint16_t SCREENSHOT_ENDPOINT_ID = 8000;
|
||||
static bool s_screenshot_in_progress = false;
|
||||
|
||||
typedef enum {
|
||||
SCREENSHOT_OK = 0,
|
||||
SCREENSHOT_MALFORMED_COMMAND = 1,
|
||||
SCREENSHOT_OOM_ERROR = 2,
|
||||
SCREENSHOT_ALREADY_IN_PROGRESS = 3,
|
||||
} ScreenshotResponse;
|
||||
|
||||
typedef struct FrameBufferState {
|
||||
FrameBuffer *fb;
|
||||
uint32_t row;
|
||||
uint32_t col;
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
} FrameBufferState;
|
||||
|
||||
typedef struct ScreenshotState {
|
||||
CommSession *session;
|
||||
FrameBufferState framebuffer;
|
||||
bool sent_header;
|
||||
} ScreenshotState;
|
||||
static ScreenshotState s_screenshot_state;
|
||||
|
||||
typedef struct PACKED {
|
||||
uint8_t response_code;
|
||||
uint32_t version;
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
} ScreenshotHeader;
|
||||
|
||||
typedef struct ScreenshotErrorResponseData {
|
||||
CommSession *session;
|
||||
ScreenshotHeader header;
|
||||
} ScreenshotErrorResponseData;
|
||||
|
||||
static void prv_send_error_response(CommSession *session, uint8_t response) {
|
||||
ScreenshotErrorResponseData error_response = (ScreenshotErrorResponseData) {
|
||||
.session = session,
|
||||
.header = {
|
||||
.response_code = response,
|
||||
.version = htonl(1),
|
||||
.width = htonl(0),
|
||||
.height = htonl(0),
|
||||
},
|
||||
};
|
||||
|
||||
comm_session_send_data(
|
||||
error_response.session, SCREENSHOT_ENDPOINT_ID,
|
||||
(const uint8_t *) &error_response.header, sizeof(error_response.header),
|
||||
COMM_SESSION_DEFAULT_TIMEOUT);
|
||||
}
|
||||
|
||||
static void prv_finish(ScreenshotState *state) {
|
||||
comm_session_set_responsiveness(state->session, BtConsumerPpScreenshot, ResponseTimeMax, 0);
|
||||
compositor_unfreeze();
|
||||
s_screenshot_in_progress = false;
|
||||
}
|
||||
|
||||
static void prv_request_fast_connection(CommSession *session) {
|
||||
comm_session_set_responsiveness(session, BtConsumerPpScreenshot, ResponseTimeMin,
|
||||
MIN_LATENCY_MODE_TIMEOUT_SCREENSHOT_SECS);
|
||||
}
|
||||
|
||||
static uint32_t prv_framebuffer_next_chunk(FrameBufferState *restrict state,
|
||||
uint32_t max_chunk_bytes, uint8_t *output_buffer) {
|
||||
const uint32_t bytes_per_row = SCREEN_COLOR_DEPTH_BITS * DISP_COLS / 8;
|
||||
const uint32_t cols_per_byte = DISP_COLS / bytes_per_row;
|
||||
|
||||
uint32_t remaining_chunk_bytes = max_chunk_bytes;
|
||||
uint8_t *output_buffer_with_offset = output_buffer;
|
||||
|
||||
while (remaining_chunk_bytes > 0 && state->row < state->height) {
|
||||
const uint8_t *restrict framebuffer_row_data = (uint8_t *)framebuffer_get_line(state->fb,
|
||||
state->row);
|
||||
const uint16_t framebuffer_current_column = state->col / cols_per_byte;
|
||||
uint16_t remaining_framebuffer_row_bytes = bytes_per_row - framebuffer_current_column;
|
||||
const bool framebuffer_row_is_larger_than_chunk =
|
||||
(remaining_framebuffer_row_bytes > remaining_chunk_bytes);
|
||||
if (framebuffer_row_is_larger_than_chunk) {
|
||||
remaining_framebuffer_row_bytes = remaining_chunk_bytes;
|
||||
}
|
||||
|
||||
#ifdef PLATFORM_SPALDING
|
||||
const GBitmapDataRowInfoInternal *row_infos = g_gbitmap_spalding_data_row_infos;
|
||||
const size_t framebuffer_row_min_pixel = row_infos[state->row].min_x;
|
||||
const size_t framebuffer_row_max_pixel = row_infos[state->row].max_x;
|
||||
for (uint32_t i = 0; i < remaining_framebuffer_row_bytes; i++) {
|
||||
const uint32_t i_with_offset = i + framebuffer_current_column;
|
||||
if (WITHIN(i_with_offset, framebuffer_row_min_pixel, framebuffer_row_max_pixel)) {
|
||||
output_buffer_with_offset[i] = framebuffer_row_data[i_with_offset];
|
||||
} else {
|
||||
output_buffer_with_offset[i] = GColorClear.argb;
|
||||
}
|
||||
}
|
||||
#else
|
||||
memcpy(output_buffer_with_offset, framebuffer_row_data + framebuffer_current_column,
|
||||
remaining_framebuffer_row_bytes);
|
||||
#endif
|
||||
if (framebuffer_row_is_larger_than_chunk) {
|
||||
state->col = remaining_chunk_bytes * cols_per_byte;
|
||||
} else {
|
||||
state->col = 0;
|
||||
state->row++;
|
||||
}
|
||||
|
||||
remaining_chunk_bytes -= remaining_framebuffer_row_bytes;
|
||||
output_buffer_with_offset += remaining_framebuffer_row_bytes;
|
||||
}
|
||||
|
||||
return max_chunk_bytes - remaining_chunk_bytes;
|
||||
}
|
||||
|
||||
void screenshot_send_next_chunk(void* raw_state) {
|
||||
ScreenshotState* state = (ScreenshotState*)raw_state;
|
||||
|
||||
CommSession *session = state->session;
|
||||
uint32_t max_buf_len = comm_session_send_buffer_get_max_payload_length(session);
|
||||
uint32_t session_len = 0;
|
||||
if (!state->sent_header) {
|
||||
max_buf_len -= sizeof(ScreenshotHeader);
|
||||
session_len += sizeof(ScreenshotHeader);
|
||||
}
|
||||
|
||||
void *buffer = kernel_zalloc(max_buf_len);
|
||||
if (!buffer) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Screenshot aborted, OOM.");
|
||||
prv_send_error_response(session, SCREENSHOT_OOM_ERROR);
|
||||
prv_finish(state);
|
||||
}
|
||||
uint32_t len = prv_framebuffer_next_chunk(&state->framebuffer, max_buf_len, buffer);
|
||||
session_len += len;
|
||||
|
||||
if (len == 0) {
|
||||
kernel_free(buffer);
|
||||
prv_finish(state);
|
||||
return;
|
||||
}
|
||||
|
||||
SendBuffer *sb;
|
||||
if (max_buf_len == 0 /* disconnected */ ||
|
||||
!(sb = comm_session_send_buffer_begin_write(session, SCREENSHOT_ENDPOINT_ID,
|
||||
session_len, COMM_SESSION_DEFAULT_TIMEOUT))) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Terminating screenshot send early: %"PRIu32, max_buf_len);
|
||||
prv_finish(state);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state->sent_header) {
|
||||
comm_session_send_buffer_write(sb, buffer, len);
|
||||
} else {
|
||||
const ScreenshotHeader header = (const ScreenshotHeader) {
|
||||
.response_code = SCREENSHOT_OK,
|
||||
#if SCREEN_COLOR_DEPTH_BITS == 1
|
||||
.version = htonl(1),
|
||||
#elif SCREEN_COLOR_DEPTH_BITS == 8
|
||||
.version = htonl(2),
|
||||
#else
|
||||
#warning "Need SCREEN_COLOR_DEPTH_BITS for screenshot version."
|
||||
#endif
|
||||
.width = htonl(state->framebuffer.width),
|
||||
.height = htonl(state->framebuffer.height),
|
||||
};
|
||||
comm_session_send_buffer_write(sb, (const uint8_t *) &header, sizeof(header));
|
||||
// Fill the rest of this packet with image data.
|
||||
comm_session_send_buffer_write(sb, buffer, len);
|
||||
|
||||
state->sent_header = true;
|
||||
}
|
||||
comm_session_send_buffer_end_write(sb);
|
||||
|
||||
kernel_free(buffer);
|
||||
|
||||
prv_request_fast_connection(session);
|
||||
|
||||
system_task_add_callback(screenshot_send_next_chunk, state);
|
||||
}
|
||||
|
||||
void screenshot_protocol_msg_callback(CommSession *session, const uint8_t* msg_data, unsigned int msg_len) {
|
||||
uint8_t sub_command = msg_data[0];
|
||||
if (sub_command != 0x00) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "first byte can't be %u", sub_command);
|
||||
prv_send_error_response(session, SCREENSHOT_MALFORMED_COMMAND);
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_screenshot_in_progress) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Screenshot already in progress.");
|
||||
// Use a low timeout, if we are already in screenshot_send_next_chunk with the send buffer locked, then this
|
||||
// would block for a long time, causing the comm_protocol_dispatch_message()'s 150ms max timeout to trip.
|
||||
prv_send_error_response(session, SCREENSHOT_ALREADY_IN_PROGRESS);
|
||||
return;
|
||||
}
|
||||
s_screenshot_in_progress = true;
|
||||
|
||||
prv_request_fast_connection(session);
|
||||
|
||||
compositor_freeze();
|
||||
|
||||
s_screenshot_state = (ScreenshotState) {
|
||||
.session = session,
|
||||
.framebuffer = (FrameBufferState) {
|
||||
.fb = compositor_get_framebuffer(),
|
||||
.row = 0,
|
||||
.col = 0,
|
||||
.width = DISP_COLS,
|
||||
.height = DISP_ROWS,
|
||||
},
|
||||
.sent_header = false,
|
||||
};
|
||||
|
||||
screenshot_send_next_chunk(&s_screenshot_state);
|
||||
}
|
23
src/fw/services/common/compositor/screenshot_pp.h
Normal file
23
src/fw/services/common/compositor/screenshot_pp.h
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <inttypes.h>
|
||||
#include "services/common/comm_session/session.h"
|
||||
|
||||
//! Callback for handling a screenshot request message from the client
|
||||
void screenshot_protocol_msg_callback(CommSession *session, const uint8_t* data, unsigned int length);
|
465
src/fw/services/common/cron.c
Normal file
465
src/fw/services/common/cron.c
Normal file
|
@ -0,0 +1,465 @@
|
|||
/*
|
||||
* 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/cron.h"
|
||||
#include <pebbleos/cron.h>
|
||||
|
||||
#include "os/mutex.h"
|
||||
#include "system/passert.h"
|
||||
#include "services/common/regular_timer.h"
|
||||
#include "system/logging.h"
|
||||
#include "util/math.h"
|
||||
|
||||
//! Don't let users modify the list while callbacks are occurring.
|
||||
static PebbleMutex *s_list_mutex = NULL;
|
||||
|
||||
static void prv_timer_callback(void* data);
|
||||
static RegularTimerInfo s_regular = {
|
||||
.cb = prv_timer_callback,
|
||||
};
|
||||
|
||||
// List of jobs sorted from soonest to farthest.
|
||||
static ListNode *s_scheduled_jobs;
|
||||
|
||||
// -------------------------------------------------------------------------------------------
|
||||
static bool prv_is_scheduled(CronJob *job) {
|
||||
// Assumes mutex lock is already taken
|
||||
return list_contains(s_scheduled_jobs, &job->list_node);
|
||||
}
|
||||
|
||||
static int prv_sort(void *a, void *b) {
|
||||
CronJob *job_a = (CronJob*)a;
|
||||
CronJob *job_b = (CronJob*)b;
|
||||
return job_b->cached_execute_time - job_a->cached_execute_time;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------
|
||||
static void prv_timer_callback(void* data) {
|
||||
mutex_lock(s_list_mutex);
|
||||
while (s_scheduled_jobs != NULL &&
|
||||
((CronJob*)s_scheduled_jobs)->cached_execute_time <= rtc_get_time()) {
|
||||
CronJob *job = (CronJob*)s_scheduled_jobs;
|
||||
// Remove the job from the list, it's done.
|
||||
s_scheduled_jobs = list_pop_head(s_scheduled_jobs);
|
||||
|
||||
// Release the mutex while we execute the callback
|
||||
mutex_unlock(s_list_mutex);
|
||||
job->cb(job, job->cb_data);
|
||||
mutex_lock(s_list_mutex);
|
||||
}
|
||||
mutex_unlock(s_list_mutex);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------------
|
||||
void cron_service_handle_clock_change(PebbleSetTimeEvent *set_time_info) {
|
||||
mutex_lock(s_list_mutex);
|
||||
|
||||
const bool must_recalc = set_time_info->gmt_offset_delta != 0 || set_time_info->dst_changed;
|
||||
// Because it's ABS, it'll be unsigned. This makes the compiler behave.
|
||||
const uint32_t change_diff = ABS(set_time_info->utc_time_delta);
|
||||
// Need to re-build the list somewhere else
|
||||
ListNode *newlist = NULL;
|
||||
while (s_scheduled_jobs != NULL) {
|
||||
CronJob* job = (CronJob*)s_scheduled_jobs;
|
||||
s_scheduled_jobs = list_pop_head(s_scheduled_jobs);
|
||||
// Re-calculate the execute time.
|
||||
// See the notes in the API header on how this works.
|
||||
if (must_recalc || change_diff >= job->clock_change_tolerance) {
|
||||
job->cached_execute_time = cron_job_get_execute_time(job);
|
||||
}
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Cron job rescheduled for %ld", job->cached_execute_time);
|
||||
|
||||
newlist = list_sorted_add(newlist, &job->list_node, prv_sort, true);
|
||||
}
|
||||
// Then move it back to the static
|
||||
s_scheduled_jobs = newlist;
|
||||
|
||||
mutex_unlock(s_list_mutex);
|
||||
|
||||
// We want to run any tasks we've skipped over.
|
||||
prv_timer_callback(NULL);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------------
|
||||
void cron_service_init(void) {
|
||||
PBL_ASSERTN(s_list_mutex == NULL);
|
||||
|
||||
s_list_mutex = mutex_create();
|
||||
s_scheduled_jobs = NULL;
|
||||
|
||||
regular_timer_add_seconds_callback(&s_regular);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------
|
||||
time_t cron_job_schedule(CronJob *job) {
|
||||
PBL_ASSERTN(s_list_mutex);
|
||||
|
||||
mutex_lock(s_list_mutex);
|
||||
|
||||
const time_t now = rtc_get_time();
|
||||
// Always update the execution time.
|
||||
job->cached_execute_time = cron_job_get_execute_time_from_epoch(job, now);
|
||||
// If not scheduled yet, schedule it.
|
||||
if (!prv_is_scheduled(job)) {
|
||||
s_scheduled_jobs = list_sorted_add(s_scheduled_jobs, &job->list_node, prv_sort, true);
|
||||
}
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Cron job scheduled for %ld (%+ld)", job->cached_execute_time,
|
||||
(job->cached_execute_time - now));
|
||||
|
||||
mutex_unlock(s_list_mutex);
|
||||
|
||||
return job->cached_execute_time;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
time_t cron_job_schedule_after(CronJob *job, CronJob *new_job) {
|
||||
PBL_ASSERTN(s_list_mutex);
|
||||
|
||||
mutex_lock(s_list_mutex);
|
||||
|
||||
// can't schedule an already scheduled job
|
||||
PBL_ASSERTN(!prv_is_scheduled(new_job));
|
||||
// can't schedule after an unscheduled job
|
||||
PBL_ASSERTN(prv_is_scheduled(job));
|
||||
|
||||
// copy schedule info from existing job
|
||||
CronJob temp_job = *job;
|
||||
list_init(&temp_job.list_node);
|
||||
temp_job.cb = new_job->cb;
|
||||
temp_job.cb_data = new_job->cb_data;
|
||||
*new_job = temp_job;
|
||||
|
||||
// insert after in the list, which guarantees it gets executed after
|
||||
list_insert_after(&job->list_node, &new_job->list_node);
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Cron job scheduled for %ld", job->cached_execute_time);
|
||||
|
||||
mutex_unlock(s_list_mutex);
|
||||
|
||||
return job->cached_execute_time;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
bool cron_job_is_scheduled(CronJob *job) {
|
||||
PBL_ASSERTN(s_list_mutex);
|
||||
|
||||
mutex_lock(s_list_mutex);
|
||||
bool rv = prv_is_scheduled(job);
|
||||
mutex_unlock(s_list_mutex);
|
||||
|
||||
return (rv);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
bool cron_job_unschedule(CronJob *job) {
|
||||
PBL_ASSERTN(s_list_mutex);
|
||||
bool removed = false;
|
||||
mutex_lock(s_list_mutex);
|
||||
|
||||
if (prv_is_scheduled(job)) {
|
||||
list_remove(&job->list_node, &s_scheduled_jobs, NULL);
|
||||
removed = true;
|
||||
}
|
||||
|
||||
mutex_unlock(s_list_mutex);
|
||||
return removed;
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// For Testing:
|
||||
|
||||
void cron_clear_all_jobs(void) {
|
||||
mutex_lock(s_list_mutex);
|
||||
|
||||
// Iterate over all the jobs to remove them all.
|
||||
for (ListNode* iter = s_scheduled_jobs; iter != NULL; ) {
|
||||
CronJob* job = (CronJob*)iter;
|
||||
iter = list_get_next(iter);
|
||||
// Remove the job from the list.
|
||||
list_remove(&job->list_node, NULL, NULL);
|
||||
}
|
||||
s_scheduled_jobs = NULL;
|
||||
|
||||
mutex_unlock(s_list_mutex);
|
||||
}
|
||||
|
||||
void cron_service_deinit(void) {
|
||||
cron_clear_all_jobs();
|
||||
|
||||
mutex_destroy(s_list_mutex);
|
||||
s_list_mutex = NULL;
|
||||
|
||||
regular_timer_remove_callback(&s_regular);
|
||||
}
|
||||
|
||||
uint32_t cron_service_get_job_count(void) {
|
||||
uint32_t count = 0;
|
||||
mutex_lock(s_list_mutex);
|
||||
count = list_count(s_scheduled_jobs);
|
||||
mutex_unlock(s_list_mutex);
|
||||
return count;
|
||||
}
|
||||
|
||||
void cron_service_wakeup(void) {
|
||||
prv_timer_callback(NULL);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// The brains.
|
||||
typedef enum {
|
||||
CronAssignMode_LocalEpoch, // 'any' uses local epoch's value
|
||||
CronAssignMode_Zero, // 'any' uses 0
|
||||
} CronAssignMode;
|
||||
|
||||
// Indices for the access arrays
|
||||
#define CRON_INDEX_YEAR 0
|
||||
#define CRON_INDEX_MONTH 1
|
||||
#define CRON_INDEX_DAY 2
|
||||
#define CRON_INDEX_HOUR 3
|
||||
#define CRON_INDEX_MIN 4
|
||||
#define CRON_INDEX_SEC 5
|
||||
#define CRON_INDEX_COUNT 6
|
||||
|
||||
#define CRON_GENERIC_ANY (-1)
|
||||
#define CRON_YEAR_ANY (-1)
|
||||
#define CRON_SECOND_ANY (-1)
|
||||
|
||||
// If the 'working' time is ahead of local epoch, we return 1. If behind, we return -1.
|
||||
// Otherwise, return 0.
|
||||
static int prv_future_past_direction(int **dest_arr, const int *curr_arr) {
|
||||
// Iterate from highest order to lowest.
|
||||
for (int i = 0; i < CRON_INDEX_COUNT; i++) {
|
||||
if (*(dest_arr[i]) > curr_arr[i]) {
|
||||
// In future
|
||||
return 1;
|
||||
} else if (*(dest_arr[i]) < curr_arr[i]) {
|
||||
// In past
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Increase the day in `cron_tm` to fit into the wday set in `cron`.
|
||||
// This doesn't take mday into account because that's way too hard and we won't need it.
|
||||
static bool prv_adjust_for_wday_spec(const CronJob *cron, struct tm *cron_tm) {
|
||||
// If we're allowing any wday, we're not adjusting.
|
||||
if (cron->wday == WDAY_ANY || cron->wday == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Keep track of whether we've adjusted or not.
|
||||
bool adjusted = false;
|
||||
|
||||
// We need to update cron_tm's tm_wday for proper checking.
|
||||
cron_tm->tm_mday += 1; // Adjustment because struct tm has mday 1-indexed for whatever reason
|
||||
mktime(cron_tm);
|
||||
cron_tm->tm_mday -= 1;
|
||||
// We have 1 week to find a fitting date
|
||||
for (int l = 0; l < DAYS_PER_WEEK; l++) {
|
||||
if (cron->wday & (1 << cron_tm->tm_wday)) {
|
||||
break;
|
||||
}
|
||||
// Advance the day.
|
||||
cron_tm->tm_mday++;
|
||||
cron_tm->tm_wday = (cron_tm->tm_wday + 1) % DAYS_PER_WEEK;
|
||||
adjusted = true;
|
||||
}
|
||||
return adjusted;
|
||||
}
|
||||
|
||||
static time_t prv_get_execute_time_from_epoch(const CronJob *job, time_t local_epoch) {
|
||||
struct tm current_tm;
|
||||
// We work off of each element, so we need a struct tm.
|
||||
localtime_r(&local_epoch, ¤t_tm);
|
||||
|
||||
// Adjust to be zero-indexed
|
||||
current_tm.tm_mday -= 1;
|
||||
|
||||
// If the job isn't allowed to fire instantly, we're going to force the current second to be
|
||||
// 1, and the destination second to be 0. This works because it means we cannot use the current
|
||||
// time as-is, but it will not influence the other fields more than necessary.
|
||||
if (!job->may_be_instant) {
|
||||
current_tm.tm_sec = 1;
|
||||
}
|
||||
// Cron tm is based on the current tm
|
||||
struct tm cron_tm = current_tm;
|
||||
// Don't listen to this stuff (yet)
|
||||
cron_tm.tm_gmtoff = 0;
|
||||
cron_tm.tm_isdst = 0;
|
||||
|
||||
// Access everything as arrays because it's way easier that way.
|
||||
int *dest_arr[CRON_INDEX_COUNT] = {
|
||||
&cron_tm.tm_year,
|
||||
&cron_tm.tm_mon,
|
||||
&cron_tm.tm_mday,
|
||||
&cron_tm.tm_hour,
|
||||
&cron_tm.tm_min,
|
||||
&cron_tm.tm_sec,
|
||||
};
|
||||
const int curr_arr[CRON_INDEX_COUNT] = {
|
||||
current_tm.tm_year,
|
||||
current_tm.tm_mon,
|
||||
current_tm.tm_mday,
|
||||
current_tm.tm_hour,
|
||||
current_tm.tm_min,
|
||||
current_tm.tm_sec,
|
||||
};
|
||||
const int spec_arr[CRON_INDEX_COUNT] = {
|
||||
CRON_YEAR_ANY, // year should always default
|
||||
job->month,
|
||||
job->mday,
|
||||
job->hour,
|
||||
job->minute,
|
||||
// If can be instant, second should default.
|
||||
// If it can't, use 0 because it's less than 1.
|
||||
job->may_be_instant ? CRON_SECOND_ANY : 0,
|
||||
};
|
||||
|
||||
/*
|
||||
This is where the actual date finding is done. Essentially, we start with setting the result to
|
||||
the local epoch, and modify from there.
|
||||
|
||||
We iterate over the fields from most significant to least significant. The reasoning for this is
|
||||
that we will only know how to properly adjust a less significant field based on the value of the
|
||||
more significant fields.
|
||||
|
||||
When a field in the spec is marked as ANY (-1), we need to decide what to put in the result:
|
||||
- If all values so far are still the same as the local epoch, we will use the local epoch's
|
||||
value.
|
||||
- Otherwise, the value stored will be 0, because the result is in the future, so a value of 0
|
||||
will definitely be the soonest time that matches.
|
||||
|
||||
Now, if the result is behind the local epoch, we step through higher order fields for a field
|
||||
that was not specified. When we find one, we increase the value by 1. Since this is a higher
|
||||
order field, this is guaranteed to put the result ahead of the local epoch.
|
||||
*/
|
||||
|
||||
// 'any' assignment defaults to using the local epoch's values.
|
||||
CronAssignMode assign_mode = CronAssignMode_LocalEpoch;
|
||||
// Iterate over all the fields
|
||||
for (int i = CRON_INDEX_YEAR; i < CRON_INDEX_COUNT; i++) {
|
||||
// If the spec had an 'any':
|
||||
if (spec_arr[i] <= CRON_GENERIC_ANY) {
|
||||
switch (assign_mode) {
|
||||
case CronAssignMode_LocalEpoch:
|
||||
// value will be the local epoch's value, we don't need to change anything
|
||||
break;
|
||||
case CronAssignMode_Zero:
|
||||
// value will be set to 0
|
||||
*(dest_arr[i]) = 0;
|
||||
break;
|
||||
}
|
||||
} else { // Otherwise, use the spec's value.
|
||||
*(dest_arr[i]) = spec_arr[i];
|
||||
}
|
||||
|
||||
if (assign_mode == CronAssignMode_LocalEpoch) {
|
||||
// If we haven't started adjusting things yet, we need to do checking.
|
||||
|
||||
const int direction = prv_future_past_direction(dest_arr, curr_arr);
|
||||
if (direction < 0) {
|
||||
// If the target is _behind_ the current time, we need to increase a higher order field.
|
||||
|
||||
// Step from next highest all the way up to a year. We adjust the least significant field
|
||||
// that is more significant than the current field, and is unspec'd.
|
||||
for (int l = i - 1; l >= CRON_INDEX_YEAR; l--) {
|
||||
// If the field isn't set in the spec, increasing it by 1 will put us back in the
|
||||
// future.
|
||||
if (spec_arr[l] <= CRON_GENERIC_ANY) {
|
||||
*(dest_arr[l]) += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (direction != 0) {
|
||||
// The target is now ahead of the current time, the rest of the unspec'd fields
|
||||
// should be 0.
|
||||
assign_mode = CronAssignMode_Zero;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Increase the day until we fit into the `wday` spec.
|
||||
if (prv_adjust_for_wday_spec(job, &cron_tm)) {
|
||||
// If the day has been adjusted, we need to re-set hour+minute+second.
|
||||
// Since we are definitely in the future on an adjustment, fields with 'any' should be
|
||||
// set to 0, otherwise set to the spec value.
|
||||
cron_tm.tm_hour = MAX(job->hour, 0);
|
||||
cron_tm.tm_min = MAX(job->minute, 0);
|
||||
// Second is always 0 when we're in the future.
|
||||
cron_tm.tm_sec = 0;
|
||||
}
|
||||
|
||||
// Adjust back to 1-indexed
|
||||
cron_tm.tm_mday += 1;
|
||||
|
||||
// Decide the DSTny (Adjust for DST transitions)
|
||||
cron_tm.tm_gmtoff = current_tm.tm_gmtoff; // We're using the current time's GMT offset
|
||||
cron_tm.tm_isdst = 0; // We'll do the DST adjust ourselves
|
||||
time_t t = mktime(&cron_tm);
|
||||
|
||||
// Apply offset seconds
|
||||
t += job->offset_seconds;
|
||||
|
||||
if (time_get_isdst(t)) {
|
||||
t -= time_get_dstoffset();
|
||||
if (!time_get_isdst(t)) {
|
||||
// We're in the hole where DST starts.
|
||||
// We want holed alarms to fire instantly, so set time to DST start time.
|
||||
t = time_get_dst_start();
|
||||
}
|
||||
}
|
||||
// We could be in the overlap where DST ends, but we don't actually care about it.
|
||||
// Why, you ask? This gives us the 'first' matching time if we ignore it.
|
||||
// So 1:30 will give us the first 1:30, not the second one.
|
||||
// Yes it's arbitrary. Yes it's confusing. But that's timekeeping and DST for you.
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
time_t cron_job_get_execute_time_from_epoch(const CronJob *job, time_t local_epoch) {
|
||||
time_t t = prv_get_execute_time_from_epoch(job, local_epoch);
|
||||
|
||||
if (job->offset_seconds != 0) {
|
||||
time_t t_last = t;
|
||||
time_t offset_epoch = local_epoch;
|
||||
while (true) {
|
||||
const time_t t_delta = (t - local_epoch) * ((job->offset_seconds > 0) ? 1 : -1);
|
||||
if ((job->may_be_instant ? (t_delta <= 0) : (t_delta < 0))) {
|
||||
break;
|
||||
}
|
||||
// Offset seconds is positive => Applying a positive offset seconds could result in a trigger
|
||||
// time after the nearest trigger time, find and check the previous time.
|
||||
// Offset seconds is negative => Applying a negative offset seconds resulted in a time before
|
||||
// local_epoch, calculate the next time.
|
||||
t_last = t;
|
||||
offset_epoch -= job->offset_seconds;
|
||||
const time_t rv = prv_get_execute_time_from_epoch(job, offset_epoch);
|
||||
t = rv < local_epoch ? t : rv;
|
||||
if (job->offset_seconds > 0 && t == t_last) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
|
||||
time_t cron_job_get_execute_time(const CronJob *job) {
|
||||
return cron_job_get_execute_time_from_epoch(job, rtc_get_time());
|
||||
}
|
48
src/fw/services/common/cron.h
Normal file
48
src/fw/services/common/cron.h
Normal file
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "kernel/events.h"
|
||||
|
||||
//! @file cron.h
|
||||
//! Wall-clock based timer system. Designed for use in things such as alarms, calendar events, etc.
|
||||
//! Properly handles DST, etc.
|
||||
//! This file is for controlling the service itself. The actual job API is in <pebbleos/cron.h>
|
||||
|
||||
//! Initialize the cron service.
|
||||
void cron_service_init(void);
|
||||
|
||||
//! Adjust all cron jobs, as the wall clock has changed.
|
||||
//! This means DST and/or time zone may have changed!
|
||||
void cron_service_handle_clock_change(PebbleSetTimeEvent *set_time_info);
|
||||
|
||||
#if UNITTEST
|
||||
// -----------------------------------------------------------------------------
|
||||
// For testing:
|
||||
|
||||
//! Remove all jobs.
|
||||
void cron_clear_all_jobs(void);
|
||||
|
||||
//! Clean up the cron service.
|
||||
void cron_service_deinit(void);
|
||||
|
||||
//! The number of registered cron jobs.
|
||||
uint32_t cron_service_get_job_count(void);
|
||||
|
||||
//! Run the cron timers if they've fired.
|
||||
void cron_service_wakeup(void);
|
||||
#endif
|
103
src/fw/services/common/debounced_connection_service.c
Normal file
103
src/fw/services/common/debounced_connection_service.c
Normal file
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "debounced_connection_service.h"
|
||||
|
||||
#include "services/common/comm_session/session.h"
|
||||
#include "services/common/regular_timer.h"
|
||||
#include "syscall/syscall_internal.h"
|
||||
|
||||
//! This module is responsible for propagating debounced connection events.
|
||||
//! Connection events are passed through right away to subscribers but
|
||||
//! disconnection events are only passed through if a re-connection did not
|
||||
//! occur within a small window of time. This way, short disconnect periods
|
||||
//! can go unnoticed to the end consumer resulting in a better perception of
|
||||
//! connection reliability
|
||||
//!
|
||||
//! At the moment, the connections this module tracks are:
|
||||
//! + Watch <-> Mobile App / PebbleKit JS
|
||||
//! + Watch <-> third-party App using PebbleKit
|
||||
|
||||
typedef enum {
|
||||
MobileAppDebounce = 0,
|
||||
PebbleKitDebounce,
|
||||
NumConnectionsToDebounce,
|
||||
} DebounceConnection;
|
||||
|
||||
static RegularTimerInfo s_debounce_timers[NumConnectionsToDebounce];
|
||||
static bool s_debounced_state_is_connected[NumConnectionsToDebounce];
|
||||
|
||||
static void prv_put_debounced_connection_event(DebounceConnection conn_id) {
|
||||
PebbleEvent event = {
|
||||
.type = PEBBLE_BT_CONNECTION_DEBOUNCED_EVENT,
|
||||
.bluetooth.comm_session_event.is_open = s_debounced_state_is_connected[conn_id],
|
||||
.bluetooth.comm_session_event.is_system = (conn_id == MobileAppDebounce),
|
||||
};
|
||||
event_put(&event);
|
||||
}
|
||||
|
||||
static void prv_handle_disconnection_debounced(void *data) {
|
||||
DebounceConnection conn_id = (DebounceConnection)data;
|
||||
s_debounced_state_is_connected[conn_id] = false;
|
||||
prv_put_debounced_connection_event(conn_id);
|
||||
regular_timer_remove_callback(&s_debounce_timers[conn_id]);
|
||||
}
|
||||
|
||||
void debounced_connection_service_init(void) {
|
||||
for (int i = 0; i < NumConnectionsToDebounce; i++) {
|
||||
s_debounce_timers[i].cb = prv_handle_disconnection_debounced;
|
||||
s_debounce_timers[i].cb_data = (void *)(uintptr_t)i;
|
||||
}
|
||||
|
||||
// initial state of the connections
|
||||
s_debounced_state_is_connected[MobileAppDebounce] = (comm_session_get_system_session() != NULL);
|
||||
s_debounced_state_is_connected[PebbleKitDebounce] =
|
||||
(comm_session_get_current_app_session() != NULL);
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(bool, sys_mobile_app_is_connected_debounced, void) {
|
||||
return s_debounced_state_is_connected[MobileAppDebounce];
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(bool, sys_pebblekit_is_connected_debounced, void) {
|
||||
return s_debounced_state_is_connected[PebbleKitDebounce];
|
||||
}
|
||||
|
||||
#define DISCONNECT_HIDE_DURATION_SECS 25
|
||||
void debounced_connection_service_handle_event(PebbleCommSessionEvent *e) {
|
||||
DebounceConnection conn_id = e->is_system ? MobileAppDebounce : PebbleKitDebounce;
|
||||
bool timer_stopped = false;
|
||||
if (!e->is_open) {
|
||||
// If we become disconnected don't update apps until we have had a chance
|
||||
// to recover the connection. This will make our BT connection seem more
|
||||
// reliable.
|
||||
regular_timer_add_multisecond_callback(
|
||||
&s_debounce_timers[conn_id], DISCONNECT_HIDE_DURATION_SECS);
|
||||
return;
|
||||
}
|
||||
|
||||
if (regular_timer_is_scheduled(&s_debounce_timers[conn_id])) {
|
||||
// we reconnected quickly so no need to notify the app about it
|
||||
timer_stopped = regular_timer_remove_callback(&s_debounce_timers[conn_id]);
|
||||
}
|
||||
|
||||
if (!timer_stopped) {
|
||||
// We've been disconnected long enough that we've already told the app that
|
||||
// we disconnected so let the app know that we are connected again.
|
||||
s_debounced_state_is_connected[conn_id] = true;
|
||||
prv_put_debounced_connection_event(conn_id);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue