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

29
src/fw/comm/ble/ble_log.h Normal file
View file

@ -0,0 +1,29 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#define FILE_LOG_COLOR LOG_COLOR_BLUE
#include "system/logging.h"
#include "system/passert.h"
#include "system/hexdump.h"
#define BLE_LOG_DEBUG(fmt, args...) PBL_LOG_D(LOG_DOMAIN_BLE, LOG_LEVEL_DEBUG, fmt, ## args)
#define BLE_LOG_VERBOSE(fmt, args...) PBL_LOG_D(LOG_DOMAIN_BLE, LOG_LEVEL_DEBUG_VERBOSE, fmt, ## args)
#define BLE_CORE_LOG_DEBUG(fmt, args...) PBL_LOG_D(LOG_DOMAIN_BLE_CORE, LOG_LEVEL_DEBUG, fmt, ## args)
#define BLE_GAP_LOG_DEBUG(fmt, args...) PBL_LOG_D(LOG_DOMAIN_BLE_GAP, LOG_LEVEL_DEBUG, fmt, ## args)
#define BLE_HEXDUMP(data, length) PBL_HEXDUMP_D(LOG_DOMAIN_BLE, LOG_LEVEL_DEBUG, data, length)

55
src/fw/comm/ble/gap_le.c Normal file
View file

@ -0,0 +1,55 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "gap_le.h"
#include "comm/bt_lock.h"
#include "gap_le_advert.h"
#include "gap_le_connection.h"
#include "gap_le_connect.h"
#include "gap_le_scan.h"
#include "gap_le_slave_discovery.h"
#include "kernel_le_client/kernel_le_client.h"
void gap_le_init(void) {
bt_lock();
{
gap_le_connection_init();
gap_le_scan_init();
gap_le_advert_init();
gap_le_slave_discovery_init();
// Depends on gap_le_advert:
gap_le_connect_init();
kernel_le_client_init();
}
bt_unlock();
}
void gap_le_deinit(void) {
bt_lock();
{
kernel_le_client_deinit();
gap_le_connect_deinit();
gap_le_slave_discovery_deinit();
gap_le_advert_deinit();
gap_le_scan_deinit();
gap_le_connection_deinit();
}
bt_unlock();
}

22
src/fw/comm/ble/gap_le.h Normal file
View file

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

View file

@ -0,0 +1,698 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "gap_le_advert.h"
#include "gap_le_connect.h"
#include <bluetooth/bt_driver_advert.h>
#include "comm/ble/ble_log.h"
#include "comm/bt_lock.h"
#include "kernel/pbl_malloc.h"
#include "services/common/analytics/analytics.h"
#include "services/common/regular_timer.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/list.h"
//! CC2564 / HCI Advertising Limitation:
//! ------------------------------
//! The Bluetooth chip can accept only one advertising payload, one
//! corresponding scan response and one set of intervals. However, we need to
//! juggle multiple advertising payloads for different needs. For example,
//! to be discoverable we need to advertise, to be reconnectable we need to
//! advertise something else, to be an iBeacon we need to advertise yet
//! something different, etc.
//! Unfortunately, the Ti CC2564 Bluetooth controller does not offer built-in
//! functionality to cope with this, so we need to implement a scheduling
//! mechanism in the firmware of the host / microcontroller.
//!
//! Advertisement Scheduling:
//! -------------------------
//! The advertisement scheduling is pretty dumb and works as follows:
//! The scheduler has "cycles" which are fixed size windows in time, during
//! which one of the scheduled jobs is set to advertise.
//!
//! At the beginning of a cycle, the scheduler decides which job to advertise
//! next. It will just round-robin through the jobs to advertise.
//!
//! Note that only one job is advertising at a time. Even though a job might
//! have such a long interval that another job could be squeezed in between,
//! clever things like that are not considered for simplicity's sake.
//!
//! To-Do's:
//! --------
//! - ble_discoverability/pairability.c
//! - Use private addresses for privacy / harder tracebility.
#define GAP_LE_ADVERT_LOG_LEVEL LOG_LEVEL_DEBUG
typedef struct GAPLEAdvertisingJob {
ListNode node;
//! The callback to call when this job is unscheduled.
GAPLEAdvertisingJobUnscheduleCallback unscheduled_callback;
//! The data to pass into the unscheduled callback.
void *unscheduled_callback_data;
//! The number of seconds the current term has been on air.
uint16_t term_time_elapsed_secs;
uint8_t cur_term;
uint8_t num_terms;
//! The terms are run in the order that they appear in this array
GAPLEAdvertisingJobTerm *terms;
GAPLEAdvertisingJobTag tag:8;
//! The advertisement and scan response data
BLEAdData payload;
} GAPLEAdvertisingJob;
// -----------------------------------------------------------------------------
// Static Variables -- MUST be protected with bt_lock/unlock!
static bool s_gap_le_advert_is_initialized;
static bool s_deinit_in_progress;
//! Circular list! Pointing to the current job that needs air-time.
static GAPLEAdvertisingJob *s_jobs;
//! Job that is currently on air.
static GAPLEAdvertisingJob *s_current;
//! Advertising data that was last configured into the controller.
//! @note This pointer may be dangling, don't try to reference!
static const BLEAdData *s_current_ad_data;
//! The regular timer that marks the end of a cycle and triggers the next job
//! to be aired.
static RegularTimerInfo s_cycle_regular_timer;
static bool s_is_advertising;
//! Cache of the last advertising transmission power in dBm. A cache is kept in
//! case the API call fails, for example because Bluetooth is disabled.
//! 12 dBm is what the PAN1315 Bluetooth module reports.
static int8_t s_tx_power_cached = 12;
// -----------------------------------------------------------------------------
//! Prototypes
static void prv_perform_next_job(bool force_refresh);
// -----------------------------------------------------------------------------
static const char * prv_string_for_debug_tag(GAPLEAdvertisingJobTag tag) {
switch (tag) {
case GAPLEAdvertisingJobTagDiscovery: return "DIS";
case GAPLEAdvertisingJobTagReconnection: return "RCN";
case GAPLEAdvertisingJobTagiOSAppLaunch: return "iOS";
default: return "?";
}
}
// -----------------------------------------------------------------------------
//! Helpers to manage the s_jobs list
//! bt_lock is expected to be taken with all of them!
static bool prv_is_current_term_infinite(const GAPLEAdvertisingJob *job) {
return (job->terms[job->cur_term].duration_secs == GAPLE_ADVERTISING_DURATION_INFINITE);
}
static bool prv_is_current_term_silent(GAPLEAdvertisingJob *job) {
return (job->terms[job->cur_term].min_interval_slots == GAPLE_ADVERTISING_SILENCE_INTERVAL_SLOTS);
}
//! Links the job into the ring of jobs. Will make the new job the start (s_jobs) of the ring only
//! if the first term isn't silent.
//! @return True if the new job was made the start of the ring, false if not.
static bool prv_link_job(GAPLEAdvertisingJob *job) {
if (!s_jobs) {
// First job, make it point to itself:
job->node.next = &job->node;
job->node.prev = &job->node;
s_jobs = job;
return true;
}
list_insert_after(&s_jobs->node, &job->node);
// Make it the next one up, unless the first term is silent:
if (!prv_is_current_term_silent(job)) {
s_jobs = job;
return true;
}
return false;
}
static void prv_unlink_job(GAPLEAdvertisingJob *job) {
if (job->node.next == &job->node) {
// Last job left...
job->node.next = NULL;
job->node.prev = NULL;
s_jobs = NULL;
} else {
list_remove(&job->node, (ListNode **) &s_jobs, NULL);
}
// Part of CC2564 advertising bug work-around:
if (job == s_current) {
bt_driver_advert_client_set_cycled(false);
}
}
static bool prv_is_registered_job(const GAPLEAdvertisingJob *job) {
if (!job) {
return false;
}
// Search jobs (can't use list_contains(), because circular):
ListNode *node = &s_jobs->node;
while (node) {
if (node == (const ListNode *) job) {
return true;
}
node = node->next;
if (node == &s_jobs->node) {
// wrapped around
break;
}
}
return false;
}
static void prv_increment_elapsed_time_for_job(GAPLEAdvertisingJob **job_ptr, bool *has_new_term) {
GAPLEAdvertisingJob *job = *job_ptr;
if (prv_is_current_term_infinite(job)) {
return;
}
// Increment the `time elapsed` counter:
++(job->term_time_elapsed_secs);
if (job->term_time_elapsed_secs >= job->terms[job->cur_term].duration_secs) {
// The current term has elapsed
job->cur_term++;
// Schedule the next term
if (job->cur_term < job->num_terms) {
// Take care of GAPLE_ADVERTISING_DURATION_LOOP_AROUND:
if (job->terms[job->cur_term].duration_secs == GAPLE_ADVERTISING_DURATION_LOOP_AROUND) {
BLE_LOG_DEBUG("Job looped around to term %"PRIu16,
job->terms[job->cur_term].loop_around_index);
job->cur_term = job->terms[job->cur_term].loop_around_index;
}
job->term_time_elapsed_secs = 0;
BLE_LOG_DEBUG("Job is performing next advertising term (%d/%d)",
job->cur_term, job->num_terms);
// force an update to make sure the new requested term takes
if (has_new_term) {
*has_new_term = true;
}
} else {
// Job's done, remove done job:
// If it's the last, this will update s_jobs to NULL as well:
prv_unlink_job(job);
// Call the unscheduled callback:
if (job->unscheduled_callback) {
job->unscheduled_callback(job, true /* completed */, job->unscheduled_callback_data);
}
BLE_LOG_DEBUG("Unscheduled advertising completed job: %s",
prv_string_for_debug_tag(job->tag));
kernel_free(job->terms);
kernel_free(job);
*job_ptr = NULL;
}
}
}
static void prv_increment_time_elapsed_for_all_silent_terms_except_current(void) {
if (!s_jobs) {
return;
}
GAPLEAdvertisingJob *job = s_jobs;
do {
GAPLEAdvertisingJob *next = (GAPLEAdvertisingJob *) job->node.next;
if (job != s_current && prv_is_current_term_silent(job)) {
prv_increment_elapsed_time_for_job(&job, NULL);
}
job = next;
} while (job != s_jobs);
}
// -----------------------------------------------------------------------------
//! Cycle timer callback.
//! It increments the air-time counter of the job's current term.
//! Updates the job's term if the term is done.
//! It removes the job if it's done.
//! It updates the s_jobs list.
//! It calls prv_perform_next_job() to set up the next job.
static void prv_cycle_timer_callback(void *unused) {
bool force_update = false;
bt_lock();
{
if (!s_current || !bt_driver_advert_client_has_cycled() || !s_gap_le_advert_is_initialized) {
// Job got removed in the meantime.
goto unlock;
}
prv_increment_time_elapsed_for_all_silent_terms_except_current();
GAPLEAdvertisingJob *job = s_current;
BLE_LOG_DEBUG("Currently running job: %s (non-connectable=%u)",
prv_string_for_debug_tag(job->tag), gap_le_connect_is_connected_as_slave());
// Set to next job (round-robin) that isn't silent (unless there is no non-silent one):
s_jobs = (GAPLEAdvertisingJob *) job->node.next;
while (prv_is_current_term_silent(s_jobs) &&
s_jobs != job /* looped around */) {
s_jobs = (GAPLEAdvertisingJob *) s_jobs->node.next;
};
prv_increment_elapsed_time_for_job(&job, &force_update);
prv_perform_next_job(force_update);
}
unlock:
bt_unlock();
}
// -----------------------------------------------------------------------------
//! Timer start / stop utilities
//! bt_lock is expected to be taken!
static void prv_timer_start(void) {
if (regular_timer_is_scheduled(&s_cycle_regular_timer)) {
PBL_LOG(LOG_LEVEL_ERROR, "Advertising timer already started");
regular_timer_remove_callback(&s_cycle_regular_timer);
}
regular_timer_add_seconds_callback(&s_cycle_regular_timer);
}
static void prv_timer_stop(void) {
regular_timer_remove_callback(&s_cycle_regular_timer);
}
// -----------------------------------------------------------------------------
//! Airs the next advertisement job.
//! It sends the ad & scan response data to the Bluetooth controller and
//! enables/disables advertising.
//! It sets up / cleans up the cycle timer.
//! It updates the s_current pointer.
//! It does *not* mutate the s_jobs list.
//! bt_lock is expected to be taken!
//! @param force_refresh If true, the advertisement job will be re-setup even
//! though the current job has not changed. This is (only) useful when the
//! connectability mode has changed.
static void prv_perform_next_job(bool force_refresh) {
// Part of work-around for CC2564 bug (see comment with prv_work_around_should_not_cycle).
// When forced, we've just connected or disconnected, in that case cycle as well, because
// otherwise we'll continue to advertise non-connectable if we've just disconnected and there
// was already a job being advertised.
if (!force_refresh && bt_driver_advert_should_not_cycle()) {
return;
}
// Pick the next job:
GAPLEAdvertisingJob *next = s_jobs;
// s_is_dangling is checked here, in case the next job happens to have been allocated at the
// same address as the old s_current:
const bool is_same_job = (next == s_current && bt_driver_advert_client_has_cycled());
if (is_same_job && !force_refresh && s_is_advertising) {
// No change in job to give air time, keep going.
return;
}
if (s_current) {
// Clean up old job:
if (!next) {
// No more jobs. Stop timer:
prv_timer_stop();
}
if (s_is_advertising) {
// Controller needs to stop advertising before we can start a new job:
PBL_LOG(GAP_LE_ADVERT_LOG_LEVEL, "Disable last Ad job");
bt_driver_advert_advertising_disable();
s_is_advertising = false;
analytics_stopwatch_stop(ANALYTICS_DEVICE_METRIC_BLE_ESTIMATED_BYTES_ADVERTISED_COUNT);
}
}
if (next) {
// Set up the next job to be on air:
if (!s_current) {
// No current job, start timer:
prv_timer_start();
}
if (!prv_is_current_term_silent(next)) {
const bool enable_scan_resp = (next->payload.scan_resp_data_length > 0);
if (s_current_ad_data != &next->payload) {
// Give the advertisement data to the BT controller:
bt_driver_advert_set_advertising_data(&next->payload);
s_current_ad_data = &next->payload;
}
// One slot is 625us:
const uint32_t min_interval_ms = ((next->terms[next->cur_term].min_interval_slots * 5) / 8);
const uint32_t max_interval_ms = ((next->terms[next->cur_term].max_interval_slots * 5) / 8);
BLE_LOG_DEBUG("Enable Ad job %s", prv_string_for_debug_tag(next->tag));
bool result = bt_driver_advert_advertising_enable(min_interval_ms, max_interval_ms,
enable_scan_resp);
if (result) {
s_is_advertising = true;
PBL_LOG(GAP_LE_ADVERT_LOG_LEVEL, "Airing advertising job: %s ",
prv_string_for_debug_tag(next->tag));
// Use average interval ms. BT controller does not report back what it uses.
const uint32_t interval_ms = (min_interval_ms + max_interval_ms) / 2;
// The ad data is fixed in size. See below.
// The scan response data size is omitted here, because we can't tell how
// often a scan request happens. BT controller does not report it either.
const uint32_t size = next->payload.ad_data_length /* ad data */ + 10 /* packet overhead */;
const uint32_t bytes_per_second = (size * 1000 /* ms */) / interval_ms;
analytics_stopwatch_start_at_rate(
ANALYTICS_DEVICE_METRIC_BLE_ESTIMATED_BYTES_ADVERTISED_COUNT,
bytes_per_second, AnalyticsClient_System);
}
}
}
s_current = next;
bt_driver_advert_client_set_cycled(true);
}
// -----------------------------------------------------------------------------
GAPLEAdvertisingJobRef gap_le_advert_schedule(const BLEAdData *payload,
const GAPLEAdvertisingJobTerm *terms,
uint8_t num_terms,
GAPLEAdvertisingJobUnscheduleCallback callback,
void *callback_data,
GAPLEAdvertisingJobTag tag) {
// Sanity check payload:
if (!payload ||
payload->ad_data_length > GAP_LE_AD_REPORT_DATA_MAX_LENGTH ||
payload->scan_resp_data_length > GAP_LE_AD_REPORT_DATA_MAX_LENGTH) {
return NULL;
}
// Each job must have at least 1 term
if (num_terms == 0 || terms == NULL) {
return NULL;
}
// Minimum interval is 32 slots (20ms), or 160 slots (100ms) when there is a
// scan response:
const uint16_t min_threshold = payload->scan_resp_data_length ? 160 : 32;
for (int i = 0; i < num_terms; i++) {
// Loop-around term:
const bool is_loop_around = (terms[i].duration_secs == GAPLE_ADVERTISING_DURATION_LOOP_AROUND);
if (is_loop_around) {
if (i == 0) {
// First term cannot be loop-around
return NULL;
}
continue;
}
// Silent term:
const bool is_silent =
(terms[i].min_interval_slots == GAPLE_ADVERTISING_SILENCE_INTERVAL_SLOTS &&
terms[i].max_interval_slots == GAPLE_ADVERTISING_SILENCE_INTERVAL_SLOTS);
if (is_silent) {
if (terms[i].duration_secs == GAPLE_ADVERTISING_DURATION_INFINITE) {
// Can't be silent forever
return NULL;
}
continue;
}
// Normal term, verify min and max interval values:
if (terms[i].min_interval_slots < min_threshold ||
terms[i].max_interval_slots < terms[i].min_interval_slots) {
return NULL;
}
}
// Create the job data structure:
GAPLEAdvertisingJob *job = kernel_malloc_check(sizeof(GAPLEAdvertisingJob) +
payload->ad_data_length +
payload->scan_resp_data_length);
*job = (const GAPLEAdvertisingJob) {
.unscheduled_callback = callback,
.unscheduled_callback_data = callback_data,
.term_time_elapsed_secs = 0,
.num_terms = num_terms,
.tag = tag,
.payload = {
.ad_data_length = payload->ad_data_length,
.scan_resp_data_length = payload->scan_resp_data_length,
},
};
job->terms = kernel_malloc_check(sizeof(GAPLEAdvertisingJobTerm) * num_terms);
memcpy(job->terms, terms, sizeof(GAPLEAdvertisingJobTerm) * num_terms);
memcpy(job->payload.data, payload->data,
payload->ad_data_length + payload->scan_resp_data_length);
PBL_LOG(LOG_LEVEL_INFO, "Scheduling advertising job: %s",
prv_string_for_debug_tag(job->tag));
// Schedule
bt_lock();
{
if (s_gap_le_advert_is_initialized && !s_deinit_in_progress) {
if (prv_link_job(job)) {
prv_perform_next_job(false);
}
} else {
kernel_free(job->terms);
kernel_free(job);
job = NULL;
}
}
bt_unlock();
return job;
}
// -----------------------------------------------------------------------------
void gap_le_advert_unschedule(GAPLEAdvertisingJobRef job) {
if (!job) {
return;
}
bool is_registered = false;
bt_lock();
{
if (!s_gap_le_advert_is_initialized) {
goto unlock;
}
is_registered = prv_is_registered_job(job);
if (is_registered) {
PBL_LOG(LOG_LEVEL_INFO, "Unscheduling advertising job: %s",
prv_string_for_debug_tag(job->tag));
prv_unlink_job(job);
prv_perform_next_job(false);
// Call the unscheduled callback:
if (job->unscheduled_callback) {
job->unscheduled_callback(job, false /* completed */,
job->unscheduled_callback_data);
}
// In case the payload pointer of a future jobs ends up being the same, ensure the adv data
// will get updated in that case:
if (s_current_ad_data == &job->payload) {
s_current_ad_data = NULL;
}
}
}
unlock:
bt_unlock();
if (is_registered) {
kernel_free(job->terms);
kernel_free(job);
}
}
void gap_le_advert_unschedule_job_types(
GAPLEAdvertisingJobTag *tag_types, size_t num_types) {
bt_lock();
ListNode *first_node = bt_driver_advert_client_has_cycled() ? &s_current->node : &s_jobs->node;
// get the last job in the list
ListNode *curr_node = list_get_prev(first_node);
// Note: We attempt to get the currently running job and walk through the
// list backwards so that we don't keep updating the running job as we remove
// advertisements from our list
while (curr_node) {
GAPLEAdvertisingJob *job = (GAPLEAdvertisingJob *)curr_node;
ListNode *prev_node = job->node.prev;
for (size_t i = 0; i < num_types; i++) {
if (job->tag == tag_types[i]) {
BLE_LOG_DEBUG("Removing advertisement of type %s",
prv_string_for_debug_tag(job->tag));
gap_le_advert_unschedule(job);
}
}
if (curr_node == first_node) {
break; // we have cycled through all the jobs
}
curr_node = prev_node;
}
bt_unlock();
}
// -----------------------------------------------------------------------------
int8_t gap_le_advert_get_tx_power(void) {
int8_t tx_power;
bt_lock();
{
// In case this API call fails, (e.g. Airplane Mode),
// the s_tx_power_cached is untouched:
if (bt_driver_advert_client_get_tx_power(&tx_power)) {
s_tx_power_cached = tx_power;
}
}
bt_unlock();
return tx_power;
}
// -----------------------------------------------------------------------------
void gap_le_advert_init(void) {
bt_lock();
{
if (s_gap_le_advert_is_initialized) {
PBL_LOG(LOG_LEVEL_ERROR, "gap le advert has already been initialized");
goto unlock;
}
s_deinit_in_progress = false;
s_jobs = NULL;
s_current = NULL;
s_current_ad_data = NULL;
s_cycle_regular_timer = (const RegularTimerInfo) {
.cb = prv_cycle_timer_callback,
};
s_is_advertising = false;
s_gap_le_advert_is_initialized = true;
}
unlock:
bt_unlock();
}
// -----------------------------------------------------------------------------
void gap_le_advert_deinit(void) {
bt_lock();
{
s_deinit_in_progress = true;
while (s_jobs) {
gap_le_advert_unschedule(s_jobs);
}
PBL_ASSERTN(!regular_timer_is_scheduled(&s_cycle_regular_timer) ||
regular_timer_pending_deletion(&s_cycle_regular_timer));
s_gap_le_advert_is_initialized = false;
}
bt_unlock();
}
// -----------------------------------------------------------------------------
void gap_le_advert_handle_connect_as_slave(void) {
bt_lock();
{
if (!s_gap_le_advert_is_initialized) {
goto unlock;
}
// The link layer state machine inside the Bluetooth controller
// automatically stops advertising when transitioning to "connected", so
// update our own state. See 7.8.9 of Bluetooth Specification
//
// We don't instantly cycle the advertisements because our LE client
// handler (kernel_le_client.c) will unschedule jobs accordingly and we
// want to avoid unnecessary refreshes of the advertising state
s_is_advertising = false;
if (!bt_driver_advert_client_has_cycled()) {
s_current = NULL;
bt_driver_advert_client_set_cycled(true);
}
analytics_stopwatch_stop(ANALYTICS_DEVICE_METRIC_BLE_ESTIMATED_BYTES_ADVERTISED_COUNT);
}
unlock:
bt_unlock();
}
// -----------------------------------------------------------------------------
void gap_le_advert_handle_disconnect_as_slave(void) {
bt_lock();
{
if (!s_gap_le_advert_is_initialized) {
goto unlock;
}
// Call prv_perform_next_job() to trigger refreshing the configuration of
// the controller: it can advertise connectable packets again.
prv_perform_next_job(true /* force refresh, connectability mode changed */);
}
unlock:
bt_unlock();
}
GAPLEAdvertisingJobRef gap_le_advert_get_current_job(void) {
return s_current;
}
GAPLEAdvertisingJobRef gap_le_advert_get_jobs(void) {
return s_jobs;
}
GAPLEAdvertisingJobTag gap_le_advert_get_job_tag(GAPLEAdvertisingJobRef job) {
return job->tag;
}
#undef GAP_LE_ADVERT_LOG_LEVEL

View file

@ -0,0 +1,153 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 {
GAPLEAdvertisingJobTagLegacy = 1,
GAPLEAdvertisingJobTagDiscovery,
GAPLEAdvertisingJobTagReconnection,
GAPLEAdvertisingJobTagiOSAppLaunch,
} GAPLEAdvertisingJobTag;
struct GAPLEAdvertisingJob;
//! Opaque reference to an advertising job.
typedef struct GAPLEAdvertisingJob * GAPLEAdvertisingJobRef;
// Each GAPLEAdvertisingJob consists of 1 or more term.
typedef struct GAPLEAdvertisingJobTerm {
//! The number of seconds this term is going to last for.
//! @note Use GAPLE_ADVERTISING_DURATION_INFINITE to indicate the term should last forever.
//! @note Use GAPLE_ADVERTISING_DURATION_LOOP_AROUND to indicate that the scheduler
//! should loop back to the first term.
uint16_t duration_secs;
union {
struct {
//! Advertising interval range in slots:
//! @note Use GAPLE_ADVERTISING_INFINITE_INTERVAL_SLOTS to indicate
//! the term should be "silent".
uint16_t min_interval_slots;
uint16_t max_interval_slots;
};
//! The index to loop back to.
//! @note only valid when duration_secs is GAPLE_ADVERTISING_DURATION_LOOP_AROUND.
uint16_t loop_around_index;
};
} GAPLEAdvertisingJobTerm;
//! Function pointer to callback to handle the unscheduling of a job.
//! In the callback, the client can clear its reference to the
//! job and update any other state. There can be 3 reasons for a job to get
//! unscheduled: 1) the desired job duration has been reached 2) the job was
//! manually unscheduled by calling gap_le_advert_unschedule 3) the advertising
//! subsystem was torn down, for example when the user put the device into
//! Airplane Mode.
//! @param job The advertising job that is unscheduled.
//! @param completed True if the job was unscheduled automatically because the
//! duration that it was supposed to be on-air, has been reached. False if it
//! was unscheduled and had not reached its duration yet (because it was
//! unscheduled using gap_le_advert_unschedule() or gap_le_advert_deinit()).
//! For infinite jobs, the value will always be false when unscheduled.
//! @param cb_data Pointer to client data as passed into gap_le_advert_schedule
typedef void (*GAPLEAdvertisingJobUnscheduleCallback)(GAPLEAdvertisingJobRef job,
bool completed,
void *cb_data);
//! Constant to use with gap_le_advert_schedule to schedule an advertisement job
//! with infinite duration.
#define GAPLE_ADVERTISING_DURATION_INFINITE ((uint16_t) ~0)
//! Constant to use with gap_le_advert_schedule to indicate that the job
//! scheduler should loop back to the first term.
#define GAPLE_ADVERTISING_DURATION_LOOP_AROUND ((uint16_t) 0)
//! Constant to use with gap_le_advert_schedule to schedule a "silence" term.
#define GAPLE_ADVERTISING_SILENCE_INTERVAL_SLOTS ((uint16_t) 0)
//! Schedules an advertisement & scan response job.
//! Based on the given minimum and maximum interval values, an interval is
//! used depending on other time related tasks the Bluetooth controller has to
//! perform.
//! @discussion Note that scheduled jobs will be unscheduled when the Bluetooth
//! stack is torn down (e.g. when going into Airplane Mode).
//! @param payload The payload with the advertising and scan response data to
//! be scheduled for air-time. @see ble_ad_parse.h for functions to build the
//! payload.
//! @param terms A combination of minimum advertisement interval, maximum advertisement
//! interval and duration. Each term is run in the order that they appear in the terms array.
//! The minimum advertisement interval for each term must be at minumum 32 slots (20ms), or
//! 160 slots (100ms) when there is a scan response. The maximum advertisement interval must
//! be larger than or equal to its corresponding min_interval_slots. The duration is the
//! minimum number of seconds that the term will be active. The sum of all the durations is
//! the minimum number of seconds that the advertisement payload has to be on-air.
//! The job is not guaranteed to get a consecutive period of air-time nor is it guaranteed that
//! it will get air-time immediately after returning from this function.
//! @param callback Pointer to a function that should be called when the job
//! is unscheduled. Note: bt_lock() *WILL* be held during the callback to
//! prevent subtle concurrency problems that can cause out-of-order state
//! updates.
//! @see GAPLEAdvertisingJobUnscheduleCallback for more info.
//! @param callback_data Pointer to arbitrary client data that is passed as an
//! argument with the unschedule callback.
//! @param tag A tag that will be used for debug logging.
//! @return Reference to the scheduled job, or NULL if the parameters were not
//! valid.
GAPLEAdvertisingJobRef gap_le_advert_schedule(const BLEAdData *payload,
const GAPLEAdvertisingJobTerm *terms,
uint8_t num_terms,
GAPLEAdvertisingJobUnscheduleCallback callback,
void *callback_data,
GAPLEAdvertisingJobTag tag);
//! Unschedules an existing advertisement job.
//! It is safe to call this function with a reference to a non-existing job.
//! @param advertisement_job Reference to the job to unschedule.
void gap_le_advert_unschedule(GAPLEAdvertisingJobRef advertisement_job);
//! Unschedules existing advertisement jobs of particular tag types. Only
//! reschedules advertisements after all the requested tag types have been
//! removed
//! @param types an array of tags for the Advertisement Types to remove
//! @param num_types the length of the 'types' list
void gap_le_advert_unschedule_job_types(
GAPLEAdvertisingJobTag *tag_types, size_t num_types);
//! Convenience function to get the transmission power level in dBm for
//! advertising channels.
int8_t gap_le_advert_get_tx_power(void);
//! Initialize the advertising scheduler.
//! This should be called when setting up the Bluetooth stack.
void gap_le_advert_init(void);
//! Tear down the advertising scheduler and any current jobs.
//! This should be called when tearing down the Bluetooth stack.
void gap_le_advert_deinit(void);
//! The BT controller stops advertising automatically when the master connects
//! to it (the local device being the slave). This should be called so that
//! gap_le_advert can update its internal state and start advertising
//! non-connectable advertisements after the connection is established.
void gap_le_advert_handle_connect_as_slave(void);
//! This should be called so that gap_le_advert can update its internal state
//! and start advertising connectable advertisements.
void gap_le_advert_handle_disconnect_as_slave(void);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,75 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <bluetooth/bluetooth_types.h>
#include "gap_le_task.h"
#include "services/common/bluetooth/bluetooth_persistent_storage.h"
#define GAP_LE_CONNECT_MASTER_MAX_CONNECTION_INTENTS (5)
//! Internal extensions to the standard HCI status values (see HCITypes.h)
typedef enum {
//! The virtual connection was disconnected because the user removed the bonding.
GAPLEConnectHCIReasonExtensionUserRemovedBonding = 0xFB,
//! The virtual connection was disconnected because the client called
//! gap_le_connect_cancel().
GAPLEConnectHCIReasonExtensionCancelConnect = 0xFC,
//! The virtual connection was disconnected because the system went into
//! airplane mode.
GAPLEConnectHCIReasonExtensionAirPlaneMode = 0xFD,
} GAPLEConnectHCIReasonExtension;
void gap_le_connect_init(void);
void gap_le_connect_deinit(void);
bool gap_le_connect_is_connected_as_slave(void);
void gap_le_connect_handle_bonding_change(BTBondingID bonding, BtPersistBondingOp op);
BTErrno gap_le_connect_connect(const BTDeviceInternal *device, bool auto_reconnect,
bool is_pairing_required, GAPLEClient client);
BTErrno gap_le_connect_cancel(const BTDeviceInternal *device, GAPLEClient client);
BTErrno gap_le_connect_connect_by_bonding(BTBondingID bonding_id, bool auto_reconnect,
bool is_pairing_required, GAPLEClient client);
BTErrno gap_le_connect_cancel_by_bonding(BTBondingID bonding_id, GAPLEClient client);
//! @note As opposed to gap_le_connect_cancel(), this function will not
//! generate virtual disconnection events for any connected devices.
//! This is because this function is used by the kernel to clean up after the client (app)
//! when it is in the process of terminating.
void gap_le_connect_cancel_all(GAPLEClient client);
// -------------------------------------------------------------------------------------------------
// For unit testing
bool gap_le_connect_has_pending_create_connection(void);
//! @return true if there is a connection intent for the specified device and
//! specified client.
bool gap_le_connect_has_connection_intent(const BTDeviceInternal *device,
GAPLEClient client);
bool gap_le_connect_has_connection_intent_for_bonding(BTBondingID bonding_id,
GAPLEClient c);
uint32_t gap_le_connect_connection_intents_count(void);

View file

@ -0,0 +1,347 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "gap_le_connect_params.h"
#include "gap_le_connection.h"
#include "bluetooth/gap_le_connect.h"
#include "bluetooth/responsiveness.h"
#include "comm/bluetooth_analytics.h"
#include "comm/bt_conn_mgr.h"
#include "comm/bt_lock.h"
#include "drivers/rtc.h"
#include "kernel/pbl_malloc.h"
#include "services/common/new_timer/new_timer.h"
#include "system/logging.h"
#include "util/time/time.h"
#include <bluetooth/bluetooth_types.h>
#include <stdint.h>
// [MT] See page 129 of BLE Developer's Handbook (R. Heydon) and also
// http://www.ti.com/lit/ug/swru271f/swru271f.pdf
//
// Connection Event In a BLE connection between two devices, a
// frequency-hopping scheme is used, in that the two devices each send and
// receive data from one another on a specific channel, then “meet” at a new
// channel (the link layer of the BLE stack handles the channel switching) at a
// specific amount of time later. This “meeting” where the two devices send and
// receive data is known as a “connection event”. Even if there is no
// application data to be sent or received, the two devices will still exchange
// link layer data to maintain the connection.
//
// Connection Interval - The connection interval is the amount of time between
// two connection events, in units of 1.25ms. The connection interval can range
// from a minimum value of 6 (7.5ms) to a maximum of 3200 (4.0s).
//
// Slave Latency (SL): the number of connection events that the slave can
// ignore. This allows the slave save power. When needed, the slave can respond
// to a connection event. Therefore the slave gets (SL+1) opportunities to
// send data back to the master. In other words, this enables lower latency
// responses from the slave, at the cost of the master's energy budget.
// Valid values: 0 - 499, however the maximum value must not make the effective
// connection interval (see below) greater than 16.0s
//
// Supervision timeout: This is the maximum amount of time between two
// successful connection events. If this amount of time passes without a
// successful connection event, the device is to consider the connection lost,
// and return to an unconnected (standby) state.
// Valid values: 100ms to 32,000ms. In addition, the timeout must be larger
// than the effective connection interval (explained below).
// Rule of thumb: the slave should be given at least 6 opportunities
// to resynchronize.
//
// Effective connection interval: is equal to the amount of time between two
// connection events, assuming that the slave skips the maximum number of
// possible events if slave latency is allowed (the effective connection
// interval is equal to the actual connection interval if slave latency is set
// to zero). It can be calculated using the formula:
// Effective Connection Interval = (Connection Interval) * (1+(Slave Latency))
//! This module contains a work-around for parameter update requests not being applied by
//! iOS / Apple's BT controller, even though they get "accepted" by the host.
//! @see gap_le_connect_params_handle_connection_parameter_update_response below for more
//! commentary about the erronous behavior.
//! Apple bugs / shortcomings: http://www.openradar.me/21400278 and http://www.openradar.me/21400457
//! It seems that if we start hammering the iOS device with more change requests, things don't get
//! better. This timeout value is empirically established using the "ble mode_monkey" prompt
//! command. After running the "ble mode_monkey" for a couple hours, no re-requests had happened.
#define REQUEST_TIMEOUT_MS (40 * 1000)
//! See v4.2 "9.3.12 Connection Interval Timing Parameters":
//! "The Peripheral device should not perform a Connection Parameter Update procedure
//! within TGAP(conn_pause_peripheral = 5 seconds) after establishing a connection."
#define REQUIRED_INIT_PAUSE_S (5)
#define REQUIRED_INIT_PAUSE_TICKS (REQUIRED_INIT_PAUSE_S * RTC_TICKS_HZ)
//! Try 3 times before giving up.
#define MAX_UPDATE_REQUEST_ATTEMPTS (3)
static const GAPLEConnectRequestParams s_default_connection_params_table[NumResponseTimeState] = {
[ResponseTimeMax] = {
#if BT_CONTROLLER_DA14681
.slave_latency_events = 0, // See PBL-38653
#else
.slave_latency_events = 4, // Max. allowed by iOS
#endif
.connection_interval_min_1_25ms = 135,
.connection_interval_max_1_25ms = 161,
.supervision_timeout_10ms = 600,
},
[ResponseTimeMiddle] = {
#if BT_CONTROLLER_DA14681
.slave_latency_events = 0, // See PBL-38653
#else
.slave_latency_events = 2,
#endif
.connection_interval_min_1_25ms = 135,
.connection_interval_max_1_25ms = 161,
.supervision_timeout_10ms = 600,
},
[ResponseTimeMin] = {
.slave_latency_events = 0, // Not using Slave Latency
.connection_interval_min_1_25ms = 9, // Min. allowed by iOS
.connection_interval_max_1_25ms = 17,
.supervision_timeout_10ms = 600,
},
};
extern void conn_mgr_handle_desired_state_granted(GAPLEConnection *hdl,
ResponseTimeState granted_state);
static void prv_watchdog_timer_callback(void *ctx);
static const GAPLEConnectRequestParams *prv_params_for_state(const GAPLEConnection *connection,
ResponseTimeState state) {
if (connection->connection_parameter_sets) {
return &connection->connection_parameter_sets[state];
}
return &s_default_connection_params_table[state];
}
static bool prv_do_actual_params_match_desired_state(const GAPLEConnection *connection,
ResponseTimeState state,
uint16_t *actual_conn_interval_ms_out) {
const BleConnectionParams *actual_params = &connection->conn_params;
if (actual_conn_interval_ms_out) {
*actual_conn_interval_ms_out = actual_params->conn_interval_1_25ms;
}
const GAPLEConnectRequestParams *desired_params = prv_params_for_state(connection, state);
// When the fastest state is desired, ignore the minimum bound:
bool is_interval_min_acceptable;
if (state == ResponseTimeMin) {
is_interval_min_acceptable = true;
} else {
is_interval_min_acceptable =
(actual_params->conn_interval_1_25ms >= desired_params->connection_interval_min_1_25ms);
}
return (is_interval_min_acceptable &&
actual_params->conn_interval_1_25ms <= desired_params->connection_interval_max_1_25ms &&
actual_params->slave_latency_events == desired_params->slave_latency_events);
}
static void prv_request_params_update(GAPLEConnection *connection,
ResponseTimeState state) {
if (connection->is_remote_device_managing_connection_parameters ||
connection->param_update_info.is_request_pending) {
return;
}
// We need to wait at least REQUIRED_INIT_PAUSE_TICKS after a connection before
// requesting new parameters.
uint32_t retry_ms = REQUEST_TIMEOUT_MS;
if ((rtc_get_ticks() - connection->ticks_since_connection) < REQUIRED_INIT_PAUSE_TICKS) {
retry_ms = (REQUIRED_INIT_PAUSE_S * MS_PER_SECOND);
goto retry;
}
// Fall-back:
uint16_t actual_connection_interval_ms =
prv_params_for_state(connection, ResponseTimeMax)->connection_interval_max_1_25ms;
if (prv_do_actual_params_match_desired_state(connection, state, &actual_connection_interval_ms)) {
return;
}
if (connection->param_update_info.attempts++ >= MAX_UPDATE_REQUEST_ATTEMPTS) {
// [MT]: I've hit this once now. When this happened the TI CC2564B became unresponsive.
// From the iOS side, it appeared as a connection timeout. A little while after this happened,
// the BT chip auto-reset work-around kicked in.
PBL_LOG(LOG_LEVEL_ERROR, "Max attempts reached, giving up. desired_state=%u", state);
bluetooth_analytics_handle_param_update_failed();
return;
}
// Note: the spec recommends waiting for a 30 second Tgap timeout before issuing a new update
// request. Bluetopia does not enforce this. However, Sriram Hariharan of Apple confirmed we
// do not need to do this with Apple devices: "As long as your stack ensures that connection
// update requests are sent only after the previous request is completed, you can ignore the
// 30 second Tgap timeout."
const GAPLEConnectRequestParams *desired_params = prv_params_for_state(connection, state);
BleConnectionParamsUpdateReq req = {
.interval_min_1_25ms = desired_params->connection_interval_min_1_25ms,
.interval_max_1_25ms = desired_params->connection_interval_max_1_25ms,
.slave_latency_events = desired_params->slave_latency_events,
.supervision_timeout_10ms = desired_params->supervision_timeout_10ms,
};
const bool success = bt_driver_le_connection_parameter_update(&connection->device, &req);
if (success) {
connection->param_update_info.is_request_pending = true;
}
retry:
// Restart watchdog timer:
new_timer_start(connection->param_update_info.watchdog_timer, retry_ms,
prv_watchdog_timer_callback, connection, 0);
}
static void prv_watchdog_timer_callback(void *ctx) {
// This should all take very little time, so just execute on NewTimer task:
bt_lock();
GAPLEConnection *connection = (GAPLEConnection *)ctx;
if (gap_le_connection_is_valid(connection)) {
// Override the flag:
connection->param_update_info.is_request_pending = false;
// Retry with most recently requested latency:
const ResponseTimeState state = conn_mgr_get_latency_for_le_connection(connection, NULL);
if (connection->param_update_info.attempts > 0) {
PBL_LOG(LOG_LEVEL_INFO, "Conn param request timed out: re-requesting %u", state);
}
prv_request_params_update(connection, state);
}
bt_unlock();
}
void gap_le_connect_params_request(GAPLEConnection *connection,
ResponseTimeState desired_state) {
// A new desired state is requested by the FW, start afresh:
connection->param_update_info.attempts = 0;
prv_request_params_update(connection, desired_state);
}
void gap_le_connect_params_setup_connection(GAPLEConnection *connection) {
connection->param_update_info.watchdog_timer = new_timer_create();
}
void gap_le_connect_params_cleanup_by_connection(GAPLEConnection *connection) {
new_timer_delete(connection->param_update_info.watchdog_timer);
}
// -------------------------------------------------------------------------------------------------
//! Extern'd for and used by bt_conn_mgr.c
ResponseTimeState gap_le_connect_params_get_actual_state(GAPLEConnection *connection) {
for (ResponseTimeState state = 0; state < NumResponseTimeState; ++state) {
if (prv_do_actual_params_match_desired_state(connection, state, NULL)) {
return state;
}
}
return ResponseTimeInvalid;
}
static void prv_evaluate(GAPLEConnection *connection, ResponseTimeState desired_state) {
if (prv_do_actual_params_match_desired_state(connection, desired_state, NULL)) {
conn_mgr_handle_desired_state_granted(connection, desired_state);
// If the timer callback is executing (waiting on bt_lock) at this point, it's not a problem
// because the actual vs desired state gets checked in the timer callback path as well.
new_timer_stop(connection->param_update_info.watchdog_timer);
return;
}
// Connection parameters are updated, but they don't match the desired parameters.
// (Re)request a parameter update:
prv_request_params_update(connection, desired_state);
}
// -------------------------------------------------------------------------------------------------
//! Extern'd for and used by services/common/bluetooth/pebble_pairing_service.c
//! bt_lock is assumed to be taken before calling this function.
//! Forces the module to re-evaluate whether the current parameters match the desired ones.
//! This is used when the set of desired request params are changed through Pebble Pairing Service.
void gap_le_connect_params_re_evaluate(GAPLEConnection *connection) {
const ResponseTimeState desired_state = conn_mgr_get_latency_for_le_connection(connection, NULL);
prv_evaluate(connection, desired_state);
}
// -------------------------------------------------------------------------------------------------
//! Extern'd for and used by gap_le_connect.c
//! Handles Bluetopia's Connection Parameter Updated event.
//! This event is sent by our BT controller when the updated parameters have actually been applied
//! and taken effect.
//! bt_lock is assumed to be taken before calling this function.
void bt_driver_handle_le_conn_params_update_event(const BleConnectionUpdateCompleteEvent *event) {
bt_lock();
const BleConnectionParams *params = &event->conn_params;
if (event->status != HciStatusCode_Success) {
goto unlock;
}
GAPLEConnection *connection = gap_le_connection_by_addr(&event->dev_address);
if (!connection) {
PBL_LOG(LOG_LEVEL_DEBUG, "Receiving conn param update but connection is no longer open");
goto unlock;
}
const ResponseTimeState desired_state = conn_mgr_get_latency_for_le_connection(connection, NULL);
const bool did_match_desired_state_before =
prv_do_actual_params_match_desired_state(connection, desired_state, NULL);
PBL_LOG(LOG_LEVEL_INFO,
"LE Conn params updated: status: %u, %u, slave lat: %u, supervision timeout: %u "
"did_match_before: %u",
event->status, params->conn_interval_1_25ms, params->slave_latency_events,
params->supervision_timeout_10ms, did_match_desired_state_before);
// Cache the BLE connection parameters
connection->conn_params = *params;
connection->param_update_info.is_request_pending = false;
const bool local_is_master = connection->local_is_master;
if (!local_is_master) {
bluetooth_analytics_handle_connection_params_update(params);
}
prv_evaluate(connection, desired_state);
unlock:
bt_unlock();
}
// -------------------------------------------------------------------------------------------------
//! Extern'd for and used by gap_le_connect.c
//! Handles Bluetopia's Connection Parameter Update Response.
//! This event is sent by the remote's host over the LE Signaling L2CAP channel, to either "accept"
//! or "reject" the parameter set as requested with GAP_LE_Connection_Parameter_Update_Request.
//! When the parameters are "accepted" the other side ought to apply them and a
//! LL_CONNECTION_UPDATE_REQ message (link layer) ought to be the result. However, this does not
//! always seem to be the case on iOS (8.3 and 9.0 beta 1).
#if 0 // TODO: Move to cc2564x driver and keep logging around
void gap_le_connect_params_handle_connection_parameter_update_response(
const GAP_LE_Connection_Parameter_Update_Response_Event_Data_t *event_data) {
if (event_data->Accepted) {
PBL_LOG(LOG_LEVEL_DEBUG, "Connection Parameter Update Response: accepted=%u",
event_data->Accepted);
} else {
PBL_LOG(LOG_LEVEL_ERROR, "Connection Parameter Update Response: accepted=%u",
event_data->Accepted);
}
}
#endif

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 <bluetooth/responsiveness.h>
typedef struct GAPLEConnection GAPLEConnection;
typedef struct GAPLEConnectRequestParams {
uint16_t connection_interval_min_1_25ms;
uint16_t connection_interval_max_1_25ms;
uint16_t slave_latency_events;
uint16_t supervision_timeout_10ms;
} GAPLEConnectRequestParams;
//! Requests a desired connection speed/power/latency behavior.
//! @param connection The connection for which the request the behavior.
//! @param desired_state The desired behavior.
//! @note The change does not take effect immediately. When Pebble is the LE slave, it depends on
//! the other side (master) to actually act upon the request and apply the change. With iOS
//! devices, this does not always happen.
void gap_le_connect_params_request(GAPLEConnection *connection,
ResponseTimeState desired_state);

View file

@ -0,0 +1,350 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "gap_le_connection.h"
#include "comm/bt_conn_mgr.h"
#include "comm/bt_lock.h"
#include "kernel/pbl_malloc.h"
#include "services/common/bluetooth/bluetooth_persistent_storage.h"
#include "system/passert.h"
#include "system/logging.h"
#include "util/list.h"
#include <btutil/bt_device.h>
#include <btutil/sm_util.h>
//! About this module
//! -----------------
//! - Book-keeping of connection-related state for GAP and GATT.
//! - gap_le_connection.c registers connections with this module.
//! - Passive. Does not initiate (dis)connections.
// -------------------------------------------------------------------------------------------------
// Defined in gatt_service_changed.c
extern void gatt_service_changed_server_cleanup_by_connection(struct GAPLEConnection *connection);
// Defined in gatt_client_discovery.c
extern void gatt_client_discovery_cleanup_by_connection(GAPLEConnection *connection,
BTErrno reason);
extern void gatt_client_cleanup_discovery_jobs(GAPLEConnection *connection);
// Defined in gap_le_connect_params.c
extern void gap_le_connect_params_setup_connection(GAPLEConnection *connection);
extern void gap_le_connect_params_cleanup_by_connection(GAPLEConnection *connection);
// -------------------------------------------------------------------------------------------------
// Static Variables -- MUST be protected with bt_lock/unlock!
static GAPLEConnection *s_connections;
static bool s_le_connection_module_initialized = false;
// -------------------------------------------------------------------------------------------------
// Internal helpers
static bool prv_list_filter_by_gatt_id(ListNode *found_node, void *data) {
const unsigned int connection_id = (uintptr_t) data;
const GAPLEConnection *connection = (const GAPLEConnection *) found_node;
return (connection->gatt_connection_id == connection_id);
}
static GAPLEConnection * prv_find_connection_by_gatt_id(uintptr_t connection_id) {
return (GAPLEConnection *) list_find(&s_connections->node,
prv_list_filter_by_gatt_id,
(void *) connection_id);
}
static bool prv_list_filter_for_addr(ListNode *found_node, void *data) {
const BTDeviceAddress *addr = (const BTDeviceAddress *) data;
const GAPLEConnection *connection = (const GAPLEConnection *) found_node;
return bt_device_address_equal(&connection->device.address, addr);
}
static GAPLEConnection * prv_find_connection_by_addr(const BTDeviceAddress *addr) {
return (GAPLEConnection *) list_find(&s_connections->node,
prv_list_filter_for_addr,
(void *) addr);
}
static bool prv_list_filter_for_device(ListNode *found_node, void *data) {
const BTDeviceInternal *device = (const BTDeviceInternal *) data;
const GAPLEConnection *connection = (const GAPLEConnection *) found_node;
return bt_device_equal(&connection->device.opaque, &device->opaque);
}
static GAPLEConnection * prv_find_connection(const BTDeviceInternal *device) {
return (GAPLEConnection *) list_find(&s_connections->node,
prv_list_filter_for_device,
(void *) device);
}
// -------------------------------------------------------------------------------------------------
static bool prv_find_connection_by_irk_filter(GAPLEConnection *connection, void *data) {
const SMIdentityResolvingKey *irk = (const SMIdentityResolvingKey *)data;
if (!connection->irk) {
return false;
}
return (0 == memcmp(irk, connection->irk, sizeof(*irk)));
}
//! bt_lock() is expected to be taken by the caller
GAPLEConnection *gap_le_connection_find_by_irk(const SMIdentityResolvingKey *irk) {
return gap_le_connection_find(prv_find_connection_by_irk_filter, (void *)irk);
}
// -------------------------------------------------------------------------------------------------
//! bt_lock() is expected to be taken by the caller
void gap_le_connection_set_irk(GAPLEConnection *connection, const SMIdentityResolvingKey *irk) {
if (connection->irk) {
kernel_free(connection->irk);
}
SMIdentityResolvingKey *irk_copy = NULL;
if (irk) {
irk_copy = kernel_zalloc_check(sizeof(*irk_copy));
memcpy(irk_copy, irk, sizeof(*irk_copy));
}
connection->irk = irk_copy;
}
// -------------------------------------------------------------------------------------------------
GAPLEConnection *gap_le_connection_add(const BTDeviceInternal *device,
const SMIdentityResolvingKey *irk,
bool local_is_master) {
bt_lock_assert_held(true /* is_held */);
PBL_ASSERTN(!gap_le_connection_is_connected(device));
GAPLEConnection *connection = kernel_zalloc_check(sizeof(GAPLEConnection));
*connection = (const GAPLEConnection) {
.device = *device,
.local_is_master = local_is_master,
.conn_mgr_info = bt_conn_mgr_info_init(),
.bonding_id = BT_BONDING_ID_INVALID,
.ticks_since_connection = rtc_get_ticks(),
.is_remote_device_managing_connection_parameters = false,
.connection_parameter_sets = NULL,
};
gap_le_connection_set_irk(connection, irk);
s_connections = (GAPLEConnection *) list_prepend(&s_connections->node,
&connection->node);
gap_le_connect_params_setup_connection(connection);
return connection;
}
// -------------------------------------------------------------------------------------------------
void prv_destroy_connection(GAPLEConnection *connection) {
gatt_service_changed_server_cleanup_by_connection(connection);
gap_le_connect_params_cleanup_by_connection(connection);
gatt_client_discovery_cleanup_by_connection(connection, BTErrnoServiceDiscoveryDisconnected);
gatt_client_subscriptions_cleanup_by_connection(connection, false /* should_unsubscribe */);
gatt_client_cleanup_discovery_jobs(connection);
list_remove(&connection->node, (ListNode **) &s_connections, NULL);
bt_conn_mgr_info_deinit(&connection->conn_mgr_info);
kernel_free(connection->connection_parameter_sets);
kernel_free(connection->pairing_state);
kernel_free(connection->device_name);
kernel_free(connection->irk);
kernel_free(connection);
}
void gap_le_connection_remove(const BTDeviceInternal *device) {
bt_lock();
{
GAPLEConnection *connection = prv_find_connection(device);
// Verify that:
// the reason we can't find a connection is because we have deinitialized everything
// we only have connections stored after the module has been initialized
PBL_ASSERTN((connection != NULL) == s_le_connection_module_initialized);
if (connection) {
prv_destroy_connection(connection);
}
}
bt_unlock();
}
// -------------------------------------------------------------------------------------------------
bool gap_le_connection_is_connected(const BTDeviceInternal *device) {
bt_lock();
const bool connected = (prv_find_connection(device) != NULL);
bt_unlock();
return connected;
}
// -------------------------------------------------------------------------------------------------
uint16_t gap_le_connection_get_gatt_mtu(const BTDeviceInternal *device) {
bt_lock();
const GAPLEConnection *connection = prv_find_connection(device);
const uint16_t mtu = connection ? connection->gatt_mtu : 0;
bt_unlock();
return mtu;
}
// -------------------------------------------------------------------------------------------------
void gap_le_connection_init(void) {
s_le_connection_module_initialized = true;
}
// -------------------------------------------------------------------------------------------------
void gap_le_connection_deinit(void) {
bt_lock();
{
GAPLEConnection *connection = s_connections;
while (connection) {
GAPLEConnection *next_connection =
(GAPLEConnection *) connection->node.next;
prv_destroy_connection(connection);
connection = next_connection;
}
s_connections = NULL;
s_le_connection_module_initialized = false;
}
bt_unlock();
}
// -------------------------------------------------------------------------------------------------
// The call below require the caller to own the bt_lock while calling the
// function and for as long as the result is being used / accessed.
// -------------------------------------------------------------------------------------------------
GAPLEConnection *gap_le_connection_any(void) {
return s_connections;
}
static bool prv_valid_conn_filter(ListNode *found_node, void *data) {
GAPLEConnection *searching_for = (GAPLEConnection *)data;
GAPLEConnection *conn = (GAPLEConnection *)found_node;
return (searching_for == conn);
}
bool gap_le_connection_is_valid(const GAPLEConnection *conn) {
return (list_find(&s_connections->node, prv_valid_conn_filter, (void *)conn) != NULL);
}
//! @note !!! To access the returned context bt_lock MUST be held!!!
GAPLEConnection *gap_le_connection_by_device(const BTDeviceInternal *device) {
return prv_find_connection(device);
}
// -------------------------------------------------------------------------------------------------
//! @note !!! To access the returned context bt_lock MUST be held!!!
GAPLEConnection *gap_le_connection_by_addr(const BTDeviceAddress *addr) {
return prv_find_connection_by_addr(addr);
}
// -------------------------------------------------------------------------------------------------
//! @note !!! To access the returned context bt_lock MUST be held!!!
GAPLEConnection *gap_le_connection_by_gatt_id(unsigned int connection_id) {
return prv_find_connection_by_gatt_id(connection_id);
}
// -------------------------------------------------------------------------------------------------
//! @note !!! To access the returned context bt_lock MUST be held!!!
GAPLEConnection *gap_le_connection_find(GAPLEConnectionFindCallback filter,
void *data) {
return (GAPLEConnection *) list_find(&s_connections->node,
(ListFilterCallback) filter,
data);
}
// -------------------------------------------------------------------------------------------------
//! @note !!! To access the returned context bt_lock MUST be held!!!
void gap_le_connection_for_each(GAPLEConnectionForEachCallback cb, void *data) {
GAPLEConnection *connection = s_connections;
while (connection) {
cb(connection, data);
connection = (GAPLEConnection *) connection->node.next;
}
}
// -------------------------------------------------------------------------------------------------
void gap_le_connection_set_gateway(GAPLEConnection *connection, bool is_gateway) {
connection->is_gateway = is_gateway;
// TODO: update bonding `is_gateway` flag
// bt_persistent_storage_...
}
// -------------------------------------------------------------------------------------------------
static bool prv_find_gateway(GAPLEConnection *connection, void *data) {
return connection->is_gateway;
}
GAPLEConnection *gap_le_connection_get_gateway(void) {
return gap_le_connection_find(prv_find_gateway, NULL);
}
// -------------------------------------------------------------------------------------------------
static bool prv_find_connection_with_bonding_id(GAPLEConnection *connection, void *data) {
BTBondingID bonding_id = (BTBondingID)(uintptr_t)data;
return (connection->bonding_id == bonding_id);
}
void gap_le_connection_handle_bonding_change(BTBondingID bonding, BtPersistBondingOp op) {
if (op != BtPersistBondingOpWillDelete) {
return;
}
// Clean up the bonding_id field for the bonding that just got removed:
bt_lock();
GAPLEConnection *connection = gap_le_connection_find(prv_find_connection_with_bonding_id,
(void *)(uintptr_t)bonding);
if (connection) {
connection->bonding_id = BT_BONDING_ID_INVALID;
}
bt_unlock();
}
void gap_le_connection_copy_device_name(
const GAPLEConnection *connection, char *name_out, size_t name_out_len) {
bt_lock();
{
if (!gap_le_connection_is_valid(connection)) {
goto unlock;
}
if (connection->device_name != NULL) {
strncpy(name_out, connection->device_name, name_out_len);
}
name_out[name_out_len - 1] = '\0';
}
unlock:
bt_unlock();
}

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 "comm/bt_conn_mgr_impl.h"
#include "drivers/rtc.h"
#include "gatt_client_accessors.h"
#include "gatt_client_discovery.h"
#include "gatt_client_subscriptions.h"
#include "services/common/new_timer/new_timer.h"
#include <bluetooth/bluetooth_types.h>
#include <bluetooth/gap_le_connect.h>
#include <bluetooth/sm_types.h>
// FIXME: Including this header results in a compile time failure because the
// chain eventually includes a Bluetopia API. Figure out why this is problematic
// #include "services/common/bluetooth/bluetooth_persistent_storage.h"
// void gap_le_connection_handle_bonding_change(BTBondingID bonding, BtPersistBondingOp op);
// -----------------------------------------------------------------------------
// The calls below are thread-safe, no need to own bt_lock, per se, before
// calling them.
void gap_le_connection_remove(const BTDeviceInternal *device);
bool gap_le_connection_is_connected(const BTDeviceInternal *device);
uint16_t gap_le_connection_get_gatt_mtu(const BTDeviceInternal *device);
void gap_le_connection_init(void);
void gap_le_connection_deinit(void);
typedef struct DiscoveryJobQueue DiscoveryJobQueue;
typedef struct GAPLEConnectRequestParams GAPLEConnectRequestParams;
typedef struct SMPairingState SMPairingState;
// -----------------------------------------------------------------------------
// The calls below require the caller to own the bt_lock while calling the
// function and for as long as the result is being used / accessed.
typedef struct GAPLEConnection {
ListNode node;
//! The remote device its (connection) address.
BTDeviceInternal device;
//! Whether we are the master for this connection.
bool local_is_master:1;
//! Whether the connection is encrypted or not.
bool is_encrypted:1;
//! Whether GATT service discovery is in progress
bool gatt_is_service_discovery_in_progress:1;
//! Whether the connected device is our gateway (aka "the phone running Pebble app")
bool is_gateway:1;
//! @see pebble_pairing_service.c
bool is_subscribed_to_connection_status_notifications:1;
bool is_subscribed_to_gatt_mtu_notifications:1;
//! Whether the device is subscribed to heart rate monitor value updates (the other device has
//! enabled the "Notifications" bit of the CCCD).
bool hrm_service_is_subscribed:1;
//! The number of service discovery retries.
//! See field `gatt_service_discovery_watchdog_timer`
uint8_t gatt_service_discovery_retries:GATT_CLIENT_DISCOVERY_MAX_RETRY_BITS;
//! The generation number of the remote services that have been discovered.
uint8_t gatt_service_discovery_generation;
//! Bluetopia's internal identifier for the GATT connection.
//! This is not a concept that can be found in the Bluetooth specification,
//! it's internal to Bluetopia.
uintptr_t gatt_connection_id;
//! Maximum Transmission Unit. "The maximum size of payload data, in octets,
//! that the upper layer entity is capable of accepting."
uint16_t gatt_mtu;
//! The ATT handle of the "Service Changed" characteristic.
//! See gatt_service_changed.c
uint16_t gatt_service_changed_att_handle;
bool has_sent_gatt_service_changed_indication;
TimerID gatt_service_changed_indication_timer;
//! The bonding ID (only for BLE at the moment).
//! If the device is not bonded, the field will be BT_BONDING_ID_INVALID
BTBondingID bonding_id;
//! The IRK of the remote device, NULL if the connection address was not resolved.
SMIdentityResolvingKey *irk;
//! @see gap_le_device_name.c
char *device_name;
//! List of services that have been discovered on the remote device.
GATTServiceNode *gatt_remote_services;
//! List of subscriptions to notifications/
GATTClientSubscriptionNode *gatt_subscriptions;
//! Temporary, connection related pairing data (Bluetopia/cc2564 only)
SMPairingState *pairing_state;
//! Opaque, used by bt_conn_mgr to decide speed connection should run at
ConnectionMgrInfo *conn_mgr_info;
//! Opaque, used by gatt_client_discovery.c
DiscoveryJobQueue *discovery_jobs;
//! @see gap_le_connect_params.c
struct {
TimerID watchdog_timer;
uint8_t attempts;
bool is_request_pending;
} param_update_info;
//! Current BLE connection parameter cache
BleConnectionParams conn_params;
//! Contains the BT chip version info for the remote device if available (all 0's if not)
BleRemoteVersionInfo remote_version_info;
//! @see pebble_pairing_service.h for info on these fields:
bool is_remote_device_managing_connection_parameters;
//! Custom connection parameter sets for each ResponseTimeState, as written by the remote through
//! the Pebble Pairing Service. Can be NULL if the remote has never written any.
GAPLEConnectRequestParams *connection_parameter_sets;
RtcTicks ticks_since_connection;
} GAPLEConnection;
GAPLEConnection *gap_le_connection_add(const BTDeviceInternal *device,
const SMIdentityResolvingKey *irk,
bool local_is_master);
//! Checks to see if the LE connection is in our list of currently tracked
//! connections
bool gap_le_connection_is_valid(const GAPLEConnection *conn);
//! Find the first GAPLEConnection
//! Added for legacy support (pp_ble_control_legacy.c)
GAPLEConnection *gap_le_connection_any(void);
//! Find the GAPLEConnection by device.
//! @note !!! To access the returned context bt_lock MUST be held!!!
GAPLEConnection *gap_le_connection_by_device(const BTDeviceInternal *device);
//! Find the GAPLEConnection by Bluetooth device address.
//! @note !!! To access the returned context bt_lock MUST be held!!!
//! @note Bluetopia's GATT API seems to make no difference between public /
//! private addresses. Therefore, this function does not take a BTDevice.
GAPLEConnection *gap_le_connection_by_addr(const BTDeviceAddress *addr);
//! Find the GAPLEConnection by Bluetopia GATT ConnectionID.
//! @note !!! To access the returned context bt_lock MUST be held!!!
GAPLEConnection *gap_le_connection_by_gatt_id(unsigned int connection_id);
//! Find the GAPLEConnection by IRK.
GAPLEConnection *gap_le_connection_find_by_irk(const SMIdentityResolvingKey *irk);
typedef bool (*GAPLEConnectionFindCallback)(GAPLEConnection *connection,
void *data);
GAPLEConnection *gap_le_connection_find(GAPLEConnectionFindCallback filter,
void *data);
typedef void (*GAPLEConnectionForEachCallback)(GAPLEConnection *connection,
void *data);
void gap_le_connection_for_each(GAPLEConnectionForEachCallback cb, void *data);
//! @note deep-copies the IRK.
void gap_le_connection_set_irk(GAPLEConnection *connection, const SMIdentityResolvingKey *irk);
//! Sets whether the connection is to the gateway device (aka "the phone").
//! Updates the is_gateway flag on any associated bonding as well.
void gap_le_connection_set_gateway(GAPLEConnection *connection, bool is_gateway);
GAPLEConnection *gap_le_connection_get_gateway(void);
void gap_le_connection_copy_device_name(
const GAPLEConnection *connection, char *name_out, size_t namelen);

View file

@ -0,0 +1,77 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "gap_le_device_name.h"
#include "bluetooth/gap_le_device_name.h"
#include "comm/bt_lock.h"
#include "kernel/events.h"
#include "kernel/pbl_malloc.h"
#include "services/common/bluetooth/bluetooth_persistent_storage.h"
BTBondingID prv_get_bonding_id_and_name_from_address_safe(void *ctx, char* device_name) {
BTBondingID bonding_id = BT_BONDING_ID_INVALID;
BTDeviceAddress *addr = (BTDeviceAddress *)ctx;
GAPLEConnection *connection = gap_le_connection_by_addr(addr);
bt_lock();
if (!gap_le_connection_is_valid(connection)) {
goto unlock;
}
bonding_id = connection->bonding_id;
if (device_name) {
strncpy(device_name, connection->device_name, BT_DEVICE_NAME_BUFFER_SIZE);
device_name[BT_DEVICE_NAME_BUFFER_SIZE - 1] = '\0';
}
unlock:
bt_unlock();
return bonding_id;
}
void bt_driver_store_device_name_kernelbg_cb(void *ctx) {
char device_name[BT_DEVICE_NAME_BUFFER_SIZE];
BTBondingID bonding_id = prv_get_bonding_id_and_name_from_address_safe(ctx, device_name);
kernel_free(ctx);
if (bonding_id == BT_BONDING_ID_INVALID) {
return;
}
// Can't access flash when bt_lock() is held...
if (!bt_persistent_storage_update_ble_device_name(bonding_id, device_name)) {
return;
}
PebbleEvent event = {
.type = PEBBLE_BLE_DEVICE_NAME_UPDATED_EVENT,
};
event_put(&event);
}
void gap_le_device_name_request_all(void) {
bt_lock();
bt_driver_gap_le_device_name_request_all();
bt_unlock();
}
void gap_le_device_name_request(const BTDeviceInternal *address) {
bt_lock();
bt_driver_gap_le_device_name_request(address);
bt_unlock();
}

View file

@ -0,0 +1,29 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "gap_le_connection.h"
#include <bluetooth/bluetooth_types.h>
//! Requests the device name, caches the result in bt_persistent_storage and into
//! connection->device_name.
void gap_le_device_name_request(const BTDeviceInternal *address);
//! Convenience wrapper to request the device name for each connected BLE device, by calling
//! gap_le_device_name_request for each connection.
void gap_le_device_name_request_all(void);

View file

@ -0,0 +1,215 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#define FILE_LOG_COLOR LOG_COLOR_BLUE
#include "bluetooth/gap_le_scan.h"
#include "kernel/pbl_malloc.h"
#include "comm/bt_lock.h"
#include "gap_le_scan.h"
#include "kernel/events.h"
#include "system/hexdump.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/circular_buffer.h"
#include "util/likely.h"
#include <btutil/bt_device.h>
#include <string.h>
// -----------------------------------------------------------------------------
// Static Variables -- MUST be protected with bt_lock/unlock!
//! The current scanning state of the controller.
static bool s_is_scanning;
//! The backing array for the circular buffer of reports to be processed.
static uint8_t *s_reports_buffer;
//! The circular buffer that tracks reports to be processed.
//! [MT] Currently, there is only one potential client that reads from the
//! buffer (the app). In the future, I can imagine the kernel also wants to
//! the scan at the same time. When that happens, we need to keep a cursor for
//! each client.
static CircularBuffer s_circular_buffer;
//! Flag that is set true when the cancelling of the timer was attempted, but
//! did not succeed. If true, the timer callback will not finalize the pending
//! report because it is not the report for which the timer was set initially.
//! @see gap_le_scan_get_dropped_reports_count
static uint32_t s_dropped_reports;
// -----------------------------------------------------------------------------
bool gap_le_start_scan(void) {
bool success = false;
bt_lock();
{
if (!s_is_scanning) {
s_dropped_reports = 0;
success = bt_driver_start_le_scan(
true /* active_scan */, false /* use_white_list_filter */,
true /* filter_dups */, 10240, 10240);
if (success) {
// Allocate report buffers if advertising started successfully
const size_t buffer_size = GAP_LE_SCAN_REPORTS_BUFFER_SIZE;
s_reports_buffer = (uint8_t *) kernel_malloc_check(buffer_size);
circular_buffer_init(&s_circular_buffer, s_reports_buffer, buffer_size);
s_is_scanning = true;
}
}
}
bt_unlock();
return success;
}
// -----------------------------------------------------------------------------
bool gap_le_stop_scan(void) {
bool success = false;
bt_lock();
{
if (s_is_scanning) {
success = bt_driver_stop_le_scan();
kernel_free(s_reports_buffer);
s_reports_buffer = NULL;
s_is_scanning = false;
if (s_dropped_reports) {
PBL_LOG(LOG_LEVEL_INFO, "LE Scan -- Dropped reports: %" PRIu32, s_dropped_reports);
}
}
}
bt_unlock();
return success;
}
// -----------------------------------------------------------------------------
bool gap_le_is_scanning(void) {
bt_lock();
const bool is_scanning = s_is_scanning;
bt_unlock();
return is_scanning;
}
// -----------------------------------------------------------------------------
//! Copies over the pending report to the circular buffer and free the pending
//! "slot". In case there is no space left, the pending report will be dropped.
//! and a counter will be incremented
void bt_driver_cb_le_scan_handle_report(const GAPLERawAdReport *report_buffer, int length) {
const bool written = circular_buffer_write(
&s_circular_buffer, (uint8_t *)report_buffer, length);
if (!written) {
++s_dropped_reports;
} else { // notify clients there's a new event available
PebbleEvent e = {
.type = PEBBLE_BLE_SCAN_EVENT,
};
event_put(&e);
}
}
// -----------------------------------------------------------------------------
bool gap_le_consume_scan_results(uint8_t *buffer, uint16_t *size_in_out) {
// The number of bytes left to read:
uint16_t read_space;
// The space left in the output buffer:
uint16_t write_space = *size_in_out;
bt_lock();
{
if (UNLIKELY(!s_is_scanning)) {
// Return, the buffers are deallocated by now already...
*size_in_out = 0;
bt_unlock();
return false;
}
// We can't just copy over up to the maximum buffer size, because we could
// end up with half reports.
read_space = circular_buffer_get_read_space_remaining(&s_circular_buffer);
// While there are reports to read and there is enough space for at least
// the GAPLERawAdReport header:
while (read_space && write_space >= sizeof(GAPLERawAdReport)) {
// First copy the header. We know for sure this will fit into buffer,
// because it was tested in the while() condition:
circular_buffer_copy(&s_circular_buffer,
buffer, sizeof(GAPLERawAdReport));
const GAPLERawAdReport *report = (const GAPLERawAdReport *) buffer;
// Now use the copied header to figure out how big the report actually is:
const uint32_t payload_len = report->payload.ad_data_length +
report->payload.scan_resp_data_length;
const uint32_t report_len = sizeof(GAPLERawAdReport) + payload_len;
// There should always be at least enough bytes to read in the circular
// read the length of report, otherwise there's an internal inconsistency.
PBL_ASSERTN(read_space >= report_len);
if (report_len <= write_space) {
// Mark the bytes of the header as "consumed", as it's already copied:
circular_buffer_consume(&s_circular_buffer, sizeof(GAPLERawAdReport));
buffer += sizeof(GAPLERawAdReport);
// Now copy the payload:
circular_buffer_copy(&s_circular_buffer, buffer, payload_len);
circular_buffer_consume(&s_circular_buffer, payload_len);
buffer += payload_len;
// Update the counters:
write_space -= report_len;
read_space -= report_len;
} else {
// No more space in out buffer.
// The header might have been copied, but this has not been counted
// towards the "bytes copied" that is reported back to the client.
break;
}
}
// Out: number of bytes copied:
*size_in_out -= write_space;
}
bt_unlock();
return (read_space != 0);
}
// -----------------------------------------------------------------------------
void gap_le_scan_init(void) {
bt_lock();
s_is_scanning = false;
bt_unlock();
}
// -----------------------------------------------------------------------------
void gap_le_scan_deinit(void) {
bt_lock();
if (s_is_scanning) {
gap_le_stop_scan();
s_is_scanning = false;
}
bt_unlock();
}
// For UNIT Tests
uint32_t gap_le_scan_get_dropped_reports_count(void) {
return s_dropped_reports;
}

View file

@ -0,0 +1,84 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 <stdbool.h>
//! @internal
//! The number of reports that the circular reports buffer can contain.
//! Accomodate for 4 reports with advertisment and scan reponse data:
#define GAP_LE_SCAN_REPORTS_BUFFER_SIZE (4 * (sizeof(GAPLERawAdReport) + \
(2 * GAP_LE_AD_REPORT_DATA_MAX_LENGTH)))
//! @internal
//! This is a semi-processed advertisement report. It is "raw" in the sense that
//! the payload is not parsed. We use the unparsed payload to make it easier to
//! stuff into a circular buffer.
typedef struct {
//! Is the advertiser's address a public address or random address?
bool is_random_address:1;
uint8_t rsvd:7; // free for use
//! The address of the advertiser
BTDeviceInternal address;
//! Received signal strength indication
int8_t rssi;
//! The raw advertisement data, concatenated with the raw scan response data.
//! This will be parsed later down the road.
BLEAdData payload;
} GAPLERawAdReport;
//! @internal
//! Starts scanning for advertising reports and performs scan requests when
//! possible. Duplicates are filtered to avoid flooding the system. Advertising
//! reports and scan responses will be buffered. A PEBBLE_BLE_SCAN_EVENT will
//! be generated when there is data to be collected.
//! @see gap_le_consume_scan_results
//! @return 0 if scanning started succesfully or an error code otherwise.
bool gap_le_start_scan(void);
//! @internal
//! Stops scanning.
//! @return 0 if scanning stopped succesfully or an error code otherwise.
bool gap_le_stop_scan(void);
//! @internal
//! @return true if the controller is currently scanning, or false otherwise.
bool gap_le_is_scanning(void);
//! @internal
//! Copies the number of reports that have been collected.
//! @param[out] buffer into which the reports should be copied.
//! @param[in,out] size_in_out In: The number of bytes the buffer can hold.
//! It must be a valid address and not be NULL. Out: Number of copied bytes.
//! @return true if there were more reports to be copied, false if all buffered
//! reports have been copied.
bool gap_le_consume_scan_results(uint8_t *buffer, uint16_t *size_in_out);
//! @internal
//! Initializes the static state for this module and creates anything it needs
//! to function.
void gap_le_scan_init(void);
//! @internal
//! Stops any ongoing scanning and related activitie and cleans up anything that
//! had been created by gap_le_scan_init()
void gap_le_scan_deinit(void);

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.
*/
#include "gap_le_slave_discovery.h"
#include "gap_le_advert.h"
#include "applib/bluetooth/ble_ad_parse.h"
#include "comm/ble/ble_log.h"
#include "comm/bt_lock.h"
#include "git_version.auto.h"
#include "mfg/mfg_info.h"
#include "mfg/mfg_serials.h"
#include "services/common/bluetooth/local_id.h"
#include "services/normal/bluetooth/ble_hrm.h"
#include "system/passert.h"
#include "system/version.h"
#include <bluetooth/pebble_bt.h>
#include <bluetooth/pebble_pairing_service.h>
#include <bluetooth/bluetooth_types.h>
#include <btutil/bt_uuid.h>
#include <util/attributes.h>
#include <util/size.h>
static GAPLEAdvertisingJobRef s_discovery_advert_job;
// -----------------------------------------------------------------------------
//! Handles unscheduling of the discovery advertisement job.
static void prv_job_unschedule_callback(GAPLEAdvertisingJobRef job,
bool completed,
void *cb_data) {
// Cleanup:
s_discovery_advert_job = NULL;
}
// -----------------------------------------------------------------------------
//! Schedules the discovery advertisement job.
//! We don't want to be advertising at a high rate infinitely. When duration
//! is 0, a short period of high-rate advertising will be used. When this short
//! period is completed, an indefinite, low-rate job will be scheduled.
static void prv_schedule_ad_job(void) {
BLEAdData *ad = ble_ad_create();
// Advertisement part:
// Centrals will be filtering on Service UUID first. Assuming that the
// central is only doing a scan request if the Service UUID matches with their
// interests, to save radio time / battery life we keep the advertisement part
// as "small" as possible (21 bytes currently).
ble_ad_set_flags(ad, GAP_LE_AD_FLAGS_GEN_DISCOVERABLE_MASK);
// *DO NOT* use pebble_bt_uuid_expand() here!
// ble_ad_set_service_uuids() will be "smart" and include only the 16-bit UUID, but only if the
// BT SIG Base UUID is used.
Uuid service_uuids[2];
size_t num_uuids = 0;
#if CAPABILITY_HAS_BUILTIN_HRM
// NOTE: The HRM service has to be first in the list because otherwise the Pebble won't
// show up as an HRM device in Strava for Android...
if (ble_hrm_is_supported_and_enabled()) {
service_uuids[num_uuids++] = bt_uuid_expand_16bit(0x180D); // Heart Rate Service
}
#endif
// Pebble Pairing Service UUID:
service_uuids[num_uuids++] = bt_uuid_expand_16bit(PEBBLE_BT_PAIRING_SERVICE_UUID_16BIT);
ble_ad_set_service_uuids(ad, service_uuids, num_uuids);
char device_name[BT_DEVICE_NAME_BUFFER_SIZE];
bt_local_id_copy_device_name(device_name, true);
ble_ad_set_local_name(ad, device_name);
ble_ad_set_tx_power_level(ad);
// Scan response part:
ble_ad_start_scan_response(ad);
// Add serial number in a Manufacturer Specific AD Type:
struct PACKED ManufacturerSpecificData {
uint8_t payload_type;
char serial_number[MFG_SERIAL_NUMBER_SIZE];
uint8_t hw_platform;
uint8_t color;
struct {
uint8_t major;
uint8_t minor;
uint8_t patch;
} fw_version;
union {
uint8_t flags;
struct {
bool is_running_recovery_firmware:1;
bool is_first_use:1;
};
};
} mfg_data = {
.payload_type = 0 /* For future proofing. Only one type for now.*/,
.hw_platform = TINTIN_METADATA.hw_platform,
.color = mfg_info_get_watch_color(),
.fw_version = {
.major = GIT_MAJOR_VERSION,
.minor = GIT_MINOR_VERSION,
.patch = GIT_PATCH_VERSION,
},
.is_running_recovery_firmware = TINTIN_METADATA.is_recovery_firmware,
.is_first_use = false, // !getting_started_is_complete(), // TODO
};
memcpy(&mfg_data.serial_number,
mfg_get_serial_number(),
MFG_SERIAL_NUMBER_SIZE);
ble_ad_set_manufacturer_specific_data(ad,
PEBBLE_BT_VENDOR_ID,
(const uint8_t *) &mfg_data,
sizeof(struct ManufacturerSpecificData));
#if !RECOVERY_FW
// Initial high-rate period of 5 minutes long, then go slow for power savings:
const GAPLEAdvertisingJobTerm advert_terms[] = {
{
.min_interval_slots = 160, // 100ms
.max_interval_slots = 320, // 200ms
.duration_secs = 5 * 60, // 5 minutes
},
{
.min_interval_slots = 1636, // 1022.5ms
.max_interval_slots = 2056, // 1285ms
.duration_secs = GAPLE_ADVERTISING_DURATION_INFINITE,
}
};
s_discovery_advert_job = gap_le_advert_schedule(ad,
advert_terms,
sizeof(advert_terms)/sizeof(GAPLEAdvertisingJobTerm),
prv_job_unschedule_callback,
NULL,
GAPLEAdvertisingJobTagDiscovery);
#else
BLE_LOG_DEBUG("Running at PRF advertising rate for LE discovery");
// For PRF, just use a fast advertising rate indefinitely so the watch gets
// discovered as fast as possible
const GAPLEAdvertisingJobTerm advert_term = {
.min_interval_slots = 244, // 152.5ms
.max_interval_slots = 256, // 160ms
.duration_secs = GAPLE_ADVERTISING_DURATION_INFINITE,
};
s_discovery_advert_job = gap_le_advert_schedule(
ad, &advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
prv_job_unschedule_callback, NULL, GAPLEAdvertisingJobTagDiscovery);
#endif
ble_ad_destroy(ad);
}
// -----------------------------------------------------------------------------
bool gap_le_slave_is_discoverable(void) {
bool is_discoverable = false;
bt_lock();
{
is_discoverable = (s_discovery_advert_job != NULL);
}
bt_unlock();
return is_discoverable;
}
// -----------------------------------------------------------------------------
void gap_le_slave_set_discoverable(bool discoverable) {
bt_lock();
{
// Always stop and re-start, so we start with the high rate again:
gap_le_advert_unschedule(s_discovery_advert_job);
if (discoverable) {
prv_schedule_ad_job();
}
}
bt_unlock();
}
// -----------------------------------------------------------------------------
void gap_le_slave_discovery_init(void) {
bt_lock();
{
PBL_ASSERTN(!s_discovery_advert_job);
}
bt_unlock();
}
// -----------------------------------------------------------------------------
void gap_le_slave_discovery_deinit(void) {
bt_lock();
{
gap_le_advert_unschedule(s_discovery_advert_job);
}
bt_unlock();
}

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
//! @file gap_le_slave_discovery.h
//! This sub-module is responsible for advertising explicitely for device
//! discovery purposes. The advertisement will contain the device name,
//! transmit power level (to be able to order devices by estimated proximity),
//! Pebble Service UUID and discoverability flags.
//! Advertising devices will implicitely become the slave when being connected
//! to, so the "slave" part in the file name is redundant, but kept for
//! the sake of completeness.
#include <stdbool.h>
#include <stdint.h>
//! @return True is Pebble is currently explicitely discoverable as BLE slave
//! or false if not.
bool gap_le_slave_is_discoverable(void);
//! @param discoverable True to make Pebble currently explicitely discoverable
//! as BLE slave. Initially, Pebble will advertise at a relatively high rate for
//! a few seconds. After this, the rate will drop to save battery life.
void gap_le_slave_set_discoverable(bool discoverable);
//! Initializes the gap_le_slave_discovery module.
void gap_le_slave_discovery_init(void);
//! De-Initializes the gap_le_slave_discovery module.
void gap_le_slave_discovery_deinit(void);

View file

@ -0,0 +1,228 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "gap_le_slave_reconnect.h"
#include "applib/bluetooth/ble_ad_parse.h"
#include "gap_le.h"
#include "gap_le_advert.h"
#include "gap_le_connect.h"
#include "comm/ble/ble_log.h"
#include "comm/bt_lock.h"
#include "kernel/event_loop.h"
#include "services/common/bluetooth/bluetooth_persistent_storage.h"
#include "services/common/regular_timer.h"
#include "util/size.h"
#include <bluetooth/adv_reconnect.h>
#include <btutil/bt_uuid.h>
//! Reference to the reconnection advertising job.
//! bt_lock() needs to be taken before accessing this variable.
static GAPLEAdvertisingJobRef s_reconnect_advert_job;
static bool s_is_basic_reconnection_enabled;
static bool s_is_hrm_reconnection_enabled;
typedef enum {
ReconnectType_None, // Not advertising for reconnection
ReconnectType_Plain, // Advertising for reconnection with empty payload
ReconnectType_BleHrm // Advertising for reconnection with HRM payload
} ReconnectType;
// -----------------------------------------------------------------------------
//! Static, internal helper functions
static void prv_advert_job_unscheduled_callback(GAPLEAdvertisingJobRef job,
bool completed,
void *data) {
// bt_lock() is still held for us by gap_le_advert
s_reconnect_advert_job = NULL;
}
static bool prv_is_advertising_for_reconnection(void) {
return (s_reconnect_advert_job != NULL);
}
static ReconnectType prv_current_reconnect_type(void) {
if (s_is_hrm_reconnection_enabled) {
return ReconnectType_BleHrm;
}
if (s_is_basic_reconnection_enabled) {
return ReconnectType_Plain;
}
return ReconnectType_None;
}
static void prv_unschedule_adv_if_needed(void) {
if (prv_is_advertising_for_reconnection()) {
gap_le_advert_unschedule(s_reconnect_advert_job);
}
}
static void prv_evaluate(ReconnectType prev_type) {
ReconnectType cur_type = prv_current_reconnect_type();
if (cur_type == prev_type) {
return;
}
if (cur_type != ReconnectType_None) {
prv_unschedule_adv_if_needed();
#if CAPABILITY_HAS_BUILTIN_HRM
const bool use_hrm_payload = (cur_type == ReconnectType_BleHrm);
#else
const bool use_hrm_payload = false;
#endif
BLEAdData *ad;
if (use_hrm_payload) {
// Create adv payload with only flags + HR service UUID. This is enough for various mobile
// fitness apps to be able to reconnect to Pebble as BLE HRM.
ad = ble_ad_create();
ble_ad_set_flags(ad, GAP_LE_AD_FLAGS_GEN_DISCOVERABLE_MASK);
Uuid service_uuid = bt_uuid_expand_16bit(0x180D);
ble_ad_set_service_uuids(ad, &service_uuid, 1);
} else {
// [MT] Advertise with an empty payload to save battery life with these
// reconnection ad packets. This should be enough for the other
// device to be able to reconnect. With iOS it works, need to test Android.
// [MT] Note we leave out the Flags AD. According to the spec you have to
// include flags if any are non-zero. To abide, Pebble ought to always
// include the SIMULTANEOUS_LE_BR_EDR_TO_SAME_DEVICE_CONTROLLER and
// SIMULTANEOUS_LE_BR_EDR_TO_SAME_DEVICE_HOST flags. However, we have never
// done this (ignorance) and gotten by, by using a "random" address (the
// public address, but then inverted) as a work-around for the problems
// leaving out these flags caused with Android.
// I intend to use use the "Peripheral privacy feature" some time in the
// near future. With this, these flags and the issues on Android become
// a non-issue (because addresses will be private). Therefore I decided to
// still leave out the flags.
static BLEAdData payload = {
.ad_data_length = 0,
.scan_resp_data_length = 0,
};
ad = &payload;
}
size_t num_terms = 0;
const GAPLEAdvertisingJobTerm *advert_terms = bt_driver_adv_reconnect_get_job_terms(&num_terms);
s_reconnect_advert_job = gap_le_advert_schedule(ad,
advert_terms, num_terms,
prv_advert_job_unscheduled_callback,
NULL,
GAPLEAdvertisingJobTagReconnection);
if (use_hrm_payload) {
ble_ad_destroy(ad);
}
} else {
prv_unschedule_adv_if_needed();
}
}
static void prv_set_and_evaluate(bool *val, bool new_value) {
const ReconnectType prev_type = prv_current_reconnect_type();
*val = new_value;
prv_evaluate(prev_type);
}
// -----------------------------------------------------------------------------
void gap_le_slave_reconnect_stop(void) {
bt_lock();
{
prv_set_and_evaluate(&s_is_basic_reconnection_enabled, false);
}
bt_unlock();
}
// -----------------------------------------------------------------------------
void gap_le_slave_reconnect_start(void) {
#if RECOVERY_FW
return; // Only use discoverable packet for PRF
#endif
bt_lock();
{
if (prv_is_advertising_for_reconnection()) {
// Already advertising for reconnection...
goto unlock;
}
if (gap_le_connect_is_connected_as_slave()) {
// Already connected as slave...
goto unlock;
}
if (!bt_persistent_storage_has_active_ble_gateway_bonding() &&
!bt_persistent_storage_has_ble_ancs_bonding()) {
// No bonded master device that would want to reconnect, do nothing.
goto unlock;
}
prv_set_and_evaluate(&s_is_basic_reconnection_enabled, true);
}
unlock:
bt_unlock();
}
#if CAPABILITY_HAS_BUILTIN_HRM
#define RECONNECT_HRM_TIMEOUT_SECS (60)
static RegularTimerInfo s_hrm_reconnect_timer;
static void prv_hrm_reconnect_timeout_kernel_main_callback(void *data) {
gap_le_slave_reconnect_hrm_stop();
}
static void prv_hrm_reconnect_timeout_timer_callback(void *data) {
launcher_task_add_callback(prv_hrm_reconnect_timeout_kernel_main_callback, NULL);
}
// -----------------------------------------------------------------------------
void gap_le_slave_reconnect_hrm_restart(void) {
bt_lock();
{
prv_set_and_evaluate(&s_is_hrm_reconnection_enabled, true);
// Always restart the timer:
if (!regular_timer_is_scheduled(&s_hrm_reconnect_timer)) {
s_hrm_reconnect_timer = (RegularTimerInfo) {
.cb = prv_hrm_reconnect_timeout_timer_callback,
};
regular_timer_add_multisecond_callback(&s_hrm_reconnect_timer, RECONNECT_HRM_TIMEOUT_SECS);
}
}
bt_unlock();
}
// -----------------------------------------------------------------------------
void gap_le_slave_reconnect_hrm_stop(void) {
bt_lock();
{
prv_set_and_evaluate(&s_is_hrm_reconnection_enabled, false);
if (regular_timer_is_scheduled(&s_hrm_reconnect_timer)) {
regular_timer_remove_callback(&s_hrm_reconnect_timer);
}
}
bt_unlock();
}
#endif

View file

@ -0,0 +1,51 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <stdbool.h>
//! @file gap_le_slave_reconnect.h
//! Sub-system that will start advertising for reconnection, whenever there is a
//! bonded master device AND the local device is not already connected as slave.
//! The interface of the sub-system is merely a set of handlers to respond to
//! changes in slave connectivity and bonding.
//! Stops advertising for reconnection. For example, for when a connection to a
//! master gets established (only one master allowed in BT 4.0)
void gap_le_slave_reconnect_stop(void);
//! Start advertising for reconnection, but only if there is a bonded master
//! device. Otherwise, this is a no-op. In case the sub-system is already
//! advertising for reconnection, this function is a no-op.
//! Events for which this function should be called:
//! - When a connection to a master is lost
//! - When the list of bonded devices changes
//! - When Bluetooth is turned on
void gap_le_slave_reconnect_start(void);
#if CAPABILITY_HAS_BUILTIN_HRM
//! Start advertising for reconnection using a payload containing the Heart Rate Service UUID.
//! It will automatically stop after 60 seconds, in case gap_le_slave_reconnect_hrm_stop() is not
//! called sooner.
void gap_le_slave_reconnect_hrm_restart(void);
//! Stop advertising for reconnection using a payload containing the Heart Rate Service UUID.
//! This is a no-op when not advertising for HRM reconnection.
void gap_le_slave_reconnect_hrm_stop(void);
#endif

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.
*/
#include "gap_le_task.h"
#include "system/passert.h"
PebbleTaskBitset gap_le_pebble_task_bit_for_client(GAPLEClient c) {
switch (c) {
case GAPLEClientApp:
return (1 << PebbleTask_App);
case GAPLEClientKernel:
return (1 << PebbleTask_KernelMain);
default:
WTF;
}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "kernel/pebble_tasks.h"
typedef enum GAPLEClient {
GAPLEClientKernel,
GAPLEClientApp,
GAPLEClientNum
} GAPLEClient;
typedef uint8_t GAPLEClientBitset;
//! Converts from GAPLEClient enum to PebbleTaskBitset
PebbleTaskBitset gap_le_pebble_task_bit_for_client(GAPLEClient);

158
src/fw/comm/ble/gatt.c Normal file
View file

@ -0,0 +1,158 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <bluetooth/gatt.h>
#include "comm/ble/ble_log.h"
#include "comm/ble/gap_le_connection.h"
#include "comm/ble/gatt_service_changed.h"
#include "comm/bt_lock.h"
#include "kernel/events.h"
#include <bluetooth/pebble_pairing_service.h>
//! @see comment in gatt_client_subscriptions.c
extern void gatt_client_subscriptions_handle_server_notification(GAPLEConnection *connection,
uint16_t att_handle,
const uint8_t *att_value,
uint16_t att_length);
extern PebbleTaskBitset gap_le_connect_task_mask_for_connection(const GAPLEConnection *connection);
void bt_driver_cb_gatt_handle_connect(const GattDeviceConnectionEvent *event) {
bt_lock();
{
GAPLEConnection *connection = gap_le_connection_by_addr(&event->dev_address);
if (!connection) {
goto unlock;
}
connection->gatt_connection_id = event->connection_id;
connection->gatt_mtu = event->mtu;
BLE_LOG_DEBUG("GATT Connection for " BT_DEVICE_ADDRESS_FMT,
BT_DEVICE_ADDRESS_XPLODE(event->dev_address));
}
unlock:
bt_unlock();
}
void bt_driver_cb_gatt_handle_disconnect(const GattDeviceDisconnectionEvent *event) {
bt_lock();
{
GAPLEConnection *connection = gap_le_connection_by_addr(&event->dev_address);
if (!connection) {
goto unlock;
}
connection->gatt_connection_id = 0;
connection->gatt_mtu = 0;
BLE_LOG_DEBUG("GATT Disconnection for " BT_DEVICE_ADDRESS_FMT,
BT_DEVICE_ADDRESS_XPLODE(event->dev_address));
}
unlock:
bt_unlock();
}
void bt_driver_cb_gatt_handle_mtu_update(const GattDeviceMtuUpdateEvent *event) {
bt_lock();
{
GAPLEConnection *connection = gap_le_connection_by_addr(&event->dev_address);
if (!connection) {
goto unlock;
}
const bool has_mtu_changed = (connection->gatt_mtu != event->mtu);
PBL_LOG(LOG_LEVEL_DEBUG, "Handle MTU change from %d to %d bytes",
connection->gatt_mtu, event->mtu);
if (has_mtu_changed) {
connection->gatt_mtu = event->mtu;
bt_driver_pebble_pairing_service_handle_gatt_mtu_change(connection);
}
BLE_LOG_DEBUG("GATT MTU Updated: remote: %u", event->mtu);
}
unlock:
bt_unlock();
}
void bt_driver_cb_gatt_handle_notification(const GattServerNotifIndicEvent *event) {
GAPLEConnection *connection = NULL;
bt_lock();
{
connection = gap_le_connection_by_addr(&event->dev_address);
}
bt_unlock();
if (connection == NULL) {
return;
}
gatt_client_subscriptions_handle_server_notification(connection,
event->attr_handle,
event->attr_val,
event->attr_val_len);
BLE_LOG_DEBUG("GATT Server Notification for handle %u " BT_DEVICE_ADDRESS_FMT,
event->attr_handle, BT_DEVICE_ADDRESS_XPLODE(event->dev_address));
}
void bt_driver_cb_gatt_handle_indication(const GattServerNotifIndicEvent *event) {
GAPLEConnection *connection = NULL;
bool done = false;
bt_lock();
{
connection = gap_le_connection_by_addr(&event->dev_address);
BLE_LOG_DEBUG("GATT Server Indication for handle %u " BT_DEVICE_ADDRESS_FMT,
event->attr_handle,
BT_DEVICE_ADDRESS_XPLODE(event->dev_address));
// We are done if we got disconnected in the meantime or if this is a Service Changed indication
// consumed by gatt_service_changed.c
done = (connection == NULL) || gatt_service_changed_client_handle_indication(
connection, event->attr_handle, event->attr_val, event->attr_val_len);
}
bt_unlock();
if (done) {
return;
}
gatt_client_subscriptions_handle_server_notification(
connection, event->attr_handle, event->attr_val, event->attr_val_len);
}
void bt_driver_cb_gatt_handle_buffer_empty(const GattDeviceBufferEmptyEvent *event) {
bt_lock();
{
const GAPLEConnection *connection = gap_le_connection_by_addr(&event->dev_address);
if (!connection) {
goto unlock;
}
PebbleTaskBitset task_mask = gap_le_connect_task_mask_for_connection(connection);
PebbleEvent e = {
.type = PEBBLE_BLE_GATT_CLIENT_EVENT,
.task_mask = task_mask,
.bluetooth = {
.le = {
.gatt_client = {
.subtype = PebbleBLEGATTClientEventTypeBufferEmpty,
},
},
},
};
event_put(&e);
}
unlock:
bt_unlock();
}

View file

@ -0,0 +1,870 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#define FILE_LOG_COLOR LOG_COLOR_BLUE
#include "system/logging.h"
#include "gatt_client_accessors.h"
#include "gap_le_connection.h"
#include "comm/bt_lock.h"
#include "util/likely.h"
#include <btutil/bt_device.h>
#include <btutil/bt_uuid.h>
// -------------------------------------------------------------------------------------------------
// Helpers to calculate the BLEService, BLECharacteristic and BLEDescriptor
// opaque references. To avoid having to store separate identifiers, the values
// of these references are based on the pointer values to the internal data
// structures GATTService, GATTCharacteristic and GATTDescriptor. To provide
// extra protection against the scenario where an app uses a stale (pointer)
// value that after a new service discovery still happens to map to a valid
// object, the pointer values are XOR'd with a "generation" number. This
// generation number is changed whenever gatt_remote_services are updated.
// The most significant bit (MACHINE_WORD_MSB) is not used for RAM addresses
// and forced to be always set for a reference. This way, 0 is never used and
// we can use it to symbolize an "invalid reference".
#define MACHINE_WORD_MSB (((uintptr_t) 1) << ((sizeof(uintptr_t) * 8) - 1))
static uintptr_t prv_get_generation(const GAPLEConnection *connection) {
const uintptr_t mask = ~MACHINE_WORD_MSB;
const uint32_t timestamp = (connection->ticks_since_connection / RTC_TICKS_HZ);
return mask & ((uintptr_t) timestamp);
}
//! Please don't use directly, but use the prv_get_..._ref helpers so that the
//! compiler can catch type errors.
//! @see prv_get_object_by_ref for the inverse
static uintptr_t prv_get_ref(const GAPLEConnection *connection, const void *object) {
const uintptr_t generation = prv_get_generation(connection);
return (((uintptr_t)(void *) object) ^ generation) | MACHINE_WORD_MSB;
}
static uintptr_t prv_get_service_ref(const GAPLEConnection *connection,
const GATTServiceNode *service_node) {
return prv_get_ref(connection, service_node);
}
static uintptr_t prv_get_characteristic_ref(const GAPLEConnection *connection,
const GATTCharacteristic *characteristic) {
return prv_get_ref(connection, characteristic);
}
static uintptr_t prv_get_descriptor_ref(const GAPLEConnection *connection,
const GATTDescriptor *descriptor) {
return prv_get_ref(connection, descriptor);
}
//! Please don't use directly, but use the prv_get_...by_ref helpers so that the
//! compiler can catch type errors.
//! @see prv_get_ref for the inverse
static void * prv_get_object_by_ref(const GAPLEConnection *connection,
uintptr_t ref) {
const uintptr_t generation = prv_get_generation(connection);
const uintptr_t mask = ~MACHINE_WORD_MSB;
return (void *) ((ref ^ generation) & mask);
}
//! Returns internal GATTServiceNode associated with the connection and service reference.
//! Does not perform any validity checking on the reference, so not safe to call directly
//! with an untrusted service reference. @see prv_get_service_deref
static const GATTServiceNode *prv_get_service_by_ref(const GAPLEConnection *connection,
uintptr_t service_ref) {
return prv_get_object_by_ref(connection, service_ref);
}
// -------------------------------------------------------------------------------------------------
// Iteration Helpers
typedef bool (*GATTCharacteristicIterator)(const GATTCharacteristic *characteristic, void *cb_data);
typedef bool (*GATTDescriptorIterator)(const GATTDescriptor *descriptor, void *cb_data);
typedef bool (*GATTIncludedServicesIterator)(const GATTServiceNode *included_service_node,
void *cb_data);
typedef struct {
GATTCharacteristicIterator characteristic_iterator;
GATTDescriptorIterator descriptor_iterator;
GATTIncludedServicesIterator included_services_iterator;
} GATTIterationCallbacks;
static bool prv_find_service_node_by_att_handle_callback(ListNode *node, void *cb_data) {
const uint16_t att_handle = (uintptr_t) cb_data;
const GATTServiceNode *service_node = (const GATTServiceNode *) node;
return (service_node->service->att_handle == att_handle);
}
//! Find a sibling service node with given ATT handle
static const GATTServiceNode * prv_find_service_node_by_att_handle(
const GATTServiceNode *service_node,
uint16_t att_handle) {
return (const GATTServiceNode *) list_find_next((ListNode *)service_node,
prv_find_service_node_by_att_handle_callback,
true /* wrap around end */,
(void *)(uintptr_t) att_handle);
}
//! @return false if an iterator callback indicated it should not continue iterating,
//! or true if the iterator reached the end completely.
static bool prv_iter_service_node(const GATTServiceNode *service_node,
const GATTIterationCallbacks *callbacks,
void *cb_data) {
const GATTService *service = service_node->service;
// Walk all the characteristics for the service:
const GATTCharacteristic *characteristic = service->characteristics;
for (unsigned int c = 0; c < service->num_characteristics; ++c) {
if (callbacks->characteristic_iterator) {
const bool should_continue = callbacks->characteristic_iterator(characteristic, cb_data);
if (!should_continue) {
return false;
}
}
// Walk all the descriptors for this characteristic:
if (callbacks->descriptor_iterator) {
for (unsigned int d = 0; d < characteristic->num_descriptors; ++d) {
const GATTDescriptor *descriptor = &characteristic->descriptors[d];
const bool should_continue = callbacks->descriptor_iterator(descriptor, cb_data);
if (!should_continue) {
return false;
}
}
}
characteristic =
(const GATTCharacteristic *) &characteristic->descriptors[characteristic->num_descriptors];
}
// Walk all the Included Services:
if (callbacks->included_services_iterator &&
service->num_att_handles_included_services) {
// Included Services handles are tacked at the end, after the *last* descriptor of the *last*
// characteristic. The `characteristic` variable is pointing to the end at this point.
const uint16_t *handle = (const uint16_t *) characteristic;
for (int h = 0; h < service->num_att_handles_included_services; ++h) {
const GATTServiceNode *inc_service_node = prv_find_service_node_by_att_handle(service_node,
handle[h]);
if (inc_service_node) {
callbacks->included_services_iterator(inc_service_node, cb_data);
} else {
PBL_LOG(LOG_LEVEL_DEBUG, "Included Service with handle %u not found!", handle[h]);
}
}
}
return true;
}
// -------------------------------------------------------------------------------------------------
// Service lookup & validation of references
typedef struct {
BLEService service_ref;
const GATTServiceNode *service_node;
} FindServiceNodeByRefCtx;
static bool prv_find_connection_and_service_node_by_service_ref_find_cb(GAPLEConnection *connection,
void *cb_data) {
FindServiceNodeByRefCtx *ctx = (FindServiceNodeByRefCtx *) cb_data;
ListNode *head = &connection->gatt_remote_services->node;
const GATTServiceNode *service_node = prv_get_service_by_ref(connection, ctx->service_ref);
if (list_contains(head, &service_node->node)) {
// The service_ref is valid :)
// The associated GATTService is found with this GAPLEConnection!
ctx->service_node = service_node;
return true;
}
return false;
}
//! Based on a potentially invalid service reference, find the internal GATTServiceNode and
//! GAPLEConnection. This function is actually safe to call with an invalid / bogus service ref.
static const GATTServiceNode * prv_find_service_and_connection(BLEService service_ref,
const GAPLEConnection **out_connection) {
// Find the GAPLEConnection & GATTServiceNode with the BLEService service_ref:
FindServiceNodeByRefCtx ctx = {
.service_ref = service_ref,
};
const GAPLEConnection *connection =
gap_le_connection_find(prv_find_connection_and_service_node_by_service_ref_find_cb, &ctx);
if (connection) {
if (out_connection) {
*out_connection = connection;
}
return ctx.service_node;
}
return NULL;
}
// -------------------------------------------------------------------------------------------------
// Characteristic/Descriptor lookup & validation of references
typedef struct {
uintptr_t object_ref_in;
const GATTIterationCallbacks *object_iter_callbacks_in;
const GAPLEConnection *connection_out;
const GATTServiceNode *service_node_out;
const GATTCharacteristic *characteristic_out;
const GATTDescriptor *descriptor_out;
} FindObjectByRefCtx;
static bool prv_find_characteristic_cb(const GATTCharacteristic *characteristic, void *cb_data) {
FindObjectByRefCtx *ctx = (FindObjectByRefCtx *) cb_data;
if (ctx->object_ref_in == prv_get_ref(ctx->connection_out, characteristic)) {
ctx->characteristic_out = characteristic;
return false /* should_continue */;
}
return true /* should_continue */;
}
static bool prv_find_descriptor_cb(const GATTDescriptor *descriptor, void *cb_data) {
FindObjectByRefCtx *ctx = (FindObjectByRefCtx *) cb_data;
if (ctx->object_ref_in == prv_get_ref(ctx->connection_out, descriptor)) {
ctx->descriptor_out = descriptor;
return false /* should_continue */;
}
return true /* should_continue */;
}
//! Used only in prv_find_descriptor to keep track of the characteristic that contains the found
//! descriptor. It's kind of ugly, I know.
static bool prv_track_last_characteristic_cb(const GATTCharacteristic *characteristic,
void *cb_data) {
FindObjectByRefCtx *ctx = (FindObjectByRefCtx *) cb_data;
ctx->characteristic_out = characteristic;
return true /* should_continue */;
}
static bool prv_find_service_containing_object_by_ref_find_cb(ListNode *node, void *cb_data) {
FindObjectByRefCtx *ctx = (FindObjectByRefCtx *) cb_data;
const GATTServiceNode *service_node = (const GATTServiceNode *) node;
// Bail out early if the object reference resolves to an address outside of the service blob:
const uintptr_t object_addr = (uintptr_t) prv_get_object_by_ref(ctx->connection_out,
ctx->object_ref_in);
const uintptr_t service_node_addr = (uintptr_t) (void *) service_node->service;
const size_t size_bytes = service_node->service->size_bytes;
if (object_addr < service_node_addr || object_addr >= service_node_addr + size_bytes) {
return false /* should_stop -- list_find() is different... */;
}
// Try to find the object:
return !prv_iter_service_node(service_node, ctx->object_iter_callbacks_in, cb_data);
}
static bool prv_find_connection_and_object_by_ref_find_cb(GAPLEConnection *connection,
void *cb_data) {
FindObjectByRefCtx *ctx = (FindObjectByRefCtx *) cb_data;
// connection needed by:
// - prv_find_service_containing_object_by_ref_list_find_cb
// - prv_find_characteristic_cb
ctx->connection_out = connection;
ListNode *head = &connection->gatt_remote_services->node;
ctx->service_node_out =
(const GATTServiceNode *) list_find(head, prv_find_service_containing_object_by_ref_find_cb,
cb_data);
return (ctx->service_node_out != NULL);
}
static void prv_find_object(uintptr_t object_ref,
const GATTDescriptor **descriptor_out,
const GATTCharacteristic **characteristic_out,
const GATTServiceNode **service_node_out,
const GAPLEConnection **connection_out,
const GATTIterationCallbacks *object_iter_callbacks) {
FindObjectByRefCtx ctx = {
.object_ref_in = object_ref,
.object_iter_callbacks_in = object_iter_callbacks,
};
const GAPLEConnection *connection =
gap_le_connection_find(prv_find_connection_and_object_by_ref_find_cb, &ctx);
if (connection_out) {
*connection_out = connection;
}
if (service_node_out) {
*service_node_out = ctx.service_node_out;
}
if (characteristic_out) {
*characteristic_out = ctx.characteristic_out;
}
if (descriptor_out) {
*descriptor_out = ctx.descriptor_out;
}
}
//! Based on a potentially invalid characteristic reference, find the internal GATTCharacteristic
//! and GAPLEConnection.
//! This function is actually safe to call with an invalid / bogus characteristic reference.
static const GATTCharacteristic * prv_find_characteristic(BLECharacteristic characteristic_ref,
const GATTServiceNode **service_node_out,
const GAPLEConnection **connection_out) {
const GATTIterationCallbacks object_iter_callbacks = {
.characteristic_iterator = prv_find_characteristic_cb,
};
const GATTCharacteristic *characteristic;
prv_find_object(characteristic_ref, NULL, &characteristic, service_node_out, connection_out,
&object_iter_callbacks);
return characteristic;
}
//! Based on a potentially invalid descriptor reference, find the internal GATTDescriptor and
//! GAPLEConnection.
//! This function is actually safe to call with an invalid / bogus descriptor reference.
static const GATTDescriptor * prv_find_descriptor(BLEDescriptor descriptor_ref,
const GATTCharacteristic **characteristic_out,
const GATTServiceNode **service_node_out,
const GAPLEConnection **connection_out) {
const GATTIterationCallbacks object_iter_callbacks = {
.characteristic_iterator = prv_track_last_characteristic_cb,
.descriptor_iterator = prv_find_descriptor_cb,
};
const GATTDescriptor *descriptor;
const GATTCharacteristic *characteristic;
prv_find_object(descriptor_ref, &descriptor, &characteristic, service_node_out, connection_out,
&object_iter_callbacks);
if (characteristic_out) {
*characteristic_out = descriptor ? characteristic : NULL;
}
return descriptor;
}
// -------------------------------------------------------------------------------------------------
uint8_t gatt_client_copy_service_refs(const BTDeviceInternal *device,
BLEService services_out[],
uint8_t num_services) {
return gatt_client_copy_service_refs_matching_uuid(
device, services_out, num_services, NULL);
}
uint8_t gatt_client_copy_service_refs_by_discovery_generation(
const BTDeviceInternal *device, BLEService services_out[],
uint8_t num_services, uint8_t discovery_gen) {
uint8_t index = 0;
bt_lock();
{
GAPLEConnection *connection = gap_le_connection_by_device(device);
if (!connection) {
PBL_LOG(LOG_LEVEL_ERROR, "Disconnected in the mean time...");
goto unlock;
}
GATTServiceNode *node = connection->gatt_remote_services;
while (node) {
const bool is_match = (discovery_gen == node->service->discovery_generation);
if (is_match) {
if (index < num_services) {
services_out[index] = prv_get_service_ref(connection, node);
}
++index;
}
node = (GATTServiceNode *) node->node.next;
}
}
unlock:
bt_unlock();
// Contains number of available services because of final increment
return index;
}
uint8_t gatt_client_copy_service_refs_matching_uuid(const BTDeviceInternal *device,
BLEService services_out[],
uint8_t num_services,
const Uuid *matching_service_uuid) {
uint8_t index = 0;
bt_lock();
{
GAPLEConnection *connection = gap_le_connection_by_device(device);
if (!connection) {
PBL_LOG(LOG_LEVEL_ERROR, "Disconnected in the mean time...");
goto unlock;
}
GATTServiceNode *node = connection->gatt_remote_services;
while (node) {
const bool is_match = (!matching_service_uuid ||
uuid_equal(matching_service_uuid, &node->service->uuid));
if (is_match) {
if (index < num_services) {
services_out[index] = prv_get_service_ref(connection, node);
}
++index;
}
node = (GATTServiceNode *) node->node.next;
}
}
unlock:
bt_unlock();
// Contains number of available services because of final increment
return index;
}
// -------------------------------------------------------------------------------------------------
// Iteration callbacks to copy arrays of references into callback data of type CopyRefsCtx:
typedef struct {
const GAPLEConnection *connection;
uintptr_t *refs_out;
uint8_t num_found;
uint8_t num_max;
const Uuid * const matching_uuids;
} CopyRefsCtx;
//! Please do not use the functions that take GATTObjectHeader directly, but use the typed versions
//! instead: prv_copy_characteristic_refs_cb, prv_copy_descriptor_refs_cb and
//! prv_copy_included_service_refs_cb. This way the compiler can detect type errors.
//! Copies the reference for object into the CopyRefsCtx->refs_out array
static bool prv_copy_refs_cb(const GATTObjectHeader *object,
void *cb_data) {
CopyRefsCtx *ctx = (CopyRefsCtx *) cb_data;
int index = ctx->num_found++;
if (index < ctx->num_max) {
ctx->refs_out[index] = prv_get_ref(ctx->connection, object);
}
return true /* should_continue */;
}
//! Copies the reference for object into the CopyRefsCtx->refs_out array, only when
//! its Uuid is found in the matching_uuids array
static bool prv_copy_refs_matching_cb(const GATTObjectHeader *object,
void *cb_data) {
CopyRefsCtx *ctx = (CopyRefsCtx *) cb_data;
for (int i = 0; i < ctx->num_max; ++i) {
if (uuid_equal(&ctx->matching_uuids[i], &object->uuid)) {
ctx->refs_out[i] = prv_get_ref(ctx->connection, object);
++ctx->num_found;
return true; /* should_continue */
}
}
// No match, don't copy...
return true; /* should_continue */
}
//! Copies the reference for characteristic into the CopyRefsCtx->refs_out array
static bool prv_copy_characteristic_refs_cb(const GATTCharacteristic *characteristic,
void *cb_data) {
return prv_copy_refs_cb((const GATTObjectHeader *) characteristic, cb_data);
}
//! Copies the reference for characteristic into the CopyRefsCtx->refs_out array, only when
//! its Uuid is found in the matching_uuids array
static bool prv_copy_characteristic_refs_matching_cb(const GATTCharacteristic *characteristic,
void *cb_data) {
return prv_copy_refs_matching_cb((const GATTObjectHeader *) characteristic, cb_data);
}
//! Copies the reference for included service into the CopyRefsCtx->refs_out array
static bool prv_copy_included_service_refs_cb(const GATTServiceNode *inc_service,
void *cb_data) {
return prv_copy_refs_cb((const GATTObjectHeader *) inc_service, cb_data);
}
//! Copies object references associated with service_ref into refs_out.
//! @param callback This callback determines references for what objects need to be copied out
//! (characteristics, descriptors or included services)
static uint8_t prv_locked_copy_refs_with_service_ref(BLEService service_ref,
uintptr_t refs_out[],
uint8_t num_refs_out,
const Uuid *matching_uuids,
const GATTIterationCallbacks *callbacks) {
CopyRefsCtx ctx = {
.refs_out = refs_out,
.num_max = num_refs_out,
.matching_uuids = matching_uuids,
};
bt_lock();
{
const GATTServiceNode *service_node = prv_find_service_and_connection(service_ref,
&ctx.connection);
if (!service_node) {
goto unlock;
}
prv_iter_service_node(service_node, callbacks, &ctx);
}
unlock:
bt_unlock();
// Contains number of available objects because of final increment that happens in
// prv_copy_refs_cb
return ctx.num_found;
}
// -------------------------------------------------------------------------------------------------
uint8_t gatt_client_service_get_characteristics(BLEService service_ref,
BLECharacteristic characteristics[],
uint8_t num_characteristics) {
const GATTIterationCallbacks callbacks = {
.characteristic_iterator = prv_copy_characteristic_refs_cb,
};
return prv_locked_copy_refs_with_service_ref(service_ref, characteristics, num_characteristics,
NULL, &callbacks);
}
// -------------------------------------------------------------------------------------------------
uint8_t gatt_client_service_get_characteristics_matching_uuids(BLEService service_ref,
BLECharacteristic characteristics[],
const Uuid matching_characteristic_uuids[],
uint8_t num_characteristics) {
const GATTIterationCallbacks callbacks = {
.characteristic_iterator = prv_copy_characteristic_refs_matching_cb,
};
// Set all elements to BLE_CHARACTERISTIC_INVALID first:
memset(characteristics, 0, sizeof(characteristics[0]) * num_characteristics);
return prv_locked_copy_refs_with_service_ref(service_ref, characteristics, num_characteristics,
matching_characteristic_uuids, &callbacks);
}
// -------------------------------------------------------------------------------------------------
uint8_t gatt_client_service_get_included_services(BLEService service_ref,
BLEService services_out[],
uint8_t num_services_out) {
const GATTIterationCallbacks callbacks = {
.included_services_iterator = prv_copy_included_service_refs_cb,
};
return prv_locked_copy_refs_with_service_ref(service_ref, services_out, num_services_out,
NULL, &callbacks);
}
// -------------------------------------------------------------------------------------------------
Uuid gatt_client_service_get_uuid(BLEService service_ref) {
Uuid uuid;
bt_lock();
{
const GATTServiceNode *service_node = prv_find_service_and_connection(service_ref, NULL);
if (!service_node) {
uuid = UUID_INVALID;
goto unlock;
}
uuid = service_node->service->uuid;
}
unlock:
bt_unlock();
return uuid;
}
// -------------------------------------------------------------------------------------------------
BTDeviceInternal gatt_client_service_get_device(BLEService service_ref) {
BTDeviceInternal device;
bt_lock();
{
const GAPLEConnection *connection = NULL;
prv_find_service_and_connection(service_ref, &connection);
if (!connection) {
device = BT_DEVICE_INTERNAL_INVALID;
goto unlock;
}
device = connection->device;
}
unlock:
bt_unlock();
return device;
}
// -------------------------------------------------------------------------------------------------
Uuid gatt_client_characteristic_get_uuid(BLECharacteristic characteristic_ref) {
bt_lock();
const GATTCharacteristic * const characteristic = prv_find_characteristic(characteristic_ref,
NULL, NULL);
// MT: Working around compiler bug in gcc 4.7.2, when written using ?: it generates broken code
Uuid characteristic_uuid = UUID_INVALID;
if (characteristic) {
characteristic_uuid = characteristic->uuid;
}
bt_unlock();
return characteristic_uuid;
}
// -------------------------------------------------------------------------------------------------
BLEAttributeProperty gatt_client_characteristic_get_properties(
BLECharacteristic characteristic_ref) {
bt_lock();
const GATTCharacteristic * const characteristic = prv_find_characteristic(characteristic_ref,
NULL, NULL);
const uint8_t properties = characteristic ? characteristic->properties : 0;
bt_unlock();
return properties;
}
// -------------------------------------------------------------------------------------------------
BLEService gatt_client_characteristic_get_service(BLECharacteristic characteristic_ref) {
bt_lock();
const GATTServiceNode *service_node = NULL;
const GAPLEConnection *connection;
prv_find_characteristic(characteristic_ref, &service_node, &connection);
const BLEService service_ref = service_node ?
prv_get_service_ref(connection, service_node) : BLE_SERVICE_INVALID;
bt_unlock();
return service_ref;
}
// -------------------------------------------------------------------------------------------------
BTDeviceInternal gatt_client_characteristic_get_device(BLECharacteristic characteristic_ref) {
bt_lock();
const GAPLEConnection *connection;
prv_find_characteristic(characteristic_ref, NULL, &connection);
const BTDeviceInternal device = connection ? connection->device : BT_DEVICE_INTERNAL_INVALID;
bt_unlock();
return device;
}
// -------------------------------------------------------------------------------------------------
// Used by and extern'd for ppogatt.c and dis.c
GAPLEConnection *gatt_client_characteristic_get_connection(BLECharacteristic characteristic_ref) {
bt_lock_assert_held(true);
GAPLEConnection *connection;
prv_find_characteristic(characteristic_ref, NULL, (const GAPLEConnection **) &connection);
return connection;
}
// -------------------------------------------------------------------------------------------------
uint8_t gatt_client_characteristic_get_descriptors(BLECharacteristic characteristic_ref,
BLEDescriptor descriptor_refs_out[],
uint8_t num_descriptors) {
uint8_t index = 0;
bt_lock();
const GAPLEConnection *connection;
const GATTCharacteristic *characteristic = prv_find_characteristic(characteristic_ref, NULL,
&connection);
if (characteristic) {
const GATTDescriptor *descriptor = characteristic->descriptors;
while (index < characteristic->num_descriptors) {
if (index < num_descriptors) {
descriptor_refs_out[index] = prv_get_descriptor_ref(connection, descriptor);
}
++descriptor;
++index;
}
}
bt_unlock();
return index;
}
void gatt_client_service_get_all_characteristics_and_descriptors(
GAPLEConnection *connection, GATTService *service,
BLECharacteristic *characteristic_hdls_out,
BLEDescriptor *descriptor_hdls_out) {
uint8_t curr_desc_idx = 0;
const GATTCharacteristic *characteristic = service->characteristics;
for (unsigned int c = 0; c < service->num_characteristics; c++) {
for (unsigned int d = 0; d < characteristic->num_descriptors; ++d) {
const GATTDescriptor *descriptor = &characteristic->descriptors[d];
descriptor_hdls_out[curr_desc_idx] =
prv_get_descriptor_ref(connection, descriptor);
curr_desc_idx++;
}
characteristic_hdls_out[c] = prv_get_characteristic_ref(connection, characteristic);
characteristic =
(const GATTCharacteristic *) &characteristic->descriptors[characteristic->num_descriptors];
}
}
// -------------------------------------------------------------------------------------------------
Uuid gatt_client_descriptor_get_uuid(BLEDescriptor descriptor_ref) {
bt_lock();
const GATTDescriptor *descriptor = prv_find_descriptor(descriptor_ref, NULL, NULL, NULL);
// MT: Working around compiler bug in gcc 4.7.2, when written using ?: it generates broken code
Uuid uuid = UUID_INVALID;
if (descriptor) {
uuid = descriptor->uuid;
}
bt_unlock();
return uuid;
}
// -------------------------------------------------------------------------------------------------
BLECharacteristic gatt_client_descriptor_get_characteristic_and_connection(
BLEDescriptor descriptor_ref,
GAPLEConnection **connection_out);
BLECharacteristic gatt_client_descriptor_get_characteristic(BLEDescriptor descriptor_ref) {
bt_lock();
const BLECharacteristic characteristic_ref =
gatt_client_descriptor_get_characteristic_and_connection(descriptor_ref, NULL);
bt_unlock();
return characteristic_ref;
}
// -------------------------------------------------------------------------------------------------
//! @note !!! To access the returned GAPLEConnection bt_lock MUST be held!!!
uint16_t gatt_client_characteristic_get_handle_and_connection(BLECharacteristic characteristic_ref,
GAPLEConnection **connection_out) {
GAPLEConnection *connection;
const GATTServiceNode *service_node;
const GATTCharacteristic *characteristic =
prv_find_characteristic(characteristic_ref, &service_node,
(const GAPLEConnection **) &connection);
if (!characteristic) {
return GATTHandleInvalid;
}
if (connection_out) {
*connection_out = connection;
}
return service_node->service->att_handle + characteristic->att_handle_offset;
}
static uint16_t prv_get_largest_att_handle_offset(const GATTService *service) {
uint16_t largest_offset_hdl = 0;
const GATTCharacteristic *characteristic = service->characteristics;
for (unsigned int c = 0; c < service->num_characteristics; ++c) {
if (characteristic->att_handle_offset > largest_offset_hdl) {
largest_offset_hdl = characteristic->att_handle_offset;
}
for (unsigned int d = 0; d < characteristic->num_descriptors; ++d) {
const GATTDescriptor *descriptor = &characteristic->descriptors[d];
if (descriptor->att_handle_offset > largest_offset_hdl) {
largest_offset_hdl = descriptor->att_handle_offset;
}
}
characteristic =
(const GATTCharacteristic *) &characteristic->descriptors[characteristic->num_descriptors];
}
return largest_offset_hdl;
}
bool gatt_client_service_get_handle_range(BLEService service_ref, ATTHandleRange *range) {
bool success = false;
bt_lock();
{
const GATTServiceNode *service_node = prv_find_service_and_connection(service_ref, NULL);
if (service_node == NULL) {
goto done;
}
uint16_t start_hdl = service_node->service->att_handle;
*range = (ATTHandleRange) {
.start = start_hdl,
.end = start_hdl + prv_get_largest_att_handle_offset(service_node->service),
};
}
success = true;
done:
bt_unlock();
return success;
}
// -------------------------------------------------------------------------------------------------
//! @note !!! To access the returned GAPLEConnection bt_lock MUST be held!!!
uint16_t gatt_client_descriptor_get_handle_and_connection(BLEDescriptor descriptor_ref,
GAPLEConnection **connection_out) {
GAPLEConnection *connection;
const GATTServiceNode *service_node;
const GATTDescriptor *descriptor = prv_find_descriptor(descriptor_ref, NULL, &service_node,
(const GAPLEConnection **) &connection);
if (!descriptor) {
return GATTHandleInvalid;
}
if (connection_out) {
*connection_out = connection;
}
return service_node->service->att_handle + descriptor->att_handle_offset;
}
// -------------------------------------------------------------------------------------------------
//! @note !!! To access the returned GAPLEConnection bt_lock MUST be held!!!
const GATTCharacteristic * gatt_client_find_characteristic(BLECharacteristic characteristic_ref,
const GATTServiceNode **service_node_out,
const GAPLEConnection **connection_out) {
return prv_find_characteristic(characteristic_ref, service_node_out, connection_out);
}
// -------------------------------------------------------------------------------------------------
//! Used by gatt_client_subscription.c
//! @note !!! To access the returned GAPLEConnection bt_lock MUST be held!!!
BLEDescriptor gatt_client_accessors_find_cccd_with_characteristic(
BLECharacteristic characteristic_ref,
uint8_t *characteristic_properties_out,
uint16_t *characteristic_att_handle_out,
GAPLEConnection **connection_out) {
const GAPLEConnection *connection;
const GATTServiceNode *service_node;
const GATTCharacteristic *characteristic = gatt_client_find_characteristic(characteristic_ref,
&service_node,
&connection);
if (characteristic) {
*characteristic_properties_out = characteristic->properties;
const Uuid cccd_uuid = bt_uuid_expand_16bit(0x2902);
for (unsigned int d = 0; d < characteristic->num_descriptors; ++d) {
const GATTDescriptor *descriptor = &characteristic->descriptors[d];
if (uuid_equal(&descriptor->uuid, &cccd_uuid)) {
*connection_out = (GAPLEConnection *) connection;
*characteristic_att_handle_out = characteristic->att_handle_offset +
service_node->service->att_handle;
return prv_get_descriptor_ref(connection, descriptor);
}
}
}
*connection_out = NULL;
*characteristic_att_handle_out = 0;
return BLE_DESCRIPTOR_INVALID;
}
// -------------------------------------------------------------------------------------------------
//! Used by gatt_client_subscription.c
//! @note !!! To access the returned GAPLEConnection bt_lock MUST be held!!!
BLECharacteristic gatt_client_descriptor_get_characteristic_and_connection(
BLEDescriptor descriptor_ref,
GAPLEConnection **connection_out) {
const GATTCharacteristic *characteristic;
GAPLEConnection *connection;
const GATTDescriptor *descriptor = prv_find_descriptor(descriptor_ref, &characteristic, NULL,
(const GAPLEConnection **) &connection);
const BLECharacteristic characteristic_ref = descriptor ?
prv_get_characteristic_ref(connection, characteristic) : BLE_CHARACTERISTIC_INVALID;
if (connection_out) {
*connection_out = connection;
}
return characteristic_ref;
}
BLEService gatt_client_att_handle_get_service(
GAPLEConnection *connection, uint16_t att_handle, const GATTServiceNode **service_node_out) {
const GATTServiceNode *node =
prv_find_service_node_by_att_handle(connection->gatt_remote_services, att_handle);
*service_node_out = node;
return node ? prv_get_service_ref(connection, node) : BLE_SERVICE_INVALID;
}

View file

@ -0,0 +1,114 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <bluetooth/bluetooth_types.h>
#include <bluetooth/gatt_service_types.h>
//! @file This file contains functions to access any discovered GATT Services,
//! Characteristics and Descriptors. The data structures are used internally
//! in gatt_client_accessors.c and gatt_client_discovery.c.
typedef struct GATTServiceNode {
ListNode node;
GATTService *service;
} GATTServiceNode;
#define GATTHandleInvalid ((uint16_t) 0)
//! Copies the BLEService references for the gatt_remote_services associated
//! with the device.
//! @see prv_handle_service_change in ble_client.c
uint8_t gatt_client_copy_service_refs(const BTDeviceInternal *device,
BLEService services_out[],
uint8_t num_services);
// TODO: add public API to applib
//! Copies the BLEService references for the gatt_remote_services associated
//! with the device, that match a given Service UUID.
//! @note It is possible to have multiple service instances with the same Service UUID.
uint8_t gatt_client_copy_service_refs_matching_uuid(const BTDeviceInternal *device,
BLEService services_out[],
uint8_t num_services,
const Uuid *matching_service_uuid);
//! Copies the BLECharacteristic references associated with the service.
//! @see ble_service_get_characteristics
uint8_t gatt_client_service_get_characteristics(BLEService service_ref,
BLECharacteristic characteristics[],
uint8_t num_characteristics);
// TODO: add public API to applib
//! Copies BLECharacteristic references associated with the service, filtered by an array of
//! Characteristic UUIDs.
//! @param characteristics_out The array into which the matching BLECharacteristic references
//! will be copied.
//! @param matching_characteristic_uuids The array of Characteristic Uuid`s that will be used to
//! determine what references to copy. For every matching characteristic, the reference will be
//! copied into the `characteristics_out` array, at the same index as the Uuid
//! in the `matching_characteristic_uuids` array. The array must contain each Uuid only once.
//! The behavior is undefined when the array contains the same Uuid multiple times.
//! @param num_characteristics The length of both the characteristics_out and
//! matching_characteristic_uuids arrays.
//! @return The number of references that were copied.
//! @note If a characteristic was not found, the element will be set to BLE_CHARACTERISTIC_INVALID.
//! If there were multiple characteristics with the same Uuid, the first one to be found will be
//! copied.
//! @see ble_service_get_characteristics
uint8_t gatt_client_service_get_characteristics_matching_uuids(BLEService service_ref,
BLECharacteristic characteristics_out[],
const Uuid matching_characteristic_uuids[],
uint8_t num_characteristics);
//! Gets the Service UUID associated with the service
//! @see ble_service_get_uuid
Uuid gatt_client_service_get_uuid(BLEService service_ref);
//! Gets the device associated with the service
//! @see ble_service_get_device
BTDeviceInternal gatt_client_service_get_device(BLEService service_ref);
//! Gets the included services associated with the service
//! @see ble_service_get_included_services
uint8_t gatt_client_service_get_included_services(BLEService service_ref,
BLEService services_out[],
uint8_t num_services_out);
//! Gets the UUID of the characteristic
//! @see ble_characteristic_get_uuid
Uuid gatt_client_characteristic_get_uuid(BLECharacteristic characteristic);
//! @see ble_characteristic_get_properties
BLEAttributeProperty gatt_client_characteristic_get_properties(BLECharacteristic characteristic);
//! @see ble_characteristic_get_service
BLEService gatt_client_characteristic_get_service(BLECharacteristic characteristic);
//! @see ble_characteristic_get_device
BTDeviceInternal gatt_client_characteristic_get_device(BLECharacteristic characteristic);
//! @see ble_characteristic_get_descriptors
uint8_t gatt_client_characteristic_get_descriptors(BLECharacteristic characteristic,
BLEDescriptor descriptors_out[],
uint8_t num_descriptors);
//! @see ble_descriptor_get_uuid
Uuid gatt_client_descriptor_get_uuid(BLEDescriptor descriptor);
//! @see ble_descriptor_get_characteristic
BLECharacteristic gatt_client_descriptor_get_characteristic(BLEDescriptor descriptor);
bool gatt_client_service_get_handle_range(BLEService service_ref, ATTHandleRange *range);

View file

@ -0,0 +1,481 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "gatt_client_discovery.h"
#include "gatt_service_changed.h"
#include "gap_le_connection.h"
#include "ble_log.h"
#include "comm/bt_lock.h"
#include "comm/bt_conn_mgr.h"
#include "kernel/core_dump.h"
#include "kernel/events.h"
#include "kernel/pbl_malloc.h"
#include "gatt_client_accessors.h"
#include "system/logging.h"
#include <bluetooth/gatt.h>
#include <bluetooth/gatt_discovery.h>
#include <btutil/bt_device.h>
// TODO: virtualize the gatt_client_discovery_discover_all() call
//! Defined in gatt_client_subscriptions.c. Should only be called when receiving
//! notification of a service change
extern void gatt_client_subscription_cleanup_by_att_handle_range(
struct GAPLEConnection *connection, ATTHandleRange *range);
//! Defined in gatt_client_accessors.c. Should only be needed in this module
extern BLEService gatt_client_att_handle_get_service(
GAPLEConnection *connection, uint16_t att_handle, GATTServiceNode **service_node_out);
// -------------------------------------------------------------------------------------------------
// Static function prototypes
static BTErrno prv_run_next_job(GAPLEConnection *connection);
// -------------------------------------------------------------------------------------------------
// Wrappers around Bluetopia's API
#define MIN_ATT_HANDLE 0x1
#define MAX_ATT_HANDLE 0xFFFF
typedef struct DiscoveryJobQueue {
ListNode node;
ATTHandleRange hdl;
} DiscoveryJobQueue;
// Assumes we are holding the BT lock
static void prv_add_discovery_job(
GAPLEConnection *connection, ATTHandleRange *hdl_range) {
DiscoveryJobQueue *node = kernel_zalloc_check(sizeof(DiscoveryJobQueue));
if (hdl_range) {
node->hdl = *hdl_range;
} else { // discover everything
node->hdl = (ATTHandleRange) {
.start = MIN_ATT_HANDLE,
.end = MAX_ATT_HANDLE
};
}
if (!connection->discovery_jobs) {
list_init(&node->node);
connection->discovery_jobs = node;
} else {
list_append((ListNode *)connection->discovery_jobs, (ListNode *)node);
}
}
void gatt_client_discovery_discover_range(GAPLEConnection *connection, ATTHandleRange *hdl_range) {
bt_lock();
{
prv_add_discovery_job(connection, hdl_range);
if (!connection->gatt_is_service_discovery_in_progress) {
prv_run_next_job(connection);
}
}
bt_unlock();
}
// assumes bt lock is held
static BTErrno prv_run_next_job(GAPLEConnection *connection) {
DiscoveryJobQueue *node = connection->discovery_jobs;
if (!node) {
return BTErrnoOK; // no more jobs to run
}
// Note, that the job only gets removed from the list after discovery
// has finished or error'ed out. That way the watchdog retry mechanism
// can simply call this routine again to kick off another discovery attempt
PBL_LOG(LOG_LEVEL_INFO, "Starting BLE Service Discovery: 0x%x to 0x%x",
node->hdl.start, node->hdl.end);
ATTHandleRange hdl = {
.start = node->hdl.start,
.end = node->hdl.end
};
BTErrno rv = bt_driver_gatt_start_discovery_range(connection, &hdl);
if (rv == BTErrnoOK) {
// if we are back here because a timeout occurred, let the
// driver handle resetting the watchdog timer (cc2564x issue)
connection->gatt_is_service_discovery_in_progress = true;
}
return rv;
}
// This function returns true if a retry started. If a retry did not start
// it sets e to BTErrnoOK if discovery completed or the actual error that happened
// which should be forwarded on
static bool prv_discovery_handle_timeout(GAPLEConnection *connection, BTErrno *e) {
bool retry_started = false;
BTErrno finalize_result = BTErrnoOK;
// Executing on NewTimer task, so need to bt_lock():
PBL_LOG(LOG_LEVEL_WARNING, "Service Discovery Watchdog Timeout");
bt_lock();
{
if (!gap_le_connection_is_valid(connection)) {
goto unlock;
}
if (bt_driver_gatt_stop_discovery(connection) != BTErrnoOK) {
// Handle the race: Bluetopia service discovery has stopped in the mean time, for example
// because of a disconnection, internal error or it completed right when the timer fired.
goto unlock;
}
if (connection->gatt_service_discovery_retries == GATT_CLIENT_DISCOVERY_MAX_RETRY) {
#if !RELEASE && !UNITTEST
core_dump_reset(true /* is_forced */);
#endif
// Done retrying, just error out:
finalize_result = BTErrnoServiceDiscoveryTimeout;
goto unlock;
}
// Retry transparently (don't let the clients know):
BTErrno ret_val = prv_run_next_job(connection);
if (ret_val != BTErrnoOK) {
// Start failed, just error out
finalize_result = ret_val;
goto unlock;
}
++connection->gatt_service_discovery_retries;
retry_started = true;
}
unlock:
*e = finalize_result;
bt_unlock();
return retry_started;
}
// -------------------------------------------------------------------------------------------------
extern uint8_t gatt_client_copy_service_refs_by_discovery_generation(
const BTDeviceInternal *device, BLEService services_out[],
uint8_t num_services, uint8_t discovery_gen);
static void prv_send_event(PebbleBLEGATTClientServiceEventInfo *info) {
PebbleEvent e = (const PebbleEvent) {
.type = PEBBLE_BLE_GATT_CLIENT_EVENT,
.task_mask = 0,
.bluetooth = {
.le = {
.gatt_client_service = {
.info = info,
.subtype = PebbleBLEGATTClientEventTypeServiceChange,
},
},
},
};
// TODO: send only to tasks that are connected virtually
event_put(&e);
}
static void prv_send_services_added_event(
const GAPLEConnection *connection, BTErrno status) {
uint8_t num_services_changed = (status == BTErrnoOK) ?
list_count(&connection->gatt_remote_services->node) : 0;
if (num_services_changed > BLE_GATT_MAX_SERVICES_CHANGED) {
PBL_LOG(LOG_LEVEL_ERROR, "Remote has %u services, more than we can handle.",
num_services_changed);
num_services_changed = BLE_GATT_MAX_SERVICES_CHANGED;
}
size_t space_needed = num_services_changed * sizeof(PebbleBLEGATTClientServiceHandles)
+ sizeof(PebbleBLEGATTClientServiceEventInfo);
PebbleBLEGATTClientServiceEventInfo *info = kernel_zalloc_check(space_needed);
*info = (PebbleBLEGATTClientServiceEventInfo) {
.type = PebbleServicesAdded,
.device = connection->device,
.status = status
};
info->services_added_data.num_services_added =
gatt_client_copy_service_refs_by_discovery_generation(
&connection->device, &info->services_added_data.services[0],
BLE_GATT_MAX_SERVICES_CHANGED, connection->gatt_service_discovery_generation);
prv_send_event(info);
}
static void prv_send_services_invalidate_all_event(
const GAPLEConnection *connection, BTErrno status) {
PebbleBLEGATTClientServiceEventInfo *info =
kernel_zalloc_check(sizeof(PebbleBLEGATTClientServiceEventInfo));
*info = (PebbleBLEGATTClientServiceEventInfo) {
.type = PebbleServicesInvalidateAll,
.device = connection->device,
.status = status
};
prv_send_event(info);
}
extern void gatt_client_service_get_all_characteristics_and_descriptors(
GAPLEConnection *connection, GATTService *service,
BLECharacteristic *characteristics_hdls_out,
BLEDescriptor *descriptor_hdls_out);
//! @note bt_lock is assumed to be taken by the caller
void gatt_client_discovery_handle_service_range_change(
GAPLEConnection *connection, ATTHandleRange *range) {
GATTServiceNode *service_node;
BLEService service = gatt_client_att_handle_get_service(connection, range->start, &service_node);
if (service == BLE_SERVICE_INVALID) {
// Must be a new service
return;
}
int memory_needed = service_node->service->num_characteristics * sizeof(BLECharacteristic) +
service_node->service->num_descriptors * sizeof(BLEDescriptor);
memory_needed +=
sizeof(PebbleBLEGATTClientServiceEventInfo) + sizeof(PebbleBLEGATTClientServiceHandles);
PebbleBLEGATTClientServiceEventInfo *info = kernel_zalloc_check(memory_needed);
*info = (PebbleBLEGATTClientServiceEventInfo) {
.type = PebbleServicesRemoved,
.device = connection->device,
.status = BTErrnoOK
};
info->services_removed_data.num_services_removed = 1;
PebbleBLEGATTClientServiceHandles *remove_hdl = &info->services_removed_data.handles[0];
remove_hdl->service = service;
remove_hdl->uuid = service_node->service->uuid;
remove_hdl->num_characteristics = service_node->service->num_characteristics;
remove_hdl->num_descriptors = service_node->service->num_descriptors;
gatt_client_service_get_all_characteristics_and_descriptors(
connection, service_node->service,
&remove_hdl->char_and_desc_handles[0],
&remove_hdl->char_and_desc_handles[service_node->service->num_characteristics]);
// a service has been removed/updated
gatt_client_subscription_cleanup_by_att_handle_range(connection, range);
ListNode **head = (ListNode **) &connection->gatt_remote_services;
list_remove((ListNode *)service_node, head, NULL);
kernel_free(service_node->service);
service_node->service = NULL;
kernel_free(service_node);
prv_send_event(info);
}
static void prv_free_service_nodes(GAPLEConnection *connection) {
GATTServiceNode *node = connection->gatt_remote_services;
while (node) {
GATTServiceNode *next = (GATTServiceNode *) node->node.next;
kernel_free(node->service);
node->service = NULL;
kernel_free(node);
node = next;
}
connection->gatt_remote_services = NULL;
}
static void prv_remove_current_discovery_job(GAPLEConnection *connection) {
DiscoveryJobQueue *node = connection->discovery_jobs;
if (!node) {
return;
}
list_remove((ListNode *)connection->discovery_jobs,
(ListNode **)&connection->discovery_jobs, NULL);
kernel_free(node);
// Handle the case where we are have received service change indication
// messages for the same range in quick succession and have multiple jobs
// scheduled as a result. This shouldn't be a frequent occurrence but see
// PBL-24741 as an example
DiscoveryJobQueue *new_job = connection->discovery_jobs;
if (!new_job) {
return; // nothing to do
}
if ((new_job->hdl.start == MIN_ATT_HANDLE) && (new_job->hdl.end == MAX_ATT_HANDLE)) {
// we are rediscovering all services so flush everything
prv_free_service_nodes(connection);
prv_send_services_invalidate_all_event(
connection, BTErrnoServiceDiscoveryDatabaseChanged);
} else { // we are rediscovering one service
gatt_client_discovery_handle_service_range_change(connection, &new_job->hdl);
}
}
void gatt_client_cleanup_discovery_jobs(GAPLEConnection *connection) {
bt_lock();
{
while (connection->discovery_jobs != NULL) {
prv_remove_current_discovery_job(connection);
}
}
bt_unlock();
}
static void prv_finalize_discovery(GAPLEConnection *connection, BTErrno errno) {
if (errno != BTErrnoOK) {
// Handle failure -- cleanup and dispatch event:
prv_free_service_nodes(connection);
gatt_client_subscriptions_cleanup_by_connection(connection, false /* should_unsubscribe */);
}
prv_remove_current_discovery_job(connection);
connection->gatt_is_service_discovery_in_progress = false;
connection->gatt_service_discovery_retries = 0;
if (errno == BTErrnoServiceDiscoveryDatabaseChanged) {
prv_send_services_invalidate_all_event(connection, errno);
} else {
prv_send_services_added_event(connection, errno);
}
++connection->gatt_service_discovery_generation;
prv_run_next_job(connection);
}
void bt_driver_cb_gatt_client_discovery_handle_indication(
GAPLEConnection *connection, GATTService *service, BTErrno error) {
// We experienced some kind of conversion error, pass it on
if (error != BTErrnoOK) {
prv_send_services_added_event(connection, error);
return;
}
GATTServiceNode *node = kernel_zalloc_check(sizeof(GATTServiceNode));
node->service = service;
// tag the service with the generation it was discovered as a part of
node->service->discovery_generation = connection->gatt_service_discovery_generation;
bt_lock();
{
ListNode **head = (ListNode **) &connection->gatt_remote_services;
if (*head) {
list_append(*head, &node->node);
} else {
*head = &node->node;
}
}
bt_unlock();
}
bool bt_driver_cb_gatt_client_discovery_complete(GAPLEConnection *connection, BTErrno errno) {
bool finalize_discovery = true;
bt_lock();
{
if (errno == BTErrnoServiceDiscoveryTimeout) {
if (prv_discovery_handle_timeout(connection, &errno)) {
// if a retry started, don't generate any events yet
finalize_discovery = false;
goto unlock;
}
// it's possible the discovery completed before we handled the timeout, in which case
// we get a BTErrnoOK which means we will get a completion event already
finalize_discovery = (errno != BTErrnoOK);
}
// Completion of service discovery implies we are about to have more BLE
// traffic (for example, ANCS notifications, PPoG communication). Keep the
// channel at a high throughput speed for a little bit longer to handle these bursts.
conn_mgr_set_ble_conn_response_time(connection, BtConsumerLeServiceDiscovery,
ResponseTimeMin, 10);
if (finalize_discovery) {
prv_finalize_discovery(connection, errno);
}
}
unlock:
bt_unlock();
return finalize_discovery;
}
BTErrno gatt_client_discovery_discover_all(const BTDeviceInternal *device) {
BTErrno ret_val = BTErrnoOK;
bt_lock();
{
GAPLEConnection *connection = gap_le_connection_by_device(device);
if (!connection) {
ret_val = BTErrnoInvalidParameter;
goto unlock;
}
if (connection->gatt_is_service_discovery_in_progress) {
ret_val = BTErrnoInvalidState;
goto unlock;
}
if (connection->gatt_remote_services) {
// Already discovered, no need to do it again!
prv_send_services_added_event(connection, BTErrnoOK);
goto unlock;
}
conn_mgr_set_ble_conn_response_time(connection, BtConsumerLeServiceDiscovery,
ResponseTimeMin, 30);
prv_add_discovery_job(connection, NULL);
// if we get here there is no discovery in progress so dispatch the job
ret_val = prv_run_next_job(connection);
}
unlock:
bt_unlock();
return ret_val;
}
//! extern for gap_le_connnection.c
//! Cleans up any state and frees the associated memory of all the things this module might have
//! created for a given connection.
//! bt_lock() is assumed to be taken by the caller
void gatt_client_discovery_cleanup_by_connection(GAPLEConnection *connection, BTErrno reason) {
if (connection->gatt_is_service_discovery_in_progress) {
// Assuming "disconnection" reason is appropriate here:
prv_finalize_discovery(connection, reason);
bt_driver_gatt_handle_discovery_abandoned();
} else {
prv_free_service_nodes(connection);
}
}
//! extern for gatt_service_changed.c
//! Same as gatt_client_discovery_discover_all, but cleans up existing service discovery
//! state and stops any existing service discovery process.
BTErrno gatt_client_discovery_rediscover_all(const BTDeviceInternal *device) {
BTErrno ret_val = BTErrnoServiceDiscoveryDisconnected;
bt_lock();
{
GAPLEConnection *connection = gap_le_connection_by_device(device);
if (connection) {
if (connection->gatt_is_service_discovery_in_progress) {
// Remove any partial jobs which may be pending
// since we are going to rediscover everything
gatt_client_cleanup_discovery_jobs(connection);
bt_driver_gatt_stop_discovery(connection);
} else {
// Queue up CCCD writes to unsubscribe all the subscriptions:
gatt_client_subscriptions_cleanup_by_connection(connection, true /* should_unsubscribe */);
}
prv_finalize_discovery(connection, BTErrnoServiceDiscoveryDatabaseChanged);
ret_val = gatt_client_discovery_discover_all(device);
}
}
bt_unlock();
return ret_val;
}

View file

@ -0,0 +1,31 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <bluetooth/bluetooth_types.h>
#define GATT_CLIENT_DISCOVERY_MAX_RETRY_BITS (2)
#define GATT_CLIENT_DISCOVERY_MAX_RETRY ((1 << GATT_CLIENT_DISCOVERY_MAX_RETRY_BITS) - 1)
//! Starts discovery of all GATT services, characteristics and descriptors.
//! @param device The device of which its services, characteristics and
//! descriptors need to be discovered.
//! @return BTErrnoOK If the discovery process was started successfully,
//! BTErrnoInvalidParameter if the device was not connected,
//! BTErrnoInvalidState if service discovery was already on-going, or
//! an internal error otherwise (>= BTErrnoInternalErrorBegin).
BTErrno gatt_client_discovery_discover_all(const BTDeviceInternal *device);

View file

@ -0,0 +1,363 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <bluetooth/gatt.h>
#include "gatt_client_operations.h"
#include "gatt_client_accessors.h"
#include "comm/bt_lock.h"
#include "kernel/events.h"
#include "kernel/pbl_malloc.h"
#include "util/list.h"
#include "system/passert.h"
// -------------------------------------------------------------------------------------------------
//! @see gatt_client_accessors.c / gatt_client_subscriptions.c
// These calls require the caller to own the bt_lock while calling the
// function and for as long as the result is being used / accessed.
extern uint16_t gatt_client_characteristic_get_handle_and_connection(
BLECharacteristic characteristic_ref,
GAPLEConnection **connection_out);
extern uint16_t gatt_client_descriptor_get_handle_and_connection(BLEDescriptor descriptor_ref,
GAPLEConnection **connection_out);
extern void gatt_client_subscriptions_handle_write_cccd_response(BLEDescriptor cccd,
BLEGATTError error);
// -------------------------------------------------------------------------------------------------
typedef struct {
ListNode node;
//! This is redundant, PebbleEvent already has this info, but added as integrity check.
uintptr_t object_ref;
uint16_t length;
uint8_t value[];
} ReadResponseData;
typedef struct GattClientEventContext {
ListNode node;
PebbleBLEGATTClientEventType subtype;
GAPLEClient client;
uintptr_t obj_ref;
} GattClientEventContext;
static ReadResponseData *s_read_responses[GAPLEClientNum];
//! Keeps track of the current outstanding GattClientOperations (reads/writes). Useful for freeing
//! the outstanding op's memory when a connection dies in the middle.
static GattClientEventContext *s_client_event_ctxs[GAPLEClientNum];
static void prv_send_event(PebbleBLEGATTClientEventType subtype, GAPLEClient client,
uintptr_t object_ref, uint16_t value_length, BLEGATTError gatt_error) {
PebbleEvent e = {
.type = PEBBLE_BLE_GATT_CLIENT_EVENT,
.task_mask = ~(gap_le_pebble_task_bit_for_client(client)),
.bluetooth = {
.le = {
.gatt_client = {
.subtype = subtype,
.object_ref = object_ref,
.gatt_error = gatt_error,
.value_length = value_length,
},
},
},
};
event_put(&e);
}
static void prv_internal_write_cccd_response_cb(GattClientOpResponseHdr *event) {
const GattClientEventContext *data = event->context;
const BLEDescriptor cccd = data->obj_ref;
const BLEGATTError error = event->error_code;
gatt_client_subscriptions_handle_write_cccd_response(cccd, error);
}
static BLEGATTError prv_handle_response(const GattClientOpReadReponse *resp,
const GattClientEventContext *data,
uint16_t *gatt_value_length) {
uint16_t val_len = resp->value_length;
if (val_len) {
// Only create ReadResponseData node if length is not 0
ReadResponseData *read_response = kernel_malloc(sizeof(ReadResponseData) + val_len);
if (!read_response) {
*gatt_value_length = 0;
return BLEGATTErrorLocalInsufficientResources;
}
*read_response = (const ReadResponseData) {
.object_ref = data->obj_ref,
.length = val_len,
};
memcpy(read_response->value, resp->value, val_len);
if (s_read_responses[data->client]) {
list_append(&s_read_responses[data->client]->node, &read_response->node);
} else {
s_read_responses[data->client] = read_response;
}
}
*gatt_value_length = val_len;
return BLEGATTErrorSuccess;
}
static bool prv_ctx_in_client_event_ctxs(GattClientEventContext *context) {
const bool exists =
(list_contains(&s_client_event_ctxs[GAPLEClientApp]->node, &context->node) ||
list_contains(&s_client_event_ctxs[GAPLEClientKernel]->node, &context->node));
return exists;
}
void bt_driver_cb_gatt_client_operations_handle_response(GattClientOpResponseHdr *event) {
const GattClientEventContext *data = event->context;
bt_lock();
{
//! Special case: writes to the "Client Characteristic Configuration Descriptor" are handled by
//! the gatt_client_subscriptions.c module.
if (data->client == GAPLEClientKernel
&& data->subtype == PebbleBLEGATTClientEventTypeCharacteristicSubscribe) {
prv_internal_write_cccd_response_cb(event);
goto cleanup;
}
//! There is a time when we have disconnected, but there are still outstanding responses
//! coming back to the MCU (e.g. HcProtocol). When we disconnect, we call gatt_client_op_cleanup
//! which cleans up all of the memory for the nodes in the lists in `s_client_event_ctxs`.
//! Here, we check if the context related to the response coming in has already been cleaned up,
//! and if it has, we instantly unlock and continue on.
if (!prv_ctx_in_client_event_ctxs(event->context)) {
goto unlock;
}
// Default values
uint16_t gatt_value_length = 0;
uint16_t gatt_err_code = BLEGATTErrorSuccess;
if (event->error_code != BLEGATTErrorSuccess) {
gatt_err_code = event->error_code;
} else {
switch (event->type) {
case GattClientOpResponseRead: {
const GattClientOpReadReponse *resp = (GattClientOpReadReponse *)event;
PBL_ASSERTN(data->subtype == PebbleBLEGATTClientEventTypeCharacteristicRead ||
data->subtype == PebbleBLEGATTClientEventTypeDescriptorRead);
gatt_err_code = prv_handle_response(resp, data, &gatt_value_length);
break;
}
case GattClientOpResponseWrite: {
PBL_ASSERTN(data->subtype == PebbleBLEGATTClientEventTypeCharacteristicWrite ||
data->subtype == PebbleBLEGATTClientEventTypeDescriptorWrite);
break;
}
default:
WTF;
}
}
prv_send_event(data->subtype, data->client, data->obj_ref, gatt_value_length, gatt_err_code);
}
cleanup:
list_remove(event->context, (ListNode **)&s_client_event_ctxs[data->client], NULL);
kernel_free(event->context);
unlock:
bt_unlock();
}
typedef uint16_t (*HandleAndConnectionGetter)(uintptr_t obj_ref,
GAPLEConnection **connection_out);
static GattClientEventContext *prv_create_event_context(GAPLEClient client) {
GattClientEventContext *evt_ctx = kernel_zalloc(sizeof(GattClientEventContext));
if (evt_ctx) {
s_client_event_ctxs[client] =
(GattClientEventContext *)list_prepend(&s_client_event_ctxs[client]->node, &evt_ctx->node);
}
return evt_ctx;
}
static BTErrno prv_read(uintptr_t obj_ref, GAPLEClient client,
HandleAndConnectionGetter handle_getter,
PebbleBLEGATTClientEventType subtype) {
BTErrno ret_val = BTErrnoOK;
bt_lock();
{
GAPLEConnection *connection;
const uint16_t att_handle = handle_getter(obj_ref, &connection);
if (!att_handle) {
ret_val = BTErrnoInvalidParameter;
goto unlock;
}
GattClientEventContext *data = prv_create_event_context(client);
if (!data) {
ret_val = BTErrnoNotEnoughResources;
goto unlock;
}
// Zero'd out and added to list in `prv_create_event_context`
data->client = client;
data->subtype = subtype;
data->obj_ref = obj_ref;
ret_val = bt_driver_gatt_read(connection, att_handle, data);
}
unlock:
bt_unlock();
return ret_val;
}
static BTErrno prv_write(uintptr_t obj_ref, const uint8_t *value, size_t value_length,
GAPLEClient client, HandleAndConnectionGetter handle_getter,
PebbleBLEGATTClientEventType subtype) {
BTErrno ret_val = BTErrnoOK;
bt_lock();
{
GAPLEConnection *connection;
const uint16_t att_handle = handle_getter(obj_ref, &connection);
if (!att_handle) {
ret_val = BTErrnoInvalidParameter;
goto unlock;
}
GattClientEventContext *data = prv_create_event_context(client);
if (!data) {
ret_val = BTErrnoNotEnoughResources;
goto unlock;
}
// Zero'd out and added to list in `prv_create_event_context`
data->client = client;
data->subtype = subtype;
data->obj_ref = obj_ref;
ret_val = bt_driver_gatt_write(connection, value, value_length, att_handle, data);
}
unlock:
bt_unlock();
return ret_val;
}
BTErrno gatt_client_op_read(BLECharacteristic characteristic,
GAPLEClient client) {
return prv_read(characteristic, client,
gatt_client_characteristic_get_handle_and_connection,
PebbleBLEGATTClientEventTypeCharacteristicRead);
}
void gatt_client_consume_read_response(uintptr_t object_ref,
uint8_t value_out[],
uint16_t value_length,
GAPLEClient client) {
bt_lock();
{
// For responses with 0 length, no ReadResponseData is created therefore
// should not be attempted to be consumed.
PBL_ASSERTN(value_length);
PBL_ASSERTN(s_read_responses[client]);
ReadResponseData *read_response = s_read_responses[client];
PBL_ASSERTN(value_length == read_response->length);
PBL_ASSERTN(object_ref == read_response->object_ref);
if (value_out) {
memcpy(value_out, read_response->value, read_response->length);
}
list_remove(&read_response->node, (ListNode **) &s_read_responses[client], NULL);
kernel_free(read_response);
}
bt_unlock();
}
BTErrno gatt_client_op_write(BLECharacteristic characteristic,
const uint8_t *value,
size_t value_length,
GAPLEClient client) {
return prv_write(characteristic, value, value_length, client,
gatt_client_characteristic_get_handle_and_connection,
PebbleBLEGATTClientEventTypeCharacteristicWrite);
}
BTErrno gatt_client_op_write_without_response(BLECharacteristic characteristic,
const uint8_t *value,
size_t value_length,
GAPLEClient client) {
BTErrno ret_val = BTErrnoOK;
bt_lock();
{
GAPLEConnection *connection;
const uint16_t att_handle =
gatt_client_characteristic_get_handle_and_connection(characteristic, &connection);
if (!att_handle) {
ret_val = BTErrnoInvalidParameter;
goto unlock;
}
ret_val = bt_driver_gatt_write_without_response(connection, value, value_length, att_handle);
}
unlock:
bt_unlock();
return ret_val;
}
BTErrno gatt_client_op_write_descriptor(BLEDescriptor descriptor,
const uint8_t *value,
size_t value_length,
GAPLEClient client) {
return prv_write(descriptor, value, value_length, client,
gatt_client_descriptor_get_handle_and_connection,
PebbleBLEGATTClientEventTypeDescriptorWrite);
}
BTErrno gatt_client_op_read_descriptor(BLEDescriptor descriptor,
GAPLEClient client) {
return prv_read(descriptor, client,
gatt_client_descriptor_get_handle_and_connection,
PebbleBLEGATTClientEventTypeDescriptorRead);
}
BTErrno gatt_client_op_write_descriptor_cccd(BLEDescriptor cccd, const uint16_t *value) {
return prv_write(cccd, (const uint8_t *) value, sizeof(*value), GAPLEClientKernel,
gatt_client_descriptor_get_handle_and_connection,
PebbleBLEGATTClientEventTypeCharacteristicSubscribe);
}
static bool prv_deinit_ctx_list(ListNode *node, void *unused) {
kernel_free(node);
return true;
}
void gatt_client_op_cleanup(GAPLEClient client) {
bt_lock();
{
// Free all memory associated with outstanding operations
list_foreach(&s_client_event_ctxs[client]->node, prv_deinit_ctx_list, NULL);
s_client_event_ctxs[client] = NULL;
ReadResponseData *read_response = s_read_responses[client];
while (read_response) {
ReadResponseData *next_read_response = (ReadResponseData *) read_response->node.next;
kernel_free(read_response);
read_response = next_read_response;
}
s_read_responses[client] = NULL;
}
bt_unlock();
}

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
//! @file This file contains adapter code between Bluetopia's GATT APIs and
//! Pebble's GATT/API code. The functions in this file take the the internal
//! reference types BLECharacteristic and BLEDescriptor to perform operations
//! upon those remote resources. The implementation uses the functions
//! gatt_client_characteristic_get_handle_and_connection_id and
//! gatt_client_descriptor_get_handle_and_connection_id from
//! gatt_client_accessors.c to look up the Bluetopia ConnectionID and the
//! ATT handles. These pieces of information is what Bluetopia cares about
//! when asked to perform a GATT operation.
#include <bluetooth/bluetooth_types.h>
#include "gap_le_task.h"
#define GATT_MTU_MINIMUM (23)
BTErrno gatt_client_op_read(BLECharacteristic characteristic,
GAPLEClient client);
void gatt_client_consume_read_response(uintptr_t object_ref,
uint8_t value_out[],
uint16_t value_length,
GAPLEClient client);
BTErrno gatt_client_op_write(BLECharacteristic characteristic,
const uint8_t *value,
size_t value_length,
GAPLEClient client);
BTErrno gatt_client_op_write_without_response(BLECharacteristic characteristic,
const uint8_t *value,
size_t value_length,
GAPLEClient client);
BTErrno gatt_client_op_write_descriptor(BLEDescriptor descriptor,
const uint8_t *value,
size_t value_length,
GAPLEClient client);
BTErrno gatt_client_op_read_descriptor(BLEDescriptor descriptor,
GAPLEClient client);
void gatt_client_op_cleanup(GAPLEClient client);

View file

@ -0,0 +1,828 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "gatt_client_subscriptions.h"
#include "gatt_client_accessors.h"
#include "gatt_client_operations.h"
#include "gatt_service_changed.h"
#include <bluetooth/gatt.h>
#include "gap_le_connection.h"
#include "comm/bt_lock.h"
#include "drivers/rtc.h"
#include "kernel/events.h"
#include "kernel/pbl_malloc.h"
#include "services/common/analytics/analytics.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/circular_buffer.h"
#include "util/likely.h"
#include <os/mutex.h>
#include <os/tick.h>
#include "FreeRTOS.h"
#include "semphr.h"
//! Time to wait/block for when the buffer is full and needs to be drained by the client.
//! Note that bt_lock() is held while waiting, so this has to be rather small.
#define GATT_CLIENT_SUBSCRIPTIONS_WRITE_TIMEOUT_MS (100)
// TODO:
// - Intercept "manual" CCCD writes from the app, error for now? or translate to
// ble_client_subscribe calls?
// - Filter out ANCS / AMS services -- apps shouldn't be able to muck with these
// -------------------------------------------------------------------------------------------------
// Static variables
static PebbleRecursiveMutex *s_gatt_client_subscriptions_mutex;
static SemaphoreHandle_t s_gatt_client_subscriptions_semphr;
//! s_gatt_client_subscriptions_mutex must be taken when accessing these static variables below!
//! Circular buffer holding notifications/indications that still need to be
//! consumed by the client. One circular buffer is created for a client as soon
//! as it subscribes to one (or more) characteristic.
static CircularBuffer *s_circular_buffer[GAPLEClientNum];
static uint32_t s_circular_buffer_retain_count[GAPLEClientNum];
//! Whether a PEBBLE_BLE_GATT_CLIENT_EVENT has been scheduled for the particular GAPLEClient.
//! This is to bound the number of these events to one per queue.
static bool s_is_notification_event_pending[GAPLEClientNum];
// -------------------------------------------------------------------------------------------------
// The call below requires the caller to own the bt_lock while calling the
// function and for as long as the result is being used / accessed.
extern BLEDescriptor gatt_client_accessors_find_cccd_with_characteristic(
BLECharacteristic characteristic_ref,
uint8_t *characteristic_properties_out,
uint16_t *characteristic_att_handle_out,
GAPLEConnection **connection_out);
extern BLECharacteristic gatt_client_descriptor_get_characteristic_and_connection(
BLEDescriptor descriptor_ref,
GAPLEConnection **connection_out);
// -------------------------------------------------------------------------------------------------
// Function implemented by the gatt_client_operations module to write the CCCD (to alter the remote
// subscription state). The big difference with gatt_client_op_write_descriptor() is that this
// function calls back to the gatt_client_subscriptions module when the result of the write is
// received, so that that module can take care of sending the appropriate events to the clients.
extern BTErrno gatt_client_op_write_descriptor_cccd(BLEDescriptor cccd_ref,
const uint16_t *cccd_value);
// -------------------------------------------------------------------------------------------------
// Static function prototypes
static GATTClientSubscriptionNode * prv_find_subscription_for_characteristic(
BLECharacteristic characteristic_ref,
GAPLEConnection *connection);
static BLESubscription prv_prevailing_subscription_type(GATTClientSubscriptionNode *subscription);
static void prv_release_buffer(GAPLEClient client);
static void prv_remove_subscription(GAPLEConnection *connection,
GATTClientSubscriptionNode *subscription);
// -------------------------------------------------------------------------------------------------
//! bt_lock() may only (optionally) be taken *before* prv_lock(), otherwise we'll deadlock.
static void prv_lock(void) {
mutex_lock_recursive(s_gatt_client_subscriptions_mutex);
}
static void prv_unlock(void) {
mutex_unlock_recursive(s_gatt_client_subscriptions_mutex);
}
static void prv_send_notification_event(PebbleTaskBitset task_mask) {
PebbleEvent e = {
.type = PEBBLE_BLE_GATT_CLIENT_EVENT,
.task_mask = task_mask,
.bluetooth = {
.le = {
.gatt_client = {
.subtype = PebbleBLEGATTClientEventTypeNotification,
.gatt_error = BLEGATTErrorSuccess,
},
},
},
};
event_put(&e);
}
static void prv_send_subscription_event(BLECharacteristic characteristic_ref,
PebbleTaskBitset task_mask, BLESubscription type,
BLEGATTError gatt_error) {
PebbleEvent e = {
.type = PEBBLE_BLE_GATT_CLIENT_EVENT,
.task_mask = task_mask,
.bluetooth = {
.le = {
.gatt_client = {
.subtype = PebbleBLEGATTClientEventTypeCharacteristicSubscribe,
.object_ref = characteristic_ref,
.subscription_type = type,
.gatt_error = gatt_error,
},
},
},
};
event_put(&e);
}
static bool prv_find_subscription_by_att_handle(ListNode *node, void *data) {
const GATTClientSubscriptionNode *subscription = (const GATTClientSubscriptionNode *) node;
const uint16_t att_handle = (const uint16_t)(uintptr_t) data;
return (subscription->att_handle == att_handle);
}
static bool prv_retain_buffer(GAPLEClient client);
static bool prv_wait_until_write_space_available(const CircularBuffer *buffer,
size_t required_length, uint32_t timeout_ms) {
bool did_stall = false;
const RtcTicks timeout_end_ticks = rtc_get_ticks() + milliseconds_to_ticks(timeout_ms);
while (true) {
prv_lock();
// bt_lock() is held when this function is called. Unsubscribing also requires taking bt_lock(),
// therefore it can't have been released in the mean time and therefore no need to check whether
// it still exists.
const uint16_t write_space = circular_buffer_get_write_space_remaining(buffer);
prv_unlock();
if (LIKELY(write_space >= required_length)) {
if (UNLIKELY(did_stall)) {
PBL_LOG(LOG_LEVEL_DEBUG, "GATT notification stalled for %d ms...",
(int)(timeout_ms - ticks_to_milliseconds(timeout_end_ticks - rtc_get_ticks())));
analytics_inc(ANALYTICS_DEVICE_METRIC_BLE_GATT_STALLED_NOTIFICATIONS_COUNT,
AnalyticsClient_System);
}
return true;
}
const RtcTicks now_ticks = rtc_get_ticks();
if (now_ticks > timeout_end_ticks) {
// Timeout expired.
return false;
}
// Wait until space is freed up:
const uint32_t timeout_ticks = (timeout_end_ticks - now_ticks);
if (pdFALSE == xSemaphoreTake(s_gatt_client_subscriptions_semphr, timeout_ticks)) {
// Timeout expired while waiting for the semaphore.
return false;
}
did_stall = true;
}
}
//! Internally used by gatt.c, should not be called otherwise.
//! For some reason, Bluetopia considers server notifications / indications the
//! be "connection events", while they are really client events...
//! @note bt_lock may be held by the caller. If the bt_lock is not held we will block for a little
//! if the subscription buffer is full
void gatt_client_subscriptions_handle_server_notification(GAPLEConnection *connection,
uint16_t att_handle,
const uint8_t *value,
uint16_t length) {
bt_lock();
ListNode *head = (ListNode *) connection->gatt_subscriptions;
const GATTClientSubscriptionNode *subscription =
(const GATTClientSubscriptionNode *) list_find(head, prv_find_subscription_by_att_handle,
(void *)(uintptr_t) att_handle);
if (UNLIKELY(!subscription)) {
// MT: I suspect this can be hit when the remote remembers the CCCD subscription state across
// disconnections (while we don't remember it across disconnections).
// iOS 7 behaves like this. iOS 8 supposedly does not.
static uint16_t s_last_logged_handle;
if (s_last_logged_handle != att_handle) {
// Only log the same handle once. Logging to flash adds enough of a delay to cause the
// Bluetopia Mailbox to get backed up quicker when running at a 15ms connection interval.
s_last_logged_handle = att_handle;
PBL_LOG(LOG_LEVEL_ERROR, "No subscription found for ATT handle %u", att_handle);
}
goto unlock;
}
// Mask to mask out all tasks
const PebbleTaskBitset task_mask_none = ~0;
PebbleTaskBitset task_mask = task_mask_none;
for (GAPLEClient c = 0; c < GAPLEClientNum; ++c) {
if (UNLIKELY(subscription->subscriptions[c] == BLESubscriptionNone)) {
// Not subscribed, continue
continue;
}
// Write the header first, then write the payload:
GATTBufferedNotificationHeader header = {
.characteristic = subscription->characteristic,
.value_length = length,
};
CircularBuffer *buffer = s_circular_buffer[c];
bt_unlock();
// If we do not hold the bt_lock() at this point it's safe to block for a little bit waiting
// for notifications to be consumed
uint32_t write_timeout = bt_lock_is_held() ? 0 : GATT_CLIENT_SUBSCRIPTIONS_WRITE_TIMEOUT_MS;
bool consumed = prv_wait_until_write_space_available(buffer, (sizeof(header) + length),
write_timeout);
bt_lock();
if (!consumed) {
PBL_LOG(LOG_LEVEL_ERROR, "Subscription buffer full. Dropping GATT notification of %u bytes",
length);
analytics_inc(ANALYTICS_DEVICE_METRIC_BLE_GATT_DROPPED_NOTIFICATIONS_COUNT,
AnalyticsClient_System);
continue;
}
prv_lock();
{
circular_buffer_write(buffer, (const uint8_t *) &header, sizeof(header));
circular_buffer_write(buffer, value, length);
if (UNLIKELY(!s_is_notification_event_pending[c])) {
task_mask &= ~gap_le_pebble_task_bit_for_client(c);
s_is_notification_event_pending[c] = true;
}
}
prv_unlock();
}
if (UNLIKELY(task_mask != task_mask_none)) {
prv_send_notification_event(task_mask);
}
unlock:
bt_unlock();
}
// -------------------------------------------------------------------------------------------------
static GATTClientSubscriptionNode * prv_find_subscription_and_connection_for_cccd(
BLEDescriptor cccd_ref,
GAPLEConnection **connection_out) {
BLECharacteristic characteristic_ref =
gatt_client_descriptor_get_characteristic_and_connection(cccd_ref,
connection_out);
if (!*connection_out) {
return NULL;
}
return prv_find_subscription_for_characteristic(characteristic_ref, *connection_out);
}
//! Internally used by gatt_client_operations.c, should not be called otherwise.
//! This function handles the completion of pending (un)subscriptions (confirmations of the writing
//! to the remote CCCD).
//! @note bt_lock is assumed to be already been taken by the caller!
void gatt_client_subscriptions_handle_write_cccd_response(BLEDescriptor cccd, BLEGATTError error) {
GAPLEConnection *connection;
GATTClientSubscriptionNode *subscription =
prv_find_subscription_and_connection_for_cccd(cccd, &connection);
if (!subscription || !connection) {
// FIXME: When unsubscribing, the GATTClientSubscriptionNode is already removed at this point
PBL_LOG(LOG_LEVEL_DEBUG,
"No subscription and/or connection found for CCCD write response (%u)", error);
return;
}
// Mask to mask out all tasks
const PebbleTaskBitset task_mask_none = ~0;
PebbleTaskBitset task_mask = task_mask_none;
const bool has_error = (error != BLEGATTErrorSuccess);
const BLESubscription type = has_error ?
BLESubscriptionNone : prv_prevailing_subscription_type(subscription);
for (GAPLEClient c = 0; c < GAPLEClientNum; ++c) {
if (subscription->pending_confirmation[c]) {
subscription->pending_confirmation[c] = false;
if (subscription->subscriptions[c] == BLESubscriptionNone) {
// Client unsubscribed in the mean-time. Confirmation should already have been sent.
continue;
}
if (has_error) {
// Subscribe failed. Record that the client is not subscribed and release buffer:
subscription->subscriptions[c] = BLESubscriptionNone;
prv_release_buffer(c);
}
task_mask &= ~gap_le_pebble_task_bit_for_client(c);
}
}
if (task_mask != task_mask_none) {
prv_send_subscription_event(subscription->characteristic, task_mask, type, error);
}
// In the error case, clean up the subscription data structure, if no longer used:
if (has_error && prv_prevailing_subscription_type(subscription) == BLESubscriptionNone) {
prv_remove_subscription(connection, subscription);
}
}
// -------------------------------------------------------------------------------------------------
static bool prv_check_buffer(GAPLEClient client) {
if (s_circular_buffer[client] == NULL) {
PBL_LOG(LOG_LEVEL_ERROR, "App attempted to consume notifications without buffer.");
return false;
}
return true;
}
// -------------------------------------------------------------------------------------------------
bool prv_get_next_notification_header(GAPLEClient client,
GATTBufferedNotificationHeader *header_out) {
bool has_notification = false;
GATTBufferedNotificationHeader header;
const uint16_t copied_length = circular_buffer_copy(s_circular_buffer[client],
(uint8_t *) &header,
sizeof(header));
if (copied_length == sizeof(header)) {
has_notification = true;
if (header_out) {
*header_out = header;
}
}
return has_notification;
}
// -------------------------------------------------------------------------------------------------
bool gatt_client_subscriptions_get_notification_header(GAPLEClient client,
GATTBufferedNotificationHeader *header_out) {
bool has_notification = false;
prv_lock();
if (!prv_check_buffer(client)) {
goto unlock;
}
has_notification = prv_get_next_notification_header(client, header_out);
const uint16_t read_space = circular_buffer_get_read_space_remaining(s_circular_buffer[client]);
if (has_notification && header_out) {
// When tackling https://pebbletechnology.atlassian.net/browse/PBL-14151 this should probably
// not be an assert, but just return 0, in case the app mucked with the storage
PBL_ASSERTN(header_out->value_length <= read_space - sizeof(*header_out));
}
unlock:
prv_unlock();
return has_notification;
}
// -------------------------------------------------------------------------------------------------
uint16_t gatt_client_subscriptions_consume_notification(BLECharacteristic *characteristic_ref_out,
uint8_t *value_out,
uint16_t *value_length_in_out,
GAPLEClient client, bool *has_more_out) {
bool has_more = false;
GATTBufferedNotificationHeader next_header = {};
prv_lock();
{
if (!prv_check_buffer(client)) {
has_more = false; // the client went away
goto unlock;
}
GATTBufferedNotificationHeader header = {};
const bool has_notification = prv_get_next_notification_header(client, &header);
if (LIKELY(has_notification)) {
if (LIKELY(*value_length_in_out >= header.value_length)) {
const uint16_t copied_length =
circular_buffer_copy_offset(s_circular_buffer[client],
sizeof(header), /* skip header */
value_out,
header.value_length);
if (UNLIKELY(copied_length != header.value_length)) {
PBL_LOG(LOG_LEVEL_ERROR, "Couldn't copy the number of requested byes (%u vs %u)",
header.value_length, copied_length);
}
*characteristic_ref_out = header.characteristic;
*value_length_in_out = copied_length;
} else {
PBL_LOG(LOG_LEVEL_ERROR, "Client didn't provide buffer that was big enough (%u vs %u)",
*value_length_in_out, header.value_length);
*characteristic_ref_out = BLE_CHARACTERISTIC_INVALID;
*value_length_in_out = 0;
}
// Always eat the notification:
circular_buffer_consume(s_circular_buffer[client],
sizeof(header) + header.value_length);
} else {
PBL_LOG(LOG_LEVEL_WARNING, "Consume called while no notifications in buffer");
*characteristic_ref_out = BLE_CHARACTERISTIC_INVALID;
*value_length_in_out = 0;
}
has_more = has_notification &&
prv_get_next_notification_header(client, &next_header);
}
unlock:
if (!has_more) {
s_is_notification_event_pending[client] = false;
}
if (has_more_out) {
*has_more_out = has_more;
}
prv_unlock();
// In the interest of simplicity, just give unconditionally (regardless of the number of bytes
// consumed and regardless of which buffer was freed) to make
// prv_wait_until_write_space_available() "poll" once whether there's enough space. We could be
// smarter about this and add additional book-keeping so the semaphore is only given if enough
// bytes have been freed up in the buffer of interest.
xSemaphoreGive(s_gatt_client_subscriptions_semphr);
return next_header.value_length;
}
// -------------------------------------------------------------------------------------------------
void gatt_client_subscriptions_reschedule(GAPLEClient c) {
prv_lock();
const PebbleTaskBitset task_mask = ~gap_le_pebble_task_bit_for_client(c);
prv_send_notification_event(task_mask);
s_is_notification_event_pending[c] = true;
prv_unlock();
}
// -------------------------------------------------------------------------------------------------
// Decrements ownership count
static void prv_release_buffer(GAPLEClient client) {
prv_lock();
{
PBL_ASSERTN(s_circular_buffer_retain_count[client]);
--s_circular_buffer_retain_count[client];
if (s_circular_buffer_retain_count[client] == 0) {
// Last subscription for this client to require the circular buffer, go ahead and clean it up:
kernel_free(s_circular_buffer[client]);
s_circular_buffer[client] = NULL;
// if the buffer is destroyed, there are no more events
s_is_notification_event_pending[client] = false;
}
}
prv_unlock();
}
// Increments ownership count
static bool prv_retain_buffer(GAPLEClient client) {
bool rv = true;
prv_lock();
{
if (s_circular_buffer_retain_count[client] == 0) {
// First subscription for this client to require the circular buffer, go ahead and create it:
PBL_ASSERTN(s_circular_buffer[client] == NULL);
const size_t size = sizeof(CircularBuffer) + GATT_CLIENT_SUBSCRIPTIONS_BUFFER_SIZE;
// TODO: Use app_malloc for the storage when client is app
// https://pebbletechnology.atlassian.net/browse/PBL-14151
uint8_t *buffer = (uint8_t *) kernel_zalloc(size);
if (!buffer) {
rv = false;
goto unlock;
}
CircularBuffer *circular_buffer = (CircularBuffer *) buffer;
circular_buffer_init(circular_buffer, (uint8_t *) (circular_buffer + 1),
GATT_CLIENT_SUBSCRIPTIONS_BUFFER_SIZE);
s_circular_buffer[client] = circular_buffer;
}
++s_circular_buffer_retain_count[client];
}
unlock:
prv_unlock();
return rv;
}
// -------------------------------------------------------------------------------------------------
static bool prv_find_subscription_cb(ListNode *node, void *data) {
const GATTClientSubscriptionNode *subscription = (const GATTClientSubscriptionNode *) node;
const BLECharacteristic characteristic_ref = (BLECharacteristic) data;
return (subscription->characteristic == characteristic_ref);
}
static GATTClientSubscriptionNode * prv_find_subscription_for_characteristic(
BLECharacteristic characteristic_ref,
GAPLEConnection *connection) {
ListNode *head = (ListNode *) connection->gatt_subscriptions;
return (GATTClientSubscriptionNode *) list_find(head, prv_find_subscription_cb,
(void *) characteristic_ref);
}
// -------------------------------------------------------------------------------------------------
static bool prv_has_pending_cccd_write(GATTClientSubscriptionNode *subscription) {
for (GAPLEClient c = 0; c < GAPLEClientNum; ++c) {
if (subscription->pending_confirmation[c]) {
return true;
}
}
return false;
}
static BLESubscription prv_prevailing_subscription_type(GATTClientSubscriptionNode *subscription) {
const BLESubscription orred = subscription->subscriptions[GAPLEClientApp] |
subscription->subscriptions[GAPLEClientKernel];
// Notifications wins over None and Indications:
if (orred & BLESubscriptionNotifications) {
return BLESubscriptionNotifications;
}
// None or Indications:
return (orred & BLESubscriptionIndications);
}
//! Mask out unsupported subscription type bits based on the
//! supported_properties of a characteristic.
//! @return true if the subscription_type is supported, false if not.
static bool prv_sanitize_subscription_type(BLESubscription *subscription_type,
uint8_t supported_properties) {
if (*subscription_type == BLESubscriptionNone) {
// None is always supported
return true;
}
BLESubscription supported = BLESubscriptionNone;
if (supported_properties & BLEAttributePropertyNotify) {
supported |= BLESubscriptionNotifications;
}
if (supported_properties & BLEAttributePropertyIndicate) {
supported |= BLESubscriptionIndications;
}
// Mask out the unsupported type bits:
*subscription_type &= supported;
return (*subscription_type != BLESubscriptionNone);
}
// -------------------------------------------------------------------------------------------------
static void prv_remove_subscription(GAPLEConnection *connection,
GATTClientSubscriptionNode *subscription) {
list_remove(&subscription->node,
(ListNode **) &connection->gatt_subscriptions, NULL);
kernel_free(subscription);
}
// -------------------------------------------------------------------------------------------------
static BTErrno prv_subscribe(BLECharacteristic characteristic_ref,
BLESubscription subscription_type,
GAPLEClient client, bool is_cleaning_up) {
BLESubscription previous_prevailing_type = BLESubscriptionNone;
GAPLEConnection *connection;
uint8_t supported_properties;
uint16_t att_handle;
BLEDescriptor cccd_ref =
gatt_client_accessors_find_cccd_with_characteristic(characteristic_ref, &supported_properties,
&att_handle, &connection);
if (cccd_ref == BLE_DESCRIPTOR_INVALID || !connection) {
// Invalid characteristic or characteristic does not have a CCCD
return BTErrnoInvalidParameter;
}
if (!prv_sanitize_subscription_type(&subscription_type, supported_properties)) {
// Unsupported subscription type
return BTErrnoInvalidParameter;
}
// Try to find existing subscription
GATTClientSubscriptionNode *subscription =
prv_find_subscription_for_characteristic(characteristic_ref, connection);
bool did_create_new_subscription = false;
if (subscription) {
if (subscription->subscriptions[client] == subscription_type) {
// Already subscribed
return BTErrnoInvalidState;
}
if (subscription->pending_confirmation[client] && !is_cleaning_up) {
// Already a pending subscription in flight...
return BTErrnoInvalidState;
}
previous_prevailing_type = prv_prevailing_subscription_type(subscription);
} else {
if (subscription_type == BLESubscriptionNone) {
// No subscription, so nothing to unsubscribe from...
return BTErrnoInvalidState;
}
// No subscriptions for the characteristic yet, go create one:
subscription = (GATTClientSubscriptionNode *) kernel_malloc(sizeof(GATTClientSubscriptionNode));
if (!subscription) {
// OOM
return BTErrnoNotEnoughResources;
}
// Initialize it:
*subscription = (const GATTClientSubscriptionNode) {
.characteristic = characteristic_ref,
.att_handle = att_handle,
};
// Prepend to the list of subscriptions of the connection:
ListNode *head = &connection->gatt_subscriptions->node;
connection->gatt_subscriptions =
(GATTClientSubscriptionNode *) list_prepend(head, &subscription->node);
PBL_LOG(LOG_LEVEL_DEBUG, "Added BLE subscription for handle 0x%x", att_handle);
did_create_new_subscription = true;
}
// Keeping this around in case the write fails:
const BLESubscription previous_type = subscription->subscriptions[client];
// Update the client state:
subscription->subscriptions[client] = subscription_type;
// Manage the GATT subscription state:
BTErrno ret_val = BTErrnoOK;
bool has_pending_write = prv_has_pending_cccd_write(subscription);
const BLESubscription next_prevailing_type = prv_prevailing_subscription_type(subscription);
if (next_prevailing_type != previous_prevailing_type) {
// The subscription type changed for this characteristic:
// Write to the Client Configuration Characteristic Descriptor on the
// remote to change the subscription:
const uint16_t value = subscription_type;
ret_val = gatt_client_op_write_descriptor_cccd(cccd_ref, &value);
if (ret_val != BTErrnoOK) {
// Write failed, bail out!
if (did_create_new_subscription) {
// Clean up...
prv_remove_subscription(connection, subscription);
} else {
// ... or restore previous state:
subscription->subscriptions[client] = previous_type;
}
return ret_val;
}
has_pending_write = true;
}
// Manage the client buffer:
if (subscription_type == BLESubscriptionNone) {
// Decrement retain count, or free:
prv_release_buffer(client);
} else {
// Increment retain count, or create buffer:
if (!prv_retain_buffer(client)) {
// Failed to create buffer, abort!
if (did_create_new_subscription) {
prv_remove_subscription(connection, subscription);
}
return BTErrnoNotEnoughResources;
}
}
if (ret_val == BTErrnoOK && !is_cleaning_up) {
if (subscription_type == BLESubscriptionNone || !has_pending_write) {
// When unsubscribing or when Pebble was already subscribed,
// immediately send unsubscription confirmation event to client:
prv_send_subscription_event(characteristic_ref, ~gap_le_pebble_task_bit_for_client(client),
subscription_type, BLEGATTErrorSuccess);
} else {
// When subscribing, wait for the CCCD Write Response before sending the confirmation event
// to the client.
subscription->pending_confirmation[client] = true;
}
}
if (next_prevailing_type == BLESubscriptionNone) {
// No more subscribers or CCCD write failed, free the node:
prv_remove_subscription(connection, subscription);
}
return ret_val;
}
BTErrno gatt_client_subscriptions_subscribe(BLECharacteristic characteristic_ref,
BLESubscription subscription_type,
GAPLEClient client) {
bt_lock();
BTErrno ret_val = prv_subscribe(characteristic_ref, subscription_type, client,
false /* is_cleaning_up */);
bt_unlock();
return ret_val;
}
// -------------------------------------------------------------------------------------------------
bool prv_cleanup_subscriptions_for_client(GAPLEConnection *connection, void *data) {
const GAPLEClient client = (const GAPLEClient)(uintptr_t) data;
GATTClientSubscriptionNode *subscription = connection->gatt_subscriptions;
while (subscription) {
GATTClientSubscriptionNode *next_subscription =
(GATTClientSubscriptionNode *) subscription->node.next;
// If subscribed, unsubscribe:
if (subscription->subscriptions[client] != BLESubscriptionNone) {
prv_subscribe(subscription->characteristic, BLESubscriptionNone, client,
true /* is_cleaning_up */);
}
subscription = next_subscription;
}
return false /* should_stop */;
}
void gatt_client_subscriptions_cleanup_by_client(GAPLEClient client) {
bt_lock();
{
// Walk all the connections to find subscriptions to unsubscribe:
gap_le_connection_find(prv_cleanup_subscriptions_for_client, (void *)(uintptr_t) client);
}
bt_unlock();
}
// -------------------------------------------------------------------------------------------------
void gatt_client_subscriptions_cleanup_by_connection(struct GAPLEConnection *connection,
bool should_unsubscribe) {
bt_lock();
{
GATTClientSubscriptionNode *node = connection->gatt_subscriptions;
while (node) {
GATTClientSubscriptionNode *next = (GATTClientSubscriptionNode *) node->node.next;
// Decrement circular buffer retain count:
for (GAPLEClient c = 0; c < GAPLEClientNum; ++c) {
if (node->subscriptions[c] != BLESubscriptionNone) {
if (should_unsubscribe) {
// The connection is not gone, so unsubscribe for this client, this will also
// free the GATTClientSubscriptionNode when both clients are unsubscribed:
prv_subscribe(node->characteristic, BLESubscriptionNone, c,
true /* is_cleaning_up */);
} else {
// Just release the buffer on behalf of the subscription
prv_release_buffer(c);
}
}
}
if (!should_unsubscribe) {
// Just free the node and don't bother unsubscribing:
kernel_free(node);
}
node = next;
}
connection->gatt_subscriptions = NULL;
}
bt_unlock();
}
void gatt_client_subscription_cleanup_by_att_handle_range(
struct GAPLEConnection *connection, ATTHandleRange *range) {
bt_lock();
{
GATTClientSubscriptionNode *node = connection->gatt_subscriptions;
while (node) {
GATTClientSubscriptionNode *next = (GATTClientSubscriptionNode *) node->node.next;
if (node->att_handle >= range->start && node->att_handle <= range->end) {
for (GAPLEClient c = 0; c < GAPLEClientNum; ++c) {
prv_subscribe(node->characteristic, BLESubscriptionNone, c,
true);
}
}
node = next;
}
}
bt_unlock();
}
void gatt_client_subscription_boot(void) {
s_gatt_client_subscriptions_mutex = mutex_create_recursive();
s_gatt_client_subscriptions_semphr = xSemaphoreCreateBinary();
PBL_ASSERTN(s_gatt_client_subscriptions_semphr);
}
//! Only for unit tests
T_STATIC bool gatt_client_get_event_pending_state(GAPLEClient client) {
return s_is_notification_event_pending[client];
}
//! Only for unit tests
SemaphoreHandle_t gatt_client_subscription_get_semaphore(void) {
return s_gatt_client_subscriptions_semphr;
}
//! Only for unit tests
void gatt_client_subscription_cleanup(void) {
mutex_destroy((PebbleMutex *)s_gatt_client_subscriptions_mutex);
s_gatt_client_subscriptions_mutex = NULL;
vSemaphoreDelete(s_gatt_client_subscriptions_semphr);
s_gatt_client_subscriptions_semphr = NULL;
}

View file

@ -0,0 +1,118 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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/bluetooth/ble_client.h"
#include "util/attributes.h"
#include "gap_le_task.h"
#include <bluetooth/mtu.h>
struct GAPLEConnection;
#define MAX_ATT_WRITE_PAYLOAD_SIZE (ATT_MAX_SUPPORTED_MTU - 3)
#ifdef RECOVERY_FW
// In PRF, we use a very high connection interval to make the FW update go as fast as possible.
// We're requesting between 11-21ms (see gap_le_connect_params.c). It ultimately depends on the
// master device (iOS) to decide which value to pick. So far the shortest I've seen is 15ms (not
// using the 'hack' to act like a HID device/keyboard/mouse, which will make it go down to 11ms).
// However, at 15ms notifications are getting dropped regularly already. Just to be safe, make the
// buffer in PRF really big:
#define GATT_CLIENT_SUBSCRIPTIONS_BUFFER_SIZE ((MAX_ATT_WRITE_PAYLOAD_SIZE + \
sizeof(GATTBufferedNotificationHeader)) * 6)
#else
// FIXME: https://pebbletechnology.atlassian.net/browse/PBL-11671
#define GATT_CLIENT_SUBSCRIPTIONS_BUFFER_SIZE ((MAX_ATT_WRITE_PAYLOAD_SIZE + \
sizeof(GATTBufferedNotificationHeader)) * 4)
#endif
//! Data structure representing a subscription of a specific client for
//! noticications or indications of a GATT characteristic for a specific
//! client (GAPLEClientApp or GAPLEClientKernel). The GAPLEConnection struct has
//! the head for each BLE connection.
typedef struct {
ListNode node;
//! The characteristic to which the client is subscribed
BLECharacteristic characteristic;
//! Cached ATT handle of the characteristic
uint16_t att_handle;
//! Array of subscription types for each client
BLESubscription subscriptions[GAPLEClientNum];
//! For each client, whether it is waiting for an event to confirm the subscription
bool pending_confirmation[GAPLEClientNum];
} GATTClientSubscriptionNode;
//! Data structure representing a serialized GATT notification header.
typedef struct PACKED {
BLECharacteristic characteristic;
uint16_t value_length;
uint8_t value[];
} GATTBufferedNotificationHeader;
BTErrno gatt_client_subscriptions_subscribe(BLECharacteristic characteristic,
BLESubscription subscription_type,
GAPLEClient client);
//! Gets the length of the next notification in the buffer that was received.
//! @param[out] header_out The header of the notification, containing the value length and the
//! characteristic reference.
//! @return True if there is a notification in the buffer, false if not
bool gatt_client_subscriptions_get_notification_header(GAPLEClient client,
GATTBufferedNotificationHeader *header_out);
//! Copies the data of the next notification and marks it as "consumed".
//! The client *MUST* keep on calling gatt_client_subscriptions_consume_notification() in a loop
//! until 0 is returned.
//! @see gatt_client_subscriptions_get_notification_value_length() to get the length of the next
//! notification.
//! @param[in,out] value_length_in_out Cannot be NULL. In: the size of the value_out buffer.
//! Out: the number of bytes copied into the value_out buffer.
//! @param[out] has_more_out Cannot be NULL. Will be set to true if there are more notifications
//! in the buffer, or to false if there are no more notifiations in the buffer.
//! @return The length of the next notification's payload, if there is any (has_more_out is true),
//! undefined otherwise.
uint16_t gatt_client_subscriptions_consume_notification(BLECharacteristic *characteristic_ref_out,
uint8_t *value_out,
uint16_t *value_length_in_out,
GAPLEClient client, bool *has_more_out);
//! Indicates that the client wants to pause processing notifications and yield to keep the system
//! responsive. This puts a new event on the queue so the client can continue processing later on.
void gatt_client_subscriptions_reschedule(GAPLEClient c);
//! Unsubscribes all subscriptions associated with the client. This function
//! assumes the connection is still alive and will write to the CCCD to
//! "unsubscribe" from the remote as well, if the specified client was the
//! last one to be registered for a particular characteristic.
void gatt_client_subscriptions_cleanup_by_client(GAPLEClient client);
//! Frees the GATTClientSubscriptionNode nodes that might have been associated
//! with the connection as result of gatt_client_subscriptions_subscribe calls.
//! @param should_unsubscribe If true, the current subscriptions will be unsubscribed before
//! cleanup. If false, the current subscriptions will not be unsubscribed (this is useful when
//! the connection is already severed.) No unsubscription events will be emitted regardless of the
//! value of this argument.
void gatt_client_subscriptions_cleanup_by_connection(struct GAPLEConnection *connection,
bool should_unsubscribe);
//! Called once at boot.
void gatt_client_subscription_boot(void);

View file

@ -0,0 +1,213 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "gatt_service_changed.h"
#include "gap_le_connection.h"
#include "comm/bt_lock.h"
#include "kernel/pbl_malloc.h"
#include "services/common/new_timer/new_timer.h"
#include "services/common/system_task.h"
#include "system/logging.h"
#include "util/net.h"
#include "system/hexdump.h"
#include <bluetooth/gatt.h>
#include <btutil/bt_device.h>
extern BTErrno gatt_client_discovery_rediscover_all(const BTDeviceInternal *device);
extern void gatt_client_discovery_handle_service_range_change(GAPLEConnection *connection,
ATTHandleRange *range);
extern void gatt_client_discovery_discover_range(GAPLEConnection *connection,
ATTHandleRange *hdl_range);
////////////////////////////////////////////////////////////////////////////////////////////////////
// Client -- Pebble consuming the remote's "Service Changed" characteristic
static void prv_rediscover_kernelbg_cb(void *data) {
// Rediscover the world:
BTDeviceInternal *device = (BTDeviceInternal *) data;
const BTErrno e = gatt_client_discovery_rediscover_all(device);
kernel_free(device);
if (e != BTErrnoOK) {
PBL_LOG(LOG_LEVEL_ERROR, "Service Changed couldn't restart discovery: %i", e);
}
}
//! @note bt_lock is assumed to be taken by the caller
bool gatt_service_changed_client_handle_indication(struct GAPLEConnection *connection,
uint16_t att_handle, const uint8_t *value,
uint16_t value_length) {
if (connection->gatt_service_changed_att_handle != att_handle) {
return false;
}
if (value_length != sizeof(ATTHandleRange)) {
PBL_LOG(LOG_LEVEL_ERROR, "Service Changed Indication incorrect length: %u", value_length);
// Pretend we ate the indication. There will be no GAPLECharacteristic in the system that will
// match this ATT handle anyway.
return true;
}
ATTHandleRange *range = (ATTHandleRange *) value;
PBL_LOG(LOG_LEVEL_DEBUG, "Service Changed Indication: %x - %x", range->start, range->end);
// Initiate rediscovery on KernelBG if the Server is asking us to rediscover everything
// (See "2.5.2 Attribute Caching" in BT Core Specification)
if ((range->start == 0x001 && range->end == 0xFFFF)) {
BTDeviceInternal *device = (BTDeviceInternal *) kernel_malloc_check(sizeof(BTDeviceInternal));
*device = connection->device;
system_task_add_callback(prv_rediscover_kernelbg_cb, device);
return true;
}
// My understanding is if we get here we will receive a range of handles for
// _one_ service. "The start Attribute Handle shall be the start Attribute
// Handle of the service definition containing the change and the end
// Attribute Handle shall be the last Attribute Handle of the service
// definition containing the change" (Core Spec 2.5.2 Attribute Caching)
// Send an event to notify us that service was removed/added
gatt_client_discovery_handle_service_range_change(connection, range);
// Let's spawn a new discovery
gatt_client_discovery_discover_range(connection, range);
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// Server -- Pebble serving up the "Service Changed" characteristic to the remote
// Work-around for iOS issue where sending the indication immediately when iOS subscribes to the
// characteristic is causing problems:
// BTServer: ATT Failed to locate GAP primary service on device ...
#define GATT_SERVICE_CHANGED_INDICATION_DELAY_MS (10000)
#define GATT_SERVICE_CHANGED_INDICATION_MAX_TIMES (5)
static uint32_t s_service_changed_indications_left;
// For unit testing
void gatt_service_changed_server_init(void) {
s_service_changed_indications_left = 0;
}
void gatt_service_changed_server_handle_fw_update(void) {
// Once set, just keep it set until the next "normal" reboot.
// It will cause Pebble to send the "Service Change" indication to be sent every time the other
// end subscribes to it, causing the remote cache to be invalidated each time and force the
// remote to do discover services again. However, cap the total number of times we send the
// "Service Change" indication:
s_service_changed_indications_left = GATT_SERVICE_CHANGED_INDICATION_MAX_TIMES;
}
void bt_driver_cb_gatt_service_changed_server_confirmation(
const GattServerChangedConfirmationEvent *event) {
if (event->status_code != HciStatusCode_Success) {
PBL_LOG(LOG_LEVEL_ERROR, "Service Changed indication confirmation failure (timed out?) %"PRIu32,
(uint32_t)event->status_code);
}
}
void gatt_service_changed_server_cleanup_by_connection(GAPLEConnection *connection) {
if (connection->gatt_service_changed_indication_timer != TIMER_INVALID_ID) {
new_timer_delete(connection->gatt_service_changed_indication_timer);
connection->gatt_service_changed_indication_timer = TIMER_INVALID_ID;
}
}
static void prv_send_service_changed_indication(void *ctx) {
}
static void prv_send_indication_timer_cb(void *ctx) {
GAPLEConnection *connection = (GAPLEConnection *)ctx;
system_task_add_callback(prv_send_service_changed_indication, connection);
}
void bt_driver_cb_gatt_service_changed_server_subscribe(
const GattServerSubscribeEvent *event) {
bt_lock();
{
const bool subscribed = event->is_subscribing;
if (subscribed) {
PBL_LOG(LOG_LEVEL_DEBUG, "Remote subscribed to Service Changed characteristic");
GAPLEConnection *connection = gap_le_connection_by_addr(&event->dev_address);
if (!connection || connection->has_sent_gatt_service_changed_indication) {
// Already sent indication once during the lifetime of this connection, don't send again.
goto unlock;
}
// PRF will always send a "Service Changed" indication:
#if !RECOVERY_FW
if (s_service_changed_indications_left <= 0) {
goto unlock;
}
#endif
PBL_LOG(LOG_LEVEL_INFO, "Indicating Service Changed to remote device");
// Work-around for iOS issue (see comment above), send the indication after a short delay:
connection->gatt_service_changed_indication_timer = new_timer_create();
new_timer_start(connection->gatt_service_changed_indication_timer,
GATT_SERVICE_CHANGED_INDICATION_DELAY_MS, prv_send_indication_timer_cb,
connection, 0);
--s_service_changed_indications_left;
// Don't send again for this connection:
connection->has_sent_gatt_service_changed_indication = true;
}
}
unlock:
bt_unlock();
}
void bt_driver_cb_gatt_service_changed_server_read_subscription(
const GattServerReadSubscriptionEvent *event) {
bt_lock();
{
bt_driver_gatt_respond_read_subscription(event->transaction_id, 0 /* not subscribed */);
}
bt_unlock();
}
void bt_driver_cb_gatt_client_discovery_handle_service_changed(GAPLEConnection *connection,
uint16_t handle) {
bt_lock();
{
connection->gatt_service_changed_att_handle = handle;
}
bt_unlock();
}
//////////////////////////////////
// Prompt commands
//////////////////////////////////
void command_ble_send_service_changed_indication(void) {
prv_send_service_changed_indication(gap_le_connection_any());
}
void command_ble_rediscover(void) {
// assume we only have one connection for debug
GAPLEConnection *conn_hdl = gap_le_connection_any();
BTDeviceInternal *device = (BTDeviceInternal *) kernel_malloc_check(sizeof(BTDeviceInternal));
*device = conn_hdl->device;
system_task_add_callback(prv_rediscover_kernelbg_cb, device);
}

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 <stdint.h>
#include <stdbool.h>
//! @file This module contains the "Generic Attribute Profile Service" code, both the server and
//! client parts. Both ends can optionally implement this service (and client). iOS does for example
//! and so does Pebble. The one characteristic this service has is called "Service Changed". Its
//! purpose is to indicate to the other side whenever there are changes to the local GATT database
//! (and what ATT handle range the change is affecting), for example when an app adds or removes
//! a GATT service or characteristics.
//!
//! The server is mostly implemented in Bluetopia's GATT.c, but relies on our FW for some mundane
//! things like handling subscription events and actually firing off "Service Changed" indications.
//!
//! The client part is hooking into the guts of Pebble's gatt.c and gatt_client_discovery.c,
//! to catch GATT Indications before they reach higher layers and to trigger transparent rediscovery
//! of remote services.
//!
//! See BT Spec 4.0, Volume 3, Part G, 7.1 "Service Changed" for more information about the service.
////////////////////////////////////////////////////////////////////////////////////////////////////
// Client -- Pebble consuming the remote's "Service Changed" characteristic
struct GAPLEConnection;
//! Optionally handles GATT Value Indications, in case the ATT handle matches the GATT Service
//! Changed characteristic value for the connection. When it matches, it will autonomously iniate
//! GATT Service Discovery to refresh the local GATT cache.
//! @note bt_lock is assumed to be taken by the caller
bool gatt_service_changed_client_handle_indication(struct GAPLEConnection *connection,
uint16_t att_handle, const uint8_t *value,
uint16_t value_length);
////////////////////////////////////////////////////////////////////////////////////////////////////
// Server -- Pebble serving up the "Service Changed" characteristic to the remote
void gatt_service_changed_server_handle_fw_update(void);
////////////////////////////
// For Testing:
// BLEChrIdx gatt_service_changed_get_characteristic_idx(void);

View file

@ -0,0 +1,709 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "ams.h"
#include "ams_analytics.h"
#include "ams_util.h"
#include "comm/ble/ble_log.h"
#include "comm/ble/gap_le_connection.h"
#include "comm/ble/gatt_client_accessors.h"
#include "comm/ble/gatt_client_operations.h"
#include "comm/ble/gatt_client_subscriptions.h"
#include "comm/bt_conn_mgr.h"
#include "comm/bt_lock.h"
#include "kernel/event_loop.h"
#include "kernel/pbl_malloc.h"
#include "services/common/analytics/analytics_event.h"
#include "services/normal/music_internal.h"
#include "system/logging.h"
#include "system/hexdump.h"
#include "system/passert.h"
#include "util/likely.h"
#include "util/time/time.h"
#include <btutil/bt_device.h>
#include <string.h>
// -------------------------------------------------------------------------------------------------
// Function prototypes
static void prv_perform_on_kernel_main_task(void (*callback)(void *), void *data);
// -------------------------------------------------------------------------------------------------
// Static variables
typedef struct {
bool connected;
BLECharacteristic characteristics[NumAMSCharacteristic];
AMSEntityID next_entity_to_register;
} AMSClient;
//! All accesses should happen from KernelMain. Therefore no locking is needed.
static AMSClient *s_ams_client;
// -------------------------------------------------------------------------------------------------
// MusicServerImplementation
static AMSRemoteCommandID prv_ams_command_for_music_command(MusicCommand command) {
switch (command) {
case MusicCommandPlay:
return AMSRemoteCommandIDPlay;
case MusicCommandPause:
return AMSRemoteCommandIDPause;
case MusicCommandTogglePlayPause:
return AMSRemoteCommandIDTogglePlayPause;
case MusicCommandNextTrack:
return AMSRemoteCommandIDNextTrack;
case MusicCommandPreviousTrack:
return AMSRemoteCommandIDPreviousTrack;
case MusicCommandVolumeUp:
return AMSRemoteCommandIDVolumeUp;
case MusicCommandVolumeDown:
return AMSRemoteCommandIDVolumeDown;
case MusicCommandAdvanceRepeatMode:
return AMSRemoteCommandIDAdvanceRepeatMode;
case MusicCommandAdvanceShuffleMode:
return AMSRemoteCommandIDAdvanceShuffleMode;
case MusicCommandSkipForward:
return AMSRemoteCommandIDSkipForward;
case MusicCommandSkipBackward:
return AMSRemoteCommandIDSkipBackward;
case MusicCommandLike:
return AMSRemoteCommandIDLike;
case MusicCommandDislike:
return AMSRemoteCommandIDDislike;
case MusicCommandBookmark:
return AMSRemoteCommandIDBookmark;
default:
return AMSRemoteCommandIDInvalid;
}
}
static bool prv_music_is_command_supported(MusicCommand command) {
return (prv_ams_command_for_music_command(command) != AMSRemoteCommandIDInvalid);
}
static void prv_music_command_send(MusicCommand command) {
const AMSRemoteCommandID ams_command = prv_ams_command_for_music_command(command);
if (ams_command == AMSRemoteCommandIDInvalid) {
return;
}
ams_send_command(ams_command);
}
static MusicServerCapability prv_music_get_capability_bitset(void) {
return (MusicServerCapabilityPlaybackStateReporting |
MusicServerCapabilityProgressReporting |
MusicServerCapabilityVolumeReporting);
}
static bool prv_music_needs_user_to_start_playback_on_phone(void) {
return !music_has_now_playing();
}
static void prv_request_response_time(BtConsumer consumer, ResponseTimeState state,
uint16_t max_period_secs) {
PBL_ASSERT_TASK(PebbleTask_KernelMain);
bt_lock();
if (s_ams_client) {
const BLECharacteristic characteristic = s_ams_client->characteristics[0];
const BTDeviceInternal device = gatt_client_characteristic_get_device(characteristic);
if (!bt_device_is_invalid(&device.opaque)) {
GAPLEConnection *connection = gap_le_connection_by_device(&device);
conn_mgr_set_ble_conn_response_time(connection, consumer, state, max_period_secs);
}
}
bt_unlock();
}
static void prv_request_reduced_latency_cb(void *data) {
const bool reduced_latency = (uintptr_t)data;
const ResponseTimeState state = reduced_latency ? ResponseTimeMiddle : ResponseTimeMax;
prv_request_response_time(BtConsumerMusicServiceIndefinite, state, MAX_PERIOD_RUN_FOREVER);
}
static void prv_request_low_latency_for_period_cb(void *data) {
const uint32_t period_ms = (uintptr_t)data;
prv_request_response_time(BtConsumerMusicServiceMomentary, ResponseTimeMin,
period_ms / MS_PER_SECOND);
}
static void prv_music_request_reduced_latency(bool reduced_latency) {
prv_perform_on_kernel_main_task(prv_request_reduced_latency_cb,
(void *)(uintptr_t)reduced_latency);
}
static void prv_music_request_low_latency_for_period(uint32_t period_ms) {
prv_perform_on_kernel_main_task(prv_request_low_latency_for_period_cb,
(void *)(uintptr_t)period_ms);
}
static const MusicServerImplementation s_ams_music_implementation = {
.debug_name = "AMS",
.is_command_supported = prv_music_is_command_supported,
.command_send = prv_music_command_send,
.needs_user_to_start_playback_on_phone = prv_music_needs_user_to_start_playback_on_phone,
.get_capability_bitset = prv_music_get_capability_bitset,
.request_reduced_latency = prv_music_request_reduced_latency,
.request_low_latency_for_period = prv_music_request_low_latency_for_period,
};
// -------------------------------------------------------------------------------------------------
// Internal helpers
static void prv_analytics_log_event_with_info(AMSAnalyticsEvent event, int32_t aux_info) {
analytics_event_ams(event, aux_info);
}
static void prv_perform_on_kernel_main_task(void (*callback)(void *), void *data) {
const bool is_kernel_main = (pebble_task_get_current() == PebbleTask_KernelMain);
if (is_kernel_main) {
callback(data);
} else {
launcher_task_add_callback(callback, data);
}
}
static AMSCharacteristic prv_get_id_for_characteristic(BLECharacteristic characteristic_to_find) {
if (!s_ams_client) {
return AMSCharacteristicInvalid;
}
const BLECharacteristic *characteristic = s_ams_client->characteristics;
for (AMSCharacteristic id = 0; id < NumAMSCharacteristic; ++id, ++characteristic) {
if (*characteristic == characteristic_to_find) {
return id;
}
}
return AMSCharacteristicInvalid;
}
static const uint8_t *prv_get_registration_cmd_for_entity(AMSEntityID entity_id,
uint8_t *cmd_length_out) {
static const uint8_t register_for_player_entity_updates_cmd[] = {
AMSEntityIDPlayer,
// Apple bug #21283910
// http://www.openradar.me/radar?id=6752237204275200
// Registering for the Player Name attribute can cause BTLEServer to crash repeatedly.
// (verified in iOS 8.3 and iOS 9 beta 1)
// AMSPlayerAttributeIDName,
AMSPlayerAttributeIDPlaybackInfo,
AMSPlayerAttributeIDVolume,
};
static const uint8_t register_for_queue_entity_updates_cmd[] = {
AMSEntityIDQueue,
AMSQueueAttributeIDIndex,
AMSQueueAttributeIDCount,
AMSQueueAttributeIDShuffleMode,
AMSQueueAttributeIDRepeatMode,
};
static const uint8_t register_for_track_entity_updates_cmd[] = {
AMSEntityIDTrack,
AMSTrackAttributeIDArtist,
AMSTrackAttributeIDAlbum,
AMSTrackAttributeIDTitle,
AMSTrackAttributeIDDuration,
};
static const struct {
const uint8_t length;
const uint8_t * const value;
} packet_length_and_data[NumAMSEntityID] = {
[AMSEntityIDPlayer] = {
.length = sizeof(register_for_player_entity_updates_cmd),
.value = register_for_player_entity_updates_cmd,
},
[AMSEntityIDQueue] = {
.length = sizeof(register_for_queue_entity_updates_cmd),
.value = register_for_queue_entity_updates_cmd,
},
[AMSEntityIDTrack] = {
.length = sizeof(register_for_track_entity_updates_cmd),
.value = register_for_track_entity_updates_cmd,
},
};
if (cmd_length_out) {
*cmd_length_out = packet_length_and_data[entity_id].length;
}
return packet_length_and_data[entity_id].value;
}
static void prv_reset_next_entity_to_register(void) {
s_ams_client->next_entity_to_register = AMSEntityIDPlayer;
}
static bool prv_is_entity_update_registration_done(void) {
return (s_ams_client->next_entity_to_register == AMSEntityIDInvalid);
}
static void prv_register_next_entity(void *unused) {
if (LIKELY(!s_ams_client || prv_is_entity_update_registration_done())) {
return;
}
// Make the Bluetopia heap consumption of this module as minimal and predictable as possible,
// by having only one outstanding GATT operation queued up at any moment in time (instead of
// queueing up all the writes in one go):
const AMSEntityID entity_id = s_ams_client->next_entity_to_register;
const BLECharacteristic entity_update_characteristic =
s_ams_client->characteristics[AMSCharacteristicEntityUpdate];
uint8_t cmd_length = 0;
const uint8_t *cmd_value = prv_get_registration_cmd_for_entity(entity_id, &cmd_length);
const BTErrno e = gatt_client_op_write(entity_update_characteristic,
cmd_value, cmd_length,
GAPLEClientKernel);
if (e != BTErrnoOK) {
if (e == BTErrnoNotEnoughResources) {
// Need to wait for space to become available
launcher_task_add_callback(&prv_register_next_entity, NULL);
} else {
// Most likely the LE connection got busted, don't think retrying will help.
PBL_LOG(LOG_LEVEL_ERROR, "Write failed %i", e);
}
prv_analytics_log_event_with_info(AMSAnalyticsEventErrorRegisterEntityWrite, e);
}
}
static bool prv_set_connected(bool connected) {
if (s_ams_client->connected == connected) {
return true;
}
s_ams_client->connected = connected;
const bool has_error = !music_set_connected_server(&s_ams_music_implementation, connected);
if (has_error) {
s_ams_client->connected = false;
PBL_LOG(LOG_LEVEL_ERROR, "AMS could not (dis)connect to music service (%u)", connected);
prv_analytics_log_event_with_info(AMSAnalyticsEventErrorMusicServiceConnect, connected ? 1 : 2);
}
return !has_error;
}
// -------------------------------------------------------------------------------------------------
// Player entity update handlers
static void prv_handle_player_name_update(const AMSEntityUpdateNotification *update,
const uint16_t value_length) {
music_update_player_name(update->value_str, value_length);
}
static MusicPlayState prv_music_playstate_for_ams_playback_state(int32_t ams_playback_state) {
switch (ams_playback_state) {
case AMSPlaybackStatePaused: return MusicPlayStatePaused;
case AMSPlaybackStatePlaying: return MusicPlayStatePlaying;
case AMSPlaybackStateRewinding: return MusicPlayStateRewinding;
case AMSPlaybackStateForwarding: return MusicPlayStateForwarding;
default: return MusicPlayStateUnknown;
}
}
static bool prv_handle_player_playback_info_value(const char *value, uint32_t value_length,
uint32_t idx, void *context) {
// Default to -1 for playback state, or 0 otherwise, in case "value" is an empty string:
// This will cause the playback state to be set to MusicPlayStateUnknown.
int32_t value_out = (idx == AMSPlaybackInfoIdxState) ? -1 : 0;
const int32_t multiplier[] = {
// First value is the AMSPlaybackState enum, so unity multiplier:
[AMSPlaybackInfoIdxState] = 1,
// Second value is the playback rate [0.0, 1.0]. We store as percent, so 100x multiplier:
[AMSPlaybackInfoIdxRate] = 100,
// Third value is the elapsed time in seconds. We store as ms, so 1000x multiplier:
[AMSPlaybackInfoIdxElapsedTime] = 1000,
};
if (value_length && !ams_util_float_string_parse(value, value_length,
multiplier[idx], &value_out)) {
PBL_LOG(LOG_LEVEL_ERROR, "AMS playback info value failed to parse: %s", value);
prv_analytics_log_event_with_info(AMSAnalyticsEventErrorPlayerPlaybackInfoFloatParse, idx);
return false /* should_continue */;
}
PBL_LOG(LOG_LEVEL_DEBUG, "Playback info value update %"PRId32"=%"PRId32, idx, value_out);
MusicPlayerStateUpdate *state = (MusicPlayerStateUpdate *)context;
switch (idx) {
case AMSPlaybackInfoIdxState: {
state->playback_state = prv_music_playstate_for_ams_playback_state(value_out);
break;
}
case AMSPlaybackInfoIdxRate:
state->playback_rate_percent = value_out;
break;
case AMSPlaybackInfoIdxElapsedTime:
state->elapsed_time_ms = value_out;
break;
default:
WTF;
}
return true /* should_continue */;
}
static void prv_handle_player_playback_info_update(const AMSEntityUpdateNotification *update,
const uint16_t value_length) {
MusicPlayerStateUpdate state = {};
const uint8_t num_results = ams_util_csv_parse(update->value_str, value_length, &state,
prv_handle_player_playback_info_value);
const bool success = (num_results == 3);
if (success) {
music_update_player_playback_state(&state);
} else {
PBL_LOG(LOG_LEVEL_ERROR, "Expected CSV with 3 values:");
PBL_HEXDUMP(LOG_LEVEL_ERROR, (const uint8_t *) update->value_str, value_length);
prv_analytics_log_event_with_info(AMSAnalyticsEventErrorPlayerPlaybackInfoUpdate, num_results);
}
}
static bool prv_float_string_parse(const char *value, const uint16_t value_length,
int32_t multiplier, int32_t *value_in_out) {
if (value_length &&
!ams_util_float_string_parse(value, value_length, multiplier, value_in_out)) {
PBL_LOG(LOG_LEVEL_ERROR, "AMS float failed to parse:");
PBL_HEXDUMP(LOG_LEVEL_ERROR, (const uint8_t *)value, value_length);
return false;
}
return true;
}
static void prv_handle_player_volume_update(const AMSEntityUpdateNotification *update,
const uint16_t value_length) {
int32_t value_out = 0;
const bool success = prv_float_string_parse(update->value_str, value_length, 100, &value_out);
if (success) {
music_update_player_volume_percent(value_out);
} else {
prv_analytics_log_event_with_info(AMSAnalyticsEventErrorPlayerVolumeUpdate, value_length);
}
}
// -------------------------------------------------------------------------------------------------
// Queue entity update handlers
static int32_t prv_parse_queue_value(const char *value, const uint16_t value_length) {
int32_t value_out = 0;
prv_float_string_parse(value, value_length, 1, &value_out);
return value_out;
}
static void prv_handle_queue_index_update(const AMSEntityUpdateNotification *update,
const uint16_t value_length) {
const int32_t idx = prv_parse_queue_value(update->value_str, value_length);
PBL_LOG(LOG_LEVEL_DEBUG, "Queue index update: %"PRId32, idx);
// TODO: Do something with this info
}
static void prv_handle_queue_count_update(const AMSEntityUpdateNotification *update,
const uint16_t value_length) {
const int32_t count = prv_parse_queue_value(update->value_str, value_length);
PBL_LOG(LOG_LEVEL_DEBUG, "Queue count update: %"PRId32, count);
// TODO: Do something with this info
}
static void prv_handle_queue_shuffle_mode_update(const AMSEntityUpdateNotification *update,
const uint16_t value_length) {
const AMSShuffleMode shuffle_mode = prv_parse_queue_value(update->value_str, value_length);
PBL_LOG(LOG_LEVEL_DEBUG, "Queue shuffle mode update: %d", shuffle_mode);
// TODO: Do something with this info
}
static void prv_handle_queue_repeat_mode_update(const AMSEntityUpdateNotification *update,
const uint16_t value_length) {
const AMSRepeatMode repeat_mode = prv_parse_queue_value(update->value_str, value_length);
PBL_LOG(LOG_LEVEL_DEBUG, "Queue repeat mode update: %d", repeat_mode);
// TODO: Do something with this info
}
// -------------------------------------------------------------------------------------------------
// Track entity update handlers
static void prv_handle_track_artist_update(const AMSEntityUpdateNotification *update,
const uint16_t value_length) {
music_update_track_artist(update->value_str, value_length);
}
static void prv_handle_track_album_update(const AMSEntityUpdateNotification *update,
const uint16_t value_length) {
music_update_track_album(update->value_str, value_length);
}
static void prv_handle_track_title_update(const AMSEntityUpdateNotification *update,
const uint16_t value_length) {
music_update_track_title(update->value_str, value_length);
}
static void prv_handle_track_duration_update(const AMSEntityUpdateNotification *update,
const uint16_t value_length) {
int32_t duration_ms = 0; // Default to 0 in case value_length is 0
const bool success =
(!value_length ||
ams_util_float_string_parse(update->value_str, value_length, MS_PER_SECOND, &duration_ms));
if (success) {
music_update_track_duration(duration_ms);
} else {
PBL_LOG(LOG_LEVEL_ERROR, "AMS duration failed to parse");
prv_analytics_log_event_with_info(AMSAnalyticsEventErrorTrackDurationUpdate, value_length);
}
}
// -------------------------------------------------------------------------------------------------
// Update handler dispatch table
typedef void (*AMSUpdateHandler)(const AMSEntityUpdateNotification *update,
const uint16_t value_length);
static void prv_handle_update(const AMSEntityUpdateNotification *update,
const uint16_t value_length) {
switch (update->entity_id) {
case AMSEntityIDPlayer:
switch (update->attribute_id) {
case AMSPlayerAttributeIDName:
prv_handle_player_name_update(update, value_length);
return;
case AMSPlayerAttributeIDPlaybackInfo:
prv_handle_player_playback_info_update(update, value_length);
return;
case AMSPlayerAttributeIDVolume:
prv_handle_player_volume_update(update, value_length);
return;
default:
break;
}
break;
case AMSEntityIDQueue:
switch (update->attribute_id) {
case AMSQueueAttributeIDIndex:
prv_handle_queue_index_update(update, value_length);
return;
case AMSQueueAttributeIDCount:
prv_handle_queue_count_update(update, value_length);
return;
case AMSQueueAttributeIDShuffleMode:
prv_handle_queue_shuffle_mode_update(update, value_length);
return;
case AMSQueueAttributeIDRepeatMode:
prv_handle_queue_repeat_mode_update(update, value_length);
return;
default:
break;
}
break;
case AMSEntityIDTrack:
switch (update->attribute_id) {
case AMSTrackAttributeIDArtist:
prv_handle_track_artist_update(update, value_length);
return;
case AMSTrackAttributeIDAlbum:
prv_handle_track_album_update(update, value_length);
return;
case AMSTrackAttributeIDTitle:
prv_handle_track_title_update(update, value_length);
// FIXME: This is a workaround. See PBL-21818
music_update_track_position(0);
return;
case AMSTrackAttributeIDDuration:
prv_handle_track_duration_update(update, value_length);
return;
default:
break;
}
break;
default:
break;
}
PBL_LOG(LOG_LEVEL_ERROR, "Unknown EntityID:%u + AttrID:%u",
update->entity_id, update->attribute_id);
}
// -------------------------------------------------------------------------------------------------
// Interface towards kernel_le_client.c
void ams_create(void) {
PBL_ASSERTN(!s_ams_client);
s_ams_client = (AMSClient *) kernel_zalloc_check(sizeof(AMSClient));
}
void ams_invalidate_all_references(void) {
// We've gotten new characteristic references,
// this means the old ones will have been unsubscribed, so we're disconnected from AMS:
prv_set_connected(false);
// We also need to register for entity updates again:
prv_reset_next_entity_to_register();
for (int c = 0; c < NumAMSCharacteristic; c++) {
s_ams_client->characteristics[c] = BLE_CHARACTERISTIC_INVALID;
}
}
void ams_handle_service_removed(BLECharacteristic *characteristics, uint8_t num_characteristics) {
ams_invalidate_all_references();
}
void ams_handle_service_discovered(BLECharacteristic *characteristics) {
if (!s_ams_client) {
return;
}
BLE_LOG_DEBUG("In AMS service discovery CB");
PBL_ASSERTN(characteristics);
if (s_ams_client->characteristics[0] != BLE_CHARACTERISTIC_INVALID) {
PBL_LOG(LOG_LEVEL_WARNING, "Multiple AMS instances registered!?");
return;
}
// Keep around the BLECharacteristic references:
memcpy(s_ams_client->characteristics, characteristics,
sizeof(BLECharacteristic) * NumAMSCharacteristic);
const BLECharacteristic entity_update_characteristic =
characteristics[AMSCharacteristicEntityUpdate];
const BTErrno e = gatt_client_subscriptions_subscribe(entity_update_characteristic,
BLESubscriptionNotifications,
GAPLEClientKernel);
PBL_ASSERTN(e == BTErrnoOK);
}
bool ams_can_handle_characteristic(BLECharacteristic characteristic) {
if (!s_ams_client) {
return false;
}
for (int c = 0; c < NumAMSCharacteristic; ++c) {
if (s_ams_client->characteristics[c] == characteristic) {
return true;
}
}
return false;
}
void ams_handle_subscribe(BLECharacteristic subscribed_characteristic,
BLESubscription subscription_type, BLEGATTError error) {
AMSCharacteristic characteristic_id = prv_get_id_for_characteristic(subscribed_characteristic);
if (characteristic_id != AMSCharacteristicEntityUpdate) {
// Only Entity Update characteristic is expected to be subscribed to
WTF;
}
if (error != BLEGATTErrorSuccess) {
prv_analytics_log_event_with_info(AMSAnalyticsEventErrorSubscribe, error);
PBL_LOG(LOG_LEVEL_ERROR, "Failed to subscribe AMS");
return;
}
PBL_LOG(LOG_LEVEL_INFO, "Hurray! AMS subscribed");
if (!prv_set_connected(true)) {
PBL_LOG(LOG_LEVEL_ERROR, "Another music service was already connected. Aborting AMS setup.");
return;
}
prv_register_next_entity(NULL);
}
void ams_handle_write_response(BLECharacteristic characteristic, BLEGATTError error) {
if (!s_ams_client) {
return;
}
const bool is_entity_update_characteristic =
(characteristic == s_ams_client->characteristics[AMSCharacteristicEntityUpdate]);
const bool has_error = (error != BLEGATTErrorSuccess);
if (has_error) {
const AMSAnalyticsEvent event = is_entity_update_characteristic ?
AMSAnalyticsEventErrorRegisterEntityWriteResponse :
AMSAnalyticsEventErrorOtherWriteResponse;
prv_analytics_log_event_with_info(event, error);
}
if (!is_entity_update_characteristic) {
// We only need to act upon getting a write response of the Entity Update characteristic.
// Just ignore write responses for the Remote Command characteristic.
return;
}
const AMSEntityID entity_id = s_ams_client->next_entity_to_register;
if (has_error) {
PBL_LOG(LOG_LEVEL_ERROR, "AMS Failed to register entity_id=%u: %u", entity_id, error);
// TODO: Log error event
// Don't retry here, chances of succeeding are slim.
return;
}
PBL_LOG(LOG_LEVEL_DEBUG, "AMS Registered for entity_id=%u", entity_id);
++s_ams_client->next_entity_to_register;
prv_register_next_entity(NULL);
}
void ams_handle_read_or_notification(BLECharacteristic characteristic, const uint8_t *value,
size_t value_length, BLEGATTError error) {
if (!s_ams_client ||
s_ams_client->characteristics[AMSCharacteristicEntityUpdate] != characteristic) {
PBL_LOG(LOG_LEVEL_ERROR, "Unexpected characteristic (s_ams_client=%p)", s_ams_client);
return;
}
PBL_HEXDUMP(LOG_LEVEL_DEBUG, value, value_length);
const AMSEntityUpdateNotification *update = (const AMSEntityUpdateNotification *)value;
prv_handle_update(update, value_length - sizeof(*update));
}
void ams_destroy(void) {
if (!s_ams_client) {
return;
}
prv_set_connected(false);
kernel_free(s_ams_client);
s_ams_client = NULL;
}
static void prv_send_command_kernel_main_task_cb(void *data) {
if (!s_ams_client) {
return;
}
const AMSRemoteCommandID command_id = (uintptr_t)data;
BLECharacteristic characteristic = s_ams_client->characteristics[AMSCharacteristicRemoteCommand];
BTErrno error = gatt_client_op_write(characteristic,
(const uint8_t *) &command_id, 1, GAPLEClientKernel);
const bool has_error = (error != BTErrnoOK);
if (has_error) {
PBL_LOG(LOG_LEVEL_ERROR, "Couldn't write command: %d", error);
prv_analytics_log_event_with_info(AMSAnalyticsEventErrorSendRemoteCommand, error);
}
}
void ams_send_command(AMSRemoteCommandID command_id) {
prv_perform_on_kernel_main_task(prv_send_command_kernel_main_task_cb,
(void *)(uintptr_t)command_id);
}
const char *ams_music_server_debug_name(void) {
return s_ams_music_implementation.debug_name;
}
bool ams_is_registered_for_all_entity_updates(void) {
if (!s_ams_client) {
return false;
}
return (s_ams_client->next_entity_to_register == AMSEntityIDInvalid);
}

View file

@ -0,0 +1,101 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "ams_types.h"
#include "applib/bluetooth/ble_client.h"
//! @file ams.h Module implementing an AMS client.
//! See http://bit.ly/ams-spec for Apple's documentation of AMS.
//!
//! @note Most of the functions must be called from KernelMain. Forcing all accesses to happen
//! from one task avoids the need for a mutex.
//! Enum indexing the AMS characteristics
//! @note The order is actually important for ams.c's implementation. Don't shuffle!
typedef enum {
//! Writable.
//! Used to send commands to the AMS.
//! @see AMSRemoteCommandID
AMSCharacteristicRemoteCommand = 0,
//! Writable w/o Response, Notifiable.
//! Used to register for attribute updates (by writing w/o response).
//! Also used to receive attribute updates (as GATT notifications).
AMSCharacteristicEntityUpdate = 1,
//! Writable, Readable.
//! @note Currently left unused. This characteristic is used to fetch a complete value,
//! in case it got truncated in the update notification.
AMSCharacteristicEntityAttribute = 2,
NumAMSCharacteristic,
AMSCharacteristicInvalid = NumAMSCharacteristic,
} AMSCharacteristic;
//! Creates the AMS client.
//! Must only be called from KernelMain!
void ams_create(void);
//! Updates the BLECharacteristic references, in case new ones have been obtained after a
//! re-discovery of the remote services.
//! @param characteristics Matrix of characteristics references of the AMS service
//! @note This module only uses the first service instance, any others will be ignored.
//! Must only be called from KernelMain!
void ams_handle_service_discovered(BLECharacteristic *characteristics);
void ams_invalidate_all_references(void);
void ams_handle_service_removed(BLECharacteristic *characteristics, uint8_t num_characteristics);
//! @param characteristic The characteristic for which to test whether the AMS module handles
//! reads/writes/notifications for it.
//! @return True whether the AMS module handles reads/writes/etc for it, false if not
bool ams_can_handle_characteristic(BLECharacteristic characteristic);
//! Handles GATT subscriptions
//! @see BLEClientSubscribeHandler
//! Must only be called from KernelMain!
void ams_handle_subscribe(BLECharacteristic characteristic,
BLESubscription subscription_type, BLEGATTError error);
//! Handles GATT write responses
//! @see BLEClientWriteHandler
//! Must only be called from KernelMain!
void ams_handle_write_response(BLECharacteristic characteristic, BLEGATTError error);
//! Handles GATT notifications
//! Must only be called from KernelMain!
void ams_handle_read_or_notification(BLECharacteristic characteristic, const uint8_t *value,
size_t value_length, BLEGATTError error);
//! Destroys the AMS client.
//! Must only be called from KernelMain!
void ams_destroy(void);
//! This function is exported only for (unit) testing purposes!
//! OK to call from any task.
void ams_send_command(AMSRemoteCommandID command_id);
//! For testing purposes.
//! @return The debug name with which AMS registers itself with the music.c service.
const char *ams_music_server_debug_name(void);
//! For testing purposes.
//! @return Whether AMS has registered itself for updates of all entities (Player, Queue and Track).
bool ams_is_registered_for_all_entity_updates(void);

View file

@ -0,0 +1,35 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
typedef enum {
AMSAnalyticsEventErrorReserved = 0,
// Lifecycle
AMSAnalyticsEventErrorDiscovery = 1,
AMSAnalyticsEventErrorSubscribe = 2,
AMSAnalyticsEventErrorMusicServiceConnect = 3,
AMSAnalyticsEventErrorRegisterEntityWrite = 4,
AMSAnalyticsEventErrorRegisterEntityWriteResponse = 5,
AMSAnalyticsEventErrorOtherWriteResponse = 6,
// Updates
AMSAnalyticsEventErrorPlayerVolumeUpdate = 7,
AMSAnalyticsEventErrorPlayerPlaybackInfoUpdate = 8,
AMSAnalyticsEventErrorPlayerPlaybackInfoFloatParse = 9,
AMSAnalyticsEventErrorTrackDurationUpdate = 10,
// Sending Remote Commands
AMSAnalyticsEventErrorSendRemoteCommand = 11,
} AMSAnalyticsEvent;

View file

@ -0,0 +1,46 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "ams.h"
//! AMS Service UUID - 89D3502B-0F36-433A-8EF4-C502AD55F8DC
static const Uuid s_ams_service_uuid = {
0x89, 0xD3, 0x50, 0x2B, 0x0F, 0x36, 0x43, 0x3A,
0x8E, 0xF4, 0xC5, 0x02, 0xAD, 0x55, 0xF8, 0xDC,
};
//! AMS Characteristic UUIDs
static const Uuid s_ams_characteristic_uuids[NumAMSCharacteristic] = {
//! Remote Command - 9B3C81D8-57B1-4A8A-B8DF-0E56F7CA51C2
[AMSCharacteristicRemoteCommand] = {
0x9B, 0x3C, 0x81, 0xD8, 0x57, 0xB1, 0x4A, 0x8A,
0xB8, 0xDF, 0x0E, 0x56, 0xF7, 0xCA, 0x51, 0xC2,
},
//! Entity Update - 2F7CABCE-808D-411F-9A0C-BB92BA96C102
[AMSCharacteristicEntityUpdate] = {
0x2F, 0x7C, 0xAB, 0xCE, 0x80, 0x8D, 0x41, 0x1F,
0x9A, 0x0C, 0xBB, 0x92, 0xBA, 0x96, 0xC1, 0x02,
},
//! Entity Attribute - C6B2F38C-23AB-46D8-A6AB-A3A870BBD5D7
[AMSCharacteristicEntityAttribute] = {
0xC6, 0xB2, 0xF3, 0x8C, 0x23, 0xAB, 0x46, 0xD8,
0xA6, 0xAB, 0xA3, 0xA8, 0x70, 0xBB, 0xD5, 0xD7,
},
};

View file

@ -0,0 +1,198 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "util/attributes.h"
#include "util/math.h"
#include <stdint.h>
//! Dumping ground for Apple Media Service types
//! All these values come from the Appendix in the specification:
//! https://developer.apple.com/library/ios/documentation/CoreBluetooth/Reference/AppleMediaService_Reference/Appendix/Appendix.html#//apple_ref/doc/uid/TP40014716-CH3-SW2
////////////////////////////////////////////////////////////////////////////////////////////////////
// Enumerations
//! When writing to any characteristic, or when reading the Entity Attribute,
//! the client may receive the following AMS-specific error codes:
typedef enum {
//! The MR has not properly set up the AMS, e.g. it wrote to the Entity Update or Entity
//! Attribute characteristic without subscribing to GATT notifications for the Entity Update
//! characteristic
AMSErrorInvalidState = 0xA0,
//! The command was improperly formatted.
AMSErrorInvalidCommand = 0xA1,
//! The corresponding attribute is empty.
AMSErrorAbsentAttributes = 0xA2,
} AMSError;
//! Command IDs that can be sent to the AMS
typedef enum {
AMSRemoteCommandIDPlay = 0,
AMSRemoteCommandIDPause = 1,
AMSRemoteCommandIDTogglePlayPause = 2,
AMSRemoteCommandIDNextTrack = 3,
AMSRemoteCommandIDPreviousTrack = 4,
AMSRemoteCommandIDVolumeUp = 5,
AMSRemoteCommandIDVolumeDown = 6,
AMSRemoteCommandIDAdvanceRepeatMode = 7,
AMSRemoteCommandIDAdvanceShuffleMode = 8,
AMSRemoteCommandIDSkipForward = 9,
AMSRemoteCommandIDSkipBackward = 10,
AMSRemoteCommandIDLike = 11,
AMSRemoteCommandIDDislike = 12,
AMSRemoteCommandIDBookmark = 13,
AMSRemoteCommandIDInvalid = 0xff,
} AMSRemoteCommandID;
//! Entity IDs to represent the entities on the AMS
typedef enum {
AMSEntityIDPlayer = 0,
AMSEntityIDQueue = 1,
AMSEntityIDTrack = 2,
NumAMSEntityID,
AMSEntityIDInvalid = NumAMSEntityID,
} AMSEntityID;
typedef enum {
AMSEntityUpdateFlagTruncated = (1 << 0),
AMSEntityUpdateFlagReserved = ~((1 << 1) - 1),
} AMSEntityUpdateFlag;
typedef enum {
//! A string containing the localized name of the app.
AMSPlayerAttributeIDName = 0,
//! A concatenation of three comma-separated values:
//! - PlaybackState as string (see AMSPlaybackState)
//! - PlaybackRate floating point as string
//! - ElapsedTime floating point as string
//! @see AMSPlaybackInfoIdx
AMSPlayerAttributeIDPlaybackInfo = 1,
//! Volume floating point as string, ranging from 0 (silent) to 1 (full volume)
AMSPlayerAttributeIDVolume = 2,
//! A string containing the bundle identifier of the app.
//! @note Available since iOS 8.3
AMSPlayerAttributeIDBundleIdentifier = 3,
NumAMSPlayerAttributeID,
} AMSPlayerAttributeID;
typedef enum {
AMSPlaybackInfoIdxState,
AMSPlaybackInfoIdxRate,
AMSPlaybackInfoIdxElapsedTime,
} AMSPlaybackInfoIdx;
typedef enum {
AMSPlaybackStatePaused = 0,
AMSPlaybackStatePlaying = 1,
AMSPlaybackStateRewinding = 2,
AMSPlaybackStateForwarding = 3,
} AMSPlaybackState;
typedef enum {
//! A string containing the integer value of the queue index, zero-based.
AMSQueueAttributeIDIndex = 0,
//! A string containing the integer value of the total number of items in the queue.
AMSQueueAttributeIDCount = 1,
//! A string containing the integer value of the shuffle mode. See AMSShuffleMode.
AMSQueueAttributeIDShuffleMode = 2,
//! A string containing the integer value value of the repeat mode. See AMSRepeatMode.
AMSQueueAttributeIDRepeatMode = 3,
NumAMSQueueAttributeID,
} AMSQueueAttributeID;
typedef enum {
AMSShuffleModeOff = 0,
AMSShuffleModeOne = 1,
AMSShuffleModeAll = 2,
} AMSShuffleMode;
typedef enum {
AMSRepeatModeOff = 0,
AMSRepeatModeOne = 1,
AMSRepeatModeAll = 2,
} AMSRepeatMode;
typedef enum {
//! A string containing the name of the artist.
AMSTrackAttributeIDArtist = 0,
//! A string containing the name of the album.
AMSTrackAttributeIDAlbum = 1,
//! A string containing the title of the track.
AMSTrackAttributeIDTitle = 2,
//! A string containing the floating point value of the total duration of the track in seconds.
AMSTrackAttributeIDDuration = 3,
NumAMSTrackAttributeID,
} AMSTrackAttributeID;
#define AMS_MAX_NUM_ATTRIBUTE_ID (MAX(MAX((int)NumAMSTrackAttributeID, \
(int)NumAMSQueueAttributeID), (int)NumAMSPlayerAttributeID))
////////////////////////////////////////////////////////////////////////////////////////////////////
// Packet Formats
//! Written (with Response) to the Remote Command characteristic,
//! to execute the specified command on the AMS.
typedef struct PACKED {
AMSRemoteCommandID command_id:8;
} AMSRemoteCommand;
//! Written (without Response) to the Entity Update characteristic,
//! to indicate that the client is interested in receiving updates for the specified entity
//! and attributes.
typedef struct PACKED {
AMSEntityID entity_id:8;
//! Array of Attribute IDs for which the client wants to receive updates.
//! Can be of type AMSPlayerAttributeID, AMSQueueAttributeID, AMSTrackAttributeID, depending on
//! the value of `entity_id`.
uint8_t attributes[];
} AMSEntityUpdateCommand;
//! Notification from the Entity Update characteristic,
//! sent to notify the client of an updated attribute value.
typedef struct PACKED {
AMSEntityID entity_id:8;
//! The Attribute ID of the updated value.
//! Can be of type AMSPlayerAttributeID, AMSQueueAttributeID, AMSTrackAttributeID, depending on
//! the value of `entity_id`.
uint8_t attribute_id;
AMSEntityUpdateFlag flags:8;
//! The updated value.
//! @note The string is never zero-terminated, so cannot be used as a C-string, as-is.
char value_str[];
} AMSEntityUpdateNotification;

View file

@ -0,0 +1,141 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#define FILE_LOG_COLOR LOG_COLOR_BLUE
#include "system/logging.h"
#include "ams_util.h"
#include <stdarg.h>
#include "util/math.h"
// -------------------------------------------------------------------------------------------------
// Parsing C-string with real number to an integer using a given multiplication factor
bool ams_util_float_string_parse(const char *number_str, uint32_t number_str_length,
int32_t multiplier, int32_t *number_out) {
if (!number_str || number_str_length == 0 || number_str[0] == 0 || multiplier == 0) {
return false;
}
const int32_t base = 10;
bool has_more = true;
bool is_negative = false;
bool number_started = false;
uint32_t decimal_divisor = 0;
int64_t result = 0;
const char * const number_str_end = number_str + number_str_length;
do {
const char c = *number_str;
switch (c) {
case '\0':
has_more = false;
break;
case '0' ... '9': {
number_started = true;
result *= base;
result += (c - '0') * multiplier;
if (decimal_divisor) {
decimal_divisor *= base;
}
break;
}
case '-': {
if (number_started || is_negative) {
return false; // Encountered minus in the middle of a number or multiple minus signs
}
is_negative = true;
break;
}
case ',':
case '.': {
number_started = true;
if (decimal_divisor) {
return false; // Encountered multiple separators
}
decimal_divisor = 1;
break;
}
default:
return false;
}
} while (has_more && (++number_str < number_str_end));
if (decimal_divisor > 1) {
result /= (decimal_divisor / base);
const bool round_up = ((ABS(result) % base) >= (base / 2));
result /= base;
if (round_up) {
if (result > 0) {
++result;
} else {
--result;
}
}
}
if (is_negative) {
result *= -1;
}
if (result > INT32_MAX || result < INT32_MIN) {
return false; // overflow
}
if (number_started && number_out) {
*number_out = result;
}
return number_started;
}
// -------------------------------------------------------------------------------------------------
// Parsing comma-separated value
uint8_t ams_util_csv_parse(const char *csv_value, uint32_t csv_length,
void *context, AMSUtilCSVCallback callback) {
if (csv_value == NULL || csv_length == 0) {
return 0;
}
uint8_t values_parsed_count = 0;
bool should_continue = true;
const char *value_begin = csv_value;
while (should_continue) {
uint32_t idx = 0;
const uint8_t end_idx = (csv_value + csv_length) - value_begin;
while (true) {
const char c = value_begin[idx];
const bool is_terminated = (c == '\0' || idx == end_idx);
if (c == ',' || is_terminated) {
should_continue = callback(value_begin, idx, values_parsed_count, context);
value_begin += idx + 1;
++values_parsed_count;
if (is_terminated) {
goto finally;
}
break;
}
++idx;
}
}
finally:
return values_parsed_count;
}

View file

@ -0,0 +1,56 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <stdbool.h>
#include <stdint.h>
//! Parses a string with a real number into int32_t, using given multiplication factor.
//! @param number_str The string containing a real, base-10 number. Some valid examples: "-1.234",
//! "42", "-.1", "1,0", "-0".
//! The string does not have to be zero-terminated, since the length is passed as an argument.
//! @param number_str_length The length of the number_str buffer.
//! @param multiplier The factor by which to multiply the parsed number.
//! @param[out] number_out If the parsing was succesfull, the result will be stored here.
//! @return True if the string was parsed succesfully.
//! @note The first comma or period found is treated as decimal separator. Any subsequent comma or
//! period that is found will cause parsing to be aborted and return false.
//! @note An empty / zero-length string still will fail to parse and return false.
//! @note When the input number multiplied by the multiplier overflows the output storage (int32_t)
//! the function will return false.
bool ams_util_float_string_parse(const char *number_str, uint32_t number_str_length,
int32_t multiplier, int32_t *number_out);
//! Value callback for use with ams_util_csv_parse
//! @param value The found value (not zero terminated!)
//! @param value_length The length of the found value in bytes
//! @param index The index of the value in the total CSV list
//! @param context User-specified callback, as passed into ams_util_csv_parse
//! @return True to continue parsing, false to stop parsing
typedef bool (*AMSUtilCSVCallback)(const char *value, uint32_t value_length,
uint32_t index, void *context);
//! Parses a comma separated value string.
//! @param csv_value The buffer with the CSV string. The string does not necessarily need to be
//! NULL-terminated.
//! @param csv_length The length in bytes of csv_value
//! @param context User context that will be passed into the callback
//! @param callback The function to call for each found value.
//! @return The number of parsed values. In case the number of values in csv_value is different from
//! the number of callbacks passed, only up to the smallest number will be parsed.
uint8_t ams_util_csv_parse(const char *csv_value, uint32_t csv_length,
void *context, AMSUtilCSVCallback callback);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,92 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "applib/bluetooth/ble_client.h"
//! @file ancs.h Module implementing an ANCS client.
//! See http://bit.ly/ancs-spec for Apple's documentation of ANCS
typedef enum {
ANCSClientStateIdle = 0,
ANCSClientStateRequestedNotification,
ANCSClientStateReassemblingNotification,
ANCSClientStatePerformingAction,
ANCSClientStateRequestedApp,
ANCSClientStateAliveCheck,
ANCSClientStateRetrying,
} ANCSClientState;
//! Enum indexing the ANCS characteristics
//! @note The order is actually important for ancs.c's implementation. Don't shuffle!
typedef enum {
// Subscribe-able:
ANCSCharacteristicNotification = 0, //<! Notification Source
ANCSCharacteristicData = 1, //<! Data Source
// Writable:
ANCSCharacteristicControl = 2, //<! Control Point
NumANCSCharacteristic,
ANCSCharacteristicInvalid = NumANCSCharacteristic,
} ANCSCharacteristic;
//! Creates the ANCS client.
//! Must only be called from KernelMain!
void ancs_create(void);
//! Updates the BLECharacteristic references, in case new ones have been obtained after a
//! re-discovery of the remote services.
//! @param characteristics Matrix of characteristics references of the ANCS service(s)
//! @note This module only uses the first service instance, any others will be ignored.
//! Must only be called from KernelMain!
void ancs_handle_service_discovered(BLECharacteristic *characteristics);
void ancs_invalidate_all_references(void);
void ancs_handle_service_removed(BLECharacteristic *characteristics, uint8_t num_characteristics);
//! @param characteristic The characteristic for which to test whether the ANCS module handles
//! reads/writes/notifications for it.
//! @return True whether the ANCS module handles reads/writes/etc for it, false if not
bool ancs_can_handle_characteristic(BLECharacteristic characteristic);
//! Handles GATT write responses
//! @see BLEClientWriteHandler
//! Must only be called from KernelMain!
void ancs_handle_write_response(BLECharacteristic characteristic, BLEGATTError error);
//! Handles GATT subscriptions
//! @see BLEClientSubscribeHandler
//! Must only be called from KernelMain!
void ancs_handle_subscribe(BLECharacteristic characteristic,
BLESubscription subscription_type, BLEGATTError error);
//! Handles GATT notifications
//! Must only be called from KernelMain!
void ancs_handle_read_or_notification(BLECharacteristic characteristic, const uint8_t *value,
size_t value_length, BLEGATTError error);
//! Destroys the ANCS client.
//! Must only be called from KernelMain!
void ancs_destroy(void);
//! This function is safe to call from any task.
void ancs_perform_action(uint32_t notification_uid, uint8_t action_id);
//! Called by kernel_le_client/dis/dis.c
void ancs_handle_ios9_or_newer_detected(void);

View file

@ -0,0 +1,108 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "ancs_app_name_storage.h"
#include <string.h>
#include "util/circular_cache.h"
#include "kernel/pbl_malloc.h"
#include "system/passert.h"
static const unsigned ANCS_APP_NAME_STORAGE_SIZE = 30;
static CircularCache *s_cache = NULL;
typedef struct {
ANCSAttribute *app_id;
ANCSAttribute *app_name;
} AncsAppNameStorageEntry;
static int prv_comparator(void *a, void *b) {
AncsAppNameStorageEntry *entry_a = (AncsAppNameStorageEntry *)a;
AncsAppNameStorageEntry *entry_b = (AncsAppNameStorageEntry *)b;
if (entry_a->app_id == NULL || entry_b->app_id == NULL) {
return -1;
}
if (entry_a->app_id->length != entry_b->app_id->length) {
return -1;
}
return (memcmp(entry_a->app_id->value, entry_b->app_id->value, entry_a->app_id->length));
}
static void prv_destructor(void *item) {
if (item) {
AncsAppNameStorageEntry *entry = item;
kernel_free(entry->app_id);
kernel_free(entry->app_name);
}
}
void ancs_app_name_storage_init(void) {
PBL_ASSERTN(!s_cache);
s_cache = kernel_zalloc_check(sizeof(*s_cache));
uint8_t *buffer =
kernel_zalloc_check(sizeof(AncsAppNameStorageEntry) * ANCS_APP_NAME_STORAGE_SIZE);
circular_cache_init(s_cache,
buffer,
sizeof(AncsAppNameStorageEntry),
ANCS_APP_NAME_STORAGE_SIZE,
prv_comparator);
circular_cache_set_item_destructor(s_cache, prv_destructor);
}
void ancs_app_name_storage_deinit(void) {
if (s_cache) {
circular_cache_flush(s_cache);
kernel_free(s_cache->cache);
kernel_free(s_cache);
s_cache = NULL;
}
}
void ancs_app_name_storage_store(const ANCSAttribute *app_id, const ANCSAttribute *app_name) {
if (!app_id || !app_name) {
return;
}
// copy app id
ANCSAttribute *app_id_copy = kernel_zalloc_check(sizeof(ANCSAttribute) + app_id->length);;
memcpy(app_id_copy, app_id, sizeof(ANCSAttribute) + app_id->length);
// copy app name
ANCSAttribute *app_name_copy = kernel_zalloc_check(sizeof(ANCSAttribute) + app_name->length);;
memcpy(app_name_copy, app_name, sizeof(ANCSAttribute) + app_name->length);
AncsAppNameStorageEntry entry = {
.app_id = app_id_copy,
.app_name = app_name_copy,
};
circular_cache_push(s_cache, &entry);
}
ANCSAttribute *ancs_app_name_storage_get(const ANCSAttribute *app_id) {
AncsAppNameStorageEntry entry = {
.app_id = (ANCSAttribute *)app_id,
};
AncsAppNameStorageEntry *found = circular_cache_get(s_cache, &entry);
if (found) {
return found->app_name;
} else {
return NULL;
}
}

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 "ancs_types.h"
//! Initialize app name storage and allocate cache
void ancs_app_name_storage_init(void);
//! Deinitialize app name storage and free cache
void ancs_app_name_storage_deinit(void);
//! Store an app name/app id pair in the app name cache
//! @param app_id the app ID attribute to use as key
//! @param app_name the app name attribute to store
//! @note will evict the oldest entry if the cache is full
void ancs_app_name_storage_store(const ANCSAttribute *app_id, const ANCSAttribute *app_name);
//! Retrieve an app name from storage.
//! @param app_id the app ID attribute used as key
//! @return an ANCSAttribute containing the name of the app, NULL if not found.
//! @note This returns a pointer to the data in the cache. If the cache gets invalidated
//! the pointer will no longer be valid. Copy the data somewhere if you want to keep it
//! around for a while.
ANCSAttribute *ancs_app_name_storage_get(const ANCSAttribute *app_id);

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 "ancs.h"
//! ANCS Service UUID
static const Uuid s_ancs_service_uuid = {
0x79, 0x05, 0xf4, 0x31, 0xb5, 0xce, 0x4e, 0x99,
0xa4, 0x0f, 0x4b, 0x1e, 0x12, 0x2d, 0x00, 0xd0
};
//! ANCS Characteristic UUIDs
static const Uuid s_ancs_characteristic_uuids[NumANCSCharacteristic] = {
[ANCSCharacteristicNotification] = {
0x9f, 0xbf, 0x12, 0x0d, 0x63, 0x01, 0x42, 0xd9,
0x8c, 0x58, 0x25, 0xe6, 0x99, 0xa2, 0x1d, 0xbd
},
[ANCSCharacteristicData] = {
0x22, 0xea, 0xc6, 0xe9, 0x24, 0xd6, 0x4b, 0xb5,
0xbe, 0x44, 0xb3, 0x6a, 0xce, 0x7c, 0x7b, 0xfb
},
[ANCSCharacteristicControl] = {
0x69, 0xd1, 0xd8, 0xf3, 0x45, 0xe1, 0x49, 0xa8,
0x98, 0x21, 0x9b, 0xbd, 0xfd, 0xaa, 0xd9, 0xd9
},
};

View file

@ -0,0 +1,244 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "util/attributes.h"
#include "util/pstring.h"
#include "util/size.h"
#include <inttypes.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
//! Dumping ground for ANCS types
//! Invalid ANCS UID. This is not officially invalid, but a representation is necessary, and this
//! is the most unlikely UID that an iOS device would use.
#define INVALID_UID UINT32_MAX
typedef bool (*AttrDictCompletePredicate)(const uint8_t* data, const size_t length, bool* out_error);
typedef enum {
EventIDNotificationAdded = 0,
EventIDNotificationModified = 1,
EventIDNotificationRemoved = 2,
} EventID;
typedef enum {
EventFlagSilent = (1 << 0),
EventFlagImportant = (1 << 1),
EventFlagPreExisting = (1 << 2),
EventFlagPositiveAction = (1 << 3),
EventFlagNegativeAction = (1 << 4),
EventFlagMultiMedia = (1 << 5),
EventFlagReserved = ~((1 << 6) - 1),
} EventFlags;
typedef enum {
ActionIDPositive = 0,
ActionIDNegative = 1,
} ActionId;
typedef enum {
CategoryIDOther = 0,
CategoryIDIncomingCall = 1,
CategoryIDMissedCall = 2,
CategoryIDVoicemail = 3,
CategoryIDSocial = 4,
CategoryIDSchedule = 5,
CategoryIDEmail = 6,
CategoryIDNews = 7,
CategoryIDHealthAndFitness = 8,
CategoryIDBusinessAndFinance = 9,
CategoryIDLocation = 10,
CategoryIDEntertainment = 11,
} CategoryID;
//! Notification Source's "Notification" format
typedef struct PACKED {
EventID event_id:8;
EventFlags event_flags:8;
CategoryID category_id:8;
uint8_t category_count; //<! FIXME PBL-1619: signed?
uint32_t uid;
} NSNotification;
typedef enum {
CommandIDGetNotificationAttributes = 0,
CommandIDGetAppAttributes = 1,
CommandIDPerformNotificationAction = 2,
CommandIdInvalid
} CommandID;
//! Header for Control Point (CP) and Data Source (DS) messages
typedef struct PACKED {
CommandID command_id:8;
uint8_t data[];
} CPDSMessage;
typedef struct PACKED {
CommandID command_id:8;
uint32_t notification_uid;
uint8_t attributes_data[];
} GetNotificationAttributesMsg;
typedef struct PACKED {
CommandID command_id:8;
char app_id[];
// uint8_t attributes_data[] follows after the zero-terminated app_id string,
// but it's not possible to express this in a C struct.
} GetAppAttributesMsg;
typedef struct PACKED {
CommandID command_id:8;
uint32_t notification_uid;
uint8_t action_id;
} PerformNotificationActionMsg;
typedef enum {
NotificationAttributeIDAppIdentifier = 0,
NotificationAttributeIDTitle = 1, //<! Must be followed by a 2-bytes max length param
NotificationAttributeIDSubtitle = 2, //<! Must be followed by a 2-bytes max length param
NotificationAttributeIDMessage = 3, //<! Must be followed by a 2-bytes max length param
NotificationAttributeIDMessageSize = 4,
NotificationAttributeIDDate = 5,
NotificationAttributeIDPositiveActionLabel = 6,
NotificationAttributeIDNegativeActionLabel = 7,
} NotificationAttributeID;
typedef enum {
AppAttributeIDDisplayName = 0,
} AppAttributeID;
typedef enum {
FetchedAttributeFlagOptional = (1 << 0),
} FetchedAttributeFlag;
typedef struct {
uint8_t id;
uint8_t max_length;
uint8_t flags;
} FetchedAttribute;
typedef enum {
FetchedNotifAttributeIndexAppID = 0,
FetchedNotifAttributeIndexTitle,
FetchedNotifAttributeIndexSubtitle,
FetchedNotifAttributeIndexMessage,
FetchedNotifAttributeIndexMessageSize,
FetchedNotifAttributeIndexDate,
FetchedNotifAttributeIndexPositiveActionLabel,
FetchedNotifAttributeIndexNegativeActionLabel,
} FetchedNotifAttributeIndex;
// FIXME AS: APP ID max length determined by looking through installed apps on iOS. Not sure what actual maximum is
#define APP_ID_MAX_LENGTH (60)
#define TITLE_MAX_LENGTH (40)
#define SUBTITLE_MAX_LENGTH (40)
#define MESSAGE_MAX_LENGTH (200)
#define MESSAGE_SIZE_MAX_LENGTH (3)
#define DATE_LENGTH (15)
#define ACTION_MAX_LENGTH (10)
#define MAX_NUM_ACTIONS (2)
#define NOTIFICATION_ATTRIBUTES_MAX_BUFFER_LENGTH \
(APP_ID_MAX_LENGTH + TITLE_MAX_LENGTH + SUBTITLE_MAX_LENGTH + \
MESSAGE_MAX_LENGTH + MESSAGE_SIZE_MAX_LENGTH + DATE_LENGTH + \
(ACTION_MAX_LENGTH * MAX_NUM_ACTIONS))
#define APP_DISPLAY_NAME_MAX_LENGTH (200)
static const FetchedAttribute s_fetched_notif_attributes[] = {
[FetchedNotifAttributeIndexAppID] = {
.id = NotificationAttributeIDAppIdentifier,
.flags = 0,
.max_length = 0
},
[FetchedNotifAttributeIndexTitle] = {
.id = NotificationAttributeIDTitle,
.flags = 0,
.max_length = TITLE_MAX_LENGTH
},
[FetchedNotifAttributeIndexSubtitle] = {
.id = NotificationAttributeIDSubtitle,
.flags = 0,
.max_length = SUBTITLE_MAX_LENGTH
},
[FetchedNotifAttributeIndexMessage] = {
.id = NotificationAttributeIDMessage,
.flags = 0,
.max_length = MESSAGE_MAX_LENGTH
},
[FetchedNotifAttributeIndexMessageSize] = {
.id = NotificationAttributeIDMessageSize,
.flags = FetchedAttributeFlagOptional,
.max_length = 0,
},
[FetchedNotifAttributeIndexDate] = {
.id = NotificationAttributeIDDate,
.flags = 0,
.max_length = DATE_LENGTH
},
[FetchedNotifAttributeIndexPositiveActionLabel] = {
.id = NotificationAttributeIDPositiveActionLabel,
.flags = FetchedAttributeFlagOptional,
.max_length = 0
},
[FetchedNotifAttributeIndexNegativeActionLabel] = {
.id = NotificationAttributeIDNegativeActionLabel,
.flags = FetchedAttributeFlagOptional,
.max_length = 0
},
};
#define NUM_FETCHED_NOTIF_ATTRIBUTES (ARRAY_LENGTH(s_fetched_notif_attributes))
typedef enum {
FetchedAppAttributeIndexDisplayName = 0,
} FetchedAppAttributeIndex;
static const FetchedAttribute s_fetched_app_attributes[] = {
[FetchedAppAttributeIndexDisplayName] = {
.id = AppAttributeIDDisplayName,
},
};
#define NUM_FETCHED_APP_ATTRIBUTES (ARRAY_LENGTH(s_fetched_app_attributes))
typedef struct PACKED {
uint8_t id;
union {
PascalString16 pstr;
struct {
uint16_t length;
uint8_t value[]; //<! Not null terminated!
};
};
} ANCSAttribute;
//! Enum with ANCS boolean properties
//! When a certain ANCS notification qualifies, it is passed along with relevant properties
//! These are for internal ANCS client use and not specified by the ANCS spec
typedef enum {
ANCSProperty_None = 0,
ANCSProperty_MissedCall = (1 << 0),
ANCSProperty_IncomingCall = (1 << 1),
ANCSProperty_VoiceMail = (1 << 2),
ANCSProperty_MultiMedia = (1 << 3),
ANCSProperty_iOS9 = (1 << 4),
} ANCSProperty;

View file

@ -0,0 +1,142 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "ancs_util.h"
#include "ancs_types.h"
#include "comm/ble/ble_log.h"
#include "syscall/syscall.h"
#include "system/hexdump.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/size.h"
#include <stdint.h>
bool ancs_util_is_complete_notif_attr_response(const uint8_t* data, const size_t length, bool* out_error) {
PBL_ASSERTN(out_error);
const size_t header_len = sizeof(GetNotificationAttributesMsg);
if (length > header_len) {
ANCSAttribute *attributes[NUM_FETCHED_NOTIF_ATTRIBUTES] = {};
bool complete = ancs_util_get_attr_ptrs(data + header_len, length - header_len,
s_fetched_notif_attributes,
ARRAY_LENGTH(s_fetched_notif_attributes),
attributes, out_error);
return complete;
}
return false;
}
bool ancs_util_is_complete_app_attr_dict(const uint8_t* data, size_t length, bool* out_error) {
PBL_ASSERTN(out_error);
// Search for end of the App ID before checking that all attributes are present.
while (length > 0) {
length--;
if (*data++ == 0) {
break;
}
}
if (length == 0) {
*out_error = false;
return false;
}
return ancs_util_get_attr_ptrs(data, length,
s_fetched_app_attributes,
ARRAY_LENGTH(s_fetched_app_attributes),
NULL, out_error);
}
bool ancs_util_get_attr_ptrs(const uint8_t* data, const size_t length, const FetchedAttribute* attr_list,
const int num_attrs, ANCSAttribute *out_attr_ptrs[], bool* out_error) {
PBL_ASSERTN(out_error);
*out_error = false;
const uint8_t* iter = data;
if (length < sizeof(ANCSAttribute)) {
PBL_LOG(LOG_LEVEL_INFO, "ANCS data length is too small. Length: %d, sizeof(ANCSAttribute): %d",
(int)length, (int)sizeof(ANCSAttribute));
*out_error = true;
return false;
}
bool attrs_found[num_attrs];
memset(attrs_found, 0, sizeof(attrs_found));
bool extracted_complete_attribute = false;
// Iterate over the contents of the buffer
while ((iter + sizeof(ANCSAttribute)) <= (data + length)) {
ANCSAttribute* attr = (ANCSAttribute*) iter;
const uint8_t* next_iter = (uint8_t*) attr->value + attr->length;
// Match this attribute with its entry in the FetchedNotifAttribute list
bool is_found = false;
for (int i = 0; i < num_attrs; ++i) {
is_found = (attr->id == attr_list[i].id);
if (is_found) {
// Check that attribute length is valid
bool attr_length_invalid = (attr_list[i].max_length != 0) && (attr->length > attr_list[i].max_length);
if (attr_length_invalid) {
PBL_LOG(LOG_LEVEL_INFO, "Length of ANCS attribute %d is invalid: length: %d, max_length: %d",
attr->id, attr->length, attr_list[i].max_length);
*out_error = true;
return false;
}
attrs_found[i] = true;
if (out_attr_ptrs) {
out_attr_ptrs[i] = (ANCSAttribute *) attr;
}
break;
}
}
if (!is_found) {
// The attribute was unexpected, the dictionary is malformed
PBL_LOG(LOG_LEVEL_INFO, "Unexpected ANCS attribute. ID = %d. The dictionary is malformed",
attr->id);
*out_error = true;
return false;
}
extracted_complete_attribute = ((uint8_t*)attr->value + attr->length <= data + length);
iter = next_iter;
}
// The dictionary was well-formed, all the attributes found so far are ones
// that were in the FetchedNotifAttribute list
// Check if there are any outstanding attributes that have not been found
for (int i = 0; i < num_attrs; ++i) {
const bool optional = (attr_list[i].flags & FetchedAttributeFlagOptional);
if (optional) {
continue;
}
if (!attrs_found[i]) {
return false;
}
}
return extracted_complete_attribute;
}

View file

@ -0,0 +1,38 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "ancs_types.h"
#include "services/normal/notifications/notifications.h"
#include <stdbool.h>
int ancs_util_get_notif_attr_response_len(const uint8_t* data, const size_t length);
//! Helper functions that wrap ancs_util_get_attrs, checking if all respective attr list
//! parameters were found
bool ancs_util_is_complete_notif_attr_response(const uint8_t* data, const size_t length, bool* out_error);
bool ancs_util_is_complete_app_attr_dict(const uint8_t* data, const size_t length, bool* out_error);
//! Extract pointers to the start of each attribute in attr_list
//! @param out_error Set if the dictionary was invalid and could not be parsed;
//! if true, bail out!
//! @return True if all requested attributes are present and complete; false if
//! one or more attributes are missing or the last attribute is truncated.
bool ancs_util_get_attr_ptrs(const uint8_t* data, const size_t length, const FetchedAttribute* attr_list,
const int num_attrs, ANCSAttribute *out_attr_ptrs[], bool* out_error);

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.
*/
#define FILE_LOG_COLOR LOG_COLOR_BLUE
#include "app_launch.h"
#include "comm/ble/gatt_client_operations.h"
#include "services/common/analytics/analytics.h"
#include "services/common/analytics/analytics_event.h"
#include "services/common/comm_session/session.h"
#include "system/logging.h"
#include "system/passert.h"
//! See https://pebbletechnology.atlassian.net/wiki/display/DEV/Pebble+GATT+Services
// -------------------------------------------------------------------------------------------------
// Static variables
static BLECharacteristic s_app_launch_characteristic = BLE_CHARACTERISTIC_INVALID;
// -------------------------------------------------------------------------------------------------
void app_launch_handle_service_discovered(BLECharacteristic *characteristics) {
PBL_ASSERTN(characteristics);
if (s_app_launch_characteristic != BLE_CHARACTERISTIC_INVALID) {
PBL_LOG(LOG_LEVEL_WARNING, "Multiple app launch services!? Will use most recent one.");
}
s_app_launch_characteristic = *characteristics;
// If there was no system session, try launching the Pebble app:
if (!comm_session_get_system_session()) {
app_launch_trigger();
}
}
void app_launch_invalidate_all_references(void) {
s_app_launch_characteristic = BLE_CHARACTERISTIC_INVALID;
}
void app_launch_handle_service_removed(
BLECharacteristic *characteristics, uint8_t num_characteristics) {
app_launch_invalidate_all_references();
}
// -------------------------------------------------------------------------------------------------
bool app_launch_can_handle_characteristic(BLECharacteristic characteristic) {
return (characteristic == s_app_launch_characteristic);
}
// -------------------------------------------------------------------------------------------------
void app_launch_handle_read_or_notification(BLECharacteristic characteristic, const uint8_t *value,
size_t value_length, BLEGATTError error) {
// If error is BLEGATTErrorSuccess, it means the Pebble app responded.
PBL_LOG(LOG_LEVEL_INFO, "App relaunch result: %u", error);
if (error == BLEGATTErrorSuccess) {
analytics_inc(ANALYTICS_DEVICE_METRIC_BT_PEBBLE_APP_LAUNCH_SUCCESS_COUNT,
AnalyticsClient_System);
} else {
analytics_event_bt_app_launch_error(error);
}
}
// -------------------------------------------------------------------------------------------------
void app_launch_handle_disconnection(void) {
s_app_launch_characteristic = BLE_CHARACTERISTIC_INVALID;
}
// -------------------------------------------------------------------------------------------------
void app_launch_trigger(void) {
if (s_app_launch_characteristic == BLE_CHARACTERISTIC_INVALID) {
return;
}
BTErrno err = gatt_client_op_read(s_app_launch_characteristic, GAPLEClientKernel);
if (err != BTErrnoOK) {
PBL_LOG(LOG_LEVEL_ERROR, "App relaunch failed: %u", err);
}
}

View file

@ -0,0 +1,40 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "applib/bluetooth/ble_client.h"
typedef enum {
AppLaunchCharacteristicAppLaunch,
AppLaunchCharacteristicNum
} AppLaunchCharacteristic;
void app_launch_handle_service_discovered(BLECharacteristic *characteristics);
void app_launch_invalidate_all_references(void);
void app_launch_handle_service_removed(
BLECharacteristic *characteristics, uint8_t num_characteristics);
bool app_launch_can_handle_characteristic(BLECharacteristic characteristic);
void app_launch_handle_read_or_notification(BLECharacteristic characteristic, const uint8_t *value,
size_t value_length, BLEGATTError error);
void app_launch_handle_disconnection(void);
void app_launch_trigger(void);

View file

@ -0,0 +1,31 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "app_launch.h"
#include <bluetooth/pebble_bt.h>
static const Uuid s_app_launch_service_uuid = {
PEBBLE_BT_UUID_EXPAND(PEBBLE_BT_APP_LAUNCH_SERVICE_UUID_32BIT)
};
static const Uuid s_app_launch_characteristic_uuids[AppLaunchCharacteristicNum] = {
[AppLaunchCharacteristicAppLaunch] = {
PEBBLE_BT_UUID_EXPAND(PEBBLE_BT_APP_LAUNCH_CHARACTERISTIC_UUID_32BIT),
},
};

View file

@ -0,0 +1,42 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "dis.h"
#include "comm/ble/ble_log.h"
#include "comm/ble/gap_le_connection.h"
#include "comm/ble/kernel_le_client/ancs/ancs.h"
#include "comm/bt_lock.h"
#include "system/logging.h"
#include "system/passert.h"
// -------------------------------------------------------------------------------------------------
// Interface towards kernel_le_client.c
void dis_invalidate_all_references(void) {
}
void dis_handle_service_removed(BLECharacteristic *characteristics, uint8_t num_characteristics) {
// dis_service_discovered doesn't get set to false here, since services can temporarily disappear
// and we're just using this to detect whether or not we're on iOS 9
}
void dis_handle_service_discovered(BLECharacteristic *characteristics) {
BLE_LOG_DEBUG("In DIS service discovery CB");
PBL_ASSERTN(characteristics);
ancs_handle_ios9_or_newer_detected();
}

View file

@ -0,0 +1,40 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "applib/bluetooth/ble_client.h"
//! @file dis.h Module implementing an DIS client.
//! See https://developer.bluetooth.org/TechnologyOverview/Pages/DIS.aspx
//! Enum indexing the DIS characteristics
typedef enum {
// We need at least one characteristic to look up the GAPLEConnection & flag the presence of DIS
// since Apple doesn't expose the SW version yet
DISCharacteristicManufacturerNameString = 0,
NumDISCharacteristic,
DISCharacteristicInvalid = NumDISCharacteristic,
} DISCharacteristic;
//! Updates the /ref GAPLEConnection to register that the DIS service has been discovered
//! @param characteristics Matrix of characteristics references of the DIS service
void dis_handle_service_discovered(BLECharacteristic *characteristics);
void dis_invalidate_all_references(void);
void dis_handle_service_removed(BLECharacteristic *characteristics, uint8_t num_characteristics);

View file

@ -0,0 +1,34 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "dis.h"
#include <btutil/bt_device.h>
//! DIS Service UUID - 0000180A-0000-1000-8000-00805F9B34FB
static const Uuid s_dis_service_uuid = {
BT_UUID_EXPAND(0x180A)
};
//! DIS Characteristic UUIDs
static const Uuid s_dis_characteristic_uuids[NumDISCharacteristic] = {
//! Manufacturer Name String - 00002A29-0000-1000-8000-00805F9B34FB
[DISCharacteristicManufacturerNameString] = {
BT_UUID_EXPAND(0x2A29)
},
};

View file

@ -0,0 +1,584 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "kernel_le_client.h"
#include "ancs/ancs_definition.h"
#include "ams/ams_definition.h"
#include "app_launch/app_launch_definition.h"
#include "dis/dis_definition.h"
#include "ppogatt/ppogatt_definition.h"
#if UNITTEST
#include "test/test_definition.h"
#endif
#include "comm/bt_conn_mgr.h"
#include "comm/bt_lock.h"
#include "kernel/event_loop.h"
#include "kernel/pbl_malloc.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/likely.h"
#include "util/size.h"
#include "comm/ble/gap_le_advert.h"
#include "comm/ble/gap_le_connect.h"
#include "comm/ble/gap_le_connection.h"
#include "comm/ble/gap_le_slave_reconnect.h"
#include "comm/ble/gatt_client_accessors.h"
#include "comm/ble/gatt_client_discovery.h"
#include "comm/ble/gatt_client_operations.h"
#include "comm/ble/gatt_client_subscriptions.h"
#include <bluetooth/classic_connect.h>
#include <bluetooth/pebble_bt.h>
#include <bluetooth/reconnect.h>
#define MAX_SERVICE_INSTANCES (8)
//! Array indices for the different client "classes"
enum {
#if UNITTEST
KernelLEClientUnitTest = 0,
#else
KernelLEClientPPoGATT = 0,
KernelLEClientANCS,
KernelLEClientAMS,
KernelLEClientAppLaunch,
KernelLEClientDIS,
#endif
KernelLEClientNum,
};
typedef struct {
//! Name of the GATT profile that will be used in debug logs
const char * const debug_name;
//! The Service UUID of the remote GATT service
const Uuid * const service_uuid;
//! Array of Characteristic UUIDs that are expected to be part of the remote GATT service
const Uuid * const characteristic_uuids;
//! The number of elements in the `characteristic_uuids` array
const uint8_t num_characteristics;
//! Callback executed every time a BT LE service matching 'service_uuid' is discovered
//!
//! @param characteristics - handles for the characteristics discovered.
//! The array will be 'num_characteristics' size and ordered the same way
//! as the characteristics_uuids array provided
void (*handle_service_discovered)(BLECharacteristic *characteristics);
//! Callback executed every time a BT LE service matching 'service_uuid' is removed
//!
//! @param characteristics - An array of all the characteristic handles that
//! have been invalidated
//! @param num_characteristics - The length of the array
void (*handle_service_removed)(BLECharacteristic *characteristics, uint8_t num_characteristics);
//! Invoked when all handles should be flushed by the connection
//! (events such as a disconnect or full re-discovery will trigger this)
void (*invalidate_all_references)(void);
//! Function that is called to test whether the client handles the characteristic, in which case
//! write/read responses/notifications will be routed to this client (can be NULL)
bool (*can_handle_characteristic)(BLECharacteristic characteristic);
//! Handler for GATT read responses and notifications / indications (can be NULL)
void (*handle_read_or_notification)(BLECharacteristic characteristic, const uint8_t *value,
size_t value_length, BLEGATTError error);
//! Handler for GATT write responses (can be NULL)
void (*handle_write_response)(BLECharacteristic characteristic, BLEGATTError error);
//! Handler for GATT subscription confirmations (can be NULL)
void (*handle_subscribe)(BLECharacteristic subscribed_characteristic,
BLESubscription subscription_type, BLEGATTError error);
} KernelLEClient;
static const KernelLEClient s_clients[KernelLEClientNum] = {
#if UNITTEST
[KernelLEClientUnitTest] = {
.debug_name = "TEST",
.service_uuid = &s_test_service_uuid,
.characteristic_uuids = s_test_characteristic_uuids,
.num_characteristics = TestCharacteristicCount,
.handle_service_discovered = test_client_handle_service_discovered,
.handle_service_removed = test_client_handle_service_removed,
.invalidate_all_references = test_client_invalidate_all_references,
.can_handle_characteristic = test_client_can_handle_characteristic,
.handle_write_response = test_client_handle_write_response,
.handle_subscribe = test_client_handle_subscribe,
.handle_read_or_notification = test_client_handle_read_or_notification,
},
#else
[KernelLEClientPPoGATT] = {
.debug_name = "PPoG",
.service_uuid = &s_ppogatt_service_uuid,
.characteristic_uuids = s_ppogatt_characteristic_uuids,
.num_characteristics = PPoGATTCharacteristicNum,
.handle_service_discovered = ppogatt_handle_service_discovered,
.handle_service_removed = ppogatt_handle_service_removed,
.invalidate_all_references = ppogatt_invalidate_all_references,
.can_handle_characteristic = ppogatt_can_handle_characteristic,
.handle_write_response = NULL,
.handle_subscribe = ppogatt_handle_subscribe,
.handle_read_or_notification = ppogatt_handle_read_or_notification,
},
[KernelLEClientANCS] = {
.debug_name = "ANCS",
.service_uuid = &s_ancs_service_uuid,
.characteristic_uuids = s_ancs_characteristic_uuids,
.num_characteristics = NumANCSCharacteristic,
.handle_service_discovered = ancs_handle_service_discovered,
.handle_service_removed = ancs_handle_service_removed,
.invalidate_all_references = ancs_invalidate_all_references,
.can_handle_characteristic = ancs_can_handle_characteristic,
.handle_write_response = ancs_handle_write_response,
.handle_subscribe = ancs_handle_subscribe,
.handle_read_or_notification = ancs_handle_read_or_notification,
},
[KernelLEClientAMS] = {
.debug_name = "AMS",
.service_uuid = &s_ams_service_uuid,
.characteristic_uuids = s_ams_characteristic_uuids,
.num_characteristics = NumAMSCharacteristic,
.handle_service_discovered = ams_handle_service_discovered,
.handle_service_removed = ams_handle_service_removed,
.invalidate_all_references = ams_invalidate_all_references,
.can_handle_characteristic = ams_can_handle_characteristic,
.handle_write_response = ams_handle_write_response,
.handle_subscribe = ams_handle_subscribe,
.handle_read_or_notification = ams_handle_read_or_notification,
},
[KernelLEClientAppLaunch] = {
.debug_name = "Lnch",
.service_uuid = &s_app_launch_service_uuid,
.characteristic_uuids = s_app_launch_characteristic_uuids,
.num_characteristics = AppLaunchCharacteristicNum,
.handle_service_discovered = app_launch_handle_service_discovered,
.handle_service_removed = app_launch_handle_service_removed,
.invalidate_all_references = app_launch_invalidate_all_references,
.can_handle_characteristic = app_launch_can_handle_characteristic,
.handle_read_or_notification = app_launch_handle_read_or_notification,
},
[KernelLEClientDIS] = {
.debug_name = "DIS",
.service_uuid = &s_dis_service_uuid,
.characteristic_uuids = s_dis_characteristic_uuids,
.num_characteristics = NumDISCharacteristic,
.handle_service_discovered = dis_handle_service_discovered,
.handle_service_removed = dis_handle_service_removed,
.invalidate_all_references = dis_invalidate_all_references,
.can_handle_characteristic = NULL,
.handle_read_or_notification = NULL,
},
#endif // UNITTEST
};
// Disconnect BT Classic (for iAP) if connected and make this LE device the active gateway,
// to prevent that iAP gets reconnected in the future:
static void prv_set_active_gateway_and_disconn_bt_classic(const BTDeviceInternal *gateway_device) {
BTBondingID bonding_id = BT_BONDING_ID_INVALID;
bt_lock();
// Find the Bonding ID for the LE connection that supports PPoGATT:
GAPLEConnection *connection = gap_le_connection_by_device(gateway_device);
// It's possible the connection is gone in the mean time; this runs on KernelMain.
if (connection) {
bonding_id = connection->bonding_id;
}
bt_unlock();
// don't hold bt_lock while calling bt_persistent_storage_... because it accesses flash
if (bonding_id != BT_BONDING_ID_INVALID) {
bt_persistent_storage_set_active_gateway(bonding_id);
} else {
PBL_LOG(LOG_LEVEL_ERROR, "Not bonded or disconnected (%p)", connection);
}
bt_lock();
bt_driver_classic_disconnect(NULL);
bt_unlock();
}
static void prv_handle_services_removed(PebbleBLEGATTClientServicesRemoved *services_removed) {
PebbleBLEGATTClientServiceHandles *service_remove_info = &services_removed->handles[0];
for (int s = 0; s < services_removed->num_services_removed; s++) {
bool removed = false;
for (int c = 0; c < KernelLEClientNum; c++) {
const KernelLEClient * const client = &s_clients[c];
if (uuid_equal(&service_remove_info->uuid, client->service_uuid)) {
removed = true;
client->handle_service_removed(
(BLECharacteristic *)&service_remove_info->char_and_desc_handles[0],
service_remove_info->num_characteristics);
}
}
#if !RELEASE
char uuid_string[UUID_STRING_BUFFER_LENGTH];
uuid_to_string(&service_remove_info->uuid, uuid_string);
PBL_LOG(LOG_LEVEL_INFO, "%s removed: %d", uuid_string, (int)removed);
#endif
int num_hdls = service_remove_info->num_descriptors +
service_remove_info->num_characteristics;
service_remove_info =
(PebbleBLEGATTClientServiceHandles *)&service_remove_info->char_and_desc_handles[num_hdls];
}
}
static void prv_handle_all_services_invalidated(void) {
for (int c = 0; c < KernelLEClientNum; c++) {
const KernelLEClient * const client = &s_clients[c];
client->invalidate_all_references();
}
}
static void prv_handle_services_added(
PebbleBLEGATTClientServicesAdded *added_services, BTDeviceInternal *device) {
// loop through the new services
for (int s = 0; s < added_services->num_services_added; s++) {
// get the uuid for the service
Uuid service_uuid = gatt_client_service_get_uuid(added_services->services[s]);
// are any clients looking for this uuid?
for (int c = 0; c < KernelLEClientNum; c++) {
const KernelLEClient * const client = &s_clients[c];
if (!uuid_equal(&service_uuid, (const Uuid *)client->service_uuid)) {
continue;
}
// We have found a service that a client is looking for. Make sure the
// characteristics we want are present and if so notify the interested client about it
BLECharacteristic characteristics[client->num_characteristics];
const uint8_t num_characteristics =
gatt_client_service_get_characteristics_matching_uuids(
added_services->services[s], &characteristics[0], client->characteristic_uuids,
client->num_characteristics);
if (num_characteristics != client->num_characteristics) {
PBL_LOG(LOG_LEVEL_ERROR, "Found %s, but only %u characteristics...",
client->debug_name, num_characteristics);
continue;
}
#if 0 // TODO: PBL-21864 - Disconnect BT Classic when PPoGATT is used
if (c == KernelLEClientPPoGATT) {
prv_set_active_gateway_and_disconn_bt_classic(&device);
}
#endif
#if !UNITTEST
ATTHandleRange range = { };
gatt_client_service_get_handle_range(added_services->services[s], &range);
if (c == KernelLEClientPPoGATT) {
// We are trying to track down an issue on iOS where PPoGATT doesn't get opened (PBL-40084)
// This message should help us determine if iOS is publishing the service
PBL_LOG(LOG_LEVEL_INFO, "Found an instance of %s at 0x%"PRIx16"-0x%"PRIx16"!",
client->debug_name, range.start, range.end);
} else {
PBL_LOG(LOG_LEVEL_DEBUG, "Found an instance of %s at 0x%"PRIx16"-0x%"PRIx16"!",
client->debug_name, range.start, range.end);
}
#endif
client->handle_service_discovered(characteristics);
}
}
}
static void prv_handle_gatt_service_discovery_event(const PebbleBLEGATTClientServiceEvent *event) {
PebbleBLEGATTClientServiceEventInfo *event_info = event->info;
if (event_info->status == BTErrnoServiceDiscoveryDisconnected) {
// TODO: In the past we'd disconnect when service discovery
// failed (not due to a disconnection)
return;
}
if (event_info->status != BTErrnoServiceDiscoveryDatabaseChanged &&
event_info->status != BTErrnoOK) {
// gatt_client_discovery.c already logs errors for this condition
return;
}
if (event_info->type != PebbleServicesRemoved) {
// For removals, we log info in the handler routine
PBL_LOG(LOG_LEVEL_INFO, "Service changed Indication: type: %d status: %d",
event_info->type, event_info->status);
}
switch (event_info->type) {
case PebbleServicesRemoved:
prv_handle_services_removed(&event_info->services_removed_data);
break;
case PebbleServicesInvalidateAll:
prv_handle_all_services_invalidated();
break;
case PebbleServicesAdded:
prv_handle_services_added(&event_info->services_added_data, &event_info->device);
break;
default:
WTF;
}
}
static const KernelLEClient * prv_client_for_characteristic(BLECharacteristic characteristic) {
for (int c = 0; c < KernelLEClientNum; ++c) {
const KernelLEClient * const client = &s_clients[c];
if (client->can_handle_characteristic && client->can_handle_characteristic(characteristic)) {
return client;
}
}
return NULL;
}
typedef void (*ConsumeFuncPtr)(BLECharacteristic characteristic_ref,
uint8_t *value_out, uint16_t value_length, GAPLEClient client);
typedef void (*ReadNotifyHandler)(BLECharacteristic characteristic, const uint8_t *value,
size_t value_length, BLEGATTError error);
static void prv_consume_read_response(const PebbleBLEGATTClientEvent *event,
const KernelLEClient *client) {
const uint16_t value_length = event->value_length;
uint8_t *buffer = NULL;
if (value_length) {
// This is ugly and causes double-copying the data...
// TODO: https://pebbletechnology.atlassian.net/browse/PBL-14164
buffer = (uint8_t *) kernel_malloc(value_length);
if (UNLIKELY(!buffer)) {
PBL_LOG(LOG_LEVEL_ERROR, "OOM for GATT read response - %d bytes", (int)value_length);
return;
}
gatt_client_consume_read_response(event->object_ref,
buffer, value_length, GAPLEClientKernel);
}
if (client->handle_read_or_notification) {
client->handle_read_or_notification(event->object_ref, buffer,
value_length, event->gatt_error);
}
kernel_free(buffer);
}
static void prv_consume_notifications(const PebbleBLEGATTClientEvent *event) {
GATTBufferedNotificationHeader header = {};
bool has_more = gatt_client_subscriptions_get_notification_header(GAPLEClientKernel, &header);
const RtcTicks start_ticks = rtc_get_ticks();
while (has_more) {
const uint32_t ticks_spent = rtc_get_ticks() - start_ticks;
// Don't spend more than ~33ms (or one 30fps animation frame interval) processing the pending
// GATT notifications:
if (ticks_spent >= ((RTC_TICKS_HZ * 33) / 1000)) {
// Doing this might actually cause an issue if the characteristic(s) for which there are still
// notifications pending in the buffer become invalid before the time they are processed.
// Probably not a big deal.
gatt_client_subscriptions_reschedule(GAPLEClientKernel);
return; // yield
}
// This is ugly and causes double-copying the data...
// TODO: https://pebbletechnology.atlassian.net/browse/PBL-14164
uint8_t *buffer = (uint8_t *) kernel_malloc(header.value_length);
if (UNLIKELY(header.value_length && !buffer)) {
PBL_LOG(LOG_LEVEL_ERROR, "OOM for GATT notification");
return;
}
const uint16_t next_value_length =
gatt_client_subscriptions_consume_notification(&header.characteristic,
buffer, &header.value_length,
GAPLEClientKernel, &has_more);
const KernelLEClient * const client = prv_client_for_characteristic(header.characteristic);
if (client->handle_read_or_notification) {
client->handle_read_or_notification(header.characteristic, buffer, header.value_length,
BLEGATTErrorSuccess);
} else {
PBL_LOG(LOG_LEVEL_DEBUG, "No client to handle GATT notification from characteristic %p",
(void*) header.characteristic);
}
kernel_free(buffer);
header.value_length = next_value_length;
}
}
static void prv_handle_gatt_event(const PebbleBLEGATTClientEvent *event) {
if (event->subtype == PebbleBLEGATTClientEventTypeBufferEmpty) {
// Taking a shortcut here:
ppogatt_handle_buffer_empty();
return;
} else if (event->subtype == PebbleBLEGATTClientEventTypeNotification) {
prv_consume_notifications(event);
return;
}
const KernelLEClient * const client = prv_client_for_characteristic(event->object_ref);
if (!client) {
// Read responses still need to be consumed, even if the client has disappeared:
if (event->subtype == PebbleBLEGATTClientEventTypeCharacteristicRead && event->value_length) {
gatt_client_consume_read_response(event->object_ref,
NULL, event->value_length, GAPLEClientKernel);
}
goto log_error;
}
switch (event->subtype) {
case PebbleBLEGATTClientEventTypeCharacteristicWrite:
if (!client->handle_write_response) {
goto log_error;
}
client->handle_write_response(event->object_ref, event->gatt_error);
return;
case PebbleBLEGATTClientEventTypeCharacteristicSubscribe:
if (!client->handle_subscribe) {
goto log_error;
}
client->handle_subscribe(event->object_ref, event->subscription_type, event->gatt_error);
return;
case PebbleBLEGATTClientEventTypeCharacteristicRead:
if (!client->handle_read_or_notification) {
goto log_error;
}
prv_consume_read_response(event, client);
return;
default:
break;
}
log_error:
PBL_LOG(LOG_LEVEL_ERROR,
"Unhandled GATT event:%u ref:%"PRIu32" err:%"PRIu16" len:%"PRIu16" cl:%p",
event->subtype,
(uint32_t)event->object_ref,
(uint16_t)event->gatt_error,
(uint16_t)event->value_length,
client);
}
static void prv_handle_connection_event(const PebbleBLEConnectionEvent *event) {
PBL_LOG(LOG_LEVEL_DEBUG, "PEBBLE_BLE_CONNECTION_EVENT: reason=0x%x, conn=%u, bond=%u",
event->hci_reason, event->connected, event->bonding_id);
const bool connected = event->connected;
// FIXME: When PPoGATT is supported add a check for active gateway
// https://pebbletechnology.atlassian.net/browse/PBL-15277
//
// For now, we just assume that the Kernel LE client is _always_ bonded for
// ANCS. Note that we cannot use bt_persistent_storage calls in this routine because
// we could be getting this call as a result of a disconnect due to
// forgetting a pairing key
const BTDeviceInternal device = PebbleEventToBTDeviceInternal(event);
if (connected) {
PBL_LOG(LOG_LEVEL_DEBUG, "Connected to Gateway!");
ancs_create();
ams_create();
ppogatt_create();
gap_le_slave_reconnect_stop();
gatt_client_discovery_discover_all(&device);
const bool gateway_is_classic_paired = true; // TODO
if (gateway_is_classic_paired) {
// [MT] Kick reconnection for BT Classic when BLE comes up.
// If BLE is able to reconnect, chances are BT Classic is able too, so try
// immediately instead of waiting for reconnect.c's timer to fire.
bt_driver_reconnect_try_now(false /*ignore_paused*/);
}
} else {
PBL_LOG(LOG_LEVEL_DEBUG, "Disconnected from Gateway!");
ppogatt_destroy();
ams_destroy();
ancs_destroy();
app_launch_handle_disconnection();
gap_le_slave_reconnect_start();
gatt_client_op_cleanup(GAPLEClientKernel);
}
}
// -------------------------------------------------------------------------------------------------
void kernel_le_client_handle_event(const PebbleEvent *e) {
switch (e->type) {
case PEBBLE_BLE_SCAN_EVENT:
PBL_LOG(LOG_LEVEL_DEBUG, "PEBBLE_BLE_SCAN_EVENT");
return;
case PEBBLE_BLE_CONNECTION_EVENT:
prv_handle_connection_event(&e->bluetooth.le.connection);
return;
case PEBBLE_BLE_GATT_CLIENT_EVENT:
if (e->bluetooth.le.gatt_client.subtype == PebbleBLEGATTClientEventTypeServiceChange) {
prv_handle_gatt_service_discovery_event(&e->bluetooth.le.gatt_client_service);
} else {
prv_handle_gatt_event(&e->bluetooth.le.gatt_client);
}
return;
default:
return;
}
}
// -------------------------------------------------------------------------------------------------
static void prv_connect_gateway_bonding(BTBondingID gateway_bonding) {
gap_le_slave_reconnect_start();
gap_le_connect_connect_by_bonding(gateway_bonding, true /* auto_reconnect */,
true /* is_pairing_required */, GAPLEClientKernel);
}
// -------------------------------------------------------------------------------------------------
static void prv_cancel_connect_gateway_bonding(BTBondingID gateway_bonding) {
gap_le_slave_reconnect_stop();
// FIXME: Redundant? since gap_le_connect will also clean up?
gap_le_connect_cancel_by_bonding(gateway_bonding, GAPLEClientKernel);
}
// -------------------------------------------------------------------------------------------------
static void prv_cleanup_clients_kernel_main_cb(void *unused) {
ancs_destroy();
ams_destroy();
}
// -------------------------------------------------------------------------------------------------
void kernel_le_client_handle_bonding_change(BTBondingID bonding, BtPersistBondingOp op) {
if (bt_persistent_storage_is_ble_ancs_bonding(bonding)) {
if (op == BtPersistBondingOpWillDelete) {
prv_cancel_connect_gateway_bonding(bonding);
} else if (op == BtPersistBondingOpDidAdd) {
prv_connect_gateway_bonding(bonding);
}
}
}
// -------------------------------------------------------------------------------------------------
void kernel_le_client_init(void) {
// Reset analytics
ppogatt_reset_disconnect_counter();
BTBondingID gateway_bonding = bt_persistent_storage_get_ble_ancs_bonding();
if (gateway_bonding != BT_BONDING_ID_INVALID) {
prv_connect_gateway_bonding(gateway_bonding);
}
}
// -------------------------------------------------------------------------------------------------
void kernel_le_client_deinit(void) {
// Cleanup clients: their code must execute on KernelMain, so add callback:
launcher_task_add_callback(prv_cleanup_clients_kernel_main_cb, NULL);
gap_le_slave_reconnect_stop();
gap_le_connect_cancel_all(GAPLEClientKernel);
ppogatt_destroy();
}

View file

@ -0,0 +1,34 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "kernel/events.h"
#include "services/common/bluetooth/bluetooth_persistent_storage.h"
//! @file kernel_le_client.h
//! Module that is responsible of connecting to the BLE gateway (aka "the phone") in order to:
//! - bootstrap the Pebble Protocol over GATT (PPoGATT) module
//! - bootstrap the ANCS module
//! - bootstrap the "Service Changed" module
void kernel_le_client_handle_bonding_change(BTBondingID bonding, BtPersistBondingOp op);
void kernel_le_client_handle_event(const PebbleEvent *event);
void kernel_le_client_init(void);
void kernel_le_client_deinit(void);

File diff suppressed because it is too large Load diff

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 "applib/bluetooth/ble_client.h"
struct Transport;
typedef enum {
PPoGATTCharacteristicData,
PPoGATTCharacteristicMeta,
PPoGATTCharacteristicNum
} PPoGATTCharacteristic;
void ppogatt_create(void);
void ppogatt_handle_service_discovered(BLECharacteristic *characteristics);
bool ppogatt_can_handle_characteristic(BLECharacteristic characteristic);
void ppogatt_handle_subscribe(BLECharacteristic subscribed_characteristic,
BLESubscription subscription_type, BLEGATTError error);
void ppogatt_handle_read_or_notification(BLECharacteristic characteristic, const uint8_t *value,
size_t value_length, BLEGATTError error);
void ppogatt_handle_service_removed(
BLECharacteristic *characteristics, uint8_t num_characteristics);
void ppogatt_invalidate_all_references(void);
//! Interface for kernel_le_client, to handle the event that the Bluetooth stack has space available
//! again in its outbound queue. It will trigger the PPoGATT module to send out the next packet(s).
void ppogatt_handle_buffer_empty(void);
//! Interface for CommSession, to let it signal the PPoGATT transport that data has been written
//! into the SendBuffer and can be sent out.
void ppogatt_send_next(struct Transport *transport);
void ppogatt_close(struct Transport *transport);
void ppogatt_reset(struct Transport *transport);
void ppogatt_destroy(void);
//! Interface for analytics
void ppogatt_reset_disconnect_counter(void);

View file

@ -0,0 +1,34 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "ppogatt.h"
#include <bluetooth/pebble_bt.h>
static const Uuid s_ppogatt_service_uuid = {
PEBBLE_BT_UUID_EXPAND(PEBBLE_BT_PPOGATT_SERVICE_UUID_32BIT)
};
static const Uuid s_ppogatt_characteristic_uuids[PPoGATTCharacteristicNum] = {
[PPoGATTCharacteristicData] = {
PEBBLE_BT_UUID_EXPAND(PEBBLE_BT_PPOGATT_DATA_CHARACTERISTIC_UUID_32BIT),
},
[PPoGATTCharacteristicMeta] = {
PEBBLE_BT_UUID_EXPAND(PEBBLE_BT_PPOGATT_META_CHARACTERISTIC_UUID_32BIT),
},
};

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 "comm/ble/gatt_client_subscriptions.h"
#include <stdint.h>
#include "util/uuid.h"
#include "mfg/mfg_serials.h"
#include "util/attributes.h"
#define PPOGATT_V1_DESIRED_RX_WINDOW_SIZE (4500 / MAX_ATT_WRITE_PAYLOAD_SIZE)
#define PPOGATT_MIN_VERSION (0x00)
#define PPOGATT_MAX_VERSION (0x01)
#define PPOGATT_SN_BITS (5)
#define PPOGATT_SN_MOD_DIV (1 << PPOGATT_SN_BITS)
#define PPOGATT_V0_WINDOW_SIZE (4)
#define PPOGATT_TIMEOUT_TICK_INTERVAL_SECS (2)
//! Effective timeout: between 5 - 6 secs, because packet could be sent out just before the
//! RegularTimer second tick is about to fire.
#define PPOGATT_TIMEOUT_TICKS (3)
//! Number of maximum consecutive timeouts without getting a packet Ack'd
#define PPOGATT_TIMEOUT_COUNT_MAX (2)
//! Number of maximum consecutive resets without getting a packet Ack'd
#define PPOGATT_RESET_COUNT_MAX (10)
//! Number of maximum consecutive disconnects without getting a packet Ack'd
#define PPOGATT_DISCONNECT_COUNT_MAX (2)
//! Maximum amount of time PPoGATT will wait before sending an Ack for received data
#define PPOGATT_MAX_DATA_ACK_LATENCY_MS (200)
typedef enum {
PPoGATTPacketTypeData = 0x0,
PPoGATTPacketTypeAck = 0x1,
PPoGATTPacketTypeResetRequest = 0x2,
PPoGATTPacketTypeResetComplete = 0x3,
PPoGATTPacketTypeInvalidRangeStart,
} PPoGATTPacketType;
_Static_assert(PPoGATTPacketTypeAck != 0, "Ack type can't be 0; see ack_packet_byte");
_Static_assert(PPoGATTPacketTypeResetRequest != 0, "Reset type can't be 0; see reset_packet_byte");
_Static_assert(PPoGATTPacketTypeResetComplete != 0, "Reset type can't be 0; see reset_packet_byte");
typedef struct PACKED {
PPoGATTPacketType type:3;
uint8_t sn:PPOGATT_SN_BITS;
uint8_t payload[];
} PPoGATTPacket;
_Static_assert(sizeof(PPoGATTPacket) == 1,
"You can't increase the size of PPoGATTPacket. It's set in stone now!");
//! Client identification payload that is attached to the client's Reset Request messages
typedef struct PACKED {
//! The PPoGATT version that the client wants to use.
//! Must be within the server's [ppogatt_min_version, ppogatt_max_version]
uint8_t ppogatt_version;
//! The serial number of the client device.
char serial_number[MFG_SERIAL_NUMBER_SIZE];
} PPoGATTResetRequestClientIDPayload;
typedef struct PACKED {
uint8_t ppogatt_max_rx_window;
uint8_t ppogatt_max_tx_window;
} PPoGATTResetCompleteClientIDPayloadV1;
typedef struct PACKED {
uint8_t ppogatt_min_version;
uint8_t ppogatt_max_version;
Uuid app_uuid;
} PPoGATTMetaV0;
typedef enum {
PPoGATTSessionType_InferredFromUuid = 0x00,
PPoGATTSessionType_Hybrid = 0x01,
PPoGATTSessionTypeCount,
} PPoGATTSessionType;
typedef struct PACKED {
uint8_t ppogatt_min_version;
uint8_t ppogatt_max_version;
Uuid app_uuid;
PPoGATTSessionType pp_session_type:8;
} PPoGATTMetaV1;

View file

@ -0,0 +1,62 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "applib/bluetooth/ble_client.h"
typedef enum {
TestCharacteristic_One,
TestCharacteristic_Two,
TestCharacteristicCount,
} TestCharacteristic;
//! Test Service UUID
static const Uuid s_test_service_uuid = {
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
};
//! Test Characteristic UUIDs
static const Uuid s_test_characteristic_uuids[TestCharacteristicCount] = {
[TestCharacteristic_One] = {
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
},
[TestCharacteristic_Two] = {
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
},
};
void test_client_handle_service_discovered(BLECharacteristic *characteristics);
void test_client_invalidate_all_references(void);
void test_client_handle_service_removed(BLECharacteristic *characteristics,
uint8_t num_characteristics);
bool test_client_can_handle_characteristic(BLECharacteristic characteristic);
void test_client_handle_write_response(BLECharacteristic characteristic, BLEGATTError error);
void test_client_handle_subscribe(BLECharacteristic characteristic,
BLESubscription subscription_type, BLEGATTError error);
void test_client_handle_read_or_notification(BLECharacteristic characteristic, const uint8_t *value,
size_t value_length, BLEGATTError error);

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.
*/
#include <bluetooth/pebble_bt.h>
#include <stddef.h>
#include <string.h>
void pebble_bt_uuid_expand(Uuid *uuid, uint32_t value) {
static const uint8_t pebble_base_uuid_last_12_bytes[] = {
0x32, 0x8E, 0x0F, 0xBB,
0xC6, 0x42, 0x1A, 0xA6,
0x69, 0x9B, 0xDA, 0xDA,
};
memcpy(&uuid->byte4, &pebble_base_uuid_last_12_bytes, sizeof(Uuid) - offsetof(Uuid, byte4));
uuid->byte0 = (value >> 24) & 0xFF;
uuid->byte1 = (value >> 16) & 0xFF;
uuid->byte2 = (value >> 8) & 0xFF;
uuid->byte3 = (value >> 0) & 0xFF;
}

View file

@ -0,0 +1,281 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "bluetooth_analytics.h"
#include "comm/ble/gap_le_connection.h"
#include "comm/bt_lock.h"
#include "drivers/rtc.h"
#include "services/common/analytics/analytics.h"
#include "services/common/bluetooth/bluetooth_ctl.h"
#include "services/common/comm_session/session.h"
#include "system/logging.h"
#include "util/bitset.h"
#include "util/math.h"
#include <bluetooth/analytics.h>
#include <bluetooth/gap_le_connect.h>
typedef struct {
uint32_t slave_latency_events;
uint32_t supervision_to_ms;
int num_samps;
} LeConnectionParams;
static LeConnectionParams s_le_conn_params = { 0 };
void bluetooth_analytics_get_param_averages(uint16_t *params) {
int num_samps = s_le_conn_params.num_samps;
if (num_samps != 0) {
params[0] = s_le_conn_params.slave_latency_events / num_samps;
params[1] = s_le_conn_params.supervision_to_ms / num_samps;
}
s_le_conn_params = (LeConnectionParams){};
}
static void prv_update_conn_params(uint16_t slave_latency_events,
uint16_t supervision_to_10ms) {
bt_lock();
s_le_conn_params.slave_latency_events += slave_latency_events;
s_le_conn_params.supervision_to_ms += (supervision_to_10ms * 10);
s_le_conn_params.num_samps++;
bt_unlock();
}
static void prv_update_conn_event_timer(uint32_t interval_1_25ms, bool stop) {
bt_lock();
static bool s_analytic_conn_timer_running = false;
if (stop || s_analytic_conn_timer_running) {
analytics_stopwatch_stop(ANALYTICS_DEVICE_METRIC_BLE_CONN_EVENT_COUNT);
s_analytic_conn_timer_running = false;
}
if (!stop) {
// track (# connection attempts * 10^3) / sec
uint32_t conn_attempts_per_sec = ((1000 * 1000 * 5) / (interval_1_25ms)) / 4;
analytics_stopwatch_start_at_rate(
ANALYTICS_DEVICE_METRIC_BLE_CONN_EVENT_COUNT,
conn_attempts_per_sec, AnalyticsClient_System);
s_analytic_conn_timer_running = true;
}
bt_unlock();
}
void bluetooth_analytics_handle_param_update_failed(void) {
analytics_inc(ANALYTICS_DEVICE_METRIC_BLE_CONN_PARAM_UPDATE_FAILED_COUNT,
AnalyticsClient_System);
}
//! only called when we are connected as a slave
void bluetooth_analytics_handle_connection_params_update(const BleConnectionParams *params) {
// When connected as a slave device, the 'Slave Latency' connection parameter allows
// the controller to skip the connection sync for that number of connection events.
uint32_t effective_interval = params->conn_interval_1_25ms * (1 + params->slave_latency_events);
prv_update_conn_event_timer(effective_interval, false);
prv_update_conn_params(params->slave_latency_events, params->supervision_timeout_10ms);
}
void bluetooth_analytics_handle_connection_disconnection_event(
AnalyticsEvent type, uint8_t reason, const BleRemoteVersionInfo *vers_info) {
static uint32_t last_reset_counter_ticks = 0;
static uint8_t num_events_logged = 0;
const uint32_t ticks_per_hour = RTC_TICKS_HZ * 60 * 60;
if ((rtc_get_ticks() - last_reset_counter_ticks) > ticks_per_hour) {
num_events_logged = 0;
last_reset_counter_ticks = rtc_get_ticks();
}
if (num_events_logged > 100) { // don't log a ridiculous amount of tightly looped disconnects
return;
}
// It's okay to log to analytics directly from the BT02 callback thread
// because flash writes are dispatched to KernelBG if the datalogging session
// is buffered
if (type != AnalyticsEvent_BtLeDisconnect) {
analytics_event_bt_connection_or_disconnection(type, reason);
} else {
if (!vers_info) { // We expect version info
PBL_LOG(LOG_LEVEL_WARNING, "Le Disconnect but no version info?");
} else {
analytics_event_bt_le_disconnection(reason, vers_info->version_number,
vers_info->company_identifier,
vers_info->subversion_number);
}
}
num_events_logged++;
}
void bluetooth_analytics_handle_connect(
const BTDeviceInternal *peer_addr, const BleConnectionParams *conn_params) {
analytics_inc(ANALYTICS_DEVICE_METRIC_BLE_CONNECT_COUNT, AnalyticsClient_System);
analytics_stopwatch_start(ANALYTICS_DEVICE_METRIC_BLE_CONNECT_TIME, AnalyticsClient_System);
bluetooth_analytics_handle_connection_params_update(conn_params);
uint8_t link_quality = 0;
int8_t rssi = 0;
bool success = bt_driver_analytics_get_connection_quality(peer_addr, &link_quality, &rssi);
if (success) {
PBL_LOG(LOG_LEVEL_DEBUG, "Link quality: %x, RSSI: %d", link_quality, rssi);
analytics_add(ANALYTICS_DEVICE_METRIC_BLE_LINK_QUALITY_SUM,
link_quality, AnalyticsClient_System);
analytics_add(ANALYTICS_DEVICE_METRIC_BLE_RSSI_SUM,
ABS(rssi), AnalyticsClient_System);
}
}
void bluetooth_analytics_handle_disconnect(bool local_is_master) {
if (!local_is_master) {
analytics_stopwatch_stop(ANALYTICS_DEVICE_METRIC_BLE_CONNECT_TIME);
analytics_stopwatch_stop(ANALYTICS_DEVICE_METRIC_BLE_CONNECT_ENCRYPTED_TIME);
prv_update_conn_event_timer(0, true);
}
}
void bluetooth_analytics_handle_encryption_change(void) {
analytics_stopwatch_start(ANALYTICS_DEVICE_METRIC_BLE_CONNECT_ENCRYPTED_TIME,
AnalyticsClient_System);
}
void bluetooth_analytics_handle_no_intent_for_connection(void) {
analytics_inc(ANALYTICS_DEVICE_METRIC_BLE_CONNECT_NO_INTENT_COUNT, AnalyticsClient_System);
}
void bluetooth_analytics_handle_ble_pairing_request(void) {
analytics_inc(ANALYTICS_DEVICE_METRIC_BLE_PAIRING_COUNT, AnalyticsClient_System);
}
void bluetooth_analytics_handle_bt_classic_pairing_request(void) {
analytics_inc(ANALYTICS_DEVICE_METRIC_BT_PAIRING_COUNT, AnalyticsClient_System);
}
void bluetooth_analytics_handle_ble_pairing_error(uint32_t error) {
analytics_event_bt_error(AnalyticsEvent_BtLePairingError, error);
}
void bluetooth_analytics_handle_bt_classic_pairing_error(uint32_t error) {
analytics_event_bt_error(AnalyticsEvent_BtClassicPairingError, error);
}
void bluetooth_analytics_ble_mic_error(uint32_t num_sequential_mic_errors) {
PBL_LOG(LOG_LEVEL_INFO, "MIC Error detected ... %"PRIu32" packets", num_sequential_mic_errors);
analytics_event_bt_error(AnalyticsEvent_BtLeMicError, num_sequential_mic_errors);
}
static uint32_t prv_calc_other_errors(const SlaveConnEventStats *stats) {
return (stats->num_type_errors + stats->num_len_errors + stats->num_crc_errors +
stats->num_mic_errors);
}
static bool prv_calc_stats_and_print(const SlaveConnEventStats *orig_stats,
SlaveConnEventStats *stats_buf, bool is_putbytes) {
if (bt_driver_analytics_get_conn_event_stats(stats_buf)) {
stats_buf->num_conn_events =
serial_distance32(orig_stats->num_conn_events, stats_buf->num_conn_events);
stats_buf->num_sync_errors =
serial_distance32(orig_stats->num_sync_errors, stats_buf->num_sync_errors);
stats_buf->num_conn_events_skipped =
serial_distance32(orig_stats->num_conn_events_skipped, stats_buf->num_conn_events_skipped);
stats_buf->num_type_errors =
serial_distance32(orig_stats->num_type_errors, stats_buf->num_type_errors);
stats_buf->num_len_errors =
serial_distance32(orig_stats->num_len_errors, stats_buf->num_len_errors);
stats_buf->num_crc_errors =
serial_distance32(orig_stats->num_crc_errors, stats_buf->num_crc_errors);
stats_buf->num_mic_errors =
serial_distance32(orig_stats->num_mic_errors, stats_buf->num_mic_errors);
PBL_LOG(LOG_LEVEL_INFO, "%sBytes Conn Stats: Events: %"PRIu32", Sync Errs: %"PRIu32
", Skipped Events: %"PRIu32" Other Errs: %"PRIu32, is_putbytes ? "Put" : "Get",
stats_buf->num_conn_events, stats_buf->num_sync_errors,
stats_buf->num_conn_events_skipped, prv_calc_other_errors(stats_buf));
return true;
}
return false;
}
void bluetooth_analytics_handle_put_bytes_stats(bool successful, uint8_t type, uint32_t total_size,
uint32_t elapsed_time_ms,
const SlaveConnEventStats *orig_stats) {
SlaveConnEventStats new_stats = {};
prv_calc_stats_and_print(orig_stats, &new_stats, true /* is_putbytes */);
analytics_event_put_byte_stats(
comm_session_get_system_session(), successful, type,
total_size, elapsed_time_ms, new_stats.num_conn_events,
new_stats.num_sync_errors, new_stats.num_conn_events_skipped,
prv_calc_other_errors(&new_stats));
}
void bluetooth_analytics_handle_get_bytes_stats(uint8_t type, uint32_t total_size,
uint32_t elapsed_time_ms,
const SlaveConnEventStats *orig_stats) {
SlaveConnEventStats new_stats = {};
prv_calc_stats_and_print(orig_stats, &new_stats, false /* is_putbytes */);
analytics_event_get_bytes_stats(
comm_session_get_system_session(), type,
total_size, elapsed_time_ms, new_stats.num_conn_events,
new_stats.num_sync_errors, new_stats.num_conn_events_skipped,
prv_calc_other_errors(&new_stats));
}
void analytics_external_collect_ble_parameters(void) {
bt_lock();
{
GAPLEConnection *connection = gap_le_connection_get_gateway();
if (!connection) {
goto unlock;
}
LEChannelMap le_channel_map;
const bool success =
bt_driver_analytics_collect_ble_parameters(&connection->device, &le_channel_map);
if (success) {
analytics_set(ANALYTICS_DEVICE_METRIC_BLE_CHAN_USE_COUNT,
count_bits_set((uint8_t *)&le_channel_map, NUM_LE_CHANNELS),
AnalyticsClient_System);
}
}
unlock:
bt_unlock();
}
void analytics_external_collect_chip_specific_parameters(void) {
bt_lock();
bt_driver_analytics_external_collect_chip_specific_parameters();
bt_unlock();
}
void analytics_external_collect_bt_chip_heartbeat(void) {
// TODO: PBL-38365: Re-enable this once it is fixed :(
#if 0
if (bt_ctl_is_bluetooth_running()) {
// No need for lock
bt_driver_analytics_external_collect_bt_chip_heartbeat();
}
#endif
}

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>
#include "bluetooth/gap_le_connect.h"
#include "services/common/analytics/analytics_event.h"
typedef struct SlaveConnEventStats SlaveConnEventStats;
void bluetooth_analytics_get_param_averages(uint16_t *params);
void bluetooth_analytics_handle_param_update_failed(void);
void bluetooth_analytics_handle_connection_params_update(const BleConnectionParams *params);
void bluetooth_analytics_handle_connect(
const BTDeviceInternal *peer_addr, const BleConnectionParams *conn_params);
void bluetooth_analytics_handle_disconnect(bool local_is_master);
void bluetooth_analytics_handle_encryption_change(void);
void bluetooth_analytics_handle_no_intent_for_connection(void);
void bluetooth_analytics_handle_ble_pairing_request(void);
void bluetooth_analytics_handle_bt_classic_pairing_request(void);
void bluetooth_analytics_handle_ble_pairing_error(uint32_t error);
void bluetooth_analytics_handle_bt_classic_pairing_error(uint32_t error);
void bluetooth_analytics_handle_connection_disconnection_event(
AnalyticsEvent type, uint8_t reason, const BleRemoteVersionInfo *vers_info);
void bluetooth_analytics_handle_put_bytes_stats(bool successful, uint8_t type, uint32_t total_size,
uint32_t elapsed_time_ms,
const SlaveConnEventStats *orig_stats);
void bluetooth_analytics_handle_get_bytes_stats(uint8_t type, uint32_t total_size,
uint32_t elapsed_time_ms,
const SlaveConnEventStats *orig_stats);
void bluetooth_analytics_ble_mic_error(uint32_t num_sequential_mic_errors);

96
src/fw/comm/bt_conn_mgr.h Normal file
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 <bluetooth/responsiveness.h>
#include <inttypes.h>
struct Remote;
typedef struct GAPLEConnection GAPLEConnection;
#define BT_CONN_MGR_INACTIVITY_TIMEOUT_SECS (2)
#define MAX_PERIOD_RUN_FOREVER ((uint16_t)(~0))
//! Informs the BT manger module that we want to run the provided LE connection
//! at the requested rate. Care should be taken to minimize the amount of time
//! we need to be in low latency states as they consume more power.
//!
//! Note: Users should really be calling this twice. Once to enter a fast
//! connection state and then to exit back to the lowest power state. The
//! max_period_secs variable will protect against being stuck
//! indefinitely in a high power state.
//!
//! Note: The second call for a particular consumer will override the settings
//! specified for that consumer during the first call
//!
//! Note: Depending on the mode the controller is currently in there can be a
//! several second delay before entering the requested state
//!
//! @param[in] hdl The LE connection to update
//! @param[in] consumer The consumer requesting the rate change
//! @param[in] state Choose between the latency levels in ResponseTimeState.
//! The lower the latency, the more power being consumed
//! @param[in] max_period_secs The maximum amount of time to keep the connection in an
//! elevated response state before returning to ResponseTimeMax
//! If MAX_PERIOD_RUN_FOREVER, the requested state will never timeout
//! @param[in] granted_handler The function to call back to when a state has been entered that is
//! *as least* as responsive as the requested state.
//! It will be executed on KernelMain.
//! It is guaranteed to be called exactly once per call to this function.
void conn_mgr_set_ble_conn_response_time_ext(
GAPLEConnection *hdl, BtConsumer consumer, ResponseTimeState state,
uint16_t max_period_secs, ResponsivenessGrantedHandler granted_handler);
//! Same as conn_mgr_set_ble_conn_response_time_ext, but without granted_handler.
void conn_mgr_set_ble_conn_response_time(
GAPLEConnection *hdl, BtConsumer consumer, ResponseTimeState state,
uint16_t max_period_secs);
//! Informs the BT manager module that we want to run the provided classic
//! connection at the requested rate.
//!
//! Note: This currently supports two modes. ResponseTimeMax maps to BT clasic sniff mode
//! and anything fatser maps to BT classic active mode
//!
//! @param[in] remote The BT Classic connection requesting the rate change
//! @param[in] consumer The consumer requesting the rate change
//! @param[in] state Choose between the latency levels in ResponseTimeState.
//! The lower the latency, the more power being consumed
//! @param[in] max_period_secs The maximum amount of time to expect being out of sniff mode
//! @param[in] granted_handler The function to call back to when a state has been entered that is
//! *as least* as responsive as the requested state.
//! It will be executed on KernelMain.
//! It is guaranteed to be called exactly once per call to this function.
void conn_mgr_set_bt_classic_conn_response_time_ext(
struct Remote *remote, BtConsumer consumer, ResponseTimeState state,
uint16_t max_period_secs, ResponsivenessGrantedHandler granted_handler);
//! Same as conn_mgr_set_bt_classic_conn_response_time_ext, but without granted_handler.s
void conn_mgr_set_bt_classic_conn_response_time(
struct Remote *remote, BtConsumer consumer, ResponseTimeState state,
uint16_t max_period_secs);
//! @param[in] connection The connection for which to get the lowest requested latency.
//! @param[out] secs_to_wait The longest amount of time that interval has been requested.
//! If the caller is not interested in this information, NULL can be passed in.
//! @return the lowest latency requested for the connection.
//! @note bt_lock MUST be held by the caller.
ResponseTimeState conn_mgr_get_latency_for_le_connection(GAPLEConnection *connection,
uint16_t *secs_to_wait);

View file

@ -0,0 +1,26 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
//! Opaque Bluetooth connection manager struct. It is expected that Bluetooth
//! Handles will have a ptr to this in their handler
typedef struct ConnectionMgrInfo ConnectionMgrInfo;
//! Constructor/Destructor which bluetooth handles need to call to init &
//! cleanup the ConnectionMgrInfo struct
ConnectionMgrInfo *bt_conn_mgr_info_init(void);
void bt_conn_mgr_info_deinit(ConnectionMgrInfo **info);

61
src/fw/comm/bt_lock.c 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.
*/
#include "bt_lock.h"
#include "system/passert.h"
#include "portmacro.h"
// NOTE: The s_bt_lock is the global Bluetooth lock that is used by the firmware
// *and* by Bluetopia. It gets handed to Bluetopia using bt_lock_get() in
// BTPSKRNL.c when Bluetopia is initialized. The firmware uses this lock to
// protect Bluetooth-related state that is read and written from the Bluetooth
// callback task (PebbleTask_BTCallback) and other tasks. If we created our
// own mutex for this purpose, we would encounter dead-lock situations.
// For example:
// Task1: Bluetopia code -> grabs BT stack lock -> Pebble callback -> grabs pebble mutex
// Task2: Pebble code -> grabs pebble mutex -> calls into Bluetopia -> tries to grab BT stack lock
static PebbleRecursiveMutex *s_bt_lock;
void bt_lock_init(void) {
// Never free'd.
PBL_ASSERTN(!s_bt_lock);
s_bt_lock = mutex_create_recursive();
}
PebbleRecursiveMutex *bt_lock_get(void) {
return s_bt_lock;
}
void bt_lock(void) {
register uint32_t LR __asm ("lr");
uint32_t myLR = LR;
mutex_lock_recursive_with_timeout_and_lr(s_bt_lock, portMAX_DELAY, myLR);
}
void bt_unlock(void) {
mutex_unlock_recursive(s_bt_lock);
}
void bt_lock_assert_held(bool is_held) {
mutex_assert_recursive_held_by_curr_task(s_bt_lock, is_held);
}
bool bt_lock_is_held(void) {
return mutex_is_owned_recursive(s_bt_lock);
}

37
src/fw/comm/bt_lock.h Normal file
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 "os/mutex.h"
void bt_lock_init(void);
//! Function to get the shared mutex. This is used in BTPSKRNL.c to hand
//! Bluetopia this mutex to use as its BSC_LockBluetoothStack mutex.
PebbleRecursiveMutex *bt_lock_get(void);
//! Lock the shared Bluetooth recursive lock to protect Bluetooth related state.
void bt_lock(void);
//! Unlock the shared Bluetooth recursive lock.
void bt_unlock(void);
//! Asserts whether the bt_lock() is held or not.
void bt_lock_assert_held(bool is_held);
//! Returns true if the bt lock is held, else false
bool bt_lock_is_held(void);

View file

@ -0,0 +1,413 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 <bluetooth/responsiveness.h>
#include "comm/ble/gap_le_connect_params.h"
#include "comm/ble/gap_le_connection.h"
#include "comm/bt_conn_mgr.h"
#include "comm/bt_lock.h"
#include "drivers/rtc.h"
#include "kernel/event_loop.h"
#include "kernel/pbl_malloc.h"
#include "services/common/new_timer/new_timer.h"
#include "services/common/regular_timer.h"
#include "services/common/system_task.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/list.h"
#include "util/math.h"
#include "util/rand.h"
#include <stdlib.h>
//! The Bluetooth Connection Manager is responsible for managing the power
//! state of the active bluetooth connections. Sub-modules using bluetooth are
//! expected to notify this module when they are active or expect inbound data
//! and want to minimize latency. Using this info, the module decides whether
//! the LE or classic connection needs to be bumped out of its lower power
//! state in order to respond more quickly.
//!
//! Note: This module currently only manages the LE connections. In the
//! future, we will add support for handling classic connections as well
typedef struct {
ListNode list_node;
uint32_t timeout; // time to stop this request (in rtc ticks)
ResponseTimeState req_state;
BtConsumer consumer;
ResponsivenessGrantedHandler granted_handler;
} ConnectionStateRequest;
typedef struct ConnectionMgrInfo {
// callback which returns us to a low power state if user of API does not exit
// a high power state
RegularTimerInfo watchdog_cb_info;
// current running state of the connection
ResponseTimeState curr_requested_state;
// A list of consumers who have requested changes to latency state != ResponseTimeMax
ConnectionStateRequest *requests;
} ConnectionMgrInfo;
ResponseTimeState gap_le_connect_params_get_actual_state(GAPLEConnection *connection);
//! Walks through and finds the lowest latency requested for the given type of
//! connection. Also detects the longest amount of time that interval has been
//! requested. Also gets the consumer that is responsible for the lowest latency + longest timeout
//! combo. These pieces of information are then returned to the caller.
static ResponseTimeState prv_determine_latency_for_connection(
ConnectionStateRequest *requests, uint16_t *secs_to_wait, BtConsumer *consumer_out) {
ResponseTimeState state = ResponseTimeMax;
uint32_t timeout = 0;
BtConsumer responsible_consumer = BtConsumerNone;
ConnectionStateRequest *curr_request = requests;
while (curr_request != NULL) {
if (curr_request->req_state > state) {
// reset our tracker, we have found a higher power mode requested
timeout = curr_request->timeout;
state = curr_request->req_state;
responsible_consumer = curr_request->consumer;
} else if (curr_request->req_state == state) {
if (curr_request->timeout > timeout) {
timeout = curr_request->timeout;
responsible_consumer = curr_request->consumer;
}
}
curr_request = (ConnectionStateRequest *)list_get_next(&curr_request->list_node);
}
if (consumer_out) {
*consumer_out = responsible_consumer;
}
if (secs_to_wait) {
uint32_t curr_ticks = rtc_get_ticks();
if (curr_ticks < timeout) {
uint16_t wait_time = (timeout - curr_ticks) / RTC_TICKS_HZ;
*secs_to_wait = MAX(1, wait_time);
} else {
*secs_to_wait = 0;
}
}
return state;
}
/*
* LE connection manager handling for a gateway connection
*/
static void prv_bt_le_gateway_response_latency_watchdog_cb(void *data);
static void prv_granted_kernel_main_cb(void *ctx) {
ResponsivenessGrantedHandler granted_handler = ctx;
granted_handler();
}
static void prv_schedule_granted_handler(ResponsivenessGrantedHandler granted_handler) {
PBL_ASSERTN(granted_handler);
launcher_task_add_callback(prv_granted_kernel_main_cb, granted_handler);
}
//! extern'd for gap_le_connect_params.c
void conn_mgr_handle_desired_state_granted(GAPLEConnection *hdl,
ResponseTimeState granted_state) {
bt_lock_assert_held(true);
ConnectionStateRequest *curr_request = hdl->conn_mgr_info->requests;
while (curr_request != NULL) {
if (curr_request->granted_handler &&
curr_request->req_state <= granted_state) {
prv_schedule_granted_handler(curr_request->granted_handler);
curr_request->granted_handler = NULL;
}
curr_request = (ConnectionStateRequest *)list_get_next(&curr_request->list_node);
}
}
static void prv_handle_response_latency_for_le_conn(GAPLEConnection *hdl) {
uint16_t secs_til_max_latency;
ResponseTimeState state;
BtConsumer responsible_consumer;
#ifdef RECOVERY_FW
// We don't care if we burn up some power from PRF and we want FW to update quickly
secs_til_max_latency = MAX_PERIOD_RUN_FOREVER;
state = ResponseTimeMin;
responsible_consumer = 0;
#else
state = prv_determine_latency_for_connection(hdl->conn_mgr_info->requests,
&secs_til_max_latency, &responsible_consumer);
#endif
// actually request the mode if it has changed:
if (hdl->conn_mgr_info->curr_requested_state != state) {
PBL_LOG(LOG_LEVEL_INFO, "LE: Requesting state %d for %d secs, due to %u",
state, secs_til_max_latency, responsible_consumer);
gap_le_connect_params_request(hdl, state);
}
// remove a watchdog timer if it was already scheduled and schedule a new one
RegularTimerInfo *watchdog_cb_info = &hdl->conn_mgr_info->watchdog_cb_info;
if (regular_timer_is_scheduled(watchdog_cb_info)) {
regular_timer_remove_callback(watchdog_cb_info);
}
// don't start the watchdog timer if we have entered the lowest power mode or
// if we want to run at the specified rate indefinitely
if ((state != ResponseTimeMax) && (secs_til_max_latency != MAX_PERIOD_RUN_FOREVER)) {
watchdog_cb_info->cb = prv_bt_le_gateway_response_latency_watchdog_cb;
watchdog_cb_info->cb_data = hdl;
// wait an extra second since the multisecond callback will fire somewhere
// between 0 and 1 seconds from now and we want to make sure the interval
// we are currently running at actually expires
regular_timer_add_multisecond_callback(
watchdog_cb_info, secs_til_max_latency + 1);
}
hdl->conn_mgr_info->curr_requested_state = state;
}
static void prv_bt_le_gateway_response_latency_watchdog_handler(void *data) {
bt_lock();
GAPLEConnection *hdl = (GAPLEConnection *)data;
// Let's make sure our connection handle is still valid in case we
// disconnected before this CB had a chance to execute
if (!gap_le_connection_is_valid(hdl)) {
goto unlock;
}
ConnectionMgrInfo *conn_mgr_info = hdl->conn_mgr_info;
// if we are executing this cb, we have timed out running at the currently
// selected state so check and see what consumer timeouts have expired
ConnectionStateRequest *curr_request = conn_mgr_info->requests;
uint32_t curr_ticks = rtc_get_ticks();
while (curr_request != NULL) {
ConnectionStateRequest *next =
(ConnectionStateRequest *)list_get_next(&curr_request->list_node);
if (conn_mgr_info->curr_requested_state == curr_request->req_state) {
if (curr_ticks >= curr_request->timeout) {
list_remove(&curr_request->list_node, (ListNode **)&conn_mgr_info->requests, NULL);
kernel_free(curr_request);
}
}
curr_request = next;
}
// Note: As an optimization, we could track how long we have been in a lower
// latency state and subtract that from higher latency requests, but most of
// the time we should be in the maximum latency (low power) state anyway
// get & set the new state
prv_handle_response_latency_for_le_conn(hdl);
unlock:
bt_unlock();
}
static void prv_bt_le_gateway_response_latency_watchdog_cb(void *data) {
// offload handling onto KernelBG so we don't stall the timer thread
// trying to get the bt lock
system_task_add_callback(prv_bt_le_gateway_response_latency_watchdog_handler, data);
}
static bool prv_find_source(ListNode *found_node, void *data) {
return (((ConnectionStateRequest *)found_node)->consumer == (BtConsumer)data);
}
/*
* Exported APIs
*/
void conn_mgr_set_ble_conn_response_time(
GAPLEConnection *hdl, BtConsumer consumer, ResponseTimeState state,
uint16_t max_period_secs) {
conn_mgr_set_ble_conn_response_time_ext(hdl, consumer, state, max_period_secs, NULL);
}
void conn_mgr_set_ble_conn_response_time_ext(
GAPLEConnection *hdl, BtConsumer consumer, ResponseTimeState state,
uint16_t max_period_secs, ResponsivenessGrantedHandler granted_handler) {
ConnectionMgrInfo *conn_mgr_info;
if (!hdl || !((conn_mgr_info = hdl->conn_mgr_info))) {
PBL_LOG(LOG_LEVEL_ERROR, "GAP Handle not properly intialized");
return;
}
bt_lock();
// remove the watchdog timer if it was already scheduled since we are
// going to recompute
RegularTimerInfo *watchdog_cb_info = &hdl->conn_mgr_info->watchdog_cb_info;
if (regular_timer_is_scheduled(watchdog_cb_info)) {
regular_timer_remove_callback(watchdog_cb_info);
}
ConnectionStateRequest *consumer_request =
(ConnectionStateRequest *)list_find((ListNode *)conn_mgr_info->requests,
prv_find_source, (void *)consumer);
bool is_already_granted = (gap_le_connect_params_get_actual_state(hdl) >= state);
if (consumer_request == NULL) {
if (state == ResponseTimeMax) {
// No changes: there was no previous node and the new state is the default "low power" one.
goto handle_current_state;
}
// create node
consumer_request = kernel_malloc_check(sizeof(ConnectionStateRequest));
list_init(&consumer_request->list_node);
conn_mgr_info->requests = (ConnectionStateRequest *)list_prepend(
&conn_mgr_info->requests->list_node, &consumer_request->list_node);
}
// If the consumer requests to go back to low power (ResponseTimeMax), wait a little longer
// before actually going back. This prevents rapid back-n-forths between low power and fast modes,
// that can happen especially in a chain of operations, for example, the resource & bin put-bytes
// sessions to install an app.
if (state == ResponseTimeMax) {
// Keep the existing node in the list for the duration of our "activity timeout". It will be
// cleaned up automatically by the watchdog timer.
max_period_secs = BT_CONN_MGR_INACTIVITY_TIMEOUT_SECS;
state = consumer_request->req_state;
}
// populate node with new info. If it was previously set we override it
consumer_request->timeout = rtc_get_ticks() + max_period_secs * RTC_TICKS_HZ;
consumer_request->req_state = state;
consumer_request->consumer = consumer;
consumer_request->granted_handler = is_already_granted ? NULL : granted_handler;
handle_current_state:
if (is_already_granted && granted_handler) {
prv_schedule_granted_handler(granted_handler);
}
prv_handle_response_latency_for_le_conn(hdl);
bt_unlock();
}
//! expects that the bt lock is held
ConnectionMgrInfo * bt_conn_mgr_info_init(void) {
ConnectionMgrInfo *newinfo = kernel_malloc_check(sizeof(ConnectionMgrInfo));
*newinfo = (ConnectionMgrInfo) {
.curr_requested_state = ResponseTimeMax,
};
return newinfo;
}
//! expects that the bt_lock is held
void bt_conn_mgr_info_deinit(ConnectionMgrInfo **info) {
// If we have any callbacks scheduled for this device, take them out
RegularTimerInfo *watchdog_cb_info = &(*info)->watchdog_cb_info;
if (regular_timer_is_scheduled(watchdog_cb_info)) {
regular_timer_remove_callback(watchdog_cb_info);
}
ListNode *curr_request = (ListNode *)(*info)->requests;
while (curr_request != NULL) {
ListNode *temp = list_get_next(curr_request);
list_remove(curr_request, NULL, NULL);
kernel_free(curr_request);
curr_request = temp;
}
kernel_free(*info);
*info = NULL;
}
void command_change_le_mode(char *mode) {
// assume we only have one connection for debug
GAPLEConnection *conn_hdl = gap_le_connection_any();
ResponseTimeState state = atoi(mode);
conn_mgr_set_ble_conn_response_time(
conn_hdl, BtConsumerPrompt, state, MAX_PERIOD_RUN_FOREVER);
}
static TimerID s_chaos_monkey_timer;
static ResponseTimeState s_chaos_monkey_last_state;
static void prv_mode_chaos_monkey_stop(void) {
new_timer_delete(s_chaos_monkey_timer);
s_chaos_monkey_timer = TIMER_INVALID_ID;
}
static void prv_mode_chaos_monkey_callback(void *data) {
bt_lock();
GAPLEConnection *hdl = (GAPLEConnection *) data;
if (s_chaos_monkey_timer == TIMER_INVALID_ID) {
goto unlock;
}
if (!gap_le_connection_is_valid(hdl)) {
prv_mode_chaos_monkey_stop();
goto unlock;
}
ResponseTimeState requested_state;
do {
requested_state = bounded_rand_int(ResponseTimeMax, ResponseTimeMin);
} while (requested_state == s_chaos_monkey_last_state);
s_chaos_monkey_last_state = requested_state;
conn_mgr_set_ble_conn_response_time(hdl, BtConsumerPrompt,
requested_state, MAX_PERIOD_RUN_FOREVER);
const uint32_t delay_ms = bounded_rand_int(1, 3000);
PBL_LOG(LOG_LEVEL_DEBUG, "Mode chaos monkey: next change=%"PRIu32"ms", delay_ms);
new_timer_start(s_chaos_monkey_timer, delay_ms, prv_mode_chaos_monkey_callback, data, 0);
unlock:
bt_unlock();
}
void command_le_mode_chaos_monkey(char *enabled_str) {
bool new_enabled = atoi(enabled_str);
bool is_enabled = (s_chaos_monkey_timer != TIMER_INVALID_ID);
if (new_enabled == is_enabled) {
return;
}
bt_lock();
if (new_enabled) {
GAPLEConnection *conn_hdl = gap_le_connection_any();
if (conn_hdl) {
s_chaos_monkey_timer = new_timer_create();
prv_mode_chaos_monkey_callback(conn_hdl);
}
} else {
prv_mode_chaos_monkey_stop();
}
bt_unlock();
}
ResponseTimeState conn_mgr_get_latency_for_le_connection(
GAPLEConnection *hdl, uint16_t *secs_to_wait) {
bt_lock_assert_held(true);
return prv_determine_latency_for_connection(
hdl->conn_mgr_info->requests, secs_to_wait, NULL);
}

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.
*/
#include "comm/ble/kernel_le_client/ams/ams.h"
void ams_create(void) {
}
void ams_handle_service_discovered(BLECharacteristic *characteristics) {
}
bool ams_can_handle_characteristic(BLECharacteristic characteristic) {
return false;
}
void ams_handle_subscribe(BLECharacteristic subscribed_characteristic,
BLESubscription subscription_type, BLEGATTError error) {
}
void ams_handle_write_response(BLECharacteristic characteristic, BLEGATTError error) {
}
void ams_handle_read_or_notification(BLECharacteristic characteristic, const uint8_t *value,
size_t value_length, BLEGATTError error) {
}
void ams_invalidate_all_references(void) {
}
void ams_handle_service_removed(BLECharacteristic *characteristics, uint8_t num_characteristics) {
}
void ams_destroy(void) {
}
void ams_send_command(AMSRemoteCommandID command_id) {
}

View file

@ -0,0 +1,71 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "comm/ble/kernel_le_client/ancs/ancs.h"
// -------------------------------------------------------------------------------------------------
// Stub for PRF
void ancs_handle_read_or_notification(BLECharacteristic characteristic, const uint8_t *value,
size_t value_length, BLEGATTError error) {
return;
}
void ancs_handle_write_response(BLECharacteristic characteristic, BLEGATTError error) {
return;
}
void ancs_perform_action(uint32_t notification_uid, uint8_t action_id) {
return;
}
void ancs_handle_service_discovered(BLECharacteristic *characteristics) {
return;
}
bool ancs_can_handle_characteristic(BLECharacteristic characteristic) {
return false;
}
void ancs_handle_subscribe(BLECharacteristic subscribed_characteristic,
BLESubscription subscription_type, BLEGATTError error) {
return;
}
void ancs_invalidate_all_references(void) {
}
void ancs_handle_service_removed(BLECharacteristic *characteristics, uint8_t num_characteristics) {
}
void ancs_create(void) {
return;
}
void ancs_destroy(void) {
return;
}
void ancs_handle_ios9_or_newer_detected(void) {
}
// -------------------------------------------------------------------------------------------------
// Analytics
void analytics_external_collect_ancs_info(void) {
return;
}