Import of the watch repository from Pebble

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

View file

@ -0,0 +1,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

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

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

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

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

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

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

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

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

View 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

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

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

View file

@ -0,0 +1,35 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <stdbool.h>
#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);

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

View file

@ -0,0 +1,48 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#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);

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

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

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

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

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

View file

@ -0,0 +1,23 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#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);

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

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

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

View file

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

View file

@ -0,0 +1,32 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <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);

View 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

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

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

View file

@ -0,0 +1,26 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#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);

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

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

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

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

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

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

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

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

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

View file

@ -0,0 +1,72 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#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;
}
}

View file

@ -0,0 +1,23 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "services/common/comm_session/session.h"
void pp_ble_control_protocol_msg_callback(CommSession* session,
const uint8_t *data,
unsigned int length);

View 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, &current_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(&timestamp, &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(&timestamp, &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(&current_timestamp, &current_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;
}

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

View 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.
*/
#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);

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

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

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

View file

@ -0,0 +1,58 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#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");
}

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

View 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

View file

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

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

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

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

View file

@ -0,0 +1,48 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#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);

View file

@ -0,0 +1,62 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "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;

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

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

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

View file

@ -0,0 +1,184 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "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);
}

View file

@ -0,0 +1,58 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "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);

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

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

View file

@ -0,0 +1,72 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "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);

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

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

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

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

View file

@ -0,0 +1,98 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#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();
}

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

View 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

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

View file

@ -0,0 +1,35 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "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);

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

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

View file

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

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

View file

@ -0,0 +1,215 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "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(&region_sub_bitmap, bitmap, *region);
graphics_private_move_pixels_horizontally(&region_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;
}

View file

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

View file

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

View file

@ -0,0 +1,26 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#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);

View file

@ -0,0 +1,75 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "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;
}

View 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
#include "services/common/compositor/compositor_transitions.h"
const CompositorTransition *compositor_peek_transition_timeline_get(void);

View file

@ -0,0 +1,114 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#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;
}

View file

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

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

View file

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

View file

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

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

View file

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

View file

@ -0,0 +1,23 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#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);

View file

@ -0,0 +1,117 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#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;
}
}

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

View file

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

View file

@ -0,0 +1,26 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#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);

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

View file

@ -0,0 +1,23 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#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);

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

View file

@ -0,0 +1,48 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#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

View file

@ -0,0 +1,103 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "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