/* * 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 #include 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); }