mirror of
https://github.com/google/pebble.git
synced 2025-04-30 15:21:41 -04:00
481 lines
16 KiB
C
481 lines
16 KiB
C
/*
|
|
* Copyright 2024 Google LLC
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT 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_connection.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;
|
|
}
|