mirror of
https://github.com/google/pebble.git
synced 2025-11-27 09:42:24 -05:00
829 lines
33 KiB
C
829 lines
33 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_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;
|
|
}
|