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,473 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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_service.h"
#include "accel_service_private.h"
#include "applib/applib_malloc.auto.h"
#include "applib/pbl_std/pbl_std.h"
#include "event_service_client.h"
#include "kernel/kernel_applib_state.h"
#include "kernel/pbl_malloc.h"
#include "process_state/app_state/app_state.h"
#include "process_state/worker_state/worker_state.h"
#include "services/common/accel_manager.h"
#include "services/common/system_task.h"
#include "services/common/vibe_pattern.h"
#include "syscall/syscall.h"
#include "system/passert.h"
#include "FreeRTOS.h"
#include "queue.h"
static bool prv_is_session_task(void) {
PebbleTask task = pebble_task_get_current();
return (task == PebbleTask_KernelMain || task == PebbleTask_KernelBackground ||
task == PebbleTask_App);
}
// ------------------------------------------------------------------------------------------
// Assert that the current task is allowed to create/delete a session
static void prv_assert_session_task(void) {
PBL_ASSERTN(prv_is_session_task());
}
// --------------------------------------------------------------------------------------------
// Return the session ref for the given task. This should ONLY be used by 3rd party tasks
// (app or worker).
AccelServiceState * accel_service_private_get_session(PebbleTask task) {
if (task == PebbleTask_Unknown) {
task = pebble_task_get_current();
}
if (task == PebbleTask_App) {
return app_state_get_accel_state();
} else if (task == PebbleTask_Worker) {
return worker_state_get_accel_state();
} else {
WTF;
}
}
void accel_service_cleanup_task_session(PebbleTask task) {
AccelServiceState *state = accel_service_private_get_session(task);
if (state->manager_state) {
sys_accel_manager_data_unsubscribe(state->manager_state);
}
}
// ----------------------------------------------------------------------------------------------
// Event service handler for tap events
static void prv_do_shake_handle(PebbleEvent *e, void *context) {
PebbleTask task = pebble_task_get_current();
AccelServiceState *state = (AccelServiceState *)accel_service_private_get_session(task);
PBL_ASSERTN(state->shake_handler != NULL);
if (task == PebbleTask_Worker || task == PebbleTask_App) {
sys_analytics_inc(ANALYTICS_APP_METRIC_ACCEL_SHAKE_COUNT, AnalyticsClient_CurrentTask);
}
state->shake_handler(e->accel_tap.axis, e->accel_tap.direction);
}
// ----------------------------------------------------------------------------------------------
static void prv_do_double_tap_handle(PebbleEvent *e, void *context) {
PebbleTask task = pebble_task_get_current();
AccelServiceState *state = (AccelServiceState *)accel_service_private_get_session(task);
PBL_ASSERTN(state->double_tap_handler != NULL);
// only kernel clients can subscribe to double tap right now, so just increment double tap count
// device analytic here
analytics_inc(ANALYTICS_DEVICE_METRIC_ACCEL_DOUBLE_TAP_COUNT, AnalyticsClient_System);
state->double_tap_handler(e->accel_tap.axis, e->accel_tap.direction);
}
// -----------------------------------------------------------------------------------------------
// Handle a chunk of data received for a data subscription. Called by prv_do_data_handle.
static uint32_t prv_do_data_handle_chunk(AccelServiceState *state, uint16_t time_interval_ms) {
uint32_t num_samples = 0;
uint64_t timestamp_ms;
num_samples = sys_accel_manager_get_num_samples(state->manager_state, &timestamp_ms);
if (num_samples < state->samples_per_update) {
return 0;
}
#if LOG_DOMAIN_ACCEL
uint32_t time_since_last_sample =
(state->prev_timestamp_ms != 0) ? timestamp_ms - state->prev_timestamp_ms : 0;
state->prev_timestamp_ms = timestamp_ms;
ACCEL_LOG_DEBUG("got %d samples for task %d at %ld (%lu ms delta)",
(int)num_samples, (int)pebble_task_get_current(), (uint32_t)timestamp_ms,
time_since_last_sample);
for (unsigned int i=0; i<num_samples; i++) {
ACCEL_LOG_DEBUG(" => x:%d, y:%d, z:%d", state->raw_data[i].x, state->raw_data[i].y, state->raw_data[i].z);
}
#endif
if (state->raw_data_handler_deprecated) {
state->raw_data_handler_deprecated(state->raw_data, num_samples);
} else if (state->raw_data_handler) {
state->raw_data_handler(state->raw_data, num_samples, timestamp_ms);
} else {
AccelData data[num_samples];
for (uint32_t i = 0; i < num_samples; i++) {
data[i] = (AccelData) {
.x = state->raw_data[i].x,
.y = state->raw_data[i].y,
.z = state->raw_data[i].z,
.timestamp = timestamp_ms,
.did_vibrate = sys_vibe_history_was_vibrating(timestamp_ms)
};
timestamp_ms += time_interval_ms;
}
state->data_handler(data, num_samples);
}
// Tell accel_manager that it can put more data in now
bool success = sys_accel_manager_consume_samples(state->manager_state, num_samples);
PBL_ASSERTN(success);
return num_samples;
}
// ---------------------------------------------------------------------------------------------
// Called by sys_accel_manager when we have data available for this subscriber
static void prv_do_data_handle(void *context) {
AccelServiceState *state = (AccelServiceState *)context;
if (state->manager_state == NULL) {
if (state->deferred_free) {
PBL_LOG(LOG_LEVEL_DEBUG, "Deferred free");
kernel_free(state);
}
// event queue is handled kernel-side, so an event may fire after we've unsubscribed
return;
}
PBL_ASSERTN(state->data_handler != NULL || state->raw_data_handler != NULL
|| state->raw_data_handler_deprecated != NULL);
uint16_t time_interval_ms = 1000 / state->sampling_rate;
// Process in chunks to limit the amount of stack space we use up.
uint32_t num_processed;
do {
num_processed = prv_do_data_handle_chunk(state, time_interval_ms);
sys_analytics_add(ANALYTICS_APP_METRIC_ACCEL_SAMPLE_COUNT, num_processed, AnalyticsClient_CurrentTask);
} while (num_processed);
}
// -----------------------------------------------------------------------------------------------
int accel_service_set_sampling_rate(AccelSamplingRate rate) {
AccelServiceState * session = accel_service_private_get_session(PebbleTask_Unknown);
return accel_session_set_sampling_rate(session, rate);
}
// ----------------------------------------------------------------------------------------------
int accel_service_set_samples_per_update(uint32_t samples_per_update) {
AccelServiceState * session = accel_service_private_get_session(PebbleTask_Unknown);
return accel_session_set_samples_per_update(session, samples_per_update);
}
// ----------------------------------------------------------------------------------------------
static void prv_shared_subscribe(AccelServiceState *state, AccelSamplingRate sampling_rate,
uint32_t samples_per_update, PebbleTask handler_task) {
state->manager_state = sys_accel_manager_data_subscribe(
sampling_rate, prv_do_data_handle, state, handler_task);
accel_session_set_samples_per_update((AccelServiceState *)state, samples_per_update);
}
// ----------------------------------------------------------------------------------------------
void accel_data_service_subscribe(uint32_t samples_per_update, AccelDataHandler handler) {
AccelServiceState * session = accel_service_private_get_session(PebbleTask_Unknown);
accel_session_data_subscribe(session, samples_per_update, handler);
}
// ----------------------------------------------------------------------------------------------
void accel_raw_data_service_subscribe(uint32_t samples_per_update, AccelRawDataHandler handler) {
AccelServiceState * session = accel_service_private_get_session(PebbleTask_Unknown);
accel_session_raw_data_subscribe(session, ACCEL_SAMPLING_25HZ, samples_per_update, handler);
}
// ----------------------------------------------------------------------------------------------
void accel_data_service_subscribe__deprecated(uint32_t samples_per_update, AccelRawDataHandler__deprecated handler) {
AccelServiceState * session = accel_service_private_get_session(PebbleTask_Unknown);
AccelServiceState *state = (AccelServiceState *)session;
state->raw_data_handler_deprecated = handler;
state->raw_data_handler = NULL;
state->data_handler = NULL;
prv_shared_subscribe(state, ACCEL_SAMPLING_25HZ, samples_per_update, pebble_task_get_current());
}
// ----------------------------------------------------------------------------------------------
void accel_data_service_unsubscribe(void) {
AccelServiceState * session = accel_service_private_get_session(PebbleTask_Unknown);
accel_session_data_unsubscribe(session);
}
// ----------------------------------------------------------------------------------------------
void accel_tap_service_subscribe(AccelTapHandler handler) {
AccelServiceState * session = accel_service_private_get_session(PebbleTask_Unknown);
accel_session_shake_subscribe(session, handler);
}
// ----------------------------------------------------------------------------------------------
void accel_tap_service_unsubscribe(void) {
AccelServiceState * session = accel_service_private_get_session(PebbleTask_Unknown);
accel_session_shake_unsubscribe(session);
}
// ----------------------------------------------------------------------------------------------
void accel_double_tap_service_subscribe(AccelTapHandler handler) {
AccelServiceState * session = accel_service_private_get_session(PebbleTask_Unknown);
accel_session_double_tap_subscribe(session, handler);
}
// ----------------------------------------------------------------------------------------------
void accel_double_tap_service_unsubscribe(void) {
AccelServiceState * session = accel_service_private_get_session(PebbleTask_Unknown);
accel_session_double_tap_unsubscribe(session);
}
// ----------------------------------------------------------------------------------------------
int accel_service_peek(AccelData *accel_data) {
AccelServiceState *state = accel_service_private_get_session(PebbleTask_Unknown);
int rc = sys_accel_manager_peek(accel_data);
ACCEL_LOG_DEBUG("peek data x:%d, y:%d, z:%d", accel_data->x, accel_data->y, accel_data->z);
if (rc != 0 || state->raw_data_handler_deprecated || state->raw_data_handler) {
// No timestamp info needed
return rc;
}
accel_data->did_vibrate = (sys_vibe_get_vibe_strength() != 0);
return rc;
}
// ----------------------------------------------------------------------------------------------
void accel_service_state_init(AccelServiceState *state) {
*state = (AccelServiceState) {
.sampling_rate = ACCEL_DEFAULT_SAMPLING_RATE,
.accel_shake_info = {
.type = PEBBLE_ACCEL_SHAKE_EVENT,
.handler = &prv_do_shake_handle,
},
.accel_double_tap_info = {
.type = PEBBLE_ACCEL_DOUBLE_TAP_EVENT,
.handler = &prv_do_double_tap_handle,
}
};
}
// ----------------------------------------------------------------------------------------------
// Event service handler for shake events
static void prv_session_do_shake_handle(PebbleEvent *e, void *context) {
AccelServiceState *state = context;
if (state->shake_handler != NULL) {
state->shake_handler(e->accel_tap.axis, e->accel_tap.direction);
}
}
// ----------------------------------------------------------------------------------------------
// Event service handler for double tap events
static void prv_session_do_double_tap_handle(PebbleEvent *e, void *context) {
AccelServiceState *state = context;
if (state->double_tap_handler != NULL) {
state->double_tap_handler(e->accel_tap.axis, e->accel_tap.direction);
}
}
// -----------------------------------------------------------------------------------------------
AccelServiceState * accel_session_create(void) {
prv_assert_session_task();
AccelServiceState *state = kernel_malloc_check(sizeof(AccelServiceState));
*state = (AccelServiceState) {
.sampling_rate = ACCEL_DEFAULT_SAMPLING_RATE,
.accel_shake_info = {
.type = PEBBLE_ACCEL_SHAKE_EVENT,
.handler = &prv_session_do_shake_handle,
.context = state,
},
.accel_double_tap_info = {
.type = PEBBLE_ACCEL_DOUBLE_TAP_EVENT,
.handler = &prv_session_do_double_tap_handle,
.context = state,
},
};
return state;
}
// -----------------------------------------------------------------------------------------------
static void prv_cleanup_accel_session_cb(void *session) {
kernel_free(session);
}
void accel_session_delete(AccelServiceState * session) {
prv_assert_session_task();
// we better have unsubscribed at this point
PBL_ASSERTN(session->manager_state == NULL);
// A deferred free means one lingering event was posted. We will free the session once the event
// gets drained in 'prv_do_data_handle'
if (!session->deferred_free) {
kernel_free(session);
}
}
// ----------------------------------------------------------------------------------------------
void accel_session_shake_subscribe(AccelServiceState * session, AccelTapHandler handler) {
AccelServiceState *state = (AccelServiceState *)session;
state->shake_handler = handler;
event_service_client_subscribe(&state->accel_shake_info);
}
// ----------------------------------------------------------------------------------------------
void accel_session_shake_unsubscribe(AccelServiceState *state) {
event_service_client_unsubscribe(&state->accel_shake_info);
state->shake_handler = NULL;
}
// -----------------------------------------------------------------------------------------------
void accel_session_double_tap_subscribe(AccelServiceState *state, AccelTapHandler handler) {
state->double_tap_handler = handler;
event_service_client_subscribe(&state->accel_double_tap_info);
}
// -----------------------------------------------------------------------------------------------
void accel_session_double_tap_unsubscribe(AccelServiceState *state) {
event_service_client_unsubscribe(&state->accel_double_tap_info);
state->double_tap_handler = NULL;
}
// -----------------------------------------------------------------------------------------------
void accel_session_data_subscribe(AccelServiceState *state, uint32_t samples_per_update,
AccelDataHandler handler) {
state->data_handler = handler;
state->raw_data_handler = NULL;
state->raw_data_handler_deprecated = NULL;
prv_shared_subscribe(state, ACCEL_SAMPLING_25HZ, samples_per_update, pebble_task_get_current());
}
// -----------------------------------------------------------------------------------------------
void accel_session_raw_data_subscribe(
AccelServiceState *state, AccelSamplingRate sampling_rate, uint32_t samples_per_update,
AccelRawDataHandler handler) {
state->raw_data_handler = handler;
state->raw_data_handler_deprecated = NULL;
state->data_handler = NULL;
prv_shared_subscribe(state, sampling_rate, samples_per_update, pebble_task_get_current());
}
// -----------------------------------------------------------------------------------------------
void accel_session_data_unsubscribe(AccelServiceState *state) {
if (!state->manager_state) {
return;
}
if (sys_accel_manager_data_unsubscribe(state->manager_state)) {
// There is a pending event posted. Only session tasks allocate memory for their state in the
// first place so only free the memory if this is true
state->deferred_free = prv_is_session_task();
}
applib_free(state->raw_data);
state->manager_state = NULL;
state->raw_data = NULL;
state->data_handler = NULL;
state->raw_data_handler = NULL;
state->raw_data_handler_deprecated = NULL;
}
// -----------------------------------------------------------------------------------------------
int accel_session_set_sampling_rate(AccelServiceState *state, AccelSamplingRate rate) {
if (!state->manager_state || (!state->data_handler && !state->raw_data_handler
&& !state->raw_data_handler_deprecated)) {
return -1;
}
state->sampling_rate = rate;
return sys_accel_manager_set_sampling_rate(state->manager_state, rate);
}
// -----------------------------------------------------------------------------------------------
int accel_session_set_samples_per_update(AccelServiceState *state, uint32_t samples_per_update) {
if (samples_per_update > ACCEL_MAX_SAMPLES_PER_UPDATE) {
APP_LOG(LOG_LEVEL_WARNING, "%d samples per update requested, max is %d",
(int)samples_per_update, ACCEL_MAX_SAMPLES_PER_UPDATE);
samples_per_update = ACCEL_MAX_SAMPLES_PER_UPDATE;
}
if (!state->manager_state
|| (samples_per_update > 0 && !state->data_handler && !state->raw_data_handler
&& !state->raw_data_handler_deprecated)) {
return -1;
}
AccelRawData *old_buf = state->raw_data;
// This is a packed array of simple types and therefore shouldn't have compatibility padding
state->raw_data = applib_malloc(samples_per_update * sizeof(AccelRawData));
if (!state->raw_data) {
APP_LOG(LOG_LEVEL_ERROR, "Not enough memory to subscribe");
state->raw_data = old_buf;
return -1;
}
state->samples_per_update = samples_per_update;
int result = sys_accel_manager_set_sample_buffer(state->manager_state, state->raw_data,
samples_per_update);
applib_free(old_buf);
return result;
}

View file

@ -0,0 +1,148 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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/accel_manager.h"
#include "services/imu/units.h"
#include "kernel/pebble_tasks.h"
#include <inttypes.h>
#include <stdbool.h>
//! @addtogroup Foundation
//! @{
//! @addtogroup EventService
//! @{
//! @addtogroup AccelerometerService
//!
//! \brief Using the Pebble accelerometer
//!
//! The AccelerometerService enables the Pebble accelerometer to detect taps,
//! perform measures at a given frequency, and transmit samples in batches to save CPU time
//! and processing.
//!
//! For available code samples, see the
//! <a href="https://github.com/pebble-examples/feature-accel-discs/">feature-accel-discs</a>
//! example app.
//! @{
//! Enumerated values defining the three accelerometer axes.
typedef enum {
//! Accelerometer's X axis. The positive direction along the X axis goes
//! toward the right of the watch.
ACCEL_AXIS_X = 0,
//! Accelerometer's Y axis. The positive direction along the Y axis goes
//! toward the top of the watch.
ACCEL_AXIS_Y = 1,
//! Accelerometer's Z axis. The positive direction along the Z axis goes
//! vertically out of the watchface.
ACCEL_AXIS_Z = 2,
} AccelAxisType;
// Make sure the AccelAxisType enum is compatible with the unified
// IMUCoordinateAxis enum.
_Static_assert(ACCEL_AXIS_X == (int)AXIS_X,
"AccelAxisType incompatible with IMUCoordinateAxis");
_Static_assert(ACCEL_AXIS_Y == (int)AXIS_Y,
"AccelAxisType incompatible with IMUCoordinateAxis");
_Static_assert(ACCEL_AXIS_Z == (int)AXIS_Z,
"AccelAxisType incompatible with IMUCoordinateAxis");
#define ACCEL_DEFAULT_SAMPLING_RATE ACCEL_SAMPLING_25HZ
#define ACCEL_MINIMUM_SAMPLING_RATE ACCEL_SAMPLING_10HZ
//! Callback type for accelerometer tap events
//! @param axis the axis on which a tap was registered (x, y, or z)
//! @param direction the direction (-1 or +1) of the tap
typedef void (*AccelTapHandler)(AccelAxisType axis, int32_t direction);
//! Callback type for accelerometer data events
//! @param data Pointer to the collected accelerometer samples.
//! @param num_samples the number of samples stored in data.
typedef void (*AccelDataHandler)(AccelData *data, uint32_t num_samples);
//! Callback type for accelerometer raw data events
//! @param data Pointer to the collected accelerometer samples.
//! @param num_samples the number of samples stored in data.
//! @param timestamp the timestamp, in ms, of the first sample.
typedef void (*AccelRawDataHandler)(AccelRawData *data, uint32_t num_samples, uint64_t timestamp);
//! Subscribe to the accelerometer tap event service. Once subscribed, the handler
//! gets called on every tap event emitted by the accelerometer.
//! @param handler A callback to be executed on tap event
void accel_tap_service_subscribe(AccelTapHandler handler);
//! Unsubscribe from the accelerometer tap event service. Once unsubscribed,
//! the previously registered handler will no longer be called.
void accel_tap_service_unsubscribe(void);
//! @internal
//! Subscribe to the accelerometer double tap event service. Once subscribed, the handler
//! gets called on every double tap event emitted by the accelerometer.
//! @param handler A callback to be executed on double tap event
void accel_double_tap_service_subscribe(AccelTapHandler handler);
//! @internal
//! Unsubscribe from the accelerometer double tap event service. Once unsubscribed,
//! the previously registered handler will no longer be called.
void accel_double_tap_service_unsubscribe(void);
//! Subscribe to the accelerometer data event service. Once subscribed, the handler
//! gets called every time there are new accelerometer samples available.
//! @note Cannot use \ref accel_service_peek() when subscribed to accelerometer data events.
//! @param handler A callback to be executed on accelerometer data events
//! @param samples_per_update the number of samples to buffer, between 0 and 25.
void accel_data_service_subscribe(uint32_t samples_per_update, AccelDataHandler handler);
//! Subscribe to the accelerometer raw data event service. Once subscribed, the handler
//! gets called every time there are new accelerometer samples available.
//! @note Cannot use \ref accel_service_peek() when subscribed to accelerometer data events.
//! @param handler A callback to be executed on accelerometer data events
//! @param samples_per_update the number of samples to buffer, between 0 and 25.
void accel_raw_data_service_subscribe(uint32_t samples_per_update, AccelRawDataHandler handler);
//! Unsubscribe from the accelerometer data event service. Once unsubscribed,
//! the previously registered handler will no longer be called.
void accel_data_service_unsubscribe(void);
//! Change the accelerometer sampling rate.
//! @param rate The sampling rate in Hz (10Hz, 25Hz, 50Hz, and 100Hz possible)
int accel_service_set_sampling_rate(AccelSamplingRate rate);
//! Change the number of samples buffered between each accelerometer data event
//! @param num_samples the number of samples to buffer, between 0 and 25.
int accel_service_set_samples_per_update(uint32_t num_samples);
//! Peek at the last recorded reading.
//! @param[out] data a pointer to a pre-allocated AccelData item
//! @note Cannot be used when subscribed to accelerometer data events.
//! @return -1 if the accel is not running
//! @return -2 if subscribed to accelerometer events.
int accel_service_peek(AccelData *data);
//! @} // end addtogroup AccelerometerService
//! @} // end addtogroup EventService
//! @} // end addtogroup Foundation
//! @internal
typedef void (*AccelRawDataHandler__deprecated)(AccelRawData *data, uint32_t num_samples);
//! @internal
//! This is used to stay in the jump table where the old accel_data_service_subscribe was located.
//! Allows operation on AccelRawData data, which is the same as the previous version of AccelData.
void accel_data_service_subscribe__deprecated(uint32_t samples_per_update, AccelRawDataHandler__deprecated handler);

View file

@ -0,0 +1,123 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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_service.h"
#include "event_service_client.h"
#include "system/logging.h"
typedef struct AccelServiceState {
// Configuration for our data callback subscription to the accel manager
AccelManagerState *manager_state;
AccelSamplingRate sampling_rate;
bool deferred_free;
uint16_t samples_per_update;
AccelRawData *raw_data; // of size samples_per_update
// User-provided callbacks for various events
AccelDataHandler data_handler;
AccelTapHandler shake_handler;
AccelTapHandler double_tap_handler;
AccelRawDataHandler raw_data_handler;
AccelRawDataHandler__deprecated raw_data_handler_deprecated;
// Configuration for our other types of events
EventServiceInfo accel_shake_info;
EventServiceInfo accel_double_tap_info;
#if LOG_DOMAIN_ACCEL
uint64_t prev_timestamp_ms;
#endif
} AccelServiceState;
//! Initialize an existing state object
void accel_service_state_init(AccelServiceState *state);
AccelServiceState* accel_service_private_get_session(PebbleTask task);
void accel_service_cleanup_task_session(PebbleTask task);
//! Create a new accel session. Used by kernel clients only. Kernel clients MUST use the
//! AccelSession based calls (accel_session_data_subscribe, etc.) to access the
//! accel service whereas apps use the accel_service based calls (accel_data_service_subscribe,
//! etc.). The accel_.*_service_.* calls are simply wrapper functions that look up the
//! AccelServiceState given the current task_id (app or worker) and then called into the respective
//! accel_session_.* call.
//! @return A non-zero session upon success, NULL if error
AccelServiceState* accel_session_create(void);
//! Delete an accel session created by accel_session_create. Used by kernel clients only.
//! @param session An Accel session created by accel_session_create()
void accel_session_delete(AccelServiceState *session);
//! Subscribe to the accelerometer shake event service by session ref. Used by kernel clients
//! only.
//! @param session An Accel session created by accel_session_create()
//! @param handler A callback to be executed on shake event
void accel_session_shake_subscribe(AccelServiceState *session, AccelTapHandler handler);
//! Unsubscribe from the accelerometer shake event service by session ref. Used by kernel clients
//! only.
//! @param session An Accel session created by accel_session_create()
void accel_session_shake_unsubscribe(AccelServiceState *session);
//! Subscribe to the accelerometer double tap event service by session ref. Used by kernel clients
//! only.
//! @param session An Accel session created by accel_session_create()
//! @param handler A callback to be executed on tap event
void accel_session_double_tap_subscribe(AccelServiceState *session, AccelTapHandler handler);
//! Unsubscribe from the accelerometer double tap event service by session ref. Used by kernel
//! clients only.
//! @param session An Accel session created by accel_session_create()
void accel_session_double_tap_unsubscribe(AccelServiceState *session);
//! Subscribe to the accelerometer data event service by session ref. Used by kernel clients
//! only.
//! @param session An accel session created by accel_session_create()
//! @param handler A callback to be executed on accelerometer data events
//! @param samples_per_update the number of samples to buffer, between 0 and 25.
void accel_session_data_subscribe(AccelServiceState *session, uint32_t samples_per_update,
AccelDataHandler handler);
//! Subscribe to the accelerometer data event service by session ref. Used by kernel clients
//! only.
//! @param session An accel session created by accel_session_create()
//! @param sampling_rate the desired sampling_rate
//! @param samples_per_update the number of samples to buffer, between 0 and 25.
//! @param handler A callback to be executed on accelerometer data events. The callback will
//! execute on the current task calling this function.
void accel_session_raw_data_subscribe(
AccelServiceState *session, AccelSamplingRate sampling_rate, uint32_t samples_per_update,
AccelRawDataHandler handler);
//! Unsubscribe from the accelerometer data event service. Used by kernel clients
//! only.
//! @param session An accel session created by accel_session_create()
void accel_session_data_unsubscribe(AccelServiceState *session);
//! Change the accelerometer sampling rate. Used by kernel clients only.
//! @param session An accel session created by accel_session_create()
//! @param rate The sampling rate in Hz (10Hz, 25Hz, 50Hz, and 100Hz possible)
int accel_session_set_sampling_rate(AccelServiceState *session, AccelSamplingRate rate);
//! Change the number of samples buffered between each accelerometer data event. Used by kernel
//! clients only.
//! @param session An accel session created by accel_session_create()
//! @param num_samples the number of samples to buffer, between 0 and 25.
int accel_session_set_samples_per_update(AccelServiceState *session, uint32_t num_samples);

284
src/fw/applib/app.c Normal file
View file

@ -0,0 +1,284 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "app.h"
#include "applib/app_heap_analytics.h"
#include "applib/graphics/graphics_private.h"
#include "applib/ui/app_window_stack.h"
#include "applib/ui/window_stack.h"
#include "applib/ui/window_private.h"
#include "mcu/fpu.h"
#include "process_management/app_manager.h"
#include "process_state/app_state/app_state.h"
#include "syscall/syscall.h"
#include "system/logging.h"
#include "system/profiler.h"
static void prv_render_app(void) {
WindowStack *stack = app_state_get_window_stack();
GContext *ctx = app_state_get_graphics_context();
if (!window_stack_is_animating(stack)) {
SYS_PROFILER_NODE_START(render_app);
window_render(app_window_stack_get_top_window(), ctx);
SYS_PROFILER_NODE_STOP(render_app);
} else {
// TODO: PBL-17645 render container layer instead of the two windows
WindowTransitioningContext *transition_context = &stack->transition_context;
if (transition_context->implementation->render) {
transition_context->implementation->render(transition_context, ctx);
}
}
*app_state_get_framebuffer_render_pending() = true;
PebbleEvent event = {
.type = PEBBLE_RENDER_READY_EVENT,
};
sys_send_pebble_event_to_kernel(&event);
}
static bool prv_window_is_render_scheduled(Window *window) {
return window && window->is_render_scheduled;
}
static bool prv_app_is_render_scheduled() {
Window *top_window = app_window_stack_get_top_window();
if (prv_window_is_render_scheduled(top_window)) {
return true;
}
WindowStack *stack = app_state_get_window_stack();
if (!window_stack_is_animating(stack)) {
return false;
}
WindowTransitioningContext *transition_ctx = &stack->transition_context;
return prv_window_is_render_scheduled(transition_ctx->window_from) ||
prv_window_is_render_scheduled(transition_ctx->window_to);
}
void app_request_render(void) {
Window *window = app_window_stack_get_top_window();
if (window) {
window_schedule_render(window);
}
}
//! Tasks that have to be done in between each event.
static NOINLINE void event_loop_upkeep(void) {
// Check to see if the most recent event caused us to pop our final window. If that's the case, we need to
// kill ourselves.
if (app_window_stack_count() == 0) {
PBL_LOG(LOG_LEVEL_DEBUG, "No more windows, killing current app");
PebbleEvent event = { .type = PEBBLE_PROCESS_KILL_EVENT, .kill = { .gracefully = true, .task=PebbleTask_App } };
sys_send_pebble_event_to_kernel(&event);
return;
}
// Check to see if handling the previous event requires us to rerender ourselves.
if (prv_app_is_render_scheduled() &&
!*app_state_get_framebuffer_render_pending()) {
prv_render_app();
}
}
static void prv_app_will_focus_handler(PebbleEvent *e, void *context) {
Window *window = app_window_stack_get_top_window();
if (e->app_focus.in_focus) {
if (window) {
// Do not call 'appear' handler on window displacing modal window
window_set_on_screen(window, true, false);
window_render(window, app_state_get_graphics_context());
}
click_manager_reset(app_state_get_click_manager());
} else if (window) {
// Do not call 'disappear' handler on window displaced by modal window
window_set_on_screen(window, false, false);
}
}
static void prv_app_button_down_handler(PebbleEvent *e, void *context) {
WindowStack *app_window_stack = app_state_get_window_stack();
if (window_stack_is_animating(app_window_stack)) {
return;
}
sys_analytics_inc(ANALYTICS_APP_METRIC_BUTTONS_PRESSED_COUNT, AnalyticsClient_App);
if (e->button.button_id == BUTTON_ID_BACK &&
!app_window_stack_get_top_window()->overrides_back_button) {
// a transition of NULL means we will use the stored pop transition for this stack item
window_stack_pop_with_transition(app_window_stack, NULL /* transition */);
return;
}
click_recognizer_handle_button_down(
&app_state_get_click_manager()->recognizers[e->button.button_id]);
}
static void prv_app_button_up_handler(PebbleEvent *e, void *context) {
if (window_stack_is_animating(app_state_get_window_stack())) {
return;
}
click_recognizer_handle_button_up(
&app_state_get_click_manager()->recognizers[e->button.button_id]);
}
// this handler is called via the legacy2_status_bar_change_event and
// will update the status bar once a minute for non-fullscreen legacy2 apps
static void prv_legacy2_status_bar_handler(PebbleEvent *e, void *context) {
Window *window = app_window_stack_get_top_window();
// only force render if we're not fullscreen
if (!window->is_fullscreen) {
// a little logic to only force update when the minute changes
ApplibInternalEventsInfo *events_info =
app_state_get_applib_internal_events_info();
struct tm currtime;
sys_localtime_r(&e->clock_tick.tick_time, &currtime);
const int minute_of_day = (currtime.tm_hour * 60) + currtime.tm_min;
if (events_info->minute_of_last_legacy2_statusbar_change != minute_of_day) {
events_info->minute_of_last_legacy2_statusbar_change = minute_of_day;
window_schedule_render(window);
}
}
}
static void prv_legacy2_status_bar_timer_subscribe(void) {
// we only need this tick event if we are a legacy2 app
if (process_manager_compiled_with_legacy2_sdk()) {
ApplibInternalEventsInfo *events_info =
app_state_get_applib_internal_events_info();
// Initialize the state for the status bar handler.
events_info->minute_of_last_legacy2_statusbar_change = -1;
events_info->legacy2_status_bar_change_event = (EventServiceInfo) {
.type = PEBBLE_TICK_EVENT,
.handler = prv_legacy2_status_bar_handler,
};
event_service_client_subscribe(
&events_info->legacy2_status_bar_change_event);
}
// NOTE: We could be super fancy and register and unregister when the fullscreen
// status changes, but it's probably not worth it as we'll be waking up once a
// minute anyway to update the face itself and it will happen as part of the same interval
}
static void prv_legacy2_status_bar_timer_unsubscribe(void) {
// we should only unsubscribe if we subscribed in the first place
if (process_manager_compiled_with_legacy2_sdk()) {
ApplibInternalEventsInfo *events_info =
app_state_get_applib_internal_events_info();
event_service_client_unsubscribe(
&events_info->legacy2_status_bar_change_event);
}
}
static void prv_app_callback_handler(PebbleEvent *e) {
e->callback.callback(e->callback.data);
}
static NOINLINE void prv_handle_deinit_event(void) {
ApplibInternalEventsInfo *events_info =
app_state_get_applib_internal_events_info();
event_service_client_unsubscribe(&events_info->will_focus_event);
event_service_client_unsubscribe(&events_info->button_down_event);
event_service_client_unsubscribe(&events_info->button_up_event);
prv_legacy2_status_bar_timer_unsubscribe(); // a no-op on sdk3+ applications
WindowStack *app_window_stack = app_state_get_window_stack();
window_stack_lock_push(app_window_stack);
window_stack_pop_all(app_window_stack, false);
window_stack_unlock_push(app_window_stack);
}
// Get the app_id for the current app or worker
// @return INSTALL_ID_INVALID if unsuccessful
AppInstallId app_get_app_id(void) {
// Only support from app or workers
PebbleTask task = pebble_task_get_current();
if ((task != PebbleTask_App) && (task != PebbleTask_Worker)) {
APP_LOG(APP_LOG_LEVEL_ERROR, "Only supported from app or worker tasks");
return INSTALL_ID_INVALID;
}
// Get the app id
if (task == PebbleTask_App) {
return sys_app_manager_get_current_app_id();
} else {
return sys_worker_manager_get_current_worker_id();
}
}
void app_event_loop_common(void) {
// Register our event handlers before we do anything else. Registering for an event requires
// an event being sent to the kernel and therefore should be done before any other events are
// generated by us to ensure we don't miss out on anything.
ApplibInternalEventsInfo *events_info =
app_state_get_applib_internal_events_info();
events_info->will_focus_event = (EventServiceInfo) {
.type = PEBBLE_APP_WILL_CHANGE_FOCUS_EVENT,
.handler = prv_app_will_focus_handler,
};
events_info->button_down_event = (EventServiceInfo) {
.type = PEBBLE_BUTTON_DOWN_EVENT,
.handler = prv_app_button_down_handler,
};
events_info->button_up_event = (EventServiceInfo) {
.type = PEBBLE_BUTTON_UP_EVENT,
.handler = prv_app_button_up_handler,
};
event_service_client_subscribe(&events_info->will_focus_event);
event_service_client_subscribe(&events_info->button_down_event);
event_service_client_subscribe(&events_info->button_up_event);
prv_legacy2_status_bar_timer_subscribe(); // a no-op on sdk3+ applications
event_loop_upkeep();
// Event loop:
while (1) {
PebbleEvent event;
sys_get_pebble_event(&event);
if (event.type == PEBBLE_PROCESS_DEINIT_EVENT) {
prv_handle_deinit_event();
// We're done here. Return the app's main function.
event_cleanup(&event);
return;
} else if (event.type == PEBBLE_CALLBACK_EVENT) {
prv_app_callback_handler(&event);
} else if (event.type == PEBBLE_RENDER_REQUEST_EVENT) {
app_request_render();
} else if (event.type == PEBBLE_RENDER_FINISHED_EVENT) {
*app_state_get_framebuffer_render_pending() = false;
} else {
event_service_client_handle_event(&event);
}
mcu_fpu_cleanup();
event_cleanup(&event);
event_loop_upkeep();
}
}
void app_event_loop(void) {
app_event_loop_common();
app_heap_analytics_log_stats_to_app_heartbeat(false /* is_rocky_app */);
}

47
src/fw/applib/app.h Normal file
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.
*/
#pragma once
#include "applib/graphics/gtypes.h"
#include "process_management/app_install_types.h"
//! @file app.h
//!
//! @addtogroup Foundation
//! @{
//! @addtogroup App
//! @{
//! @internal
//! Requests the app to re-render by scheduling its top window to be rendered.
void app_request_render(void);
//! @internal
//! Event loop that is shared between Rocky.js and C apps. This is called by both app_event_loop()
//! as well as rocky_event_loop_with_...().
void app_event_loop_common(void);
//! The event loop for C apps, to be used in app's main().
//! Will block until the app is ready to exit.
void app_event_loop(void);
//! @internal
//! Get the AppInstallId for the current app or worker
AppInstallId app_get_app_id(void);
//! @} // end addtogroup App
//! @} // end addtogroup Foundation

28
src/fw/applib/app_comm.c Normal file
View file

@ -0,0 +1,28 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "app_comm.h"
#include "syscall/syscall.h"
void app_comm_set_sniff_interval(const SniffInterval interval) {
sys_app_comm_set_responsiveness(interval);
}
SniffInterval app_comm_get_sniff_interval(void) {
return sys_app_comm_get_sniff_interval();
}

61
src/fw/applib/app_comm.h Normal file
View file

@ -0,0 +1,61 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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
//! @addtogroup Foundation
//! @{
//! @addtogroup AppComm App Communication
//! \brief API for interacting with the Pebble communication subsystem.
//!
//! @note To send messages to a remote device, see the \ref AppMessage or
//! \ref AppSync modules.
//! @{
//! Intervals during which the Bluetooth module may enter a low power mode.
//! The sniff interval defines the period during which the Bluetooth module may
//! not exchange (ACL) packets. The longer the sniff interval, the more time the
//! Bluetooth module may spend in a low power mode.
//! It may be necessary to reduce the sniff interval if an app requires reduced
//! latency when sending messages.
//! @note These settings have a dramatic effect on the Pebble's energy
//! consumption. Use the normal sniff interval whenever possible.
//! Note, however, that switching between modes increases power consumption
//! during the process. Frequent switching between modes is thus
//! discouraged. Ensure you do not drop to normal frequently. The Bluetooth module
//! is a major consumer of the Pebble's energy.
typedef enum {
//! Set the sniff interval to normal (power-saving) mode
SNIFF_INTERVAL_NORMAL = 0,
//! Reduce the sniff interval to increase the responsiveness of the radio at
//! the expense of increasing Bluetooth energy consumption by a multiple of 2-5
//! (very significant)
SNIFF_INTERVAL_REDUCED = 1,
} SniffInterval;
//! Set the Bluetooth module's sniff interval.
//! The sniff interval will be restored to normal by the OS after the app's
//! de-init handler is called. Set the sniff interval to normal whenever
//! possible.
void app_comm_set_sniff_interval(const SniffInterval interval);
//! Get the Bluetooth module's sniff interval
//! @return The SniffInterval value corresponding to the current interval
SniffInterval app_comm_get_sniff_interval(void);
//! @} // end addtogroup AppComm
//! @} // end addtogroup Foundation

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.
*/
#include "app_exit_reason.h"
#include "syscall/syscall.h"
AppExitReason app_exit_reason_get(void) {
return sys_process_get_exit_reason();
}
void app_exit_reason_set(AppExitReason exit_reason) {
sys_process_set_exit_reason(exit_reason);
}

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
//! @addtogroup Foundation
//! @{
//! @addtogroup ExitReason Exit Reason
//! \brief API for the application to notify the system of the reason it will exit.
//!
//! If the application has not specified an exit reason before it exits, then the exit reason will
//! default to APP_EXIT_NOT_SPECIFIED.
//!
//! Only an application can set its exit reason. The system will not modify it.
//!
//! @{
//! AppExitReason is used to notify the system of the reason of an application exiting, which may
//! affect the part of the system UI that is presented after the application terminates.
//! @internal
//! New exit reasons may be added in the future. As a best practice, it is recommended to only
//! handle the cases needed, rather than trying to handle all possible exit reasons.
typedef enum AppExitReason {
APP_EXIT_NOT_SPECIFIED = 0, //!< Exit reason not specified
APP_EXIT_ACTION_PERFORMED_SUCCESSFULLY, //!< Application performed an action when it exited
NUM_EXIT_REASONS //!< Number of AppExitReason options
} AppExitReason;
//! Returns the current app exit reason.
//! @return The current app exit reason
AppExitReason app_exit_reason_get(void);
//! Set the app exit reason to a new reason.
//! @param reason The new app exit reason
void app_exit_reason_set(AppExitReason exit_reason);
//! @} // group ExitReason
//! @} // group Foundation

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.
*/
#include "app_focus_service.h"
#include "event_service_client.h"
#include "process_state/app_state/app_state.h"
#include "services/common/event_service.h"
static void prv_focus_event_handler(PebbleEvent *e, void *context) {
AppFocusState *state = app_state_get_app_focus_state();
bool in_focus = e->app_focus.in_focus;
if (e->type == PEBBLE_APP_WILL_CHANGE_FOCUS_EVENT &&
state->handlers.will_focus) {
state->handlers.will_focus(in_focus);
} else if (e->type == PEBBLE_APP_DID_CHANGE_FOCUS_EVENT &&
state->handlers.did_focus) {
state->handlers.did_focus(in_focus);
}
}
void app_focus_service_subscribe_handlers(AppFocusHandlers handlers) {
AppFocusState *state = app_state_get_app_focus_state();
app_focus_service_unsubscribe();
if (handlers.did_focus) {
// NOTE: the individual fields of state->did_focus_info are assigned
// to instead of writing
// state->did_focus_info = (EventServiceInfo) { ... }
// as the latter would zero out the ListNode embedded in the struct.
// Doing so would corrupt the events list if the event was already
// subscribed.
state->did_focus_info.type = PEBBLE_APP_DID_CHANGE_FOCUS_EVENT;
state->did_focus_info.handler = prv_focus_event_handler;
event_service_client_subscribe(&state->did_focus_info);
}
if (handlers.will_focus) {
state->will_focus_info.type = PEBBLE_APP_WILL_CHANGE_FOCUS_EVENT;
state->will_focus_info.handler = prv_focus_event_handler;
event_service_client_subscribe(&state->will_focus_info);
}
state->handlers = handlers;
}
void app_focus_service_subscribe(AppFocusHandler handler) {
AppFocusHandlers handlers = (AppFocusHandlers) { .will_focus = handler };
app_focus_service_subscribe_handlers(handlers);
}
void app_focus_service_unsubscribe(void) {
AppFocusState *state = app_state_get_app_focus_state();
if (state->handlers.will_focus) {
event_service_client_unsubscribe(&state->will_focus_info);
}
if (state->handlers.did_focus) {
event_service_client_unsubscribe(&state->did_focus_info);
}
state->handlers = (AppFocusHandlers) {};
}

View file

@ -0,0 +1,96 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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>
//! @addtogroup Foundation
//! @{
//! @addtogroup EventService
//! @{
//! @addtogroup AppFocusService
//!
//!
//! \brief Handling app focus
//! The AppFocusService allows developers to be notified when their apps become visible on the
//! screen. Common reasons your app may be running but not be on screen are: it's still in the
//! middle of launching and being revealed by a system animation, or it is being covered by a system
//! window such as a notification. This service is useful for apps that require a high degree of
//! user interactivity, like a game where you'll want to pause when a notification covers your app
//! window. It can be also used for apps that want to sync up an intro animation to the end of the
//! system animation that occurs before your app is visible.
//!
//! @{
//! Callback type for focus events
//! @param in_focus True if the app is gaining focus, false otherwise.
typedef void (*AppFocusHandler)(bool in_focus);
//! There are two different focus events which take place when transitioning to and from an app
//! being in focus. Below is an example of when these events will occur:
//! 1) The app is launched. Once the system animation to the app has completed and the app is
//! completely in focus, the did_focus handler is called with in_focus set to true.
//! 2) A notification comes in and the animation to show the notification starts. The will_focus
//! handler is called with in_focus set to false.
//! 3) The animation completes and the notification is in focus, with the app being completely
//! covered. The did_focus hander is called with in_focus set to false.
//! 4) The notification is dismissed and the animation to return to the app starts. The will_focus
//! handler is called with in_focus set to true.
//! 5) The animation completes and the app is in focus. The did_focus handler is called with
//! in_focus set to true.
typedef struct {
//! Handler which will be called right before an app will lose or gain focus.
//! @note This will be called with in_focus set to true when a window which is covering the app is
//! about to close and return focus to the app.
//! @note This will be called with in_focus set to false when a window which will cover the app is
//! about to open, causing the app to lose focus.
AppFocusHandler will_focus;
//! Handler which will be called when an animation finished which has put the app into focus or
//! taken the app out of focus.
//! @note This will be called with in_focus set to true when a window which was covering the app
//! has closed and the app has gained focus.
//! @note This will be called with in_focus set to false when a window has opened which is now
//! covering the app, causing the app to lose focus.
AppFocusHandler did_focus;
} AppFocusHandlers;
//! Subscribe to the focus event service. Once subscribed, the handlers get called every time the
//! app gains or loses focus.
//! @param handler Handlers which will be called on will-focus and did-focus events.
//! @see AppFocusHandlers
void app_focus_service_subscribe_handlers(AppFocusHandlers handlers);
//! Subscribe to the focus event service. Once subscribed, the handler
//! gets called every time the app focus changes.
//! @note Calling this function is equivalent to
//! \code{.c}
//! app_focus_service_subscribe_handlers((AppFocusHandlers){
//! .will_focus = handler,
//! });
//! \endcode
//! @note Out focus events are triggered when a modal window is about to open and cover the app.
//! @note In focus events are triggered when a modal window which is covering the app is about to
//! close.
//! @param handler A callback to be called on will-focus events.
void app_focus_service_subscribe(AppFocusHandler handler);
//! Unsubscribe from the focus event service. Once unsubscribed, the previously
//! registered handlers will no longer be called.
void app_focus_service_unsubscribe(void);
//! @} // end addtogroup AppFocusService
//! @} // end addtogroup EventService
//! @} // end addtogroup Foundation

137
src/fw/applib/app_glance.c Normal file
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.
*/
#include "app_glance.h"
#include "applib/template_string.h"
#include "apps/system_apps/launcher/default/launcher_app_glance_generic.h"
#include "process_state/app_state/app_state.h"
#include "resource/resource_ids.auto.h"
#include "services/normal/blob_db/app_glance_db_private.h"
#include "services/normal/timeline/attribute.h"
#include "services/normal/timeline/timeline_resources.h"
#include "syscall/syscall.h"
#include "util/string.h"
#include "util/uuid.h"
AppGlanceResult app_glance_add_slice(AppGlanceReloadSession *session, AppGlanceSlice slice) {
#if !CAPABILITY_HAS_APP_GLANCES
return APP_GLANCE_RESULT_INVALID_SESSION;
#endif
AppGlanceResult result = APP_GLANCE_RESULT_SUCCESS;
AppGlance *glance = app_state_get_glance();
// We use the glance set in the session as a crude way of validating the session
// Bail out if the session is invalid
if (!session || (glance != session->glance)) {
result |= APP_GLANCE_RESULT_INVALID_SESSION;
return result;
}
// From here on to the end of the function we can accumulate multiple failures in `result`
// Check if this slice would put us over the max slices per glance
if (glance->num_slices >= APP_GLANCE_DB_MAX_SLICES_PER_GLANCE) {
result |= APP_GLANCE_RESULT_SLICE_CAPACITY_EXCEEDED;
}
// Check if this slice's icon is a valid resource (but only if it's not the value that specifies
// that the app's default icon should be used)
if (slice.layout.icon != APP_GLANCE_SLICE_DEFAULT_ICON) {
Uuid app_uuid;
sys_get_app_uuid(&app_uuid);
const TimelineResourceInfo timeline_resource_info = (TimelineResourceInfo) {
.app_id = &app_uuid,
.res_id = (TimelineResourceId)slice.layout.icon,
};
AppResourceInfo app_resource_info = (AppResourceInfo) {};
sys_timeline_resources_get_id(&timeline_resource_info,
LAUNCHER_APP_GLANCE_GENERIC_ICON_SIZE_TYPE, &app_resource_info);
if (app_resource_info.res_id == RESOURCE_ID_INVALID) {
result |= APP_GLANCE_RESULT_INVALID_ICON;
}
}
// Check if the template string is too long (if it's not NULL)
// Plus one for the null terminator, then plus one more to become too long
const size_t template_string_too_long_size = ATTRIBUTE_APP_GLANCE_SUBTITLE_MAX_LEN + 1 + 1;
if (slice.layout.subtitle_template_string &&
strnlen(slice.layout.subtitle_template_string,
template_string_too_long_size) == template_string_too_long_size) {
result |= APP_GLANCE_RESULT_TEMPLATE_STRING_TOO_LONG;
}
// Check if the provided subtitle string is a valid template string, if it's present.
const time_t current_time = sys_get_time();
if (slice.layout.subtitle_template_string) {
const TemplateStringVars template_string_vars = (TemplateStringVars) {
.current_time = current_time,
};
TemplateStringError template_string_error = {0};
if (!template_string_evaluate(slice.layout.subtitle_template_string, NULL, 0, NULL,
&template_string_vars, &template_string_error)) {
result |= APP_GLANCE_RESULT_INVALID_TEMPLATE_STRING;
}
}
// Check if the slice would expire in the past (if it's not APP_GLANCE_SLICE_NO_EXPIRATION)
if ((slice.expiration_time != APP_GLANCE_SLICE_NO_EXPIRATION) &&
slice.expiration_time <= current_time) {
result |= APP_GLANCE_RESULT_EXPIRES_IN_THE_PAST;
}
// If we haven't failed at this point, we're ready to add the slice to the glance!
if (result == APP_GLANCE_RESULT_SUCCESS) {
AppGlanceSliceInternal *slice_dest = &glance->slices[glance->num_slices];
*slice_dest = (AppGlanceSliceInternal) {
.expiration_time = slice.expiration_time,
.type = AppGlanceSliceType_IconAndSubtitle,
.icon_and_subtitle.icon_resource_id = slice.layout.icon,
};
// We already checked that the provided template string fits, so use strcpy instead of strncpy
if (slice.layout.subtitle_template_string) {
strcpy(slice_dest->icon_and_subtitle.template_string, slice.layout.subtitle_template_string);
}
glance->num_slices++;
}
return result;
}
void app_glance_reload(AppGlanceReloadCallback callback, void *context) {
#if !CAPABILITY_HAS_APP_GLANCES
return;
#endif
AppGlance *glance = app_state_get_glance();
Uuid current_app_uuid;
sys_get_app_uuid(&current_app_uuid);
// Zero out the glance
app_glance_service_init_glance(glance);
if (callback) {
// Create a "reload session" on the stack that wraps the glance, and then call the user callback
AppGlanceReloadSession session = (AppGlanceReloadSession) {
.glance = glance,
};
callback(&session, APP_GLANCE_DB_MAX_SLICES_PER_GLANCE, context);
}
sys_app_glance_update(&current_app_uuid, glance);
}

106
src/fw/applib/app_glance.h Normal file
View file

@ -0,0 +1,106 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "services/normal/app_glances/app_glance_service.h"
#include "util/time/time.h"
#include <stdint.h>
//! @addtogroup Foundation
//! @{
//! @addtogroup AppGlance App Glance
//! \brief API for the application to modify its glance.
//!
//! @{
//! The ID of a published app resource defined within the publishedMedia section of package.json.
typedef uint32_t PublishedId;
//! Can be used for the expiration_time of an \ref AppGlanceSlice so that the slice never expires.
#define APP_GLANCE_SLICE_NO_EXPIRATION ((time_t)0)
//! Can be used for the icon of an \ref AppGlanceSlice so that the slice displays the app's default
//! icon.
#define APP_GLANCE_SLICE_DEFAULT_ICON ((PublishedId)0)
//! An app's glance can change over time as defined by zero or more app glance slices that each
//! describe the state of the app glance at a particular point in time. Slices are displayed in the
//! order they are added, and they are removed at the specified expiration time.
typedef struct AppGlanceSlice {
//! Describes how the slice should be visualized in the app's glance in the launcher.
struct {
//! The published resource ID of the bitmap icon to display in the app's glance. Use \ref
//! APP_GLANCE_SLICE_DEFAULT_ICON to use the app's default bitmap icon.
PublishedId icon;
//! A template string to visualize in the app's glance. The string will be copied, so it is safe
//! to destroy after adding the slice to the glance. Use NULL if no string should be displayed.
const char *subtitle_template_string;
} layout;
//! The UTC time after which this slice should no longer be shown in the app's glance. Use \ref
//! APP_GLANCE_SLICE_NO_EXPIRATION if the slice should never expire.
time_t expiration_time;
} AppGlanceSlice;
//! Bitfield enum describing the result of trying to add an AppGlanceSlice to an app's glance.
typedef enum AppGlanceResult {
//! The slice was successfully added to the app's glance.
APP_GLANCE_RESULT_SUCCESS = 0,
//! The subtitle_template_string provided in the slice was invalid.
APP_GLANCE_RESULT_INVALID_TEMPLATE_STRING = 1 << 0,
//! The subtitle_template_string provided in the slice was longer than 150 bytes.
APP_GLANCE_RESULT_TEMPLATE_STRING_TOO_LONG = 1 << 1,
//! The icon provided in the slice was invalid.
APP_GLANCE_RESULT_INVALID_ICON = 1 << 2,
//! The provided slice would exceed the app glance's slice capacity.
APP_GLANCE_RESULT_SLICE_CAPACITY_EXCEEDED = 1 << 3,
//! The expiration_time provided in the slice expires in the past.
APP_GLANCE_RESULT_EXPIRES_IN_THE_PAST = 1 << 4,
//! The \ref AppGlanceReloadSession provided was invalid.
APP_GLANCE_RESULT_INVALID_SESSION = 1 << 5,
} AppGlanceResult;
//! A session variable that is provided in an \ref AppGlanceReloadCallback and must be used when
//! adding slices to the app's glance via \ref app_glance_add_slice.
typedef struct AppGlanceReloadSession AppGlanceReloadSession;
struct AppGlanceReloadSession {
AppGlance *glance;
};
//! Add a slice to the app's glance. This function will only succeed if called with a valid
//! \ref AppGlanceReloadSession that is provided in an \ref AppGlanceReloadCallback.
//! @param session The session variable provided in an \ref AppGlanceReloadCallback
//! @param slice The slice to add to the app's glance
//! @return The result of trying to add the slice to the app's glance
AppGlanceResult app_glance_add_slice(AppGlanceReloadSession *session, AppGlanceSlice slice);
//! User-provided callback for reloading the slices in the app's glance.
//! @param session A session variable that must be passed to \ref app_glance_add_slice when adding
//! slices to the app's glance; it becomes invalid when the \ref AppGlanceReloadCallback returns
//! @param limit The number of entries that can be added to the app's glance
//! @param context User-provided context provided when calling \ref app_glance_reload()
typedef void (*AppGlanceReloadCallback)(AppGlanceReloadSession *session, size_t limit,
void *context);
//! Clear any existing slices in the app's glance and trigger a reload via the provided callback.
//! @param callback A function that will be called to add new slices to the app's glance; even if
//! the provided callback is NULL, any existing slices will still be cleared from the app's glance
//! @param context User-provided context that will be passed to the callback
void app_glance_reload(AppGlanceReloadCallback callback, void *context);
//! @} // group AppGlance
//! @} // group Foundation

View file

@ -0,0 +1,87 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "app_heap_analytics.h"
#include "kernel/pbl_malloc.h"
#include "kernel/pebble_tasks.h"
#include "services/common/analytics/analytics_event.h"
#include "syscall/syscall.h"
#if CAPABILITY_HAS_JAVASCRIPT && !RECOVERY_FW
#include "jmem-heap.h"
#endif
#include <util/heap.h>
static bool prv_is_current_task_app_or_worker(void) {
switch (pebble_task_get_current()) {
case PebbleTask_App:
case PebbleTask_Worker:
return true;
default:
return false;
}
}
void app_heap_analytics_log_native_heap_oom_fault(size_t requested_size, Heap *heap) {
if (!prv_is_current_task_app_or_worker()) {
return;
}
const uint32_t total_size = heap_size(heap);
unsigned int used = 0;
unsigned int total_free = 0;
unsigned int largest_free_block = 0;
heap_calc_totals(heap, &used, &total_free, &largest_free_block);
analytics_event_app_oom(AnalyticsEvent_AppOOMNative, requested_size,
total_size, total_free, largest_free_block);
}
void app_heap_analytics_log_rocky_heap_oom_fault(void) {
#if CAPABILITY_HAS_JAVASCRIPT && !RECOVERY_FW
if (!prv_is_current_task_app_or_worker()) {
return;
}
jmem_heap_stats_t jerry_mem_stats = {};
jmem_heap_get_stats(&jerry_mem_stats);
const size_t requested_size = 0; // not available unfortunately
const size_t free = (jerry_mem_stats.size - jerry_mem_stats.allocated_bytes);
analytics_event_app_oom(AnalyticsEvent_AppOOMRocky, requested_size,
jerry_mem_stats.size, free, jerry_mem_stats.largest_free_block_bytes);
#endif
}
void app_heap_analytics_log_stats_to_app_heartbeat(bool is_rocky_app) {
Heap *const heap = task_heap_get_for_current_task();
sys_analytics_max(ANALYTICS_APP_METRIC_MEM_NATIVE_HEAP_SIZE, heap_size(heap),
AnalyticsClient_CurrentTask);
sys_analytics_max(ANALYTICS_APP_METRIC_MEM_NATIVE_HEAP_PEAK, heap->high_water_mark,
AnalyticsClient_CurrentTask);
#if CAPABILITY_HAS_JAVASCRIPT && !RECOVERY_FW
if (is_rocky_app) {
jmem_heap_stats_t jerry_mem_stats = {};
jmem_heap_get_stats(&jerry_mem_stats);
sys_analytics_max(ANALYTICS_APP_METRIC_MEM_ROCKY_HEAP_PEAK,
jerry_mem_stats.global_peak_allocated_bytes,
AnalyticsClient_CurrentTask);
sys_analytics_max(ANALYTICS_APP_METRIC_MEM_ROCKY_HEAP_WASTE,
jerry_mem_stats.global_peak_waste_bytes,
AnalyticsClient_CurrentTask);
}
#endif
}

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 <stdbool.h>
#include <stddef.h>
typedef struct Heap Heap;
//! Logs an analytics event about the native heap OOM fault.
void app_heap_analytics_log_native_heap_oom_fault(size_t requested_size, Heap *heap);
//! Logs an analytics event about the JerryScript heap OOM fault.
void app_heap_analytics_log_rocky_heap_oom_fault(void);
//! Captures native heap and given JerryScript heap stats to the app heartbeat.
//! @param jerry_mem_stats JerryScript heap stats on NULL if not available.
void app_heap_analytics_log_stats_to_app_heartbeat(bool is_rocky_app);

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 "app_heap_util.h"
#include "process_state/app_state/app_state.h"
#include "process_state/worker_state/worker_state.h"
#include "kernel/pebble_tasks.h"
#include "system/passert.h"
#include "util/heap.h"
static Heap* get_task_heap(void) {
PebbleTask task = pebble_task_get_current();
Heap *heap = NULL;
if (task == PebbleTask_App) {
heap = app_state_get_heap();
} else if (task == PebbleTask_Worker) {
heap = worker_state_get_heap();
} else {
WTF;
}
return heap;
}
size_t heap_bytes_used(void) {
Heap *heap = get_task_heap();
return heap->current_size;
}
size_t heap_bytes_free(void) {
Heap *heap = get_task_heap();
return heap_size(heap) - heap->current_size;
}

View file

@ -0,0 +1,37 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 <stddef.h>
//! @addtogroup Foundation
//! @{
//! @addtogroup MemoryManagement Memory Management
//! \brief Utility functions for managing an application's memory.
//!
//! @{
//! Calculates the number of bytes of heap memory currently being used by the application.
//! @return The number of bytes on the heap currently being used.
size_t heap_bytes_used(void);
//! Calculates the number of bytes of heap memory \a not currently being used by the application.
//! @return The number of bytes on the heap not currently being used.
size_t heap_bytes_free(void);
//! @} // end addtogroup MemoryManagement
//! @} // end addtogroup Foundation

60
src/fw/applib/app_inbox.c Normal file
View file

@ -0,0 +1,60 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "app_inbox.h"
#include "applib/applib_malloc.auto.h"
#include "services/normal/app_inbox_service.h"
#include "syscall/syscall.h"
#include "system/logging.h"
#include "system/passert.h"
AppInbox *app_inbox_create_and_register(size_t buffer_size, uint32_t min_num_messages,
AppInboxMessageHandler message_handler,
AppInboxDroppedHandler dropped_handler) {
if (!message_handler) {
return NULL;
}
if (!buffer_size) {
return NULL;
}
if (!min_num_messages) {
return NULL;
}
buffer_size += (min_num_messages * sizeof(AppInboxMessageHeader));
uint8_t *buffer = applib_zalloc(buffer_size);
if (!buffer) {
PBL_LOG(LOG_LEVEL_ERROR, "Not enough memory to allocate App Inbox of size %"PRIu32,
(uint32_t)buffer_size);
return NULL;
}
if (!sys_app_inbox_service_register(buffer, buffer_size, message_handler, dropped_handler)) {
applib_free(buffer);
return NULL;
}
return (AppInbox *) buffer;
}
uint32_t app_inbox_destroy_and_deregister(AppInbox *app_inbox) {
uint8_t *buffer = (uint8_t *)app_inbox;
uint32_t num_messages_lost = sys_app_inbox_service_unregister(buffer);
applib_free(buffer);
return num_messages_lost;
}
void app_inbox_consume(AppInboxConsumerInfo *consumer_info) {
sys_app_inbox_service_consume(consumer_info);
}

59
src/fw/applib/app_inbox.h Normal file
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 <stddef.h>
#include <stdint.h>
typedef struct AppInboxConsumerInfo AppInboxConsumerInfo;
//! @param data Pointer to the available data.
//! @param length The length of the available data.
//! @param consumer_info Opaque context object to be passed into app_inbox_consume(). It is NOT
//! mandatory for the handler to call app_inbox_consume().
typedef void (*AppInboxMessageHandler)(const uint8_t *data, size_t length,
AppInboxConsumerInfo *consumer_info);
typedef void (*AppInboxDroppedHandler)(uint32_t num_dropped_messages);
//! Opaque reference to an app inbox.
typedef struct AppInbox AppInbox;
//! @param min_num_messages The minimum number of messages that the inbox should be able to hold
//! if the total payload size is exactly buffer_size. This is used to calculate how much additional
//! buffer space has to be allocated for message header overhead.
//! @param message_handler The callback that will handle received messages (required).
//! @param dropped_handler The callback that will handle dropped messages (optional, use NULL if
//! not needed).
//! @note The system only allows certain handlers, see prv_tag_for_event_handlers()
//! in app_inbox_service.c.
//! @return An opaque value that can be used with app_inbox_destroy_and_deregister, or NULL
//! if the process failed.
AppInbox *app_inbox_create_and_register(size_t buffer_size, uint32_t min_num_messages,
AppInboxMessageHandler message_handler,
AppInboxDroppedHandler dropped_handler);
//! @param app_inbox_ref The app inbox to destroy, pass in the value that
//! app_inbox_create_and_register returned.
//! @return The number of messages that were dropped, plus the ones that were still waiting
//! to be consumed.
uint32_t app_inbox_destroy_and_deregister(AppInbox *app_inbox_ref);
//! Call this function from a AppInboxMessageHandler to immediately consume the message and free
//! up the space in the buffer that was occupied by the message.
//! @param consume_info The opaque context object as passed into the AppInboxMessageHandler.
void app_inbox_consume(AppInboxConsumerInfo *consume_info);

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.
*/
#include "app_launch_button.h"
#include "syscall/syscall.h"
ButtonId app_launch_button(void) {
return sys_process_get_launch_button();
}

View file

@ -0,0 +1,24 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "drivers/button_id.h"
//! @internal
//! Get the button id used to launch the app.
//! Only valid if the launch reason is APP_LAUNCH_USER or APP_LAUNCH_QUICK_LAUNCH.
ButtonId app_launch_button(void);

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.
*/
#include "app_launch_reason.h"
#include "process_state/app_state/app_state.h"
#include "syscall/syscall.h"
AppLaunchReason app_launch_reason(void) {
return sys_process_get_launch_reason();
}
uint32_t app_launch_get_args(void) {
return sys_process_get_launch_args();
}

View file

@ -0,0 +1,61 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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>
//! @addtogroup Foundation
//! @{
//! @addtogroup LaunchReason Launch Reason
//! \brief API for checking what caused the application to launch.
//!
//! This includes the system, launch by user interaction (User selects the
//! application from the launcher menu),
//! launch by the mobile or a mobile companion application,
//! or launch by a scheduled wakeup event for the specified application.
//!
//! @{
//! AppLaunchReason is used to inform the application about how it was launched
//! @note New launch reasons may be added in the future. As a best practice, it
//! is recommended to only handle the cases that the app needs to know about,
//! rather than trying to handle all possible launch reasons.
typedef enum {
APP_LAUNCH_SYSTEM = 0, //!< App launched by the system
APP_LAUNCH_USER, //!< App launched by user selection in launcher menu
APP_LAUNCH_PHONE, //!< App launched by mobile or companion app
APP_LAUNCH_WAKEUP, //!< App launched by wakeup event
APP_LAUNCH_WORKER, //!< App launched by worker calling worker_launch_app()
APP_LAUNCH_QUICK_LAUNCH, //!< App launched by user using quick launch
APP_LAUNCH_TIMELINE_ACTION, //!< App launched by user opening it from a pin
APP_LAUNCH_SMARTSTRAP, //!< App launched by a smartstrap
} AppLaunchReason;
//! Provides the method used to launch the current application.
//! @return The method or reason the current application was launched
AppLaunchReason app_launch_reason(void);
//! Get the argument passed to the app when it was launched.
//! @note Currently the only way to pass arguments to apps is by using an openWatchApp action
//! on a pin.
//! @return The argument passed to the app, or 0 if the app wasn't launched from a Launch App action
uint32_t app_launch_get_args(void);
//! @} // group Launch_Reason
//! @} // group Foundation

25
src/fw/applib/app_light.c Normal file
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.
*/
#include "syscall/syscall.h"
void app_light_enable_interaction(void) {
sys_light_enable_interaction();
}
void app_light_enable(bool enable) {
sys_light_enable(enable);
}

44
src/fw/applib/app_light.h Normal file
View file

@ -0,0 +1,44 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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>
//! @file light.h
//! @addtogroup UI
//! @{
//! @addtogroup Light Light
//! \brief Controlling Pebble's backlight
//!
//! The Light API provides you with functions to turn on Pebbles backlight or
//! put it back into automatic control. You can trigger the backlight and schedule a timer
//! to automatically disable the backlight after a short delay, which is the preferred
//! method of interacting with the backlight.
//! @{
//! Trigger the backlight and schedule a timer to automatically disable the backlight
//! after a short delay. This is the preferred method of interacting with the backlight.
void app_light_enable_interaction(void);
//! Turn the watch's backlight on or put it back into automatic control.
//! Developers should take care when calling this function, keeping Pebble's backlight on for long periods of time
//! will rapidly deplete the battery.
//! @param enable Turn the backlight on if `true`, otherwise `false` to put it back into automatic control.
void app_light_enable(bool enable);
//! @} // group Light
//! @} // group UI

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 "applib/app_logging.h"
#include <stdint.h>
#include <stddef.h>
// FIXME PBL-1629: move needed declarations into applib
#include "syscall/syscall.h"
#include "process_management/app_manager.h"
#include "services/common/comm_session/session.h"
#include "system/logging.h"
#include "system/passert.h"
void app_log_vargs(uint8_t log_level, const char* src_filename, int src_line_number, const char* fmt, va_list args) {
char log_buffer[LOG_BUFFER_LENGTH];
_Static_assert(sizeof(log_buffer) > sizeof(AppLogBinaryMessage), "log_buffer too small for AppLogBinaryMessage");
AppLogBinaryMessage* msg = (AppLogBinaryMessage*)log_buffer;
sys_get_app_uuid(&msg->uuid);
const size_t log_msg_offset = offsetof(AppLogBinaryMessage, log_msg);
int bin_msg_length = pbl_log_binary_format((char*)&msg->log_msg, sizeof(log_buffer) - log_msg_offset, log_level,
src_filename, src_line_number, fmt, args);
sys_app_log(log_msg_offset + bin_msg_length, log_buffer);
}
void app_log(uint8_t log_level, const char* src_filename, int src_line_number, const char* fmt, ...) {
va_list args;
va_start(args, fmt);
app_log_vargs(log_level, src_filename, src_line_number, fmt, args);
va_end(args);
}

101
src/fw/applib/app_logging.h Normal file
View file

@ -0,0 +1,101 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*!
@file app_logging.h
@brief Interface for the SDK's App Logging API.
*/
#pragma once
#include "util/uuid.h"
// FIXME PBL-1629: move needed declarations into applib
#include "kernel/logging_private.h"
#include "system/logging.h"
#include <stdint.h>
//! @addtogroup Foundation
//! @{
//! @addtogroup Logging Logging
//! \brief Functions related to logging from apps.
//!
//! This module contains the functions necessary to log messages through
//! Bluetooth.
//! @note It is no longer necessary to enable app logging output from the "settings->about" menu on the Pebble for
//! them to be transmitted! Instead use the "pebble logs" command included with the SDK to activate logs. The logs
//! will appear right in your console. Logging
//! over Bluetooth is a fairly power hungry operation that non-developers will
//! not need when your apps are distributed.
//! @{
// @internal
// Log an app message, takes a va_list rather than varags
// @see app_log
void app_log_vargs(uint8_t log_level, const char *src_filename, int src_line_number,
const char *fmt, va_list args);
//! Log an app message.
//! @param log_level
//! @param src_filename The source file where the log originates from
//! @param src_line_number The line number in the source file where the log originates from
//! @param fmt A C formatting string
//! @param ... The arguments for the formatting string
//! @param log_level
//! \sa snprintf for details about the C formatting string.
#if __clang__
void app_log(uint8_t log_level, const char* src_filename, int src_line_number, const char* fmt,
...);
#else
void app_log(uint8_t log_level, const char* src_filename, int src_line_number, const char* fmt,
...) __attribute__((format(printf, 4, 5)));
#endif
//! A helper macro that simplifies the use of the app_log function
//! @param level The log level to log output as
//! @param fmt A C formatting string
//! @param args The arguments for the formatting string
#define APP_LOG(level, fmt, args...) \
app_log(level, __FILE_NAME__, __LINE__, fmt, ## args)
//! Suggested log level values
typedef enum {
//! Error level log message
APP_LOG_LEVEL_ERROR = 1,
//! Warning level log message
APP_LOG_LEVEL_WARNING = 50,
//! Info level log message
APP_LOG_LEVEL_INFO = 100,
//! Debug level log message
APP_LOG_LEVEL_DEBUG = 200,
//! Verbose Debug level log message
APP_LOG_LEVEL_DEBUG_VERBOSE = 255,
} AppLogLevel;
//! @}
//! @}
typedef enum AppLoggingMode {
AppLoggingDisabled = 0,
AppLoggingEnabled = 1,
NumAppLoggingModes
} AppLoggingMode;
typedef struct __attribute__((__packed__)) AppLogBinaryMessage {
Uuid uuid;
LogBinaryMessage log_msg;
} AppLogBinaryMessage;

View file

@ -0,0 +1,240 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "applib/app_message/app_message.h"
#include "applib/app_message/app_message_internal.h"
#include "kernel/pbl_malloc.h"
#include "process_state/app_state/app_state.h"
#include "services/common/comm_session/protocol.h"
#include "syscall/syscall.h"
#include "system/logging.h"
// -------- Initialization ---------------------------------------------------------------------- //
void app_message_init(void) {
AppMessageCtx *app_message_ctx = app_state_get_app_message_ctx();
*app_message_ctx = (const AppMessageCtx) {};
}
// -------- Pebble Protocol Handlers ------------------------------------------------------------ //
static bool prv_has_invalid_header_length(size_t length) {
if (length < sizeof(AppMessageHeader)) {
PBL_LOG(LOG_LEVEL_ERROR, "Too short");
return true;
}
return false;
}
//! The new implementation uses up to 72 bytes more stack space than the previous implementation.
//! Might need to do some extra work to get a "thinner" stack, if this causes issues.
//! Executes on App task.
void app_message_app_protocol_msg_callback(CommSession *session,
const uint8_t* data, size_t length,
AppInboxConsumerInfo *consumer_info) {
if (prv_has_invalid_header_length(length)) {
return;
}
AppMessageHeader *message = (AppMessageHeader *) data;
switch (message->command) {
case CMD_PUSH:
// Incoming message:
app_message_inbox_receive(session, (AppMessagePush *) message, length, consumer_info);
return;
case CMD_REQUEST:
// Incoming request for an update push:
// TODO PBL-1636: decide to implement CMD_REQUEST, or remove it
return;
case CMD_ACK:
case CMD_NACK:
// Received ACK/NACK in response to previously pushed update:
app_message_out_handle_ack_nack_received(message);
return;
default:
PBL_LOG(LOG_LEVEL_ERROR, "Unknown Cmd 0x%x", message->command);
return;
}
}
//! Executes on KernelBG, sends back NACK on behalf of the app if it is not able to do so.
//! Note that app_message_receiver_dropped_handler will also get called on the App task,
//! to report the number of missed messages.
void app_message_app_protocol_system_nack_callback(CommSession *session,
const uint8_t* data, size_t length) {
if (prv_has_invalid_header_length(length)) {
return;
}
AppMessageHeader *message = (AppMessageHeader *) data;
if (message->command != CMD_PUSH) {
return;
}
app_message_inbox_send_ack_nack_reply(session, message->transaction_id, CMD_NACK);
}
// -------- Developer Interface ----------------------------------------------------------------- //
void *app_message_get_context(void) {
return app_state_get_app_message_ctx()->inbox.user_context;
}
void *app_message_set_context(void *context) {
AppMessageCtx *app_message_ctx = app_state_get_app_message_ctx();
void *retval = app_message_ctx->inbox.user_context;
app_message_ctx->inbox.user_context = context;
app_message_ctx->outbox.user_context = context;
return retval;
}
AppMessageInboxReceived app_message_register_inbox_received(
AppMessageInboxReceived received_callback) {
AppMessageCtx *app_message_ctx = app_state_get_app_message_ctx();
AppMessageInboxReceived retval = app_message_ctx->inbox.received_callback;
app_message_ctx->inbox.received_callback = received_callback;
return retval;
}
AppMessageInboxDropped app_message_register_inbox_dropped(AppMessageInboxDropped dropped_callback) {
AppMessageCtx *app_message_ctx = app_state_get_app_message_ctx();
AppMessageInboxDropped retval = app_message_ctx->inbox.dropped_callback;
app_message_ctx->inbox.dropped_callback = dropped_callback;
return retval;
}
AppMessageOutboxSent app_message_register_outbox_sent(AppMessageOutboxSent sent_callback) {
AppMessageOutboxSent retval = app_state_get_app_message_ctx()->outbox.sent_callback;
app_state_get_app_message_ctx()->outbox.sent_callback = sent_callback;
return retval;
}
AppMessageOutboxFailed app_message_register_outbox_failed(AppMessageOutboxFailed failed_callback) {
AppMessageCtx *app_message_ctx = app_state_get_app_message_ctx();
AppMessageOutboxFailed retval = app_message_ctx->outbox.failed_callback;
app_message_ctx->outbox.failed_callback = failed_callback;
return retval;
}
void app_message_deregister_callbacks(void) {
AppMessageCtx *app_message_ctx = app_state_get_app_message_ctx();
app_message_ctx->inbox.received_callback = NULL;
app_message_ctx->inbox.dropped_callback = NULL;
app_message_ctx->inbox.user_context = NULL;
app_message_ctx->outbox.sent_callback = NULL;
app_message_ctx->outbox.failed_callback = NULL;
app_message_ctx->outbox.user_context = NULL;
}
static bool prv_supports_8k(void) {
if (!sys_app_pp_has_capability(CommSessionAppMessage8kSupport)) {
return false;
}
const Version app_sdk_version = sys_get_current_app_sdk_version();
const Version sdk_version_8k_messages_enabled = (const Version) { 0x05, 0x3f };
return (version_compare(sdk_version_8k_messages_enabled, app_sdk_version) <= 0);
}
uint32_t app_message_inbox_size_maximum(void) {
if (prv_supports_8k()) {
// New behavior, allow up to one large 8K byte array per message:
return (APP_MSG_8K_DICT_SIZE);
} else {
// Legacy behavior:
if (sys_get_current_app_is_js_allowed()) {
return (COMM_PRIVATE_MAX_INBOUND_PAYLOAD_SIZE - APP_MSG_HDR_OVRHD_SIZE);
} else {
return (COMM_PUBLIC_MAX_INBOUND_PAYLOAD_SIZE - APP_MSG_HDR_OVRHD_SIZE);
}
}
}
uint32_t app_message_outbox_size_maximum(void) {
if (prv_supports_8k()) {
return (APP_MSG_8K_DICT_SIZE);
} else {
// Legacy behavior:
return (APP_MESSAGE_OUTBOX_SIZE_MINIMUM + APP_MSG_HDR_OVRHD_SIZE);
}
}
AppMessageResult app_message_open(const uint32_t size_inbound, const uint32_t size_outbound) {
// We're making this assumption in this file; here's as good a place to check it as any.
// It's probably not super-bad if this isn't true, but we'll have type casts between different
// sizes without over/underflow verification.
#ifndef UNITTEST
_Static_assert(sizeof(size_t) == sizeof(uint32_t), "sizeof(size_t) != sizeof(uint32_t)");
#endif
AppMessageCtx *app_message_ctx = app_state_get_app_message_ctx();
if (app_message_ctx->outbox.phase != OUT_CLOSED ||
app_message_ctx->inbox.is_open) {
return APP_MSG_INVALID_STATE; // Already open
}
AppMessageResult result = app_message_outbox_open(&app_message_ctx->outbox, size_outbound);
if (APP_MSG_OK != result) {
return result;
}
result = app_message_inbox_open(&app_message_ctx->inbox, size_inbound);
if (APP_MSG_OK != result) {
app_message_outbox_close(&app_message_ctx->outbox);
return result;
}
return APP_MSG_OK;
}
void app_message_close(void) {
AppMessageCtx *app_message_ctx = app_state_get_app_message_ctx();
// TODO PBL-1634: handle the the return status when this function returns status.
// For now, continue to ignore failure.
app_message_outbox_close(&app_message_ctx->outbox);
app_message_inbox_close(&app_message_ctx->inbox);
app_message_deregister_callbacks();
}
// -------- Testing Interface (only) ------------------------------------------------------------ //
AppTimer *app_message_ack_timer_id(void) {
AppMessageCtx *app_message_ctx = app_state_get_app_message_ctx();
return app_message_ctx->outbox.ack_nack_timer;
}
bool app_message_is_accepting_inbound(void) {
AppMessageCtx *app_message_ctx = app_state_get_app_message_ctx();
return app_message_ctx->inbox.is_open;
}
bool app_message_is_accepting_outbound(void) {
AppMessageCtx *app_message_ctx = app_state_get_app_message_ctx();
return (app_message_ctx->outbox.phase == OUT_ACCEPTING);
}
bool app_message_is_closed_inbound(void) {
AppMessageCtx *app_message_ctx = app_state_get_app_message_ctx();
return (!app_message_ctx->inbox.is_open);
}
bool app_message_is_closed_outbound(void) {
AppMessageCtx *app_message_ctx = app_state_get_app_message_ctx();
return (app_message_ctx->outbox.phase == OUT_CLOSED);
}

View file

@ -0,0 +1,418 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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/dict.h"
#include "util/list.h"
//! @addtogroup Foundation
//! @{
//! @addtogroup AppMessage
//!
//!
//!
//! \brief Bi-directional communication between phone apps and Pebble watchapps
//!
//! AppMessage is a bi-directional messaging subsystem that enables communication between phone apps
//! and Pebble watchapps. This is accomplished by allowing phone and watchapps to exchange arbitrary
//! sets of key/value pairs. The key/value pairs are stored in the form of a Dictionary, the layout
//! of which is left for the application developer to define.
//!
//! AppMessage implements a push-oriented messaging protocol, enabling your app to call functions and
//! methods to push messages from Pebble to phone and vice versa. The protocol is symmetric: both Pebble
//! and the phone can send messages. All messages are acknowledged. In this context, there is no
//! client-server model, as such.
//!
//! During the sending phase, one side initiates the communication by transferring a dictionary over the air.
//! The other side then receives this message and is given an opportunity to perform actions on that data.
//! As soon as possible, the other side is expected to reply to the message with a simple acknowledgment
//! that the message was received successfully.
//!
//! PebbleKit JavaScript provides you with a set of standard JavaScript APIs that let your app receive messages
//! from the watch, make HTTP requests, and send new messages to the watch. AppMessage APIs are used to send and
//! receive data. A Pebble watchapp can use the resources of the connected phone to fetch information from web services,
//! send information to web APIs, or store login credentials. On the JavaScript side, you communicate
//! with Pebble via a Pebble object exposed in the namespace.
//!
//! Messages always need to get either ACKnowledged or "NACK'ed," that is, not acknowledged.
//! If not, messages will result in a time-out failure. The AppMessage subsystem takes care of this implicitly.
//! In the phone libraries, this step is a bit more explicit.
//!
//! The Pebble watch interfaces make a distinction between the Inbox and the Outbox calls. The Inbox
//! receives messages from the phone on the watch; the Outbox sends messages from the watch to the phone.
//! These two buffers can be managed separately.
//!
//! <h4>Warning</h4>
//! A critical constraint of AppMessage is that messages are limited in size. An ingoing (outgoing) message
//! larger than the inbox (outbox) will not be transmitted and will generate an error. You can choose your
//! inbox and outbox size when you call app_message_open().
//!
//! Pebble SDK provides a static minimum guaranteed size (APP_MESSAGE_INBOX_SIZE_MINIMUM and APP_MESSAGE_OUTBOX_SIZE_MINIMUM).
//! Requesting a buffer of the minimum guaranteed size (or smaller) is always guaranteed to succeed on all
//! Pebbles in this SDK version or higher, and with every phone.
//!
//! In some context, Pebble might be able to provide your application with larger inbox/outbox.
//! You can call app_message_inbox_size_maximum() and app_message_outbox_size_maximum() in your code to get
//! the largest possible value you can use.
//!
//! To always get the largest buffer available, follow this best practice:
//!
//! app_message_open(app_message_inbox_size_maximum(), app_message_outbox_size_maximum())
//!
//! AppMessage uses your application heap space. That means that the sizes you pick for the AppMessage
//! inbox and outbox buffers are important in optimizing your apps performance. The more you use for
//! AppMessage, the less space youll have for the rest of your app.
//!
//! To register callbacks, you should call app_message_register_inbox_received(), app_message_register_inbox_dropped(),
//! app_message_register_outbox_sent(), app_message_register_outbox_failed().
//!
//! Pebble recommends that you call them before app_message_open() to ensure you do not miss a message
//! arriving between starting AppMessage and registering the callback. You can set a context that will be passed
//! to all the callbacks with app_message_set_context().
//!
//! In circumstances that may not be ideal, when using AppMessage several types of errors may occur.
//! For example:
//!
//! * The send cant start because the system state won't allow for a success. Several reasons
//! you're unable to perform a send: A send() is already occurring (only one is possible at a time) or Bluetooth
//! is not enabled or connected.
//! * The send and receive occur, but the receiver cant accept the message. For instance, there is no app
//! that receives such a message.
//! * The send occurs, but the receiver either does not actually receive the message or cant handle it
//! in a timely fashion.
//! * In the case of a dropped message, the phone sends a message to the watchapp, while there is still
//! an unprocessed message in the Inbox.
//!
//! Other errors are possible and described by AppMessageResult. A client of the AppMessage interface
//! should use the result codes to be more robust in the face of communication problems either in the field or while debugging.
//!
//! Refer to the \htmlinclude app-phone-communication.html for a conceptual overview and code usage.
//!
//! For code examples, refer to the SDK Examples that directly use App Message. These include:
//! * <a href="https://github.com/pebble-examples/pebblekit-js-weather">
//! pebblekit-js-weather</a>
//! * <a href="https://github.com/pebble-examples/pebblekit-js-quotes">
//! pebblekit-js-quotes</a>
//! @{
//!
//! AppMessage is a messaging subsystem that allows phone and watch applications
//! to exchange arbitrary sets of key-value pairs. The key value pairs are
//! stored in the form of a Dictionary, the layout of which is left for the
//! application developer to define.
//!
//! <h3>Communication Model</h3>
//!
//! AppMessage is a simple send-receive-reply protocol. The protocol is symmetric: both the watch and phone can start
//! sending a message and expect a reply from the other side.
//!
//! In the sending phase, one side initiates the communication by transferring a dictionary over the air. The other
//! side then receives this message and is given an opportunity to perform actions on that data. As soon as possible,
//! the other side is expected to reply to the message with a simple acknowledgement that the message was received
//! successfully.
//!
//! In non-ideal circumstances, several errors may occur. For example:
//! * The send can't start as the system state won't allow for a success.
//! * The send and receive occur, but the receiver cannot accept the message (for example, there is no app that receives such
//! a message).
//! * The send occurs, but the receiver either does not actually receive the message or can't handle it in a timely
//! fashion.
//!
//! Other errors are possible, described by \ref AppMessageResult. A client of the AppMessage interface
//! can use the result codes to be more robust in the face of communication problems either in the field or while
//! debugging.
//!
//! The watch interfaces make a distinction between the Inbox and the Outbox. The Inbox receives messages from the
//! phone on the watch; the Outbox sends messages from the watch to the phone. These two objects can be managed
//! separately.
//!
//! \note Messages are actually addressed by the UUID of the watch and phone apps. This is done automatically by the
//! system for the convenience of the client. However, this does require that both the watch and phone apps
//! share their UUID. AppMessage is not capable of 1:N or M:N communication at this time, and is merely 1:1.
//!
//! \sa AppMessageResult
// -------- Defines, Enumerations, and Structures ------------------------------------------------------------------ //
//! As long as the firmware maintains its current major version, inboxes of this size or smaller will be allowed.
//!
//! \sa app_message_inbox_size_maximum()
//! \sa APP_MESSAGE_OUTBOX_SIZE_MINIMUM
//!
#define APP_MESSAGE_INBOX_SIZE_MINIMUM 124 /* bytes */
//! As long as the firmware maintains its current major version, outboxes of this size or smaller will be allowed.
//!
//! \sa app_message_outbox_size_maximum()
//! \sa APP_MESSAGE_INBOX_SIZE_MINIMUM
//!
#define APP_MESSAGE_OUTBOX_SIZE_MINIMUM 636 /* bytes */
//! AppMessage result codes.
typedef enum {
//! (0) All good, operation was successful.
APP_MSG_OK = 0,
//! (2) The other end did not confirm receiving the sent data with an (n)ack in time.
APP_MSG_SEND_TIMEOUT = 1 << 1,
//! (4) The other end rejected the sent data, with a "nack" reply.
APP_MSG_SEND_REJECTED = 1 << 2,
//! (8) The other end was not connected.
APP_MSG_NOT_CONNECTED = 1 << 3,
//! (16) The local application was not running.
APP_MSG_APP_NOT_RUNNING = 1 << 4,
//! (32) The function was called with invalid arguments.
APP_MSG_INVALID_ARGS = 1 << 5,
//! (64) There are pending (in or outbound) messages that need to be processed first before
//! new ones can be received or sent.
APP_MSG_BUSY = 1 << 6,
//! (128) The buffer was too small to contain the incoming message.
//! @internal
//! @see \ref app_message_open()
APP_MSG_BUFFER_OVERFLOW = 1 << 7,
//! (512) The resource had already been released.
APP_MSG_ALREADY_RELEASED = 1 << 9,
//! (1024) The callback was already registered.
APP_MSG_CALLBACK_ALREADY_REGISTERED = 1 << 10,
//! (2048) The callback could not be deregistered, because it had not been registered before.
APP_MSG_CALLBACK_NOT_REGISTERED = 1 << 11,
//! (4096) The system did not have sufficient application memory to
//! perform the requested operation.
APP_MSG_OUT_OF_MEMORY = 1 << 12,
//! (8192) App message was closed.
APP_MSG_CLOSED = 1 << 13,
//! (16384) An internal OS error prevented AppMessage from completing an operation.
APP_MSG_INTERNAL_ERROR = 1 << 14,
//! (32768) The function was called while App Message was not in the appropriate state.
APP_MSG_INVALID_STATE = 1 << 15,
} AppMessageResult;
//! Called after an incoming message is received.
//!
//! \param[in] iterator
//! The dictionary iterator to the received message. Never NULL. Note that the iterator cannot be modified or
//! saved off. The library may need to re-use the buffered space where this message is supplied. Returning from
//! the callback indicates to the library that the received message contents are no longer needed or have already
//! been externalized outside its buffering space and iterator.
//!
//! \param[in] context
//! Pointer to application data as specified when registering the callback.
//!
typedef void (*AppMessageInboxReceived)(DictionaryIterator *iterator, void *context);
//! Called after an incoming message is dropped.
//!
//! \param[in] result
//! The reason why the message was dropped. Some possibilities include \ref APP_MSG_BUSY and
//! \ref APP_MSG_BUFFER_OVERFLOW.
//!
//! \param[in] context
//! Pointer to application data as specified when registering the callback.
//!
//! Note that you can call app_message_outbox_begin() from this handler to prepare a new message.
//! This will invalidate the previous dictionary iterator; do not use it after calling app_message_outbox_begin().
//!
typedef void (*AppMessageInboxDropped)(AppMessageResult reason, void *context);
//! Called after an outbound message has been sent and the reply has been received.
//!
//! \param[in] iterator
//! The dictionary iterator to the sent message. The iterator will be in the final state that was sent. Note that
//! the iterator cannot be modified or saved off as the library will re-open the dictionary with dict_begin() after
//! this callback returns.
//!
//! \param[in] context
//! Pointer to application data as specified when registering the callback.
//!
typedef void (*AppMessageOutboxSent)(DictionaryIterator *iterator, void *context);
//! Called after an outbound message has not been sent successfully.
//!
//! \param[in] iterator
//! The dictionary iterator to the sent message. The iterator will be in the final state that was sent. Note that
//! the iterator cannot be modified or saved off as the library will re-open the dictionary with dict_begin() after
//! this callback returns.
//!
//! \param[in] result
//! The result of the operation. Some possibilities for the value include \ref APP_MSG_SEND_TIMEOUT,
//! \ref APP_MSG_SEND_REJECTED, \ref APP_MSG_NOT_CONNECTED, \ref APP_MSG_APP_NOT_RUNNING, and the combination
//! `(APP_MSG_NOT_CONNECTED | APP_MSG_APP_NOT_RUNNING)`.
//!
//! \param context
//! Pointer to application data as specified when registering the callback.
//!
//! Note that you can call app_message_outbox_begin() from this handler to prepare a new message.
//! This will invalidate the previous dictionary iterator; do not use it after calling app_message_outbox_begin().
//!
typedef void (*AppMessageOutboxFailed)(DictionaryIterator *iterator, AppMessageResult reason, void *context);
// -------- AppMessage Callbacks ----------------------------------------------------------------------------------- //
//! Gets the context that will be passed to all AppMessage callbacks.
//!
//! \return The current context on record.
//!
void *app_message_get_context(void);
//! Sets the context that will be passed to all AppMessage callbacks.
//!
//! \param[in] context The context that will be passed to all AppMessage callbacks.
//!
//! \return The previous context that was on record.
//!
void *app_message_set_context(void *context);
//! Registers a function that will be called after any Inbox message is received successfully.
//!
//! Only one callback may be registered at a time. Each subsequent call to this function will replace the previous
//! callback. The callback is optional; setting it to NULL will deregister the current callback and no function will
//! be called anymore.
//!
//! \param[in] received_callback The callback that will be called going forward; NULL to not have a callback.
//!
//! \return The previous callback (or NULL) that was on record.
//!
AppMessageInboxReceived app_message_register_inbox_received(AppMessageInboxReceived received_callback);
//! Registers a function that will be called after any Inbox message is received but dropped by the system.
//!
//! Only one callback may be registered at a time. Each subsequent call to this function will replace the previous
//! callback. The callback is optional; setting it to NULL will deregister the current callback and no function will
//! be called anymore.
//!
//! \param[in] dropped_callback The callback that will be called going forward; NULL to not have a callback.
//!
//! \return The previous callback (or NULL) that was on record.
//!
AppMessageInboxDropped app_message_register_inbox_dropped(AppMessageInboxDropped dropped_callback);
//! Registers a function that will be called after any Outbox message is sent and an ACK reply occurs in a timely
//! fashion.
//!
//! Only one callback may be registered at a time. Each subsequent call to this function will replace the previous
//! callback. The callback is optional; setting it to NULL will deregister the current callback and no function will
//! be called anymore.
//!
//! \param[in] sent_callback The callback that will be called going forward; NULL to not have a callback.
//!
//! \return The previous callback (or NULL) that was on record.
//!
AppMessageOutboxSent app_message_register_outbox_sent(AppMessageOutboxSent sent_callback);
//! Registers a function that will be called after any Outbox message is not sent with a timely ACK reply.
//! The call to \ref app_message_outbox_send() must have succeeded.
//!
//! Only one callback may be registered at a time. Each subsequent call to this function will replace the previous
//! callback. The callback is optional; setting it to NULL will deregister the current callback and no function will
//! be called anymore.
//!
//! \param[in] failed_callback The callback that will be called going forward; NULL to not have a callback.
//!
//! \return The previous callback (or NULL) that was on record.
//!
AppMessageOutboxFailed app_message_register_outbox_failed(AppMessageOutboxFailed failed_callback);
//! Deregisters all callbacks and their context.
//!
void app_message_deregister_callbacks(void);
// -------- AppMessage Lifecycle ----------------------------------------------------------------------------------- //
//! Programatically determine the inbox size maximum in the current configuration.
//!
//! \return The inbox size maximum on this firmware.
//!
//! \sa APP_MESSAGE_INBOX_SIZE_MINIMUM
//! \sa app_message_outbox_size_maximum()
//!
uint32_t app_message_inbox_size_maximum(void);
//! Programatically determine the outbox size maximum in the current configuration.
//!
//! \return The outbox size maximum on this firmware.
//!
//! \sa APP_MESSAGE_OUTBOX_SIZE_MINIMUM
//! \sa app_message_inbox_size_maximum()
//!
uint32_t app_message_outbox_size_maximum(void);
//! Open AppMessage to transfers.
//!
//! Use \ref dict_calc_buffer_size_from_tuplets() or \ref dict_calc_buffer_size() to estimate the size you need.
//!
//! \param[in] size_inbound The required size for the Inbox buffer
//! \param[in] size_outbound The required size for the Outbox buffer
//!
//! \return A result code such as \ref APP_MSG_OK or \ref APP_MSG_OUT_OF_MEMORY.
//!
//! \note It is recommended that if the Inbox will be used, that at least the Inbox callbacks should be registered
//! before this call. Otherwise it is possible for an Inbox message to be NACK'ed without being seen by the
//! application.
//!
AppMessageResult app_message_open(const uint32_t size_inbound, const uint32_t size_outbound);
//! Close AppMessage to further transfers.
//!
void app_message_close(void);
// -------- AppMessage Inbox --------------------------------------------------------------------------------------- //
// Note: the Inbox has no direct functions, only callbacks.
// -------- AppMessage Outbox -------------------------------------------------------------------------------------- //
//! Begin writing to the Outbox's Dictionary buffer.
//!
//! \param[out] iterator Location to write the DictionaryIterator pointer. This will be NULL on failure.
//!
//! \return A result code, including but not limited to \ref APP_MSG_OK, \ref APP_MSG_INVALID_ARGS or
//! \ref APP_MSG_BUSY.
//!
//! \note After a successful call, one can add values to the dictionary using functions like \ref dict_write_data()
//! and friends.
//!
//! \sa Dictionary
//!
AppMessageResult app_message_outbox_begin(DictionaryIterator **iterator);
//! Sends the outbound dictionary.
//!
//! \return A result code, including but not limited to \ref APP_MSG_OK or \ref APP_MSG_BUSY. The APP_MSG_OK code does
//! not mean that the message was sent successfully, but only that the start of processing was successful.
//! Since this call is asynchronous, callbacks provide the final result instead.
//!
//! \sa AppMessageOutboxSent
//! \sa AppMessageOutboxFailed
//!
AppMessageResult app_message_outbox_send(void);
//! @} // end addtogroup AppMessage
//! @} // end addtogroup Foundation

View file

@ -0,0 +1,120 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "applib/app_message/app_message_internal.h"
#include "applib/app_message/app_message_receiver.h"
#include "process_state/app_state/app_state.h"
#include "system/logging.h"
#include "syscall/syscall.h"
AppMessageResult app_message_inbox_open(AppMessageCtxInbox *inbox, size_t size_inbound) {
const size_t size_maximum = app_message_inbox_size_maximum();
if (size_inbound > size_maximum) {
// Truncate if it's more than the max:
size_inbound = size_maximum;
} else if (size_inbound == size_maximum) {
APP_LOG(LOG_LEVEL_INFO, "app_message_open() called with app_message_inbox_size_maximum().");
APP_LOG(LOG_LEVEL_INFO,
"This consumes %"PRIu32" bytes of heap memory, potentially more in the future!",
(uint32_t)size_maximum);
}
if (size_inbound == 0) {
return APP_MSG_OK;
}
// Add extra space needed for protocol overhead:
if (!app_message_receiver_open(size_inbound + APP_MSG_HDR_OVRHD_SIZE)) {
return APP_MSG_OUT_OF_MEMORY;
}
inbox->is_open = true;
return APP_MSG_OK;
}
void app_message_inbox_close(AppMessageCtxInbox *inbox) {
app_message_receiver_close();
inbox->is_open = false;
}
void app_message_inbox_send_ack_nack_reply(CommSession *session, const uint8_t transaction_id,
AppMessageCmd cmd) {
const AppMessageAck nack_message = (const AppMessageAck) {
.header = {
.command = cmd,
.transaction_id = transaction_id,
},
};
// Just use a syscall to enqueue the message using kernel heap.
// We could use app_outbox, but then we'd need to allocate the message on the app heap and I'm
// afraid this might break apps, especially if the mobile app is misbehaving and avalanching the
// app with messages that need to be (n)ack'd.
sys_app_pp_send_data(session, APP_MESSAGE_ENDPOINT_ID,
(const uint8_t *) &nack_message, sizeof(nack_message));
}
void app_message_inbox_handle_dropped_messages(uint32_t num_drops) {
// Taking a shortcut here. We used to report either APP_MSG_BUFFER_OVERFLOW or APP_MSG_BUSY back
// to the app. With the new the Receiver / AppInbox system, there are different reasons why
// messages get dropped. Just map everything to "APP_MSG_BUSY":
AppMessageCtx *app_message_ctx = app_state_get_app_message_ctx();
AppMessageCtxInbox *inbox = &app_message_ctx->inbox;
const bool is_open_and_has_handler = (inbox->is_open && inbox->dropped_callback);
for (uint32_t i = 0; i < num_drops; ++i) {
if (is_open_and_has_handler) {
inbox->dropped_callback(APP_MSG_BUSY, inbox->user_context);
}
}
}
static bool prv_is_app_with_uuid_running(const Uuid *uuid) {
Uuid app_uuid = {};
sys_get_app_uuid(&app_uuid);
return uuid_equal(&app_uuid, uuid);
}
void app_message_inbox_receive(CommSession *session, AppMessagePush *push_message, size_t length,
AppInboxConsumerInfo *consumer_info) {
// Test if the data is long enough to contain a push message:
if (length < sizeof(AppMessagePush)) {
PBL_LOG(LOG_LEVEL_ERROR, "Too short");
return;
}
AppMessageCtxInbox *inbox = &app_state_get_app_message_ctx()->inbox;
const uint8_t transaction_id = push_message->header.transaction_id;
// Verify UUID for app-bound messages:
if (!prv_is_app_with_uuid_running(&push_message->uuid)) {
app_message_inbox_send_ack_nack_reply(session, transaction_id, CMD_NACK);
sys_app_pp_app_message_analytics_count_drop();
return;
}
DictionaryIterator iterator;
const uint16_t dict_size = (length - APP_MSG_HDR_OVRHD_SIZE);
// TODO PBL-1639: Maybe do some sanity checking on the dict structure?
dict_read_begin_from_buffer(&iterator, (const uint8_t *) &push_message->dictionary, dict_size);
if (inbox->received_callback) {
inbox->received_callback(&iterator, inbox->user_context);
}
// Mark data as consumed...
app_inbox_consume(consumer_info);
// ... only then send the ACK:
app_message_inbox_send_ack_nack_reply(session, transaction_id, CMD_ACK);
}

View file

@ -0,0 +1,156 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "applib/app_message/app_message.h"
#include "applib/app_timer.h"
#include "services/normal/app_message/app_message_sender.h"
#include "util/attributes.h"
#include "util/uuid.h"
typedef struct CommSession CommSession;
#define ACK_NACK_TIME_OUT_MS (10000)
#define APP_MESSAGE_ENDPOINT_ID (0x30)
typedef enum {
CMD_PUSH = 0x01,
CMD_REQUEST = 0x02,
CMD_ACK = 0xff,
CMD_NACK = 0x7f,
} AppMessageCmd;
typedef struct PACKED {
AppMessageCmd command:8;
uint8_t transaction_id;
} AppMessageHeader;
//! The actual wire format of an app message message
typedef struct PACKED {
AppMessageHeader header;
Uuid uuid;
Dictionary dictionary; //!< Variable length!
} AppMessagePush;
// AppMessageHeader and Uuid size should be opaque to user of API
#define APP_MSG_HDR_OVRHD_SIZE (offsetof(AppMessagePush, dictionary))
#define APP_MSG_8K_DICT_SIZE (sizeof(Dictionary) + sizeof(Tuple) + (8 * 1024))
typedef struct PACKED {
AppMessageHeader header;
} AppMessageAck;
// For a diagram of the state machine:
// https://pebbletechnology.atlassian.net/wiki/pages/editpage.action?pageId=91914242
typedef enum AppMessagePhaseOut {
//! The App Message Outbox is not enabled. To enable it, call app_message_open().
OUT_CLOSED = 0,
//! The dictionary writing can be "started" by calling app_message_outbox_begin()
OUT_ACCEPTING,
//! app_message_outbox_begin() has been called and the dictionary can be written and then sent.
OUT_WRITING,
//! app_message_outbox_send() has been called. The ack/nack timeout timer has been set and
//! we're awaiting an ack/nack on the sent message AND the callback from the AppOutbox subsystem
//! that the data has been consumed. These 2 things happen in parallel, the order in which they
//! happen is undefined.
OUT_AWAITING_REPLY_AND_OUTBOX_CALLBACK,
//! We're still awaiting the AppMessage ack/nack, but the AppOutbox subsystem has indicated that
//! the data has been consumed.
OUT_AWAITING_REPLY,
//! We're still awaiting the callback from the AppOutbox subsystem to indicate the data has been
//! consumed, but we have already received the AppMessage ack/nack. This state is possible because
//! acking at a lower layer (i.e. PPoGATT) can happen with a slight delay.
OUT_AWAITING_OUTBOX_CALLBACK,
} AppMessagePhaseOut;
typedef struct AppMessageCtxInbox {
bool is_open;
void *user_context;
AppMessageInboxReceived received_callback;
AppMessageInboxDropped dropped_callback;
} AppMessageCtxInbox;
typedef struct AppMessageCtxOutbox {
DictionaryIterator iterator;
size_t transmission_size_limit;
AppMessageAppOutboxData *app_outbox_message;
AppMessageOutboxSent sent_callback;
AppMessageOutboxFailed failed_callback;
void *user_context;
AppTimer *ack_nack_timer;
struct PACKED {
AppMessagePhaseOut phase:8;
uint8_t transaction_id;
uint16_t not_ready_throttle_ms; // used for throttling app task when outbox is not ready
AppMessageResult result:16;
};
} AppMessageCtxOutbox;
typedef struct AppMessageCtx {
AppMessageCtxInbox inbox;
AppMessageCtxOutbox outbox;
} AppMessageCtx;
_Static_assert(sizeof(AppMessageCtx) <= 112,
"AppMessageCtx must not exceed 112 bytes!");
typedef struct {
CommSession *session;
//! To give us some room for future changes. This structure ends up in a buffer that is sized by
//! the app, so we can't easily increase the size of this once shipped.
uint8_t padding[8];
uint8_t data[];
} AppMessageReceiverHeader;
#ifndef UNITTEST
_Static_assert(sizeof(AppMessageReceiverHeader) == 12,
"The size of AppMessageReceiverHeader cannot grow beyond 12 bytes!");
#endif
void app_message_init(void);
AppMessageResult app_message_inbox_open(AppMessageCtxInbox *inbox, size_t size_inbound);
void app_message_inbox_close(AppMessageCtxInbox *inbox);
typedef struct AppInboxConsumerInfo AppInboxConsumerInfo;
void app_message_inbox_receive(CommSession *session, AppMessagePush *push_message, size_t length,
AppInboxConsumerInfo *consumer_info);
AppMessageResult app_message_outbox_open(AppMessageCtxOutbox *outbox, size_t size_outbound);
void app_message_outbox_close(AppMessageCtxOutbox *outbox);
void app_message_out_handle_ack_nack_received(const AppMessageHeader *header);
void app_message_inbox_send_ack_nack_reply(CommSession *session, const uint8_t transaction_id,
AppMessageCmd cmd);
void app_message_inbox_handle_dropped_messages(uint32_t num_drops);
void app_message_app_protocol_msg_callback(CommSession *session,
const uint8_t* data, size_t length,
AppInboxConsumerInfo *consumer_info);
void app_message_app_protocol_system_nack_callback(CommSession *session,
const uint8_t* data, size_t length);

View file

@ -0,0 +1,316 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "applib/applib_malloc.auto.h"
#include "applib/app_message/app_message_internal.h"
#include "applib/app_outbox.h"
#include "kernel/pbl_malloc.h"
#include "process_state/app_state/app_state.h"
#include "services/common/comm_session/session.h"
#include "syscall/syscall.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/math.h"
static void prv_outbox_prepare(AppMessageCtxOutbox *outbox);
static uint16_t prv_get_next_transaction_id(AppMessageCtxOutbox *outbox) {
return ++(outbox->transaction_id);
}
static void prv_transition_to_accepting(AppMessageCtxOutbox *outbox) {
outbox->phase = OUT_ACCEPTING;
if (outbox->result == APP_MSG_OK) {
if (outbox->sent_callback) {
outbox->sent_callback(&outbox->iterator, outbox->user_context);
}
} else {
if (outbox->failed_callback) {
outbox->failed_callback(&outbox->iterator, outbox->result, outbox->user_context);
}
}
}
static void prv_handle_nack_or_ack_timeout(AppMessageCtxOutbox *outbox,
AppMessageResult result) {
outbox->result = result;
if (outbox->phase == OUT_AWAITING_REPLY) {
prv_transition_to_accepting(outbox);
} else if (outbox->phase == OUT_AWAITING_REPLY_AND_OUTBOX_CALLBACK) {
outbox->phase = OUT_AWAITING_OUTBOX_CALLBACK;
} else {
WTF;
}
}
static void prv_handle_outbox_error_cb(void *data) {
AppMessageResult result = (AppMessageResult)(uintptr_t) data;
_Static_assert(sizeof(result) <= sizeof(data), "AppMessageResult expected to fit in void *");
AppMessageCtxOutbox *outbox = &app_state_get_app_message_ctx()->outbox;
if (outbox->phase != OUT_AWAITING_REPLY_AND_OUTBOX_CALLBACK) {
APP_LOG(LOG_LEVEL_ERROR, "Outbox failure, but unexpected state: %u", outbox->phase);
return;
}
// If app_message_outbox_handle_app_outbox_message_sent() has been called with an error,
// don't wait for an (N)ACK (it won't ever come), but finish right away:
outbox->result = result;
prv_transition_to_accepting(outbox);
}
//! Use sys_current_process_schedule_callback to maximize the stack space available to the
//! app's failed_callback.
static void prv_handle_outbox_error_async(AppMessageResult result) {
sys_current_process_schedule_callback(prv_handle_outbox_error_cb, (void *)(uintptr_t)result);
}
AppMessageResult app_message_outbox_open(AppMessageCtxOutbox *outbox, size_t size_outbound) {
const size_t size_maximum = app_message_outbox_size_maximum();
if (size_outbound > size_maximum) {
// Truncate if it's more than the max:
size_outbound = size_maximum;
} else if (size_outbound == size_maximum) {
APP_LOG(LOG_LEVEL_INFO, "app_message_open() called with app_message_outbox_size_maximum().");
APP_LOG(LOG_LEVEL_INFO,
"This consumes %"PRIu32" bytes of heap memory, potentially more in the future!",
(uint32_t)size_maximum);
}
if (size_outbound == 0) {
return APP_MSG_OK;
}
// Extra space needed by App Message protocol...:
size_outbound += APP_MSG_HDR_OVRHD_SIZE;
// ... and extra space header for app outbox message (not counting towards the transmission size):
outbox->app_outbox_message = applib_zalloc(sizeof(AppMessageAppOutboxData) + size_outbound);
if (outbox->app_outbox_message == NULL) {
return APP_MSG_OUT_OF_MEMORY;
}
outbox->transmission_size_limit = size_outbound;
outbox->transaction_id = 0;
prv_outbox_prepare(outbox);
outbox->phase = OUT_ACCEPTING;
return APP_MSG_OK;
}
static void prv_outbox_prepare(AppMessageCtxOutbox *outbox) {
AppMessagePush *push = (AppMessagePush *)outbox->app_outbox_message->payload;
dict_write_begin(&outbox->iterator,
(uint8_t *)&push->dictionary,
outbox->transmission_size_limit - APP_MSG_HDR_OVRHD_SIZE);
}
static void prv_stop_timer(AppMessageCtxOutbox *outbox) {
if (outbox->ack_nack_timer) {
app_timer_cancel(outbox->ack_nack_timer);
outbox->ack_nack_timer = NULL;
}
}
void app_message_outbox_close(AppMessageCtxOutbox *outbox) {
// Verify outbox phase.
if (outbox->phase == OUT_CLOSED) {
return;
}
// Cancel any outstanding timer.
prv_stop_timer(outbox);
outbox->transmission_size_limit = 0;
applib_free(outbox->app_outbox_message);
outbox->app_outbox_message = NULL;
// Finish by moving to the next phase.
outbox->phase = OUT_CLOSED;
}
static void prv_throttle(AppMessageCtxOutbox *outbox) {
if (outbox->not_ready_throttle_ms == 0) {
outbox->not_ready_throttle_ms = 1;
} else {
outbox->not_ready_throttle_ms = MIN(outbox->not_ready_throttle_ms * 2, 100 /*ms*/);
}
sys_psleep(outbox->not_ready_throttle_ms);
}
static bool prv_is_message_pending(AppMessagePhaseOut phase) {
return (phase == OUT_AWAITING_REPLY_AND_OUTBOX_CALLBACK ||
phase == OUT_AWAITING_REPLY ||
phase == OUT_AWAITING_OUTBOX_CALLBACK);
}
static bool prv_is_awaiting_ack(AppMessagePhaseOut phase) {
return (phase == OUT_AWAITING_REPLY_AND_OUTBOX_CALLBACK ||
phase == OUT_AWAITING_REPLY);
}
AppMessageResult app_message_outbox_begin(DictionaryIterator **iterator) {
AppMessageCtxOutbox *outbox = &app_state_get_app_message_ctx()->outbox;
if (iterator == NULL) {
return APP_MSG_INVALID_ARGS;
}
AppMessagePhaseOut phase = outbox->phase;
*iterator = NULL;
if (prv_is_message_pending(phase)) {
PBL_LOG(LOG_LEVEL_ERROR, "Can't call app_message_outbox_begin() now, wait for sent_callback!");
// See https://pebbletechnology.atlassian.net/browse/PBL-10146
// Workaround for apps that sit in a while() loop waiting on app_message_outbox_begin().
// Sleep a little longer each time we get a consecutive poll that returns failure.
prv_throttle(outbox);
return APP_MSG_BUSY;
} else if (phase == OUT_WRITING) {
PBL_LOG(LOG_LEVEL_ERROR,
"Must call app_message_outbox_send() before calling app_message_outbox_begin() again!");
return APP_MSG_INVALID_STATE;
} else if (phase == OUT_CLOSED) {
PBL_LOG(LOG_LEVEL_ERROR,
"Must call app_message_open() before calling app_message_outbox_begin()!");
return APP_MSG_INVALID_STATE;
}
// Reset the send state (dictionary, counters, etc.)
// We do this here, as this function is only called when we begin a new outbox,
// so the state should always be clean when we return successfully.
prv_outbox_prepare(outbox);
*iterator = &outbox->iterator;
outbox->phase = OUT_WRITING;
outbox->result = APP_MSG_OK;
return APP_MSG_OK;
}
static void ack_nack_timer_callback(void *data) {
AppMessageCtxOutbox *outbox = &app_state_get_app_message_ctx()->outbox;
outbox->ack_nack_timer = NULL;
if (!prv_is_awaiting_ack(outbox->phase)) {
// Reply was received and handled in the mean time, or app message was closed.
return;
}
prv_handle_nack_or_ack_timeout(outbox, APP_MSG_SEND_TIMEOUT);
}
void app_message_outbox_handle_app_outbox_message_sent(AppOutboxStatus status, void *cb_ctx) {
AppMessageCtxOutbox *outbox = &app_state_get_app_message_ctx()->outbox;
AppMessageSenderError e = (AppMessageSenderError)status;
if (e != AppMessageSenderErrorSuccess) {
if (e != AppMessageSenderErrorDisconnected) {
PBL_LOG(LOG_LEVEL_ERROR, "App message corrupted outbox? %"PRIu8, (uint8_t)e);
}
// Sleep a bit to prevent apps that hammer app_message_outbox_begin() when disconnected to
// become battery hogs:
prv_throttle(outbox);
prv_stop_timer(outbox);
// Just report any error as "not connected" to the app.
prv_handle_outbox_error_async(APP_MSG_NOT_CONNECTED);
} else {
// Only stop throttling if outbox message was consumed successfully:
outbox->not_ready_throttle_ms = 0;
if (outbox->phase == OUT_AWAITING_REPLY_AND_OUTBOX_CALLBACK) {
outbox->phase = OUT_AWAITING_REPLY;
return;
}
if (outbox->phase == OUT_AWAITING_OUTBOX_CALLBACK) {
prv_transition_to_accepting(outbox);
return;
}
}
}
AppMessageResult app_message_outbox_send(void) {
AppMessageCtxOutbox *outbox = &app_state_get_app_message_ctx()->outbox;
if (prv_is_message_pending(outbox->phase)) {
PBL_LOG(LOG_LEVEL_ERROR, "Can't call app_message_outbox_send() now, wait for sent_callback!");
return APP_MSG_BUSY;
}
if (outbox->phase != OUT_WRITING) {
return APP_MSG_INVALID_STATE;
}
const size_t transmission_size = dict_write_end(&outbox->iterator) + APP_MSG_HDR_OVRHD_SIZE;
if (transmission_size > outbox->transmission_size_limit) {
return APP_MSG_BUFFER_OVERFLOW;
}
uint8_t transaction_id = prv_get_next_transaction_id(outbox);
AppMessageAppOutboxData *app_outbox_message = outbox->app_outbox_message;
AppMessagePush *transmission = (AppMessagePush *)app_outbox_message->payload;
transmission->header.command = CMD_PUSH;
transmission->header.transaction_id = transaction_id;
sys_get_app_uuid(&transmission->uuid);
outbox->phase = OUT_AWAITING_REPLY_AND_OUTBOX_CALLBACK;
app_outbox_message->session = NULL;
app_outbox_message->endpoint_id = APP_MESSAGE_ENDPOINT_ID;
PBL_ASSERTN(!outbox->ack_nack_timer);
outbox->ack_nack_timer = app_timer_register(ACK_NACK_TIME_OUT_MS,
ack_nack_timer_callback, NULL);
app_outbox_send((const uint8_t *)app_outbox_message,
sizeof(AppMessageAppOutboxData) + transmission_size,
app_message_outbox_handle_app_outbox_message_sent, NULL);
return APP_MSG_OK;
}
void app_message_out_handle_ack_nack_received(const AppMessageHeader *header) {
AppMessageCtxOutbox *outbox = &app_state_get_app_message_ctx()->outbox;
if (!prv_is_awaiting_ack(outbox->phase)) {
PBL_LOG(LOG_LEVEL_ERROR, "Received (n)ack, but was not expecting one");
return;
}
if (outbox->transaction_id != header->transaction_id) {
PBL_LOG(LOG_LEVEL_ERROR, "Tx ID mismatch: %"PRIu8" != %"PRIu8,
outbox->transaction_id, header->transaction_id);
return;
}
prv_stop_timer(outbox);
if (header->command == CMD_NACK) {
prv_handle_nack_or_ack_timeout(outbox, APP_MSG_SEND_REJECTED);
return;
}
if (outbox->phase == OUT_AWAITING_REPLY_AND_OUTBOX_CALLBACK) {
outbox->phase = OUT_AWAITING_OUTBOX_CALLBACK;
return;
}
// phase == OUT_AWAITING_REPLY, because of !prv_is_awaiting_ack() check above.
prv_transition_to_accepting(outbox);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// Unit test interfaces
AppTimer *app_message_outbox_get_ack_nack_timer(void) {
AppMessageCtxOutbox *outbox = &app_state_get_app_message_ctx()->outbox;
return outbox ? outbox->ack_nack_timer : NULL;
}

View file

@ -0,0 +1,70 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "applib/app_message/app_message_receiver.h"
#include "applib/app_message/app_message_internal.h"
#include "applib/app_inbox.h"
#include "process_state/app_state/app_state.h"
#include "system/logging.h"
////////////////////////////////////////////////////////////////////////////////////////////////////
// All these functions execute on App Task
void app_message_receiver_message_handler(const uint8_t *data, size_t length,
AppInboxConsumerInfo *consumer_info) {
AppMessageReceiverHeader *message = (AppMessageReceiverHeader *)data;
app_message_app_protocol_msg_callback(message->session, message->data,
length - sizeof(AppMessageReceiverHeader), consumer_info);
}
void app_message_receiver_dropped_handler(uint32_t num_dropped_messages) {
app_message_inbox_handle_dropped_messages(num_dropped_messages);
}
bool app_message_receiver_open(size_t buffer_size) {
AppInbox **app_message_inbox = app_state_get_app_message_inbox();
if (*app_message_inbox) {
PBL_LOG(LOG_LEVEL_INFO, "App PP receiver already open, not opening again");
return true;
}
// Make sure that at least one message of `buffer_size` will fit, by adding the header size:
// Allocate overhead for 1 (N)ACK + 1 Push message:
static const uint32_t min_num_messages = 2;
size_t final_buffer_size =
(sizeof(AppMessageReceiverHeader) * min_num_messages) + buffer_size + sizeof(AppMessageAck);
AppInbox *inbox = app_inbox_create_and_register(final_buffer_size, min_num_messages,
app_message_receiver_message_handler,
app_message_receiver_dropped_handler);
if (!inbox) {
// No logging needed, the inner calls log themselves already
return false;
}
*app_message_inbox = inbox;
return true;
}
void app_message_receiver_close(void) {
AppInbox **inbox = app_state_get_app_message_inbox();
if (!(*inbox)) {
PBL_LOG(LOG_LEVEL_INFO, "App PP receiver already closed");
return;
}
app_inbox_destroy_and_deregister(*inbox);
*inbox = NULL;
}

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 <stdbool.h>
#include <stddef.h>
bool app_message_receiver_open(size_t buffer_size);
void app_message_receiver_close(void);

View file

@ -0,0 +1,39 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "applib/app_outbox.h"
#include "kernel/events.h"
#include "process_state/app_state/app_state.h"
#include "syscall/syscall.h"
static void prv_handle_event(PebbleEvent *e, void *unused) {
const PebbleAppOutboxSentEvent *sent_event = &e->app_outbox_sent;
sent_event->sent_handler(sent_event->status, sent_event->cb_ctx);
}
void app_outbox_send(const uint8_t *data, size_t length,
AppOutboxSentHandler sent_handler, void *cb_ctx) {
sys_app_outbox_send(data, length, sent_handler, cb_ctx);
}
void app_outbox_init(void) {
EventServiceInfo *info = app_state_get_app_outbox_subscription_info();
*info = (EventServiceInfo) {
.type = PEBBLE_APP_OUTBOX_SENT_EVENT,
.handler = prv_handle_event,
};
event_service_client_subscribe(info);
}

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 <stdint.h>
#include <stddef.h>
typedef enum {
AppOutboxStatusSuccess,
AppOutboxStatusConsumerDoesNotExist,
AppOutboxStatusOutOfResources,
AppOutboxStatusOutOfMemory,
// Clients of app outbox can use this range for use-case specific status codes:
AppOutboxStatusUserRangeStart,
AppOutboxStatusUserRangeEnd = 0xff,
} AppOutboxStatus;
typedef void (*AppOutboxSentHandler)(AppOutboxStatus status, void *cb_ctx);
//! Sends a message to the outbox.
//! If the outbox is not registered, the `sent_handler` will be called immediately with
//! status AppOutboxStatusConsumerDoesNotExist, *after* this function returns.
//! @param data The message data. The caller is responsible for keeping this buffer around until
//! `sent_handler` is called. Don't write to this buffer after calling this either, or the sent
//! data might get corrupted.
//! @param length Size of `data` in number of bytes.
//! @param sent_handler Pointer to the function to call when the kernel has consumed the message,
//! after which the `data` buffer will be no longer in use. Note that the `sent_handler` MUST be
//! white-listed in app_outbox_service.c.
//! @param cb_ctx Pointer to user data that will be passed into the `sent_handler`
void app_outbox_send(const uint8_t *data, size_t length,
AppOutboxSentHandler sent_handler, void *cb_ctx);
//! To be called once per app launch by the system.
void app_outbox_init(void);

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.
*/
#include "app_recognizers.h"
#include "process_state/app_state/app_state.h"
void app_recognizers_attach_recognizer(Recognizer *recognizer) {
recognizer_add_to_list(recognizer, app_state_get_recognizer_list());
}
void app_recognizers_detach_recognizer(Recognizer *recognizer) {
recognizer_remove_from_list(recognizer, app_state_get_recognizer_list());
}

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 "applib/ui/recognizer/recognizer.h"
#include <stdbool.h>
#include <stdint.h>
//! Attach a recognizer to the app
//! @param recognizer \ref Recognizer to attach
void app_recognizers_attach_recognizer(Recognizer *recognizer);
//! Detach a recognizer from the app
//! @param recognizer \ref Recognizer to detach
void app_recognizers_detach_recognizer(Recognizer *recognizer);

View file

@ -0,0 +1,252 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "applib/app_logging.h"
#include "applib/app_smartstrap.h"
#include "applib/app_smartstrap_private.h"
#include "applib/applib_malloc.auto.h"
#include "applib/event_service_client.h"
#include "board/board.h"
#include "process_state/app_state/app_state.h"
#include "services/normal/accessory/smartstrap_attribute.h"
#include "services/normal/accessory/smartstrap_connection.h"
#include "services/normal/accessory/smartstrap_state.h"
#include "system/passert.h"
#include "util/mbuf.h"
#define USE_SMARTSTRAP (CAPABILITY_HAS_ACCESSORY_CONNECTOR && !defined(RECOVERY_FW))
#if USE_SMARTSTRAP
// Event handler
////////////////////////////////////////////////////////////////////////////////
static void prv_app_smartstrap_event(PebbleEvent *e, void *context) {
SmartstrapConnectionState *state = app_state_get_smartstrap_state();
PebbleSmartstrapEvent event = e->smartstrap;
if (event.type == SmartstrapConnectionEvent) {
// Drop this event if there's no handler registered
if (state->handlers.availability_did_change) {
const SmartstrapServiceId service_id = event.service_id;
const bool is_available = (event.result == SmartstrapResultOk);
state->handlers.availability_did_change(service_id, is_available);
}
} else {
// All events other than SmartstrapConnectionEvent contain the attribute pointer
SmartstrapAttribute *attr = event.attribute;
PBL_ASSERTN(attr);
if ((event.type == SmartstrapDataSentEvent) && state->handlers.did_write) {
state->handlers.did_write(attr, event.result);
} else if ((event.type == SmartstrapDataReceivedEvent) && state->handlers.did_read) {
// 'attr' already points to the read buffer, so just need to cast it
state->handlers.did_read(attr, event.result, (uint8_t *)attr, event.read_length);
} else if ((event.type == SmartstrapNotifyEvent) && state->handlers.notified) {
state->handlers.notified(attr);
}
sys_smartstrap_attribute_event_processed(attr);
}
}
// Subscription functions
////////////////////////////////////////////////////////////////////////////////
static bool prv_should_subscribe(void) {
// We don't need to subscribe until either the app creates an attribute or subscribes an
// availability_did_change handler.
SmartstrapConnectionState *state = app_state_get_smartstrap_state();
return state->handlers.availability_did_change || state->num_attributes;
}
static void prv_state_init(void) {
SmartstrapConnectionState *state = app_state_get_smartstrap_state();
if (state->is_initialized) {
return;
}
state->event_info = (EventServiceInfo) {
.type = PEBBLE_SMARTSTRAP_EVENT,
.handler = prv_app_smartstrap_event
};
event_service_client_subscribe(&state->event_info);
state->timeout_ms = SMARTSTRAP_TIMEOUT_DEFAULT;
sys_smartstrap_subscribe();
state->is_initialized = true;
}
static void prv_state_deinit(void) {
SmartstrapConnectionState *state = app_state_get_smartstrap_state();
if (!state->is_initialized) {
return;
}
state->is_initialized = false;
event_service_client_unsubscribe(&state->event_info);
sys_smartstrap_unsubscribe();
}
#endif
// Internal APIs
////////////////////////////////////////////////////////////////////////////////
void app_smartstrap_cleanup(void) {
#if USE_SMARTSTRAP
prv_state_deinit();
#endif
}
// Exported APIs
////////////////////////////////////////////////////////////////////////////////
SmartstrapResult app_smartstrap_subscribe(SmartstrapHandlers handlers) {
#if USE_SMARTSTRAP
SmartstrapConnectionState *state = app_state_get_smartstrap_state();
state->handlers = handlers;
if (prv_should_subscribe()) {
prv_state_init();
}
return SmartstrapResultOk;
#else
return SmartstrapResultNotPresent;
#endif
}
void app_smartstrap_unsubscribe(void) {
#if USE_SMARTSTRAP
SmartstrapConnectionState *state = app_state_get_smartstrap_state();
state->handlers = (SmartstrapHandlers) {0};
if (!prv_should_subscribe()) {
prv_state_deinit();
}
#endif
}
void app_smartstrap_set_timeout(uint16_t timeout_ms) {
#if USE_SMARTSTRAP
SmartstrapConnectionState *state = app_state_get_smartstrap_state();
state->timeout_ms = timeout_ms;
#endif
}
SmartstrapAttribute *app_smartstrap_attribute_create(SmartstrapServiceId service_id,
SmartstrapAttributeId attribute_id,
size_t buffer_length) {
#if USE_SMARTSTRAP
if (!buffer_length) {
return NULL;
}
uint8_t *buffer = applib_zalloc(buffer_length);
if (!buffer) {
return NULL;
}
if (!sys_smartstrap_attribute_register(service_id, attribute_id, buffer, buffer_length)) {
applib_free(buffer);
return NULL;
}
SmartstrapConnectionState *state = app_state_get_smartstrap_state();
state->num_attributes++;
prv_state_init();
return (SmartstrapAttribute *)buffer;
#else
return NULL;
#endif
}
void app_smartstrap_attribute_destroy(SmartstrapAttribute *attr) {
#if USE_SMARTSTRAP
SmartstrapConnectionState *state = app_state_get_smartstrap_state();
state->num_attributes--;
if (!prv_should_subscribe()) {
prv_state_deinit();
}
sys_smartstrap_attribute_unregister(attr);
#endif
}
bool app_smartstrap_service_is_available(SmartstrapServiceId service_id) {
#if USE_SMARTSTRAP
return sys_smartstrap_is_service_connected(service_id);
#else
return false;
#endif
}
SmartstrapServiceId app_smartstrap_attribute_get_service_id(SmartstrapAttribute *attr) {
#if USE_SMARTSTRAP
SmartstrapServiceId service_id;
sys_smartstrap_attribute_get_info(attr, &service_id, NULL, NULL);
return service_id;
#else
return 0;
#endif
}
SmartstrapAttributeId app_smartstrap_attribute_get_attribute_id(SmartstrapAttribute *attr) {
#if USE_SMARTSTRAP
SmartstrapServiceId attribute_id;
sys_smartstrap_attribute_get_info(attr, NULL, &attribute_id, NULL);
return attribute_id;
#else
return 0;
#endif
}
SmartstrapResult app_smartstrap_attribute_read(SmartstrapAttribute *attr) {
#if USE_SMARTSTRAP
if (!attr) {
return SmartstrapResultInvalidArgs;
}
SmartstrapConnectionState *state = app_state_get_smartstrap_state();
return sys_smartstrap_attribute_do_request(attr, SmartstrapRequestTypeRead, state->timeout_ms, 0);
#else
return SmartstrapResultNotPresent;
#endif
}
SmartstrapResult app_smartstrap_attribute_begin_write(SmartstrapAttribute *attr, uint8_t **buffer,
size_t *buffer_length) {
#if USE_SMARTSTRAP
if (!attr || !buffer || !buffer_length) {
return SmartstrapResultInvalidArgs;
}
const SmartstrapRequestType type = SmartstrapRequestTypeBeginWrite;
SmartstrapResult result = sys_smartstrap_attribute_do_request(attr, type, 0, 0);
if (result == SmartstrapResultOk) {
*buffer = (uint8_t *)attr;
sys_smartstrap_attribute_get_info(attr, NULL, NULL, buffer_length);
}
return result;
#else
return SmartstrapResultNotPresent;
#endif
}
SmartstrapResult app_smartstrap_attribute_end_write(SmartstrapAttribute *attr, size_t write_length,
bool request_read) {
#if USE_SMARTSTRAP
if (!attr) {
return SmartstrapResultInvalidArgs;
}
SmartstrapRequestType type = request_read ?
SmartstrapRequestTypeWriteRead :
SmartstrapRequestTypeWrite;
SmartstrapConnectionState *state = app_state_get_smartstrap_state();
return sys_smartstrap_attribute_do_request(attr, type, state->timeout_ms, write_length);
#else
return SmartstrapResultNotPresent;
#endif
}

View file

@ -0,0 +1,216 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
//! @addtogroup Smartstrap
//! @{
//!
//! As long as the firmware maintains its current major version, attributes of this length or
//! less will be allowed. Note that the maximum number of bytes which may be read from the
//! smartstrap before a timeout occurs depends on the baud rate and implementation efficiency of the
//! underlying UART protocol on the smartstrap.
#define SMARTSTRAP_ATTRIBUTE_LENGTH_MAXIMUM 65535
//! The default request timeout in milliseconds (see \ref smartstrap_set_timeout).
#define SMARTSTRAP_TIMEOUT_DEFAULT 250
//! The service_id to specify in order to read/write raw data to the smartstrap.
#define SMARTSTRAP_RAW_DATA_SERVICE_ID 0
//! The attribute_id to specify in order to read/write raw data to the smartstrap.
#define SMARTSTRAP_RAW_DATA_ATTRIBUTE_ID 0
// convenient macros to distinguish between smartstrap and no smartstrap.
// TODO: PBL-21978 remove redundant comments as a workaround around for SDK generator
#if defined(PBL_SMARTSTRAP)
//! Convenience macro to switch between two expressions depending on smartstrap support.
//! On platforms with a smartstrap the first expression will be chosen, the second otherwise.
#define PBL_IF_SMARTSTRAP_ELSE(if_true, if_false) (if_true)
#else
//! Convenience macro to switch between two expressions depending on smartstrap support.
//! On platforms with a smartstrap the first expression will be chosen, the second otherwise.
#define PBL_IF_SMARTSTRAP_ELSE(if_true, if_false) (if_false)
#endif
//! Error values which may be returned from the smartstrap APIs.
typedef enum {
//! No error occured.
SmartstrapResultOk = 0,
//! Invalid function arguments were supplied.
SmartstrapResultInvalidArgs,
//! The smartstrap port is not present on this watch.
SmartstrapResultNotPresent,
//! A request is already pending on the specified attribute.
SmartstrapResultBusy,
//! Either a smartstrap is not connected or the connected smartstrap does not support the
//! specified service.
SmartstrapResultServiceUnavailable,
//! The smartstrap reported that it does not support the requested attribute.
SmartstrapResultAttributeUnsupported,
//! A time-out occured during the request.
SmartstrapResultTimeOut,
} SmartstrapResult;
//! A type representing a smartstrap ServiceId.
typedef uint16_t SmartstrapServiceId;
//! A type representing a smartstrap AttributeId.
typedef uint16_t SmartstrapAttributeId;
//! A type representing an attribute of a service provided by a smartstrap. This type is used when
//! issuing requests to the smartstrap.
typedef struct SmartstrapAttribute SmartstrapAttribute;
//! The type of function which is called after the smartstrap connection status changes.
//! @param service_id The ServiceId for which the availability changed.
//! @param is_available Whether or not this service is now available.
typedef void (*SmartstrapServiceAvailabilityHandler)(SmartstrapServiceId service_id,
bool is_available);
//! The type of function which can be called when a read request is completed.
//! @note Any write request made to the same attribute within this function will fail with
//! SmartstrapResultBusy.
//! @param attribute The attribute which was read.
//! @param result The result of the read.
//! @param data The data read from the smartstrap or NULL if the read was not successful.
//! @param length The length of the data or 0 if the read was not successful.
typedef void (*SmartstrapReadHandler)(SmartstrapAttribute *attribute, SmartstrapResult result,
const uint8_t *data, size_t length);
//! The type of function which can be called when a write request is completed.
//! @param attribute The attribute which was written.
//! @param result The result of the write.
typedef void (*SmartstrapWriteHandler)(SmartstrapAttribute *attribute, SmartstrapResult result);
//! The type of function which can be called when the smartstrap sends a notification to the watch
//! @param attribute The attribute which the notification came from.
typedef void (*SmartstrapNotifyHandler)(SmartstrapAttribute *attribute);
//! Handlers which are passed to smartstrap_subscribe.
typedef struct {
//! The connection handler is called after the connection state changes.
SmartstrapServiceAvailabilityHandler availability_did_change;
//! The read handler is called whenever a read is complete or the read times-out.
SmartstrapReadHandler did_read;
//! The did_write handler is called when a write has completed.
SmartstrapWriteHandler did_write;
//! The notified handler is called whenever a notification is received for an attribute.
SmartstrapNotifyHandler notified;
} SmartstrapHandlers;
//! Subscribes handlers to be called after certain smartstrap events occur.
//! @note Registering an availability_did_change handler will cause power to be applied to the
//! smartstrap port and connection establishment to begin.
//! @see smartstrap_unsubscribe
//! @returns `SmartstrapResultNotPresent` if the watch does not have a smartstrap port or
//! `SmartstrapResultOk` otherwise.
SmartstrapResult app_smartstrap_subscribe(SmartstrapHandlers handlers);
//! Unsubscribes the handlers. The handlers will no longer be called, but in-flight requests will
//! otherwise be unaffected.
//! @note If power was being applied to the smartstrap port and there are no attributes have been
//! created (or they have all been destroyed), this will cause the smartstrap power to be turned
//! off.
void app_smartstrap_unsubscribe(void);
//! Changes the value of the timeout which is used for smartstrap requests. This timeout is started
//! after the request is completely sent to the smartstrap and will be canceled only if the entire
//! response is received before it triggers. The new timeout value will take affect only for
//! requests made after this API is called.
//! @param timeout_ms The duration of the timeout to set, in milliseconds.
//! @note The maximum allowed timeout is currently 1000ms. If a larger value is passed, it will be
//! internally lowered to the maximum.
//! @see SMARTSTRAP_TIMEOUT_DEFAULT
void app_smartstrap_set_timeout(uint16_t timeout_ms);
//! Creates and returns a SmartstrapAttribute for the specified service and attribute. This API
//! will allocate an internal buffer of the requested length on the app's heap.
//! @note Creating an attribute will result in power being applied to the smartstrap port (if it
//! isn't already) and connection establishment to begin.
//! @param service_id The ServiceId to create the attribute for.
//! @param attribute_id The AttributeId to create the attribute for.
//! @param buffer_length The length of the internal buffer which will be used to store the read
//! and write requests for this attribute.
//! @returns The newly created SmartstrapAttribute or NULL if an internal error occured or if the
//! specified length is greater than SMARTSTRAP_ATTRIBUTE_LENGTH_MAXIMUM.
SmartstrapAttribute *app_smartstrap_attribute_create(SmartstrapServiceId service_id,
SmartstrapAttributeId attribute_id,
size_t buffer_length);
//! Destroys a SmartstrapAttribute. No further handlers will be called for this attribute and it
//! may not be used for any future requests.
//! @param[in] attribute The SmartstrapAttribute which should be destroyed.
//! @note If power was being applied to the smartstrap port, no availability_did_change handler is
//! subscribed, and the last attribute is being destroyed, this will cause the smartstrap power to
//! be turned off.
void app_smartstrap_attribute_destroy(SmartstrapAttribute *attribute);
//! Checks whether or not the specified service is currently supported by a connected smartstrap.
//! @param service_id The SmartstrapServiceId of the service to check for availability.
//! @returns Whether or not the service is available.
bool app_smartstrap_service_is_available(SmartstrapServiceId service_id);
//! Returns the ServiceId which the attribute was created for (see \ref
//! smartstrap_attribute_create).
//! @param attribute The SmartstrapAttribute for which to obtain the service ID.
//! @returns The SmartstrapServiceId which the attribute was created with.
SmartstrapServiceId app_smartstrap_attribute_get_service_id(SmartstrapAttribute *attribute);
//! Gets the AttributeId which the attribute was created for (see \ref smartstrap_attribute_create).
//! @param attribute The SmartstrapAttribute for which to obtain the attribute ID.
//! @returns The SmartstrapAttributeId which the attribute was created with.
SmartstrapAttributeId app_smartstrap_attribute_get_attribute_id(SmartstrapAttribute *attribute);
//! Performs a read request for the specified attribute. The `did_read` callback will be called when
//! the response is received from the smartstrap or when an error occurs.
//! @param attribute The attribute to be perform the read request on.
//! @returns `SmartstrapResultOk` if the read operation was started. The `did_read` callback will
//! be called once the read request has been completed.
SmartstrapResult app_smartstrap_attribute_read(SmartstrapAttribute *attribute);
//! Begins a write request for the specified attribute and returns a buffer into which the app
//! should write the data before calling smartstrap_attribute_end_write.
//! @note The buffer must not be used after smartstrap_attribute_end_write is called.
//! @param[in] attribute The attribute to begin writing for.
//! @param[out] buffer The buffer to write the data into.
//! @param[out] buffer_length The length of the buffer in bytes.
//! @returns `SmartstrapResultOk` if a write operation was started and the `buffer` and
//! `buffer_length` parameters were set, or an error otherwise.
SmartstrapResult app_smartstrap_attribute_begin_write(SmartstrapAttribute *attribute,
uint8_t **buffer, size_t *buffer_length);
//! This should be called by the app when it is done writing to the buffer provided by
//! smartstrap_begin_write and the data is ready to be sent to the smartstrap.
//! @param[in] attribute The attribute to begin writing for.
//! @param write_length The length of the data to be written, in bytes.
//! @param request_read Whether or not a read request on this attribute should be
//! automatically triggered following a successful write request.
//! @returns `SmartstrapResultOk` if a write operation was queued to be sent to the smartstrap. The
//! `did_write` handler will be called when the request is written to the smartstrap, and if
//! `request_read` was set to true, the `did_read` handler will be called when the read is complete.
SmartstrapResult app_smartstrap_attribute_end_write(SmartstrapAttribute *attribute,
size_t write_length, bool request_read);
//! @} // end addtogroup Smartstrap

View file

@ -0,0 +1,41 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "applib/app_smartstrap.h"
#include "applib/event_service_client.h"
#include "util/attributes.h"
#include "util/list.h"
#include "util/mbuf.h"
#include <stdbool.h>
#include <stdint.h>
typedef struct PACKED {
//! Info used to subscribed to smartstrap events
EventServiceInfo event_info;
//! How many attributes the app has created
uint32_t num_attributes;
//! Handlers supplied by the app
SmartstrapHandlers handlers;
//! Timeout configurable by the app
uint16_t timeout_ms;
//! Whether or not this struct is initialized
bool is_initialized;
} SmartstrapConnectionState;
void app_smartstrap_cleanup(void);

View file

@ -0,0 +1,123 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "applib/app_sync/app_sync.h"
#include "syscall/syscall.h"
#include "system/passert.h"
#include <string.h>
static void delegate_errors(AppSync *s, DictionaryResult dict_result,
AppMessageResult app_message_result) {
if (dict_result == DICT_OK && app_message_result == APP_MSG_OK) {
return;
}
if (s->callback.error) {
s->callback.error(dict_result, app_message_result, s->callback.context);
}
}
static void update_key_callback(const uint32_t key, const Tuple *new_tuple,
const Tuple *old_tuple, void *context) {
AppSync *s = context;
if (s->callback.value_changed) {
s->callback.value_changed(key, new_tuple, old_tuple, s->callback.context);
}
}
static void pass_initial_values_app_task_callback(void *data) {
AppSync *s = data;
Tuple *tuple = dict_read_first(&s->current_iter);
while (tuple) {
update_key_callback(tuple->key, tuple, NULL, s);
tuple = dict_read_next(&s->current_iter);
}
}
static void update_callback(DictionaryIterator *updated_iter, void *context) {
AppSync *s = context;
uint32_t size = s->buffer_size;
const bool update_existing_keys_only = true;
DictionaryResult result = dict_merge(&s->current_iter, &size,
updated_iter,
update_existing_keys_only,
update_key_callback, s);
delegate_errors(s, result, APP_MSG_OK);
}
static void out_failed_callback(DictionaryIterator *failed, AppMessageResult reason,
void *context) {
AppSync *s = context;
delegate_errors(s, DICT_OK, reason);
}
static void in_dropped_callback(AppMessageResult reason, void *context) {
AppSync *s = context;
delegate_errors(s, DICT_OK, reason);
}
// FIXME PBL-1709: this should return an AppMessageResult ...
void app_sync_init(AppSync *s,
uint8_t *buffer, const uint16_t buffer_size,
const Tuplet * const keys_and_initial_values, const uint8_t count,
AppSyncTupleChangedCallback tuple_changed_callback,
AppSyncErrorCallback error_callback,
void *context) {
PBL_ASSERTN(buffer != NULL);
PBL_ASSERTN(buffer_size > 0);
s->buffer = buffer;
s->buffer_size = buffer_size;
s->callback.value_changed = tuple_changed_callback;
s->callback.error = error_callback;
s->callback.context = context;
uint32_t in_out_size = buffer_size;
const DictionaryResult dict_result = dict_serialize_tuplets_to_buffer_with_iter(
&s->current_iter, keys_and_initial_values, count, s->buffer, &in_out_size);
app_message_set_context(s);
app_message_register_outbox_sent(update_callback);
app_message_register_outbox_failed(out_failed_callback);
app_message_register_inbox_received(update_callback);
app_message_register_inbox_dropped(in_dropped_callback);
sys_current_process_schedule_callback(pass_initial_values_app_task_callback, s);
delegate_errors(s, dict_result, APP_MSG_OK);
}
void app_sync_deinit(AppSync *s) {
app_message_set_context(NULL);
app_message_register_outbox_sent(NULL);
app_message_register_outbox_failed(NULL);
app_message_register_inbox_received(NULL);
app_message_register_inbox_dropped(NULL);
s->current = NULL;
}
AppMessageResult app_sync_set(AppSync *s, const Tuplet * const updated_keys_and_values,
const uint8_t count) {
DictionaryIterator *iter;
AppMessageResult result = app_message_outbox_begin(&iter);
if (iter == NULL) {
return result;
}
for (unsigned int i = 0; i < count; ++i) {
dict_write_tuplet(iter, &updated_keys_and_values[i]);
}
dict_write_end(iter);
return app_message_outbox_send();
}
const Tuple * app_sync_get(const AppSync *s, const uint32_t key) {
return dict_find(&s->current_iter, key);
}

View file

@ -0,0 +1,164 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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/dict.h"
#include "applib/app_message/app_message.h"
//! @file app_sync.h
//! @addtogroup Foundation
//! @{
//! @addtogroup AppSync
//! \brief UI synchronization layer for AppMessage
//!
//! AppSync is a convenience layer that resides on top of \ref AppMessage, and serves
//! as a UI synchronization layer for AppMessage. In so doing, AppSync makes it easier
//! to drive the information displayed in the watchapp UI with messages sent by a phone app.
//!
//! AppSync maintains and updates a Dictionary, and provides your app with a callback
//! (AppSyncTupleChangedCallback) routine that is called whenever the Dictionary changes
//! and the app's UI is updated. Note that the app UI is not updated automatically.
//! To update the UI, you need to implement the callback.
//!
//! Pebble OS provides support for data serialization utilities, like Dictionary,
//! Tuple and Tuplet data structures and their accompanying functions. You use Tuplets to create
//! a Dictionary with Tuple structures.
//!
//! AppSync manages the storage and bookkeeping chores of the current Tuple values. AppSync copies
//! incoming AppMessage Tuples into this "current" Dictionary, so that the key/values remain
//! available for the UI to use. For example, it is safe to use a C-string value provided by AppSync
//! and use it directly in a text_layer_set_text() call.
//!
//! Your app needs to supply the buffer that AppSync uses for the "current" Dictionary when
//! initializing AppSync.
//!
//! Refer to the
//! <a href="https://developer.getpebble.com/guides/pebble-apps/communications/appsync/">
//! Synchronizing App UI</a>
//! guide for a conceptual overview and code usage.
//! @{
struct AppSync;
//! Called whenever a Tuple changes. This does not necessarily mean the value in
//! the Tuple has changed. When the internal "current" dictionary gets updated,
//! existing Tuples might get shuffled around in the backing buffer, even though
//! the values stay the same. In this callback, the client code gets the chance
//! to remove the old reference and start using the new one.
//! In this callback, your application MUST clean up any references to the
//! `old_tuple` of a PREVIOUS call to this callback (and replace it with the
//! `new_tuple` that is passed in with the current call).
//! @param key The key for which the Tuple was changed.
//! @param new_tuple The new tuple. The tuple points to the actual, updated
//! "current" dictionary, as backed by the buffer internal to the AppSync
//! struct. Therefore the Tuple can be used after the callback returns, until
//! the AppSync is deinited. In case there was an error (e.g. storage shortage),
//! this `new_tuple` can be `NULL_TUPLE`.
//! @param old_tuple The values that will be replaced with `new_tuple`. The key,
//! value and type will be equal to the previous tuple in the old destination
//! dictionary; however, the `old_tuple` points to a stack-allocated copy of the
//! old data. This value will be `NULL_TUPLE` when the initial values are
//! being set.
//! @param context Pointer to application specific data, as set using
//! \ref app_sync_init()
//! @see \ref app_sync_init()
typedef void (*AppSyncTupleChangedCallback)(const uint32_t key, const Tuple *new_tuple,
const Tuple *old_tuple, void *context);
//! Called whenever there was an error.
//! @param dict_error The dictionary result error code, if the error was
//! dictionary related.
//! @param app_message_error The app_message result error code, if the error
//! was app_message related.
//! @param context Pointer to application specific data, as set using
//! \ref app_sync_init()
//! @see \ref app_sync_init()
typedef void (*AppSyncErrorCallback)(DictionaryResult dict_error,
AppMessageResult app_message_error, void *context);
//! Initialized an AppSync system with specific buffer size and initial keys and
//! values. The `callback.value_changed` callback will be called
//! __asynchronously__ with the initial keys and values, as to avoid duplicating
//! code to update your app's UI.
//! @param s The AppSync context to initialize
//! @param buffer The buffer that AppSync should use
//! @param buffer_size The size of the backing storage of the "current"
//! dictionary. Use \ref dict_calc_buffer_size_from_tuplets() to estimate the
//! size you need.
//! @param keys_and_initial_values An array of Tuplets with the initial keys and
//! values.
//! @param count The number of Tuplets in the `keys_and_initial_values` array.
//! @param tuple_changed_callback The callback that will handle changed
//! key/value pairs
//! @param error_callback The callback that will handle errors
//! @param context Pointer to app specific data that will get passed into calls
//! to the callbacks
//! @note Only updates for the keys specified in this initial array will be
//! accepted by AppSync, updates for other keys that might come in will just be
//! ignored.
void app_sync_init(struct AppSync *s, uint8_t *buffer, const uint16_t buffer_size,
const Tuplet * const keys_and_initial_values, const uint8_t count,
AppSyncTupleChangedCallback tuple_changed_callback,
AppSyncErrorCallback error_callback, void *context);
//! Cleans up an AppSync system.
//! It frees the buffer allocated by an \ref app_sync_init() call and
//! deregisters itself from the \ref AppMessage subsystem.
//! @param s The AppSync context to deinit.
void app_sync_deinit(struct AppSync *s);
//! Updates key/value pairs using an array of Tuplets.
//! @note The call will attempt to send the updated keys and values to the
//! application on the other end.
//! Only after the other end has acknowledged the update, the `.value_changed`
//! callback will be called to confirm the update has completed and your
//! application code can update its user interface.
//! @param s The AppSync context
//! @param keys_and_values_to_update An array of Tuplets with the keys and
//! values to update. The data in the Tuplets are copied during the call, so the
//! array can be stack-allocated.
//! @param count The number of Tuplets in the `keys_and_values_to_update` array.
//! @return The result code from the \ref AppMessage subsystem.
//! Can be \ref APP_MSG_OK, \ref APP_MSG_BUSY or \ref APP_MSG_INVALID_ARGS
AppMessageResult app_sync_set(struct AppSync *s, const Tuplet * const keys_and_values_to_update,
const uint8_t count);
//! Finds and gets a tuple in the "current" dictionary.
//! @param s The AppSync context
//! @param key The key for which to find a Tuple
//! @return Pointer to a found Tuple, or NULL if there was no Tuple with the
//! specified key.
const Tuple * app_sync_get(const struct AppSync *s, const uint32_t key);
//! @} // end addtogroup AppSync
//! @} // end addtogroup Foundation
typedef struct AppSync {
DictionaryIterator current_iter;
union {
Dictionary *current;
uint8_t *buffer;
};
uint16_t buffer_size;
struct {
AppSyncTupleChangedCallback value_changed;
AppSyncErrorCallback error;
void *context;
} callback;
} AppSync;

86
src/fw/applib/app_timer.c Normal file
View file

@ -0,0 +1,86 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "applib/app_timer.h"
#include "applib/app_logging.h"
#include "services/common/evented_timer.h"
#include "syscall/syscall_internal.h"
//! @file fw/applib/app_timer.c
//!
//! Surpise! All this is is a dumb wrapper around evented_timer!
DEFINE_SYSCALL(AppTimer*, app_timer_register, uint32_t timeout_ms,
AppTimerCallback callback,
void* callback_data) {
// No need to check callback_data, we only dereference it in userspace anyway.
return (AppTimer*)(uintptr_t)evented_timer_register(timeout_ms, false, callback, callback_data);
}
DEFINE_SYSCALL(AppTimer*, app_timer_register_repeatable, uint32_t timeout_ms,
AppTimerCallback callback,
void* callback_data,
bool repeating) {
// No need to check callback_data, we only dereference it in userspace anyway.
return (AppTimer*)(uintptr_t)evented_timer_register(timeout_ms,
repeating,
callback,
callback_data);
}
DEFINE_SYSCALL(bool, app_timer_reschedule, AppTimer *timer, uint32_t new_timeout_ms) {
if (PRIVILEGE_WAS_ELEVATED) {
if (!evented_timer_exists((EventedTimerID)timer)) {
APP_LOG(APP_LOG_LEVEL_ERROR, "Timer %u does not exist", (unsigned)timer);
return (false);
}
if (!evented_timer_is_current_task((EventedTimerID)timer)) {
APP_LOG(APP_LOG_LEVEL_ERROR, "Invalid timer %u used in app_timer_reschedule", (unsigned)timer);
syscall_failed();
}
}
return evented_timer_reschedule((EventedTimerID)timer, new_timeout_ms);
}
DEFINE_SYSCALL(void, app_timer_cancel, AppTimer *timer) {
if (PRIVILEGE_WAS_ELEVATED) {
if (!evented_timer_exists((EventedTimerID)timer)) {
APP_LOG(APP_LOG_LEVEL_ERROR, "Timer %u does not exist", (unsigned)timer);
return;
}
if (!evented_timer_is_current_task((EventedTimerID)timer)) {
APP_LOG(APP_LOG_LEVEL_ERROR, "Invalid timer %u used in app_timer_reschedule", (unsigned)timer);
syscall_failed();
}
}
evented_timer_cancel((EventedTimerID)timer);
}
DEFINE_SYSCALL(void *, app_timer_get_data, AppTimer *timer) {
if (PRIVILEGE_WAS_ELEVATED) {
if (!evented_timer_exists((EventedTimerID)timer)) {
APP_LOG(APP_LOG_LEVEL_ERROR, "Timer %u does not exist", (unsigned)timer);
return NULL;
}
if (!evented_timer_is_current_task((EventedTimerID)timer)) {
APP_LOG(APP_LOG_LEVEL_ERROR, "Invalid timer %u used in app_timer_reschedule", (unsigned)timer);
syscall_failed();
}
}
return evented_timer_get_data((EventedTimerID)timer);
}

70
src/fw/applib/app_timer.h Normal file
View file

@ -0,0 +1,70 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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>
//! @addtogroup Foundation
//! @{
//! @addtogroup Timer
//! \brief Can be used to execute some code at some point in the future.
//! @{
//! An opaque handle to a timer
struct AppTimer;
typedef struct AppTimer AppTimer;
//! The type of function which can be called when a timer fires. The argument will be the @p callback_data passed to
//! @ref app_timer_register().
typedef void (*AppTimerCallback)(void* data);
//! Registers a timer that ends up in callback being called some specified time in the future.
//! @param timeout_ms The expiry time in milliseconds from the current time
//! @param callback The callback that gets called at expiry time
//! @param callback_data The data that will be passed to callback
//! @return A pointer to an `AppTimer` that can be used to later reschedule or cancel this timer
AppTimer* app_timer_register(uint32_t timeout_ms, AppTimerCallback callback, void* callback_data);
//! @internal
//! Registers a timer that ends up in callback being called repeatedly at a specified interval
//! @param timeout_ms The interval time in milliseconds from the current time
//! @param callback The callback that gets called at every interval
//! @param callback_data The data that will be passed to callback
//! @return A pointer to an `AppTimer` that can be used to later reschedule or cancel this timer
AppTimer* app_timer_register_repeatable(uint32_t timeout_ms,
AppTimerCallback callback,
void* callback_data,
bool repeating);
//! @internal
//! Get the data passed to the app timer
void *app_timer_get_data(AppTimer *timer);
//! Reschedules an already running timer for some point in the future.
//! @param timer_handle The timer to reschedule
//! @param new_timeout_ms The new expiry time in milliseconds from the current time
//! @return true if the timer was rescheduled, false if the timer has already elapsed
bool app_timer_reschedule(AppTimer *timer_handle, uint32_t new_timeout_ms);
//! Cancels an already registered timer.
//! Once cancelled the handle may no longer be used for any purpose.
void app_timer_cancel(AppTimer *timer_handle);
//! @} // group Timer
//! @} // group Foundation

View file

@ -0,0 +1,82 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "applib/app_wakeup.h"
#include "event_service_client.h"
#include "process_state/app_state/app_state.h"
#include "process_management/app_manager.h"
#include "syscall/syscall.h"
#include "services/normal/wakeup.h"
#include "kernel/events.h"
static void do_handle(PebbleEvent *e, void *context) {
WakeupHandler wakeup_handler = app_state_get_wakeup_handler();
if (wakeup_handler != NULL) {
wakeup_handler(e->wakeup.wakeup_info.wakeup_id, e->wakeup.wakeup_info.wakeup_reason);
}
}
void app_wakeup_service_subscribe(WakeupHandler handler) {
if (handler) {
app_state_set_wakeup_handler(handler);
// Subscribe to PEBBLE_WAKEUP_EVENT
EventServiceInfo *wakeup_event_info = app_state_get_wakeup_event_info();
// NOTE: the individual fields of wakeup_event_info are assigned to
// instead of writing
// *wakeup_event_info = (EventServiceInfo) { ... }
// as the latter would zero out the ListNode embedded in the struct.
// Doing so would corrupt the events list if the event was already
// subscribed to (the app calls app_wakeup_service_subscribe twice).
wakeup_event_info->type = PEBBLE_WAKEUP_EVENT;
wakeup_event_info->handler = do_handle;
event_service_client_subscribe(wakeup_event_info);
}
}
WakeupId app_wakeup_schedule(time_t timestamp, int32_t cookie, bool notify_if_missed) {
return sys_wakeup_schedule(timestamp, cookie, notify_if_missed);
}
void app_wakeup_cancel(WakeupId wakeup_id) {
sys_wakeup_delete(wakeup_id);
}
void app_wakeup_cancel_all(void) {
sys_wakeup_cancel_all_for_app();
}
bool app_wakeup_get_launch_event(WakeupId *wakeup_id, int32_t *cookie) {
WakeupInfo wakeup_info;
sys_process_get_wakeup_info(&wakeup_info);
//If the id is invalid, return false
if (wakeup_info.wakeup_id <= 0) {
return false;
}
*wakeup_id = wakeup_info.wakeup_id;
*cookie = wakeup_info.wakeup_reason;
return true;
}
bool app_wakeup_query(WakeupId wakeup_id, time_t *timestamp) {
time_t scheduled_time = sys_wakeup_query(wakeup_id);
if (timestamp != NULL) {
*timestamp = scheduled_time;
}
return (scheduled_time >= 0) ? true : false;
}

View file

@ -0,0 +1,85 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "util/time/time.h"
#include "services/normal/wakeup.h"
//! @addtogroup Foundation
//! @{
//! @addtogroup Wakeup
//! \brief Allows applications to schedule to be launched even if they are not running.
//! @{
//! The type of function which can be called when a wakeup event occurs.
//! The arguments will be the id of the wakeup event that occurred,
//! as well as the scheduled cookie provided to \ref wakeup_schedule.
typedef void (*WakeupHandler)(WakeupId wakeup_id, int32_t cookie);
//! Registers a WakeupHandler to be called when wakeup events occur.
//! @param handler The callback that gets called when the wakeup event occurs
void app_wakeup_service_subscribe(WakeupHandler handler);
//! Registers a wakeup event that triggers a callback at the specified time.
//! Applications may only schedule up to 8 wakeup events.
//! Wakeup events are given a 1 minute duration window, in that no application may schedule a
//! wakeup event with 1 minute of a currently scheduled wakeup event.
//! @param timestamp The requested time (UTC) for the wakeup event to occur
//! @param cookie The application specific reason for the wakeup event
//! @param notify_if_missed On powering on Pebble, will alert user when
//! notifications were missed due to Pebble being off.
//! @return negative values indicate errors (StatusCode)
//! E_RANGE if the event cannot be scheduled due to another event in that period.
//! E_INVALID_ARGUMENT if the time requested is in the past.
//! E_OUT_OF_RESOURCES if the application has already scheduled all 8 wakeup events.
//! E_INTERNAL if a system error occurred during scheduling.
WakeupId app_wakeup_schedule(time_t timestamp, int32_t cookie, bool notify_if_missed);
//! Cancels a wakeup event.
//! @param wakeup_id Wakeup event to cancel
void app_wakeup_cancel(WakeupId wakeup_id);
//! Cancels all wakeup event for the app.
void app_wakeup_cancel_all(void);
//! Retrieves the wakeup event info for an app that was launched
//! by a wakeup_event (ie. \ref launch_reason() === APP_LAUNCH_WAKEUP)
//! so that an app may display information regarding the wakeup event
//! @param wakeup_id WakeupId for the wakeup event that caused the app to wakeup
//! @param cookie App provided reason for the wakeup event
//! @return True if app was launched due to a wakeup event, false otherwise
bool app_wakeup_get_launch_event(WakeupId *wakeup_id, int32_t *cookie);
//! Checks if the current WakeupId is still scheduled and therefore valid
//! @param wakeup_id Wakeup event to query for validity and scheduled time
//! @param timestamp Optionally points to an address of a time_t variable to
//! store the time that the wakeup event is scheduled to occur.
//! (The time is in UTC, but local time when \ref clock_is_timezone_set
//! returns false).
//! You may pass NULL instead if you do not need it.
//! @return True if WakeupId is still scheduled, false if it doesn't exist or has
//! already occurred
bool app_wakeup_query(WakeupId wakeup_id, time_t *timestamp);
//! @} // group Wakeup
//! @} // group Foundation

View file

@ -0,0 +1,90 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "app_watch_info.h"
#include "syscall/syscall_internal.h"
#include "system/version.h"
#include "mfg/mfg_info.h"
#include "git_version.auto.h"
DEFINE_SYSCALL(WatchInfoColor, sys_watch_info_get_color, void) {
return mfg_info_get_watch_color();
}
WatchInfoModel watch_info_get_model() {
// Pull the model for pebble time steel from the factory set model color bits.
switch (sys_watch_info_get_color()) {
// Pebble Original Colors
case WATCH_INFO_COLOR_BLACK:
case WATCH_INFO_COLOR_WHITE:
case WATCH_INFO_COLOR_RED:
case WATCH_INFO_COLOR_ORANGE:
case WATCH_INFO_COLOR_GRAY:
case WATCH_INFO_COLOR_BLUE:
case WATCH_INFO_COLOR_GREEN:
case WATCH_INFO_COLOR_PINK:
return WATCH_INFO_MODEL_PEBBLE_ORIGINAL;
// Pebble Steel Colors
case WATCH_INFO_COLOR_STAINLESS_STEEL:
case WATCH_INFO_COLOR_MATTE_BLACK:
return WATCH_INFO_MODEL_PEBBLE_STEEL;
// Pebble Time Colors
case WATCH_INFO_COLOR_TIME_WHITE:
case WATCH_INFO_COLOR_TIME_BLACK:
case WATCH_INFO_COLOR_TIME_RED:
return WATCH_INFO_MODEL_PEBBLE_TIME;
// Pebble Time Steel Colors
case WATCH_INFO_COLOR_TIME_STEEL_SILVER:
case WATCH_INFO_COLOR_TIME_STEEL_BLACK:
case WATCH_INFO_COLOR_TIME_STEEL_GOLD:
return WATCH_INFO_MODEL_PEBBLE_TIME_STEEL;
case WATCH_INFO_COLOR_TIME_ROUND_BLACK_14:
case WATCH_INFO_COLOR_TIME_ROUND_SILVER_14:
case WATCH_INFO_COLOR_TIME_ROUND_ROSE_GOLD_14:
return WATCH_INFO_MODEL_PEBBLE_TIME_ROUND_14;
case WATCH_INFO_COLOR_TIME_ROUND_BLACK_20:
case WATCH_INFO_COLOR_TIME_ROUND_SILVER_20:
return WATCH_INFO_MODEL_PEBBLE_TIME_ROUND_20;
case WATCH_INFO_COLOR_PEBBLE_2_HR_BLACK:
case WATCH_INFO_COLOR_PEBBLE_2_HR_LIME:
case WATCH_INFO_COLOR_PEBBLE_2_HR_FLAME:
case WATCH_INFO_COLOR_PEBBLE_2_HR_WHITE:
case WATCH_INFO_COLOR_PEBBLE_2_HR_AQUA:
return WATCH_INFO_MODEL_PEBBLE_2_HR;
case WATCH_INFO_COLOR_PEBBLE_2_SE_BLACK:
case WATCH_INFO_COLOR_PEBBLE_2_SE_WHITE:
return WATCH_INFO_MODEL_PEBBLE_2_SE;
case WATCH_INFO_COLOR_PEBBLE_TIME_2_BLACK:
case WATCH_INFO_COLOR_PEBBLE_TIME_2_SILVER:
case WATCH_INFO_COLOR_PEBBLE_TIME_2_GOLD:
return WATCH_INFO_MODEL_PEBBLE_TIME_2;
case WATCH_INFO_COLOR_UNKNOWN:
case WATCH_INFO_COLOR__MAX:
return WATCH_INFO_MODEL_UNKNOWN;
}
// Should never be reached
return WATCH_INFO_MODEL_UNKNOWN;
}
WatchInfoVersion watch_info_get_firmware_version(void) {
return (WatchInfoVersion) {
.major = GIT_MAJOR_VERSION,
.minor = GIT_MINOR_VERSION,
.patch = GIT_PATCH_VERSION
};
}

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>
//! @addtogroup Foundation
//! @{
//! @addtogroup WatchInfo
//! \brief Provides information about the watch itself.
//!
//! This API provides access to information such as the watch model, watch color
//! and watch firmware version.
//! @{
//! The different watch models.
typedef enum {
WATCH_INFO_MODEL_UNKNOWN, //!< Unknown model
WATCH_INFO_MODEL_PEBBLE_ORIGINAL, //!< Original Pebble
WATCH_INFO_MODEL_PEBBLE_STEEL, //!< Pebble Steel
WATCH_INFO_MODEL_PEBBLE_TIME, //!< Pebble Time
WATCH_INFO_MODEL_PEBBLE_TIME_STEEL, //!< Pebble Time Steel
WATCH_INFO_MODEL_PEBBLE_TIME_ROUND_14, //!< Pebble Time Round, 14mm lug size
WATCH_INFO_MODEL_PEBBLE_TIME_ROUND_20, //!< Pebble Time Round, 20mm lug size
WATCH_INFO_MODEL_PEBBLE_2_HR, //!< Pebble 2 HR
WATCH_INFO_MODEL_PEBBLE_2_SE, //!< Pebble 2 SE
WATCH_INFO_MODEL_PEBBLE_TIME_2, //!< Pebble Time 2
WATCH_INFO_MODEL__MAX
} WatchInfoModel;
//! The different watch colors.
// This color enum is programmed by the factory into the factory registry. Therefore these
// numbers must not change.
typedef enum {
WATCH_INFO_COLOR_UNKNOWN = 0, //!< Unknown color
WATCH_INFO_COLOR_BLACK = 1, //!< Black
WATCH_INFO_COLOR_WHITE = 2, //!< White
WATCH_INFO_COLOR_RED = 3, //!< Red
WATCH_INFO_COLOR_ORANGE = 4, //!< Orange
WATCH_INFO_COLOR_GRAY = 5, //!< Gray
WATCH_INFO_COLOR_STAINLESS_STEEL = 6, //!< Stainless Steel
WATCH_INFO_COLOR_MATTE_BLACK = 7, //!< Matte Black
WATCH_INFO_COLOR_BLUE = 8, //!< Blue
WATCH_INFO_COLOR_GREEN = 9, //!< Green
WATCH_INFO_COLOR_PINK = 10, //!< Pink
WATCH_INFO_COLOR_TIME_WHITE = 11, //!< Time White
WATCH_INFO_COLOR_TIME_BLACK = 12, //!< Time Black
WATCH_INFO_COLOR_TIME_RED = 13, //!< Time Red
WATCH_INFO_COLOR_TIME_STEEL_SILVER = 14, //!< Time Steel Silver
WATCH_INFO_COLOR_TIME_STEEL_BLACK = 15, //!< Time Steel Black
WATCH_INFO_COLOR_TIME_STEEL_GOLD = 16, //!< Time Steel Gold
WATCH_INFO_COLOR_TIME_ROUND_SILVER_14 = 17, //!< Time Round 14mm lug size, Silver
WATCH_INFO_COLOR_TIME_ROUND_BLACK_14 = 18, //!< Time Round 14mm lug size, Black
WATCH_INFO_COLOR_TIME_ROUND_SILVER_20 = 19, //!< Time Round 20mm lug size, Silver
WATCH_INFO_COLOR_TIME_ROUND_BLACK_20 = 20, //!< Time Round 20mm lug size, Black
WATCH_INFO_COLOR_TIME_ROUND_ROSE_GOLD_14 = 21, //!< Time Round 14mm lug size, Rose Gold
WATCH_INFO_COLOR_PEBBLE_2_HR_BLACK = 25, //!< Pebble 2 HR, Black / Charcoal
WATCH_INFO_COLOR_PEBBLE_2_HR_LIME = 27, //!< Pebble 2 HR, Charcoal / Sorbet Green
WATCH_INFO_COLOR_PEBBLE_2_HR_FLAME = 28, //!< Pebble 2 HR, Charcoal / Red
WATCH_INFO_COLOR_PEBBLE_2_HR_WHITE = 29, //!< Pebble 2 HR, White / Gray
WATCH_INFO_COLOR_PEBBLE_2_HR_AQUA = 30, //!< Pebble 2 HR, White / Turquoise
WATCH_INFO_COLOR_PEBBLE_2_SE_BLACK = 24, //!< Pebble 2 SE, Black / Charcoal
WATCH_INFO_COLOR_PEBBLE_2_SE_WHITE = 26, //!< Pebble 2 SE, White / Gray
WATCH_INFO_COLOR_PEBBLE_TIME_2_BLACK = 31, //!< Pebble Time 2, Black
WATCH_INFO_COLOR_PEBBLE_TIME_2_SILVER = 32, //!< Pebble Time 2, Silver
WATCH_INFO_COLOR_PEBBLE_TIME_2_GOLD = 33, //!< Pebble Time 2, Gold
WATCH_INFO_COLOR__MAX
} WatchInfoColor;
//! Data structure containing the version of the firmware running on the watch.
//! The version of the firmware has the form X.[X.[X]]. If a version number is not present it will be 0.
//! For example: the version numbers of 2.4.1 are 2, 4, and 1. The version numbers of 2.4 are 2, 4, and 0.
typedef struct {
uint8_t major; //!< Major version number
uint8_t minor; //!< Minor version number
uint8_t patch; //!< Patch version number
} WatchInfoVersion;
//! Provides the model of the watch.
//! @return {@link WatchInfoModel} representing the model of the watch.
WatchInfoModel watch_info_get_model(void);
//! Provides the version of the firmware running on the watch.
//! @return {@link WatchInfoVersion} representing the version of the firmware running on the watch.
WatchInfoVersion watch_info_get_firmware_version(void);
//! @internal Get the watch color from mfg info
WatchInfoColor sys_watch_info_get_color(void);
//! @} // end addtogroup WatchInfo
//! @} // end addtogroup Foundation

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.
*/
#include "syscall/syscall.h"
#include "app_worker.h"
// ---------------------------------------------------------------------------------------------------------------
// Determine if the worker for the current app is running
bool app_worker_is_running(void) {
return sys_app_worker_is_running();
}
// ---------------------------------------------------------------------------------------------------------------
// Launch the worker for the current app
AppWorkerResult app_worker_launch(void) {
return sys_app_worker_launch();
}
// ---------------------------------------------------------------------------------------------------------------
// Kill the worker for the current app
AppWorkerResult app_worker_kill(void) {
return sys_app_worker_kill();
}
// ---------------------------------------------------------------------------------------------------------------
// Subscribe to the app_message service
bool app_worker_message_subscribe(AppWorkerMessageHandler handler) {
return plugin_service_subscribe(NULL, (PluginServiceHandler)handler);
}
// ---------------------------------------------------------------------------------------------------------------
// Unsubscribe from a specific plug-in service by uuid.
bool app_worker_message_unsubscribe(void) {
return plugin_service_unsubscribe(NULL);
}
// ---------------------------------------------------------------------------------------------------------------
// Send an event to all registered subscribers of the given plugin service identified by UUID.
void app_worker_send_message(uint8_t type, AppWorkerMessage *data) {
_Static_assert(sizeof(AppWorkerMessage) == sizeof(PluginEventData), "These must match!");
plugin_service_send_event(NULL, type, (PluginEventData *)data);
}

View file

@ -0,0 +1,97 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "util/uuid.h"
//! @addtogroup Foundation
//! @{
//! @addtogroup AppWorker
//! \brief Runs in the background, and can communicate with the foreground app.
//! @{
//! Possible error codes from app_worker_launch, app_worker_kill
typedef enum {
//! Success
APP_WORKER_RESULT_SUCCESS= 0,
//! No worker found for the current app
APP_WORKER_RESULT_NO_WORKER = 1,
//! A worker for a different app is already running
APP_WORKER_RESULT_DIFFERENT_APP = 2,
//! The worker is not running
APP_WORKER_RESULT_NOT_RUNNING = 3,
//! The worker is already running
APP_WORKER_RESULT_ALREADY_RUNNING = 4,
//! The user will be asked for confirmation
APP_WORKER_RESULT_ASKING_CONFIRMATION = 5,
} AppWorkerResult;
//! Generic structure of a worker message that can be sent between an app and its worker
typedef struct {
uint16_t data0;
uint16_t data1;
uint16_t data2;
} AppWorkerMessage;
//! Determine if the worker for the current app is running
//! @return true if running
bool app_worker_is_running(void);
//! Launch the worker for the current app. Note that this is an asynchronous operation, a result code
//! of APP_WORKER_RESULT_SUCCESS merely means that the request was successfully queued up.
//! @return result code
AppWorkerResult app_worker_launch(void);
//! Kill the worker for the current app. Note that this is an asynchronous operation, a result code
//! of APP_WORKER_RESULT_SUCCESS merely means that the request was successfully queued up.
//! @return result code
AppWorkerResult app_worker_kill(void);
//! Callback type for worker messages. Messages can be sent from worker to app or vice versa.
//! @param type An application defined message type
//! @param data pointer to message data. The receiver must know the structure of the data provided by the sender.
typedef void (*AppWorkerMessageHandler)(uint16_t type, AppWorkerMessage *data);
//! Subscribe to worker messages. Once subscribed, the handler gets called on every message emitted by the other task
//! (either worker or app).
//! @param handler A callback to be executed when the event is received
//! @return true on success
bool app_worker_message_subscribe(AppWorkerMessageHandler handler);
//! Unsubscribe from worker messages. Once unsubscribed, the previously registered handler will no longer be called.
//! @return true on success
bool app_worker_message_unsubscribe(void);
//! Send a message to the other task (either worker or app).
//! @param type An application defined message type
//! @param data the message data structure
void app_worker_send_message(uint8_t type, AppWorkerMessage *data);
//! @} // end addtogroup AppWorker
//! @} // end addtogroup Foundation
//! @internal
//! Register the app message service with the event service system
void app_worker_message_init(void);

View file

@ -0,0 +1,283 @@
{
"_comment": [
"This file defines the different types we allocate on an application's heap. For ",
"compatibility purposes we need to keep these sizes the same from release to release ",
"in order to ensure that an app that ran on one version of the firmware works on another ",
"version without running out of memory. We support allocating different sizes for ",
"apps, 3.x apps, and system apps with different amounts of padding. ",
"",
"Types are defined below with the following parameters: ",
" name: The name of the type as it appears in our codebase. This type definition must ",
" be visibile in one of the headers in the headers list below. ",
" size_2x: The size in bytes that should be used for legacy2 apps ",
" size_3x: The size in bytes that should be used for 3.x apps ",
" size_3x_padding: The amount of padding to add directly to this particular struct. ",
" dependencies: Which other type is composed of. This will increase the final 3x ",
" padding by the combined padding of all dependent structs. ",
"",
" Note that sizeof(type) == size_3x_padding + sum(dependencies.size_3x_padding) or else ",
" an assert will trigger telling you that you've accidentally shifted the padding around."
],
"headers": [
"applib/event_service_client.h",
"applib/plugin_service_private.h",
"applib/graphics/gbitmap_sequence.h",
"applib/graphics/gtypes.h",
"applib/health_service_private.h",
"applib/voice/dictation_session_private.h",
"applib/voice/transcription_dialog.h",
"applib/voice/voice_window_private.h",
"applib/ui/action_bar_layer.h",
"applib/ui/action_menu_window_private.h",
"applib/ui/animation.h",
"applib/ui/animation_private.h",
"applib/ui/bitmap_layer.h",
"applib/ui/content_indicator_private.h",
"applib/ui/inverter_layer.h",
"applib/ui/layer.h",
"applib/ui/menu_layer.h",
"applib/ui/menu_layer_private.h",
"applib/ui/number_window.h",
"applib/ui/option_menu_window.h",
"applib/ui/property_animation_private.h",
"applib/ui/rotate_bitmap_layer.h",
"applib/ui/scroll_layer.h",
"applib/ui/selection_layer.h",
"applib/ui/simple_menu_layer.h",
"applib/ui/status_bar_layer.h",
"applib/ui/text_layer.h",
"applib/ui/window.h",
"applib/ui/window_stack_private.h",
"applib/legacy2/ui/animation_legacy2.h"
],
"types": [{
"name": "ActionBarLayer",
"size_3x_padding": 28,
"size_3x": 172,
"dependencies": ["Layer"],
"_comment": "ActionBarLayerLegacy2 should be used for 2.x apps"
}, {
"name": "ActionMenuItem",
"size_3x_padding": 8,
"size_3x": 20,
"_comment": "Only for 3.x apps"
}, {
"name": "ActionMenuLevel",
"size_3x_padding": 12,
"size_3x": 32,
"_comment": "Only for 3.x apps"
}, {
"name": "AnimationContext",
"size_3x_padding": 8,
"size_3x": 16,
"_comment": "Only for 3.x apps"
}, {
"name": "ActionMenuData",
"size_3x_padding": 90,
"size_3x": 930,
"dependencies": ["Window", "MenuLayer", "Layer", "Layer"],
"_comment": "Only for 3.x apps"
}, {
"name": "AnimationLegacy2",
"size_2x": 40,
"_comment": "Only for legacy2 apps"
}, {
"name": "AnimationPrivate",
"size_2x": 60,
"size_3x_padding": 16,
"size_3x": 76
}, {
"name": "AnimationAuxState",
"size_3x_padding": 12,
"size_3x": 32,
"_comment": "Only for 3.x apps"
}, {
"name": "BitmapLayer",
"size_2x": 52,
"size_3x_padding": 8,
"size_3x": 76,
"dependencies": ["Layer"]
}, {
"name": "ContentIndicator",
"size_3x_padding": 64,
"size_3x": 108,
"_comment": "Only for 3.x apps"
}, {
"name": "FontInfo",
"size_2x": 40,
"size_3x_padding": 16,
"size_3x": 56
}, {
"name": "GBitmapLegacy2",
"size_2x": 16,
"_comment": "Only for Legacy 2.x apps."
}, {
"name": "GBitmap",
"size_3x_padding": 8,
"size_3x": 32,
"_comment": "Only for 3.x apps."
}, {
"name": "GBitmapSequence",
"size_3x_padding": 16,
"size_3x": 88,
"_comment": "Only for 3.x apps"
}, {
"name": "InverterLayer",
"size_2x": 44,
"size_3x": 44,
"_comment": [ "Only meant for 2.x apps, but there are 3.x apps out there that abuse this.",
"See PBL-31276.",
"Layer is not marked as a dependency (event though it is!) because Layer ",
"has padding on it we don't want. We don't want this padding because we ",
"didn't have any originally with 3.x, and now we can't retroactively go ",
"back and add some without breaking our contract." ]
}, {
"name": "Layer",
"size_2x": 44,
"size_3x_padding": 16,
"size_3x": 60
}, {
"name": "MenuLayer",
"size_2x": 348,
"size_3x_padding": 40,
"size_3x": 452,
"dependencies": ["ScrollLayer", "InverterLayer"],
"_comment": "FIXME: MenuLayer has internal padding that needs removing"
}, {
"name": "NumberWindow",
"size_2x": 352,
"size_3x_padding": 188,
"size_3x": 568,
"dependencies": ["Window", "ActionBarLayer", "TextLayer", "TextLayer"]
}, {
"name": "PluginServiceEntry",
"size_2x": 16,
"size_3x_padding": 8,
"size_3x": 24
}, {
"name": "PropertyAnimationPrivate",
"size_3x_padding": 16,
"size_3x": 144,
"dependencies": ["AnimationPrivate"],
"_comment": "Only for 3.x apps"
}, {
"name": "RotBitmapLayer",
"size_2x": 68,
"size_3x_padding": 12,
"size_3x": 96,
"dependencies": ["Layer"]
}, {
"name": "ScrollLayer",
"size_2x": 204,
"size_3x_padding": 16,
"size_3x": 212,
"dependencies": ["Layer", "Layer", "Layer"]
}, {
"name": "SimpleMenuLayer",
"size_2x": 360,
"size_3x_padding": 32,
"size_3x": 496,
"dependencies": ["MenuLayer"]
}, {
"name": "StatusBarLayer",
"size_2x": 104,
"size_3x_padding": 80,
"size_3x": 200,
"dependencies": ["Layer"],
"_comment": "Generously padded because it's not feature complete yet"
}, {
"name": "TextLayer",
"size_2x": 60,
"size_3x_padding": 16,
"size_3x": 92,
"dependencies": ["Layer"]
}, {
"name": "TextLayout",
"size_2x": 24,
"_comment": "Only for legacy2 apps"
}, {
"name": "TextLayoutExtended",
"size_3x_padding": 0,
"size_3x": 40,
"_comment": "Only for 3.x apps"
}, {
"name": "Window",
"size_2x": 84,
"size_3x_padding": 0,
"size_3x": 100,
"dependencies": ["Layer"]
}, {
"name": "WindowStackItem",
"size_2x": 16,
"size_3x_padding": 16,
"size_3x": 32
}, {
"name": "MenuRenderIterator",
"size_2x": 96,
"size_3x_padding": 0,
"size_3x": 112,
"dependencies": ["Layer"]
}, {
"name": "EventServiceInfo",
"size_3x_padding": 20,
"size_3x": 37,
"_comment": "Generously padded because it's not feature complete yet (only 3.x)"
}, {
"name": "KinoPlayer",
"size_3x_padding": 20,
"size_3x": 52,
"_comment": "Generously padded because it's not feature complete yet (only 3.x)"
}, {
"name": "KinoLayer",
"size_3x_padding": 40,
"size_3x": 164,
"dependencies": ["Layer", "KinoPlayer"],
"_comment": "Generously padded because it's not feature complete yet (only 3.x)"
}, {
"name": "Dialog",
"size_3x_padding": 40,
"size_3x": 640,
"dependencies": ["Window", "StatusBarLayer", "TextLayer", "KinoLayer"],
"_comment": "Generously padded because it's not feature complete yet (only 3.x)"
}, {
"name": "ExpandableDialog",
"size_3x_padding": 48,
"size_3x": 1280,
"dependencies": ["Dialog", "TextLayer", "ScrollLayer", "ActionBarLayer", "Layer"],
"_comment": "Generously padded because it's not feature complete yet (only 3.x)"
}, {
"name": "TranscriptionDialog",
"size_3x_padding": 40,
"size_3x": 1352,
"dependencies": ["ExpandableDialog"],
"_comment": "Generously padded because it's not feature complete yet (only 3.x)"
}, {
"name": "VoiceWindow",
"size_3x_padding": 40,
"size_3x": 2368,
"dependencies": ["TranscriptionDialog", "Layer", "Layer", "StatusBarLayer", "TextLayer",
"KinoLayer", "Window", "EventServiceInfo"],
"_comment": "Generously padded because it's not feature complete yet (only 3.x)"
}, {
"name": "DictationSession",
"size_3x_padding": 1196,
"size_3x": 1284,
"dependencies": ["EventServiceInfo", "EventServiceInfo"],
"_comment": ["Enough padding to enable RAM allocation of required resources up-front if ",
"the resources it uses need to be moved out of MCU flash (only 3.x)"]
}, {
"name": "HealthServiceCache",
"size_3x_padding": 740,
"size_3x": 2048,
"dependencies": [],
"_comment": ["Cache to speed up frequent health service API calls (only 3.9+)"]
}, {
"name": "OptionMenu",
"size_2x": 632,
"size_3x_padding": 12,
"size_3x": 876,
"dependencies": ["Window", "StatusBarLayer", "MenuLayer", "GBitmap", "GBitmap"],
"_comment": "Not exported yet (only 3.x)"
}]
}

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 "applib/applib_malloc.auto.h"
#include "applib_resource_private.h"
#include "board/board.h"
#include "process_state/app_state/app_state.h"
#include "resource/resource_storage_builtin.h"
#include "resource/resource_storage_flash.h"
#include "syscall/syscall.h"
#include "system/passert.h"
ResHandle applib_resource_get_handle(uint32_t resource_id) {
if (sys_resource_is_valid(sys_get_current_resource_num(), resource_id)) {
return (ResHandle)resource_id;
}
return 0;
}
size_t applib_resource_size(ResHandle h) {
return sys_resource_size(sys_get_current_resource_num(), (uint32_t)h);
}
size_t applib_resource_load(ResHandle h, uint8_t *buffer, size_t max_length) {
return sys_resource_load_range(sys_get_current_resource_num(),
(uint32_t)h, 0, buffer, max_length);
}
size_t applib_resource_load_byte_range(
ResHandle h, uint32_t start_offset, uint8_t *buffer, size_t num_bytes) {
return sys_resource_load_range(sys_get_current_resource_num(),
(uint32_t)h, start_offset, buffer, num_bytes);
}
void *applib_resource_mmap_or_load(ResAppNum app_num, uint32_t resource_id,
size_t offset, size_t num_bytes, bool used_aligned) {
if (num_bytes == 0) {
return NULL;
}
const uint8_t *mapped_data = (app_num == SYSTEM_APP) ?
sys_resource_read_only_bytes(SYSTEM_APP, resource_id, NULL) : NULL;
uint8_t *result = NULL;
if (mapped_data) {
applib_resource_track_mmapped(mapped_data);
result = (uint8_t *)(mapped_data + offset);
} else {
// TODO: PBL-40010 clean this up
// we are wasting 7 bytes here so that clients of this API have the chance to
// align the data
result = applib_malloc(num_bytes + (used_aligned ? 7 : 0));
if (!result || sys_resource_load_range(app_num, resource_id, offset,
result, num_bytes) != num_bytes) {
applib_free(result);
return NULL;
}
}
return result;
}
void applib_resource_munmap_or_free(void *bytes) {
if (!applib_resource_munmap(bytes)) {
applib_free(bytes);
}
}
#if CAPABILITY_HAS_MAPPABLE_FLASH
bool applib_resource_track_mmapped(const void *bytes) {
if (resource_storage_builtin_bytes_are_readonly(bytes)) {
return true;
}
if (resource_storage_flash_bytes_are_readonly(bytes)) {
sys_resource_mapped_use();
return true;
}
return false;
}
bool applib_resource_is_mmapped(const void *bytes) {
return resource_storage_builtin_bytes_are_readonly(bytes) ||
resource_storage_flash_bytes_are_readonly(bytes);
}
bool applib_resource_munmap(const void *bytes) {
if (resource_storage_builtin_bytes_are_readonly(bytes)) {
return true;
}
if (resource_storage_flash_bytes_are_readonly(bytes)) {
sys_resource_mapped_release();
return true;
}
return false;
}
#else
bool applib_resource_track_mmapped(const void *bytes) {
return resource_storage_builtin_bytes_are_readonly(bytes);
}
bool applib_resource_is_mmapped(const void *bytes) {
return resource_storage_builtin_bytes_are_readonly(bytes);
}
bool applib_resource_munmap(const void *bytes) {
return resource_storage_builtin_bytes_are_readonly(bytes);
}
#endif // CAPABILITY_HAS_MAPPABLE_FLASH

View file

@ -0,0 +1,100 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 <stddef.h>
#include <stdint.h>
//! @file applib_resource.h
//! Wrapper functions for the resource syscalls. These functions give us a nice interface to export to 3rd party apps.
//! @addtogroup Foundation
//! @{
//! @addtogroup Resources
//! \brief Managing application resources
//!
//! Resources are data files that are bundled with your application binary and can be
//! loaded at runtime. You use resources to embed images or custom fonts in your app,
//! but also to embed any data file. Resources are always read-only.
//!
//! Resources are stored on Pebbles flash memory and only loaded in RAM when you load
//! them. This means that you can have a large number of resources embedded inside your app,
//! even though Pebbles RAM memory is very limited.
//!
//! See \htmlinclude UsingResources.html for information on how to embed
//! resources into your app's bundle.
//!
//! @{
#define RESOURCE_ID_FONT_FALLBACK RESOURCE_ID_GOTHIC_14
//! Opaque reference to a resource.
//! @see \ref resource_get_handle()
typedef void * ResHandle;
//! Gets the resource handle for a file identifier.
//! @param resource_id The resource ID
//!
//! The resource IDs are auto-generated by the Pebble build process, based
//! on the `appinfo.json`. The "name" field of each resource is prefixed
//! by `RESOURCE_ID_` and made visible to the application (through the
//! `build/src/resource_ids.auto.h` header which is automatically included).
//!
//! For example, given the following fragment of `appinfo.json`:
//! \code{.json}
//! ...
//! "resources" : {
//! "media": [
//! {
//! "name": "MY_ICON",
//! "file": "img/icon.png",
//! "type": "png",
//! },
//! ...
//! \endcode
//! The generated file identifier for this resource is `RESOURCE_ID_MY_ICON`.
//! To get a resource handle for that resource write:
//! \code{.c}
//! ResHandle rh = resource_get_handle(RESOURCE_ID_MY_ICON);
//! \endcode
ResHandle applib_resource_get_handle(uint32_t resource_id);
//! Gets the size of the resource given a resource handle.
//! @param h The handle to the resource
//! @return The size of the resource in bytes
size_t applib_resource_size(ResHandle h);
//! Copies the bytes for the resource with a given handle from flash storage into a given buffer.
//! @param h The handle to the resource
//! @param buffer The buffer to load the resource data into
//! @param max_length The maximum number of bytes to copy
//! @return The number of bytes actually copied
size_t applib_resource_load(ResHandle h, uint8_t *buffer, size_t max_length);
//! Copies a range of bytes from a resource with a given handle into a given buffer.
//! @param h The handle to the resource
//! @param start_offset The offset in bytes at which to start reading from the resource
//! @param buffer The buffer to load the resource data into
//! @param num_bytes The maximum number of bytes to copy
//! @return The number of bytes actually copied
size_t applib_resource_load_byte_range(
ResHandle h, uint32_t start_offset, uint8_t *buffer, size_t num_bytes);
//! @} // end addtogroup Resources
//! @} // end addtogroup Foundation

View file

@ -0,0 +1,51 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "applib_resource.h"
#include "resource/resource.h"
//! Checks if a passed pointer refers to builtin or memory-mapped data and manages reference
//! counters as needed.
//! @note You might want to use \ref applib_resource_mmap_or_load() instead
//! @return true, if the passed pointer expresses memory-mapped data ans was successfully tracked
bool applib_resource_track_mmapped(const void *bytes);
//! True, if the passed pointer refers to builtin or memory-mapped data
bool applib_resource_is_mmapped(const void *bytes);
//! Checks if a passed pointer refers to builtin or memory-mapped data and manages reference
//! counters as needed.
//! @note You might want to use \ref applib_resource_munmap_or_free() instead
//! @return true, if the passed pointer expresses memory-mapped data ans was successfully
//! untracked
bool applib_resource_munmap(const void *bytes);
//! Manages the reference counters for memory-mapped resources.
//! Should not be called by anyone but the process manager.
//! @return true, if any remaining resources were untracked
bool applib_resource_munmap_all();
//! Tries to load a resource as memory-mapped data. If this isn't supported on the system
//! or for a given resource if will try to allocate data and load it into RAM instead.
//! Have a look at \ref resource_load_byte_range_system for the discussion of arguments
//! @param used_aligned True, if you want this function to allocate 7 extra bytes if it cannot mmap
//! @return NULL, if the resource coudln't be memory-mapped or allocated
void *applib_resource_mmap_or_load(ResAppNum app_num, uint32_t resource_id,
size_t offset, size_t length, bool used_aligned);
//! Updates reference counters if bytes is memory-mapped,
//! alternatively it deallocates the data bytes points to.
void applib_resource_munmap_or_free(void *bytes);

View file

@ -0,0 +1,78 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "battery_state_service.h"
#include "event_service_client.h"
#include "kernel/events.h"
#include "services/common/event_service.h"
#include "syscall/syscall.h"
#include "system/passert.h"
#include "process_state/app_state/app_state.h"
#include "process_state/worker_state/worker_state.h"
// ----------------------------------------------------------------------------------------------------
static BatteryStateServiceState* prv_get_state(PebbleTask task) {
if (task == PebbleTask_Unknown) {
task = pebble_task_get_current();
}
if (task == PebbleTask_App) {
return app_state_get_battery_state_service_state();
} else if (task == PebbleTask_Worker) {
return worker_state_get_battery_state_service_state();
} else {
WTF;
}
}
static void do_handle(PebbleEvent *e, void *context) {
BatteryStateServiceState *state = prv_get_state(PebbleTask_Unknown);
PBL_ASSERTN(state->handler != NULL);
state->handler(sys_battery_get_charge_state());
}
void battery_state_service_init(void) {
event_service_init(PEBBLE_BATTERY_STATE_CHANGE_EVENT, NULL, NULL);
}
void battery_state_service_subscribe(BatteryStateHandler handler) {
BatteryStateServiceState *state = prv_get_state(PebbleTask_Unknown);
state->handler = handler;
event_service_client_subscribe(&state->bss_info);
}
BatteryChargeState battery_state_service_peek(void) {
return (sys_battery_get_charge_state());
}
void battery_state_service_unsubscribe(void) {
BatteryStateServiceState *state = prv_get_state(PebbleTask_Unknown);
event_service_client_unsubscribe(&state->bss_info);
state->handler = NULL;
}
void battery_state_service_state_init(BatteryStateServiceState *state) {
*state = (BatteryStateServiceState) {
.bss_info = {
.type = PEBBLE_BATTERY_STATE_CHANGE_EVENT,
.handler = &do_handle,
},
};
}

View file

@ -0,0 +1,60 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "services/common/battery/battery_monitor.h"
//! @addtogroup Foundation
//! @{
//! @addtogroup EventService
//! @{
//! @addtogroup BatteryStateService
//!
//! \brief Determines when the battery state changes
//!
//! The BatteryStateService API lets you know when the battery state changes, that is,
//! its current charge level, whether it is plugged and charging. It uses the
//! BatteryChargeState structure to describe the current power state of Pebble.
//!
//! Refer to the <a href="https://github.com/pebble-examples/classio-battery-connection">
//! classio-battery-connection</a> example, which demonstrates using the battery state service
//! in a watchface.
//! @{
//! Callback type for battery state change events
//! @param charge the state of the battery \ref BatteryChargeState
typedef void (*BatteryStateHandler)(BatteryChargeState charge);
//! Subscribe to the battery state event service. Once subscribed, the handler gets called
//! on every battery state change
//! @param handler A callback to be executed on battery state change event
void battery_state_service_subscribe(BatteryStateHandler handler);
//! Unsubscribe from the battery state event service. Once unsubscribed, the previously registered
//! handler will no longer be called.
void battery_state_service_unsubscribe(void);
//! Peek at the last known battery state.
//! @return a \ref BatteryChargeState containing the last known data
BatteryChargeState battery_state_service_peek(void);
//! @} // end addtogroup PEBBLE_BATTERY_STATE_CHANGE_EVENT
//! @} // end addtogroup EventService
//! @} // end addtogroup Foundation

View file

@ -0,0 +1,28 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "event_service_client.h"
#include "battery_state_service.h"
typedef struct __attribute__((packed)) BatteryStateServiceState {
BatteryStateHandler handler;
EventServiceInfo bss_info;
} BatteryStateServiceState;
void battery_state_service_state_init(BatteryStateServiceState *state);

View file

@ -0,0 +1,761 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "ble_ad_parse.h"
#include "applib/applib_malloc.auto.h"
#include "syscall/syscall.h"
#include "system/passert.h"
#include "util/math.h"
#include "util/net.h"
#include <btutil/bt_uuid.h>
// -----------------------------------------------------------------------------
//! Internal parsed advertisement data structures.
// -----------------------------------------------------------------------------
//! AD TYPE Values as specified by the Bluetooth 4.0 Spec.
//! See "Appendix C (Normative): EIR and AD Formats" in Core_v4.0.pdf
typedef enum {
BLEAdTypeFlags = 0x01,
BLEAdTypeService16BitUUIDPartial = 0x02,
BLEAdTypeService16BitUUIDComplete = 0x03,
BLEAdTypeService32BitUUIDPartial = 0x04,
BLEAdTypeService32BitUUIDComplete = 0x05,
BLEAdTypeService128BitUUIDPartial = 0x06,
BLEAdTypeService128BitUUIDComplete = 0x07,
BLEAdTypeLocalNameShortened = 0x08,
BLEAdTypeLocalNameComplete = 0x09,
BLEAdTypeTxPowerLevel = 0x0a,
BLEAdTypeManufacturerSpecific = 0xff,
} BLEAdType;
// -----------------------------------------------------------------------------
//! AD DATA element header
typedef struct __attribute__((__packed__)) {
uint8_t length;
BLEAdType type:8;
} BLEAdElementHeader;
typedef struct __attribute__((__packed__)) {
BLEAdElementHeader header;
uint8_t data[];
} BLEAdElement;
// -----------------------------------------------------------------------------
// Consuming BLEAdData:
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
//! Internal parser callback. Gets called for a parsed Service UUIDs element.
//! @param uuids The array with Service UUIDs
//! @param count The number of Service UUIDs the array contains
//! @param cb_data Pointer to arbitrary client data as passed to the parse call.
//! @return true to continue parsing or false to stop after returning.
typedef bool (*BLEAdParseServicesCallback)(const Uuid uuids[], uint8_t count,
void *cb_data);
// -----------------------------------------------------------------------------
//! Internal parser callback. Gets called for a parsed Local Name element.
//! @param local_name_bytes This is a *NON* zero terminated UTF-8 string.
//! @param length The length of local_name_bytes
//! @param cb_data Pointer to arbitrary client data as passed to the parse call.
//! @return true to continue parsing or false to stop after returning.
typedef bool (*BLEAdParseLocalNameCallback)(const uint8_t *local_name_bytes,
uint8_t length, void *cb_data);
// -----------------------------------------------------------------------------
//! Internal parser callback. Gets called for a parsed TX Power Level element.
//! @param tx_power_level The TX Power Level value.
//! @param cb_data Pointer to arbitrary client data as passed to the parse call.
//! @return true to continue parsing or false to stop after returning.
typedef bool (*BLEAdParseTXPowerLevelCallback)(int8_t tx_power_level,
void *cb_data);
// -----------------------------------------------------------------------------
//! Internal parser callback. Gets called for a Manufacturer Specific data elem.
//! @param company_id The Company ID
//! @param data The Manufacturer Specific data
//! @param length The length in bytes of data
//! @param cb_data Pointer to arbitrary client data as passed to the parse call.
//! @return true to continue parsing or false to stop after returning.
typedef bool (*BLEAdParseManufacturerSpecificCallback)(uint16_t company_id,
const uint8_t *data,
uint8_t length,
void *cb_data);
typedef struct {
BLEAdParseServicesCallback services_cb;
BLEAdParseLocalNameCallback local_name_cb;
BLEAdParseTXPowerLevelCallback tx_power_level_cb;
BLEAdParseManufacturerSpecificCallback manufacturer_cb;
} BLEAdParseCallbacks;
// -----------------------------------------------------------------------------
//! Parser for Services List data elements.
static bool parse_services_list(const BLEAdElement *elem,
const BLEAdParseCallbacks *callbacks,
void *cb_data) {
const uint8_t uuid_type = elem->header.type / 2;
// Most common is probably 128-bit UUID, then 16-bit, then 32-bit:
const size_t uuid_width = (uuid_type == 3) ? 16 : ((uuid_type == 1) ? 2 : 4);
const size_t num_uuids = (elem->header.length - 1 /* Type byte */)
/ uuid_width;
if (!num_uuids) {
return true; // continue parsing
}
Uuid uuids[num_uuids];
// Iterate through the list, expanding each UUID to 128-bit equivalents,
// then copying them into the uuids[] array:
const uint8_t *uuid_data = elem->data;
for (size_t i = 0; i < num_uuids; ++i) {
switch (uuid_type) {
case 1: { // 16 bit
uint16_t u16 = *(uint16_t *) uuid_data;
uuids[i] = bt_uuid_expand_16bit(u16);
uuid_data += sizeof(uint16_t);
break;
}
case 2: { // 32 bit
uint32_t u32 = *(uint32_t *) uuid_data;
uuids[i] = bt_uuid_expand_32bit(u32);
uuid_data += sizeof(uint32_t);
break;
}
case 3: // 128-bit
uuids[i] = UuidMakeFromLEBytes(uuid_data);
uuid_data += sizeof(Uuid);
break;
}
}
// Call back to client with parsed data:
return callbacks->services_cb(uuids, num_uuids, cb_data);
}
// -----------------------------------------------------------------------------
//! Parser for Local Name data element.
static bool parse_local_name(const BLEAdElement *elem,
const BLEAdParseCallbacks *callbacks,
void *cb_data) {
// Length of the raw string:
const uint8_t raw_length = elem->header.length - 1 /* -1 Type byte */;
if (!raw_length) {
return true; // continue parsing
}
// Call back to client with parsed data:
return callbacks->local_name_cb(elem->data, raw_length, cb_data);
}
// -----------------------------------------------------------------------------
//! Parser for TX Power Level data element.
static bool parse_power_level(const BLEAdElement *elem,
const BLEAdParseCallbacks *callbacks,
void *cb_data) {
if (elem->header.length != 2) {
// In case the length is not what it should be, do not add data element.
return true; // continue parsing
}
const int8_t tx_power_level = *(int8_t *)elem->data;
// Call back to client with parsed data:
return callbacks->tx_power_level_cb(tx_power_level, cb_data);
}
// -----------------------------------------------------------------------------
//! Parser for Manufacturer Specific data element.
static bool parse_manufact_spec(const BLEAdElement *elem,
const BLEAdParseCallbacks *callbacks,
void *cb_data) {
if (elem->header.length < 3) {
// The first 2 octets should be the Company Identifier Code
// (+1 for Type byte)
return true;
}
if (callbacks->manufacturer_cb) {
// Little-endian:
const uint16_t *company_id = (uint16_t *) elem->data;
// Call back to client with parsed data:
const uint8_t manufacturer_data_size =
elem->header.length - sizeof(*company_id) - 1 /* -1 Type Byte */;
return callbacks->manufacturer_cb(ltohs(*company_id),
elem->data + sizeof(*company_id),
manufacturer_data_size,
cb_data);
}
return true; // continue parsing
}
// -----------------------------------------------------------------------------
//! @param ad_data The advertising data and scan response data to parse.
//! @param callbacks Callbacks for each of the types of data the client wants
//! to receive parse callbacks for. You can leave a callback NULL if you are not
//! interested in receiving callbacks for that type of data.
//! @param cb_data Pointer to client data that is passed into the callback.
static void ble_ad_parse_ad_data(const BLEAdData *ad_data,
const BLEAdParseCallbacks *callbacks,
void *cb_data) {
const uint8_t *cursor = ad_data->data;
const uint8_t *end = cursor + ad_data->ad_data_length +
ad_data->scan_resp_data_length;
while (cursor < end) {
const BLEAdElement *elem = (const BLEAdElement *) cursor;
if (elem->header.length == 0) {
// We've hit a padding zero. We should be done, or this packet is corrupt.
return;
}
if (cursor + elem->header.length + 1 /* +1 length byte */ > end) {
return; // corrupted
}
bool (*parse_func)(const BLEAdElement *,
const BLEAdParseCallbacks *, void *) = NULL;
switch (elem->header.type) {
case BLEAdTypeService16BitUUIDPartial ... BLEAdTypeService128BitUUIDComplete:
if (callbacks->services_cb) {
parse_func = parse_services_list;
}
break;
case BLEAdTypeLocalNameShortened ... BLEAdTypeLocalNameComplete:
if (callbacks->local_name_cb) {
parse_func = parse_local_name;
}
break;
case BLEAdTypeTxPowerLevel:
if (callbacks->tx_power_level_cb) {
parse_func = parse_power_level;
}
break;
case BLEAdTypeManufacturerSpecific:
if (callbacks->manufacturer_cb) {
parse_func = parse_manufact_spec;
}
break;
default: // parse_func == NULL
break;
} // switch()
if (parse_func) {
if (!parse_func(elem, callbacks, cb_data)) {
// The callback indicated we should not continue parsing
return;
}
}
// The Length byte itself is not counted, so +1:
cursor += elem->header.length + 1;
}
}
// -----------------------------------------------------------------------------
//! ble_ad_includes_service() wrapper and helper function:
struct IncludesServiceCtx {
const Uuid *service_uuid;
bool included;
};
static bool includes_service_parse_cb(const Uuid uuids[], uint8_t count,
void *cb_data) {
struct IncludesServiceCtx *ctx = (struct IncludesServiceCtx *) cb_data;
for (int i = 0; i < count; ++i) {
if (uuid_equal(ctx->service_uuid, &uuids[i])) {
// Found!
ctx->included = true;
return false; // stop parsing
}
}
return true; // continue parsing
}
bool ble_ad_includes_service(const BLEAdData *ad, const Uuid *service_uuid) {
struct IncludesServiceCtx ctx = {
.service_uuid = service_uuid,
.included = false,
};
const BLEAdParseCallbacks callbacks = (const BLEAdParseCallbacks) {
.services_cb = includes_service_parse_cb,
};
ble_ad_parse_ad_data(ad, &callbacks, &ctx);
return ctx.included;
}
// -----------------------------------------------------------------------------
//! ble_ad_copy_service_uuids() wrapper and helper function:
struct CopyServiceUUIDsCtx {
Uuid *uuids_out;
const uint8_t max;
uint8_t copied;
uint8_t total;
};
static bool copy_services_parse_cb(const Uuid uuids[], uint8_t count,
void *cb_data) {
struct CopyServiceUUIDsCtx *ctx = (struct CopyServiceUUIDsCtx *)cb_data;
for (int i = 0; i < count; ++i) {
if (ctx->copied < ctx->max) {
// Still space left, so copy:
const Uuid *uuid = &uuids[i];
memcpy(&ctx->uuids_out[ctx->copied++], uuid, sizeof(Uuid));
}
++ctx->total;
}
return false; // stop parsing, only one Services UUID element allowed by spec
}
uint8_t ble_ad_copy_service_uuids(const BLEAdData *ad,
Uuid *uuids_out,
uint8_t num_uuids) {
struct CopyServiceUUIDsCtx ctx = {
.uuids_out = uuids_out,
.max = num_uuids,
};
const BLEAdParseCallbacks callbacks = {
.services_cb = copy_services_parse_cb,
};
ble_ad_parse_ad_data(ad, &callbacks, &ctx);
return ctx.total;
}
// -----------------------------------------------------------------------------
//! ble_ad_get_tx_power_level() wrapper and helper function:
struct TxPowerLevelCtx {
int8_t *tx_power_level_out;
bool included;
};
static bool tx_power_level_cb(int8_t tx_power_level,
void *cb_data) {
struct TxPowerLevelCtx *ctx = (struct TxPowerLevelCtx *)cb_data;
*ctx->tx_power_level_out = tx_power_level;
ctx->included = true;
return false; // stop parsing
}
bool ble_ad_get_tx_power_level(const BLEAdData *ad,
int8_t *tx_power_level_out) {
struct TxPowerLevelCtx ctx = {
.tx_power_level_out = tx_power_level_out,
.included = false,
};
const BLEAdParseCallbacks callbacks = {
.tx_power_level_cb = tx_power_level_cb,
};
ble_ad_parse_ad_data(ad, &callbacks, &ctx);
return ctx.included;
}
// -----------------------------------------------------------------------------
//! ble_ad_copy_local_name() wrapper and helper function:
struct LocalNameCtx {
char *buffer;
const uint8_t size;
size_t copied_size;
};
static bool copy_local_name_parse_cb(const uint8_t *local_name_bytes,
uint8_t length, void *cb_data) {
struct LocalNameCtx *ctx = (struct LocalNameCtx *)cb_data;
const uint8_t copied_size = MIN(ctx->size, length + 1 /* zero terminator */);
memcpy(ctx->buffer, local_name_bytes, copied_size - 1);
ctx->buffer[copied_size - 1] = 0; // zero terminator
ctx->copied_size = copied_size;
return false; // stop parsing
}
size_t ble_ad_copy_local_name(const BLEAdData *ad, char *buffer, size_t size) {
struct LocalNameCtx ctx = {
.buffer = buffer,
.size = MIN(size, 0xff),
.copied_size = 0,
};
const BLEAdParseCallbacks callbacks = {
.local_name_cb = copy_local_name_parse_cb,
};
ble_ad_parse_ad_data(ad, &callbacks, &ctx);
return ctx.copied_size;
}
// -----------------------------------------------------------------------------
//! ble_ad_get_raw_data_size() wrapper and helper function:
size_t ble_ad_get_raw_data_size(const BLEAdData *ad) {
return ad->ad_data_length + ad->scan_resp_data_length;
}
// -----------------------------------------------------------------------------
//! ble_ad_copy_raw_data() wrapper and helper function:
size_t ble_ad_copy_raw_data(const BLEAdData *ad, uint8_t *buffer, size_t size) {
const size_t size_to_copy = ble_ad_get_raw_data_size(ad);
if (size < size_to_copy) {
return 0;
}
memcpy(buffer, ad->data, size_to_copy);
return size_to_copy;
}
// -----------------------------------------------------------------------------
//! ble_ad_get_manufacturer_specific_data() wrapper and helper function:
struct ManufacturerSpecificCtx {
uint16_t company_id;
uint8_t *buffer;
const uint8_t size;
size_t copied_size;
};
static bool copy_manufacturer_specific_parse_cb(uint16_t company_id,
const uint8_t *data,
uint8_t length,
void *cb_data) {
struct ManufacturerSpecificCtx *ctx =
(struct ManufacturerSpecificCtx *) cb_data;
const uint8_t copied_size = MIN(ctx->size, length);
memcpy(ctx->buffer, data, copied_size);
ctx->copied_size = copied_size;
ctx->company_id = company_id;
return false; // stop parsing
}
size_t ble_ad_copy_manufacturer_specific_data(const BLEAdData *ad,
uint16_t *company_id,
uint8_t *buffer, size_t size) {
struct ManufacturerSpecificCtx ctx = {
.size = size,
.buffer = buffer,
};
const BLEAdParseCallbacks callbacks = {
.manufacturer_cb = copy_manufacturer_specific_parse_cb,
};
ble_ad_parse_ad_data(ad, &callbacks, &ctx);
if (company_id) {
*company_id = ctx.company_id;
}
return ctx.copied_size;
}
// -----------------------------------------------------------------------------
// Creating BLEAdData:
// -----------------------------------------------------------------------------
//! Magic high bit used as scan_resp_data_length to indicate that the ad_data
//! has been finalized and the next write should be counted towards the scan
//! response payload. The maximum scan_resp_data_length is 31 bytes, so this
//! value lies outside of the valid range. This is basically a memory savings
//! optimization, saving another "finalized" bool.
#define BLE_AD_DATA_FINALIZED ((uint8_t) 0x80)
bool prv_ad_is_finalized(const BLEAdData *ad_data) {
// Scan response data has already been added / started
return (ad_data->scan_resp_data_length != 0);
}
void ble_ad_start_scan_response(BLEAdData *ad_data) {
if (prv_ad_is_finalized(ad_data)) {
// Already finalized
return;
}
ad_data->scan_resp_data_length = BLE_AD_DATA_FINALIZED;
}
// -----------------------------------------------------------------------------
//! Helper to calculate whether a number of bytes will still fit when appended.
//! @param length Pointer to the length of the part for which to try to fit in
//! size_to_write number of bytes.
//! @param size_to_write The number of bytes to that need to be appended.
//! @return Pointer to length if it could still appends size_to_write bytes or
//! NULL if not.
static uint8_t *prv_length_ptr_if_fits_or_null(uint8_t *length,
size_t size_to_write) {
// Unset finalized bit:
const uint8_t used = *length & (~BLE_AD_DATA_FINALIZED);
const uint8_t left = GAP_LE_AD_REPORT_DATA_MAX_LENGTH - used;
// Return pointer to the pointer if size_to_write will fit, or NULL otherwise:
return (left >= size_to_write) ? length : NULL;
}
// -----------------------------------------------------------------------------
//! @return Pointer to the length that is incremented when writing size_to_write
//! number of bytes, or NULL if there is not enough space left.
static uint8_t* prv_length_to_increase(BLEAdData *ad_data,
size_t size_to_write) {
if (ad_data->scan_resp_data_length) {
// The scan response part is already being populated:
return prv_length_ptr_if_fits_or_null(&ad_data->scan_resp_data_length,
size_to_write);
} else {
// The advertisement is still being populated:
uint8_t *length = prv_length_ptr_if_fits_or_null(&ad_data->ad_data_length,
size_to_write);
if (length) {
// Hurray, the size_to_write fits in the advertisement part:
return length;
}
// Last resort, try fitting into scan response part:
return prv_length_ptr_if_fits_or_null(&ad_data->scan_resp_data_length,
size_to_write);
}
}
// -----------------------------------------------------------------------------
bool prv_write_element_to_ad_data(BLEAdData *ad_data,
const BLEAdElement *element) {
if (!ad_data || !element) {
return false;
}
const size_t size_to_write = element->header.length + 1 /* Length Byte */;
uint8_t* length = prv_length_to_increase(ad_data, size_to_write);
if (!length) {
// Not enough space...
return false;
}
// Undo the magic number trick:
if (*length == BLE_AD_DATA_FINALIZED) {
*length = 0;
}
// Append the element to the end:
uint8_t * const end = ad_data->data +
ad_data->ad_data_length +
ad_data->scan_resp_data_length;
memcpy(end, (const uint8_t *) element, size_to_write);
// Length book-keeping:
*length += size_to_write;
return true;
}
// -----------------------------------------------------------------------------
BLEAdData * ble_ad_create(void) {
const size_t max_ad_data_size = sizeof(BLEAdData) +
(GAP_LE_AD_REPORT_DATA_MAX_LENGTH * 2);
BLEAdData *ad_data = applib_malloc(max_ad_data_size);
if (ad_data) {
memset(ad_data, 0, sizeof(BLEAdData));
}
return ad_data;
}
// -----------------------------------------------------------------------------
void ble_ad_destroy(BLEAdData *ad) {
applib_free(ad);
}
// -----------------------------------------------------------------------------
//! The smallest UUID width, by reducing the width when a UUID is based on the
//! Bluetooth base UUID. @see bt_uuid_expand_16bit @see bt_uuid_expand_32bit
static uint8_t prv_smallest_bt_uuid_width_in_bytes(const Uuid *uuid) {
const Uuid bt_uuid_base = bt_uuid_expand_16bit(0);
// The bytes after the first 4 contain the Bluetooth base.
// Check if the uuid is based off of the Bluetooth base UUID:
const bool is_bt_uuid_based = (memcmp(&bt_uuid_base.byte4, &uuid->byte4,
sizeof(Uuid) - offsetof(Uuid, byte4)) == 0);
if (!is_bt_uuid_based) {
// Not based on the Bluetooth base UUID, so use 128-bits:
return sizeof(Uuid);
}
if (uuid->byte0 || uuid->byte1) {
// If byte0 and byte1 not zero: 32-bit UUID, Bluetooth base UUID based:
return sizeof(uint32_t);
}
// If byte0 and byte1 are zero: 16-bit UUID, Bluetooth base UUID based:
return sizeof(uint16_t);
}
// -----------------------------------------------------------------------------
//! Finds the largest common UUID width. For UUIDs that are based on the
//! Bluetooth base UUID, a reduced width will be taken of either 16-bits or
//! 32-bits.
static uint8_t prv_largest_common_bt_uuid_width(const Uuid uuids[],
uint8_t num_uuids) {
uint8_t max_width_bytes = sizeof(uint16_t);
for (unsigned int i = 0; i < num_uuids; ++i) {
const Uuid *uuid = &uuids[i];
const uint8_t width_bytes = prv_smallest_bt_uuid_width_in_bytes(uuid);
max_width_bytes = MAX(width_bytes, max_width_bytes);
}
return max_width_bytes;
}
// -----------------------------------------------------------------------------
//! Helper to reduces a 128-bit UUID to 16-bits. Note: this function does not
//! check whether the original UUID is based on the Bluetooth base.
static uint16_t prv_convert_to_16bit_uuid(const Uuid *uuid) {
uint16_t uuid_16bits = 0;
// Use bytes 2-3 of the Uuid:
for (int i = 2; i < 4; ++i) {
uuid_16bits <<= 8;
uuid_16bits += ((const uint8_t *) uuid)[i];
}
return uuid_16bits;
}
// -----------------------------------------------------------------------------
//! Helper to reduces a 128-bit UUID to 32-bits. Note: this function does not
//! check whether the original UUID is based on the Bluetooth base.
static uint32_t prv_convert_to_32bit_uuid(const Uuid *uuid) {
uint32_t uuid_32bits = 0;
// Use bytes 0-3 of the Uuid:
for (int i = 0; i < 4; ++i) {
uuid_32bits <<= 8;
uuid_32bits += ((const uint8_t *) uuid)[i];
}
return uuid_32bits;
}
// -----------------------------------------------------------------------------
bool ble_ad_set_service_uuids(BLEAdData *ad,
const Uuid uuids[], uint8_t num_uuids) {
struct __attribute__((__packed__)) BLEAdElementService {
BLEAdElementHeader header;
union {
Uuid uuid_128[0];
uint32_t uuid_32[0];
uint16_t uuid_16[0];
};
};
const uint8_t max_width_bytes = prv_largest_common_bt_uuid_width(uuids,
num_uuids);
// Allocate buffer:
const size_t buffer_size = sizeof(struct BLEAdElementService) +
(max_width_bytes * num_uuids);
uint8_t element_buffer[buffer_size];
struct BLEAdElementService *element = (struct BLEAdElementService *) element_buffer;
// Set header fields (assume Complete):
switch (max_width_bytes) {
case 16: element->header.type = BLEAdTypeService128BitUUIDComplete; break;
case 4: element->header.type = BLEAdTypeService32BitUUIDComplete; break;
case 2: element->header.type = BLEAdTypeService16BitUUIDComplete; break;
default:
WTF;
}
element->header.length = buffer_size - 1 /* -1 Length byte */;
// Copy UUIDs:
for (unsigned int i = 0; i < num_uuids; ++i) {
switch (max_width_bytes) {
case 16: element->uuid_128[i] = uuids[i]; break;
case 4: element->uuid_32[i] = prv_convert_to_32bit_uuid(&uuids[i]); break;
case 2: element->uuid_16[i] = prv_convert_to_16bit_uuid(&uuids[i]); break;
default:
WTF;
}
}
return prv_write_element_to_ad_data(ad, (const BLEAdElement *) element);
}
// -----------------------------------------------------------------------------
bool ble_ad_set_local_name(BLEAdData *ad,
const char *local_name) {
if (!local_name) {
return false;
}
const size_t length = strlen(local_name);
uint8_t element_buffer[sizeof(BLEAdElement) + length];
BLEAdElement *element = (BLEAdElement *) &element_buffer;
element->header.length = length + 1 /* +1 Type byte */;
element->header.type = BLEAdTypeLocalNameComplete; /* assume Complete */
// Note: *not* zero terminated by design
memcpy(element->data, local_name, length);
return prv_write_element_to_ad_data(ad, element);
}
// -----------------------------------------------------------------------------
bool ble_ad_set_tx_power_level(BLEAdData *ad) {
uint8_t element_buffer[sizeof(BLEAdElement) + sizeof(int8_t)];
BLEAdElement *element = (BLEAdElement *) element_buffer;
element->header.length = sizeof(int8_t) + 1 /* +1 Type byte */;
element->header.type = BLEAdTypeTxPowerLevel;
*((int8_t *) element->data) = sys_ble_get_advertising_tx_power();
return prv_write_element_to_ad_data(ad, element);
}
// -----------------------------------------------------------------------------
bool ble_ad_set_manufacturer_specific_data(BLEAdData *ad, uint16_t company_id,
const uint8_t *data, size_t size) {
struct __attribute__((__packed__)) BLEAdElementManufacturerSpecific {
BLEAdElementHeader header;
uint16_t company_id;
uint8_t data[];
};
uint8_t element_buffer[sizeof(struct BLEAdElementManufacturerSpecific) + size];
struct BLEAdElementManufacturerSpecific *element =
(struct BLEAdElementManufacturerSpecific *) element_buffer;
element->header.length = sizeof(struct BLEAdElementManufacturerSpecific)
- 1 /* -1 Length byte */ + size;
element->header.type = BLEAdTypeManufacturerSpecific;
element->company_id = ltohs(company_id);
memcpy(element->data, data, size);
return prv_write_element_to_ad_data(ad, (const BLEAdElement *) element);
}
// -----------------------------------------------------------------------------
bool ble_ad_set_flags(BLEAdData *ad, uint8_t flags) {
struct __attribute__((__packed__)) BLEAdElementManufacturerSpecific {
BLEAdElementHeader header;
uint8_t flags;
} element = {
.header = {
.length = sizeof(struct BLEAdElementManufacturerSpecific)
- 1 /* -1 Length byte */,
.type = BLEAdTypeFlags,
},
.flags = flags,
};
return prv_write_element_to_ad_data(ad, (const BLEAdElement *) &element);
}

View file

@ -0,0 +1,206 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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>
//! @file ble_ad_parse.h
//! API to serialize and deserialize advertisment and scan response payloads.
//!
//! Inbound payloads, as received using the ble_scan.h public API, can be
//! consumed/deserialized using the functions below.
//!
//! Outbound payloads can be created/serialized and then advertised using the
//! gap_le_advert.h functions. At the moment, there is no public API.
// -----------------------------------------------------------------------------
// Consuming BLEAdData:
// -----------------------------------------------------------------------------
//! Searching the advertisement data to check whether a given service UUID is
//! included.
//! @param ad The advertisement data
//! @param service_uuid The UUID of the service to look for
//! @return true if the service UUID was found, false if not
bool ble_ad_includes_service(const BLEAdData *ad, const Uuid *service_uuid);
//! If present, copies the Service UUIDs from the advertisement data.
//! @param ad The advertisement data
//! @param[out] uuids_out An array of Uuid`s into which the found Service UUIDs
//! will be copied.
//! @param num_uuids The size of the uuids_out array.
//! @return The total number of found Service UUIDs. This might be a larger
//! number than num_uuids, if the passed array was not large enough to hold all
//! the UUIDs.
//! @note All UUIDs from advertisement data will be converted to their 128-bit
//! equivalents using the Bluetooth Base UUID using bt_uuid_expand_16bit or
//! bt_uuid_expand_32bit.
//! @see ble_ad_get_number_of_service_uuids
uint8_t ble_ad_copy_service_uuids(const BLEAdData *ad,
Uuid *uuids_out,
uint8_t num_uuids);
//! If present, returns the number of Service UUIDs the advertisement data
//! contains.
//! @param ad The advertisement data
//! @return If Service UUIDs data is present, the number of UUIDs is contains,
//! or zero otherwise.
uint8_t ble_ad_get_number_of_service_uuids(const BLEAdData *ad);
//! If present, gets the TX Power Level from the advertisement data.
//! @param ad The advertisement data
//! @param[out] tx_power_level_out Will contain the TX Power Level if the return
//! value is true.
//! @return true if the TX Power Level was found and assigned.
bool ble_ad_get_tx_power_level(const BLEAdData *ad, int8_t *tx_power_level_out);
//! If present, copies the Local Name from the advertisement data.
//! If the Local Name is bigger than the size of the buffer, only the part that
//! fits will be copied. For convenience, the copied c-string will always be
//! zero terminated for you.
//! @param ad The advertisement data
//! @param buffer The buffer into which to copy the Local Name, if found.
//! @param size The size of the buffer
//! @return The size of the Local Name in bytes, *including* zero terminator.
//! Note that this might be more than the size of the provided buffer.
//! If the Local Name was not found, the return value will be zero.
size_t ble_ad_copy_local_name(const BLEAdData *ad,
char *buffer, size_t size);
//! If the Local Name is present in the advertisment data, returns the number
//! of bytes a C-string needs to be to hold the full name.
//! @param ad The advertisement data
//! @return The size of the Local Name in bytes, *including* zero terminator.
//! If the Local Name is not present, zero is returned.
size_t ble_ad_get_local_name_buffer_size(const BLEAdData *ad);
//! If present, copies the Manufacturer Specific data from the advertisement
//! data. If the provided buffer is smaller than the size of the data, only
//! the data up that fits the buffer will be copied.
//! @param ad The advertisement data
//! @param[out] company_id_out Out: The Company Identifier Code, see
//! https://www.bluetooth.org/en-us/specification/assigned-numbers/company-identifiers
//! This will only get written, if there was Manufacturer Specific data in the
//! advertisement. In case you are not interested in getting the company ID,
//! NULL can be passed in.
//! @param buffer The buffer into which to copy the Manufacturer Specific Data,
//! if found.
//! @param size The size of the buffer
//! @return The size of the Manufacturer Specific data in bytes. Note that this
//! can be larger than the size of the provided buffer. If the Manufacturer
//! Specific data was not found, the return value will be zero.
//! @see ble_ad_get_manufacturer_specific_data_size
size_t ble_ad_copy_manufacturer_specific_data(const BLEAdData *ad,
uint16_t *company_id_out,
uint8_t *buffer, size_t size);
//! Gets the size in bytes of Manufacturer Specific data in the advertisment.
//! @param ad The advertisement data
//! @return The size of the data, in bytes. If the Manufacturer Specific data is
//! not present, zero is returned.
size_t ble_ad_get_manufacturer_specific_data_size(const BLEAdData *ad);
//! Gets the size in bytes of the raw advertisement and scan response data.
//! @param ad The advertisement data
//! @return The size of the raw advertisement and scan response data in bytes.
size_t ble_ad_get_raw_data_size(const BLEAdData *ad);
//! Copies the raw bytes of advertising and scan response data into a buffer.
//! If there was scan response data, it will be concatenated directly after the
//! advertising data.
//! @param ad The advertisement data
//! @param buffer The buffer into which to copy the raw bytes
//! @param size The size of the buffer
//! @return The number of bytes copied.
size_t ble_ad_copy_raw_data(const BLEAdData *ad, uint8_t *buffer, size_t size);
// -----------------------------------------------------------------------------
// Creating BLEAdData:
// -----------------------------------------------------------------------------
//! Creates a blank, mutable advertisement and scan response payload.
//! It can contain up to 31 bytes of advertisement data and up to 31 bytes of
//! scan response data. The underlying storage for this is automatically
//! allocated.
//! @note When adding elements to the BLEAdData, using the ble_ad_set_...
//! functions, elements will be added to the advertisement data part first.
//! If the element to add does not fit, the scan response is automatically
//! used. Added elements cannot span across the two parts.
//! @return The blank payload object.
//! @note BLEAdData created with this function must be destroyed at some point
//! in time using ble_ad_destroy()
//! @note Use the ble_ad_set... functions to write data into the object. The
//! data written to it will occupy the advertisement payload until there is not
//! enough space left, in which case all following data is written into the scan
//! response. @see ble_ad_start_scan_response()
BLEAdData* ble_ad_create(void);
//! Destroys an advertisement payload that was created earlier with
//! ble_ad_create().
void ble_ad_destroy(BLEAdData *ad);
//! Marks the start of the scan response and finalizes the advertisement
//! payload. This forces successive writes to be written to the scan response,
//! even though it would have fit into the advertisement payload.
void ble_ad_start_scan_response(BLEAdData *ad_data);
//! Writes the Service UUID list to the advertisement or scan response payload.
//! The list is assumed to be the complete list of Service UUIDs.
//! @param ad The advertisement payload as created earlier by ble_ad_create()
//! @param uuids Array of UUIDs to add to the list. If the UUIDs are all derived
//! from the Bluetooth SIG base UUID, this function will automatically use
//! a smaller Service UUID size if possible.
//! @see bt_uuid_expand_16bit
//! @see bt_uuid_expand_32bit
//! @param num_uuids Number of UUIDs in the uuids array.
//! @return true if the data was successfully written or false if not.
bool ble_ad_set_service_uuids(BLEAdData *ad,
const Uuid uuids[], uint8_t num_uuids);
//! Writes the Local Name to the advertisement or scan response payload.
//! @param ad The advertisement payload as created earlier by ble_ad_create()
//! @param local_name Zero terminated, UTF-8 string with the Local Name. The
//! name is assumed to be complete and not abbreviated.
//! @return true if the data was successfully written or false if not.
bool ble_ad_set_local_name(BLEAdData *ad,
const char *local_name);
//! Writes the TX Power Level to advertisement or scan response payload.
//! The actual transmission power level value is set automatically, based on the
//! value as used by the Bluetooth hardware.
//! @param ad The advertisement payload as created earlier by ble_ad_create()
//! @return true if the data was successfully written or false if not.
bool ble_ad_set_tx_power_level(BLEAdData *ad);
//! Writes Manufacturer Specific Data to advertisement or scan response payload.
//! @param ad The advertisement payload as created earlier by ble_ad_create()
//! @param company_id The Company Identifier Code, see
//! https://www.bluetooth.org/en-us/specification/assigned-numbers/company-identifiers
//! @param data The data
//! @param size The size of data in bytes
//! @return true if the data was successfully written or false if not.
bool ble_ad_set_manufacturer_specific_data(BLEAdData *ad,
uint16_t company_id,
const uint8_t *data, size_t size);
//! @internal -- Do not export
//! Writes the Flags AD Type to the advertisement or scan response payload.
//! @param ad The advertisement payload as created earlier by ble_ad_create()
//! @param flags The flags to write. See Core_v4.0.pdf Vol 3, Appendix C, 18.1.
//! @return true if the data was successfully written or false if not.
bool ble_ad_set_flags(BLEAdData *ad, uint8_t flags);

View file

@ -0,0 +1,66 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "ble_app_support.h"
#include "comm/ble/gap_le_scan.h"
#include "comm/ble/gap_le_connect.h"
#include "comm/ble/gatt_client_operations.h"
#include "comm/ble/gatt_client_subscriptions.h"
#include "process_state/app_state/app_state.h"
//! @see ble_scan.c
extern void ble_scan_handle_event(PebbleEvent *e, void *context);
//! @see ble_central.c
extern void ble_central_handle_event(PebbleEvent *e, void *context);
//! @see ble_client.c
extern void ble_client_handle_event(PebbleEvent *e, void *context);
void ble_init_app_state(void) {
BLEAppState *ble_app_state = app_state_get_ble_app_state();
*ble_app_state = (const BLEAppState) {
// ble_scan_...:
.scan_service_info = (const EventServiceInfo) {
.type = PEBBLE_BLE_SCAN_EVENT,
.handler = ble_scan_handle_event,
},
// ble_central_...:
.connection_service_info = (const EventServiceInfo) {
.type = PEBBLE_BLE_CONNECTION_EVENT,
.handler = ble_central_handle_event,
},
// ble_client_...:
.gatt_client_service_info = (const EventServiceInfo) {
.type = PEBBLE_BLE_GATT_CLIENT_EVENT,
.handler = ble_client_handle_event,
},
};
}
void ble_app_cleanup(void) {
// Gets run on the KernelMain task.
// All kernel / shared resources for BLE that were allocated on behalf of the
// app must be freed / released here:
gap_le_stop_scan();
gap_le_connect_cancel_all(GAPLEClientApp);
gatt_client_subscriptions_cleanup_by_client(GAPLEClientApp);
gatt_client_op_cleanup(GAPLEClientApp);
}

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 "ble_scan.h"
#include "ble_central.h"
#include "ble_client.h"
#include "applib/event_service_client.h"
typedef struct {
// Scanning
EventServiceInfo scan_service_info;
BLEScanHandler scan_handler;
// Connecting
EventServiceInfo connection_service_info;
BLEConnectionHandler connection_handler;
// GATT Client
EventServiceInfo gatt_client_service_info;
BLEClientServiceChangeHandler gatt_service_change_handler;
BLEClientReadHandler gatt_characteristic_read_handler;
BLEClientWriteHandler gatt_characteristic_write_handler;
BLEClientSubscribeHandler gatt_characteristic_subscribe_handler;
BLEClientReadDescriptorHandler gatt_descriptor_read_handler;
BLEClientWriteDescriptorHandler gatt_descriptor_write_handler;
uint8_t gatt_client_num_handlers;
} BLEAppState;
//! Initializes the static BLE state for the currently running app.
void ble_init_app_state(void);
//! Must be called by the kernel, after an app is killed to stop any ongoing
//! BLE activity that was running on behalf of the app.
void ble_app_cleanup(void);

View file

@ -0,0 +1,57 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "ble_central.h"
#include "process_state/app_state/app_state.h"
#include "comm/ble/gap_le_connect.h"
static BTErrno prv_bt_errno_for_event(const PebbleBLEConnectionEvent *e) {
if (e->connected) {
return BTErrnoConnected;
}
// FIXME: PBL-35506 We need to re-evaluate what error code to actually use here
return e->hci_reason;
}
void ble_central_handle_event(PebbleEvent *e) {
BLEAppState *ble_app_state = app_state_get_ble_app_state();
if (!ble_app_state->connection_handler) {
return;
}
const PebbleBLEConnectionEvent *conn_event = &e->bluetooth.le.connection;
const BTDeviceInternal device = PebbleEventToBTDeviceInternal(conn_event);
ble_app_state->connection_handler(device.opaque,
prv_bt_errno_for_event(conn_event));
}
BTErrno ble_central_set_connection_handler(BLEConnectionHandler handler) {
BLEAppState *ble_app_state = app_state_get_ble_app_state();
const bool is_subscribed = (ble_app_state->connection_handler != NULL);
ble_app_state->connection_handler = handler;
if (handler) {
if (!is_subscribed) {
event_service_client_subscribe(&ble_app_state->connection_service_info);
}
} else {
if (is_subscribed) {
event_service_client_unsubscribe(&ble_app_state->connection_service_info);
}
}
return BTErrnoOK;
}

View file

@ -0,0 +1,86 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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>
//! Callback that is called for each connection and disconnection event.
//! @param device The device that got (dis)connected
//! @param connection_status BTErrnoConnected if connected, otherwise the
//! reason for the disconnection: BTErrnoConnectionTimeout,
//! BTErrnoRemotelyTerminated, BTErrnoLocallyTerminatedBySystem or
//! BTErrnoLocallyTerminatedByApp.
//! @note See additional notes with ble_central_set_connection_handler()
typedef void (*BLEConnectionHandler)(BTDevice device,
BTErrno connection_status);
//! Registers the connection event handler of the application.
//! This event handler will be called when connections and disconnection occur,
//! for devices for which ble_central_connect() has been called by the
//! application.
//! Only for successful connections and complete disconnections will the event
//! handler be called. Transient issues that might happen during connection
//! establishment will be not be reported to the application. Instead, the
//! system will attempt to initiate a connection to the device again.
//! If this is called again, the previous handler will be unregistered.
//! @param handler The connection event handler of the application
//! @return Always returns BTErrnoOK.
BTErrno ble_central_set_connection_handler(BLEConnectionHandler handler);
//! Attempts to initiate a connection from the application to another device.
//! In case there is no Bluetooth connection to the device yet, this function
//! configures the Bluetooth subsystem to scan for the specified
//! device and instructs it to connect to it as soon as a connectable
//! advertisement has been received. The application will need to register a
//! connection event handler prior to calling ble_central_connect(), using
//! ble_central_set_connection_handler().
//! Outstanding (auto)connection attempts can be cancelled using
//! ble_central_cancel_connect().
//! @note Connections are virtualized. This means that your application needs to
//! call the ble_central_connect, even though the device might already have an
//! actual Bluetooth connection already. This can be the case when connecting
//! to the user's phone: it is likely the system has created a Bluetooth
//! connection already, still the application has to connect internally in order
//! to use the connection.
//! @param device The device to connect to
//! @param auto_reconnect Pass in true to automatically attempt to reconnect
//! again if the connection is lost. The BLEConnectionHandler will be called for
//! each time the device is (re)connected.
//! Pass in false, if the system should connect once only.
//! The BLEConnectionHandler will be called up to one time only.
//! @param is_pairing_required If the application requires that the Bluetooth
//! traffic is encrypted, is_pairing_required can be set to true to let the
//! system transparently set up pairing, or reestablish encryption, if the
//! device is already paired. Only after this security procedure is finished,
//! the BLEConnectionHandler will be called. Note that not all devices support
//! pairing and one of the BTErrnoPairing... errors can result from a failed
//! pairing process. If the application does not require pairing, set to false.
//! @note It is possible that encryption is still enabled, even if the
//! application did not require this.
//! @return BTErrnoOK if the intent to connect was processed successfully, or
//! ... TODO
BTErrno ble_central_connect(BTDevice device,
bool auto_reconnect,
bool is_pairing_required);
//! Attempts to cancel the connection, as initiated by ble_central_connect().
//! The underlying Bluetooth connection might not be disconnected if the
//! connection is still in use by the system. However, as far as the application
//! is concerned, the device is disconnected and the connection handler will
//! be called with BTErrnoLocallyTerminatedByApp.
//! @return BTErrnoOK if the cancelling was successful, or ... TODO
BTErrno ble_central_cancel_connect(BTDevice device);

View file

@ -0,0 +1,49 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "ble_characteristic.h"
#include "syscall/syscall.h"
bool ble_characteristic_is_readable(BLECharacteristic characteristic) {
return (sys_ble_characteristic_get_properties(characteristic) &
BLEAttributePropertyRead);
}
bool ble_characteristic_is_writable(BLECharacteristic characteristic) {
return (sys_ble_characteristic_get_properties(characteristic) &
BLEAttributePropertyWrite);
}
bool ble_characteristic_is_writable_without_response(BLECharacteristic characteristic) {
return (sys_ble_characteristic_get_properties(characteristic) &
BLEAttributePropertyWriteWithoutResponse);
}
bool ble_characteristic_is_subscribable(BLECharacteristic characteristic) {
return (sys_ble_characteristic_get_properties(characteristic) &
(BLEAttributePropertyNotify | BLEAttributePropertyIndicate));
}
bool ble_characteristic_is_notifiable(BLECharacteristic characteristic) {
return (sys_ble_characteristic_get_properties(characteristic) &
BLEAttributePropertyNotify);
}
bool ble_characteristic_is_indicatable(BLECharacteristic characteristic) {
return (sys_ble_characteristic_get_properties(characteristic) &
BLEAttributePropertyIndicate);
}

View file

@ -0,0 +1,99 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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>
//! Gets the UUID for a characteristic.
//! @param characteristic The characteristic for which to get the UUID
//! @return The UUID of the characteristic
Uuid ble_characteristic_get_uuid(BLECharacteristic characteristic);
//! Gets the properties bitset for a characteristic.
//! @param characteristic The characteristic for which to get the properties
//! @return The properties bitset
BLEAttributeProperty ble_characteristic_get_properties(BLECharacteristic characteristic);
//! Gets whether the characteristic is readable or not.
//! @param characteristic The characteristic for which to get its readability.
//! @return true if the characteristic is readable, false if it is not.
bool ble_characteristic_is_readable(BLECharacteristic characteristic);
//! Gets whether the characteristic is writable or not.
//! @param characteristic The characteristic for which to get its write-ability.
//! @return true if the characteristic is writable, false if it is not.
bool ble_characteristic_is_writable(BLECharacteristic characteristic);
//! Gets whether the characteristic is writable without response or not.
//! @param characteristic The characteristic for which to get its write-ability.
//! @return true if the characteristic is writable without response, false if it is not.
bool ble_characteristic_is_writable_without_response(BLECharacteristic characteristic);
//! Gets whether the characteristic is subscribable or not.
//! @param characteristic The characteristic for which to get its subscribability.
//! @return true if the characteristic is subscribable, false if it is not.
bool ble_characteristic_is_subscribable(BLECharacteristic characteristic);
//! Gets whether the characteristic is notifiable or not.
//! @param characteristic The characteristic for which to get its notifiability.
//! @return true if the characteristic is notifiable, false if it is not.
bool ble_characteristic_is_notifiable(BLECharacteristic characteristic);
//! Gets whether the characteristic is indicatable or not.
//! @param characteristic The characteristic for which to get its indicatability.
//! @return true if the characteristic is indicatable, false if it is not.
bool ble_characteristic_is_indicatable(BLECharacteristic characteristic);
//! Gets the service that the characteristic belongs to.
//! @param characteristic The characteristic for which to find the service it
//! belongs to.
//! @return The service owning the characteristic
BLEService ble_characteristic_get_service(BLECharacteristic characteristic);
//! Gets the device that the characteristic belongs to.
//! @param characteristic The characteristic for which to find the device it
//! belongs to.
//! @return The device owning the characteristic.
BTDevice ble_characteristic_get_device(BLECharacteristic characteristic);
//! Gets the descriptors associated with the characteristic.
//! @param characteristic The characteristic for which to get the descriptors
//! @param[out] descriptors_out An array of pointers to descriptors, into which
//! the associated descriptors will be copied.
//! @param num_descriptors The size of the descriptors_out array.
//! @return The total number of descriptors for the service. This might be a
//! larger number than num_descriptors will contain, if the passed array was
//! not large enough to hold all the pointers.
//! @note For convenience, the services are owned by the system and references
//! to services, characteristics and descriptors are guaranteed to remain valid
//! *until the BLEClientServiceChangeHandler is called again* or until
//! application is terminated.
uint8_t ble_characteristic_get_descriptors(BLECharacteristic characteristic,
BLEDescriptor descriptors_out[],
uint8_t num_descriptors);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// (FUTURE / LATER / NOT SCOPED)
// Just to see how symmetric the Server APIs would be:
BLECharacteristic ble_characteristic_create(const Uuid *uuid,
BLEAttributeProperty properties);
BLECharacteristic ble_characteristic_create_with_descriptors(const Uuid *uuid,
BLEAttributeProperty properties,
BLEDescriptor descriptors[],
uint8_t num_descriptors);

View file

@ -0,0 +1,305 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "ble_client.h"
#include "ble_app_support.h"
#include "applib/applib_malloc.auto.h"
#include "process_state/app_state/app_state.h"
#include "syscall/syscall.h"
#include "syscall/syscall_internal.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/math.h"
#include <stdint.h>
// TODO:
// - device name
// - connection speed (?)
// - app_info.json
// - airplane mode service / BT HW state
// - API to detect that accessory is currently already connected to the phone?
// - How to identify? iOS SDK does not expose addresses. Use DIS info? Fall
// back to device name?
static void prv_handle_services_added(
BLEClientServiceChangeHandler handler, BTDeviceInternal device, BTErrno status) {
BLEService services[BLE_GATT_MAX_SERVICES_CHANGED];
uint8_t num_services = sys_ble_client_copy_services(device, services,
BLE_GATT_MAX_SERVICES_CHANGED);
if (num_services != 0) {
handler(device.opaque, BLEClientServicesAdded, services, num_services, status);
}
}
// TODO (PBL-22086): We should really make this easier to do ...
// We can't directly dereference the service discovery info pointer from third
// party apps because it was malloc'ed from the kernel heap. Instead copy the
// info that is used onto a variable which has been allocated from the stack.
DEFINE_SYSCALL(void, sys_get_service_discovery_info, const PebbleBLEGATTClientServiceEvent *e,
PebbleBLEGATTClientServiceEventInfo *info) {
if (PRIVILEGE_WAS_ELEVATED) {
// Note: if we start storing services, we will need to update the size
syscall_assert_userspace_buffer(info, sizeof(*info));
}
*info = (PebbleBLEGATTClientServiceEventInfo) {
.type = e->info->type,
.device = e->info->device,
.status = e->info->status
};
}
static void prv_handle_service_change(const PebbleBLEGATTClientEvent *e) {
BLEAppState *ble_app_state = app_state_get_ble_app_state();
const BLEClientServiceChangeHandler handler =
ble_app_state->gatt_service_change_handler;
if (!handler) {
return;
}
PebbleBLEGATTClientServiceEventInfo info;
sys_get_service_discovery_info((const PebbleBLEGATTClientServiceEvent *)e, &info);
switch (info.type) {
case PebbleServicesAdded:
prv_handle_services_added(handler, info.device, info.status);
break;
case PebbleServicesRemoved:
// TODO (PBL-22087): This is suboptimal. Before we release the BLE API, we should
// either communicate to the App all the handles which have changed or
// allow the getters for removed services to still work for the duration
// of the callback. For now just force a full handle flush and then resync the app
handler(info.device.opaque, BLEClientServicesInvalidateAll, NULL, 0, info.status);
prv_handle_services_added(handler, info.device, info.status);
break;
case PebbleServicesInvalidateAll:
handler(info.device.opaque, BLEClientServicesInvalidateAll, NULL, 0, info.status);
break;
default:
WTF;
}
}
typedef void (*GenericReadHandler)(BLECharacteristic characteristic,
const uint8_t *value,
size_t value_length,
uint16_t value_offset,
BLEGATTError error);
static void prv_consume_read_response(const PebbleBLEGATTClientEvent *e,
GenericReadHandler handler) {
uint8_t *value = NULL;
uint16_t value_length = e->value_length;
const uintptr_t object_ref = e->object_ref;
BLEGATTError gatt_error = e->gatt_error;
// Read Responses / Notifications with 0 length data must not be attempted to be consumed
if (value_length) {
value = (uint8_t *) applib_malloc(value_length);
if (!value) {
gatt_error = BLEGATTErrorLocalInsufficientResources;
value_length = 0;
}
// If there is a read response, we *must* consume it,
// otherwise the events and associated awaiting responses will
// get out of sync with each other.
sys_ble_client_consume_read(object_ref, value, &value_length);
}
if (handler) {
handler(object_ref, value, value_length, 0 /* value_offset (future proofing) */, gatt_error);
}
applib_free(value);
}
static void prv_consume_notifications(const PebbleBLEGATTClientEvent *e,
GenericReadHandler handler) {
uint8_t *value = NULL;
BLEGATTError gatt_error = e->gatt_error;
uint16_t heap_buffer_size = 0;
uint16_t value_length = 0;
bool has_more = sys_ble_client_get_notification_value_length(&value_length);
while (has_more) {
if (heap_buffer_size < value_length) {
const uint16_t new_heap_buffer_size = MIN(value_length, 64 /* arbitrary min size.. */);
applib_free(value);
value = (uint8_t *) applib_malloc(new_heap_buffer_size);
heap_buffer_size = value ? new_heap_buffer_size : 0;
}
if (!value) {
gatt_error = BLEGATTErrorLocalInsufficientResources;
value_length = 0;
}
uintptr_t object_ref;
// Consume, even if we didn't have enough memory, this will eat the notification and free up
// the space in the buffer.
const uint16_t next_value_length = sys_ble_client_consume_notification(&object_ref,
value, &value_length,
&has_more);
if (handler) {
handler(object_ref, value, value_length, 0 /* value_offset (future proofing) */, gatt_error);
}
value_length = next_value_length;
}
applib_free(value);
}
static void prv_handle_notifications(const PebbleBLEGATTClientEvent *e) {
BLEAppState *ble_app_state = app_state_get_ble_app_state();
prv_consume_notifications(e, ble_app_state->gatt_characteristic_read_handler);
}
static void prv_handle_characteristic_read(const PebbleBLEGATTClientEvent *e) {
BLEAppState *ble_app_state = app_state_get_ble_app_state();
prv_consume_read_response(e, ble_app_state->gatt_characteristic_read_handler);
}
static void prv_handle_characteristic_write(const PebbleBLEGATTClientEvent *e) {
BLEAppState *ble_app_state = app_state_get_ble_app_state();
const BLEClientWriteHandler handler = ble_app_state->gatt_characteristic_write_handler;
if (handler) {
handler(e->object_ref, e->gatt_error);
}
}
static void prv_handle_characteristic_subscribe(const PebbleBLEGATTClientEvent *e) {
PBL_LOG(LOG_LEVEL_DEBUG, "TODO: GATT Client Event, subtype=%u", e->subtype);
}
static void prv_handle_descriptor_read(const PebbleBLEGATTClientEvent *e) {
BLEAppState *ble_app_state = app_state_get_ble_app_state();
prv_consume_read_response(e, ble_app_state->gatt_descriptor_read_handler);
}
static void prv_handle_descriptor_write(const PebbleBLEGATTClientEvent *e) {
BLEAppState *ble_app_state = app_state_get_ble_app_state();
const BLEClientWriteDescriptorHandler handler = ble_app_state->gatt_descriptor_write_handler;
if (handler) {
handler(e->object_ref, e->gatt_error);
}
}
static void prv_handle_buffer_empty(const PebbleBLEGATTClientEvent *e) {
// TODO
}
typedef void(*PrvHandler)(const PebbleBLEGATTClientEvent *);
static PrvHandler prv_handler_for_subtype(
PebbleBLEGATTClientEventType event_subtype) {
if (event_subtype >= PebbleBLEGATTClientEventTypeNum) {
WTF;
}
// MT: This is a bit smaller in code size than a switch():
static const PrvHandler handler[] = {
[PebbleBLEGATTClientEventTypeServiceChange] = prv_handle_service_change,
[PebbleBLEGATTClientEventTypeCharacteristicRead] = prv_handle_characteristic_read,
[PebbleBLEGATTClientEventTypeNotification] = prv_handle_notifications,
[PebbleBLEGATTClientEventTypeCharacteristicWrite] = prv_handle_characteristic_write,
[PebbleBLEGATTClientEventTypeCharacteristicSubscribe] = prv_handle_characteristic_subscribe,
[PebbleBLEGATTClientEventTypeDescriptorRead] = prv_handle_descriptor_read,
[PebbleBLEGATTClientEventTypeDescriptorWrite] = prv_handle_descriptor_write,
[PebbleBLEGATTClientEventTypeBufferEmpty] = prv_handle_buffer_empty,
};
return handler[event_subtype];
}
// Exported for ble_app_support.c
void ble_client_handle_event(PebbleEvent *e) {
const PebbleBLEGATTClientEvent *gatt_event = &e->bluetooth.le.gatt_client;
prv_handler_for_subtype(gatt_event->subtype)(gatt_event);
}
static BTErrno prv_set_handler(void *new_handler, off_t struct_offset_bytes) {
BLEAppState *ble_app_state = app_state_get_ble_app_state();
typedef void (*BLEGenericHandler)(void);
BLEGenericHandler *handler_storage =
(BLEGenericHandler *)(((uint8_t *) ble_app_state) + struct_offset_bytes);
const bool had_previous_handler = (*handler_storage == NULL);
*handler_storage = (BLEGenericHandler) new_handler;
if (had_previous_handler) {
if (new_handler) {
if (ble_app_state->gatt_client_num_handlers++ == 0) {
// First GATT handler to be registered.
// Subscribe to GATT Client event service:
event_service_client_subscribe(&ble_app_state->gatt_client_service_info);
}
}
} else {
if (!new_handler) {
if (--ble_app_state->gatt_client_num_handlers == 0) {
// Last GATT handler to be de-registered.
// Unsubscribe from GATT Client event service:
event_service_client_unsubscribe(&ble_app_state->gatt_client_service_info);
}
}
}
return BTErrnoOK;
}
BTErrno ble_client_set_service_filter(const Uuid service_uuids[],
uint8_t num_uuids) {
// TODO
return 0;
}
BTErrno ble_client_set_service_change_handler(BLEClientServiceChangeHandler handler) {
const off_t offset = offsetof(BLEAppState, gatt_service_change_handler);
return prv_set_handler(handler, offset);
}
BTErrno ble_client_set_read_handler(BLEClientReadHandler handler) {
const off_t offset = offsetof(BLEAppState, gatt_characteristic_read_handler);
return prv_set_handler(handler, offset);
}
BTErrno ble_client_set_write_response_handler(BLEClientWriteHandler handler) {
const off_t offset = offsetof(BLEAppState, gatt_characteristic_write_handler);
return prv_set_handler(handler, offset);
}
BTErrno ble_client_set_subscribe_handler(BLEClientSubscribeHandler handler) {
const off_t offset = offsetof(BLEAppState, gatt_characteristic_subscribe_handler);
return prv_set_handler(handler, offset);
}
BTErrno ble_client_set_buffer_empty_handler(BLEClientBufferEmptyHandler empty_handler) {
// TODO
return BTErrnoOther;
}
BTErrno ble_client_set_descriptor_write_handler(BLEClientWriteDescriptorHandler handler) {
const off_t offset = offsetof(BLEAppState, gatt_descriptor_write_handler);
return prv_set_handler(handler, offset);
}
BTErrno ble_client_set_descriptor_read_handler(BLEClientReadDescriptorHandler handler) {
const off_t offset = offsetof(BLEAppState, gatt_descriptor_read_handler);
return prv_set_handler(handler, offset);
}

View file

@ -0,0 +1,345 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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>
typedef enum {
BLEClientServicesAdded,
BLEClientServicesRemoved,
BLEClientServicesInvalidateAll
} BLEClientServiceChangeUpdate;
//! Callback that is called when the services on a remote device that are
//! available to the application have changed. It gets called when:
//! + the initial discovery of services completes
//! + a new service has been discovered on the device after initial discovery
//! + a service has been removed
//! + the remote device has disconnected
//!
//! @note For convenience, the services are owned by the system and references
//! to services, characteristics and descriptors are guaranteed to remain valid
//! *until the service exposing the characteristic or descriptor is removed or
//! all services are invalidated* or until application is terminated.
//! @note References to services, characteristics and descriptors for the
//! specified device, that have been obtained in a previous callback to the
//! handler, are no longer valid. The application is responsible for cleaning up
//! these references.
//!
//! @param device The device associated with the service discovery process
//! @param update_type Defines what type of service update is being received
//! @param services Array of pointers to the discovered services. The array will
//! only contain Service UUIDs that matches the filter that was passed into the
//! ble_client_discover_services_and_characteristics() call.
//! @param num_services The number of discovered services matching the criteria.
//! @param status BTErrnoOK if the service discovery was completed successfully.
//! If the device was disconnected, BTErrnoConnectionTimeout,
//! BTErrnoRemotelyTerminated, BTErrnoLocallyTerminatedBySystem or
//! BTErrnoLocallyTerminatedByApp will specify the reason of the disconnection.
//! The number of services will be zero upon a disconnection.
typedef void (*BLEClientServiceChangeHandler)(BTDevice device,
BLEClientServiceChangeUpdate update_type,
const BLEService services[],
uint8_t num_services,
BTErrno status);
//! Registers the callback that handles service changes. After the call to
//! this function returns, the application should be ready to handle calls to
//! the handler.
//! @param handler Pointer to the function that will handle the service changes
//! @return BTErrnoOK if the handler was registered successfully,
//! or TODO....
BTErrno ble_client_set_service_change_handler(BLEClientServiceChangeHandler handler);
//! Registers the filter list of Service UUIDs.
//! @param service_uuids An array of the Service UUIDs that the application is
//! interested in and the system should filter by. Passing NULL will discover
//! all services on the device.
//! @param num_uuids The number of Uuid`s in the service_uuids array. Ignored
//! when NULL is passed for the service_uuids argument.
//! @return BTErrnoOK if the filter was set up successfully,
//! or TODO....
BTErrno ble_client_set_service_filter(const Uuid service_uuids[],
uint8_t num_uuids);
//! Starts a discovery of services and characteristics on a remote device.
//! The discovered services will be delivered to the application through the
//! BLEClientServiceChangeHandler. The results will be filtered with the list
//! of Service UUIDs as configured with ble_client_set_service_filter().
//! @param device The device for which to perform service discovery.
//! @return BTErrnoOK if the service discovery started successfully,
//! BTErrnoInvalidParameter if the device was not connected,
//! BTErrnoInvalidState if service discovery was already on-going, or
//! an internal error otherwise (>= BTErrnoInternalErrorBegin).
BTErrno ble_client_discover_services_and_characteristics(BTDevice device);
//! Different subscription types that can be used with ble_client_subscribe()
typedef enum {
//! No subscription.
BLESubscriptionNone = 0,
//! Notification subscription.
BLESubscriptionNotifications = (1 << 0),
//! Indication subscription.
BLESubscriptionIndications = (1 << 1),
//! Any subscription. Use this value with ble_client_subscribe(), in case
//! the application does not care about the type of subscription. If both
//! types are supported by the server, the notification subscription type
//! will be used.
BLESubscriptionAny = BLESubscriptionNotifications | BLESubscriptionIndications,
} BLESubscription;
//! Callback to receive the characteristic value, resulting from either
//! ble_client_read() and/or ble_client_subscribe().
//! @param characteristic The characteristic of the received value
//! @param value Byte-array containing the value
//! @param value_length The number of bytes the byte-array contains
//! @param value_offset The offset in bytes from the start of the characteristic
//! value that has been read.
//! @param error The error or status as returned by the remote server. If the
//! read was successful, this remote server is supposed to send
//! BLEGATTErrorSuccess.
typedef void (*BLEClientReadHandler)(BLECharacteristic characteristic,
const uint8_t *value,
size_t value_length,
uint16_t value_offset,
BLEGATTError error);
//! Callback to handle the response to a written characteristic, resulting from
//! ble_client_write().
//! @param characteristic The characteristic that was written to.
//! @param error The error or status as returned by the remote server. If the
//! write was successful, this remote server is supposed to send
//! BLEGATTErrorSuccess.
typedef void (*BLEClientWriteHandler)(BLECharacteristic characteristic,
BLEGATTError error);
//! Callback to handle the confirmation of a subscription or unsubscription to
//! characteristic value changes (notifications or indications).
//! @param characteristic The characteristic for which the client is now
//! (un)subscribed.
//! @param subscription_type The type of subscription. If the client is now
//! unsubscribed, the type will be BLESubscriptionNone.
//! @param The error or status as returned by the remote server. If the
//! (un)subscription was successful, this remote server is supposed to send
//! BLEGATTErrorSuccess.
typedef void (*BLEClientSubscribeHandler)(BLECharacteristic characteristic,
BLESubscription subscription_type,
BLEGATTError error);
//! Callback to handle the event that the buffer for outbound data is empty.
typedef void (*BLEClientBufferEmptyHandler)(void);
//! Registers the handler for characteristic value read operations.
//! @param read_handler Pointer to the function that will handle callbacks
//! with read characteristic values as result of calls to ble_client_read().
//! @return BTErrnoOK if the handlers were successfully registered, or ... TODO
BTErrno ble_client_set_read_handler(BLEClientReadHandler read_handler);
//! Registers the handler for characteristic value write (with response)
//! operations.
//! @param write_handler Pointer to the function that will handle callbacks
//! for written characteristic values as result of calls to ble_client_write().
//! @return BTErrnoOK if the handlers were successfully registered, or ... TODO
BTErrno ble_client_set_write_response_handler(BLEClientWriteHandler write_handler);
//! Registers the handler for characteristic value subscribe operations.
//! @param subscribe_handler Pointer to the function that will handle callbacks
//! for (un)subscription confirmations as result of calls to
//! ble_client_subscribe().
//! @return BTErrnoOK if the handlers were successfully registered, or ... TODO
BTErrno ble_client_set_subscribe_handler(BLEClientSubscribeHandler subscribe_handler);
//! Registers the handler to get called back when the buffer for outbound
//! data is empty again.
//! @param empty_handler Pointer to the function that will handle callbacks
//! for "buffer empty" events.
//! @return BTErrnoOK if the handlers were successfully registered, or ... TODO
BTErrno ble_client_set_buffer_empty_handler(BLEClientBufferEmptyHandler empty_handler);
//! Gets the maximum characteristic value size that can be written. This size
//! can vary depending on the connected device.
//! @param device The device for which to get the maximum characteristic value
//! size.
//! @return The maximum characteristic value size that can be written.
uint16_t ble_client_get_maximum_value_length(BTDevice device);
//! Read the value of a characteristic.
//! A call to this function will result in a callback to the registered
//! BLEClientReadHandler handler. @see ble_client_set_read_handler.
//! @param characteristic The characteristic for which to read the value
//! @return BTErrnoOK if the operation was successfully started, or ... TODO
BTErrno ble_client_read(BLECharacteristic characteristic);
//! Write the value of a characterstic.
//! A call to this function will result in a callback to the registered
//! BLEClientWriteHandler handler. @see ble_client_set_write_response_handler.
//! @param characteristic The characteristic for which to write the value
//! @param value Buffer with the value to write
//! @param value_length Number of bytes to write
//! @note Values must not be longer than ble_client_get_maximum_value_length().
//! @return BTErrnoOK if the operation was successfully started, or ... TODO
BTErrno ble_client_write(BLECharacteristic characteristic,
const uint8_t *value,
size_t value_length);
//! Write the value of a characterstic without response.
//! @param characteristic The characteristic for which to write the value
//! @param value Buffer with the value to write
//! @param value_length Number of bytes to write
//! @note Values must not be longer than ble_client_get_maximum_value_length().
//! @return BTErrnoOK if the operation was successfully started, or ... TODO
//! If the buffer for outbound data was full, BTErrnoNotEnoughResources will
//! be returned. When the buffer is emptied, the handler that is registered
//! using ble_client_set_buffer_empty_handler() will be called.
BTErrno ble_client_write_without_response(BLECharacteristic characteristic,
const uint8_t *value,
size_t value_length);
//! Subscribe to be notified or indicated of value changes of a characteristic.
//!
//! The value updates are delivered to the application through the
//! BLEClientReadHandler, which should be registered before calling this
//! function using ble_client_set_read_handler().
//!
//! There are two types of subscriptions: notifications and indications.
//! For notifications there is no flow-control. This means that notifications
//! can get dropped if the rate at which they are sent is too high. Conversely,
//! each indication needs an acknowledgement from the receiver before the next
//! one can get sent and is thus more reliable. The system performs
//! acknowledgements to indications automatically. Applications do not need to
//! worry about this, nor can they affect this.
//! @param characteristic The characteristic to subscribe to.
//! @param subscription_type The type of subscription to use.
//! If BLESubscriptionAny is used as subscription_type and both types are
//! supported by the server, the notification subscription type will be used.
//! @note This call does not block and returns quickly. A callback to
//! the BLEClientSubscribeHandler will happen at a later point in time, to
//! report the success or failure of the subscription. This handler should be
//! registered before calling this function using
//! ble_client_set_subscribe_handler().
//! @note Under the hood, this API writes to the Client Characteristic
//! Configuration Descriptor's Notifications or Indications enabled/disabled
//! bit.
//! @return BTErrnoOK if the subscription request was sent sucessfully, or
//! TODO...
BTErrno ble_client_subscribe(BLECharacteristic characteristic,
BLESubscription subscription_type);
//! Callback to receive the descriptor value, resulting from a call to
//! ble_client_read_descriptor().
//! @param descriptor The descriptor of the received value
//! @param value Byte-array containing the value
//! @param value_length The number of bytes the byte-array contains
//! @param value_offset The offset in bytes from the start of the descriptor
//! value that has been read.
//! @param error The error or status as returned by the remote server. If the
//! read was successful, this remote server is supposed to send
//! BLEGATTErrorSuccess.
typedef void (*BLEClientReadDescriptorHandler)(BLEDescriptor descriptor,
const uint8_t *value,
size_t value_length,
uint16_t value_offset,
BLEGATTError error);
//! Callback to handle the response to a written descriptor, resulting from
//! ble_client_write_descriptor().
//! @param descriptor The descriptor that was written to.
//! @param error The error or status as returned by the remote server. If the
//! write was successful, this remote server is supposed to send
//! BLEGATTErrorSuccess.
typedef void (*BLEClientWriteDescriptorHandler)(BLEDescriptor descriptor,
BLEGATTError error);
//! Registers the handlers for descriptor value write operations.
//! @param write_handler Pointer to the function that will handle callbacks
//! for written descriptor values as result of calls to
//! ble_client_write_descriptor().
//! @return BTErrnoOK if the handlers were successfully registered, or ... TODO
BTErrno ble_client_set_descriptor_write_handler(BLEClientWriteDescriptorHandler write_handler);
//! Registers the handlers for descriptor value read operations.
//! @param read_handler Pointer to the function that will handle callbacks
//! with read descriptor values as result of calls to
//! ble_client_read_descriptor().
//! @return BTErrnoOK if the handlers were successfully registered, or ... TODO
BTErrno ble_client_set_descriptor_read_handler(BLEClientReadDescriptorHandler read_handler);
//! Write the value of a descriptor.
//! A call to this function will result in a callback to the registered
//! BLEClientWriteDescriptorHandler handler.
//! @see ble_client_set_descriptor_write_handler.
//! @param descriptor The descriptor for which to write the value
//! @param value Buffer with the value to write
//! @param value_length Number of bytes to write
//! @note Values must not be longer than ble_client_get_maximum_value_length().
//! @return BTErrnoOK if the operation was successfully started, or ... TODO
BTErrno ble_client_write_descriptor(BLEDescriptor descriptor,
const uint8_t *value,
size_t value_length);
//! Read the value of a descriptor.
//! A call to this function will result in a callback to the registered
//! BLEClientReadDescriptorHandler handler.
//! @see ble_client_set_descriptor_read_handler.
//! @param descriptor The descriptor for which to read the value
//! @return BTErrnoOK if the operation was successfully started, or ... TODO
BTErrno ble_client_read_descriptor(BLEDescriptor descriptor);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// (FUTURE / LATER / NOT SCOPED)
// Just to see how symmetric the Server APIs would be:
//! Opaque ATT request context
typedef void * BLERequest;
typedef void (*BLEServerWriteHandler)(BLERequest request,
BLECharacteristic characteristic,
BTDevice remote_device,
const uint8_t *value,
size_t value_length,
uint16_t value_offset);
typedef void (*BLEServerReadHandler)(BLECharacteristic characteristic,
BTDevice remote_device,
uint16_t value_offset);
typedef void (*BLEServerSubscribeHandler)(BLECharacteristic characteristic,
BTDevice remote_device,
BLESubscription subscription_type);
BTErrno ble_server_set_handlers(BLEServerReadHandler read_handler,
BLEServerWriteHandler write_handler,
BLEServerSubscribeHandler subscription_handler);
BTErrno ble_server_start_service(BLEService service);
BTErrno ble_server_stop_service(BLEService service);
BTErrno ble_server_respond_to_write(BLERequest request, BLEGATTError error);
BTErrno ble_server_respond_to_read(BLERequest request, BLEGATTError error,
const uint8_t *value, size_t value_length,
uint16_t value_offset);
BTErrno ble_server_send_update(BLECharacteristic characteristic,
const uint8_t *value, size_t value_length);
BTErrno ble_server_send_update_selectively(BLECharacteristic characteristic,
const uint8_t *value, size_t value_length,
const BTDevice *devices, uint8_t num_devices);

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 <bluetooth/bluetooth_types.h>
//! Gets the UUID for a descriptor.
//! @param descriptor The descriptor for which to get the UUID.
//! @return The UUID of the descriptor
Uuid ble_descriptor_get_uuid(BLEDescriptor descriptor);
//! Gets the characteristic for a descriptor.
//! @param descriptor The descriptor for which to get the characteristic.
//! @return The characteristic
//! @note For convenience, the services are owned by the system and references
//! to services, characteristics and descriptors are guaranteed to remain valid
//! *until the BLEClientServiceChangeHandler is called again* or until
//! application is terminated.
BLECharacteristic ble_descriptor_get_characteristic(BLEDescriptor descriptor);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// (FUTURE / LATER / NOT SCOPED)
// Just to see how symmetric the Server APIs would be:
BLEDescriptor ble_descriptor_create(const Uuid *uuid,
BLEAttributeProperty properties);
BTErrno ble_descriptor_destroy(BLEDescriptor descriptor);

View file

@ -0,0 +1,17 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "ble_device.h"

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 <bluetooth/bluetooth_types.h>
//! Copies the devices that are known to the system. This set includes all
//! paired devices (connected or not) and devices for which there is a Bluetooth
//! connection to the system (but not necessarily paired and not necessarily
//! connected to the application).
//! @param[out] devices_out An array of BTDevice`s into which the known
//! devices will be copied.
//! @param[in,out] num_devices_out In: the size of the devices_out array.
//! Out: the number of BTDevice`s that were copied.
//! @return The total number of known devices. This might be a larger
//! number than num_devices_out will contain, if the passed array was not large
//! enough to hold all the connected devices.
uint8_t ble_device_copy_known_devices(BTDevice *devices_out,
uint8_t *num_devices_out);

View file

@ -0,0 +1,145 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "ble_ibeacon.h"
#include "applib/applib_malloc.auto.h"
#include "util/net.h"
#include <string.h>
// -----------------------------------------------------------------------------
//! Apple's iBeacon AD DATA format.
//! The byte-order of Apple's fields (uuid, major and minor) is Big Endian (!!!)
//! @see Apple's docs for more info: http://goo.gl/iOrnpj
//! @see StackOverflow distance/accuracy calculations: http://goo.gl/yH0ubM
static const uint16_t COMPANY_ID_APPLE = 0x004c;
static const uint8_t APPLE_TYPE_IBEACON = 0x02;
static const uint8_t APPLE_IBEACON_LENGTH = 0x15;
typedef struct __attribute__((__packed__)) {
//! @see APPLE_AD_TYPE_IBEACON
uint8_t type;
//! @see APPLE_IBEACON_LENGTH
uint8_t length;
//! The application "proximityUUID" of the iBeacon. Generally, multiple
//! iBeacons share one UUID and an (iOS) app scans for one particular UUID.
uint8_t uuid[16];
//! The most significant value in the beacon.
uint16_t major;
//! The least significant value in the beacon.
uint16_t minor;
//! The calibrated transmit power.
int8_t calibrated_tx_power;
} AdDataManufacturerSpecificAppleiBeacon;
// -----------------------------------------------------------------------------
// Accessors
Uuid ble_ibeacon_get_uuid(const BLEiBeacon *ibeacon) {
return ibeacon->uuid;
}
uint16_t ble_ibeacon_get_major(const BLEiBeacon *ibeacon) {
return ibeacon->major;
}
uint16_t ble_ibeacon_get_minor(const BLEiBeacon *ibeacon) {
return ibeacon->minor;
}
uint16_t ble_ibeacon_get_distance_cm(const BLEiBeacon *ibeacon) {
return ibeacon->distance_cm;
}
BLEiBeacon *ble_ibeacon_create_from_ad_data(const BLEAdData *ad,
int8_t rssi) {
// Note, not yet exported to 3rd party apps so no padding necessary
BLEiBeacon *ibeacon = applib_malloc(sizeof(BLEiBeacon));
if (ibeacon && !ble_ibeacon_parse(ad, rssi, ibeacon)) {
// Failed to parse.
applib_free(ibeacon);
ibeacon = NULL;
}
return ibeacon;
}
void ble_ibeacon_destroy(BLEiBeacon *ibeacon) {
applib_free(ibeacon);
}
// -----------------------------------------------------------------------------
// Below is the iBeacon advertisement parsing code.
static uint16_t calculate_distance_cm(int8_t tx_power, int8_t rssi) {
return 0; // TODO
}
// -----------------------------------------------------------------------------
//! iBeacon Advertisement Data parser
bool ble_ibeacon_parse(const BLEAdData *ad, int8_t rssi,
BLEiBeacon *ibeacon_out) {
uint16_t company_id = 0;
AdDataManufacturerSpecificAppleiBeacon raw_ibeacon;
const size_t size_copied =
ble_ad_copy_manufacturer_specific_data(ad, &company_id,
(uint8_t *) &raw_ibeacon,
sizeof(raw_ibeacon));
if (size_copied != sizeof(raw_ibeacon)) {
return false;
}
if (company_id == COMPANY_ID_APPLE &&
raw_ibeacon.type == APPLE_TYPE_IBEACON &&
raw_ibeacon.length == APPLE_IBEACON_LENGTH) {
const int8_t tx_power = raw_ibeacon.calibrated_tx_power;
*ibeacon_out = (const BLEiBeacon) {
.uuid = UuidMakeFromBEBytes(raw_ibeacon.uuid),
.major = ntohs(raw_ibeacon.major),
.minor = ntohs(raw_ibeacon.minor),
.distance_cm = calculate_distance_cm(tx_power, rssi),
.rssi = rssi,
.calibrated_tx_power = tx_power,
};
return true;
}
return false;
}
// -----------------------------------------------------------------------------
//! iBeacon Advertisement Data composer
bool ble_ibeacon_compose(const BLEiBeacon *ibeacon_in, BLEAdData *ad_out) {
AdDataManufacturerSpecificAppleiBeacon raw_ibeacon = {
.type = APPLE_TYPE_IBEACON,
.length = APPLE_IBEACON_LENGTH,
// Major/Minor are part of Apple's iBeacon spec and are Big Endian!
.major = htons(ibeacon_in->major),
.minor = htons(ibeacon_in->minor),
.calibrated_tx_power = ibeacon_in->calibrated_tx_power,
};
// Uuid is stored Big Endian on Pebble, so just copy over:
memcpy(&raw_ibeacon.uuid, &ibeacon_in->uuid, sizeof(Uuid));
return ble_ad_set_manufacturer_specific_data(ad_out, COMPANY_ID_APPLE,
(uint8_t *) &raw_ibeacon,
sizeof(raw_ibeacon));
}

View file

@ -0,0 +1,105 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "applib/bluetooth/ble_ad_parse.h"
//! Size in bytes of the iBeacon advertisement data, including the length and
//! AD Type bytes.
#define IBEACON_ADVERTISEMENT_DATA_SIZE (27)
//! Data structure representing an iBeacon advertisement.
typedef struct {
//! The application UUID that the iBeacon advertised. In iOS' CoreBluetooth,
//! this corresponds to the "proximityUUID" property of instances of CLBeacon.
Uuid uuid;
//! Custom value, most significant part.
uint16_t major;
//! Custom value, least significant part.
uint16_t minor;
//! Estimated distance to the iBeacon in centimeters. In iOS' CoreBluetooth,
//! this corresponds to the "accuracy" property of instances of CLBeacon.
uint16_t distance_cm;
//! The received signal strength from the iBeacon, in decibels.
int8_t rssi;
//! The calibrated power of the iBeacon. This is the RSSI measured at 1 meter
//! distance from the iBeacon. The iBeacon transmits this information in its
//! advertisment. Using this and the actual RSSI, the distance is estimated.
int8_t calibrated_tx_power;
} BLEiBeacon;
//! Gets the UUID of the iBeacon.
//! @param The iBeacon
//! @return The UUID that the iBeacon advertised. In iOS' CoreBluetooth,
//! this corresponds to the "proximityUUID" property of instances of CLBeacon.
Uuid ble_ibeacon_get_uuid(const BLEiBeacon *ibeacon);
//! Gets the major value of the iBeacon.
//! @param The iBeacon
//! @return The major, custom value.
uint16_t ble_ibeacon_get_major(const BLEiBeacon *ibeacon);
//! Gets the minor value of the iBeacon.
//! @param The iBeacon
//! @return The minor, custom value.
uint16_t ble_ibeacon_get_minor(const BLEiBeacon *ibeacon);
//! Gets the estimated distance to the iBeacon, in centimeters.
//! @param The iBeacon
//! @return The estimated distance in centimeters.
uint16_t ble_ibeacon_get_distance_cm(const BLEiBeacon *ibeacon);
//! Create BLEiBeacon from advertisement data.
//! @param ad Advertisement data, as acquired from the BLEScanHandler callback.
//! @param rssi The RSSI of the advertisement, as acquired from the
//! BLEScanHandler callback.
//! @return BLEiBeacon object if iBeacon data is found, or NULL if the
//! advertisement data did not contain valid iBeacon data.
BLEiBeacon *ble_ibeacon_create_from_ad_data(const BLEAdData *ad,
int8_t rssi);
//! Destroys an BLEiBeacon object and frees its resources that were allocated
//! earlier by ble_ibeacon_create_from_ad_data().
//! @param ibeacon Reference to the BLEiBeacon to destroy.
void ble_ibeacon_destroy(BLEiBeacon *ibeacon);
// -----------------------------------------------------------------------------
//! Internal iBeacon Advertisement Data parser
//! @param ad The raw advertisement data
//! @param rssi The RSSI of the advertisement
//! @param[out] ibeacon_out Will contain the parsed iBeacon data if the call
//! returns true.
//! @return true if the data element was succesfully parsed as iBeacon,
//! false if the data element could not be parsed as iBeacon.
bool ble_ibeacon_parse(const BLEAdData *ad, int8_t rssi,
BLEiBeacon *ibeacon_out);
// -----------------------------------------------------------------------------
//! Internal iBeacon Advertisement Data serializer
//! @param ibeacon_in The iBeacon structure to serialize. The rssi and
//! distance_cm fields are ignored because they are only valid for received
//! iBeacon packets.
//! @param[out] ad_out The advertisement payload to write the data into.
//! @return true if the iBeacon data was written successfully.
bool ble_ibeacon_compose(const BLEiBeacon *ibeacon_in, BLEAdData *ad_out);

View file

@ -0,0 +1,109 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "ble_scan.h"
#include "ble_ad_parse.h"
#include "applib/app_logging.h"
#include "applib/applib_malloc.auto.h"
#include "process_state/app_state/app_state.h"
#include "comm/ble/gap_le_scan.h"
#include "kernel/events.h"
#include "syscall/syscall.h"
void ble_scan_handle_event(PebbleEvent *e) {
BLEAppState *ble_app_state = app_state_get_ble_app_state();
if (!ble_app_state->scan_handler) {
return;
}
// Use the same buffer size as the kernel itself:
uint8_t *buffer = (uint8_t *) applib_malloc(GAP_LE_SCAN_REPORTS_BUFFER_SIZE);
if (!buffer) {
APP_LOG(LOG_LEVEL_ERROR, "Need %u bytes of heap for ble_scan_start()",
GAP_LE_SCAN_REPORTS_BUFFER_SIZE);
return;
}
uint16_t size = GAP_LE_SCAN_REPORTS_BUFFER_SIZE;
sys_ble_consume_scan_results(buffer, &size);
if (size == 0) {
goto finally;
}
// Walk all the reports in the buffer:
const uint8_t *cursor = buffer;
while (cursor < buffer + size) {
const GAPLERawAdReport *report = (GAPLERawAdReport *)cursor;
const BTDeviceInternal device = (const BTDeviceInternal) {
.address = report->address.address,
.is_classic = false,
.is_random_address = report->is_random_address,
};
// Call the scan handler for each report:
ble_app_state->scan_handler(device.opaque, report->rssi, &report->payload);
const size_t report_length = sizeof(GAPLERawAdReport) +
report->payload.ad_data_length +
report->payload.scan_resp_data_length;
cursor += report_length;
}
finally:
applib_free(buffer);
}
BTErrno ble_scan_start(BLEScanHandler handler) {
if (!handler) {
return (BTErrnoInvalidParameter);
}
BLEAppState *ble_app_state = app_state_get_ble_app_state();
if (ble_app_state->scan_handler) {
return (BTErrnoInvalidState);
}
const bool result = sys_ble_scan_start();
if (!result) {
return BTErrnoOther;
}
ble_app_state->scan_handler = handler;
event_service_client_subscribe(&ble_app_state->scan_service_info);
return BTErrnoOK;
}
BTErrno ble_scan_stop(void) {
BLEAppState *ble_app_state = app_state_get_ble_app_state();
if (!ble_app_state->scan_handler) {
return (BTErrnoInvalidState);
}
const bool result = sys_ble_scan_stop();
if (!result) {
return BTErrnoOther;
}
event_service_client_unsubscribe(&ble_app_state->scan_service_info);
ble_app_state->scan_handler = NULL;
return BTErrnoOK;
}
bool ble_scan_is_scanning(void) {
return sys_ble_scan_is_scanning();
}

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.
*/
#pragma once
#include <bluetooth/bluetooth_types.h>
//! Callback that is called for each advertisment that is found while scanning
//! using ble_scan_start().
//! @param device The device from which the advertisment originated.
//! @param rssi The RSSI (Received Signal Strength Indication) of the
//! advertisement.
//! @param advertisement_data The payload of the advertisement. When there was
//! a scan response, this payload will contain the data of the scan response
//! as well. The information in the payload can be accessed using the
//! ble_ad_... functions, @see for example ble_ad_copy_local_name() and
//! ble_ad_includes_service().
//! @note The advertisement_data is cleaned up by the system automatically
//! immediately after returning from this callback. Do not keep around
//! any long-lived references around to the advertisement_data.
//! @note Do not use ble_ad_destroy() on the advertisement_data.
typedef void (*BLEScanHandler)(BTDevice device,
int8_t rssi,
const BLEAdData *advertisement_data);
//! Start scanning for advertisements. Pebble will scan actively, meaning it
//! will perform scan requests whenever the advertisement is scannable.
//! @param handler The callback to handle the found advertisments. It must not
//! be NULL.
//! @return BTErrnoOK if scanning started successfully, BTErrnoInvalidParameter
//! if the handler was invalid or BTErrnoInvalidState if scanning had already
//! been started.
BTErrno ble_scan_start(BLEScanHandler handler);
//! Stop scanning for advertisements.
//! @return BTErrnoOK if scanning stopped successfully, or TODO...
BTErrno ble_scan_stop(void);
//! @return True if the system is scanning for advertisements or false if not.
bool ble_scan_is_scanning(void);

View file

@ -0,0 +1,17 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "ble_security.h"

View file

@ -0,0 +1,94 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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>
//------------------------------------------------------------------------------
// Out-Of-Band additions
//! "Out-of-Band" (OOB) is one of the mechanisms to exchange a shared secret
//! during a pairing procedure between two devices. "PIN" and "Just Works" are
//! the two other exchange mechanisms that the Bluetooth 4.0 Specification
//! defines, but both are susceptible to eavesdropping of the exchanged keys.
//! OOB provides better protection against this, by offering a way to exchange
//! the shared secret via a communications channel other than Bluetooth itself
//! (hence the name "Out-of-Band"). Of course, this is only more secure if the
//! channel through which the OOB data is exchanged itself is harder to
//! eavesdrop.
//!
//! The exchanged OOB data is used as Temporary-Key (TK) to encrypt the
//! connection during the one-time pairing information exchange. Part of this
//! information exchange are Long-Term-Key(s) (LTK) that will be used upon
//! successive reconnections. For more details, see Bluetooth 4.0 Specification,
//! Volume 3, Part H, 2.3.5, "Pairing Algorithms".
//!
//! The OOB APIs enable the application to provide the system with OOB data.
//! The application will need to indicate to the system for what devices it
//! is capable of providing OOB data. Later, when a pairing procedure takes
//! place with an OOB-enabled device, the system will ask the application to
//! provide that OOB data.
//!
//! It is up to the application and the manufacturer of the device how the OOB
//! data is exchanged between the application and the remote device. Examples of
//! how this can be done:
//! - The application could generate the OOB data and show a QR code containing
//! the data on the screen of the Pebble that is then read by the device.
//! - If the device is connected to the Internet, the OOB data could be
//! provisioned to Pebble via a web service. The application would use the
//! JavaScript APIs to fetch the data from the web service and transfer the
//! data to the application on the watch using the AppMessage APIs.
//! Pointer to a function that can provide Out-Of-Band keys.
//! @see ble_security_set_oob_handler() and ble_security_enable_oob()
//! @param device The device for which the OOB key needs to be provided
//! @param oob_key_buffer_out The buffer into which the OOB key should be
//! written.
//! @param oob_key_buffer_size The size of the buffer in bytes. Currently only
//! keys of 128-bit (16 byte) size are supported.
//! @return true if the OOB key was written or false if no OOB data could be
//! provided for the device.
typedef bool (*BLESecurityOOBHandler)(BTDevice device,
uint8_t *oob_key_buffer_out,
size_t oob_key_buffer_size);
//! Registers a permanent callback function that is responsible for providing
//! Out-Of-Band (OOB) keys. The callback is guaranteed to get called only for
//! devices for which the application has enabled OOB using
//! ble_security_enable_oob(). The callback will get called by the system during
//! a pairing procedure, but only if the remote device indicated to have OOB
//! data as well.
//! @param oob_handler Pointer to the function that will provide OOB key data.
//! @return BTErrnoOK if the call was successful, or TODO...
BTErrno ble_security_set_oob_handler(BLESecurityOOBHandler oob_handler);
//! Enable or disable Out-Of-Band pairing for the device.
//! This function is a way to indicate to the system that the application has
//! Out-Of-Band data that can be used when pairing with a particular device.
//! @note The application is encouraged to configure OOB as soon as possible,
//! *before* connecting to any devices. If the application supports OOB, but
//! enables is *after* connecting, there is a chance that the remote requests to
//! start pairing before your application has had the chance to enable OOB.
//! @note After terminating the application, the system will automatically
//! disable OOB for any devices it had enabled OOB for. Upon re-launching the
//! application, it will need to re-enable OOB if required.
//! @param device The device for which to enable or disable OOB
//! @param enable Pass in true to enable OOB for the device, or false to disable
//! @return BTErrnoOK if the call was successful, or TODO...
BTErrno ble_security_enable_oob(BTDevice device, bool enable);

View file

@ -0,0 +1,17 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "ble_service.h"

View file

@ -0,0 +1,88 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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>
//! Gets the characteristics associated with a service.
//! @param service The service for which to get the characteristics
//! @param[out] characteristics_out An array of pointers to characteristics,
//! into which the associated characteristics will be copied.
//! @param num_characteristics The size of the characteristics_out array.
//! @return The total number of characteristics for the service. This might be a
//! larger number than num_in_out will contain, if the passed array was not
//! large enough to hold all the pointers.
//! @note For convenience, the services are owned by the system and references
//! to services, characteristics and descriptors are guaranteed to remain valid
//! *until the BLEClientServiceChangeHandler is called again* or until
//! application is terminated.
uint8_t ble_service_get_characteristics(BLEService service,
BLECharacteristic characteristics_out[],
uint8_t num_characteristics);
//! Gets the Service UUID of a service.
//! @param service The service for which to get the Service UUID.
//! @return The 128-bit Service UUID, or UUID_INVALID if the service reference
//! was invalid.
//! @note The returned UUID is always a 128-bit UUID, even if the device
//! its interal GATT service database uses 16-bit or 32-bit Service UUIDs.
//! @see bt_uuid_expand_16bit for a macro that converts 16-bit UUIDs to 128-bit
//! equivalents.
//! @see bt_uuid_expand_32bit for a macro that converts 32-bit UUIDs to 128-bit
//! equivalents.
Uuid ble_service_get_uuid(BLEService service);
//! Gets the device that hosts the service.
//! @param service The service for which to find the device it belongs to.
//! @return The device hosting the service, or an invalid device if the service
//! reference was invalid. Use bt_device_is_invalid() to test whether the
//! returned device is invalid.
BTDevice ble_service_get_device(BLEService service);
//! Gets the services that are references by a service as "Included Service".
//! @param service The service for which to get the included services
//! @param[out] included_services_out An array of pointers to services,
//! into which the included services will be copied.
//! @param num_services the size of the included_services_out array.
//! @return The total number of included services for the service. This might be
//! a larger number than included_services_out can contain, if the passed array
//! was not large enough to hold all the pointers.
//! @note For convenience, the services are owned by the system and references
//! to services, characteristics and descriptors are guaranteed to remain valid
//! *until the BLEClientServiceChangeHandler is called again* or until
//! application is terminated.
uint8_t ble_service_get_included_services(BLEService service,
BLEService included_services_out[],
uint8_t num_services);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// (FUTURE / LATER / NOT SCOPED)
// Just to see how symmetric the Server APIs could be:
// creates + adds to GATT DB (?)
// Services aren't supposed to change. Pass everything into the 'create' call:
BLEService ble_service_create(const Uuid *service_uuid,
BLECharacteristic characteristics[],
uint8_t num_characteristics);
void ble_service_set_included_services(BLEService service,
BLEService included_services[],
uint8_t num_included_services);
// removes from GATT DB (?) + destroys
void ble_service_destroy(BLEService service);

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.
*/
#if !CAPABILITY_HAS_MAGNETOMETER
#error "Use fw/applib/compass_service_stub.c instead if we don't have a magnetometer"
#endif
#include "compass_service.h"
#include "compass_service_private.h"
#include "applib/app_logging.h"
#include "applib/app_timer.h"
#include "applib/event_service_client.h"
#include "util/trig.h"
#include "drivers/mag.h"
#include "kernel/events.h"
#include "kernel/pbl_malloc.h"
#include "syscall/syscall.h"
#include "system/passert.h"
#include "kernel/kernel_applib_state.h"
#include "process_state/app_state/app_state.h"
#include "process_state/worker_state/worker_state.h"
#include <inttypes.h>
#define PEEK_TIMEOUT_MS 11 * 1000
static CompassServiceConfig **prv_get_config(PebbleTask task) {
CompassServiceConfig **config = NULL;
if (task == PebbleTask_Unknown) {
task = pebble_task_get_current();
}
if (task == PebbleTask_App) {
config = app_state_get_compass_config();
} else if (task == PebbleTask_Worker) {
config = worker_state_get_compass_config();
} else if (task == PebbleTask_KernelMain) {
config = kernel_applib_get_compass_config();
} else {
WTF;
}
if (*config == NULL) {
// Note that config will never be NULL after grabbing it from an app,
// worker, or kernel state. However, the value pointed to it may be
// NULL.
*config = task_zalloc_check(sizeof(CompassServiceConfig));
}
return config;
}
static void prv_do_data_handle(PebbleEvent *e, void *context) {
PebbleCompassDataEvent *m = &e->compass_data;
CompassHeadingData data = {
.is_declination_valid = false,
.compass_status = m->calib_status,
.magnetic_heading = m->magnetic_heading,
.true_heading = m->magnetic_heading
};
CompassServiceConfig *config = *prv_get_config(PebbleTask_Unknown);
if (config->compass_cb != NULL) {
if (ABS(config->last_angle - data.magnetic_heading) > config->compass_filter) {
config->compass_cb(data);
config->last_angle = data.magnetic_heading;
}
}
}
// Callback for disabling compass service on timeout
static void prv_peek_timeout_callback(void *data) {
compass_service_unsubscribe();
}
int compass_service_peek(CompassHeadingData *data) {
CompassServiceConfig *config = *prv_get_config(PebbleTask_Unknown);
if ((config->peek_timer == NULL) && !sys_ecompass_service_subscribed()) {
// If we haven't initialized the compass yet by subscribing, do that now.
compass_service_subscribe(NULL);
}
sys_ecompass_get_last_heading(data);
if (data->is_declination_valid) {
data->true_heading += config->heading_declination;
}
// 11 second timer to turn off compass, reset timeout every peek
if (config->peek_timer == NULL) {
config->peek_timer = app_timer_register(PEEK_TIMEOUT_MS,
prv_peek_timeout_callback, NULL);
} else {
app_timer_reschedule(config->peek_timer, PEEK_TIMEOUT_MS);
}
return (0);
}
int compass_service_set_heading_filter(CompassHeading filter) {
if ((filter < 0) || (filter > (TRIG_MAX_ANGLE / 2))) {
return (-1);
}
CompassServiceConfig *config = *prv_get_config(PebbleTask_Unknown);
config->compass_filter = filter;
return (0);
}
void compass_service_subscribe(CompassHeadingHandler handler) {
CompassServiceConfig *config = *prv_get_config(PebbleTask_Unknown);
*config = (const CompassServiceConfig){ 0 };
config->compass_cb = handler;
config->info = (EventServiceInfo) {
.type = PEBBLE_COMPASS_DATA_EVENT,
.handler = &prv_do_data_handle
};
event_service_client_subscribe(&config->info);
}
void compass_service_unsubscribe(void) {
CompassServiceConfig **config = prv_get_config(PebbleTask_Unknown);
event_service_client_unsubscribe(&(*config)->info);
task_free(*config);
*config = NULL;
}

View file

@ -0,0 +1,85 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "drivers/mag.h"
#include "services/common/ecompass.h"
//! @addtogroup Foundation
//! @{
//! @addtogroup EventService
//! @{
//! @addtogroup CompassService
//!
//! \brief The Compass Service combines information from Pebble's accelerometer and
//! magnetometer to automatically calibrate
//! the compass and transform the raw magnetic field information into a \ref CompassHeading,
//! that is an angle to north. It also
//! provides magnetic north and information about its status and accuracy through the \ref
//! CompassHeadingData structure. The API is designed to also provide true north in a future
//! release.
//!
//! Note that not all platforms have compasses. To check for the presence of a compass at
//! compile time for the current platform use the `PBL_COMPASS` define.
//!
//! To learn more about the Compass Service and how to use it, read the
//! <a href="https://developer.getpebble.com/guides/pebble-apps/sensors/magnetometer/">
//! Determining Direction</a> guide.
//!
//! For available code samples, see the
//! <a href="https://github.com/pebble-examples/feature-compass">feature-compass</a> example.
//!
//! @{
//! Peek at the last recorded reading.
//! @param[out] data a pointer to a pre-allocated CompassHeadingData
//! @return Always returns 0 to indicate success.
int compass_service_peek(CompassHeadingData *data);
//! Set the minimum angular change required to generate new compass heading events.
//! The angular distance is measured relative to the last delivered heading event.
//! Use 0 to be notified of all movements.
//! Negative values and values > TRIG_MAX_ANGLE / 2 are not valid.
//! The default value of this property is TRIG_MAX_ANGLE / 360.
//! @return 0, success.
//! @return Non-Zero, if filter is invalid.
//! @see compass_service_subscribe
int compass_service_set_heading_filter(CompassHeading filter);
//! Callback type for compass heading events
//! @param heading copy of last recorded heading
typedef void (*CompassHeadingHandler)(CompassHeadingData heading);
//! Subscribe to the compass heading event service. Once subscribed, the handler
//! gets called every time the angular distance relative to the previous value
//! exceeds the configured filter.
//! @param handler A callback to be executed on heading events
//! @see compass_service_set_heading_filter
//! @see compass_service_unsubscribe
void compass_service_subscribe(CompassHeadingHandler handler);
//! Unsubscribe from the compass heading event service. Once unsubscribed,
//! the previously registered handler will no longer be called.
//! @see compass_service_subscribe
void compass_service_unsubscribe(void);
//! @} // end addtogroup CompassService
//! @} // end addtogroup EventService
//! @} // end addtogroup Foundation

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 "compass_service.h"
#include "applib/app_timer.h"
#include "applib/event_service_client.h"
typedef struct __attribute__((__packed__)) {
CompassHeading compass_filter;
int32_t last_angle;
CompassHeading heading_declination;
AppTimer* peek_timer;
CompassHeadingHandler compass_cb;
EventServiceInfo info;
} CompassServiceConfig;

View file

@ -0,0 +1,66 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#if CAPABILITY_HAS_MAGNETOMETER
#error "Use fw/applib/compass_service.c instead if we don't have a magnetometer"
#endif
//! @file compass_service_stub.c
//!
//! Implements the compass_service for devices that don't actually have a compass. See
//! fw/applib/compass_service.c for the real implementation for boards that do have a compass.
#include "compass_service.h"
#include "process_management/process_manager.h"
//! @return which status value we should use to indicate we have no compass
static CompassStatus prv_get_status(void) {
if (process_manager_compiled_with_legacy2_sdk() ||
process_manager_compiled_with_legacy3_sdk()) {
// This value is new in 4.x. Use the old CompassStatusDataInvalid value instead for old apps
// that may not know how to handle the previously undefined status.
return CompassStatusDataInvalid;
}
return CompassStatusUnavailable;
}
int compass_service_peek(CompassHeadingData *data) {
*data = (CompassHeadingData) {
.compass_status = prv_get_status()
};
return 0;
}
int compass_service_set_heading_filter(CompassHeading filter) {
// Just ignore the filter, we're not going to call the handler regularly anyway.
return 0;
}
void compass_service_subscribe(CompassHeadingHandler handler) {
CompassHeadingData data = {
.compass_status = prv_get_status()
};
// Call the handler once to indicate status
handler(data);
}
void compass_service_unsubscribe(void) {
// Nothing to do because we never handle the subscribe in the first place
}

View file

@ -0,0 +1,105 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "connection_service.h"
#include "connection_service_private.h"
#include "applib/event_service_client.h"
#include "kernel/events.h"
#include "services/common/debounced_connection_service.h"
#include "services/common/event_service.h"
#include "syscall/syscall.h"
#include "system/passert.h"
#include "process_state/app_state/app_state.h"
#include "process_state/worker_state/worker_state.h"
static ConnectionServiceState* prv_get_state(void) {
PebbleTask task = pebble_task_get_current();
if (task == PebbleTask_App) {
return app_state_get_connection_service_state();
} else if (task == PebbleTask_Worker) {
return worker_state_get_connection_service_state();
}
WTF;
}
static void prv_do_handle(PebbleEvent *e, void *context) {
ConnectionServiceState *state = prv_get_state();
bool connected = e->bluetooth.comm_session_event.is_open;
ConnectionHandler handler =
(e->bluetooth.comm_session_event.is_system ?
state->handlers.pebble_app_connection_handler :
state->handlers.pebblekit_connection_handler);
if (handler) {
if (!connected) {
sys_analytics_inc(ANALYTICS_DEVICE_METRIC_APP_NOTIFIED_DISCONNECTED_COUNT,
AnalyticsClient_System);
}
handler(connected);
}
}
bool connection_service_peek_pebble_app_connection(void) {
return sys_mobile_app_is_connected_debounced();
}
bool connection_service_peek_pebblekit_connection(void) {
return sys_pebblekit_is_connected_debounced();
}
void connection_service_unsubscribe(void) {
ConnectionServiceState *state = prv_get_state();
event_service_client_unsubscribe(&state->bcs_info);
memset(&state->handlers, 0x0, sizeof(state->handlers));
}
void connection_service_subscribe(ConnectionHandlers conn_handlers) {
ConnectionServiceState *state = prv_get_state();
state->handlers = conn_handlers;
event_service_client_subscribe(&state->bcs_info);
}
void connection_service_state_init(ConnectionServiceState *state) {
*state = (ConnectionServiceState) {
.bcs_info = {
.type = PEBBLE_BT_CONNECTION_DEBOUNCED_EVENT,
.handler = prv_do_handle,
},
};
}
// Deprecated routines kept around for backward compile compatibility
void bluetooth_connection_service_subscribe(ConnectionHandler handler) {
ConnectionHandlers conn_handlers = {
.pebble_app_connection_handler = handler,
.pebblekit_connection_handler = NULL
};
connection_service_subscribe(conn_handlers);
}
void bluetooth_connection_service_unsubscribe(void) {
connection_service_unsubscribe();
}
bool bluetooth_connection_service_peek(void) {
return connection_service_peek_pebble_app_connection();
}

View file

@ -0,0 +1,109 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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>
//! @addtogroup Foundation
//! @{
//! @addtogroup EventService
//! @{
//! @addtogroup ConnectionService
//! \brief Determine what the Pebble watch is connected to
//!
//! The ConnectionService allows your app to learn about the apps the Pebble
//! watch is connected to. You can ask the system for this information at a
//! given time or you can register to receive events every time connection or
//! disconnection events occur.
//!
//! It allows you to determine whether the watch is connected to the Pebble
//! mobile app by subscribing to the pebble_app_connection_handler or by calling
//! the connection_service_peek_pebble_app_connection function. Note that when
//! the Pebble app is connected, you can assume PebbleKit JS apps will also be
//! running correctly.
//!
//! The service also allows you to determine if the Pebble watch can establish
//! a connection to a PebbleKit companion app by subscribing to the
//! pebblekit_connection_handler or by calling the
//! connection_service_peek_pebblekit_connection function. Today, due to
//! architectural differences between iOS and Android, this will return true
//! for Android anytime a connection with the Pebble mobile app is established
//! (since PebbleKit messages are routed through the Android app). For iOS,
//! this will return true when any PebbleKit companion app has established a
//! connection with the Pebble watch (since companion app messages are routed
//! directly to the watch)
//!
//! @{
//! Callback type for connection events
//! @param connected true on connection, false on disconnection
typedef void (*ConnectionHandler)(bool connected);
//! Query the bluetooth connection service for the current Pebble app connection status
//! @return true if the Pebble app is connected, false otherwise
bool connection_service_peek_pebble_app_connection(void);
//! Query the bluetooth connection service for the current PebbleKit connection status
//! @return true if a PebbleKit companion app is connected, false otherwise
bool connection_service_peek_pebblekit_connection(void);
typedef struct {
//! callback to be executed when the connection state between the watch and
//! the phone app has changed. Note, if the phone App is connected, PebbleKit JS apps
//! will also be working correctly
ConnectionHandler pebble_app_connection_handler;
//! ID for callback to be executed on PebbleKit connection event
ConnectionHandler pebblekit_connection_handler;
} ConnectionHandlers;
//! Subscribe to the connection event service. Once subscribed, the appropriate
//! handler gets called based on the type of connection event and user provided
//! handlers
//! @param ConnectionHandlers A struct populated with the handlers to
//! be called when the specified connection event occurs. If a given handler is
//! NULL, no function will be called.
void connection_service_subscribe(ConnectionHandlers conn_handlers);
//! Unsubscribe from the bluetooth event service. Once unsubscribed, the previously registered
//! handler will no longer be called.
void connection_service_unsubscribe(void);
//! @deprecated Backwards compatibility typedef for ConnectionHandler. New code
//! should use ConnectionHandler directly. This will be removed in a future
//! version of the Pebble SDK.
typedef ConnectionHandler BluetoothConnectionHandler;
//! @deprecated Backward compatibility function for
//! connection_service_subscribe. New code should use
//! connection_service_subscribe directly. This will be removed in a future
//! version of the Pebble SDK
void bluetooth_connection_service_subscribe(ConnectionHandler handler);
//! @deprecated Backward compatibility function for
//! connection_service_unsubscribe. New code should use
//! connection_service_unsubscribe directly. This will be removed in a future
//! version of the Pebble SDK
void bluetooth_connection_service_unsubscribe(void);
//! @deprecated Backward compatibility function for
//! connection_service_peek_pebble_app_connection. New code should use
//! connection_service_peek_pebble_app_connection directly. This will be
//! removed in a future version of the Pebble SDK
bool bluetooth_connection_service_peek(void);
//! @} // end addtogroup ConnectionService
//! @} // end addtogroup EventService
//! @} // end addtogroup Foundation

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 "applib/event_service_client.h"
#include "connection_service.h"
typedef struct ConnectionServiceState {
ConnectionHandlers handlers;
EventServiceInfo bcs_info;
} ConnectionServiceState;
void connection_service_state_init(ConnectionServiceState *state);

44
src/fw/applib/cpu_cache.c Normal file
View file

@ -0,0 +1,44 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "cpu_cache.h"
#include "syscall/syscall_internal.h"
#include "mcu/cache.h"
DEFINE_SYSCALL(void, memory_cache_flush, void *start, size_t size) {
// We need to align the address and size properly for the cache functions. It needs to be done
// before we do syscall_assert_userspace_buffer though, because otherwise the user could
// potentially abuse this behavior to flush+invalidate kernel memory. Theoretically that
// shouldn't actually have any really effect, but it's better to be safe than sorry.
// That should only be possible if the user region is not cache aligned, so in any realistic
// case this won't even matter.
uintptr_t start_addr = (uintptr_t)start;
icache_align(&start_addr, &size);
dcache_align(&start_addr, &size);
start = (void *)start_addr;
if (PRIVILEGE_WAS_ELEVATED) {
syscall_assert_userspace_buffer(start, size);
}
if (dcache_is_enabled()) {
dcache_flush(start, size);
}
if (icache_is_enabled()) {
icache_invalidate(start, size);
}
}

39
src/fw/applib/cpu_cache.h Normal file
View file

@ -0,0 +1,39 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <stddef.h>
//! @file cpu_cache.h
//! @addtogroup Foundation
//! @{
//! @addtogroup MemoryManagement Memory Management
//! \brief Utility functions for managing an application's memory.
//!
//! @{
//! Flushes the data cache and invalidates the instruction cache for the given region of memory,
//! if necessary. This is only required when your app is loading or modifying code in memory and
//! intends to execute it. On some platforms, code executed may be cached internally to improve
//! performance. After writing to memory, but before executing, this function must be called in
//! order to avoid undefined behavior. On platforms without caching, this performs no operation.
//! @param start The beginning of the buffer to flush
//! @param size How many bytes to flush
void memory_cache_flush(void *start, size_t size);
//! @} // end addtogroup MemoryManagement
//! @} // end addtogroup Foundation

View file

@ -0,0 +1,56 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "data_logging.h"
#include "services/normal/data_logging/dls_private.h"
#include "applib/app_logging.h"
#include "applib/applib_malloc.auto.h"
#include "syscall/syscall.h"
DataLoggingSessionRef data_logging_create(uint32_t tag, DataLoggingItemType item_type,
uint16_t item_length, bool resume) {
void *buffer = NULL;
// For workers, dls_create_current_process() will create the buffer for us. All others must
// allocate the buffer in their own heap (before going into privileged mode).
if (pebble_task_get_current() != PebbleTask_Worker) {
buffer = applib_malloc(DLS_SESSION_MIN_BUFFER_SIZE);
if (!buffer) {
APP_LOG(APP_LOG_LEVEL_ERROR, "insufficient memory");
return NULL;
}
}
// Create the session
DataLoggingSessionRef session = sys_data_logging_create(tag, item_type, item_length, buffer,
resume);
if (session == NULL && buffer != NULL) {
applib_free(buffer);
}
return session;
}
void data_logging_finish(DataLoggingSessionRef logging_session) {
sys_data_logging_finish(logging_session);
}
DataLoggingResult data_logging_log(DataLoggingSessionRef logging_session, const void *data,
uint32_t num_items) {
return sys_data_logging_log(logging_session, data, num_items);
}

View file

@ -0,0 +1,140 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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>
//! @addtogroup Foundation
//! @{
//! @addtogroup DataLogging
//! \brief Enables logging data asynchronously to a mobile app
//!
//! In Pebble OS, data logging is a data storage and transfer subsystem that allows watchapps to
//! save data on non-volatile storage devices when the phone is not available to process it. The
//! API provides your watchapp with a mechanism for short-term data buffering for asynchronous data
//! transmission to a mobile app.
//!
//! Using this API, your Pebble watchapp can create an arbitrary number of logs, but youre
//! limited in the amount of storage space you can use. Note that approximately 640K is available
//! for data logging, which is shared among all watchapps that use it. This value is subject to
//! change in the future. When the data spool is full, an app will start overwriting its own data.
//! An app cannot overwrite another apps's data. However, the other app might have 0 bytes for data
//! logging.
//!
//! Your app can log data to a session. Every new block of data is appended to the session.
//! The data is then sent to the associated phone application at the earliest convenience.
//! If a phone is available, the data is sent directly to the phone. Otherwise, it is saved to the
//! watch storage until the watch is connected to a phone.
//!
//!
//! For example:
//!
//! To create a data logging session for 4-byte unsigned integers with a tag of 0x1234, you would
//! do this: \code{.c}
//!
//! DataLoggingSessionRef logging_session = data_logging_create(0x1234, DATA_LOGGING_UINT, 4,
//! false);
//!
//! // Fake creating some data and logging it to the session.
//! uint32_t data[] = { 1, 2, 3};
//! data_logging_log(logging_session, &data, 3);
//!
//! // Fake creating more data and logging that as well.
//! uint32_t data2[] = { 1, 2 };
//! data_logging_log(logging_session, &data, 2);
//!
//! // When we don't need to log anything else, we can close off the session.
//! data_logging_finish(logging_session);
//! \endcode
//!
//! @{
//! The different types of session data that Pebble supports. This type describes the type of a
//! singular item in the data session. Every item in a given session is the same type and size.
typedef enum {
//! Array of bytes. Remember that this is the type of a single item in the logging session, so
//! using this type means you'll be logging multiple byte arrays (each a fixed length described
//! by item_length) for the duration of the session.
DATA_LOGGING_BYTE_ARRAY = 0,
//! Unsigned integer. This may be a 1, 2, or 4 byte integer depending on the item_length parameter
DATA_LOGGING_UINT = 2,
//! Signed integer. This may be a 1, 2, or 4 byte integer depending on the item_length parameter
DATA_LOGGING_INT = 3,
} DataLoggingItemType;
//! Enumerated values describing the possible outcomes of data logging operations
typedef enum {
DATA_LOGGING_SUCCESS = 0, //!< Successful operation
DATA_LOGGING_BUSY, //!< Someone else is writing to this logging session
DATA_LOGGING_FULL, //!< No more space to save data
DATA_LOGGING_NOT_FOUND, //!< The logging session does not exist
DATA_LOGGING_CLOSED, //!< The logging session was made inactive
DATA_LOGGING_INVALID_PARAMS, //!< An invalid parameter was passed to one of the functions
DATA_LOGGING_INTERNAL_ERR //!< An internal error occurred
} DataLoggingResult;
typedef void *DataLoggingSessionRef;
//! Create a new data logging session.
//!
//! @param tag A tag associated with the logging session.
//! @param item_type The type of data stored in this logging session
//! @param item_length The size of a single data item in bytes
//! @param resume True if we want to look for a logging session of the same tag and
//! resume logging to it. If this is false and a session with the specified tag exists, that
//! session will be closed and a new session will be opened.
//! @return An opaque reference to the data logging session
DataLoggingSessionRef data_logging_create(uint32_t tag, DataLoggingItemType item_type,
uint16_t item_length, bool resume);
//! Finish up a data logging_session. Logging data is kept until it has successfully been
//! transferred over to the phone, but no data may be added to the session after this function is
//! called.
//!
//! @param logging_session a reference to the data logging session previously allocated using
//! data_logging_create
void data_logging_finish(DataLoggingSessionRef logging_session);
//! Add data to the data logging session. If a phone is available, the data is sent directly
//! to the phone. Otherwise, it is saved to the watch storage until the watch is connected to a
//! phone.
//!
//! @param logging_session a reference to the data logging session you want to add the data to
//! @param data a pointer to the data buffer that contains multiple items
//! @param num_items the number of items to log. This means data must be at least
//! (num_items * item_length) long in bytes
//! @return
//! DATA_LOGGING_SUCCESS on success
//!
//! @return
//! DATA_LOGGING_NOT_FOUND if the logging session is invalid
//!
//! @return
//! DATA_LOGGING_CLOSED if the sesion is not active
//!
//! @return
//! DATA_LOGGING_BUSY if the sesion is not available for writing
//!
//! @return
//! DATA_LOGGING_INVALID_PARAMS if num_items is 0 or data is NULL
DataLoggingResult data_logging_log(DataLoggingSessionRef logging_session, const void *data,
uint32_t num_items);
//! @} // end addtogroup DataLogging
//! @} // end addtogroup Foundation

View file

@ -0,0 +1,97 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "util/list.h"
#include "services/common/event_service.h"
#include "kernel/kernel_applib_state.h"
#include "event_service_client.h"
#include "process_management/app_manager.h"
#include "util/list.h"
#include "applib/app_logging.h"
#include "process_state/app_state/app_state.h"
#include "process_state/worker_state/worker_state.h"
#include "syscall/syscall.h"
#include "system/logging.h"
#include "system/passert.h"
static EventServiceInfo *prv_get_state(void) {
PebbleTask task = pebble_task_get_current();
if (task == PebbleTask_App) {
return app_state_get_event_service_state();
} else if (task == PebbleTask_Worker) {
return worker_state_get_event_service_state();
} else if (task == PebbleTask_KernelMain) {
return kernel_applib_get_event_service_state();
} else {
WTF;
}
}
static int event_service_comparator(EventServiceInfo *a, EventServiceInfo *b) {
return (b->type - a->type);
}
bool event_service_filter(ListNode *node, void *tp) {
EventServiceInfo *info = (EventServiceInfo *)node;
uint32_t type = (uint32_t)tp;
return (info->type == type);
}
static void do_handle(EventServiceInfo *info, PebbleEvent *e) {
PBL_ASSERTN(info->handler != NULL);
info->handler(e, info->context);
}
void event_service_client_subscribe(EventServiceInfo *handler) {
EventServiceInfo *state = prv_get_state();
ListNode *list = &state->list_node;
if (list_contains(list, &handler->list_node)) {
PBL_LOG(LOG_LEVEL_DEBUG, "Event service handler already subscribed");
return;
}
// Add to handlers list
list_sorted_add(list, &handler->list_node, (Comparator)event_service_comparator, true);
sys_event_service_client_subscribe(handler);
}
void event_service_client_unsubscribe(EventServiceInfo *handler) {
EventServiceInfo *state = prv_get_state();
ListNode *list = &state->list_node;
if (!list_contains(list, &handler->list_node)) {
PBL_LOG(LOG_LEVEL_DEBUG, "Event service handler not subscribed");
return;
}
sys_event_service_client_unsubscribe(state, handler);
}
void event_service_client_handle_event(PebbleEvent *e) {
EventServiceInfo *state = prv_get_state();
const uintptr_t type = e->type;
// find the first callback
ListNode *handler = list_find(&state->list_node, event_service_filter, (void *) type);
while (handler) {
// find the next callback before we call the current one, because the CB may alter the list
ListNode *next_handler = list_find_next(handler, event_service_filter, false, (void *) type);
do_handle((EventServiceInfo *)handler, e);
handler = next_handler;
}
sys_event_service_cleanup(e);
}

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 "kernel/events.h"
typedef void (*EventServiceEventHandler)(PebbleEvent *e, void *context);
typedef struct __attribute__((packed)) {
ListNode list_node;
PebbleEventType type;
EventServiceEventHandler handler;
void *context;
} EventServiceInfo;
void event_service_client_subscribe(EventServiceInfo * service_info);
void event_service_client_unsubscribe(EventServiceInfo * service_info);
void event_service_client_handle_event(PebbleEvent *e);
bool event_service_filter(ListNode *node, void *tp);

View file

@ -0,0 +1,147 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "codepoint.h"
#include "util/size.h"
#include <stddef.h>
#define MAX_LATIN_CODEPOINT 0x02AF
#define MIN_SOFTBANK_EMOJI_CODEPOINT 0xE000
#define MAX_SOFTBANK_EMOJI_CODEPOINT 0xE537
#define MIN_UNIFIED_EMOJI_CODEPOINT 0x1F300
#define MAX_UNIFIED_EMOJI_CODEPOINT 0x1F6FF
#define MIN_SYMBOLS_CODEPOINT 0x2000
#define MAX_SYMBOLS_CODEPOINT 0x2BFF
#define MIN_IDEOGRAPH_CODEPOINT 0x2e80
#define MIN_SPECIAL_CODEPOINT 0xE0A0
#define MAX_SPECIAL_CODEPOINT 0xE0A2
#define MIN_SKIN_TONE_CODEPOINT 0x1F3FB
#define MAX_SKIN_TONE_CODEPOINT 0x1F3FF
// Note: Please keep these sorted
static const Codepoint NONSTANDARD_EMOJI_CODEPOINTS[] = {
0x2192, // rightwards_arrow
0x25BA, // black_right_pointing_pointer
0x2605, // black_star
0x260E, // black_telephone
0x261D, // white_up_pointing_index
0x263A, // white_smiling_face
0x270A, // raised_fist
0x270B, // raised_hand
0x270C, // victory_hand
0x2764, // heavy_black_heart
};
// Note: Please keep these sorted
static const Codepoint END_OF_WORD_CODEPOINTS[] = {
NULL_CODEPOINT, // 0x0
NEWLINE_CODEPOINT, // 0xa
SPACE_CODEPOINT, // 0x20
HYPHEN_CODEPOINT, // 0x2d
ZERO_WIDTH_SPACE_CODEPOINT // 0x200b
};
// Note: Please keep these sorted
static const Codepoint FORMATTING_CODEPOINTS[] = {
0x7F, // delete
0x200C, // zero-width non-joiner
0x200D, // zero-width joiner
0x200E, // left to right
0x200F, // right to left
0x202A, // bidirectional - right to left
0x202C, // bidirectional - pop direction
0x202D, // left to right override
0xFE0E, // variation selector 1
0xFE0F, // variation selector 2
0xFEFF, // zero-width-no-break
};
// Note: Please keep these sorted
static const Codepoint ZERO_WIDTH_CODEPOINTS[] = {
ZERO_WIDTH_SPACE_CODEPOINT,
WORD_JOINER_CODEPOINT,
};
static bool codepoint_in_list(const Codepoint codepoint, const Codepoint *codepoints, size_t size) {
for (size_t i = 0; i < size; ++i) {
if (codepoints[i] >= codepoint) {
return (codepoints[i] == codepoint);
}
}
return false;
}
bool codepoint_is_formatting_indicator(const Codepoint codepoint) {
return codepoint_in_list(codepoint, FORMATTING_CODEPOINTS, ARRAY_LENGTH(FORMATTING_CODEPOINTS));
}
bool codepoint_is_ideograph(const Codepoint codepoint) {
if (codepoint > MIN_IDEOGRAPH_CODEPOINT) {
// non ideographic characters. This is an approximation that is good enough until
// we start supporting some exotic scripts (e.g. tibetan)
return true;
} else {
return false;
}
}
// see http://www.unicode.org/reports/tr14/ for the whole enchilada
bool codepoint_is_end_of_word(const Codepoint codepoint) {
return codepoint_in_list(codepoint, END_OF_WORD_CODEPOINTS, ARRAY_LENGTH(END_OF_WORD_CODEPOINTS));
}
// see http://unicode.org/reports/tr51/ section 2.2 "Diversity"
bool codepoint_is_skin_tone_modifier(const Codepoint codepoint) {
return (codepoint >= MIN_SKIN_TONE_CODEPOINT && codepoint <= MAX_SKIN_TONE_CODEPOINT);
}
bool codepoint_should_skip(const Codepoint codepoint) {
return ((codepoint < 0x20 && codepoint != NEWLINE_CODEPOINT) ||
(codepoint_is_skin_tone_modifier(codepoint)));
}
bool codepoint_is_zero_width(const Codepoint codepoint) {
return codepoint_in_list(codepoint, ZERO_WIDTH_CODEPOINTS, ARRAY_LENGTH(ZERO_WIDTH_CODEPOINTS));
}
bool codepoint_is_latin(const Codepoint codepoint) {
return (codepoint <= MAX_LATIN_CODEPOINT ||
(codepoint >= MIN_SYMBOLS_CODEPOINT &&
codepoint <= MAX_SYMBOLS_CODEPOINT));
}
bool codepoint_is_emoji(const Codepoint codepoint) {
// search for the codepoint in the list of nonstandard emoji codepoints first.
const bool found = codepoint_in_list(codepoint,
NONSTANDARD_EMOJI_CODEPOINTS,
ARRAY_LENGTH(NONSTANDARD_EMOJI_CODEPOINTS));
if (found) {
return true;
} else {
return ((codepoint >= MIN_SOFTBANK_EMOJI_CODEPOINT &&
codepoint <= MAX_SOFTBANK_EMOJI_CODEPOINT) ||
(codepoint >= MIN_UNIFIED_EMOJI_CODEPOINT &&
codepoint <= MAX_UNIFIED_EMOJI_CODEPOINT));
}
}
bool codepoint_is_special(const Codepoint codepoint) {
return (codepoint >= MIN_SPECIAL_CODEPOINT &&
codepoint <= MAX_SPECIAL_CODEPOINT);
}

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>
typedef uint32_t Codepoint;
#define EM_DASH "—"
#define EN_DASH ""
#define ELLIPSIS_CODEPOINT 0x2026
#define HYPHEN_CODEPOINT 0x002D
#define MINUS_SIGN_CODEPOINT 0x2212
#define SPACE_CODEPOINT ' '
#define NEWLINE_CODEPOINT '\n'
#define NULL_CODEPOINT '\0'
#define ZERO_WIDTH_SPACE_CODEPOINT 0x200B
#define WORD_JOINER_CODEPOINT 0x2060
bool codepoint_is_formatting_indicator(const Codepoint codepoint);
bool codepoint_is_skin_tone_modifier(const Codepoint codepoint);
bool codepoint_is_end_of_word(const Codepoint codepoint);
bool codepoint_is_ideograph(const Codepoint codepoint);
bool codepoint_should_skip(const Codepoint codepoint);
bool codepoint_is_zero_width(const Codepoint codepoint);
bool codepoint_is_latin(const Codepoint codepoint);
bool codepoint_is_emoji(const Codepoint codepoint);
// This is a least dirty hack to enable special rendering when a special codepoint is hit in the
// text being rendered
bool codepoint_is_special(const Codepoint codepoint);

147
src/fw/applib/fonts/fonts.c Normal file
View file

@ -0,0 +1,147 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "fonts.h"
#include "fonts_private.h"
#include "applib/applib_malloc.auto.h"
#include "applib/applib_resource.h"
#include "applib/graphics/text.h"
#include "applib/graphics/text_resources.h"
#include "process_management/app_manager.h"
#include "resource/resource.h"
#include "resource/resource_ids.auto.h"
#include "syscall/syscall.h"
#include "system/passert.h"
#include "system/logging.h"
#include "util/list.h"
#include "util/size.h"
#include <string.h>
GFont fonts_get_fallback_font(void) {
// No font key for the fallback font
return sys_font_get_system_font(NULL);
}
GFont fonts_get_system_font(const char *font_key) {
static const char bitham_alias[] = "RESOURCE_ID_GOTHAM";
static const char bitham_prefix[] = "RESOURCE_ID_BITHAM";
static const size_t bitham_alias_len = sizeof(bitham_alias)-1;
static const size_t bitham_prefix_len = sizeof(bitham_prefix)-1;
GFont res = sys_font_get_system_font(font_key);
// maybe they wanted a renamed font
if (NULL == res && 0 == strncmp(font_key, bitham_alias, bitham_alias_len)) {
char new_font_key[bitham_prefix_len - bitham_alias_len + strlen(font_key) + 1];
strncpy(new_font_key, bitham_prefix, bitham_prefix_len);
strcpy(new_font_key+bitham_prefix_len, font_key+bitham_alias_len);
// let's try again
res = sys_font_get_system_font(new_font_key);
}
if (NULL == res) {
PBL_LOG(LOG_LEVEL_DEBUG, "Getting fallback font instead");
res = fonts_get_fallback_font();
PBL_ASSERTN(res);
}
return res;
}
GFont fonts_load_custom_font(ResHandle handle) {
GFont res = fonts_load_custom_font_system(sys_get_current_resource_num(), (uint32_t)handle);
if (res == NULL) {
PBL_LOG(LOG_LEVEL_WARNING, "Getting fallback font instead");
res = sys_font_get_system_font("RESOURCE_ID_GOTHIC_14");
}
return res;
}
GFont fonts_load_custom_font_system(ResAppNum app_num, uint32_t resource_id) {
if (resource_id == 0) {
PBL_LOG(LOG_LEVEL_ERROR, "Tried to load a font from a NULL resource");
return NULL;
}
FontInfo *font_info = applib_type_malloc(FontInfo);
if (font_info == NULL) {
PBL_LOG(LOG_LEVEL_ERROR, "Couldn't malloc space for new font");
return NULL;
}
bool result = text_resources_init_font(app_num, resource_id,
0 /* extended resource */, font_info);
if (!result) {
// couldn't init the font
applib_free(font_info);
return NULL;
}
return font_info;
}
void fonts_unload_custom_font(GFont font) {
// fonts_load_custom_font can return gothic 14 if loading their font didn't
// work for whatever reason. We don't let the app know that it failed, so it makes sense that
// they'll later try to unload this returned pointer at a later point. We don't actually want
// to free this, so just no-op.
if (font == sys_font_get_system_font("RESOURCE_ID_GOTHIC_14")) {
return;
}
FontInfo *font_info = (FontInfo*) font;
applib_free(font_info);
}
#if !RECOVERY_FW
static const struct {
const char *key_name;
uint8_t height;
} s_emoji_fonts[] = {
// Keep this sorted in descending order
{ FONT_KEY_GOTHIC_28_EMOJI, 28 },
{ FONT_KEY_GOTHIC_24_EMOJI, 24 },
{ FONT_KEY_GOTHIC_18_EMOJI, 18 },
{ FONT_KEY_GOTHIC_14_EMOJI, 14 },
};
FontInfo *fonts_get_system_emoji_font_for_size(unsigned int font_height) {
for (uint32_t i = 0; i < ARRAY_LENGTH(s_emoji_fonts); i++) {
if (font_height == s_emoji_fonts[i].height) {
return sys_font_get_system_font(s_emoji_fonts[i].key_name);
}
}
// Didn't find a suitable emoji font
return NULL;
}
#endif
uint8_t fonts_get_font_height(GFont font) {
FontInfo* fontinfo = (FontInfo*) font;
return fontinfo->max_height;
}
int16_t fonts_get_font_cap_offset(GFont font) {
if (!font) {
return 0;
}
// FIXME PBL-25709: Actually use font-specific caps and also provide function for baseline offsets
return (int16_t)(((int16_t)font->max_height) * 22 / 100);
}

View file

@ -0,0 +1,86 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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/fonts/fonts_private.h"
#include "resource/resource.h"
#if !defined(SDK)
#include "font_resource_keys.auto.h"
#endif
//! @addtogroup Graphics
//! @{
//! @addtogroup Fonts
//! @see \ref TextLayer
//! @see \ref TextDrawing
//! @see \ref text_layer_set_font
//! @see \ref graphics_draw_text
//! @{
//! Pointer to opaque font data structure.
//! @see \ref fonts_load_custom_font()
//! @see \ref text_layer_set_font()
//! @see \ref graphics_draw_text()
typedef FontInfo* GFont;
//! @internal
//! Gets the fallback system font (14pt Raster Gothic)
GFont fonts_get_fallback_font(void);
//! Loads a system font corresponding to the specified font key.
//! @param font_key The string key of the font to load. See
//! <a href="https://developer.pebble.com/guides/app-resources/system-fonts/">System
//! Fonts</a> guide for a list of system fonts.
//! @return An opaque pointer to the loaded font, or, a pointer to the default
//! (fallback) font if the specified font cannot be loaded.
//! @note This may load a font from the flash peripheral into RAM.
GFont fonts_get_system_font(const char *font_key);
GFont fonts_get_system_emoji_font_for_size(unsigned int font_size);
//! Loads a custom font.
//! @param handle The resource handle of the font to load. See resource_ids.auto.h
//! for a list of resource IDs, and use \ref resource_get_handle() to obtain the resource handle.
//! @return An opaque pointer to the loaded font, or a pointer to the default
//! (fallback) font if the specified font cannot be loaded.
//! @see Read the <a href="http://developer.getpebble.com/guides/pebble-apps/resources/">App
//! Resources</a> guide on how to embed a font into your app.
//! @note this may load a font from the flash peripheral into RAM.
GFont fonts_load_custom_font(ResHandle handle);
//! @internal
//! firmware-only access version of fonts_load_custom_font
GFont fonts_load_custom_font_system(ResAppNum app_num, uint32_t resource_id);
//! Unloads the specified custom font and frees the memory that is occupied by
//! it.
//! @note When an application exits, the system automatically unloads all fonts
//! that have been loaded.
//! @param font The font to unload.
void fonts_unload_custom_font(GFont font);
//! @internal
uint8_t fonts_get_font_height(GFont font);
//! @internal
// Get the vertical offset of the top of the font's caps from the origin of a text frame
// Currently only an approximation, see PBL-25709
int16_t fonts_get_font_cap_offset(GFont font);
//! @} // end addtogroup Fonts
//! @} // end addtogroup Graphics

View file

@ -0,0 +1,98 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "resource/resource.h"
#include "util/attributes.h"
//
// Definitions only for font loading and text rendering
//
// Initial version
#define FONT_VERSION_1 1
// 4 byte codepoints in offset table
#define FONT_VERSION_2 2
// feature bits: 2 or 4 byte offsets, RLE encoding
#define FONT_VERSION_3 3
#define FEATURE_OFFSET_16 (1 << 0)
#define FEATURE_RLE4 (1 << 1)
// HACK ALERT: Store the v3 FontMetaDataV3 feature bits in the top two bits of FontMetaData
// version field. We need this information at the lowest levels and can't extend FontMetaData
// for legacy support reasons.
#define FONT_VERSION(_version) ((_version) & 0x3F)
#define HAS_FEATURE(_version, _feature) ((_version) & (_feature))
#define VERSION_FIELD_FEATURE_OFFSET_16 (1 << 7)
#define VERSION_FIELD_FEATURE_RLE4 (1 << 6)
// There are now three versions of the FontMetaData structure: V1 (formerly known as 'legacy'), V2
// (still known as FontMetaData), and V3 (know as V3). We can't change the stack/memory usage
// until we drop support for existing applications so we can't simply use V3 as the base.
//
// The name 'FontMetaData' is retained instead of a more consistent 'FontMetaDataV2' because the
// uses of V1 and V3 are localized but 'FontMetaData' is used in many places, requiring many ugly
// changes.
typedef struct PACKED {
uint8_t version;
uint8_t max_height;
uint16_t number_of_glyphs;
uint16_t wildcard_codepoint;
uint8_t hash_table_size;
uint8_t codepoint_bytes;
uint8_t size;
uint8_t features;
} FontMetaDataV3;
typedef struct PACKED {
uint8_t version;
uint8_t max_height;
uint16_t number_of_glyphs;
uint16_t wildcard_codepoint;
uint8_t hash_table_size;
uint8_t codepoint_bytes;
} FontMetaData;
typedef struct PACKED {
uint8_t version;
uint8_t max_height;
uint16_t number_of_glyphs;
uint16_t wildcard_codepoint;
} FontMetaDataV1;
typedef struct {
FontMetaData md;
ResAppNum app_num;
uint32_t resource_id;
} FontResource;
typedef struct {
bool loaded;
bool extended;
uint8_t max_height;
FontResource base;
FontResource extension;
ResourceCallbackHandle extension_changed_cb;
} FontInfo;
typedef struct PACKED {
uint8_t hash;
uint8_t count;
uint16_t offset;
} FontHashTableEntry;

View file

@ -0,0 +1,194 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "../bitblt_private.h"
#include "applib/app_logging.h"
#include "applib/graphics/graphics.h"
#include "applib/graphics/graphics_private.h"
#include "applib/graphics/gtypes.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/bitset.h"
#include "util/graphics.h"
#include "util/size.h"
#define MAX_SUPPORTED_PALETTE_ENTRIES 4
// stores transparent masks + color patterns for even+odd scanlines
typedef struct {
// true, if color entry will be visible on 1bit, false otherwise
bool transparent_mask[MAX_SUPPORTED_PALETTE_ENTRIES];
// a 32bit value you can OR into the 1bit destination for each color entry
uint32_t palette_pattern[MAX_SUPPORTED_PALETTE_ENTRIES];
} RowLookUp;
typedef RowLookUp TwoRowLookUp[2];
T_STATIC void prv_apply_tint_color(GColor *color, GColor tint_color) {
// tint_color.a is always 0 or 3
if (tint_color.a != 0) {
tint_color.a = (*color).a;
*color = tint_color;
}
}
T_STATIC void prv_calc_two_row_look_ups(TwoRowLookUp *look_up,
GCompOp compositing_mode,
const GColor8 *palette,
uint8_t num_entries,
GColor tint_color) {
for (unsigned int palette_index = 0; palette_index < num_entries; palette_index++) {
GColor color = palette[palette_index];
// gcolor_get_grayscale will convert any color with an alpha less than 2 to clear
// alpha should be ignored in the case of GCompOpAssign so the alpha is set to 3
if (compositing_mode == GCompOpAssign) {
color.a = 3;
} else if (compositing_mode == GCompOpTint) {
prv_apply_tint_color(&color, tint_color);
} else if (compositing_mode == GCompOpTintLuminance) {
color = gcolor_tint_using_luminance_and_multiply_alpha(color, tint_color);
}
color = gcolor_get_grayscale(color);
for (unsigned int row_number = 0; row_number < ARRAY_LENGTH(*look_up); row_number++) {
(*look_up)[row_number].palette_pattern[palette_index] =
graphics_private_get_1bit_grayscale_pattern(color, row_number);
(*look_up)[row_number].transparent_mask[palette_index] =
gcolor_is_transparent(color) ? false : true;
}
}
}
void bitblt_bitmap_into_bitmap_tiled_palette_to_1bit(GBitmap* dest_bitmap,
const GBitmap* src_bitmap, GRect dest_rect,
GPoint src_origin_offset,
GCompOp compositing_mode,
GColor tint_color) {
if (!src_bitmap->palette) {
return;
}
const int8_t dest_begin_x = (dest_rect.origin.x / 32);
const uint32_t * const dest_block_x_begin = ((uint32_t *)dest_bitmap->addr) + dest_begin_x;
const int dest_row_length_words = (dest_bitmap->row_size_bytes / 4);
// The number of bits between the beginning of dest_block and
// the beginning of the nearest 32-bit block:
const uint8_t dest_shift_at_line_begin = (dest_rect.origin.x % 32);
const int16_t src_begin_x = src_bitmap->bounds.origin.x;
const int16_t src_begin_y = src_bitmap->bounds.origin.y;
// The bounds size is relative to the bounds origin, but the offset is within the origin. This
// means that the end coordinates may need to be adjusted.
const int16_t src_end_x = grect_get_max_x(&src_bitmap->bounds);
const int16_t src_end_y = grect_get_max_y(&src_bitmap->bounds);
// how many 32-bit blocks do we need to bitblt on this row:
const int16_t dest_end_x = grect_get_max_x(&dest_rect);
const int16_t dest_y_end = grect_get_max_y(&dest_rect);
const uint8_t num_dest_blocks_per_row = (dest_end_x / 32) +
((dest_end_x % 32) ? 1 : 0) - dest_begin_x;
const GColor *palette = src_bitmap->palette;
const uint8_t *src = src_bitmap->addr;
const uint8_t src_bpp = gbitmap_get_bits_per_pixel(gbitmap_get_format(src_bitmap));
const uint8_t src_palette_size = 1 << src_bpp;
PBL_ASSERTN(src_palette_size <= MAX_SUPPORTED_PALETTE_ENTRIES);
// The bitblt loops:
int16_t src_y = src_begin_y + src_origin_offset.y;
int16_t dest_y = dest_rect.origin.y;
TwoRowLookUp look_ups;
prv_calc_two_row_look_ups(&look_ups, compositing_mode, palette, src_palette_size, tint_color);
while (dest_y < dest_y_end) {
if (src_y >= src_end_y) {
src_y = src_begin_y;
}
uint8_t dest_shift = dest_shift_at_line_begin;
RowLookUp look_up = look_ups[dest_y % 2];
int16_t src_x = src_begin_x + src_origin_offset.x;
uint8_t row_bits_left = dest_rect.size.w;
uint32_t *dest_block = (uint32_t *)dest_block_x_begin + (dest_y * dest_row_length_words);
const uint32_t *dest_block_end = dest_block + num_dest_blocks_per_row;
while (dest_block != dest_block_end) {
uint8_t dest_x = dest_shift;
while (dest_x < 32 && row_bits_left > 0) {
if (src_x >= src_end_x) { // Wrap horizontally
src_x = src_begin_x;
}
uint8_t cindex = raw_image_get_value_for_bitdepth(src, src_x, src_y,
src_bitmap->row_size_bytes, src_bpp);
uint32_t mask = 0;
bitset32_update(&mask, dest_x, look_up.transparent_mask[cindex]);
// This can be optimized by performing actions on the current word all at once
// instead of iterating through each pixel
switch (compositing_mode) {
case GCompOpAssign:
case GCompOpSet:
case GCompOpTint:
case GCompOpTintLuminance:
*dest_block = (*dest_block & ~mask) | (look_up.palette_pattern[cindex] & mask);
break;
default:
PBL_LOG(LOG_LEVEL_DEBUG,
"Only the assign, set and tint modes are allowed for palettized bitmaps");
return;
}
dest_x++;
row_bits_left--;
src_x++;
}
dest_shift = 0;
dest_block++;
}
dest_y++;
src_y++;
}
}
void bitblt_bitmap_into_bitmap_tiled(GBitmap* dest_bitmap, const GBitmap* src_bitmap,
GRect dest_rect, GPoint src_origin_offset,
GCompOp compositing_mode, GColor8 tint_color) {
if (bitblt_compositing_mode_is_noop(compositing_mode, tint_color)) {
return;
}
GBitmapFormat src_fmt = gbitmap_get_format(src_bitmap);
GBitmapFormat dest_fmt = gbitmap_get_format(dest_bitmap);
if (dest_fmt != GBitmapFormat1Bit) {
return;
}
switch (src_fmt) {
case GBitmapFormat1Bit:
bitblt_bitmap_into_bitmap_tiled_1bit_to_1bit(dest_bitmap, src_bitmap, dest_rect,
src_origin_offset, compositing_mode, tint_color);
break;
case GBitmapFormat1BitPalette:
case GBitmapFormat2BitPalette:
bitblt_bitmap_into_bitmap_tiled_palette_to_1bit(dest_bitmap, src_bitmap, dest_rect,
src_origin_offset, compositing_mode,
tint_color);
break;
default:
APP_LOG(APP_LOG_LEVEL_DEBUG, "Only 1 and 2 bit palettized images can be displayed.");
return;
}
}

View file

@ -0,0 +1,61 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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/graphics/framebuffer.h"
#include "applib/graphics/gtypes.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/bitset.h"
#include <stdint.h>
#include <string.h>
volatile const int FrameBuffer_MaxX = DISP_COLS;
volatile const int FrameBuffer_MaxY = DISP_ROWS;
volatile const int FrameBuffer_BytesPerRow = FRAMEBUFFER_BYTES_PER_ROW;
uint32_t *framebuffer_get_line(FrameBuffer *f, uint8_t y) {
PBL_ASSERTN(y < f->size.h);
return f->buffer + (y * ((f->size.w / 32) + 1));
}
inline size_t framebuffer_get_size_bytes(FrameBuffer *f) {
// TODO: Make FRAMEBUFFER_SIZE_BYTES a macro which takes the cols and rows if we ever want to
// support different size framebuffers for watches which have native 1-bit framebuffers where the
// size is not just COLS * ROWS.
return FRAMEBUFFER_SIZE_BYTES;
}
void framebuffer_clear(FrameBuffer *f) {
memset(f->buffer, 0xff, framebuffer_get_size_bytes(f));
framebuffer_dirty_all(f);
f->is_dirty = true;
}
void framebuffer_mark_dirty_rect(FrameBuffer *f, GRect rect) {
if (!f->is_dirty) {
f->dirty_rect = rect;
} else {
f->dirty_rect = grect_union(&f->dirty_rect, &rect);
}
const GRect clip_rect = (GRect) { GPointZero, f->size };
grect_clip(&f->dirty_rect, &clip_rect);
f->is_dirty = true;
}

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
#define FRAMEBUFFER_WORDS_PER_ROW ((DISP_COLS / 32) + 1)
#define FRAMEBUFFER_SIZE_DWORDS (DISP_ROWS * FRAMEBUFFER_WORDS_PER_ROW)
#define FRAMEBUFFER_BYTES_PER_ROW (FRAMEBUFFER_WORDS_PER_ROW * 4)
#define FRAMEBUFFER_SIZE_BYTES (DISP_ROWS * FRAMEBUFFER_BYTES_PER_ROW)
typedef struct FrameBuffer {
uint32_t buffer[FRAMEBUFFER_SIZE_DWORDS];
GSize size;
GRect dirty_rect; //<! Smallest rect covering all dirty pixels.
bool is_dirty;
} FrameBuffer;
uint32_t* framebuffer_get_line(FrameBuffer* f, uint8_t y);

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