Files
2026-07-13 13:04:25 +08:00

1548 lines
61 KiB
C

/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
/* LE L2CAP Enhanced Credit Based Flow Control (ECFC) - signaling 0x17/0x18/0x19/0x1A. */
#include <string.h>
#include "device/controller.h"
#include "stack/bt_types.h"
#include "stack/l2cdefs.h"
#include "l2c_int.h"
#include "stack/l2c_api.h"
#include "stack/btu.h"
#if (BLE_EATT_INCLUDED == TRUE)
#include "gatt_eatt_int.h"
#endif
#if (BLE_L2CAP_ENHANCED_COC_INCLUDED == TRUE)
#define L2C_BLE_ECFC_TRACE_API(fmt, ...) L2CAP_TRACE_API("LE_ECFC: " fmt, ##__VA_ARGS__)
#define L2C_BLE_ECFC_TRACE_DEBUG(fmt, ...) L2CAP_TRACE_DEBUG("LE_ECFC: " fmt, ##__VA_ARGS__)
#define L2C_BLE_ECFC_TRACE_WARN(fmt, ...) L2CAP_TRACE_WARNING("LE_ECFC: " fmt, ##__VA_ARGS__)
#define L2C_BLE_ECFC_TRACE_ERROR(fmt, ...) L2CAP_TRACE_ERROR("LE_ECFC: " fmt, ##__VA_ARGS__)
/* Core Spec v6.2 Vol 3 Part A 4.25/4.27: a single enhanced credit based
* connection or reconfiguration request may target at most five channels. This
* is independent of BLE_MAX_L2CAP_CLIENTS, which sizes the local channel
* pool and can be configured up to 15. */
#define L2C_BLE_ECFC_MAX_REQ_CHANS 5
typedef struct {
BOOLEAN in_use;
BOOLEAN pending_link;
UINT8 sig_id;
UINT8 num_chan;
tL2C_LCB *p_lcb;
tL2C_CCB *ccbs[BLE_MAX_L2CAP_CLIENTS];
} tL2C_BLE_ECFC_CL_TXN;
typedef struct {
BOOLEAN in_use;
UINT8 rem_id;
tL2C_LCB *p_lcb;
UINT8 num_total;
UINT8 num_done;
BOOLEAN rsp_recorded; /* canonical rsp_* captured from first accepted chan */
UINT16 reject_result; /* app-supplied reject reason (security codes prioritized) */
UINT16 rsp_mtu;
UINT16 rsp_mps;
UINT16 rsp_credits;
tL2C_CCB *ccbs[BLE_MAX_L2CAP_CLIENTS];
BOOLEAN accepted[BLE_MAX_L2CAP_CLIENTS];
} tL2C_BLE_ECFC_SRV_TXN;
typedef struct {
BOOLEAN in_use;
UINT8 sig_id;
UINT8 num_chan;
UINT16 new_mtu;
UINT16 new_mps;
tL2C_LCB *p_lcb;
tL2C_CCB *ccbs[BLE_MAX_L2CAP_CLIENTS];
} tL2C_BLE_ECFC_RECONFIG_TXN;
#if (BLE_L2CAP_COC_CLIENT_INCLUDED == TRUE)
static tL2C_BLE_ECFC_CL_TXN s_ecfc_cl_txn[MAX_L2CAP_LINKS];
#endif
#if (BLE_L2CAP_COC_SERVER_INCLUDED == TRUE)
static tL2C_BLE_ECFC_SRV_TXN s_ecfc_srv_txn[MAX_L2CAP_LINKS];
#endif
static tL2C_BLE_ECFC_RECONFIG_TXN s_ecfc_reconfig_txn[MAX_L2CAP_LINKS];
static void l2c_ble_ecfc_apply_default_cfg(tL2CAP_LE_CFG_INFO *cfg);
static void l2c_ble_ecfc_open_ccb(tL2C_CCB *p_ccb, UINT16 result);
#if (BLE_L2CAP_COC_CLIENT_INCLUDED == TRUE)
static tL2C_BLE_ECFC_CL_TXN *l2c_ble_ecfc_find_cl_txn(tL2C_LCB *p_lcb, UINT8 sig_id);
static void l2c_ble_ecfc_cl_txn_free(tL2C_BLE_ECFC_CL_TXN *txn);
static BOOLEAN l2c_ble_ecfc_cl_txn_has_ccb(tL2C_BLE_ECFC_CL_TXN *txn);
static BOOLEAN l2c_ble_ecfc_cl_send_connect(tL2C_BLE_ECFC_CL_TXN *txn);
#endif
static tL2C_BLE_ECFC_RECONFIG_TXN *l2c_ble_ecfc_find_reconfig_txn(tL2C_LCB *p_lcb, UINT8 sig_id);
#if (BLE_L2CAP_COC_SERVER_INCLUDED == TRUE)
static void l2c_ble_ecfc_srv_txn_free(tL2C_BLE_ECFC_SRV_TXN *txn);
static void l2c_ble_ecfc_srv_txn_try_complete(tL2C_BLE_ECFC_SRV_TXN *txn);
static void l2c_ble_ecfc_sec_cback(BD_ADDR bd_addr, tBT_TRANSPORT transport,
void *p_ref_data, tBTM_STATUS result);
static void l2c_ble_ecfc_fire_connect_ind(tL2C_BLE_ECFC_SRV_TXN *txn);
static BOOLEAN l2c_ble_ecfc_srv_txn_has_ccb(tL2C_BLE_ECFC_SRV_TXN *txn);
#endif
static BOOLEAN l2c_ble_ecfc_reconfig_txn_has_ccb(tL2C_BLE_ECFC_RECONFIG_TXN *txn);
static void l2c_ble_ecfc_apply_default_cfg(tL2CAP_LE_CFG_INFO *cfg)
{
if (cfg->mtu == 0) {
cfg->mtu = L2CAP_LE_DEFAULT_MTU;
}
if (cfg->mps == 0) {
cfg->mps = L2CAP_LE_COC_MPS;
}
cfg->mps = L2CAP_LE_CLAMP_MPS(cfg->mps);
if (cfg->mps < L2CAP_LE_ECFC_MIN_MPS) {
cfg->mps = L2CAP_LE_ECFC_MIN_MPS;
}
if (cfg->mtu < L2CAP_LE_ECFC_MIN_MTU) {
cfg->mtu = L2CAP_LE_ECFC_MIN_MTU;
}
if (cfg->credits == 0) {
cfg->credits = L2CAP_LE_INIT_CREDITS;
}
}
#if (BLE_L2CAP_COC_CLIENT_INCLUDED == TRUE)
static tL2C_BLE_ECFC_CL_TXN *l2c_ble_ecfc_alloc_cl_txn(tL2C_LCB *p_lcb, UINT8 sig_id)
{
for (int i = 0; i < MAX_L2CAP_LINKS; i++) {
if (!s_ecfc_cl_txn[i].in_use) {
memset(&s_ecfc_cl_txn[i], 0, sizeof(s_ecfc_cl_txn[i]));
s_ecfc_cl_txn[i].in_use = TRUE;
s_ecfc_cl_txn[i].p_lcb = p_lcb;
s_ecfc_cl_txn[i].sig_id = sig_id;
return &s_ecfc_cl_txn[i];
}
}
L2C_BLE_ECFC_TRACE_WARN("alloc cl txn failed sig_id=%u", sig_id);
return NULL;
}
static tL2C_BLE_ECFC_CL_TXN *l2c_ble_ecfc_find_cl_txn(tL2C_LCB *p_lcb, UINT8 sig_id)
{
for (int i = 0; i < MAX_L2CAP_LINKS; i++) {
if (s_ecfc_cl_txn[i].in_use && s_ecfc_cl_txn[i].p_lcb == p_lcb &&
s_ecfc_cl_txn[i].sig_id == sig_id) {
return &s_ecfc_cl_txn[i];
}
}
return NULL;
}
static void l2c_ble_ecfc_cl_txn_free(tL2C_BLE_ECFC_CL_TXN *txn)
{
if (txn) {
memset(txn, 0, sizeof(*txn));
}
}
static BOOLEAN l2c_ble_ecfc_cl_txn_has_ccb(tL2C_BLE_ECFC_CL_TXN *txn)
{
for (int i = 0; i < txn->num_chan; i++) {
if (txn->ccbs[i] != NULL) {
return TRUE;
}
}
return FALSE;
}
static BOOLEAN l2c_ble_ecfc_cl_send_connect(tL2C_BLE_ECFC_CL_TXN *txn)
{
tL2C_LCB *p_lcb;
UINT16 scids[BLE_MAX_L2CAP_CLIENTS];
int i;
if (txn == NULL || txn->num_chan == 0) {
return FALSE;
}
p_lcb = txn->p_lcb;
if (p_lcb == NULL || p_lcb->link_state != LST_CONNECTED) {
return FALSE;
}
/* Validate every CCB before mutating any state: a NULL entry mid-array (a CCB
* released via on_ccb_release while the txn waited for the link) must not
* leave earlier CCBs stuck in CST_W4_L2CAP_CONNECT_RSP with no rollback. */
for (i = 0; i < txn->num_chan; i++) {
if (txn->ccbs[i] == NULL || txn->ccbs[i]->p_rcb == NULL) {
return FALSE;
}
}
for (i = 0; i < txn->num_chan; i++) {
scids[i] = txn->ccbs[i]->local_cid;
txn->ccbs[i]->local_id = txn->sig_id;
txn->ccbs[i]->chnl_state = CST_W4_L2CAP_CONNECT_RSP;
}
/* If the request could not even be built/queued (e.g. buffer OOM), the peer
* will never respond, so the CCBs would stay stuck in
* CST_W4_L2CAP_CONNECT_RSP forever. Roll the state back and report failure so
* the caller can clean up. */
if (!l2cu_send_peer_ble_enhanced_credit_conn_req(p_lcb, txn->sig_id,
txn->ccbs[0]->p_rcb->real_psm,
txn->ccbs[0]->local_conn_cfg.mtu,
txn->ccbs[0]->local_conn_cfg.mps,
txn->ccbs[0]->local_conn_cfg.credits,
txn->num_chan, scids)) {
L2C_BLE_ECFC_TRACE_ERROR("0x17 send failed sig_id=%u", txn->sig_id);
for (i = 0; i < txn->num_chan; i++) {
if (txn->ccbs[i]) {
txn->ccbs[i]->chnl_state = CST_CLOSED;
}
}
return FALSE;
}
/* Guard against a peer that never sends the 0x18 response: one timer per
* transaction (on ccbs[0]) aborts the whole batch on expiry. */
l2c_ble_le_coc_start_rsp_timer(txn->ccbs[0], L2CAP_CHNL_CONNECT_TOUT);
return TRUE;
}
void l2c_ble_ecfc_on_link_up(tL2C_LCB *p_lcb)
{
if (p_lcb == NULL) {
return;
}
for (int i = 0; i < MAX_L2CAP_LINKS; i++) {
tL2C_BLE_ECFC_CL_TXN *txn = &s_ecfc_cl_txn[i];
if (!txn->in_use || !txn->pending_link || txn->p_lcb != p_lcb) {
continue;
}
txn->pending_link = FALSE;
if (!l2c_ble_ecfc_cl_send_connect(txn)) {
L2C_BLE_ECFC_TRACE_ERROR("deferred ConnectLEEcocReq send failed");
/* Fail every channel so the app is notified and the CCBs are
* released (open_ccb releases on non-OK), then free the txn to
* avoid leaking it and leaving CCBs stuck in W4_CONNECT_RSP.
* Snapshot then free before opening (see l2c_ble_ecfc_abort_cl_txn):
* a non-OK open re-enters on_ccb_release and may free/realloc this
* slot during the callback chain. */
tL2C_CCB *ccbs[BLE_MAX_L2CAP_CLIENTS];
UINT8 num_chan = txn->num_chan;
if (num_chan > BLE_MAX_L2CAP_CLIENTS) {
num_chan = BLE_MAX_L2CAP_CLIENTS;
}
memcpy(ccbs, txn->ccbs, num_chan * sizeof(tL2C_CCB *));
l2c_ble_ecfc_cl_txn_free(txn);
for (int j = 0; j < num_chan; j++) {
if (ccbs[j]) {
l2c_ble_ecfc_open_ccb(ccbs[j], L2CAP_CONN_NO_PSM);
}
}
}
}
}
#endif /* BLE_L2CAP_COC_CLIENT_INCLUDED */
#if (BLE_L2CAP_COC_SERVER_INCLUDED == TRUE)
static tL2C_BLE_ECFC_SRV_TXN *l2c_ble_ecfc_alloc_srv_txn(tL2C_LCB *p_lcb, UINT8 rem_id)
{
for (int i = 0; i < MAX_L2CAP_LINKS; i++) {
if (!s_ecfc_srv_txn[i].in_use) {
memset(&s_ecfc_srv_txn[i], 0, sizeof(s_ecfc_srv_txn[i]));
s_ecfc_srv_txn[i].in_use = TRUE;
s_ecfc_srv_txn[i].p_lcb = p_lcb;
s_ecfc_srv_txn[i].rem_id = rem_id;
return &s_ecfc_srv_txn[i];
}
}
L2C_BLE_ECFC_TRACE_WARN("alloc srv txn failed rem_id=%u", rem_id);
return NULL;
}
static tL2C_BLE_ECFC_SRV_TXN *l2c_ble_ecfc_find_srv_txn_by_ccb(tL2C_CCB *p_ccb)
{
for (int i = 0; i < MAX_L2CAP_LINKS; i++) {
if (!s_ecfc_srv_txn[i].in_use) {
continue;
}
for (int j = 0; j < s_ecfc_srv_txn[i].num_total; j++) {
if (s_ecfc_srv_txn[i].ccbs[j] == p_ccb) {
return &s_ecfc_srv_txn[i];
}
}
}
return NULL;
}
static void l2c_ble_ecfc_srv_txn_free(tL2C_BLE_ECFC_SRV_TXN *txn)
{
if (txn) {
#if (SMP_INCLUDED == TRUE)
/* The server security check (l2ble_sec_access_req in
* l2c_ble_ecfc_handle_conn_req) is queued with p_ref_data == txn, not a
* CCB, so the CCB-based flush in l2c_ble_le_coc_cleanup_ccb never matches
* it. If the txn is torn down (e.g. link loss) while that check is still
* outstanding, drop the pending request here so the deferred SMP callback
* cannot later fire l2c_ble_ecfc_sec_cback on a reused txn slot. Must run
* before the memset since p_lcb is needed for the queue lookup. */
l2ble_sec_flush_pending_req(txn->p_lcb, txn);
#endif
memset(txn, 0, sizeof(*txn));
}
}
static BOOLEAN l2c_ble_ecfc_srv_txn_has_ccb(tL2C_BLE_ECFC_SRV_TXN *txn)
{
for (int i = 0; i < txn->num_total; i++) {
if (txn->ccbs[i] != NULL) {
return TRUE;
}
}
return FALSE;
}
#endif /* BLE_L2CAP_COC_SERVER_INCLUDED */
static tL2C_BLE_ECFC_RECONFIG_TXN *l2c_ble_ecfc_alloc_reconfig_txn(tL2C_LCB *p_lcb, UINT8 sig_id)
{
for (int i = 0; i < MAX_L2CAP_LINKS; i++) {
if (!s_ecfc_reconfig_txn[i].in_use) {
memset(&s_ecfc_reconfig_txn[i], 0, sizeof(s_ecfc_reconfig_txn[i]));
s_ecfc_reconfig_txn[i].in_use = TRUE;
s_ecfc_reconfig_txn[i].p_lcb = p_lcb;
s_ecfc_reconfig_txn[i].sig_id = sig_id;
return &s_ecfc_reconfig_txn[i];
}
}
L2C_BLE_ECFC_TRACE_WARN("alloc reconfig txn failed sig_id=%u", sig_id);
return NULL;
}
static tL2C_BLE_ECFC_RECONFIG_TXN *l2c_ble_ecfc_find_reconfig_txn(tL2C_LCB *p_lcb, UINT8 sig_id)
{
for (int i = 0; i < MAX_L2CAP_LINKS; i++) {
if (s_ecfc_reconfig_txn[i].in_use && s_ecfc_reconfig_txn[i].p_lcb == p_lcb &&
s_ecfc_reconfig_txn[i].sig_id == sig_id) {
return &s_ecfc_reconfig_txn[i];
}
}
return NULL;
}
static BOOLEAN l2c_ble_ecfc_reconfig_txn_has_ccb(tL2C_BLE_ECFC_RECONFIG_TXN *txn)
{
for (int i = 0; i < txn->num_chan; i++) {
if (txn->ccbs[i] != NULL) {
return TRUE;
}
}
return FALSE;
}
void l2c_ble_ecfc_on_ccb_release(tL2C_CCB *p_ccb)
{
int t, i;
if (p_ccb == NULL || p_ccb->p_lcb == NULL ||
p_ccb->p_lcb->transport != BT_TRANSPORT_LE) {
return;
}
#if (BLE_L2CAP_COC_CLIENT_INCLUDED == TRUE)
for (t = 0; t < MAX_L2CAP_LINKS; t++) {
tL2C_BLE_ECFC_CL_TXN *cl = &s_ecfc_cl_txn[t];
if (!cl->in_use) {
continue;
}
for (i = 0; i < cl->num_chan; i++) {
if (cl->ccbs[i] == p_ccb) {
cl->ccbs[i] = NULL;
}
}
if (!l2c_ble_ecfc_cl_txn_has_ccb(cl)) {
l2c_ble_ecfc_cl_txn_free(cl);
}
}
#endif
#if (BLE_L2CAP_COC_SERVER_INCLUDED == TRUE)
for (t = 0; t < MAX_L2CAP_LINKS; t++) {
tL2C_BLE_ECFC_SRV_TXN *srv = &s_ecfc_srv_txn[t];
if (!srv->in_use) {
continue;
}
for (i = 0; i < srv->num_total; i++) {
if (srv->ccbs[i] == p_ccb) {
srv->ccbs[i] = NULL;
if (!srv->accepted[i]) {
srv->num_done++;
}
}
}
if (!srv->in_use) {
continue;
}
if (srv->num_done >= srv->num_total) {
l2c_ble_ecfc_srv_txn_try_complete(srv);
} else if (!l2c_ble_ecfc_srv_txn_has_ccb(srv) && srv->num_done == 0) {
/* Abandon a txn whose CCBs were torn down before any connect_rsp
* (e.g. link loss). Do not free when num_done > 0: connect_rsp may
* still be aggregating rejects and must send the 0x18 response. */
l2c_ble_ecfc_srv_txn_free(srv);
}
}
#endif
for (t = 0; t < MAX_L2CAP_LINKS; t++) {
tL2C_BLE_ECFC_RECONFIG_TXN *rc = &s_ecfc_reconfig_txn[t];
if (!rc->in_use) {
continue;
}
for (i = 0; i < rc->num_chan; i++) {
if (rc->ccbs[i] == p_ccb) {
rc->ccbs[i] = NULL;
}
}
if (!l2c_ble_ecfc_reconfig_txn_has_ccb(rc)) {
memset(rc, 0, sizeof(*rc));
}
}
}
static void l2c_ble_ecfc_open_ccb(tL2C_CCB *p_ccb, UINT16 result)
{
l2c_ble_le_coc_open_channel(p_ccb, result);
}
#if (BLE_L2CAP_COC_SERVER_INCLUDED == TRUE)
static void l2c_ble_ecfc_srv_txn_try_complete(tL2C_BLE_ECFC_SRV_TXN *txn)
{
UINT16 dcids[BLE_MAX_L2CAP_CLIENTS];
UINT16 result = L2CAP_LE_RESULT_CONN_OK;
UINT8 created = 0;
int i;
if (txn == NULL || !txn->in_use || txn->num_done < txn->num_total) {
return;
}
/* Once num_done == num_total, every channel was either accepted (accepted[i]
* set with the CCB opened) or already released (ccbs[i] set NULL at the point
* num_done was incremented). So a not-accepted, still-attached CCB cannot
* exist here: reporting the accepted ones and zeroing the rest is enough,
* and we must not call l2cu_release_ccb() from this path (it would re-enter
* l2c_ble_ecfc_on_ccb_release() -> try_complete() and send a duplicate res). */
for (i = 0; i < txn->num_total; i++) {
if (txn->accepted[i] && txn->ccbs[i] && txn->ccbs[i]->chnl_state == CST_OPEN) {
dcids[i] = txn->ccbs[i]->local_cid;
created++;
} else {
dcids[i] = 0;
}
}
/* Application/resource level rejection (all DCIDs 0). Prefer the specific
* app-supplied reason (e.g. a security code 0x0005-0x0008, mandated by Core
* Spec v6.2 Vol 3 Part A 10.2) so the peer learns to encrypt/authenticate
* instead of treating it as a transient resource shortage. Fall back to
* "insufficient resources" (0x0004) when no specific reason was recorded. */
if (created == 0) {
result = (txn->reject_result != L2CAP_LE_RESULT_CONN_OK) ?
txn->reject_result : L2CAP_LE_RESULT_NO_RESOURCES;
} else if (created < txn->num_total) {
result = L2CAP_LE_RESULT_NO_RESOURCES;
}
l2cu_send_peer_ble_enhanced_credit_conn_res(txn->p_lcb, txn->rem_id,
txn->rsp_mtu, txn->rsp_mps, txn->rsp_credits, result,
txn->num_total, dcids);
l2c_ble_ecfc_srv_txn_free(txn);
}
static void l2c_ble_ecfc_fire_connect_ind(tL2C_BLE_ECFC_SRV_TXN *txn)
{
tL2CA_CONNECT_IND_CB *ind_cb;
int i;
for (i = 0; i < txn->num_total; i++) {
tL2C_CCB *p_ccb = txn->ccbs[i];
if (p_ccb == NULL || p_ccb->p_rcb == NULL) {
continue;
}
/* Skip a channel already accepted by a connect_rsp re-entered from an
* earlier iteration's ind_cb (batch accept sharing rem_id); firing ind_cb
* again would deliver a duplicate ConnectInd for an already-open channel.
* Mirrors the accepted[] duplicate guard in l2c_ble_ecfc_connect_rsp. */
if (txn->accepted[i]) {
continue;
}
ind_cb = p_ccb->p_rcb->api.pL2CA_ConnectInd_Cb;
if (ind_cb) {
L2C_BLE_ECFC_TRACE_API("ConnectInd lcid=0x%04x psm=0x%04x id=%u",
p_ccb->local_cid, p_ccb->p_rcb->real_psm, txn->rem_id);
if (txn->in_use) {
(*ind_cb)(p_ccb->p_lcb->remote_bd_addr, p_ccb->local_cid,
p_ccb->p_rcb->real_psm, txn->rem_id);
}
} else {
/* Increment num_done BEFORE open_ccb: open_ccb fires ConnectCfm,
* whose handler may synchronously release this CCB and re-enter
* l2c_ble_ecfc_on_ccb_release(). If num_done were still 0 there, the
* abandon guard (!has_ccb && num_done == 0) would free the txn out
* from under us and the 0x18 response would never be sent. */
txn->accepted[i] = TRUE;
txn->num_done++;
l2c_ble_ecfc_open_ccb(p_ccb, L2CAP_CONN_OK);
}
}
if (txn->in_use) {
l2c_ble_ecfc_srv_txn_try_complete(txn);
}
}
static void l2c_ble_ecfc_sec_cback(BD_ADDR bd_addr, tBT_TRANSPORT transport,
void *p_ref_data, tBTM_STATUS result)
{
tL2C_BLE_ECFC_SRV_TXN *txn = (tL2C_BLE_ECFC_SRV_TXN *)p_ref_data;
UNUSED(transport);
L2C_BLE_ECFC_TRACE_DEBUG("sec_cback bda=%02x:%02x:%02x:%02x:%02x:%02x result=%u rem_id=%u num_total=%u",
bd_addr[0], bd_addr[1], bd_addr[2], bd_addr[3], bd_addr[4], bd_addr[5],
result, txn ? txn->rem_id : 0, txn ? txn->num_total : 0);
if (txn == NULL || !txn->in_use) {
L2C_BLE_ECFC_TRACE_WARN("sec_cback txn invalid");
return;
}
if (result != BTM_SUCCESS) {
L2C_BLE_ECFC_TRACE_WARN("sec_cback security failed result=%u rem_id=%u", result, txn->rem_id);
l2cu_reject_ble_enhanced_connection(txn->p_lcb, txn->rem_id,
l2c_ble_coc_sec_status_to_result(bd_addr, result),
txn->num_total);
for (int i = 0; i < txn->num_total; i++) {
if (txn->ccbs[i]) {
tL2C_CCB *p_rel = txn->ccbs[i];
txn->ccbs[i] = NULL;
l2cu_release_ccb(p_rel);
}
}
/* Releasing the last CCB above can reach l2c_ble_ecfc_on_ccb_release,
* whose abandon guard (all ccbs[] NULL && num_done == 0) may already have
* freed this txn. Only free again if it is still in use, mirroring the
* guard in l2c_ble_ecfc_fire_connect_ind, to avoid a double free. */
if (txn->in_use) {
l2c_ble_ecfc_srv_txn_free(txn);
}
return;
}
L2C_BLE_ECFC_TRACE_DEBUG("sec_cback security ok rem_id=%u, fire connect ind", txn->rem_id);
l2c_ble_ecfc_fire_connect_ind(txn);
}
#endif /* BLE_L2CAP_COC_SERVER_INCLUDED */
#if (BLE_L2CAP_COC_SERVER_INCLUDED == TRUE)
void l2c_ble_ecfc_connect_rsp(tL2C_CCB *p_ccb, UINT16 result)
{
tL2C_BLE_ECFC_SRV_TXN *txn;
int idx;
L2C_BLE_ECFC_TRACE_DEBUG("connect_rsp local_cid=0x%04x result=%u",
p_ccb ? p_ccb->local_cid : 0, result);
if (p_ccb == NULL || !p_ccb->le_ecfc_channel) {
L2C_BLE_ECFC_TRACE_WARN("connect_rsp not an ECFC channel");
return;
}
txn = l2c_ble_ecfc_find_srv_txn_by_ccb(p_ccb);
if (txn == NULL) {
L2C_BLE_ECFC_TRACE_WARN("connect_rsp srv txn not found lcid=0x%04x", p_ccb->local_cid);
return;
}
for (idx = 0; idx < txn->num_total; idx++) {
if (txn->ccbs[idx] == p_ccb) {
break;
}
}
if (idx >= txn->num_total) {
L2C_BLE_ECFC_TRACE_WARN("connect_rsp ccb not in txn lcid=0x%04x", p_ccb->local_cid);
return;
}
/* Guard against a duplicate response for the same channel (e.g. the app
* calls accept twice for one chan_handle). Re-processing would inflate
* num_done, fire a second ConnectCfm and re-seed the RX window. */
if (txn->accepted[idx]) {
L2C_BLE_ECFC_TRACE_WARN("connect_rsp duplicate response lcid=0x%04x", p_ccb->local_cid);
return;
}
if (result == L2CAP_CONN_OK) {
l2c_ble_ecfc_apply_default_cfg(&p_ccb->local_conn_cfg);
/* The 0x18 response carries a single MTU/MPS/credits set that the peer
* applies uniformly to every accepted channel (Core Spec v6.2 Vol 3
* Part A 4.26). Record the canonical values from the first accepted
* channel and force every subsequent channel's local config to match, so
* the RX window (le_coc_rx_avail) seeded in l2c_ble_le_coc_open_channel
* stays consistent with what the peer is told. */
if (!txn->rsp_recorded) {
txn->rsp_mtu = p_ccb->local_conn_cfg.mtu;
txn->rsp_mps = p_ccb->local_conn_cfg.mps;
txn->rsp_credits = p_ccb->local_conn_cfg.credits;
txn->rsp_recorded = TRUE;
} else {
p_ccb->local_conn_cfg.mtu = txn->rsp_mtu;
p_ccb->local_conn_cfg.mps = txn->rsp_mps;
p_ccb->local_conn_cfg.credits = txn->rsp_credits;
}
/* Increment num_done BEFORE open_ccb so a synchronous CCB release from
* within the ConnectCfm callback (re-entering on_ccb_release) cannot hit
* the abandon guard (!has_ccb && num_done == 0) and free the txn before
* the 0x18 response is sent. */
txn->accepted[idx] = TRUE;
txn->num_done++;
l2c_ble_ecfc_open_ccb(p_ccb, L2CAP_CONN_OK);
/* open_ccb fires ConnectCfm synchronously; its handler may release this
* CCB and re-enter on_ccb_release -> try_complete, which frees the txn.
* Skip the second try_complete on a freed txn (mirrors the in_use guard
* in l2c_ble_ecfc_fire_connect_ind and l2c_ble_ecfc_sec_cback). */
if (txn->in_use) {
l2c_ble_ecfc_srv_txn_try_complete(txn);
}
return;
}
/* Record the app-supplied reject reason. A security-related code
* (0x0005-0x0008) takes precedence over a previously recorded generic code
* so the aggregate response conveys the strongest security requirement. */
if (result != L2CAP_CONN_OK) {
BOOLEAN is_sec = (result >= L2CAP_LE_RESULT_INSUFFICIENT_AUTHENTICATION &&
result <= L2CAP_LE_RESULT_INSUFFICIENT_ENCRY);
if (txn->reject_result == L2CAP_LE_RESULT_CONN_OK || is_sec) {
txn->reject_result = result;
}
}
/* Reject: aggregate the 0x18 response before releasing the CCB. Pre-nulling
* ccbs[idx] and calling l2cu_release_ccb() first lets on_ccb_release() free the
* txn while num_done is still short, so try_complete() never reaches the peer. */
txn->num_done++;
if (txn->num_done >= txn->num_total) {
l2c_ble_ecfc_srv_txn_try_complete(txn);
l2cu_release_ccb(p_ccb);
return;
}
txn->ccbs[idx] = NULL;
l2cu_release_ccb(p_ccb);
}
#endif /* BLE_L2CAP_COC_SERVER_INCLUDED */
#if (BLE_L2CAP_COC_CLIENT_INCLUDED == TRUE)
UINT8 L2CA_ConnectLEEcocReq(UINT16 psm, BD_ADDR p_bd_addr, tL2CAP_LE_CFG_INFO *p_cfg,
UINT8 num_chan, UINT16 *p_lcids)
{
tL2C_RCB *p_rcb;
tL2C_LCB *p_lcb;
tL2C_CCB *p_ccb;
tL2C_BLE_ECFC_CL_TXN *txn;
UINT16 scids[BLE_MAX_L2CAP_CLIENTS];
UINT8 sig_id;
int i;
if (num_chan == 0 || num_chan > BLE_MAX_L2CAP_CLIENTS ||
num_chan > L2C_BLE_ECFC_MAX_REQ_CHANS) {
L2C_BLE_ECFC_TRACE_WARN("ConnectLEEcocReq invalid num_chan=%u", num_chan);
return 0;
}
L2C_BLE_ECFC_TRACE_API("ConnectLEEcocReq psm=0x%04x num=%u", psm, num_chan);
if (!BTM_IsDeviceUp()) {
L2C_BLE_ECFC_TRACE_WARN("ConnectLEEcocReq BTM not up psm=0x%04x", psm);
return 0;
}
/* Bail out before allocating an LCB if BLE is unsupported: l2cu_create_conn()'s
* !supports_ble() path returns FALSE without releasing the LCB, so relying on
* it below would leak the freshly-allocated LCB (mirrors L2CA_ConnectLECocReq). */
if (!controller_get_interface()->supports_ble()) {
L2C_BLE_ECFC_TRACE_WARN("ConnectLEEcocReq BLE not supported");
return 0;
}
p_rcb = l2cu_find_ble_rcb_by_psm(psm);
if (p_rcb == NULL) {
L2C_BLE_ECFC_TRACE_WARN("ConnectLEEcocReq no RCB for psm=0x%04x", psm);
return 0;
}
p_lcb = l2cu_find_lcb_by_bd_addr(p_bd_addr, BT_TRANSPORT_LE);
if (p_lcb == NULL) {
p_lcb = l2cu_allocate_lcb(p_bd_addr, FALSE, BT_TRANSPORT_LE);
if (p_lcb == NULL || !l2cu_create_conn(p_lcb, BT_TRANSPORT_LE)) {
L2C_BLE_ECFC_TRACE_ERROR("ConnectLEEcocReq LCB alloc/create_conn failed");
return 0;
}
}
for (i = 0; i < num_chan; i++) {
p_ccb = l2cu_allocate_ccb(p_lcb, 0);
if (p_ccb == NULL) {
L2C_BLE_ECFC_TRACE_ERROR("ConnectLEEcocReq CCB alloc failed at chan %d", i);
while (--i >= 0) {
p_ccb = l2cu_find_ccb_by_cid(p_lcb, scids[i]);
if (p_ccb) {
l2cu_release_ccb(p_ccb);
}
}
return 0;
}
p_ccb->p_rcb = p_rcb;
p_ccb->le_coc_active = TRUE;
p_ccb->le_ecfc_channel = TRUE;
/* A pooled CCB reused from a released non-CoC channel keeps its stale
* remote_cid; clear it so the DCID/SCID dedup checks in
* l2c_ble_ecfc_handle_conn_res/handle_conn_req cannot false-match this
* channel-in-setup before its real remote_cid is assigned (mirrors
* L2CA_ConnectLECocReq). */
p_ccb->remote_cid = 0;
if (p_cfg) {
memcpy(&p_ccb->local_conn_cfg, p_cfg, sizeof(tL2CAP_LE_CFG_INFO));
}
l2c_ble_ecfc_apply_default_cfg(&p_ccb->local_conn_cfg);
scids[i] = p_ccb->local_cid;
if (p_lcids) {
p_lcids[i] = p_ccb->local_cid;
}
}
p_lcb->id++;
l2cu_adj_id(p_lcb, L2CAP_ADJ_ID);
sig_id = p_lcb->id;
txn = l2c_ble_ecfc_alloc_cl_txn(p_lcb, sig_id);
if (txn == NULL) {
L2C_BLE_ECFC_TRACE_WARN("ConnectLEEcocReq cl txn alloc failed sig_id=%u", sig_id);
for (i = 0; i < num_chan; i++) {
p_ccb = l2cu_find_ccb_by_cid(p_lcb, scids[i]);
if (p_ccb) {
l2cu_release_ccb(p_ccb);
}
}
return 0;
}
txn->num_chan = num_chan;
for (i = 0; i < num_chan; i++) {
txn->ccbs[i] = l2cu_find_ccb_by_cid(p_lcb, scids[i]);
}
if (p_lcb->link_state != LST_CONNECTED) {
if (p_lcb->link_state == LST_DISCONNECTING) {
L2C_BLE_ECFC_TRACE_WARN("ConnectLEEcocReq link disconnecting, abort sig_id=%u", sig_id);
l2c_ble_ecfc_cl_txn_free(txn);
for (i = 0; i < num_chan; i++) {
p_ccb = l2cu_find_ccb_by_cid(p_lcb, scids[i]);
if (p_ccb) {
l2cu_release_ccb(p_ccb);
}
}
return 0;
}
txn->pending_link = TRUE;
L2C_BLE_ECFC_TRACE_DEBUG("ConnectLEEcocReq deferred until link up");
return num_chan;
}
if (!l2c_ble_ecfc_cl_send_connect(txn)) {
L2C_BLE_ECFC_TRACE_ERROR("ConnectLEEcocReq send_connect failed sig_id=%u", sig_id);
l2c_ble_ecfc_cl_txn_free(txn);
for (i = 0; i < num_chan; i++) {
p_ccb = l2cu_find_ccb_by_cid(p_lcb, scids[i]);
if (p_ccb) {
l2cu_release_ccb(p_ccb);
}
}
return 0;
}
return num_chan;
}
#endif /* BLE_L2CAP_COC_CLIENT_INCLUDED */
BOOLEAN L2CA_LEEcocReconfig(UINT16 lcids[], UINT8 num, UINT16 new_mtu, UINT16 new_mps)
{
tL2C_CCB *p_ccb;
tL2C_LCB *p_lcb = NULL;
tL2C_BLE_ECFC_RECONFIG_TXN *txn;
UINT16 dcids[BLE_MAX_L2CAP_CLIENTS];
UINT8 sig_id;
int i;
L2C_BLE_ECFC_TRACE_DEBUG("LEEcocReconfig num=%u new_mtu=%u new_mps=%u", num, new_mtu, new_mps);
if (lcids == NULL || num == 0 || num > BLE_MAX_L2CAP_CLIENTS ||
num > L2C_BLE_ECFC_MAX_REQ_CHANS ||
new_mtu < L2CAP_LE_ECFC_MIN_MTU || new_mps < L2CAP_LE_ECFC_MIN_MPS ||
new_mps > L2CAP_LE_MAX_MPS) {
L2C_BLE_ECFC_TRACE_WARN("LEEcocReconfig invalid params num=%u new_mtu=%u new_mps=%u",
num, new_mtu, new_mps);
return FALSE;
}
for (i = 0; i < num; i++) {
/* Reject a request that lists the same LCID more than once: a duplicate
* resolves to the same CCB, so txn->ccbs[] would store it twice and the
* 0x1A response would apply/notify the reconfigure on one channel twice.
* Mirrors the seen_dcids[] dedup in l2c_ble_ecfc_handle_reconfig_req.
* num <= 5, so the O(n^2) scan is cheap. */
for (int j = 0; j < i; j++) {
if (lcids[j] == lcids[i]) {
L2C_BLE_ECFC_TRACE_WARN("LEEcocReconfig duplicate lcid=0x%04x", lcids[i]);
return FALSE;
}
}
p_ccb = l2cu_find_ccb_by_cid(NULL, lcids[i]);
if (p_ccb == NULL || !p_ccb->le_coc_active || p_ccb->chnl_state != CST_OPEN) {
L2C_BLE_ECFC_TRACE_WARN("LEEcocReconfig invalid ccb/not open lcid=0x%04x", lcids[i]);
return FALSE;
}
/* Reconfiguration (0x19) is only defined for ECFC channels. Reject an
* attempt to reconfigure a base LE CoC channel, which cannot carry the
* enhanced reconfig request. */
if (!p_ccb->le_ecfc_channel) {
L2C_BLE_ECFC_TRACE_WARN("LEEcocReconfig not an ECFC channel lcid=0x%04x", lcids[i]);
return FALSE;
}
if (p_lcb == NULL) {
p_lcb = p_ccb->p_lcb;
} else if (p_ccb->p_lcb != p_lcb) {
L2C_BLE_ECFC_TRACE_WARN("LEEcocReconfig lcid=0x%04x on different link", lcids[i]);
return FALSE;
}
if (p_ccb->local_conn_cfg.mtu > new_mtu) {
L2C_BLE_ECFC_TRACE_WARN("LEEcocReconfig MTU reduction not allowed lcid=0x%04x cur=%u new=%u",
lcids[i], p_ccb->local_conn_cfg.mtu, new_mtu);
return FALSE;
}
/* Core Spec v6.2 Vol 3 Part A 4.27: when more than one channel is
* reconfigured, the new MPS must be >= the current MPS of each channel.
* Reject up front instead of sending a request the peer will refuse
* (result 0x0002) while we wrongly report success. */
if (num > 1 && p_ccb->local_conn_cfg.mps > new_mps) {
L2C_BLE_ECFC_TRACE_WARN("LEEcocReconfig multi-chan MPS reduction not allowed lcid=0x%04x cur=%u new=%u",
lcids[i], p_ccb->local_conn_cfg.mps, new_mps);
return FALSE;
}
dcids[i] = p_ccb->local_cid;
}
p_lcb->id++;
l2cu_adj_id(p_lcb, L2CAP_ADJ_ID);
sig_id = p_lcb->id;
txn = l2c_ble_ecfc_alloc_reconfig_txn(p_lcb, sig_id);
if (txn == NULL) {
L2C_BLE_ECFC_TRACE_WARN("LEEcocReconfig reconfig txn alloc failed sig_id=%u", sig_id);
return FALSE;
}
txn->num_chan = num;
txn->new_mtu = new_mtu;
txn->new_mps = new_mps;
for (i = 0; i < num; i++) {
txn->ccbs[i] = l2cu_find_ccb_by_cid(p_lcb, lcids[i]);
}
/* If the request cannot be sent (e.g. buffer OOM) the peer never answers, so
* release the txn slot instead of leaking it and reporting success. */
if (!l2cu_send_peer_ble_credit_reconfig_req(p_lcb, sig_id, new_mtu, new_mps, num, dcids)) {
L2C_BLE_ECFC_TRACE_ERROR("LEEcocReconfig 0x19 send failed sig_id=%u", sig_id);
memset(txn, 0, sizeof(*txn));
return FALSE;
}
/* Guard against a peer that never sends the 0x1A response: one timer per
* transaction (on ccbs[0]) aborts the reconfigure on expiry. Channels stay
* OPEN, so this timer is stopped explicitly on completion/abort. */
if (txn->ccbs[0]) {
l2c_ble_le_coc_start_rsp_timer(txn->ccbs[0], L2CAP_CHNL_CONNECT_TOUT);
}
L2C_BLE_ECFC_TRACE_DEBUG("LEEcocReconfig 0x19 sent sig_id=%u num=%u", sig_id, num);
return TRUE;
}
#if (BLE_L2CAP_COC_SERVER_INCLUDED == TRUE)
void l2c_ble_ecfc_handle_conn_req(tL2C_LCB *p_lcb, UINT8 *p, UINT8 id, UINT16 cmd_len)
{
tL2C_RCB *p_rcb;
tL2C_CCB *p_ccb;
tL2C_BLE_ECFC_SRV_TXN *txn;
UINT16 spsm, mtu, mps, credits, scid;
UINT16 n_scids;
UINT8 num_scids;
int i;
if (cmd_len < L2CAP_CMD_BLE_ENHANCED_CONN_REQ_BASE_LEN + sizeof(UINT16)) {
L2C_BLE_ECFC_TRACE_WARN("reject 0x17 short cmd len=%u id=%u", cmd_len, id);
l2cu_reject_ble_enhanced_connection(p_lcb, id, L2CAP_LE_RESULT_INVALID_PARAMETERS, 1);
return;
}
STREAM_TO_UINT16(spsm, p);
STREAM_TO_UINT16(mtu, p);
STREAM_TO_UINT16(mps, p);
STREAM_TO_UINT16(credits, p);
/* Compute the SCID count in a wide type first: (cmd_len - base)/2 can exceed
* 255 for an oversized packet and would truncate if assigned to a UINT8
* before the upper-bound check, letting a malformed request slip through. */
n_scids = (UINT16)((cmd_len - L2CAP_CMD_BLE_ENHANCED_CONN_REQ_BASE_LEN) / sizeof(UINT16));
/* Core Spec v6.2 Vol 3 Part A 4.25 caps a single request at 5 source CIDs.
* BLE_MAX_L2CAP_CLIENTS may be configured up to 15, so enforce the 5
* spec limit here too (matches handle_reconfig_req / L2CA_ConnectLEEcocReq). */
/* The reject response must echo one DCID per requested SCID (Core Spec v6.2
* Vol 3 Part A 4.26: 1:1 positional mapping), so pass the actual n_scids as
* the count rather than clamping it to the local channel budget. Cap the
* count at 255 only to fit the UINT8 API argument: n_scids = (cmd_len-8)/2
* can exceed 255 for an oversized/malformed packet, and a bare (UINT8) cast
* would truncate it (e.g. 256 -> 0, then promoted to 1). */
if (n_scids == 0) {
L2C_BLE_ECFC_TRACE_WARN("reject 0x17 n_scids=0 id=%u", id);
l2cu_reject_ble_enhanced_connection(p_lcb, id, L2CAP_LE_RESULT_INVALID_PARAMETERS, 1);
return;
}
UINT8 reject_scids = (n_scids > 255) ? 255 : (UINT8)n_scids;
if (n_scids > L2C_BLE_ECFC_MAX_REQ_CHANS) {
/* More SCIDs than the spec's per-request maximum of 5: malformed. */
L2C_BLE_ECFC_TRACE_WARN("reject 0x17 n_scids=%u > max id=%u", n_scids, id);
l2cu_reject_ble_enhanced_connection(p_lcb, id, L2CAP_LE_RESULT_INVALID_PARAMETERS,
reject_scids);
return;
}
if (n_scids > BLE_MAX_L2CAP_CLIENTS) {
/* Spec-valid count but exceeds our local channel budget: resource limit. */
L2C_BLE_ECFC_TRACE_WARN("reject 0x17 n_scids=%u over budget id=%u", n_scids, id);
l2cu_reject_ble_enhanced_connection(p_lcb, id, L2CAP_LE_RESULT_NO_RESOURCES,
reject_scids);
return;
}
num_scids = (UINT8)n_scids;
/* Defensive: reject an initial credit value of 0 for the enhanced
* credit-based (0x17) request. NOTE: confirm against Core Spec v6.2 4.25
* whether 0 is strictly illegal for 0x17; regardless, seeding a channel
* with 0 TX credits leaves it unusable until the peer later grants credit,
* so rejecting up front is the safer behaviour. */
if (mtu < L2CAP_LE_ECFC_MIN_MTU || mps < L2CAP_LE_ECFC_MIN_MPS ||
mps > L2CAP_LE_MAX_MPS || credits == 0) {
L2C_BLE_ECFC_TRACE_WARN("reject 0x17 invalid mtu=%u mps=%u cred=%u id=%u", mtu, mps, credits, id);
l2cu_reject_ble_enhanced_connection(p_lcb, id, L2CAP_LE_RESULT_INVALID_PARAMETERS, num_scids);
return;
}
L2C_BLE_ECFC_TRACE_API("rx 0x17 spsm=0x%04x mtu=%u mps=%u cred=%u n=%u id=%u",
spsm, mtu, mps, credits, num_scids, id);
p_rcb = l2cu_find_ble_rcb_by_psm(spsm);
if (p_rcb == NULL) {
L2C_BLE_ECFC_TRACE_WARN("reject 0x17 no PSM spsm=0x%04x id=%u", spsm, id);
l2cu_reject_ble_enhanced_connection(p_lcb, id, L2CAP_LE_RESULT_NO_PSM, num_scids);
return;
}
txn = l2c_ble_ecfc_alloc_srv_txn(p_lcb, id);
if (txn == NULL) {
L2C_BLE_ECFC_TRACE_ERROR("reject 0x17 srv txn alloc failed id=%u", id);
l2cu_reject_ble_enhanced_connection(p_lcb, id, L2CAP_LE_RESULT_NO_RESOURCES, num_scids);
return;
}
txn->num_total = num_scids;
for (i = 0; i < num_scids; i++) {
STREAM_TO_UINT16(scid, p);
/* Each SCID must be a peer dynamic LE-U CID (0x0040-0x007F). Reject a
* fixed/invalid CID (e.g. 0x0004 ATT) so we never target it with
* K-frames (Core Spec v6.2 Vol 3 Part A 4.25 / result 0x0009). */
if (scid < L2CAP_BASE_APPL_CID || scid > L2CAP_BLE_CONN_MAX_CID) {
L2C_BLE_ECFC_TRACE_WARN("reject 0x17 invalid scid=0x%04x id=%u", scid, id);
l2cu_reject_ble_enhanced_connection(p_lcb, id, L2CAP_LE_RESULT_INVALID_SOURCE_CID, num_scids);
for (int j = 0; j < i; j++) {
if (txn->ccbs[j]) {
tL2C_CCB *p_rel = txn->ccbs[j];
txn->ccbs[j] = NULL;
l2cu_release_ccb(p_rel);
}
}
if (txn->in_use) {
l2c_ble_ecfc_srv_txn_free(txn);
}
return;
}
if (l2cu_find_ccb_by_remote_cid(p_lcb, scid)) {
L2C_BLE_ECFC_TRACE_WARN("reject 0x17 duplicate scid=0x%04x id=%u", scid, id);
l2cu_reject_ble_enhanced_connection(p_lcb, id, L2CAP_LE_RESULT_SOURCE_CID_ALREADY_ALLOCATED, num_scids);
for (int j = 0; j < i; j++) {
if (txn->ccbs[j]) {
tL2C_CCB *p_rel = txn->ccbs[j];
txn->ccbs[j] = NULL;
l2cu_release_ccb(p_rel);
}
}
if (txn->in_use) {
l2c_ble_ecfc_srv_txn_free(txn);
}
return;
}
p_ccb = l2cu_allocate_ccb(p_lcb, 0);
if (p_ccb == NULL) {
L2C_BLE_ECFC_TRACE_ERROR("reject 0x17 CCB alloc failed id=%u chan=%d", id, i);
l2cu_reject_ble_enhanced_connection(p_lcb, id, L2CAP_LE_RESULT_NO_RESOURCES, num_scids);
for (int j = 0; j < i; j++) {
if (txn->ccbs[j]) {
tL2C_CCB *p_rel = txn->ccbs[j];
txn->ccbs[j] = NULL;
l2cu_release_ccb(p_rel);
}
}
if (txn->in_use) {
l2c_ble_ecfc_srv_txn_free(txn);
}
return;
}
p_ccb->le_coc_active = TRUE;
p_ccb->le_ecfc_channel = TRUE;
p_ccb->remote_id = id;
p_ccb->p_rcb = p_rcb;
p_ccb->remote_cid = scid;
p_ccb->peer_conn_cfg.mtu = mtu;
p_ccb->peer_conn_cfg.mps = mps;
p_ccb->peer_conn_cfg.credits = credits;
l2c_ble_ecfc_apply_default_cfg(&p_ccb->local_conn_cfg);
txn->ccbs[i] = p_ccb;
}
txn->rsp_credits = txn->ccbs[0]->local_conn_cfg.credits;
txn->rsp_mtu = txn->ccbs[0]->local_conn_cfg.mtu;
txn->rsp_mps = txn->ccbs[0]->local_conn_cfg.mps;
/* Move every channel in the batch (not just ccbs[0]) into the security-wait
* state so link/CSM lookups and cleanup treat them consistently while the
* single security check for this connection request is in flight. */
for (i = 0; i < num_scids; i++) {
txn->ccbs[i]->chnl_state = CST_TERM_W4_SEC_COMP;
}
p_ccb = txn->ccbs[0];
(void)p_ccb;
#if (SMP_INCLUDED == TRUE)
l2ble_sec_access_req(p_lcb->remote_bd_addr, p_rcb->real_psm, FALSE,
l2c_ble_ecfc_sec_cback, txn);
#else
/* SMP disabled: no security procedure to run, so complete the access check
* immediately (l2ble_sec_access_req is only compiled with SMP). */
l2c_ble_ecfc_sec_cback(p_lcb->remote_bd_addr, BT_TRANSPORT_LE, txn, BTM_SUCCESS);
#endif
}
#endif /* BLE_L2CAP_COC_SERVER_INCLUDED */
#if (BLE_L2CAP_COC_CLIENT_INCLUDED == TRUE)
void l2c_ble_ecfc_abort_cl_txn(tL2C_LCB *p_lcb, UINT8 sig_id, UINT16 result)
{
tL2C_BLE_ECFC_CL_TXN *txn = l2c_ble_ecfc_find_cl_txn(p_lcb, sig_id);
if (txn == NULL) {
/* Common no-op: a CMD_REJECT that does not target a pending ECFC client
* transaction (it may be for a base CoC or reconfigure request). */
return;
}
L2C_BLE_ECFC_TRACE_WARN("abort cl txn sig_id=%u result=%u", sig_id, result);
/* Snapshot the CCBs and free the txn slot BEFORE opening any CCB. A non-OK
* open releases the CCB, which re-enters l2c_ble_ecfc_on_ccb_release() and may
* free this txn once no CCBs remain. Working from a local copy means neither
* the loop condition nor the trailing free can read or corrupt a slot that
* was reallocated during the callback chain. */
tL2C_CCB *ccbs[BLE_MAX_L2CAP_CLIENTS];
UINT8 num_chan = txn->num_chan;
if (num_chan > BLE_MAX_L2CAP_CLIENTS) {
num_chan = BLE_MAX_L2CAP_CLIENTS;
}
memcpy(ccbs, txn->ccbs, num_chan * sizeof(tL2C_CCB *));
l2c_ble_ecfc_cl_txn_free(txn);
for (int i = 0; i < num_chan; i++) {
if (ccbs[i]) {
l2c_ble_ecfc_open_ccb(ccbs[i], result);
}
}
}
void l2c_ble_ecfc_handle_conn_res(tL2C_LCB *p_lcb, UINT8 *p, UINT8 id, UINT16 cmd_len)
{
tL2C_BLE_ECFC_CL_TXN *txn;
UINT16 mtu, mps, credits, result, dcid;
UINT16 n_dcids;
int i;
txn = l2c_ble_ecfc_find_cl_txn(p_lcb, id);
if (txn == NULL) {
L2C_BLE_ECFC_TRACE_WARN("0x18 unknown id=%u", id);
return;
}
/* Response received: cancel the connect-response timeout (armed on ccbs[0]). */
if (txn->ccbs[0]) {
l2c_ble_le_coc_stop_rsp_timer(txn->ccbs[0]);
}
/* Snapshot the CCBs and free the txn BEFORE opening any channel. A non-OK
* open releases the CCB and re-enters l2c_ble_ecfc_on_ccb_release(), which
* frees this txn once its last CCB is gone; a synchronous ConnectCfm callback
* (e.g. EATT) could then reallocate the same slot. Working from a local copy
* keeps the loop bound, the stream parsing and the field writes from touching
* a reused txn, matching l2c_ble_ecfc_abort_cl_txn / l2c_ble_ecfc_on_link_up. */
tL2C_CCB *ccbs[BLE_MAX_L2CAP_CLIENTS];
UINT8 num_chan = txn->num_chan;
if (num_chan > BLE_MAX_L2CAP_CLIENTS) {
num_chan = BLE_MAX_L2CAP_CLIENTS;
}
memcpy(ccbs, txn->ccbs, num_chan * sizeof(tL2C_CCB *));
if (cmd_len < L2CAP_CMD_BLE_ENHANCED_CONN_RES_BASE_LEN) {
L2C_BLE_ECFC_TRACE_WARN("0x18 short cmd len=%u", cmd_len);
l2c_ble_ecfc_cl_txn_free(txn);
for (i = 0; i < num_chan; i++) {
if (ccbs[i]) {
l2c_ble_ecfc_open_ccb(ccbs[i], L2CAP_CONN_NO_PSM);
}
}
return;
}
STREAM_TO_UINT16(mtu, p);
STREAM_TO_UINT16(mps, p);
STREAM_TO_UINT16(credits, p);
STREAM_TO_UINT16(result, p);
/* Compute the DCID count in a wide type first: (cmd_len - base)/2 can exceed
* 255 for an oversized packet and would truncate if assigned to a UINT8
* before the count check, letting a malformed response slip through. Mirrors
* the request handler (l2c_ble_ecfc_handle_conn_req). */
n_dcids = (UINT16)((cmd_len - L2CAP_CMD_BLE_ENHANCED_CONN_RES_BASE_LEN) / sizeof(UINT16));
L2C_BLE_ECFC_TRACE_API("rx 0x18 id=%u mtu=%u mps=%u cred=%u result=%u n=%u",
id, mtu, mps, credits, result, n_dcids);
if (n_dcids != num_chan) {
L2C_BLE_ECFC_TRACE_WARN("0x18 dcid count mismatch n=%u txn=%u", n_dcids, num_chan);
l2c_ble_ecfc_cl_txn_free(txn);
for (i = 0; i < num_chan; i++) {
if (ccbs[i]) {
l2c_ble_ecfc_open_ccb(ccbs[i], L2CAP_CONN_NO_PSM);
}
}
return;
}
/* Validate the peer's common MTU/MPS/credits once. An mps of 0 would later
* be used as a divisor when fragmenting SDUs (divide-by-zero); mtu/credits of
* 0 leave the channel unusable while being reported as opened (Core Spec v6.2
* Vol 3 Part A 4.26). The check is applied per accepted channel (dcid != 0)
* inside the loop below rather than up front, so that on an "all connections
* refused" result — where MTU/MPS/Credits shall be ignored and every DCID is
* zero — the loop still forwards the real reject reason to the app instead of
* masking every channel with L2CAP_CONN_NO_PSM. */
BOOLEAN params_valid = !(mtu < L2CAP_LE_ECFC_MIN_MTU || mps < L2CAP_LE_ECFC_MIN_MPS ||
mps > L2CAP_LE_MAX_MPS || credits == 0);
/* Everything needed from txn has been read; free it now so the open_ccb calls
* below cannot trigger a re-entrant free of a slot we still read. */
l2c_ble_ecfc_cl_txn_free(txn);
for (i = 0; i < num_chan; i++) {
tL2C_CCB *p_ccb;
/* Each DCID in the response maps 1:1 to the SCID at the same index in
* the request (Core Spec 4.26). Consume the stream entry before checking
* ccbs[i]: a NULL slot (CCB released while the txn was pending) must
* still advance p past its DCID. */
STREAM_TO_UINT16(dcid, p);
p_ccb = ccbs[i];
if (p_ccb == NULL) {
continue;
}
/* Per Core Spec v6.2 Vol 3 Part A 4.26, on a "some connections refused"
* result each channel is accepted iff its DCID is non-zero; a zero DCID
* marks that individual channel as refused. Do not tear down channels
* with a valid DCID just because the aggregate result is not CONN_OK. */
if (dcid == 0) {
UINT16 fail_result = (result != L2CAP_LE_RESULT_CONN_OK) ? result : L2CAP_CONN_NO_PSM;
l2c_ble_ecfc_open_ccb(p_ccb, fail_result);
continue;
}
/* A non-zero DCID must be a peer dynamic LE-U CID (0x0040-0x007F) and
* unique on this link; otherwise our outgoing K-frames would target an
* invalid/duplicate peer CID (matches the 0x15 DCID check). Validate it
* (and record remote_cid) BEFORE the params check so a channel the peer
* accepted can be torn down on the air. An invalid/duplicate DCID cannot
* be cleanly disconnected (no valid target, and a duplicate would drop
* the wrong channel), so that case just drops our CCB. */
if (dcid < L2CAP_BASE_APPL_CID || dcid > L2CAP_BLE_CONN_MAX_CID ||
l2cu_find_ccb_by_remote_cid(p_lcb, dcid)) {
L2C_BLE_ECFC_TRACE_WARN("0x18 invalid/duplicate dcid=0x%04x lcid=0x%04x",
dcid, p_ccb->local_cid);
l2c_ble_ecfc_open_ccb(p_ccb, L2CAP_CONN_NO_PSM);
continue;
}
p_ccb->remote_cid = dcid;
/* A channel the peer accepted (valid non-zero DCID) must carry usable
* common parameters; if the shared MTU/MPS/credits are invalid the peer
* still has an open channel, so tear it down on the air (best-effort
* DISC_REQ) before dropping our CCB rather than leaving it orphaned. */
if (!params_valid) {
L2C_BLE_ECFC_TRACE_WARN("0x18 invalid peer params mtu=%u mps=%u cred=%u", mtu, mps, credits);
l2cble_send_peer_disc_req(p_ccb);
l2c_ble_ecfc_open_ccb(p_ccb, L2CAP_CONN_NO_PSM);
continue;
}
p_ccb->peer_conn_cfg.mtu = mtu;
p_ccb->peer_conn_cfg.mps = mps;
p_ccb->peer_conn_cfg.credits = credits;
l2c_ble_ecfc_open_ccb(p_ccb, L2CAP_CONN_OK);
}
}
#endif /* BLE_L2CAP_COC_CLIENT_INCLUDED */
void l2c_ble_ecfc_handle_reconfig_req(tL2C_LCB *p_lcb, UINT8 *p, UINT8 id, UINT16 cmd_len)
{
UINT8 *p_start = p;
UINT16 mtu, mps, dcid;
UINT16 n_dcids;
UINT8 num_dcids;
tL2C_CCB *p_ccb;
UINT8 reduction_mps = 0;
UINT16 seen_dcids[BLE_MAX_L2CAP_CLIENTS];
int i;
L2C_BLE_ECFC_TRACE_DEBUG("rx 0x19 id=%u cmd_len=%u", id, cmd_len);
if (cmd_len < L2CAP_CMD_BLE_CREDIT_RECONFIG_REQ_BASE_LEN + sizeof(UINT16)) {
L2C_BLE_ECFC_TRACE_WARN("reject 0x19 short cmd len=%u id=%u", cmd_len, id);
l2cu_send_peer_ble_credit_reconfig_rsp(p_lcb, id, L2CAP_LE_RECONFIG_UNACCEPTED_PARAM);
return;
}
STREAM_TO_UINT16(mtu, p);
STREAM_TO_UINT16(mps, p);
/* Wide-type count first to avoid UINT8 truncation on an oversized packet,
* then enforce the spec upper bound of 5 DCIDs (Core Spec v6.2 Vol 3
* Part A 4.27). */
n_dcids = (UINT16)((cmd_len - L2CAP_CMD_BLE_CREDIT_RECONFIG_REQ_BASE_LEN) / sizeof(UINT16));
if (mtu < L2CAP_LE_ECFC_MIN_MTU || mps < L2CAP_LE_ECFC_MIN_MPS ||
mps > L2CAP_LE_MAX_MPS || n_dcids == 0 ||
n_dcids > BLE_MAX_L2CAP_CLIENTS || n_dcids > L2C_BLE_ECFC_MAX_REQ_CHANS) {
L2C_BLE_ECFC_TRACE_WARN("reject 0x19 invalid mtu=%u mps=%u n_dcids=%u id=%u",
mtu, mps, n_dcids, id);
l2cu_send_peer_ble_credit_reconfig_rsp(p_lcb, id, L2CAP_LE_RECONFIG_UNACCEPTED_PARAM);
return;
}
num_dcids = (UINT8)n_dcids;
for (i = 0; i < num_dcids; i++) {
STREAM_TO_UINT16(dcid, p);
/* Reject a request that lists the same DCID more than once. Each DCID
* "shall be non-zero and represent channel endpoints" (Core Spec v6.2
* Vol 3 Part A 4.27); a duplicate is malformed and, if let through, would
* update the same CCB and fire notify_reconfig twice. n <= 5, so an O(n^2)
* scan of the DCIDs already parsed is cheap. */
for (int j = 0; j < i; j++) {
if (seen_dcids[j] == dcid) {
L2C_BLE_ECFC_TRACE_WARN("reject 0x19 duplicate dcid=0x%04x id=%u", dcid, id);
l2cu_send_peer_ble_credit_reconfig_rsp(p_lcb, id, L2CAP_LE_RECONFIG_INVALID_DCID);
return;
}
}
seen_dcids[i] = dcid;
/* The Destination CID array in a reconfigure request holds the peer's
* local CIDs (Core Spec v6.2 Vol 3 Part A 4.27), which map to our
* remote_cid, not our local_cid. */
p_ccb = l2cu_find_ccb_by_remote_cid(p_lcb, dcid);
/* Reconfiguration (0x19) is only defined for an established ECFC channel
* (Core Spec v6.2 Vol 3 Part A 4.27). Reject a request that targets a
* base LE CoC channel (le_coc_active but !le_ecfc_channel) or a channel
* not yet fully open, mirroring the local L2CA_LEEcocReconfig checks. */
if (p_ccb == NULL || !p_ccb->le_coc_active || !p_ccb->le_ecfc_channel ||
p_ccb->chnl_state != CST_OPEN) {
L2C_BLE_ECFC_TRACE_WARN("reject 0x19 invalid dcid=0x%04x id=%u", dcid, id);
l2cu_send_peer_ble_credit_reconfig_rsp(p_lcb, id, L2CAP_LE_RECONFIG_INVALID_DCID);
return;
}
/* A reconfigure request changes the requester's (peer's) receive
* MTU/MPS, i.e. our peer_conn_cfg. Per spec the new MTU must not be
* smaller than the peer's previously agreed MTU. Compare against
* peer_conn_cfg.mtu (matches the peer_conn_cfg.mps check below), not
* our own local RX MTU. */
if (p_ccb->peer_conn_cfg.mtu > mtu) {
L2C_BLE_ECFC_TRACE_WARN("reject 0x19 MTU reduction not allowed dcid=0x%04x cur=%u new=%u id=%u",
dcid, p_ccb->peer_conn_cfg.mtu, mtu, id);
l2cu_send_peer_ble_credit_reconfig_rsp(p_lcb, id, L2CAP_LE_RECONFIG_REDUCTION_MTU_NOT_ALLOWED);
return;
}
if (p_ccb->peer_conn_cfg.mps > mps) {
reduction_mps++;
}
}
if (reduction_mps > 0 && num_dcids > 1) {
L2C_BLE_ECFC_TRACE_WARN("reject 0x19 multi-chan MPS reduction not allowed n=%u id=%u",
num_dcids, id);
l2cu_send_peer_ble_credit_reconfig_rsp(p_lcb, id, L2CAP_LE_RECONFIG_REDUCTION_MPS_NOT_ALLOWED);
return;
}
/* Core Spec v6.2 Vol 3 Part A 7.11 mandates the ordering: finish sending any
* existing PDUs that need the old (larger) MPS, THEN send the
* L2CAP_CREDIT_BASED_RECONFIGURE_RSP, and only afterwards adopt the new
* MTU/MPS for subsequent SDUs. Send the response before updating the local
* peer_conn_cfg / notifying the upper layer, otherwise a data write driven
* synchronously from the notify callback would fragment SDUs with the new
* (possibly smaller) MPS before the peer has seen the confirming response. */
l2cu_send_peer_ble_credit_reconfig_rsp(p_lcb, id, L2CAP_LE_RECONFIG_OK);
L2C_BLE_ECFC_TRACE_DEBUG("0x1A OK id=%u mtu=%u mps=%u num_dcids=%u", id, mtu, mps, num_dcids);
p = p_start + sizeof(UINT16) + sizeof(UINT16);
for (i = 0; i < num_dcids; i++) {
STREAM_TO_UINT16(dcid, p);
p_ccb = l2cu_find_ccb_by_remote_cid(p_lcb, dcid);
if (p_ccb) {
p_ccb->peer_conn_cfg.mtu = mtu;
p_ccb->peer_conn_cfg.mps = mps;
l2c_ble_le_coc_notify_reconfig(p_ccb, L2CAP_LE_RECONFIG_OK, TRUE);
#if (BLE_EATT_INCLUDED == TRUE)
gatt_eatt_on_chan_mtu_changed(p_lcb->remote_bd_addr, p_ccb->local_cid);
#endif
}
}
}
void l2c_ble_ecfc_handle_reconfig_res(tL2C_LCB *p_lcb, UINT8 *p, UINT8 id, UINT16 cmd_len)
{
tL2C_BLE_ECFC_RECONFIG_TXN *txn;
UINT16 result;
int i;
tL2C_CCB *ccbs[BLE_MAX_L2CAP_CLIENTS];
UINT8 num_chan;
UINT16 new_mtu, new_mps;
txn = l2c_ble_ecfc_find_reconfig_txn(p_lcb, id);
if (txn == NULL) {
L2C_BLE_ECFC_TRACE_WARN("0x1A unknown id=%u", id);
return;
}
/* Response received: cancel the reconfigure-response timeout (armed on
* ccbs[0]); channels remain OPEN so the timer is not freed by release. */
if (txn->ccbs[0]) {
l2c_ble_le_coc_stop_rsp_timer(txn->ccbs[0]);
}
/* Snapshot the CCBs and reconfigure params, then free the txn slot BEFORE
* invoking any user callback. l2c_ble_le_coc_notify_reconfig() calls
* pL2CA_LeReconfigInd_Cb, whose handler may synchronously disconnect a
* channel; that re-enters l2c_ble_ecfc_on_ccb_release(), which memset-frees
* this reconfig txn once its last CCB is gone, and a re-entrant reconfigure
* could then reallocate the slot. Working from a local copy keeps the loop
* and the trailing writes from reading/corrupting a reused slot (matches
* l2c_ble_ecfc_abort_cl_txn / l2c_ble_ecfc_handle_conn_res).
*
* Residual (theoretical only): the snapshot could still hold a CCB that a
* callback releases mid-loop, so a later apply/notify would touch freed
* memory. This is the same edge already accepted for l2c_ble_ecfc_handle_conn_res
* and is not reachable today: the only pL2CA_LeReconfigInd_Cb registrant is
* btc_ble_l2cap_reconfig_ind(), which merely posts an async event (no
* synchronous l2cu_release_ccb), and EATT registers no reconfig callback. */
num_chan = txn->num_chan;
if (num_chan > BLE_MAX_L2CAP_CLIENTS) {
num_chan = BLE_MAX_L2CAP_CLIENTS;
}
memcpy(ccbs, txn->ccbs, num_chan * sizeof(tL2C_CCB *));
new_mtu = txn->new_mtu;
new_mps = txn->new_mps;
/* A malformed/short response still terminates this pending reconfigure, so
* the txn must be released here (it was already looked up) or it leaks. */
if (cmd_len < L2CAP_CMD_BLE_CREDIT_RECONFIG_RSP_LEN) {
L2C_BLE_ECFC_TRACE_WARN("0x1A short cmd len=%u id=%u, notify unaccepted", cmd_len, id);
memset(txn, 0, sizeof(*txn));
for (i = 0; i < num_chan; i++) {
if (ccbs[i]) {
l2c_ble_le_coc_notify_reconfig(ccbs[i], L2CAP_LE_RECONFIG_UNACCEPTED_PARAM, FALSE);
}
}
return;
}
STREAM_TO_UINT16(result, p);
memset(txn, 0, sizeof(*txn));
if (result == L2CAP_LE_RECONFIG_OK) {
L2C_BLE_ECFC_TRACE_DEBUG("0x1A reconfig ok id=%u num_chan=%u new_mtu=%u new_mps=%u",
id, num_chan, new_mtu, new_mps);
for (i = 0; i < num_chan; i++) {
if (ccbs[i]) {
l2c_ble_le_coc_apply_reconfig(ccbs[i], new_mtu, new_mps);
l2c_ble_le_coc_notify_reconfig(ccbs[i], L2CAP_LE_RECONFIG_OK, FALSE);
}
}
} else {
L2C_BLE_ECFC_TRACE_WARN("0x1A reconfig failed id=%u result=%u", id, result);
for (i = 0; i < num_chan; i++) {
if (ccbs[i]) {
l2c_ble_le_coc_notify_reconfig(ccbs[i], result, FALSE);
}
}
}
}
void l2c_ble_ecfc_abort_reconfig_txn(tL2C_LCB *p_lcb, UINT8 sig_id)
{
tL2C_BLE_ECFC_RECONFIG_TXN *txn = l2c_ble_ecfc_find_reconfig_txn(p_lcb, sig_id);
tL2C_CCB *ccbs[BLE_MAX_L2CAP_CLIENTS];
UINT8 num_chan;
int i;
if (txn == NULL) {
/* Common no-op: a CMD_REJECT that does not target a pending reconfigure. */
return;
}
L2C_BLE_ECFC_TRACE_WARN("abort reconfig txn sig_id=%u", sig_id);
/* Peer rejected the reconfigure request with L2CAP_CMD_REJECT instead of a
* 0x1A response, so no reconfig response will ever arrive. Snapshot the
* CCBs and free the txn slot BEFORE notifying: l2c_ble_le_coc_notify_reconfig
* calls pL2CA_LeReconfigInd_Cb, whose handler may synchronously disconnect a
* channel and re-enter l2c_ble_ecfc_on_ccb_release() (which memset-frees this
* txn once its last CCB is gone), after which a re-entrant reconfigure could
* reallocate the slot. Working from a local copy avoids corrupting a reused
* slot (matches l2c_ble_ecfc_abort_cl_txn / l2c_ble_ecfc_handle_reconfig_res).
*
* Residual (theoretical only): the snapshot could still hold a CCB that a
* notify callback releases mid-loop, so a later stop_timer/notify would touch
* freed memory. Same edge already accepted for l2c_ble_ecfc_handle_conn_res,
* and not reachable today: the only pL2CA_LeReconfigInd_Cb registrant
* (btc_ble_l2cap_reconfig_ind) posts an async event without a synchronous
* l2cu_release_ccb, and EATT registers no reconfig callback. */
num_chan = txn->num_chan;
if (num_chan > BLE_MAX_L2CAP_CLIENTS) {
num_chan = BLE_MAX_L2CAP_CLIENTS;
}
memcpy(ccbs, txn->ccbs, num_chan * sizeof(tL2C_CCB *));
memset(txn, 0, sizeof(*txn));
/* The reconfigure-response timer is armed only on ccbs[0] (see
* L2CA_LEEcocReconfig), so stop it there and nowhere else. Do NOT stop it
* per-CCB: another channel in this txn may have independently armed a
* disconnect RTX timer on the same timer_entry (l2c_ble_le_coc_initiate_disc);
* cancelling that would strand the channel in CST_W4_L2CAP_DISCONNECT_RSP
* with no timeout. Channels stay OPEN through a reconfigure, so release does
* not run here. Mirrors l2c_ble_ecfc_handle_reconfig_res. */
if (ccbs[0]) {
l2c_ble_le_coc_stop_rsp_timer(ccbs[0]);
}
for (i = 0; i < num_chan; i++) {
if (ccbs[i]) {
l2c_ble_le_coc_notify_reconfig(ccbs[i], L2CAP_LE_RECONFIG_UNACCEPTED_PARAM, FALSE);
}
}
}
#if (BLE_L2CAP_COC_CLIENT_INCLUDED == TRUE)
BOOLEAN l2c_ble_ecfc_on_conn_timeout(tL2C_CCB *p_ccb)
{
int i, j;
for (i = 0; i < MAX_L2CAP_LINKS; i++) {
tL2C_BLE_ECFC_CL_TXN *txn = &s_ecfc_cl_txn[i];
if (!txn->in_use) {
continue;
}
for (j = 0; j < txn->num_chan; j++) {
if (txn->ccbs[j] == p_ccb) {
L2C_BLE_ECFC_TRACE_WARN("0x18 timeout sig_id=%u", txn->sig_id);
/* Fails every channel (open_ccb releases them, freeing timers)
* and releases the txn slot. Report L2CAP_CONN_TIMEOUT (not the
* hardcoded NO_PSM) so the app sees a retryable timeout, matching
* the base LE CoC timeout path (l2c_ble_le_coc_channel_timeout). */
l2c_ble_ecfc_abort_cl_txn(txn->p_lcb, txn->sig_id, L2CAP_CONN_TIMEOUT);
return TRUE;
}
}
}
return FALSE;
}
#endif /* BLE_L2CAP_COC_CLIENT_INCLUDED */
BOOLEAN l2c_ble_ecfc_on_reconfig_timeout(tL2C_CCB *p_ccb)
{
int i, j;
for (i = 0; i < MAX_L2CAP_LINKS; i++) {
tL2C_BLE_ECFC_RECONFIG_TXN *txn = &s_ecfc_reconfig_txn[i];
if (!txn->in_use) {
continue;
}
for (j = 0; j < txn->num_chan; j++) {
if (txn->ccbs[j] == p_ccb) {
L2C_BLE_ECFC_TRACE_WARN("0x1A timeout sig_id=%u", txn->sig_id);
/* Notifies each channel of the failed reconfigure and frees the
* txn (also stops the response timers). */
l2c_ble_ecfc_abort_reconfig_txn(txn->p_lcb, txn->sig_id);
return TRUE;
}
}
}
return FALSE;
}
#if (BLE_L2CAP_COC_CLIENT_INCLUDED != TRUE)
UINT8 L2CA_ConnectLEEcocReq(UINT16 psm, BD_ADDR p_bd_addr, tL2CAP_LE_CFG_INFO *p_cfg,
UINT8 num_chan, UINT16 *p_lcids)
{
UNUSED(psm);
UNUSED(p_bd_addr);
UNUSED(p_cfg);
UNUSED(num_chan);
UNUSED(p_lcids);
return 0;
}
#endif
#endif /* BLE_L2CAP_ENHANCED_COC_INCLUDED == TRUE */