mirror of
https://github.com/google/pebble.git
synced 2025-05-29 22:43:12 +00:00
Import of the watch repository from Pebble
This commit is contained in:
commit
3b92768480
10334 changed files with 2564465 additions and 0 deletions
29
src/fw/comm/ble/ble_log.h
Normal file
29
src/fw/comm/ble/ble_log.h
Normal 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
55
src/fw/comm/ble/gap_le.c
Normal 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
22
src/fw/comm/ble/gap_le.h
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
void gap_le_init(void);
|
||||
void gap_le_deinit(void);
|
698
src/fw/comm/ble/gap_le_advert.c
Normal file
698
src/fw/comm/ble/gap_le_advert.c
Normal 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
|
153
src/fw/comm/ble/gap_le_advert.h
Normal file
153
src/fw/comm/ble/gap_le_advert.h
Normal 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);
|
1237
src/fw/comm/ble/gap_le_connect.c
Normal file
1237
src/fw/comm/ble/gap_le_connect.c
Normal file
File diff suppressed because it is too large
Load diff
75
src/fw/comm/ble/gap_le_connect.h
Normal file
75
src/fw/comm/ble/gap_le_connect.h
Normal 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);
|
347
src/fw/comm/ble/gap_le_connect_params.c
Normal file
347
src/fw/comm/ble/gap_le_connect_params.c
Normal 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
|
37
src/fw/comm/ble/gap_le_connect_params.h
Normal file
37
src/fw/comm/ble/gap_le_connect_params.h
Normal 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);
|
350
src/fw/comm/ble/gap_le_connection.c
Normal file
350
src/fw/comm/ble/gap_le_connection.c
Normal 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();
|
||||
}
|
206
src/fw/comm/ble/gap_le_connection.h
Normal file
206
src/fw/comm/ble/gap_le_connection.h
Normal 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);
|
77
src/fw/comm/ble/gap_le_device_name.c
Normal file
77
src/fw/comm/ble/gap_le_device_name.c
Normal file
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#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();
|
||||
}
|
29
src/fw/comm/ble/gap_le_device_name.h
Normal file
29
src/fw/comm/ble/gap_le_device_name.h
Normal 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);
|
215
src/fw/comm/ble/gap_le_scan.c
Normal file
215
src/fw/comm/ble/gap_le_scan.c
Normal 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;
|
||||
}
|
84
src/fw/comm/ble/gap_le_scan.h
Normal file
84
src/fw/comm/ble/gap_le_scan.h
Normal 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);
|
216
src/fw/comm/ble/gap_le_slave_discovery.c
Normal file
216
src/fw/comm/ble/gap_le_slave_discovery.c
Normal 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();
|
||||
}
|
44
src/fw/comm/ble/gap_le_slave_discovery.h
Normal file
44
src/fw/comm/ble/gap_le_slave_discovery.h
Normal 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);
|
228
src/fw/comm/ble/gap_le_slave_reconnect.c
Normal file
228
src/fw/comm/ble/gap_le_slave_reconnect.c
Normal 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
|
51
src/fw/comm/ble/gap_le_slave_reconnect.h
Normal file
51
src/fw/comm/ble/gap_le_slave_reconnect.h
Normal 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
|
30
src/fw/comm/ble/gap_le_task.c
Normal file
30
src/fw/comm/ble/gap_le_task.c
Normal file
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#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;
|
||||
}
|
||||
}
|
31
src/fw/comm/ble/gap_le_task.h
Normal file
31
src/fw/comm/ble/gap_le_task.h
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#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
158
src/fw/comm/ble/gatt.c
Normal file
|
@ -0,0 +1,158 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <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();
|
||||
}
|
870
src/fw/comm/ble/gatt_client_accessors.c
Normal file
870
src/fw/comm/ble/gatt_client_accessors.c
Normal 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;
|
||||
}
|
114
src/fw/comm/ble/gatt_client_accessors.h
Normal file
114
src/fw/comm/ble/gatt_client_accessors.h
Normal 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);
|
481
src/fw/comm/ble/gatt_client_discovery.c
Normal file
481
src/fw/comm/ble/gatt_client_discovery.c
Normal 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;
|
||||
}
|
31
src/fw/comm/ble/gatt_client_discovery.h
Normal file
31
src/fw/comm/ble/gatt_client_discovery.h
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#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);
|
363
src/fw/comm/ble/gatt_client_operations.c
Normal file
363
src/fw/comm/ble/gatt_client_operations.c
Normal 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();
|
||||
}
|
61
src/fw/comm/ble/gatt_client_operations.h
Normal file
61
src/fw/comm/ble/gatt_client_operations.h
Normal 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);
|
828
src/fw/comm/ble/gatt_client_subscriptions.c
Normal file
828
src/fw/comm/ble/gatt_client_subscriptions.c
Normal 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;
|
||||
}
|
118
src/fw/comm/ble/gatt_client_subscriptions.h
Normal file
118
src/fw/comm/ble/gatt_client_subscriptions.h
Normal 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);
|
213
src/fw/comm/ble/gatt_service_changed.c
Normal file
213
src/fw/comm/ble/gatt_service_changed.c
Normal 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);
|
||||
}
|
59
src/fw/comm/ble/gatt_service_changed.h
Normal file
59
src/fw/comm/ble/gatt_service_changed.h
Normal file
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <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);
|
709
src/fw/comm/ble/kernel_le_client/ams/ams.c
Normal file
709
src/fw/comm/ble/kernel_le_client/ams/ams.c
Normal 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);
|
||||
}
|
101
src/fw/comm/ble/kernel_le_client/ams/ams.h
Normal file
101
src/fw/comm/ble/kernel_le_client/ams/ams.h
Normal 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);
|
35
src/fw/comm/ble/kernel_le_client/ams/ams_analytics.h
Normal file
35
src/fw/comm/ble/kernel_le_client/ams/ams_analytics.h
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
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;
|
46
src/fw/comm/ble/kernel_le_client/ams/ams_definition.h
Normal file
46
src/fw/comm/ble/kernel_le_client/ams/ams_definition.h
Normal 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,
|
||||
},
|
||||
};
|
198
src/fw/comm/ble/kernel_le_client/ams/ams_types.h
Normal file
198
src/fw/comm/ble/kernel_le_client/ams/ams_types.h
Normal 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;
|
141
src/fw/comm/ble/kernel_le_client/ams/ams_util.c
Normal file
141
src/fw/comm/ble/kernel_le_client/ams/ams_util.c
Normal 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;
|
||||
}
|
56
src/fw/comm/ble/kernel_le_client/ams/ams_util.h
Normal file
56
src/fw/comm/ble/kernel_le_client/ams/ams_util.h
Normal 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);
|
1105
src/fw/comm/ble/kernel_le_client/ancs/ancs.c
Normal file
1105
src/fw/comm/ble/kernel_le_client/ancs/ancs.c
Normal file
File diff suppressed because it is too large
Load diff
92
src/fw/comm/ble/kernel_le_client/ancs/ancs.h
Normal file
92
src/fw/comm/ble/kernel_le_client/ancs/ancs.h
Normal file
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#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);
|
108
src/fw/comm/ble/kernel_le_client/ancs/ancs_app_name_storage.c
Normal file
108
src/fw/comm/ble/kernel_le_client/ancs/ancs_app_name_storage.c
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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);
|
41
src/fw/comm/ble/kernel_le_client/ancs/ancs_definition.h
Normal file
41
src/fw/comm/ble/kernel_le_client/ancs/ancs_definition.h
Normal 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
|
||||
},
|
||||
};
|
244
src/fw/comm/ble/kernel_le_client/ancs/ancs_types.h
Normal file
244
src/fw/comm/ble/kernel_le_client/ancs/ancs_types.h
Normal 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;
|
142
src/fw/comm/ble/kernel_le_client/ancs/ancs_util.c
Normal file
142
src/fw/comm/ble/kernel_le_client/ancs/ancs_util.c
Normal 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;
|
||||
}
|
38
src/fw/comm/ble/kernel_le_client/ancs/ancs_util.h
Normal file
38
src/fw/comm/ble/kernel_le_client/ancs/ancs_util.h
Normal 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);
|
97
src/fw/comm/ble/kernel_le_client/app_launch/app_launch.c
Normal file
97
src/fw/comm/ble/kernel_le_client/app_launch/app_launch.c
Normal 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);
|
||||
}
|
||||
}
|
40
src/fw/comm/ble/kernel_le_client/app_launch/app_launch.h
Normal file
40
src/fw/comm/ble/kernel_le_client/app_launch/app_launch.h
Normal 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);
|
|
@ -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),
|
||||
},
|
||||
};
|
42
src/fw/comm/ble/kernel_le_client/dis/dis.c
Normal file
42
src/fw/comm/ble/kernel_le_client/dis/dis.c
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#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();
|
||||
}
|
40
src/fw/comm/ble/kernel_le_client/dis/dis.h
Normal file
40
src/fw/comm/ble/kernel_le_client/dis/dis.h
Normal 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);
|
34
src/fw/comm/ble/kernel_le_client/dis/dis_definition.h
Normal file
34
src/fw/comm/ble/kernel_le_client/dis/dis_definition.h
Normal 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)
|
||||
},
|
||||
};
|
584
src/fw/comm/ble/kernel_le_client/kernel_le_client.c
Normal file
584
src/fw/comm/ble/kernel_le_client/kernel_le_client.c
Normal 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();
|
||||
}
|
34
src/fw/comm/ble/kernel_le_client/kernel_le_client.h
Normal file
34
src/fw/comm/ble/kernel_le_client/kernel_le_client.h
Normal 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);
|
1274
src/fw/comm/ble/kernel_le_client/ppogatt/ppogatt.c
Normal file
1274
src/fw/comm/ble/kernel_le_client/ppogatt/ppogatt.c
Normal file
File diff suppressed because it is too large
Load diff
61
src/fw/comm/ble/kernel_le_client/ppogatt/ppogatt.h
Normal file
61
src/fw/comm/ble/kernel_le_client/ppogatt/ppogatt.h
Normal 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);
|
|
@ -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),
|
||||
},
|
||||
};
|
100
src/fw/comm/ble/kernel_le_client/ppogatt/ppogatt_internal.h
Normal file
100
src/fw/comm/ble/kernel_le_client/ppogatt/ppogatt_internal.h
Normal 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;
|
62
src/fw/comm/ble/kernel_le_client/test/test_definition.h
Normal file
62
src/fw/comm/ble/kernel_le_client/test/test_definition.h
Normal file
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "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);
|
33
src/fw/comm/ble/pebble_bt.c
Normal file
33
src/fw/comm/ble/pebble_bt.c
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#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;
|
||||
}
|
281
src/fw/comm/bluetooth_analytics.c
Normal file
281
src/fw/comm/bluetooth_analytics.c
Normal 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
|
||||
}
|
61
src/fw/comm/bluetooth_analytics.h
Normal file
61
src/fw/comm/bluetooth_analytics.h
Normal 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
96
src/fw/comm/bt_conn_mgr.h
Normal 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);
|
26
src/fw/comm/bt_conn_mgr_impl.h
Normal file
26
src/fw/comm/bt_conn_mgr_impl.h
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
//! 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
61
src/fw/comm/bt_lock.c
Normal 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
37
src/fw/comm/bt_lock.h
Normal 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);
|
413
src/fw/comm/internals/bt_conn_mgr.c
Normal file
413
src/fw/comm/internals/bt_conn_mgr.c
Normal 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);
|
||||
}
|
50
src/fw/comm/prf_stubs/ams.c
Normal file
50
src/fw/comm/prf_stubs/ams.c
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#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) {
|
||||
}
|
71
src/fw/comm/prf_stubs/ancs.c
Normal file
71
src/fw/comm/prf_stubs/ancs.c
Normal 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;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue