Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a3acd5cc authored by HsingYuan Lo's avatar HsingYuan Lo
Browse files

Implement robust caching (server)

Flag:
- Use flag to enable/disable robust caching on server side

btif_storage
- Add APIs to set/get/remove database hash and client supported features

Database Hash
- Add database hash definition into GATT profile
- Set client to change aware when reading database hash

Client Supported Features
- Add write check for client supported features characteristic
- Store client supported feature into btif_storage

Behaviors
- When a service is added/removed
  . Update database hash
  . Set clients that support robust caching to change unaware
- After ack for service changed indication is received
  . Set the client to change aware
- When the client is change unaware
  . Allow request of reading database hash (by handle or by uuid)
  . Allow write_execute, config_mtu and handle_value_conf
  . Send DATABASE_OUT_OF_SYNC error when it is a request
  . Ignore if it is a command
  . After the response is sent, set the client to change aware
- When a client is set from change unaware to change aware
  . Store current database hash to btif_stroage for that client

Connect and Disconnect:
- When a tGATT_TCB instance is allocated, load status from btif_storage
- When disconnected, if the device is untrusted, remove data from btif_storage

Tag: #feature
Test: atest net_test_stack_gatt_native:GattSrRobustCachingTest
Bug: 154056389
Change-Id: I81971012618472dd9e0e6a9e41868caa68998ad4
parent f7cd98b2
Loading
Loading
Loading
Loading
+12 −0
Original line number Diff line number Diff line
@@ -239,6 +239,18 @@ void btif_storage_set_gatt_cl_supp_feat(const RawAddress& bd_addr,
/** Get client supported features */
uint8_t btif_storage_get_gatt_cl_supp_feat(const RawAddress& bd_addr);

/** Remove client supported features */
void btif_storage_remove_gatt_cl_supp_feat(const RawAddress& bd_addr);

/** Store last server database hash for remote client */
void btif_storage_set_gatt_cl_db_hash(const RawAddress& bd_addr, Octet16 hash);

/** Get last server database hash for remote client */
Octet16 btif_storage_get_gatt_cl_db_hash(const RawAddress& bd_addr);

/** Remove last server database hash for remote client */
void btif_storage_remove_gatt_cl_db_hash(const RawAddress& bd_addr);

/** Get the hearing aid device properties. */
bool btif_storage_get_hearing_aid_prop(
    const RawAddress& address, uint8_t* capabilities, uint64_t* hi_sync_id,
+62 −0
Original line number Diff line number Diff line
@@ -83,6 +83,7 @@ using bluetooth::Uuid;
#define BTIF_STORAGE_KEY_LOCAL_IO_CAPS_BLE "LocalIOCapsBLE"
#define BTIF_STORAGE_KEY_ADAPTER_DISC_TIMEOUT "DiscoveryTimeout"
#define BTIF_STORAGE_KEY_GATT_CLIENT_SUPPORTED "GattClientSupportedFeatures"
#define BTIF_STORAGE_KEY_GATT_CLIENT_DB_HASH "GattClientDatabaseHash"

/* This is a local property to add a device found */
#define BT_PROPERTY_REMOTE_DEVICE_TIMESTAMP 0xFF
@@ -852,6 +853,9 @@ bt_status_t btif_storage_remove_bonded_device(
  if (btif_config_exist(bdstr, BTIF_STORAGE_KEY_GATT_CLIENT_SUPPORTED)) {
    ret &= btif_config_remove(bdstr, BTIF_STORAGE_KEY_GATT_CLIENT_SUPPORTED);
  }
  if (btif_config_exist(bdstr, BTIF_STORAGE_KEY_GATT_CLIENT_DB_HASH)) {
    ret &= btif_config_remove(bdstr, BTIF_STORAGE_KEY_GATT_CLIENT_DB_HASH);
  }

  /* write bonded info immediately */
  btif_config_flush();
@@ -1783,3 +1787,61 @@ uint8_t btif_storage_get_gatt_cl_supp_feat(const RawAddress& bd_addr) {

  return value;
}

/** Remove client supported features */
void btif_storage_remove_gatt_cl_supp_feat(const RawAddress& bd_addr) {
  do_in_jni_thread(
      FROM_HERE, Bind(
                     [](const RawAddress& bd_addr) {
                       auto bdstr = bd_addr.ToString();
                       if (btif_config_exist(
                               bdstr, BTIF_STORAGE_KEY_GATT_CLIENT_SUPPORTED)) {
                         btif_config_remove(
                             bdstr, BTIF_STORAGE_KEY_GATT_CLIENT_SUPPORTED);
                         btif_config_save();
                       }
                     },
                     bd_addr));
}

/** Store last server database hash for remote client */
void btif_storage_set_gatt_cl_db_hash(const RawAddress& bd_addr, Octet16 hash) {
  do_in_jni_thread(FROM_HERE, Bind(
                                  [](const RawAddress& bd_addr, Octet16 hash) {
                                    auto bdstr = bd_addr.ToString();
                                    btif_config_set_bin(
                                        bdstr,
                                        BTIF_STORAGE_KEY_GATT_CLIENT_DB_HASH,
                                        hash.data(), hash.size());
                                    btif_config_save();
                                  },
                                  bd_addr, hash));
}

/** Get last server database hash for remote client */
Octet16 btif_storage_get_gatt_cl_db_hash(const RawAddress& bd_addr) {
  auto bdstr = bd_addr.ToString();

  Octet16 hash;
  size_t size = hash.size();
  btif_config_get_bin(bdstr, BTIF_STORAGE_KEY_GATT_CLIENT_DB_HASH, hash.data(),
                      &size);

  return hash;
}

/** Remove las server database hash for remote client */
void btif_storage_remove_gatt_cl_db_hash(const RawAddress& bd_addr) {
  do_in_jni_thread(FROM_HERE,
                   Bind(
                       [](const RawAddress& bd_addr) {
                         auto bdstr = bd_addr.ToString();
                         if (btif_config_exist(
                                 bdstr, BTIF_STORAGE_KEY_GATT_CLIENT_DB_HASH)) {
                           btif_config_remove(
                               bdstr, BTIF_STORAGE_KEY_GATT_CLIENT_DB_HASH);
                           btif_config_save();
                         }
                       },
                       bd_addr));
}
+15 −2
Original line number Diff line number Diff line
@@ -140,6 +140,17 @@ static void gatt_update_last_srv_info() {
  }
}

/** Update database hash and client status */
static void gatt_update_for_database_change() {
  gatt_cb.database_hash = gatts_calculate_database_hash(gatt_cb.srv_list_info);

  uint8_t i = 0;
  for (i = 0; i < GATT_MAX_PHY_CHANNEL; i++) {
    tGATT_TCB& tcb = gatt_cb.tcb[i];
    if (tcb.in_use) gatt_sr_update_cl_status(tcb, /* chg_aware= */ false);
  }
}

/*******************************************************************************
 *
 * Function         GATTS_AddService
@@ -309,6 +320,7 @@ tGATT_STATUS GATTS_AddService(tGATT_IF gatt_if, btgatt_db_element_t* service,
          << ", e_hdl=" << loghex(elem.e_hdl) << ", type=" << loghex(elem.type)
          << ", sdp_hdl=" << loghex(elem.sdp_handle);

  gatt_update_for_database_change();
  gatt_proc_srv_chg();

  return GATT_SERVICE_STARTED;
@@ -359,12 +371,13 @@ bool GATTS_DeleteService(tGATT_IF gatt_if, Uuid* p_svc_uuid,
    return false;
  }

  gatt_proc_srv_chg();

  if (is_active_service(p_reg->app_uuid128, p_svc_uuid, svc_inst)) {
    GATTS_StopService(it->asgn_range.s_handle);
  }

  gatt_update_for_database_change();
  gatt_proc_srv_chg();

  VLOG(1) << "released handles s_hdl=" << loghex(it->asgn_range.s_handle)
          << ", e_hdl=" << loghex(it->asgn_range.e_handle);

+252 −30
Original line number Diff line number Diff line
@@ -29,9 +29,9 @@
#include "bt_target.h"
#include "bt_utils.h"
#include "btif/include/btif_storage.h"

#include "gatt_api.h"
#include "gatt_int.h"
#include "gd/common/init_flags.h"
#include "osi/include/log.h"
#include "osi/include/osi.h"

@@ -68,6 +68,15 @@ static void gatt_cl_op_cmpl_cback(uint16_t conn_id, tGATTC_OPTYPE op,

static void gatt_cl_start_config_ccc(tGATT_PROFILE_CLCB* p_clcb);

static bool gatt_sr_is_robust_caching_enabled();

static tGATT_STATUS gatt_sr_read_db_hash(uint16_t conn_id,
                                         tGATT_VALUE* p_value);
static tGATT_STATUS gatt_sr_read_cl_supp_feat(uint16_t conn_id,
                                              tGATT_VALUE* p_value);
static tGATT_STATUS gatt_sr_write_cl_supp_feat(uint16_t conn_id,
                                               tGATT_WRITE_REQ* p_data);

static tGATT_CBACK gatt_profile_cback = {gatt_connect_cback,
                                         gatt_cl_op_cmpl_cback,
                                         gatt_disc_res_cback,
@@ -207,18 +216,14 @@ tGATT_STATUS read_attr_value(uint16_t conn_id, uint16_t handle,
    /*GATT_UUID_CLIENT_SUP_FEAT */
    if (is_long) return GATT_NOT_LONG;

    tGATT_PROFILE_CLCB* p_clcb = gatt_profile_find_clcb_by_conn_id(conn_id);
    if (!p_clcb) {
      LOG(ERROR) << __func__ << " Context does not exist anymore for "
                 << int(conn_id);
      return GATT_ERR_UNLIKELY;
    return gatt_sr_read_cl_supp_feat(conn_id, p_value);
  }

    uint8_t cl_gatt_supp_feat = btif_storage_get_gatt_cl_supp_feat(p_clcb->bda);
    UINT8_TO_STREAM(p, cl_gatt_supp_feat);
  if (handle == gatt_cb.handle_of_database_hash) {
    /* GATT_UUID_DATABASE_HASH */
    if (is_long) return GATT_NOT_LONG;

    p_value->len = 1;
    return GATT_SUCCESS;
    return gatt_sr_read_db_hash(conn_id, p_value);
  }

  if (handle == gatt_cb.handle_of_h_r) {
@@ -243,31 +248,20 @@ tGATT_STATUS proc_read_req(uint16_t conn_id, tGATTS_REQ_TYPE,
/** GAP ATT server process a write request */
tGATT_STATUS proc_write_req(uint16_t conn_id, tGATTS_REQ_TYPE,
                            tGATT_WRITE_REQ* p_data) {
  uint16_t handle = p_data->handle;

  /* GATT_UUID_SERVER_SUP_FEAT*/
  if (p_data->handle == gatt_cb.handle_sr_supported_feat)
    return GATT_WRITE_NOT_PERMIT;
  if (handle == gatt_cb.handle_sr_supported_feat) return GATT_WRITE_NOT_PERMIT;

  /* GATT_UUID_CLIENT_SUP_FEAT*/
  if (p_data->handle == gatt_cb.handle_cl_supported_feat) {
    /* We store the value set by the peer but we don't use it */
    tGATT_PROFILE_CLCB* p_clcb = gatt_profile_find_clcb_by_conn_id(conn_id);
    if (!p_clcb) {
      LOG(ERROR) << __func__ << " Context does not exist anymore for "
                 << int(conn_id);
      return GATT_ERR_UNLIKELY;
    }

    uint8_t* p = p_data->value;
  if (handle == gatt_cb.handle_cl_supported_feat)
    return gatt_sr_write_cl_supp_feat(conn_id, p_data);

    uint8_t cl_gatt_supp_feat;
    STREAM_TO_UINT8(cl_gatt_supp_feat, p);

    btif_storage_set_gatt_cl_supp_feat(p_clcb->bda, cl_gatt_supp_feat);
    return GATT_SUCCESS;
  }
  /* GATT_UUID_DATABASE_HASH */
  if (handle == gatt_cb.handle_of_database_hash) return GATT_WRITE_NOT_PERMIT;

  /* GATT_UUID_GATT_SRV_CHGD */
  if (p_data->handle == gatt_cb.handle_of_h_r) return GATT_WRITE_NOT_PERMIT;
  if (handle == gatt_cb.handle_of_h_r) return GATT_WRITE_NOT_PERMIT;

  return GATT_NOT_FOUND;
}
@@ -333,6 +327,13 @@ static void gatt_connect_cback(UNUSED_ATTR tGATT_IF gatt_if,
  VLOG(1) << __func__ << ": from " << bda << " connected: " << connected
          << ", conn_id: " << loghex(conn_id);

  // if the device is not trusted, remove data when the link is disconnected
  if (!connected && !btm_sec_is_a_bonded_dev(bda)) {
    LOG(INFO) << __func__ << ": remove untrusted client status, bda=" << bda;
    btif_storage_remove_gatt_cl_supp_feat(bda);
    btif_storage_remove_gatt_cl_db_hash(bda);
  }

  tGATT_PROFILE_CLCB* p_clcb =
      gatt_profile_find_clcb_by_bd_addr(bda, transport);
  if (p_clcb == NULL) return;
@@ -374,6 +375,7 @@ void gatt_profile_db_init(void) {
  Uuid srv_changed_char_uuid = Uuid::From16Bit(GATT_UUID_GATT_SRV_CHGD);
  Uuid svr_sup_feat_uuid = Uuid::From16Bit(GATT_UUID_SERVER_SUP_FEAT);
  Uuid cl_sup_feat_uuid = Uuid::From16Bit(GATT_UUID_CLIENT_SUP_FEAT);
  Uuid database_hash_uuid = Uuid::From16Bit(GATT_UUID_DATABASE_HASH);

  btgatt_db_element_t service[] = {
      {
@@ -397,6 +399,12 @@ void gatt_profile_db_init(void) {
          .uuid = cl_sup_feat_uuid,
          .properties = GATT_CHAR_PROP_BIT_READ | GATT_CHAR_PROP_BIT_WRITE,
          .permissions = GATT_PERM_READ | GATT_PERM_WRITE,
      },
      {
          .uuid = database_hash_uuid,
          .type = BTGATT_DB_CHARACTERISTIC,
          .properties = GATT_CHAR_PROP_BIT_READ,
          .permissions = GATT_PERM_READ,
      }};

  GATTS_AddService(gatt_cb.gatt_if, service,
@@ -406,10 +414,14 @@ void gatt_profile_db_init(void) {
  gatt_cb.handle_of_h_r = service[1].attribute_handle;
  gatt_cb.handle_sr_supported_feat = service[2].attribute_handle;
  gatt_cb.handle_cl_supported_feat = service[3].attribute_handle;
  gatt_cb.handle_of_database_hash = service[4].attribute_handle;

  gatt_cb.gatt_svr_supported_feat_mask |= BLE_GATT_SVR_SUP_FEAT_EATT_BITMASK;
  gatt_cb.gatt_cl_supported_feat_mask |= BLE_GATT_CL_SUP_FEAT_EATT_BITMASK;

  if (gatt_sr_is_robust_caching_enabled())
    gatt_cb.gatt_cl_supported_feat_mask |= BLE_GATT_CL_SUP_FEAT_CACHING_BITMASK;

  VLOG(1) << __func__ << ": gatt_if=" << gatt_cb.gatt_if << " EATT supported";
}

@@ -771,3 +783,213 @@ bool gatt_profile_get_eatt_support(

  return gatt_svc_read_supp_feat_req(remote_bda, conn_id, std::move(cb));
}

/*******************************************************************************
 *
 * Function         gatt_sr_is_robust_caching_enabled
 *
 * Description      Check if Robust Caching is enabled on server side.
 *
 * Returns          true if enabled in gd flag, otherwise false
 *
 ******************************************************************************/
static bool gatt_sr_is_robust_caching_enabled() {
  return bluetooth::common::init_flags::gatt_robust_caching_is_enabled();
}

/*******************************************************************************
 *
 * Function         gatt_sr_is_cl_robust_caching_supported
 *
 * Description      Check if Robust Caching is supported for the connection
 *
 * Returns          true if enabled by client side, otherwise false
 *
 ******************************************************************************/
static bool gatt_sr_is_cl_robust_caching_supported(tGATT_TCB& tcb) {
  // if robust caching is not enabled, should always return false
  if (!gatt_sr_is_robust_caching_enabled()) return false;
  return (tcb.cl_supp_feat & BLE_GATT_CL_SUP_FEAT_CACHING_BITMASK);
}

/*******************************************************************************
 *
 * Function         gatt_sr_is_cl_change_aware
 *
 * Description      Check if the connection is change-aware
 *
 * Returns          true if change aware, otherwise false
 *
 ******************************************************************************/
bool gatt_sr_is_cl_change_aware(tGATT_TCB& tcb) {
  // if robust caching is not supported, should always return true by default
  if (!gatt_sr_is_cl_robust_caching_supported(tcb)) return true;
  return tcb.is_robust_cache_change_aware;
}

/*******************************************************************************
 *
 * Function         gatt_sr_init_cl_status
 *
 * Description      Restore status for trusted device
 *
 * Returns          none
 *
 ******************************************************************************/
void gatt_sr_init_cl_status(tGATT_TCB& tcb) {
  tcb.cl_supp_feat = btif_storage_get_gatt_cl_supp_feat(tcb.peer_bda);
  // This is used to reset bit when robust caching is disabled
  if (!gatt_sr_is_robust_caching_enabled()) {
    tcb.cl_supp_feat &= ~BLE_GATT_CL_SUP_FEAT_CACHING_BITMASK;
  }

  if (gatt_sr_is_cl_robust_caching_supported(tcb)) {
    Octet16 stored_hash = btif_storage_get_gatt_cl_db_hash(tcb.peer_bda);
    tcb.is_robust_cache_change_aware = (stored_hash == gatt_cb.database_hash);
  } else {
    // set default value for untrusted device
    tcb.is_robust_cache_change_aware = true;
  }

  LOG(INFO) << __func__ << ": bda=" << tcb.peer_bda
            << ", cl_supp_feat=" << loghex(tcb.cl_supp_feat)
            << ", aware=" << tcb.is_robust_cache_change_aware;
}

/*******************************************************************************
 *
 * Function         gatt_sr_update_cl_status
 *
 * Description      Update change-aware status for the remote device
 *
 * Returns          none
 *
 ******************************************************************************/
void gatt_sr_update_cl_status(tGATT_TCB& tcb, bool chg_aware) {
  // if robust caching is not supported, do nothing
  if (!gatt_sr_is_cl_robust_caching_supported(tcb)) return;

  // only when client status is changed from change-unaware to change-aware, we
  // can then store database hash into btif_storage
  if (!tcb.is_robust_cache_change_aware && chg_aware) {
    btif_storage_set_gatt_cl_db_hash(tcb.peer_bda, gatt_cb.database_hash);
  }

  // only when the status is changed, print the log
  if (tcb.is_robust_cache_change_aware != chg_aware) {
    LOG(INFO) << __func__ << ": bda=" << tcb.peer_bda
              << ", chg_aware=" << chg_aware;
  }

  tcb.is_robust_cache_change_aware = chg_aware;
}

/* handle request for reading database hash */
static tGATT_STATUS gatt_sr_read_db_hash(uint16_t conn_id,
                                         tGATT_VALUE* p_value) {
  LOG(INFO) << __func__ << ": conn_id=" << loghex(conn_id);

  uint8_t* p = p_value->value;
  Octet16& db_hash = gatt_cb.database_hash;
  ARRAY_TO_STREAM(p, db_hash.data(), (uint16_t)db_hash.size());
  p_value->len = (uint16_t)db_hash.size();

  // Every time when database hash is requested, reset flag.
  uint8_t tcb_idx = GATT_GET_TCB_IDX(conn_id);
  gatt_sr_update_cl_status(gatt_cb.tcb[tcb_idx], /* chg_aware= */ true);
  return GATT_SUCCESS;
}

/* handle request for reading client supported features */
static tGATT_STATUS gatt_sr_read_cl_supp_feat(uint16_t conn_id,
                                              tGATT_VALUE* p_value) {
  // Get tcb info
  uint8_t tcb_idx = GATT_GET_TCB_IDX(conn_id);
  tGATT_TCB& tcb = gatt_cb.tcb[tcb_idx];

  uint8_t* p = p_value->value;
  UINT8_TO_STREAM(p, tcb.cl_supp_feat);
  p_value->len = 1;

  return GATT_SUCCESS;
}

/* handle request for writing client supported features */
static tGATT_STATUS gatt_sr_write_cl_supp_feat(uint16_t conn_id,
                                               tGATT_WRITE_REQ* p_data) {
  std::list<uint8_t> tmp;
  uint16_t len = p_data->len;
  uint8_t value, *p = p_data->value;
  // Read all octets into list
  while (len > 0) {
    STREAM_TO_UINT8(value, p);
    tmp.push_back(value);
    len--;
  }
  // Remove trailing zero octets
  while (!tmp.empty()) {
    if (tmp.back() != 0x00) break;
    tmp.pop_back();
  }

  // Get tcb info
  uint8_t tcb_idx = GATT_GET_TCB_IDX(conn_id);
  tGATT_TCB& tcb = gatt_cb.tcb[tcb_idx];

  std::list<uint8_t> feature_list;
  feature_list.push_back(tcb.cl_supp_feat);

  // If input length is zero, return value_not_allowed
  if (tmp.empty()) {
    LOG(INFO) << __func__ << ": zero length, conn_id=" << loghex(conn_id)
              << ", bda=" << tcb.peer_bda;
    return GATT_VALUE_NOT_ALLOWED;
  }
  // if original length is longer than new one, it must be the bit reset case.
  if (feature_list.size() > tmp.size()) {
    LOG(INFO) << __func__ << ": shorter length, conn_id=" << loghex(conn_id)
              << ", bda=" << tcb.peer_bda;
    return GATT_VALUE_NOT_ALLOWED;
  }
  // new length is longer or equals to the original, need to check bits
  // one by one. Here we use bit-wise operation.
  // 1. Use XOR to locate the change bit, val_xor is the change bit mask
  // 2. Use AND for val_xor and *it_new to get val_and
  // 3. If val_and != val_xor, it means the change is from 1 to 0
  auto it_old = feature_list.cbegin();
  auto it_new = tmp.cbegin();
  for (; it_old != feature_list.cend(); it_old++, it_new++) {
    uint8_t val_xor = *it_old ^ *it_new;
    uint8_t val_and = val_xor & *it_new;
    if (val_and != val_xor) {
      LOG(INFO) << __func__
                << ": bit cannot be reset, conn_id=" << loghex(conn_id)
                << ", bda=" << tcb.peer_bda;
      return GATT_VALUE_NOT_ALLOWED;
    }
  }

  // get current robust caching status before setting new one
  bool curr_caching_state = gatt_sr_is_cl_robust_caching_supported(tcb);

  tcb.cl_supp_feat = tmp.front();
  if (!gatt_sr_is_robust_caching_enabled()) {
    // remove robust caching bit
    tcb.cl_supp_feat &= ~BLE_GATT_CL_SUP_FEAT_CACHING_BITMASK;
    LOG(INFO) << __func__
              << ": reset robust caching bit, conn_id=" << loghex(conn_id)
              << ", bda=" << tcb.peer_bda;
  }
  // TODO(hylo): save data as byte array
  btif_storage_set_gatt_cl_supp_feat(tcb.peer_bda, tcb.cl_supp_feat);

  // get new robust caching status after setting new one
  bool new_caching_state = gatt_sr_is_cl_robust_caching_supported(tcb);
  // only when the first time robust caching request, print the log
  if (!curr_caching_state && new_caching_state) {
    LOG(INFO) << __func__ << ": robust caching enabled by client"
              << ", conn_id=" << loghex(conn_id);
  }

  return GATT_SUCCESS;
}
+13 −0
Original line number Diff line number Diff line
@@ -300,6 +300,12 @@ typedef struct {
  std::queue<tGATT_CMD_Q> cl_cmd_q;
  alarm_t* ind_ack_timer; /* local app confirm to indication timer */

  // TODO(hylo): support byte array data
  /* Client supported feature*/
  uint8_t cl_supp_feat;
  /* Use for server. if false, should handle database out of sync. */
  bool is_robust_cache_change_aware;

  bool in_use;
  uint8_t tcb_idx;
} tGATT_TCB;
@@ -398,6 +404,9 @@ typedef struct {
   */
  uint8_t gatt_cl_supported_feat_mask;

  uint16_t handle_of_database_hash;
  Octet16 database_hash;

  tGATT_APPL_INFO cb_info;

  tGATT_HDL_CFG hdl_cfg;
@@ -440,6 +449,10 @@ extern bool gatt_profile_get_eatt_support(
    const RawAddress& remote_bda,
    base::OnceCallback<void(const RawAddress&, bool)> cb);

extern bool gatt_sr_is_cl_change_aware(tGATT_TCB& tcb);
extern void gatt_sr_init_cl_status(tGATT_TCB& tcb);
extern void gatt_sr_update_cl_status(tGATT_TCB& tcb, bool chg_unaware);

/* Functions provided by att_protocol.cc */
extern tGATT_STATUS attp_send_cl_confirmation_msg(tGATT_TCB& tcb, uint16_t cid);
extern tGATT_STATUS attp_send_cl_msg(tGATT_TCB& tcb, tGATT_CLCB* p_clcb,
Loading