From 95bd892dbac1ba64a96aa901d8a02571282535dd Mon Sep 17 00:00:00 2001 From: Zhi Wei Jian Date: Tue, 14 Jul 2026 11:56:02 +0800 Subject: [PATCH] feat(ble/bluedroid): Support bluedroid LE COC and EATT features (cherry picked from commit 83f0831c5384e36ae6e72434b1b2a705f0717fff) Co-authored-by: zhiweijian --- components/bt/common/btc/core/btc_task.c | 6 + .../bt/common/btc/include/btc/btc_task.h | 3 + components/bt/host/bluedroid/CMakeLists.txt | 20 + components/bt/host/bluedroid/Kconfig.in | 81 + .../bt/host/bluedroid/api/esp_ble_l2cap_api.c | 222 +++ .../bt/host/bluedroid/api/esp_gap_ble_api.c | 37 + .../api/include/api/esp_ble_l2cap_api.h | 382 ++++ .../api/include/api/esp_gap_ble_api.h | 60 + .../bt/host/bluedroid/btc/core/btc_main.c | 12 + .../btc/profile/std/ble_l2cap/btc_ble_l2cap.c | 1060 +++++++++++ .../btc/profile/std/gap/btc_gap_ble.c | 35 + .../btc/profile/std/include/btc_ble_l2cap.h | 89 + .../btc/profile/std/include/btc_gap_ble.h | 4 + .../include/common/bluedroid_user_config.h | 48 + .../common/include/common/bt_target.h | 80 +- .../bt/host/bluedroid/hci/packet_fragmenter.c | 4 + .../bt/host/bluedroid/stack/btm/btm_pm.c | 10 + .../bt/host/bluedroid/stack/btu/btu_init.c | 11 + .../host/bluedroid/stack/gatt/att_protocol.c | 91 +- .../bt/host/bluedroid/stack/gatt/gatt_api.c | 63 + .../bt/host/bluedroid/stack/gatt/gatt_auth.c | 6 + .../bt/host/bluedroid/stack/gatt/gatt_cl.c | 83 +- .../bt/host/bluedroid/stack/gatt/gatt_eatt.c | 1369 +++++++++++++++ .../bt/host/bluedroid/stack/gatt/gatt_main.c | 21 +- .../bt/host/bluedroid/stack/gatt/gatt_sr.c | 90 +- .../bt/host/bluedroid/stack/gatt/gatt_utils.c | 27 + .../stack/gatt/include/gatt_eatt_int.h | 48 + .../bluedroid/stack/gatt/include/gatt_int.h | 41 +- .../bluedroid/stack/include/stack/gatt_api.h | 5 + .../bluedroid/stack/include/stack/l2c_api.h | 24 + .../bluedroid/stack/include/stack/l2cdefs.h | 16 +- .../bluedroid/stack/l2cap/include/l2c_int.h | 109 +- .../bt/host/bluedroid/stack/l2cap/l2c_api.c | 115 +- .../bt/host/bluedroid/stack/l2cap/l2c_ble.c | 222 ++- .../host/bluedroid/stack/l2cap/l2c_ble_ecfc.c | 1547 +++++++++++++++++ .../bluedroid/stack/l2cap/l2c_ble_le_coc.c | 1458 ++++++++++++++++ .../bt/host/bluedroid/stack/l2cap/l2c_link.c | 6 + .../bt/host/bluedroid/stack/l2cap/l2c_main.c | 41 +- .../bt/host/bluedroid/stack/l2cap/l2c_utils.c | 207 ++- 39 files changed, 7689 insertions(+), 64 deletions(-) create mode 100644 components/bt/host/bluedroid/api/esp_ble_l2cap_api.c create mode 100644 components/bt/host/bluedroid/api/include/api/esp_ble_l2cap_api.h create mode 100644 components/bt/host/bluedroid/btc/profile/std/ble_l2cap/btc_ble_l2cap.c create mode 100644 components/bt/host/bluedroid/btc/profile/std/include/btc_ble_l2cap.h create mode 100644 components/bt/host/bluedroid/stack/gatt/gatt_eatt.c create mode 100644 components/bt/host/bluedroid/stack/gatt/include/gatt_eatt_int.h create mode 100644 components/bt/host/bluedroid/stack/l2cap/l2c_ble_ecfc.c create mode 100644 components/bt/host/bluedroid/stack/l2cap/l2c_ble_le_coc.c diff --git a/components/bt/common/btc/core/btc_task.c b/components/bt/common/btc/core/btc_task.c index ee3ce1fe153..c3d35040d4d 100644 --- a/components/bt/common/btc/core/btc_task.c +++ b/components/bt/common/btc/core/btc_task.c @@ -26,6 +26,9 @@ #include "btc_gap_ble.h" #include "btc_iso_ble.h" #include "btc_ble_cte.h" +#if (BLE_L2CAP_COC_INCLUDED == TRUE) +#include "btc_ble_l2cap.h" +#endif #include "btc/btc_dm.h" #include "bta/bta_gatt_api.h" #if CLASSIC_BT_INCLUDED @@ -273,6 +276,9 @@ static const btc_func_t profile_tab[BTC_PID_NUM] = { #if (BLE_FEAT_CTE_EN == TRUE) [BTC_PID_BLE_CTE] = {btc_ble_cte_call_handler, btc_ble_cte_cb_handler }, #endif // #if (BLE_FEAT_CTE_EN == TRUE) +#if (BLE_L2CAP_COC_INCLUDED == TRUE) + [BTC_PID_BLE_L2CAP] = {btc_ble_l2cap_call_handler, btc_ble_l2cap_cb_handler }, +#endif // #if (BLE_L2CAP_COC_INCLUDED == TRUE) }; /***************************************************************************** diff --git a/components/bt/common/btc/include/btc/btc_task.h b/components/bt/common/btc/include/btc/btc_task.h index 00f0c1c233e..f8cb25f8748 100644 --- a/components/bt/common/btc/include/btc/btc_task.h +++ b/components/bt/common/btc/include/btc/btc_task.h @@ -117,6 +117,9 @@ typedef enum { #if (BLE_FEAT_CTE_EN == TRUE) BTC_PID_BLE_CTE, #endif // #if (BLE_FEAT_CTE_EN == TRUE) +#if (BLE_L2CAP_COC_INCLUDED == TRUE) + BTC_PID_BLE_L2CAP, +#endif // #if (BLE_L2CAP_COC_INCLUDED == TRUE) BTC_PID_NUM, } btc_pid_t; //btc profile id diff --git a/components/bt/host/bluedroid/CMakeLists.txt b/components/bt/host/bluedroid/CMakeLists.txt index c5b9acc0f9b..5648ac2fc74 100644 --- a/components/bt/host/bluedroid/CMakeLists.txt +++ b/components/bt/host/bluedroid/CMakeLists.txt @@ -324,6 +324,26 @@ if(CONFIG_BT_BLE_FEAT_ISO_EN) ) endif() +if(CONFIG_BT_BLE_L2CAP_COC_ENABLED) + list(APPEND bluedroid_host_srcs + "${CMAKE_CURRENT_LIST_DIR}/stack/l2cap/l2c_ble_le_coc.c" + "${CMAKE_CURRENT_LIST_DIR}/btc/profile/std/ble_l2cap/btc_ble_l2cap.c" + "${CMAKE_CURRENT_LIST_DIR}/api/esp_ble_l2cap_api.c" + ) +endif() + +if(CONFIG_BT_BLE_L2CAP_ENHANCED_COC) + list(APPEND bluedroid_host_srcs + "${CMAKE_CURRENT_LIST_DIR}/stack/l2cap/l2c_ble_ecfc.c" + ) +endif() + +if(CONFIG_BT_BLE_EATT_ENABLE) + list(APPEND bluedroid_host_srcs + "${CMAKE_CURRENT_LIST_DIR}/stack/gatt/gatt_eatt.c" + ) +endif() + if(CONFIG_BT_BLE_FEAT_CTE_EN) list(APPEND bluedroid_host_srcs "${CMAKE_CURRENT_LIST_DIR}/stack/btm/btm_ble_cte.c" diff --git a/components/bt/host/bluedroid/Kconfig.in b/components/bt/host/bluedroid/Kconfig.in index 32699f84816..bb5215d39e2 100644 --- a/components/bt/host/bluedroid/Kconfig.in +++ b/components/bt/host/bluedroid/Kconfig.in @@ -1666,6 +1666,87 @@ config BT_BLE_HIGH_DUTY_ADV_INTERVAL help This enable BLE high duty advertising interval feature +menu "Bluedroid L2CAP CoC" + # LE CoC client code is compiled only with GATTC and server code only with + # GATTS (see BLE_L2CAP_COC_CLIENT/SERVER_INCLUDED in bt_target.h). Without + # either, enabling CoC would silently compile out entirely, so require at + # least one GATT role to be enabled. + depends on BT_BLE_ENABLED && (BT_GATTC_ENABLE || BT_GATTS_ENABLE) + + config BT_BLE_L2CAP_COC_ENABLED + bool "Enable BLE L2CAP Connection Oriented Channels" + default n + help + Enable LE Credit Based Flow Control mode L2CAP CoC in Bluedroid stack. + Independent of Classic Bluetooth L2CAP; does not affect esp_bt_l2cap_* APIs. + + config BT_BLE_L2CAP_COC_MAX_CHAN + int "Maximum LE CoC channels" + depends on BT_BLE_L2CAP_COC_ENABLED + range 1 15 + default 5 + + config BT_BLE_L2CAP_COC_MPS + int "Default MPS (L2CAP fragment size)" + depends on BT_BLE_L2CAP_COC_ENABLED + range 23 65533 if !BT_BLE_L2CAP_ENHANCED_COC + range 64 65533 if BT_BLE_L2CAP_ENHANCED_COC + default 247 + help + Default Maximum PDU Payload Size for LE CoC channels. Legacy LE Credit + Based Flow Control allows 23–65533 octets (section 4.22). Enhanced + Credit Based Flow Control (ECFC/EATT) requires 64–65533 (section 4.25). + When ECFC is enabled the minimum is raised to 64 automatically. + + config BT_BLE_L2CAP_COC_INIT_CREDITS + int "Initial RX credit window (K-frames per channel)" + depends on BT_BLE_L2CAP_COC_ENABLED + range 1 64 + default 24 + help + Number of LE CoC RX credits granted to the peer when a channel opens. + One credit allows the peer to send one K-frame. A larger window can + raise sustained throughput but increases how many in-flight frames + the peer may send before waiting for more credits (higher RX memory + pressure). A smaller window reduces that pressure but can lower + throughput. Manual credit mode can stall if a single SDU needs more + K-frames than this window; prefer automatic credit mode or a larger + MPS when using large MTUs. + + config BT_BLE_L2CAP_ENHANCED_COC + bool "Enable Enhanced Credit Based Flow Control (ECFC)" + depends on BT_BLE_L2CAP_COC_ENABLED + default n + help + Enable LE Enhanced CoC (L2CAP signaling 0x17/0x18) for multi-channel + establishment. Required for EATT multi-bearer support. + + config BT_BLE_EATT_ENABLE + bool "Enable Enhanced ATT (EATT)" + depends on BT_BLE_L2CAP_COC_ENABLED && BT_BLE_L2CAP_ENHANCED_COC + default n + help + Enable EATT bearers over LE Enhanced CoC (PSM 0x0027). + GATT operations may use multiple parallel bearers after link encryption. + + config BT_BLE_EATT_CHAN_NUM + int "Number of EATT bearers per connection" + depends on BT_BLE_EATT_ENABLE + range 1 BT_BLE_L2CAP_COC_MAX_CHAN + default 3 + help + Number of parallel EATT bearers established per connection. Must not + exceed the maximum LE CoC channels, since EATT bearers are LE CoC + channels; the range is capped by BT_BLE_L2CAP_COC_MAX_CHAN. + + config BT_BLE_EATT_MTU + int "EATT bearer MTU" + depends on BT_BLE_EATT_ENABLE + range 64 517 + default 247 + +endmenu + config BT_ABORT_WHEN_ALLOCATION_FAILS bool "Abort when memory allocation fails in BT/BLE stack" depends on BT_BLUEDROID_ENABLED diff --git a/components/bt/host/bluedroid/api/esp_ble_l2cap_api.c b/components/bt/host/bluedroid/api/esp_ble_l2cap_api.c new file mode 100644 index 00000000000..250b5ef5748 --- /dev/null +++ b/components/bt/host/bluedroid/api/esp_ble_l2cap_api.c @@ -0,0 +1,222 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include "esp_bt_main.h" +#include "esp_bt_defs.h" +#include "esp_ble_l2cap_api.h" +#include "btc/btc_manage.h" +#include "btc/btc_task.h" + +#if (BLE_L2CAP_COC_INCLUDED == TRUE) +#include "common/bt_target.h" +#include "btc_ble_l2cap.h" + +/* LE PSM valid range per Core Spec: 0x0001..0x00FF */ +#define ESP_BLE_L2CAP_IS_VALID_LE_PSM(psm) ((psm) > 0x0000 && (psm) < 0x0100) + +/* Minimum MTU per Core Spec Vol 3 Part A: 23 for LE credit based (4.22), + * 64 for enhanced credit based / ECFC (4.25). */ +#define ESP_BLE_L2CAP_LE_MIN_MTU 23 +#define ESP_BLE_L2CAP_ECFC_MIN_MTU 64 +/* Minimum MPS for enhanced credit based / ECFC channels (Core Spec Vol 3 + * Part A 4.25). */ +#define ESP_BLE_L2CAP_ECFC_MIN_MPS 64 +/* Core Spec Vol 3 Part A 4.25/4.27: a single enhanced credit based connection + * or reconfiguration request may target at most five channels, regardless of + * the (pool-sized) BT_BLE_L2CAP_COC_MAX_CHAN Kconfig value. */ +#define ESP_BLE_L2CAP_ECFC_MAX_REQ_CHANS 5 + +static esp_err_t btc_ble_l2cap_transfer(btc_ble_l2cap_act_t act, btc_ble_l2cap_args_t *arg) +{ + btc_msg_t msg = {0}; + + msg.sig = BTC_SIG_API_CALL; + msg.pid = BTC_PID_BLE_L2CAP; + msg.act = act; + + return (btc_transfer_context(&msg, arg, sizeof(btc_ble_l2cap_args_t), + btc_ble_l2cap_arg_deep_copy, + btc_ble_l2cap_arg_deep_free) == BT_STATUS_SUCCESS) + ? ESP_OK : ESP_FAIL; +} + +esp_err_t esp_ble_l2cap_register_callback(esp_ble_l2cap_cb_t callback) +{ + if (callback == NULL) { + return ESP_ERR_INVALID_ARG; + } + ESP_BLUEDROID_STATUS_CHECK(ESP_BLUEDROID_STATUS_ENABLED); + return (btc_profile_cb_set(BTC_PID_BLE_L2CAP, callback) == 0) ? ESP_OK : ESP_FAIL; +} + +esp_err_t esp_ble_l2cap_init(void) +{ + btc_ble_l2cap_args_t arg = {0}; + + ESP_BLUEDROID_STATUS_CHECK(ESP_BLUEDROID_STATUS_ENABLED); + return btc_ble_l2cap_transfer(BTC_BLE_L2CAP_ACT_INIT, &arg); +} + +esp_err_t esp_ble_l2cap_deinit(void) +{ + btc_ble_l2cap_args_t arg = {0}; + + ESP_BLUEDROID_STATUS_CHECK(ESP_BLUEDROID_STATUS_ENABLED); + return btc_ble_l2cap_transfer(BTC_BLE_L2CAP_ACT_DEINIT, &arg); +} + +#if (BLE_L2CAP_COC_SERVER_INCLUDED == TRUE) +esp_err_t esp_ble_l2cap_create_server(uint16_t psm, uint16_t mtu) +{ + btc_ble_l2cap_args_t arg = {0}; + + ESP_BLUEDROID_STATUS_CHECK(ESP_BLUEDROID_STATUS_ENABLED); + if (psm == 0 || mtu < ESP_BLE_L2CAP_LE_MIN_MTU || !ESP_BLE_L2CAP_IS_VALID_LE_PSM(psm)) { + return ESP_ERR_INVALID_ARG; + } + arg.create_server.psm = psm; + arg.create_server.mtu = mtu; + return btc_ble_l2cap_transfer(BTC_BLE_L2CAP_ACT_CREATE_SERVER, &arg); +} + +esp_err_t esp_ble_l2cap_delete_server(uint16_t psm) +{ + btc_ble_l2cap_args_t arg = {0}; + + ESP_BLUEDROID_STATUS_CHECK(ESP_BLUEDROID_STATUS_ENABLED); + if (psm == 0 || !ESP_BLE_L2CAP_IS_VALID_LE_PSM(psm)) { + return ESP_ERR_INVALID_ARG; + } + arg.delete_server.psm = psm; + return btc_ble_l2cap_transfer(BTC_BLE_L2CAP_ACT_DELETE_SERVER, &arg); +} + +esp_err_t esp_ble_l2cap_accept(uint16_t conn_id, uint8_t l2cap_id, + uint16_t chan_handle, bool accept, uint16_t mtu) +{ + btc_ble_l2cap_args_t arg = {0}; + + ESP_BLUEDROID_STATUS_CHECK(ESP_BLUEDROID_STATUS_ENABLED); + if (chan_handle == 0 || (accept && mtu < ESP_BLE_L2CAP_LE_MIN_MTU)) { + return ESP_ERR_INVALID_ARG; + } + arg.accept.conn_id = conn_id; + arg.accept.l2cap_id = l2cap_id; + arg.accept.chan_handle = chan_handle; + arg.accept.accept = accept; + arg.accept.mtu = mtu; + return btc_ble_l2cap_transfer(BTC_BLE_L2CAP_ACT_ACCEPT, &arg); +} +#endif /* BLE_L2CAP_COC_SERVER_INCLUDED */ + +#if (BLE_L2CAP_COC_CLIENT_INCLUDED == TRUE) +esp_err_t esp_ble_l2cap_connect(uint16_t conn_id, uint16_t psm, uint16_t mtu) +{ + btc_ble_l2cap_args_t arg = {0}; + + ESP_BLUEDROID_STATUS_CHECK(ESP_BLUEDROID_STATUS_ENABLED); + if (psm == 0 || mtu < ESP_BLE_L2CAP_LE_MIN_MTU || !ESP_BLE_L2CAP_IS_VALID_LE_PSM(psm)) { + return ESP_ERR_INVALID_ARG; + } + arg.connect.conn_id = conn_id; + arg.connect.psm = psm; + arg.connect.mtu = mtu; + return btc_ble_l2cap_transfer(BTC_BLE_L2CAP_ACT_CONNECT, &arg); +} +#endif /* BLE_L2CAP_COC_CLIENT_INCLUDED */ + +esp_err_t esp_ble_l2cap_disconnect(uint16_t chan_handle) +{ + btc_ble_l2cap_args_t arg = {0}; + + ESP_BLUEDROID_STATUS_CHECK(ESP_BLUEDROID_STATUS_ENABLED); + if (chan_handle == 0) { + return ESP_ERR_INVALID_ARG; + } + arg.disconnect.chan_handle = chan_handle; + return btc_ble_l2cap_transfer(BTC_BLE_L2CAP_ACT_DISCONNECT, &arg); +} + +esp_err_t esp_ble_l2cap_send(uint16_t chan_handle, uint8_t *data, uint16_t len) +{ + btc_ble_l2cap_args_t arg = {0}; + + ESP_BLUEDROID_STATUS_CHECK(ESP_BLUEDROID_STATUS_ENABLED); + if (chan_handle == 0 || data == NULL || len == 0) { + return ESP_ERR_INVALID_ARG; + } + arg.send.chan_handle = chan_handle; + arg.send.len = len; + arg.send.data = data; + return btc_ble_l2cap_transfer(BTC_BLE_L2CAP_ACT_SEND, &arg); +} + +esp_err_t esp_ble_l2cap_recv_ready(uint16_t chan_handle) +{ + btc_ble_l2cap_args_t arg = {0}; + + ESP_BLUEDROID_STATUS_CHECK(ESP_BLUEDROID_STATUS_ENABLED); + if (chan_handle == 0) { + return ESP_ERR_INVALID_ARG; + } + arg.recv_ready.chan_handle = chan_handle; + return btc_ble_l2cap_transfer(BTC_BLE_L2CAP_ACT_RECV_READY, &arg); +} + +esp_err_t esp_ble_l2cap_set_auto_credit(uint16_t chan_handle, bool enable) +{ + btc_ble_l2cap_args_t arg = {0}; + + ESP_BLUEDROID_STATUS_CHECK(ESP_BLUEDROID_STATUS_ENABLED); + if (chan_handle == 0) { + return ESP_ERR_INVALID_ARG; + } + arg.set_auto_credit.chan_handle = chan_handle; + arg.set_auto_credit.enable = enable; + return btc_ble_l2cap_transfer(BTC_BLE_L2CAP_ACT_SET_AUTO_CREDIT, &arg); +} + +#if (BLE_L2CAP_ENHANCED_COC_INCLUDED == TRUE) +#if (BLE_L2CAP_COC_CLIENT_INCLUDED == TRUE) +esp_err_t esp_ble_l2cap_connect_ecoc(uint16_t conn_id, uint16_t psm, uint16_t mtu, uint8_t num_chan) +{ + btc_ble_l2cap_args_t arg = {0}; + + ESP_BLUEDROID_STATUS_CHECK(ESP_BLUEDROID_STATUS_ENABLED); + if (psm == 0 || mtu < ESP_BLE_L2CAP_ECFC_MIN_MTU || num_chan == 0 || + num_chan > BLE_MAX_L2CAP_CLIENTS || + num_chan > ESP_BLE_L2CAP_ECFC_MAX_REQ_CHANS || !ESP_BLE_L2CAP_IS_VALID_LE_PSM(psm)) { + return ESP_ERR_INVALID_ARG; + } + arg.connect_ecoc.conn_id = conn_id; + arg.connect_ecoc.psm = psm; + arg.connect_ecoc.mtu = mtu; + arg.connect_ecoc.num_chan = num_chan; + return btc_ble_l2cap_transfer(BTC_BLE_L2CAP_ACT_CONNECT_ECOC, &arg); +} +#endif /* BLE_L2CAP_COC_CLIENT_INCLUDED */ + +esp_err_t esp_ble_l2cap_reconfig(uint16_t *chan_handles, uint8_t num_chan, uint16_t mtu, uint16_t mps) +{ + btc_ble_l2cap_args_t arg = {0}; + + ESP_BLUEDROID_STATUS_CHECK(ESP_BLUEDROID_STATUS_ENABLED); + if (chan_handles == NULL || num_chan == 0 || num_chan > BLE_MAX_L2CAP_CLIENTS || + num_chan > ESP_BLE_L2CAP_ECFC_MAX_REQ_CHANS || + mtu < ESP_BLE_L2CAP_ECFC_MIN_MTU || mps < ESP_BLE_L2CAP_ECFC_MIN_MPS) { + return ESP_ERR_INVALID_ARG; + } + arg.reconfig.num_chan = num_chan; + arg.reconfig.mtu = mtu; + arg.reconfig.mps = mps; + memcpy(arg.reconfig.chan_handles, chan_handles, num_chan * sizeof(uint16_t)); + return btc_ble_l2cap_transfer(BTC_BLE_L2CAP_ACT_RECONFIG, &arg); +} +#endif /* BLE_L2CAP_ENHANCED_COC_INCLUDED */ + +#endif /* BLE_L2CAP_COC_INCLUDED == TRUE */ diff --git a/components/bt/host/bluedroid/api/esp_gap_ble_api.c b/components/bt/host/bluedroid/api/esp_gap_ble_api.c index 81a8069848b..36bdf0f4c9c 100644 --- a/components/bt/host/bluedroid/api/esp_gap_ble_api.c +++ b/components/bt/host/bluedroid/api/esp_gap_ble_api.c @@ -14,6 +14,10 @@ #include "btc_gap_ble.h" #include "btc/btc_ble_storage.h" #include "esp_random.h" +#include "common/bt_target.h" +#if (BLE_EATT_INCLUDED == TRUE) +#include "stack/gatt_api.h" +#endif /* Hard upper bound to prevent excessive allocations in BTC/BTA layers. */ #define ESP_GAP_BLE_EXT_ADV_DATA_MAX_LEN 1650U @@ -3226,3 +3230,36 @@ esp_err_t esp_ble_cs_procedure_enable(esp_ble_cs_procedure_enable_params *proced } #endif + +#if (BLE_EATT_INCLUDED == TRUE) +/* Intentionally synchronous: updates the pre-connection EATT bearer count only. + * Must be called before the link is encrypted / bearers are established (see API + * doc). No btc_transfer_context dispatch — this is a setup-time config write, not + * an async stack procedure, and callers need immediate ESP_ERR_INVALID_ARG feedback. */ +esp_err_t esp_ble_eatt_set_chan_num(uint8_t num_chan) +{ + ESP_BLUEDROID_STATUS_CHECK(ESP_BLUEDROID_STATUS_ENABLED); + if (num_chan == 0 || num_chan > GATT_EATT_MAX_CHAN) { + return ESP_ERR_INVALID_ARG; + } + GATT_EattSetChanNum(num_chan); + return ESP_OK; +} + +/* Intentionally synchronous: sets the preferred EATT bearer (ec->default_lcid) for + * subsequent GATT client TX routing on this connection. No btc_transfer_context + * dispatch — by design this is an immediate preference update with synchronous + * validation (invalid conn_id/cid returns ESP_ERR_INVALID_ARG at call time). + * Client-only: defined solely when the EATT client role is built in, so a build + * without it fails at link time rather than exposing a stub. */ +#if (BLE_EATT_CLIENT_INCLUDED == TRUE) +esp_err_t esp_ble_eatt_set_default_bearer(uint16_t conn_id, uint16_t cid) +{ + ESP_BLUEDROID_STATUS_CHECK(ESP_BLUEDROID_STATUS_ENABLED); + if (!GATT_EattSetDefaultBearer(conn_id, cid)) { + return ESP_ERR_INVALID_ARG; + } + return ESP_OK; +} +#endif /* BLE_EATT_CLIENT_INCLUDED */ +#endif /* BLE_EATT_INCLUDED */ diff --git a/components/bt/host/bluedroid/api/include/api/esp_ble_l2cap_api.h b/components/bt/host/bluedroid/api/include/api/esp_ble_l2cap_api.h new file mode 100644 index 00000000000..0a23f180987 --- /dev/null +++ b/components/bt/host/bluedroid/api/include/api/esp_ble_l2cap_api.h @@ -0,0 +1,382 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef __ESP_BLE_L2CAP_API_H__ +#define __ESP_BLE_L2CAP_API_H__ + +#include +#include + +#include "esp_err.h" +#include "esp_bt_defs.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief LE L2CAP connection-oriented channel (CoC) callback events + */ +typedef enum { + ESP_BLE_L2CAP_COC_CONNECTED_EVT = 0, /*!< When an LE CoC channel is connected or the connection attempt fails, the event comes */ + ESP_BLE_L2CAP_COC_DISCONNECTED_EVT, /*!< When an LE CoC channel is disconnected, the event comes */ + ESP_BLE_L2CAP_COC_ACCEPT_EVT, /*!< When a remote device requests a new LE CoC connection to a local server, the event comes */ + ESP_BLE_L2CAP_COC_DATA_RECEIVED_EVT, /*!< When a complete SDU is received on an LE CoC channel, the event comes */ + ESP_BLE_L2CAP_COC_TX_UNSTALLED_EVT, /*!< When TX credits are restored and more data may be sent, the event comes */ + ESP_BLE_L2CAP_COC_RECONFIG_COMPLETED_EVT, /*!< When a local channel reconfiguration request completes, the event comes */ + ESP_BLE_L2CAP_COC_PEER_RECONFIGURED_EVT, /*!< When the peer completes a channel reconfiguration, the event comes */ + ESP_BLE_L2CAP_COC_EVT_MAX, +} esp_ble_l2cap_evt_t; + +/** + * @brief LE CoC channel information + * + * Delivered in `ESP_BLE_L2CAP_COC_CONNECTED_EVT` and reconfiguration events when the + * operation succeeds. + */ +typedef struct { + uint16_t scid; /*!< Local channel identifier (CID) */ + uint16_t dcid; /*!< Remote channel identifier (CID) */ + uint16_t psm; /*!< Protocol/Service Multiplexer */ + uint16_t our_mtu; /*!< Local maximum SDU size (MTU) */ + uint16_t peer_mtu; /*!< Peer maximum SDU size (MTU) */ + uint16_t our_mps; /*!< Local maximum PDU payload size (MPS) */ + uint16_t peer_mps; /*!< Peer maximum PDU payload size (MPS) */ +} esp_ble_l2cap_chan_info_t; + +/** + * @brief LE L2CAP CoC callback parameters union + */ +typedef union { + /** + * @brief ESP_BLE_L2CAP_COC_CONNECTED_EVT + */ + struct { + uint16_t conn_id; /*!< GATT connection id of the underlying ACL link. May be 0 if not yet bound */ + uint16_t chan_handle; /*!< Local L2CAP channel identifier (CID) of the CoC */ + uint16_t status; /*!< Connection result. 0 (`L2CAP_CONN_OK`) means success; other values are L2CAP connection result codes */ + esp_ble_l2cap_chan_info_t chan_info; /*!< Channel information. Valid only when `status` is 0 (`L2CAP_CONN_OK`) */ + } coc_connected; /*!< LE L2CAP callback param of ESP_BLE_L2CAP_COC_CONNECTED_EVT */ + + /** + * @brief ESP_BLE_L2CAP_COC_DISCONNECTED_EVT + */ + struct { + uint16_t conn_id; /*!< Reserved. Currently not populated by the stack (0) */ + uint16_t chan_handle; /*!< Local L2CAP channel identifier (CID) of the disconnected CoC */ + } coc_disconnected; /*!< LE L2CAP callback param of ESP_BLE_L2CAP_COC_DISCONNECTED_EVT */ + + /** + * @brief ESP_BLE_L2CAP_COC_ACCEPT_EVT + */ + struct { + uint16_t conn_id; /*!< GATT connection id of the underlying ACL link. May be 0 if not yet bound */ + uint16_t chan_handle; /*!< Proposed local L2CAP channel identifier (CID) */ + uint8_t l2cap_id; /*!< L2CAP signaling identifier of the connection request */ + uint16_t psm; /*!< Protocol/Service Multiplexer requested by the peer */ + } coc_accept; /*!< LE L2CAP callback param of ESP_BLE_L2CAP_COC_ACCEPT_EVT */ + + /** + * @brief ESP_BLE_L2CAP_COC_DATA_RECEIVED_EVT + */ + struct { + uint16_t chan_handle; /*!< Local L2CAP channel identifier (CID) that received the SDU */ + uint16_t len; /*!< SDU length in bytes */ + uint8_t *data; /*!< Pointer to the received SDU payload. Valid only during the callback */ + } data_received; /*!< LE L2CAP callback param of ESP_BLE_L2CAP_COC_DATA_RECEIVED_EVT */ + + /** + * @brief ESP_BLE_L2CAP_COC_TX_UNSTALLED_EVT + */ + struct { + uint16_t chan_handle; /*!< Local L2CAP channel identifier (CID) whose TX path is no longer congested */ + } tx_unstalled; /*!< LE L2CAP callback param of ESP_BLE_L2CAP_COC_TX_UNSTALLED_EVT */ + + /** + * @brief ESP_BLE_L2CAP_COC_RECONFIG_COMPLETED_EVT + */ + struct { + uint16_t chan_handle; /*!< Local L2CAP channel identifier (CID) that was reconfigured */ + uint16_t status; /*!< Reconfiguration result. 0 (`L2CAP_LE_RECONFIG_OK`) means success */ + esp_ble_l2cap_chan_info_t chan_info; /*!< Updated channel information. Valid only when `status` is 0 (`L2CAP_LE_RECONFIG_OK`) */ + } reconfig_completed; /*!< LE L2CAP callback param of ESP_BLE_L2CAP_COC_RECONFIG_COMPLETED_EVT */ + + /** + * @brief ESP_BLE_L2CAP_COC_PEER_RECONFIGURED_EVT + */ + struct { + uint16_t chan_handle; /*!< Local L2CAP channel identifier (CID) reconfigured by the peer */ + uint16_t status; /*!< Reconfiguration result. 0 (`L2CAP_LE_RECONFIG_OK`) means success */ + esp_ble_l2cap_chan_info_t chan_info; /*!< Updated channel information. Valid only when `status` is 0 (`L2CAP_LE_RECONFIG_OK`) */ + } peer_reconfigured; /*!< LE L2CAP callback param of ESP_BLE_L2CAP_COC_PEER_RECONFIGURED_EVT */ +} esp_ble_l2cap_cb_param_t; + +/** + * @brief LE L2CAP CoC callback function type + * + * @param[in] event: Event type + * @param[in] param: Pointer to callback parameter, currently is union type + */ +typedef void (*esp_ble_l2cap_cb_t)(esp_ble_l2cap_evt_t event, esp_ble_l2cap_cb_param_t *param); + +/** + * @brief Register the LE L2CAP CoC callback function + * + * @param[in] callback: Pointer to the callback function + * + * @return + * - ESP_OK: success + * - ESP_ERR_INVALID_ARG: callback is NULL + * - ESP_FAIL: other error + */ +esp_err_t esp_ble_l2cap_register_callback(esp_ble_l2cap_cb_t callback); + +/** + * @brief Initialize the LE L2CAP CoC module + * + * Requires `CONFIG_BT_BLE_L2CAP_COC_ENABLED`. + * This function should be called after `esp_bluedroid_enable()` completes successfully. + * + * @return + * - ESP_OK: success + * - ESP_FAIL: other error + */ +esp_err_t esp_ble_l2cap_init(void); + +/** + * @brief Deinitialize the LE L2CAP CoC module + * + * Deregisters all local CoC servers created by this module. + * This function should be called after `esp_ble_l2cap_init()` completes successfully. + * + * @return + * - ESP_OK: success + * - ESP_FAIL: other error + */ +esp_err_t esp_ble_l2cap_deinit(void); + +/** + * @brief Register a local LE CoC server on the given PSM + * + * When a remote device requests a connection to this PSM, the callback receives + * `ESP_BLE_L2CAP_COC_ACCEPT_EVT`. + * + * @param[in] psm: LE Protocol/Service Multiplexer. Valid range is 0x0001 to 0x00FF + * @param[in] mtu: Local maximum SDU size (MTU) for channels accepted on this PSM + * + * @return + * - ESP_OK: success + * - ESP_ERR_INVALID_ARG: invalid `psm` or `mtu` + * - ESP_FAIL: other error + */ +esp_err_t esp_ble_l2cap_create_server(uint16_t psm, uint16_t mtu); + +/** + * @brief Deregister a local LE CoC server + * + * @param[in] psm: LE Protocol/Service Multiplexer previously registered with `esp_ble_l2cap_create_server()` + * + * @return + * - ESP_OK: success + * - ESP_ERR_INVALID_ARG: `psm` is 0 + * - ESP_FAIL: other error + */ +esp_err_t esp_ble_l2cap_delete_server(uint16_t psm); + +/** + * @brief Connect to a remote LE CoC server (client role) + * + * When the connection attempt completes, the callback receives + * `ESP_BLE_L2CAP_COC_CONNECTED_EVT`. + * + * @param[in] conn_id: GATT connection id of the underlying ACL link + * @param[in] psm: Remote LE Protocol/Service Multiplexer. Valid range is 0x0001 to 0x00FF + * @param[in] mtu: Local maximum SDU size (MTU) to propose for the channel + * + * @return + * - ESP_OK: success + * - ESP_ERR_INVALID_ARG: invalid `psm` or `mtu` + * - ESP_FAIL: other error + */ +esp_err_t esp_ble_l2cap_connect(uint16_t conn_id, uint16_t psm, uint16_t mtu); + +/** + * @brief Accept or reject an inbound LE CoC connection request (server role) + * + * Call this function in response to `ESP_BLE_L2CAP_COC_ACCEPT_EVT`. + * When accepted, the callback receives `ESP_BLE_L2CAP_COC_CONNECTED_EVT`. + * When rejected, no `ESP_BLE_L2CAP_COC_CONNECTED_EVT` is reported to the local server. + * + * @param[in] conn_id: GATT connection id from `ESP_BLE_L2CAP_COC_ACCEPT_EVT` + * @param[in] l2cap_id: L2CAP signaling identifier from `ESP_BLE_L2CAP_COC_ACCEPT_EVT` + * @param[in] chan_handle: Proposed local channel identifier from `ESP_BLE_L2CAP_COC_ACCEPT_EVT` + * @param[in] accept: True to accept the connection; false to reject it + * @param[in] mtu: Local maximum SDU size (MTU) to use when accepting. Ignored when rejecting + * + * @return + * - ESP_OK: success + * - ESP_ERR_INVALID_ARG: `chan_handle` is 0 + * - ESP_FAIL: other error + */ +esp_err_t esp_ble_l2cap_accept(uint16_t conn_id, uint8_t l2cap_id, + uint16_t chan_handle, bool accept, uint16_t mtu); + +/** + * @brief Disconnect an LE CoC channel + * + * When the channel is closed, the callback receives `ESP_BLE_L2CAP_COC_DISCONNECTED_EVT`. + * + * @param[in] chan_handle: Local L2CAP channel identifier (CID) of the CoC to disconnect + * + * @return + * - ESP_OK: success + * - ESP_ERR_INVALID_ARG: `chan_handle` is 0 + * - ESP_FAIL: other error + */ +esp_err_t esp_ble_l2cap_disconnect(uint16_t chan_handle); + +/** + * @brief Send an SDU on an LE CoC channel + * + * Transmission is credit-based. The host accepts at most one SDU per + * channel in its TX queue; further calls return `ESP_OK` but the SDU + * may be dropped if the channel is busy. Retry on + * `ESP_BLE_L2CAP_COC_TX_UNSTALLED_EVT` or after the pipeline drains. + * + * This function returns `ESP_OK` when the send request is queued to the host stack. + * It does not indicate that the SDU has already been transmitted. + * + * @param[in] chan_handle: Local L2CAP channel identifier (CID) + * @param[in] data: Pointer to the SDU payload to send + * @param[in] len: SDU length in bytes + * + * @return + * - ESP_OK: success + * - ESP_ERR_INVALID_ARG: invalid argument + * - ESP_FAIL: other error + */ +esp_err_t esp_ble_l2cap_send(uint16_t chan_handle, uint8_t *data, uint16_t len); + +/** + * @brief Return RX credits after processing a received SDU (manual credit mode) + * + * Call this function once after the application has finished handling the SDU delivered + * in `ESP_BLE_L2CAP_COC_DATA_RECEIVED_EVT`. The stack returns the exact number of RX + * credits that SDU consumed (a multi-frame SDU consumes more than one), so no credits + * are leaked regardless of how the SDU was fragmented. + * + * This call only has an effect when the channel is in manual credit mode + * (`esp_ble_l2cap_set_auto_credit(chan_handle, false)`). In the default automatic mode + * the stack returns credits itself and this call is a harmless no-op. See + * `esp_ble_l2cap_set_auto_credit()` for the trade-offs between the two modes. + * + * @param[in] chan_handle: Local L2CAP channel identifier (CID) + * + * @return + * - ESP_OK: success + * - ESP_ERR_INVALID_ARG: `chan_handle` is 0 + * - ESP_FAIL: other error + */ +esp_err_t esp_ble_l2cap_recv_ready(uint16_t chan_handle); + +/** + * @brief Connect multiple LE CoC channels in one Enhanced Credit Flow Control request (client role) + * + * Requires `CONFIG_BT_BLE_L2CAP_ENHANCED_COC`. The corresponding API symbols are + * available only when this option is enabled at build time. + * One `ESP_BLE_L2CAP_COC_CONNECTED_EVT` is reported per channel. + * + * @param[in] conn_id: GATT connection id of the underlying ACL link + * @param[in] psm: Remote LE Protocol/Service Multiplexer. Valid range is 0x0001 to 0x00FF + * @param[in] mtu: Local maximum SDU size (MTU) to propose for each channel + * @param[in] num_chan: Number of CoC channels to open in a single request + * + * @return + * - ESP_OK: success + * - ESP_ERR_INVALID_ARG: invalid argument + * - ESP_FAIL: other error + */ +esp_err_t esp_ble_l2cap_connect_ecoc(uint16_t conn_id, uint16_t psm, uint16_t mtu, uint8_t num_chan); + +/** + * @brief Reconfigure MTU and/or MPS on one or more LE CoC channels + * + * Requires `CONFIG_BT_BLE_L2CAP_ENHANCED_COC`. The corresponding API symbols are + * available only when this option is enabled at build time. + * When the local request completes, the callback receives + * `ESP_BLE_L2CAP_COC_RECONFIG_COMPLETED_EVT` per channel. + * + * @param[in] chan_handles: Array of local L2CAP channel identifiers (CIDs) to reconfigure + * @param[in] num_chan: Number of entries in `chan_handles` + * @param[in] mtu: New local maximum SDU size (MTU) + * @param[in] mps: New local maximum PDU payload size (MPS) + * + * @return + * - ESP_OK: success + * - ESP_ERR_INVALID_ARG: invalid argument + * - ESP_FAIL: other error + */ +esp_err_t esp_ble_l2cap_reconfig(uint16_t *chan_handles, uint8_t num_chan, uint16_t mtu, uint16_t mps); + +/** + * @brief Select the RX credit return policy for an LE CoC channel + * + * LE CoC flow control is credit based: one credit == one K-frame (an L2CAP PDU of + * up to MPS bytes). A single application SDU (up to MTU bytes) may be fragmented into + * several K-frames, so it consumes several RX credits. This function chooses how those + * consumed credits are returned to the peer. + * + * Automatic mode (enable = true, the default): + * - Behaviour: the stack returns credits itself as each K-frame is consumed (returns + * are batched for efficiency and flushed as the window drains). In this mode + * `esp_ble_l2cap_recv_ready()` is a no-op and does not need to be called. + * - Pros: highest sustained RX throughput (credits are replenished on the Bluetooth + * task with no application round trip); no per-SDU bookkeeping for the application; + * works for any MTU/MPS, including SDUs larger than the credit window (credits are + * returned mid-SDU so reassembly can always complete). + * - Cons: no application-level backpressure. The peer keeps sending as fast as the + * credit window allows, regardless of how quickly the application drains the data. + * Recommended for throughput-oriented use and as the general default. + * + * Manual mode (enable = false): + * - Behaviour: the stack withholds the consumed credits; the application returns them + * by calling `esp_ble_l2cap_recv_ready()` once after it has finished processing each + * SDU delivered in `ESP_BLE_L2CAP_COC_DATA_RECEIVED_EVT`. The stack tracks the exact + * number of K-frames each SDU consumed and returns that many credits per call, so a + * multi-frame SDU does not leak credits. + * - Pros: application-level backpressure. The peer's flow is gated by the application's + * processing pace (if `recv_ready()` is not called, the peer stalls once its credits + * run out), which is useful when the receiver has limited buffering. + * - Cons: lower sustained throughput than automatic mode, because each replenishment + * incurs an application-to-stack round trip. + * + * Possible problem in manual mode (large SDUs): + * - Because credits are returned only after a complete SDU is delivered, a single SDU + * whose K-frame count exceeds the whole RX credit window (roughly when + * ceil((MTU + 2) / MPS) > window) can stall: the peer exhausts its credits before the + * SDU is complete, so the application never receives the event and never calls + * `recv_ready()`. The stack contains a deadlock breaker that returns the withheld + * credits mid-SDU in this situation so the transfer still completes (at the cost of + * weaker backpressure for that oversized SDU), and it logs a warning when manual mode + * is enabled on a channel where this can happen. For large MTUs prefer automatic mode + * or negotiate a larger MPS so a single SDU fits within the credit window. + * + * @param[in] chan_handle: Local L2CAP channel identifier (CID) + * @param[in] enable: True to enable automatic credit return (default); false for manual + * return via `esp_ble_l2cap_recv_ready()` + * + * @return + * - ESP_OK: success + * - ESP_ERR_INVALID_ARG: `chan_handle` is 0 + * - ESP_FAIL: other error + */ +esp_err_t esp_ble_l2cap_set_auto_credit(uint16_t chan_handle, bool enable); + +#ifdef __cplusplus +} +#endif + +#endif /* __ESP_BLE_L2CAP_API_H__ */ diff --git a/components/bt/host/bluedroid/api/include/api/esp_gap_ble_api.h b/components/bt/host/bluedroid/api/include/api/esp_gap_ble_api.h index b76289d8bf8..fa8165bc1ac 100644 --- a/components/bt/host/bluedroid/api/include/api/esp_gap_ble_api.h +++ b/components/bt/host/bluedroid/api/include/api/esp_gap_ble_api.h @@ -287,6 +287,7 @@ typedef enum { ESP_GAP_BLE_UTP_RECEIVE_EVT, /*!< When UTP data is received, the event comes */ ESP_GAP_BLE_CS_SET_SECURITY_REQUIREMENTS_CMPL_EVT, /*!< When CS set security requirements complete, the event comes */ ESP_GAP_BLE_CS_SET_DEFAULT_SECURITY_REQUIREMENTS_CMPL_EVT, /*!< When CS set default security requirements complete, the event comes */ + ESP_GAP_BLE_EATT_EVT, /*!< When an EATT bearer is connected or disconnected, the event comes. Requires `CONFIG_BT_BLE_EATT_ENABLE` */ ESP_GAP_BLE_EVT_MAX, /*!< when maximum advertising event complete, the event comes */ } esp_gap_ble_cb_event_t; @@ -3228,6 +3229,18 @@ typedef union { esp_ble_cs_step_info *step_info; /*!< steps information in the CS subevent */ } cs_subevt_result_continue; /*!< Event parameter of ESP_GAP_BLE_CS_SUBEVENT_RESULT_CONTINUE_EVT */ #endif // (BT_BLE_FEAT_CHANNEL_SOUNDING == TRUE) + + /** + * @brief ESP_GAP_BLE_EATT_EVT + * + * Requires `CONFIG_BT_BLE_EATT_ENABLE`. EATT bearers are established automatically + * after the ACL link is encrypted. + */ + struct ble_eatt_evt { + uint16_t conn_id; /*!< GATT connection id of the underlying ACL link. 0xFFFF (GATT_INVALID_CONN_ID) if not yet available. Note: 0 is a valid conn_id (the first BLE connection) */ + uint8_t status; /*!< EATT bearer status. 0: connected; 1: disconnected */ + uint16_t cid; /*!< Local L2CAP channel identifier (CID) of the EATT bearer */ + } eatt_evt; /*!< Event parameter of ESP_GAP_BLE_EATT_EVT */ } esp_ble_gap_cb_param_t; /** @@ -5167,6 +5180,53 @@ esp_err_t esp_ble_cs_set_procedure_params(esp_ble_cs_set_proc_params *procedure_ */ esp_err_t esp_ble_cs_procedure_enable(esp_ble_cs_procedure_enable_params *procedure_enable_params); +/** + * @brief Set the number of EATT bearers to establish per connection + * + * Requires `CONFIG_BT_BLE_EATT_ENABLE`. + * EATT bearers are created automatically after the link is encrypted. + * Call this function before the bearers are established. The value must + * not exceed `CONFIG_BT_BLE_EATT_CHAN_NUM` (compile-time maximum). + * + * This API is intentionally synchronous (does not dispatch through the + * BTC task): it only stores the requested bearer count for future + * connections and returns validation errors immediately. + * + * @param[in] num_chan: Number of EATT bearers to establish per connection + * + * @return + * - ESP_OK: success + * - ESP_ERR_INVALID_ARG: `num_chan` is 0 or greater than + * `CONFIG_BT_BLE_EATT_CHAN_NUM` + * + * @note Defined only when `CONFIG_BT_BLE_EATT_ENABLE` is set; calling it + * in a build with EATT disabled fails at link time (no definition). + */ +esp_err_t esp_ble_eatt_set_chan_num(uint8_t num_chan); + +/** + * @brief Set the preferred EATT bearer for GATT client operations on a connection + * + * Requires `CONFIG_BT_BLE_EATT_ENABLE`. + * By default the stack selects an available bearer automatically. + * Pass `cid` as 0 to restore automatic selection. + * + * This API is intentionally synchronous (does not dispatch through the + * BTC task): it updates the preferred bearer for GATT client TX routing + * and returns validation errors immediately. + * + * @param[in] conn_id: GATT connection id + * @param[in] cid: Local L2CAP channel identifier (CID) of the preferred EATT bearer + * + * @return + * - ESP_OK: success + * - ESP_ERR_INVALID_ARG: invalid `conn_id` or `cid` + * + * @note Defined only when `CONFIG_BT_BLE_EATT_ENABLE` is set; calling it + * in a build with EATT disabled fails at link time (no definition). + */ +esp_err_t esp_ble_eatt_set_default_bearer(uint16_t conn_id, uint16_t cid); + #ifdef __cplusplus } #endif diff --git a/components/bt/host/bluedroid/btc/core/btc_main.c b/components/bt/host/bluedroid/btc/core/btc_main.c index 279643bc628..e69306409e7 100644 --- a/components/bt/host/bluedroid/btc/core/btc_main.c +++ b/components/bt/host/bluedroid/btc/core/btc_main.c @@ -16,6 +16,9 @@ #include "bta_gattc_int.h" #include "bta_gatts_int.h" #include "bta_dm_int.h" +#if (BLE_EATT_INCLUDED == TRUE) +#include "gatt_eatt_int.h" +#endif static future_t *main_future[BTC_MAIN_FUTURE_NUM]; static SemaphoreHandle_t s_init_done_sem = NULL; @@ -48,6 +51,15 @@ static void btc_disable_bluetooth(void) void btc_init_callback(bt_status_t status) { +#if (BLE_EATT_INCLUDED == TRUE) + /* Only arm the EATT callback once BTE startup actually succeeded. On failure + * the partial-init cleanup path tears the stack down (and NULLs this cback + * in gatt_eatt_deinit), so registering it here would only briefly reference a + * non-running stack. Matches the deliberate NULL-on-teardown in deinit. */ + if (status == BT_STATUS_SUCCESS) { + gatt_eatt_register_evt_cback(btc_ble_gap_eatt_evt_cback); + } +#endif s_init_clean = (status == BT_STATUS_SUCCESS) ? false : true; future_ready(*btc_main_get_future_p(BTC_MAIN_INIT_FUTURE), (status == BT_STATUS_SUCCESS) ? FUTURE_SUCCESS : FUTURE_FAIL); diff --git a/components/bt/host/bluedroid/btc/profile/std/ble_l2cap/btc_ble_l2cap.c b/components/bt/host/bluedroid/btc/profile/std/ble_l2cap/btc_ble_l2cap.c new file mode 100644 index 00000000000..cb897c2f70d --- /dev/null +++ b/components/bt/host/bluedroid/btc/profile/std/ble_l2cap/btc_ble_l2cap.c @@ -0,0 +1,1060 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include "osi/allocator.h" +#include "btc/btc_task.h" +#include "btc/btc_manage.h" +#include "stack/l2c_api.h" +#include "l2c_int.h" +#include "stack/gatt_api.h" +#include "stack/btm_api.h" +#include "gatt_int.h" +#include "esp_err.h" +#include "btc_ble_l2cap.h" +#include "osi/list.h" + +#if (BLE_L2CAP_COC_INCLUDED == TRUE) + +#define BTC_BLE_L2CAP_TRACE_API(fmt, ...) BTC_TRACE_API("BLE_L2CAP: " fmt, ##__VA_ARGS__) +#define BTC_BLE_L2CAP_TRACE_DEBUG(fmt, ...) BTC_TRACE_DEBUG("BLE_L2CAP: " fmt, ##__VA_ARGS__) +#define BTC_BLE_L2CAP_TRACE_ERROR(fmt, ...) BTC_TRACE_ERROR("BLE_L2CAP: " fmt, ##__VA_ARGS__) + +typedef struct { + bool in_use; + uint16_t reg_psm; + uint16_t real_psm; + uint16_t mtu; +} btc_ble_l2cap_server_t; + +typedef struct { + bool initialized; +#if (BLE_L2CAP_COC_SERVER_INCLUDED == TRUE) + btc_ble_l2cap_server_t servers[BLE_MAX_L2CAP_CLIENTS]; +#endif +#if (BLE_L2CAP_COC_CLIENT_INCLUDED == TRUE) + /* reg_psm values this module created (via L2CA_RegisterLECoc) for outgoing + * client connections that have no server slot, so DEINIT can deregister them + * instead of leaking BLE RCB pool entries. */ + uint16_t client_reg_psms[BLE_MAX_L2CAP_CLIENTS]; +#endif +} btc_ble_l2cap_env_t; + +typedef struct { + bool in_use; + uint16_t conn_id; + BD_ADDR bda; +} btc_ble_l2cap_conn_bind_t; + +/* Per-channel lcid->bda shadow. The disconnect_ind callback only receives the + * lcid, and on some teardown paths (e.g. the classic L2CAP CSM link-loss path, + * l2cu_disconnect_chnl) the CCB is released BEFORE the callback runs, so + * l2cu_find_ccb_by_cid(lcid) returns NULL and the peer address can no longer be + * derived from the stack. Recording lcid->bda when the channel opens lets us + * still resolve the address at disconnect time and release the conn-bind slot. */ +typedef struct { + bool in_use; + uint16_t lcid; + BD_ADDR bda; +} btc_ble_l2cap_chan_bind_t; + +static btc_ble_l2cap_env_t s_l2cap_env; +static tL2CAP_APPL_INFO s_l2cap_appl; +static btc_ble_l2cap_conn_bind_t s_conn_bind[BLE_MAX_L2CAP_CLIENTS]; +static btc_ble_l2cap_chan_bind_t s_chan_bind[BLE_MAX_L2CAP_CLIENTS]; + +static void btc_ble_l2cap_post_event(esp_ble_l2cap_evt_t event, esp_ble_l2cap_cb_param_t *param); +static void btc_ble_l2cap_bind_conn(uint16_t conn_id, BD_ADDR bda); +static uint16_t btc_ble_l2cap_conn_id_from_bda(BD_ADDR bda); +static bool btc_ble_l2cap_bda_from_conn_id(uint16_t conn_id, BD_ADDR bda); +#if (BLE_L2CAP_COC_SERVER_INCLUDED == TRUE) +static btc_ble_l2cap_server_t *btc_ble_l2cap_find_server_by_real_psm(uint16_t psm); +#endif +static void btc_ble_l2cap_fill_chan_info_from_ccb(uint16_t chan_handle, + esp_ble_l2cap_chan_info_t *info); +#if (BLE_L2CAP_COC_CLIENT_INCLUDED == TRUE) +static uint16_t btc_ble_l2cap_get_or_register_psm(uint16_t real_psm, bool *newly_registered); +#endif + +/* ESP GATTC/GATTS APIs expose conn_id as tcb_idx (see BTC_GATT_GET_CONN_ID). */ +static uint16_t btc_ble_l2cap_app_conn_id_from_stack(UINT16 stack_conn_id) +{ + return (uint16_t)GATT_GET_TCB_IDX(stack_conn_id); +} + +static void btc_ble_l2cap_bind_conn(uint16_t conn_id, BD_ADDR bda) +{ + int i; + + for (i = 0; i < BLE_MAX_L2CAP_CLIENTS; i++) { + if (s_conn_bind[i].in_use && memcmp(s_conn_bind[i].bda, bda, BD_ADDR_LEN) == 0) { + s_conn_bind[i].conn_id = conn_id; + return; + } + } + for (i = 0; i < BLE_MAX_L2CAP_CLIENTS; i++) { + if (!s_conn_bind[i].in_use) { + s_conn_bind[i].in_use = true; + s_conn_bind[i].conn_id = conn_id; + memcpy(s_conn_bind[i].bda, bda, BD_ADDR_LEN); + return; + } + } + BTC_BLE_L2CAP_TRACE_ERROR("%s: conn bind table full (max=%d), conn_id=%u not tracked", + __func__, BLE_MAX_L2CAP_CLIENTS, conn_id); +} + +static void btc_ble_l2cap_unbind_bda(BD_ADDR bda) +{ + int i; + + for (i = 0; i < BLE_MAX_L2CAP_CLIENTS; i++) { + if (s_conn_bind[i].in_use && memcmp(s_conn_bind[i].bda, bda, BD_ADDR_LEN) == 0) { + memset(&s_conn_bind[i], 0, sizeof(s_conn_bind[i])); + return; + } + } +} + +/* Record (or refresh) the lcid->bda mapping for an opened CoC channel. */ +static void btc_ble_l2cap_track_chan(uint16_t lcid, BD_ADDR bda) +{ + int i, free_idx = -1; + + for (i = 0; i < BLE_MAX_L2CAP_CLIENTS; i++) { + if (s_chan_bind[i].in_use && s_chan_bind[i].lcid == lcid) { + memcpy(s_chan_bind[i].bda, bda, BD_ADDR_LEN); + return; + } + if (!s_chan_bind[i].in_use && free_idx < 0) { + free_idx = i; + } + } + if (free_idx >= 0) { + s_chan_bind[free_idx].in_use = true; + s_chan_bind[free_idx].lcid = lcid; + memcpy(s_chan_bind[free_idx].bda, bda, BD_ADDR_LEN); + } else { + /* Table full: without a mapping, disconnect_ind cannot recover the BDA + * once the CCB is gone, so the conn-bind slot for this peer would leak. + * Log it so the (normally unreachable) exhaustion is diagnosable. */ + BTC_BLE_L2CAP_TRACE_ERROR("%s: chan bind table full (max=%d), lcid=0x%04x not tracked", + __func__, BLE_MAX_L2CAP_CLIENTS, lcid); + } +} + +/* Look up the bda for an lcid and drop the entry. Returns true on success. */ +static bool btc_ble_l2cap_untrack_chan(uint16_t lcid, BD_ADDR out_bda) +{ + int i; + + for (i = 0; i < BLE_MAX_L2CAP_CLIENTS; i++) { + if (s_chan_bind[i].in_use && s_chan_bind[i].lcid == lcid) { + if (out_bda != NULL) { + memcpy(out_bda, s_chan_bind[i].bda, BD_ADDR_LEN); + } + memset(&s_chan_bind[i], 0, sizeof(s_chan_bind[i])); + return true; + } + } + return false; +} + +/* Whether the shadow table still holds another channel to this peer. */ +static bool btc_ble_l2cap_chan_has_other(BD_ADDR bda, uint16_t except_lcid) +{ + int i; + + for (i = 0; i < BLE_MAX_L2CAP_CLIENTS; i++) { + if (s_chan_bind[i].in_use && s_chan_bind[i].lcid != except_lcid && + memcmp(s_chan_bind[i].bda, bda, BD_ADDR_LEN) == 0) { + return true; + } + } + return false; +} + +static BOOLEAN btc_ble_l2cap_has_other_active_coc(BD_ADDR bda, UINT16 except_lcid) +{ + list_node_t *p_node; + tL2C_LCB *p_lcb; + tL2C_CCB *p_ccb; + + for (p_node = list_begin(l2cb.p_lcb_pool); p_node; p_node = list_next(p_node)) { + p_lcb = list_node(p_node); + /* The pool holds released LCBs too (in_use == FALSE, stale + * remote_bd_addr); skip them and non-LE links to match the established + * l2cu_find_lcb_by_bd_addr pattern. */ + if (!p_lcb->in_use || p_lcb->transport != BT_TRANSPORT_LE) { + continue; + } + if (memcmp(p_lcb->remote_bd_addr, bda, BD_ADDR_LEN) != 0) { + continue; + } + for (p_ccb = p_lcb->ccb_queue.p_first_ccb; p_ccb; p_ccb = p_ccb->p_next_ccb) { + if (p_ccb->le_coc_active && p_ccb->local_cid != except_lcid) { + return TRUE; + } + } + } + return FALSE; +} + +static uint16_t btc_ble_l2cap_conn_id_from_bda(BD_ADDR bda) +{ + int i; + UINT16 conn_id = 0; + + for (i = 0; i < BLE_MAX_L2CAP_CLIENTS; i++) { + if (s_conn_bind[i].in_use && memcmp(s_conn_bind[i].bda, bda, BD_ADDR_LEN) == 0) { + return s_conn_bind[i].conn_id; + } + } +#if (GATTC_INCLUDED == TRUE) + tGATT_REG *p_reg; + + for (i = 0, p_reg = gatt_cb.cl_rcb; i < GATT_MAX_APPS; i++, p_reg++) { + if (p_reg->in_use && + GATT_GetConnIdIfConnected(p_reg->gatt_if, bda, &conn_id, BT_TRANSPORT_LE)) { + return btc_ble_l2cap_app_conn_id_from_stack(conn_id); + } + } +#endif +#if (GATTS_INCLUDED == TRUE) + tGATT_SR_REG *p_sr_reg; + + for (i = 0, p_sr_reg = gatt_cb.sr_reg; i < GATT_MAX_SR_PROFILES; i++, p_sr_reg++) { + if (p_sr_reg->in_use && + GATT_GetConnIdIfConnected(p_sr_reg->gatt_if, bda, &conn_id, BT_TRANSPORT_LE)) { + return btc_ble_l2cap_app_conn_id_from_stack(conn_id); + } + } +#endif + return 0; +} + +static bool btc_ble_l2cap_bda_from_conn_id(uint16_t conn_id, BD_ADDR bda) +{ + int i; + tGATT_TCB *p_tcb; + + for (i = 0; i < BLE_MAX_L2CAP_CLIENTS; i++) { + if (s_conn_bind[i].in_use && s_conn_bind[i].conn_id == conn_id) { + memcpy(bda, s_conn_bind[i].bda, BD_ADDR_LEN); + return true; + } + } + + p_tcb = gatt_get_tcb_by_idx((UINT8)conn_id); + if (p_tcb != NULL && gatt_get_ch_state(p_tcb) >= GATT_CH_OPEN) { + memcpy(bda, p_tcb->peer_bda, BD_ADDR_LEN); + return true; + } + return false; +} + +static void btc_ble_l2cap_fill_chan_info_from_ccb(uint16_t chan_handle, + esp_ble_l2cap_chan_info_t *info) +{ + tL2C_CCB *p_ccb; + + if (info == NULL) { + return; + } + + memset(info, 0, sizeof(*info)); + p_ccb = l2cu_find_ccb_by_cid(NULL, chan_handle); + if (p_ccb == NULL || !p_ccb->le_coc_active) { + return; + } + + info->scid = p_ccb->local_cid; + info->dcid = p_ccb->remote_cid; + info->psm = p_ccb->p_rcb ? p_ccb->p_rcb->real_psm : 0; + info->our_mtu = p_ccb->local_conn_cfg.mtu; + info->peer_mtu = p_ccb->peer_conn_cfg.mtu; + info->our_mps = p_ccb->local_conn_cfg.mps; + info->peer_mps = p_ccb->peer_conn_cfg.mps; +} + +#if (BLE_L2CAP_COC_SERVER_INCLUDED == TRUE) +static void btc_ble_l2cap_connect_ind(BD_ADDR bd_addr, UINT16 lcid, UINT16 psm, UINT8 id) +{ + esp_ble_l2cap_cb_param_t param = {0}; + + param.coc_accept.conn_id = btc_ble_l2cap_conn_id_from_bda(bd_addr); + param.coc_accept.chan_handle = lcid; + param.coc_accept.l2cap_id = id; + param.coc_accept.psm = psm; + + BTC_BLE_L2CAP_TRACE_API("ConnectInd conn_id=%u lcid=0x%04x psm=0x%04x id=%u", + param.coc_accept.conn_id, lcid, psm, id); + btc_ble_l2cap_bind_conn(param.coc_accept.conn_id, bd_addr); + btc_ble_l2cap_post_event(ESP_BLE_L2CAP_COC_ACCEPT_EVT, ¶m); +} +#endif /* BLE_L2CAP_COC_SERVER_INCLUDED */ + +static void btc_ble_l2cap_connect_cfm(UINT16 lcid, UINT16 result) +{ + esp_ble_l2cap_cb_param_t param = {0}; + tL2C_CCB *p_ccb = l2cu_find_ccb_by_cid(NULL, lcid); + + param.coc_connected.chan_handle = lcid; + param.coc_connected.status = result; + if (p_ccb && p_ccb->p_lcb) { + param.coc_connected.conn_id = btc_ble_l2cap_conn_id_from_bda(p_ccb->p_lcb->remote_bd_addr); + } + + BTC_BLE_L2CAP_TRACE_API("ConnectCfm conn_id=%u lcid=0x%04x status=%u", + param.coc_connected.conn_id, lcid, result); + if (result == L2CAP_CONN_OK) { + btc_ble_l2cap_fill_chan_info_from_ccb(lcid, ¶m.coc_connected.chan_info); + /* Shadow the lcid->bda mapping so disconnect_ind can still resolve the + * peer address (and release the conn-bind slot) even if the CCB is freed + * before the DisconnectInd callback fires. */ + if (p_ccb && p_ccb->p_lcb) { + btc_ble_l2cap_track_chan(lcid, p_ccb->p_lcb->remote_bd_addr); + } + } else if (p_ccb && p_ccb->p_lcb && + !btc_ble_l2cap_has_other_active_coc(p_ccb->p_lcb->remote_bd_addr, lcid) && + !btc_ble_l2cap_chan_has_other(p_ccb->p_lcb->remote_bd_addr, lcid)) { + /* A failed connection is torn down via l2cu_release_ccb without a + * DisconnectInd callback, so the s_conn_bind entry created at connect + * time would leak. Release it here, mirroring disconnect_ind (which also + * consults the shadow table so we do not unbind while another channel to + * this peer is still tracked with a pending DisconnectInd). */ + btc_ble_l2cap_unbind_bda(p_ccb->p_lcb->remote_bd_addr); + } + btc_ble_l2cap_post_event(ESP_BLE_L2CAP_COC_CONNECTED_EVT, ¶m); +} + +#if (BLE_L2CAP_COC_CLIENT_INCLUDED == TRUE) +/* A connect/connect_ecoc request can be rejected synchronously by the stack + * (e.g. PSM registration or CCB allocation failure) with no CCB created, so + * neither ConnectCfm nor DisconnectInd will ever fire. Report the failure to the + * application (so it does not wait forever) and release the conn-bind slot + * created at request time. */ +static void btc_ble_l2cap_report_connect_fail(uint16_t conn_id, BD_ADDR bda, uint16_t status) +{ + esp_ble_l2cap_cb_param_t param = {0}; + + /* Consult both the live stack and the lcid->bda shadow before releasing the + * conn-bind slot, mirroring disconnect_ind. Another channel to this peer may + * have already released its CCB (so has_other_active_coc misses it) while its + * DisconnectInd is still pending in the shadow; unbinding on has_other_active_coc + * alone would drop the slot prematurely. */ + if (!btc_ble_l2cap_has_other_active_coc(bda, 0) && + !btc_ble_l2cap_chan_has_other(bda, 0)) { + btc_ble_l2cap_unbind_bda(bda); + } + param.coc_connected.conn_id = conn_id; + param.coc_connected.chan_handle = 0; + param.coc_connected.status = status; + BTC_BLE_L2CAP_TRACE_API("connect fail conn_id=%u status=%u", conn_id, status); + btc_ble_l2cap_post_event(ESP_BLE_L2CAP_COC_CONNECTED_EVT, ¶m); +} +#endif /* BLE_L2CAP_COC_CLIENT_INCLUDED */ + +static void btc_ble_l2cap_disconnect_ind(UINT16 lcid, BOOLEAN local_init) +{ + esp_ble_l2cap_cb_param_t param = {0}; + tL2C_CCB *p_ccb = l2cu_find_ccb_by_cid(NULL, lcid); + BD_ADDR bda; + bool have_bda = false; + + UNUSED(local_init); + + /* Prefer the live CCB's address; fall back to the lcid->bda shadow because + * some teardown paths release the CCB before invoking this callback, in + * which case l2cu_find_ccb_by_cid() returns NULL and the address would + * otherwise be lost, leaking the conn-bind slot. */ + if (p_ccb != NULL && p_ccb->p_lcb != NULL) { + memcpy(bda, p_ccb->p_lcb->remote_bd_addr, BD_ADDR_LEN); + have_bda = true; + btc_ble_l2cap_untrack_chan(lcid, NULL); + } else if (btc_ble_l2cap_untrack_chan(lcid, bda)) { + have_bda = true; + } + + /* Release the conn-bind slot once the last CoC channel to this peer is gone. + * Consult both the live stack (other CCBs) and the shadow table so we do not + * unbind while another channel to the same peer is still up. */ + if (have_bda && + !btc_ble_l2cap_has_other_active_coc(bda, lcid) && + !btc_ble_l2cap_chan_has_other(bda, lcid)) { + btc_ble_l2cap_unbind_bda(bda); + } + + param.coc_disconnected.chan_handle = lcid; + BTC_BLE_L2CAP_TRACE_API("DisconnectInd lcid=0x%04x", lcid); + btc_ble_l2cap_post_event(ESP_BLE_L2CAP_COC_DISCONNECTED_EVT, ¶m); +} + +static void btc_ble_l2cap_data_ind(UINT16 lcid, BT_HDR *p_buf) +{ + esp_ble_l2cap_cb_param_t param = {0}; + + if (p_buf == NULL) { + BTC_BLE_L2CAP_TRACE_ERROR("data_ind NULL buffer lcid=0x%04x", lcid); + return; + } + + param.data_received.chan_handle = lcid; + param.data_received.len = p_buf->len; + param.data_received.data = (UINT8 *)(p_buf + 1) + p_buf->offset; + + BTC_BLE_L2CAP_TRACE_DEBUG("DataInd lcid=0x%04x len=%u", lcid, p_buf->len); + /* p_buf ownership transfers to btc_ble_l2cap_cb_deep_copy() */ + btc_ble_l2cap_post_event(ESP_BLE_L2CAP_COC_DATA_RECEIVED_EVT, ¶m); +} + +static void btc_ble_l2cap_congestion(UINT16 lcid, BOOLEAN congested) +{ + esp_ble_l2cap_cb_param_t param = {0}; + + if (congested) { + return; + } + + param.tx_unstalled.chan_handle = lcid; + BTC_BLE_L2CAP_TRACE_DEBUG("TxUnstalled lcid=0x%04x", lcid); + btc_ble_l2cap_post_event(ESP_BLE_L2CAP_COC_TX_UNSTALLED_EVT, ¶m); +} + +#if (BLE_L2CAP_ENHANCED_COC_INCLUDED == TRUE) +static void btc_ble_l2cap_reconfig_ind(UINT16 lcid, UINT16 result, BOOLEAN peer_initiated) +{ + esp_ble_l2cap_cb_param_t param = {0}; + + if (peer_initiated) { + param.peer_reconfigured.chan_handle = lcid; + param.peer_reconfigured.status = result; + BTC_BLE_L2CAP_TRACE_API("PeerReconfig lcid=0x%04x status=%u", lcid, result); + if (result == L2CAP_LE_RECONFIG_OK) { + btc_ble_l2cap_fill_chan_info_from_ccb(lcid, ¶m.peer_reconfigured.chan_info); + BTC_BLE_L2CAP_TRACE_API("PeerReconfig mtu=%u mps=%u", + param.peer_reconfigured.chan_info.peer_mtu, + param.peer_reconfigured.chan_info.peer_mps); + } + btc_ble_l2cap_post_event(ESP_BLE_L2CAP_COC_PEER_RECONFIGURED_EVT, ¶m); + } else { + param.reconfig_completed.chan_handle = lcid; + param.reconfig_completed.status = result; + BTC_BLE_L2CAP_TRACE_API("ReconfigCompleted lcid=0x%04x status=%u", lcid, result); + if (result == L2CAP_LE_RECONFIG_OK) { + btc_ble_l2cap_fill_chan_info_from_ccb(lcid, ¶m.reconfig_completed.chan_info); + BTC_BLE_L2CAP_TRACE_API("ReconfigCompleted mtu=%u/%u mps=%u/%u", + param.reconfig_completed.chan_info.our_mtu, + param.reconfig_completed.chan_info.peer_mtu, + param.reconfig_completed.chan_info.our_mps, + param.reconfig_completed.chan_info.peer_mps); + } + btc_ble_l2cap_post_event(ESP_BLE_L2CAP_COC_RECONFIG_COMPLETED_EVT, ¶m); + } +} +#endif + +static void btc_ble_l2cap_post_event(esp_ble_l2cap_evt_t event, esp_ble_l2cap_cb_param_t *param) +{ + btc_msg_t msg = {0}; + bt_status_t ret; + + msg.sig = BTC_SIG_API_CB; + msg.pid = BTC_PID_BLE_L2CAP; + msg.act = event; + + ret = btc_transfer_context(&msg, param, sizeof(esp_ble_l2cap_cb_param_t), + btc_ble_l2cap_cb_deep_copy, btc_ble_l2cap_cb_deep_free); + if (ret != BT_STATUS_SUCCESS) { + BTC_BLE_L2CAP_TRACE_ERROR("btc_transfer_context failed evt=%d", event); + /* Free the original RX BT_HDR only if the deep-copy callback did not + * already free it. When btc_transfer_context fails after running the + * deep copy, that callback clears param->data_received.data to NULL, so + * the check below skips the free and avoids a double-free. If it failed + * before the deep copy (e.g. message alloc failure), the pointer is + * still valid and we free it here to avoid a leak (LE CoC delivers SDUs + * with offset 0, so the BT_HDR header sits right before data). */ + if (event == ESP_BLE_L2CAP_COC_DATA_RECEIVED_EVT && param->data_received.data != NULL) { + osi_free((UINT8 *)param->data_received.data - sizeof(BT_HDR)); + } + } +} + +#if (BLE_L2CAP_COC_SERVER_INCLUDED == TRUE) +static btc_ble_l2cap_server_t *btc_ble_l2cap_find_server_by_real_psm(uint16_t psm) +{ + for (int i = 0; i < BLE_MAX_L2CAP_CLIENTS; i++) { + if (s_l2cap_env.servers[i].in_use && s_l2cap_env.servers[i].real_psm == psm) { + return &s_l2cap_env.servers[i]; + } + } + return NULL; +} + +static btc_ble_l2cap_server_t *btc_ble_l2cap_alloc_server(uint16_t psm, uint16_t mtu) +{ + for (int i = 0; i < BLE_MAX_L2CAP_CLIENTS; i++) { + if (!s_l2cap_env.servers[i].in_use) { + s_l2cap_env.servers[i].in_use = true; + s_l2cap_env.servers[i].real_psm = psm; + s_l2cap_env.servers[i].mtu = mtu; + s_l2cap_env.servers[i].reg_psm = 0; + return &s_l2cap_env.servers[i]; + } + } + return NULL; +} +#endif /* BLE_L2CAP_COC_SERVER_INCLUDED */ + +static void btc_ble_l2cap_register_psm_security(uint16_t reg_psm) +{ + BTM_SetSecurityLevel(TRUE, "BLE_L2CAP_COC", BTM_SEC_SERVICE_GEN_NET, + BTM_SEC_NONE, reg_psm, BTM_SEC_PROTO_L2CAP, 0); + BTM_SetSecurityLevel(FALSE, "BLE_L2CAP_COC", BTM_SEC_SERVICE_GEN_NET, + BTM_SEC_NONE, reg_psm, BTM_SEC_PROTO_L2CAP, 0); +} + +#if (BLE_L2CAP_COC_CLIENT_INCLUDED == TRUE) +static bool btc_ble_l2cap_track_client_psm(uint16_t reg_psm) +{ + int free_slot = -1; + for (int i = 0; i < BLE_MAX_L2CAP_CLIENTS; i++) { + if (s_l2cap_env.client_reg_psms[i] == reg_psm) { + return true; /* already tracked */ + } + if (free_slot < 0 && s_l2cap_env.client_reg_psms[i] == 0) { + free_slot = i; + } + } + if (free_slot >= 0) { + s_l2cap_env.client_reg_psms[free_slot] = reg_psm; + return true; + } + /* Table full: report it (mirrors btc_ble_l2cap_track_chan/bind_conn) and let + * the caller deregister the PSM. A silently untracked PSM would never be + * deregistered at DEINIT, permanently leaking a BLE RCB pool entry. */ + BTC_BLE_L2CAP_TRACE_ERROR("%s client PSM track table full, psm=0x%04x", __func__, reg_psm); + return false; +} + +static void btc_ble_l2cap_untrack_client_psm(uint16_t reg_psm) +{ + for (int i = 0; i < BLE_MAX_L2CAP_CLIENTS; i++) { + if (s_l2cap_env.client_reg_psms[i] == reg_psm) { + s_l2cap_env.client_reg_psms[i] = 0; + return; + } + } +} + +/* newly_registered (optional out): set TRUE only when this call created a fresh + * L2CAP registration, so a failed connect attempt can deregister its own PSM + * without tearing down a registration shared with a server or a prior client + * connection to the same PSM. */ +static uint16_t btc_ble_l2cap_get_or_register_psm(uint16_t real_psm, bool *newly_registered) +{ + UINT16 reg_psm; + tL2C_RCB *p_rcb; + + if (newly_registered != NULL) { + *newly_registered = false; + } + +#if (BLE_L2CAP_COC_SERVER_INCLUDED == TRUE) + btc_ble_l2cap_server_t *srv = btc_ble_l2cap_find_server_by_real_psm(real_psm); + + if (srv != NULL && srv->reg_psm != 0) { + btc_ble_l2cap_register_psm_security(srv->reg_psm); + return srv->reg_psm; + } +#endif + + p_rcb = l2cu_find_ble_rcb_by_real_psm(real_psm); + if (p_rcb != NULL) { + btc_ble_l2cap_register_psm_security(p_rcb->psm); + return p_rcb->psm; + } + + reg_psm = L2CA_RegisterLECoc(real_psm, &s_l2cap_appl); + if (reg_psm != 0) { + /* Remember it so DEINIT can deregister this client-created PSM. If the + * tracking table is full, roll back the registration right away instead + * of leaving a PSM that DEINIT can never find and deregister (RCB leak). + * If the connect attempt later fails the caller deregisters + untracks. */ + if (!btc_ble_l2cap_track_client_psm(reg_psm)) { + L2CA_DeregisterLECoc(reg_psm); + return 0; + } + btc_ble_l2cap_register_psm_security(reg_psm); + if (newly_registered != NULL) { + *newly_registered = true; + } + } + return reg_psm; +} +#endif /* BLE_L2CAP_COC_CLIENT_INCLUDED */ + +static void btc_ble_l2cap_register_appl_cb(void) +{ + memset(&s_l2cap_appl, 0, sizeof(s_l2cap_appl)); +#if (BLE_L2CAP_COC_SERVER_INCLUDED == TRUE) + s_l2cap_appl.pL2CA_ConnectInd_Cb = btc_ble_l2cap_connect_ind; +#endif + s_l2cap_appl.pL2CA_ConnectCfm_Cb = btc_ble_l2cap_connect_cfm; + s_l2cap_appl.pL2CA_DisconnectInd_Cb = btc_ble_l2cap_disconnect_ind; + s_l2cap_appl.pL2CA_DataInd_Cb = btc_ble_l2cap_data_ind; + s_l2cap_appl.pL2CA_CongestionStatus_Cb = btc_ble_l2cap_congestion; +#if (BLE_L2CAP_ENHANCED_COC_INCLUDED == TRUE) + s_l2cap_appl.pL2CA_LeReconfigInd_Cb = btc_ble_l2cap_reconfig_ind; +#endif +} + +void btc_ble_l2cap_cb_deep_copy(btc_msg_t *msg, void *p_dest, void *p_src) +{ + esp_ble_l2cap_cb_param_t *dst = (esp_ble_l2cap_cb_param_t *)p_dest; + esp_ble_l2cap_cb_param_t *src = (esp_ble_l2cap_cb_param_t *)p_src; + + if (!dst || !src) { + return; + } + + memcpy(dst, src, sizeof(esp_ble_l2cap_cb_param_t)); + + /* Only the DATA_RECEIVED event carries a heap pointer that needs a deep copy. + * Do NOT clear dst->data_received.data for other events: the param is a union, + * so writing data_received.data would clobber overlapping scalar fields of + * other events (e.g. coc_accept.l2cap_id / psm). */ + if (msg->act == ESP_BLE_L2CAP_COC_DATA_RECEIVED_EVT && src->data_received.data != NULL) { + BT_HDR *p_rx; + + dst->data_received.data = NULL; + + /* LE CoC delivers SDUs with offset 0 (see l2c_ble_le_coc_data_ind). */ + p_rx = (BT_HDR *)((UINT8 *)src->data_received.data - sizeof(BT_HDR)); + + if (src->data_received.len > 0) { + dst->data_received.data = (uint8_t *)osi_malloc(src->data_received.len); + if (dst->data_received.data) { + memcpy(dst->data_received.data, src->data_received.data, src->data_received.len); + } else { + /* Deep-copy OOM: never hand the app a non-zero len with a NULL + * pointer, or it will dereference NULL. Report an empty SDU. */ + dst->data_received.len = 0; + BTC_BLE_L2CAP_TRACE_ERROR("rx deep_copy malloc failed len=%u", + src->data_received.len); + } + } + osi_free(p_rx); + /* The original RX BT_HDR is now freed. Clear the source pointer (safe: + * this is the DATA_RECEIVED union member) so the caller's error path in + * btc_ble_l2cap_post_event sees NULL and does not free it a second time + * when btc_task_post fails after this deep-copy already ran. */ + src->data_received.data = NULL; + } +} + +void btc_ble_l2cap_cb_deep_free(btc_msg_t *msg) +{ + esp_ble_l2cap_cb_param_t *param = (esp_ble_l2cap_cb_param_t *)msg->arg; + + if (param && msg->act == ESP_BLE_L2CAP_COC_DATA_RECEIVED_EVT && param->data_received.data) { + osi_free(param->data_received.data); + param->data_received.data = NULL; + } +} + +void btc_ble_l2cap_arg_deep_copy(btc_msg_t *msg, void *p_dest, void *p_src) +{ + btc_ble_l2cap_args_t *dst = (btc_ble_l2cap_args_t *)p_dest; + btc_ble_l2cap_args_t *src = (btc_ble_l2cap_args_t *)p_src; + + if (!dst || !src) { + return; + } + + memcpy(dst, src, sizeof(btc_ble_l2cap_args_t)); + + /* Only the SEND act carries a heap pointer that needs a deep copy. Do NOT + * clear dst->send.data for other acts: send/connect_ecoc/etc. share the same + * union, so writing send.data would clobber overlapping scalar fields (e.g. + * connect_ecoc.mtu / num_chan). */ + if (msg->act == BTC_BLE_L2CAP_ACT_SEND) { + dst->send.data = NULL; + if (src->send.len > 0 && src->send.data) { + BT_HDR *p_buf = (BT_HDR *)osi_malloc(sizeof(BT_HDR) + src->send.len); + if (p_buf) { + p_buf->offset = 0; + p_buf->len = src->send.len; + p_buf->event = 0; + p_buf->layer_specific = 0; + memcpy((UINT8 *)(p_buf + 1), src->send.data, src->send.len); + dst->send.data = (uint8_t *)p_buf; + } else { + BTC_BLE_L2CAP_TRACE_ERROR("deep_copy malloc failed act=%d len=%u", msg->act, src->send.len); + } + } + } +} + +void btc_ble_l2cap_arg_deep_free(btc_msg_t *msg) +{ + btc_ble_l2cap_args_t *arg = (btc_ble_l2cap_args_t *)msg->arg; + + if (arg && msg->act == BTC_BLE_L2CAP_ACT_SEND && arg->send.data) { + osi_free(arg->send.data); + arg->send.data = NULL; + } +} + +static void btc_ble_l2cap_cb_to_app(esp_ble_l2cap_evt_t event, esp_ble_l2cap_cb_param_t *param) +{ + esp_ble_l2cap_cb_t cb = (esp_ble_l2cap_cb_t)btc_profile_cb_get(BTC_PID_BLE_L2CAP); + if (cb) { + cb(event, param); + } +} + +void btc_ble_l2cap_cb_handler(btc_msg_t *msg) +{ + esp_ble_l2cap_cb_param_t *param = (esp_ble_l2cap_cb_param_t *)msg->arg; + + if (msg->act < ESP_BLE_L2CAP_COC_EVT_MAX) { + btc_ble_l2cap_cb_to_app((esp_ble_l2cap_evt_t)msg->act, param); + } else { + BTC_BLE_L2CAP_TRACE_ERROR("cb_handler invalid event act=%d", msg->act); + } + btc_ble_l2cap_cb_deep_free(msg); +} + +void btc_ble_l2cap_call_handler(btc_msg_t *msg) +{ + btc_ble_l2cap_args_t *arg = (btc_ble_l2cap_args_t *)msg->arg; + + BTC_BLE_L2CAP_TRACE_DEBUG("%s act=%d", __func__, msg->act); + + switch (msg->act) { + case BTC_BLE_L2CAP_ACT_INIT: + BTC_BLE_L2CAP_TRACE_DEBUG("INIT"); + /* Mirror DEINIT: on a re-init without a prior DEINIT, deregister any PSMs + * this module still tracks before wiping s_l2cap_env. Otherwise the + * corresponding tL2C_RCB entries leak from the limited BLE RCB pool and + * eventually make new L2CA_RegisterLECoc calls fail. */ + if (s_l2cap_env.initialized) { +#if (BLE_L2CAP_COC_SERVER_INCLUDED == TRUE) + for (int i = 0; i < BLE_MAX_L2CAP_CLIENTS; i++) { + if (s_l2cap_env.servers[i].in_use && s_l2cap_env.servers[i].reg_psm) { + L2CA_DeregisterLECoc(s_l2cap_env.servers[i].reg_psm); + } + } +#endif +#if (BLE_L2CAP_COC_CLIENT_INCLUDED == TRUE) + for (int i = 0; i < BLE_MAX_L2CAP_CLIENTS; i++) { + if (s_l2cap_env.client_reg_psms[i] != 0) { + L2CA_DeregisterLECoc(s_l2cap_env.client_reg_psms[i]); + } + } +#endif + } + memset(&s_l2cap_env, 0, sizeof(s_l2cap_env)); + memset(s_conn_bind, 0, sizeof(s_conn_bind)); + /* Mirror DEINIT: clear the channel-bind shadow table so a re-init without + * a prior DEINIT does not inherit stale entries, which would make + * btc_ble_l2cap_chan_has_other() wrongly hold conn-bind slots. */ + memset(s_chan_bind, 0, sizeof(s_chan_bind)); + s_l2cap_env.initialized = true; + btc_ble_l2cap_register_appl_cb(); + break; + + case BTC_BLE_L2CAP_ACT_DEINIT: + BTC_BLE_L2CAP_TRACE_DEBUG("DEINIT"); +#if (BLE_L2CAP_COC_SERVER_INCLUDED == TRUE) + for (int i = 0; i < BLE_MAX_L2CAP_CLIENTS; i++) { + if (s_l2cap_env.servers[i].in_use && s_l2cap_env.servers[i].reg_psm) { + BTC_BLE_L2CAP_TRACE_DEBUG("DEINIT deregister server reg_psm=0x%04x", + s_l2cap_env.servers[i].reg_psm); + L2CA_DeregisterLECoc(s_l2cap_env.servers[i].reg_psm); + } + } +#endif +#if (BLE_L2CAP_COC_CLIENT_INCLUDED == TRUE) + /* Deregister PSMs this module registered for outgoing connections that + * had no server slot, otherwise they leak BLE RCB pool entries across + * an init/deinit cycle. */ + for (int i = 0; i < BLE_MAX_L2CAP_CLIENTS; i++) { + if (s_l2cap_env.client_reg_psms[i] != 0) { + BTC_BLE_L2CAP_TRACE_DEBUG("DEINIT deregister client reg_psm=0x%04x", + s_l2cap_env.client_reg_psms[i]); + L2CA_DeregisterLECoc(s_l2cap_env.client_reg_psms[i]); + } + } +#endif + memset(&s_l2cap_env, 0, sizeof(s_l2cap_env)); + memset(s_conn_bind, 0, sizeof(s_conn_bind)); + memset(s_chan_bind, 0, sizeof(s_chan_bind)); + break; + +#if (BLE_L2CAP_COC_SERVER_INCLUDED == TRUE) + case BTC_BLE_L2CAP_ACT_CREATE_SERVER: { + btc_ble_l2cap_server_t *srv = btc_ble_l2cap_find_server_by_real_psm(arg->create_server.psm); + UINT16 reg_psm; + if (srv != NULL) { + /* Already registered for this PSM: refresh MTU instead of allocating + * another slot. A second L2CA_RegisterLECoc would overwrite the RCB + * and leak the previous slot. */ + srv->mtu = arg->create_server.mtu; + BTC_BLE_L2CAP_TRACE_API("create_server psm=0x%04x already registered", srv->real_psm); + break; + } + srv = btc_ble_l2cap_alloc_server(arg->create_server.psm, arg->create_server.mtu); + if (srv == NULL) { + BTC_BLE_L2CAP_TRACE_ERROR("no server slot psm=0x%04x", arg->create_server.psm); + break; + } + reg_psm = L2CA_RegisterLECoc(arg->create_server.psm, &s_l2cap_appl); + if (reg_psm == 0) { + srv->in_use = false; + BTC_BLE_L2CAP_TRACE_ERROR("RegisterLECoc failed psm=0x%04x", arg->create_server.psm); + break; + } + srv->reg_psm = reg_psm; + btc_ble_l2cap_register_psm_security(reg_psm); + BTC_BLE_L2CAP_TRACE_API("create_server real_psm=0x%04x reg_psm=0x%04x mtu=%u", + srv->real_psm, srv->reg_psm, srv->mtu); + break; + } + + case BTC_BLE_L2CAP_ACT_DELETE_SERVER: { + btc_ble_l2cap_server_t *srv = btc_ble_l2cap_find_server_by_real_psm(arg->delete_server.psm); + if (srv && srv->reg_psm) { + L2CA_DeregisterLECoc(srv->reg_psm); + memset(srv, 0, sizeof(*srv)); + BTC_BLE_L2CAP_TRACE_DEBUG("delete_server psm=0x%04x", arg->delete_server.psm); + } else { + BTC_BLE_L2CAP_TRACE_ERROR("delete_server psm=0x%04x not found", arg->delete_server.psm); + } + break; + } +#endif /* BLE_L2CAP_COC_SERVER_INCLUDED */ + +#if (BLE_L2CAP_COC_CLIENT_INCLUDED == TRUE) + case BTC_BLE_L2CAP_ACT_CONNECT: { + BD_ADDR bda; + tL2CAP_LE_CFG_INFO cfg = {0}; + UINT16 reg_psm; + UINT16 lcid; + + memset(bda, 0, BD_ADDR_LEN); + if (!btc_ble_l2cap_bda_from_conn_id(arg->connect.conn_id, bda)) { + BTC_BLE_L2CAP_TRACE_ERROR("invalid conn_id=%u", arg->connect.conn_id); + btc_ble_l2cap_report_connect_fail(arg->connect.conn_id, bda, L2CAP_CONN_NO_LINK); + break; + } + btc_ble_l2cap_bind_conn(arg->connect.conn_id, bda); + cfg.mtu = arg->connect.mtu; + bool newly_registered = false; + reg_psm = btc_ble_l2cap_get_or_register_psm(arg->connect.psm, &newly_registered); + if (reg_psm == 0) { + BTC_BLE_L2CAP_TRACE_ERROR("client RegisterLECoc failed psm=0x%04x", arg->connect.psm); + btc_ble_l2cap_report_connect_fail(arg->connect.conn_id, bda, L2CAP_CONN_NO_RESOURCES); + break; + } + lcid = L2CA_ConnectLECocReq(reg_psm, bda, &cfg); + if (lcid == 0) { + BTC_BLE_L2CAP_TRACE_ERROR("ConnectLECocReq failed conn_id=%u psm=0x%04x", + arg->connect.conn_id, arg->connect.psm); + /* Only deregister a PSM this attempt registered. A shared PSM (server + * or a prior client connection) must survive, or deregistering would + * tear down other active channels using the same RCB. */ + if (newly_registered && l2cu_find_ble_rcb_by_psm(reg_psm) != NULL) { + L2CA_DeregisterLECoc(reg_psm); + btc_ble_l2cap_untrack_client_psm(reg_psm); + } + /* No CCB was created, so ConnectCfm will never fire: report the + * failure and drop the conn-bind slot instead of leaking it. */ + btc_ble_l2cap_report_connect_fail(arg->connect.conn_id, bda, L2CAP_CONN_NO_RESOURCES); + break; + } + BTC_BLE_L2CAP_TRACE_API("connect conn_id=%u lcid=0x%04x", arg->connect.conn_id, lcid); + break; + } +#endif /* BLE_L2CAP_COC_CLIENT_INCLUDED */ + +#if (BLE_L2CAP_COC_SERVER_INCLUDED == TRUE) + case BTC_BLE_L2CAP_ACT_ACCEPT: { + BD_ADDR bda; + tL2CAP_LE_CFG_INFO cfg = {0}; + UINT16 result = arg->accept.accept ? L2CAP_CONN_OK : L2CAP_CONN_NO_PSM; + tL2C_CCB *p_ccb = l2cu_find_ccb_by_cid(NULL, arg->accept.chan_handle); + + /* Resolve the peer address from the (always unique) channel handle + * instead of conn_id. For GATT-less LE CoC peers conn_id is 0 for every + * such peer, so btc_ble_l2cap_bda_from_conn_id could return the wrong + * address and make L2CA_ConnectLECocRsp fail. Fall back to the conn_id + * lookup only if the CCB is no longer available. */ + if (p_ccb != NULL && p_ccb->p_lcb != NULL) { + memcpy(bda, p_ccb->p_lcb->remote_bd_addr, BD_ADDR_LEN); + } else if (!btc_ble_l2cap_bda_from_conn_id(arg->accept.conn_id, bda)) { + BTC_BLE_L2CAP_TRACE_ERROR("accept invalid conn_id=%u lcid=0x%04x", + arg->accept.conn_id, arg->accept.chan_handle); + /* Link torn down between ACCEPT_EVT and the app response: neither the + * CCB nor conn_id resolves, so L2CA_ConnectLECocRsp cannot run and no + * ConnectCfm will follow. For an accept, notify the app of the failure + * so its pending esp_ble_l2cap_accept() does not hang forever (mirrors + * the accept-fail path below and the CONNECT handler). No conn-bind was + * created yet, so nothing to unbind. Reject needs no event: the reject + * path never posts CONNECTED_EVT. */ + if (arg->accept.accept) { + esp_ble_l2cap_cb_param_t param = {0}; + param.coc_connected.conn_id = arg->accept.conn_id; + param.coc_connected.chan_handle = 0; + param.coc_connected.status = L2CAP_CONN_NO_LINK; + btc_ble_l2cap_post_event(ESP_BLE_L2CAP_COC_CONNECTED_EVT, ¶m); + } + break; + } + btc_ble_l2cap_bind_conn(arg->accept.conn_id, bda); + cfg.mtu = arg->accept.mtu; + if (!L2CA_ConnectLECocRsp(bda, arg->accept.l2cap_id, arg->accept.chan_handle, + result, 0, &cfg)) { + BTC_BLE_L2CAP_TRACE_ERROR("ConnectLECocRsp failed lcid=0x%04x", arg->accept.chan_handle); + if (arg->accept.accept) { + /* Accept failed at the stack API: no CCB and thus no ConnectCfm/ + * DisconnectInd will follow. Notify the app of the failure and drop + * the conn-bind slot created above, mirroring the CONNECT failure + * path (btc_ble_l2cap_report_connect_fail, which is client-only, so + * the equivalent is inlined here). Consult the shadow table as the + * reject path does so we do not unbind while another channel to this + * peer is still tracked with a pending DisconnectInd. */ + esp_ble_l2cap_cb_param_t param = {0}; + param.coc_connected.conn_id = arg->accept.conn_id; + param.coc_connected.chan_handle = 0; + param.coc_connected.status = L2CAP_CONN_NO_RESOURCES; + btc_ble_l2cap_post_event(ESP_BLE_L2CAP_COC_CONNECTED_EVT, ¶m); + if (!btc_ble_l2cap_has_other_active_coc(bda, arg->accept.chan_handle) && + !btc_ble_l2cap_chan_has_other(bda, arg->accept.chan_handle)) { + btc_ble_l2cap_unbind_bda(bda); + } + break; + } + } + /* On reject the stack releases the CCB directly (no DisconnectInd + * callback), so release the binding entry here to avoid leaking a slot. + * Consult the shadow table too (as disconnect_ind does) so we do not + * unbind while another channel to this peer is still tracked with a + * pending DisconnectInd. */ + if (!arg->accept.accept && + !btc_ble_l2cap_has_other_active_coc(bda, arg->accept.chan_handle) && + !btc_ble_l2cap_chan_has_other(bda, arg->accept.chan_handle)) { + btc_ble_l2cap_unbind_bda(bda); + } + break; + } +#endif /* BLE_L2CAP_COC_SERVER_INCLUDED */ + + case BTC_BLE_L2CAP_ACT_DISCONNECT: + if (!L2CA_LECocDisconnect(arg->disconnect.chan_handle)) { + BTC_BLE_L2CAP_TRACE_ERROR("disconnect failed lcid=0x%04x", arg->disconnect.chan_handle); + } + break; + + case BTC_BLE_L2CAP_ACT_SEND: { + BT_HDR *p_buf; + + if (arg->send.len == 0 || arg->send.data == NULL) { + BTC_BLE_L2CAP_TRACE_ERROR("send invalid lcid=0x%04x len=%u", + arg->send.chan_handle, arg->send.len); + break; + } + p_buf = (BT_HDR *)arg->send.data; + arg->send.data = NULL; + if (L2CA_LECocIsCongested(arg->send.chan_handle)) { + BTC_BLE_L2CAP_TRACE_DEBUG("send dropped, congested lcid=0x%04x", arg->send.chan_handle); + osi_free(p_buf); + break; + } + if (L2CA_LECocDataWrite(arg->send.chan_handle, p_buf) == L2CAP_DW_FAILED) { + /* L2CA_LECocDataWrite() already released p_buf on every DW_FAILED + * path; freeing it here would be a double free. */ + BTC_BLE_L2CAP_TRACE_ERROR("send failed lcid=0x%04x", arg->send.chan_handle); + } + break; + } + + case BTC_BLE_L2CAP_ACT_RECV_READY: + if (!L2CA_LECocGiveCredits(arg->recv_ready.chan_handle, 1)) { + BTC_BLE_L2CAP_TRACE_ERROR("recv_ready failed lcid=0x%04x", arg->recv_ready.chan_handle); + } + break; + + case BTC_BLE_L2CAP_ACT_SET_AUTO_CREDIT: + if (!L2CA_LECocSetAutoCredit(arg->set_auto_credit.chan_handle, + arg->set_auto_credit.enable ? TRUE : FALSE)) { + BTC_BLE_L2CAP_TRACE_ERROR("set_auto_credit failed lcid=0x%04x", + arg->set_auto_credit.chan_handle); + } + break; + +#if (BLE_L2CAP_ENHANCED_COC_INCLUDED == TRUE) +#if (BLE_L2CAP_COC_CLIENT_INCLUDED == TRUE) + case BTC_BLE_L2CAP_ACT_CONNECT_ECOC: { + BD_ADDR bda; + tL2CAP_LE_CFG_INFO cfg = {0}; + UINT16 lcids[BLE_MAX_L2CAP_CLIENTS]; + UINT16 reg_psm; + UINT8 num_started; + + memset(bda, 0, BD_ADDR_LEN); + if (!btc_ble_l2cap_bda_from_conn_id(arg->connect_ecoc.conn_id, bda)) { + BTC_BLE_L2CAP_TRACE_ERROR("ecoc connect invalid conn_id=%u", arg->connect_ecoc.conn_id); + btc_ble_l2cap_report_connect_fail(arg->connect_ecoc.conn_id, bda, L2CAP_CONN_NO_LINK); + break; + } + cfg.mtu = arg->connect_ecoc.mtu; + btc_ble_l2cap_bind_conn(arg->connect_ecoc.conn_id, bda); + bool ecoc_newly_registered = false; + reg_psm = btc_ble_l2cap_get_or_register_psm(arg->connect_ecoc.psm, &ecoc_newly_registered); + if (reg_psm == 0) { + BTC_BLE_L2CAP_TRACE_ERROR("client RegisterLECoc failed psm=0x%04x", arg->connect_ecoc.psm); + btc_ble_l2cap_report_connect_fail(arg->connect_ecoc.conn_id, bda, L2CAP_CONN_NO_RESOURCES); + break; + } + num_started = L2CA_ConnectLEEcocReq(reg_psm, bda, &cfg, + arg->connect_ecoc.num_chan, lcids); + if (num_started == 0) { + BTC_BLE_L2CAP_TRACE_ERROR("ConnectLEEcocReq failed"); + /* Only deregister a PSM this attempt registered; a shared PSM must + * survive so other active channels are not torn down. */ + if (ecoc_newly_registered && l2cu_find_ble_rcb_by_psm(reg_psm) != NULL) { + L2CA_DeregisterLECoc(reg_psm); + btc_ble_l2cap_untrack_client_psm(reg_psm); + } + /* No CCB created: report failure and release the conn-bind slot. */ + btc_ble_l2cap_report_connect_fail(arg->connect_ecoc.conn_id, bda, L2CAP_CONN_NO_RESOURCES); + } else { + BTC_BLE_L2CAP_TRACE_API("ecoc connect started n=%u", num_started); + } + break; + } +#endif /* BLE_L2CAP_COC_CLIENT_INCLUDED */ + + case BTC_BLE_L2CAP_ACT_RECONFIG: + if (!L2CA_LEEcocReconfig(arg->reconfig.chan_handles, arg->reconfig.num_chan, + arg->reconfig.mtu, arg->reconfig.mps)) { + BTC_BLE_L2CAP_TRACE_ERROR("LEEcocReconfig failed"); + } + break; +#endif + + default: + BTC_BLE_L2CAP_TRACE_ERROR("unknown act=%d", msg->act); + break; + } + + btc_ble_l2cap_arg_deep_free(msg); +} + +#endif /* BLE_L2CAP_COC_INCLUDED == TRUE */ diff --git a/components/bt/host/bluedroid/btc/profile/std/gap/btc_gap_ble.c b/components/bt/host/bluedroid/btc/profile/std/gap/btc_gap_ble.c index 7e4a11e3898..1d1384daeba 100644 --- a/components/bt/host/bluedroid/btc/profile/std/gap/btc_gap_ble.c +++ b/components/bt/host/bluedroid/btc/profile/std/gap/btc_gap_ble.c @@ -23,6 +23,9 @@ #include "btc/btc_util.h" #include "osi/mutex.h" #include "osi/thread.h" +#if (BLE_EATT_INCLUDED == TRUE) +#include "gatt_eatt_int.h" +#endif #include "osi/pkt_queue.h" #if (BT_CONTROLLER_INCLUDED == TRUE) #include "esp_bt.h" @@ -2286,6 +2289,31 @@ static void btc_ble_set_privacy_mode(uint8_t addr_type, BTA_DmBleSetPrivacyMode(addr_type, addr, privacy_mode); } +#if (BLE_EATT_INCLUDED == TRUE) +void btc_ble_gap_eatt_evt_cback(UINT16 conn_id, UINT8 status, UINT16 cid) +{ + btc_msg_t msg = {0}; + esp_ble_gap_cb_param_t param = {0}; + bt_status_t ret; + + param.eatt_evt.conn_id = conn_id; + param.eatt_evt.status = status; + param.eatt_evt.cid = cid; + + msg.sig = BTC_SIG_API_CB; + msg.pid = BTC_PID_GAP_BLE; + msg.act = ESP_GAP_BLE_EATT_EVT; + + /* eatt_evt holds only scalars, so no deep copy/free is needed (matches the + * convention used by the other scalar-only GAP cb events in this file). */ + ret = btc_transfer_context(&msg, ¶m, sizeof(esp_ble_gap_cb_param_t), + NULL, NULL); + if (ret != BT_STATUS_SUCCESS) { + BTC_TRACE_ERROR("EATT evt transfer failed"); + } +} +#endif /* BLE_EATT_INCLUDED == TRUE */ + void btc_gap_ble_cb_handler(btc_msg_t *msg) { esp_ble_gap_cb_param_t *param = (esp_ble_gap_cb_param_t *)msg->arg; @@ -3096,6 +3124,13 @@ void btc_gap_ble_cb_deep_free(btc_msg_t *msg) } break; #endif // (BT_BLE_FEAT_CHANNEL_SOUNDING == TRUE) +#if (BLE_EATT_INCLUDED == TRUE) + case ESP_GAP_BLE_EATT_EVT: + /* Scalar-only event: nothing to free. Handled explicitly so the + * unconditional cb_deep_free call in btc_gap_ble_cb_handler does not + * emit a spurious "Unhandled deep free" debug log. */ + break; +#endif // (BLE_EATT_INCLUDED == TRUE) default: BTC_TRACE_DEBUG("Unhandled deep free %d", msg->act); break; diff --git a/components/bt/host/bluedroid/btc/profile/std/include/btc_ble_l2cap.h b/components/bt/host/bluedroid/btc/profile/std/include/btc_ble_l2cap.h new file mode 100644 index 00000000000..3bd1bb5c9bd --- /dev/null +++ b/components/bt/host/bluedroid/btc/profile/std/include/btc_ble_l2cap.h @@ -0,0 +1,89 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef __BTC_BLE_L2CAP_H__ +#define __BTC_BLE_L2CAP_H__ + +#if (BLE_L2CAP_COC_INCLUDED == TRUE) + +#include "btc/btc_manage.h" +#include "common/bt_target.h" +#include "esp_ble_l2cap_api.h" + +typedef enum { + BTC_BLE_L2CAP_ACT_INIT = 0, + BTC_BLE_L2CAP_ACT_DEINIT, + BTC_BLE_L2CAP_ACT_CREATE_SERVER, + BTC_BLE_L2CAP_ACT_DELETE_SERVER, + BTC_BLE_L2CAP_ACT_CONNECT, + BTC_BLE_L2CAP_ACT_ACCEPT, + BTC_BLE_L2CAP_ACT_DISCONNECT, + BTC_BLE_L2CAP_ACT_SEND, + BTC_BLE_L2CAP_ACT_RECV_READY, + BTC_BLE_L2CAP_ACT_CONNECT_ECOC, + BTC_BLE_L2CAP_ACT_RECONFIG, + BTC_BLE_L2CAP_ACT_SET_AUTO_CREDIT, +} btc_ble_l2cap_act_t; + +typedef union { + struct { + uint16_t psm; + uint16_t mtu; + } create_server; + struct { + uint16_t psm; + } delete_server; + struct { + uint16_t conn_id; + uint16_t psm; + uint16_t mtu; + } connect; + struct { + uint16_t conn_id; + uint8_t l2cap_id; + uint16_t chan_handle; + bool accept; + uint16_t mtu; + } accept; + struct { + uint16_t chan_handle; + } disconnect; + struct { + uint16_t chan_handle; + uint16_t len; + uint8_t *data; + } send; + struct { + uint16_t chan_handle; + } recv_ready; + struct { + uint16_t conn_id; + uint16_t psm; + uint16_t mtu; + uint8_t num_chan; + } connect_ecoc; + struct { + uint16_t num_chan; + uint16_t mtu; + uint16_t mps; + uint16_t chan_handles[BLE_MAX_L2CAP_CLIENTS]; + } reconfig; + struct { + uint16_t chan_handle; + bool enable; + } set_auto_credit; +} btc_ble_l2cap_args_t; + +void btc_ble_l2cap_call_handler(btc_msg_t *msg); +void btc_ble_l2cap_cb_handler(btc_msg_t *msg); +void btc_ble_l2cap_arg_deep_copy(btc_msg_t *msg, void *p_dest, void *p_src); +void btc_ble_l2cap_arg_deep_free(btc_msg_t *msg); +void btc_ble_l2cap_cb_deep_copy(btc_msg_t *msg, void *p_dest, void *p_src); +void btc_ble_l2cap_cb_deep_free(btc_msg_t *msg); + +#endif /* BLE_L2CAP_COC_INCLUDED == TRUE */ + +#endif /* __BTC_BLE_L2CAP_H__ */ diff --git a/components/bt/host/bluedroid/btc/profile/std/include/btc_gap_ble.h b/components/bt/host/bluedroid/btc/profile/std/include/btc_gap_ble.h index 41fc24c7a74..b503e66e813 100644 --- a/components/bt/host/bluedroid/btc/profile/std/include/btc_gap_ble.h +++ b/components/bt/host/bluedroid/btc/profile/std/include/btc_gap_ble.h @@ -831,4 +831,8 @@ void btc_gap_ble_deinit(void); void btc_adv_list_init(void); void btc_adv_list_deinit(void); +#if (BLE_EATT_INCLUDED == TRUE) +void btc_ble_gap_eatt_evt_cback(UINT16 conn_id, UINT8 status, UINT16 cid); +#endif + #endif /* __BTC_GAP_BLE_H__ */ diff --git a/components/bt/host/bluedroid/common/include/common/bluedroid_user_config.h b/components/bt/host/bluedroid/common/include/common/bluedroid_user_config.h index dfa5d085455..e48a20488e8 100644 --- a/components/bt/host/bluedroid/common/include/common/bluedroid_user_config.h +++ b/components/bt/host/bluedroid/common/include/common/bluedroid_user_config.h @@ -670,6 +670,54 @@ #define UC_BT_BLE_RPA_TIMEOUT 900 #endif +#ifdef CONFIG_BT_BLE_L2CAP_COC_ENABLED +#define UC_BT_BLE_L2CAP_COC_ENABLED CONFIG_BT_BLE_L2CAP_COC_ENABLED +#else +#define UC_BT_BLE_L2CAP_COC_ENABLED FALSE +#endif + +#ifdef CONFIG_BT_BLE_L2CAP_COC_MAX_CHAN +#define UC_BT_BLE_L2CAP_COC_MAX_CHAN CONFIG_BT_BLE_L2CAP_COC_MAX_CHAN +#else +#define UC_BT_BLE_L2CAP_COC_MAX_CHAN 5 +#endif + +#ifdef CONFIG_BT_BLE_L2CAP_COC_MPS +#define UC_BT_BLE_L2CAP_COC_MPS CONFIG_BT_BLE_L2CAP_COC_MPS +#else +#define UC_BT_BLE_L2CAP_COC_MPS 247 +#endif + +#ifdef CONFIG_BT_BLE_L2CAP_COC_INIT_CREDITS +#define UC_BT_BLE_L2CAP_COC_INIT_CREDITS CONFIG_BT_BLE_L2CAP_COC_INIT_CREDITS +#else +#define UC_BT_BLE_L2CAP_COC_INIT_CREDITS 24 +#endif + +#ifdef CONFIG_BT_BLE_L2CAP_ENHANCED_COC +#define UC_BT_BLE_L2CAP_ENHANCED_COC CONFIG_BT_BLE_L2CAP_ENHANCED_COC +#else +#define UC_BT_BLE_L2CAP_ENHANCED_COC FALSE +#endif + +#ifdef CONFIG_BT_BLE_EATT_ENABLE +#define UC_BT_BLE_EATT_ENABLE CONFIG_BT_BLE_EATT_ENABLE +#else +#define UC_BT_BLE_EATT_ENABLE FALSE +#endif + +#ifdef CONFIG_BT_BLE_EATT_CHAN_NUM +#define UC_BT_BLE_EATT_CHAN_NUM CONFIG_BT_BLE_EATT_CHAN_NUM +#else +#define UC_BT_BLE_EATT_CHAN_NUM 3 +#endif + +#ifdef CONFIG_BT_BLE_EATT_MTU +#define UC_BT_BLE_EATT_MTU CONFIG_BT_BLE_EATT_MTU +#else +#define UC_BT_BLE_EATT_MTU 247 +#endif + //SCO VOICE OVER HCI #ifdef CONFIG_BT_HFP_AUDIO_DATA_PATH_HCI #define UC_BT_HFP_AUDIO_DATA_PATH_HCI CONFIG_BT_HFP_AUDIO_DATA_PATH_HCI diff --git a/components/bt/host/bluedroid/common/include/common/bt_target.h b/components/bt/host/bluedroid/common/include/common/bt_target.h index 31aa6f3f6e6..7a3384c01a0 100644 --- a/components/bt/host/bluedroid/common/include/common/bt_target.h +++ b/components/bt/host/bluedroid/common/include/common/bt_target.h @@ -1449,7 +1449,85 @@ /* Support status of L2CAP connection-oriented dynamic channels over LE transport with dynamic CID */ #ifndef BLE_L2CAP_COC_INCLUDED -#define BLE_L2CAP_COC_INCLUDED FALSE // LE COC not use by default +#if (UC_BT_BLE_L2CAP_COC_ENABLED == TRUE) +#define BLE_L2CAP_COC_INCLUDED TRUE +#else +#define BLE_L2CAP_COC_INCLUDED FALSE +#endif +#endif + +#if (BLE_L2CAP_COC_INCLUDED == TRUE) +#undef BLE_MAX_L2CAP_CLIENTS +#define BLE_MAX_L2CAP_CLIENTS UC_BT_BLE_L2CAP_COC_MAX_CHAN +#endif + +/* Initial LE CoC/ECFC RX credit window (K-frames) from + * CONFIG_BT_BLE_L2CAP_COC_INIT_CREDITS. Defined even when CoC is disabled so + * that internal headers that reference L2CAP_LE_INIT_CREDITS remain valid. */ +#ifndef L2CAP_LE_INIT_CREDITS +#define L2CAP_LE_INIT_CREDITS UC_BT_BLE_L2CAP_COC_INIT_CREDITS +#endif + +/* Default LE CoC/ECFC MPS from CONFIG_BT_BLE_L2CAP_COC_MPS. */ +#ifndef L2CAP_LE_COC_MPS +#define L2CAP_LE_COC_MPS UC_BT_BLE_L2CAP_COC_MPS +#endif + +#ifndef BLE_L2CAP_COC_CLIENT_INCLUDED +#if (BLE_L2CAP_COC_INCLUDED == TRUE) && (GATTC_INCLUDED == TRUE) +#define BLE_L2CAP_COC_CLIENT_INCLUDED TRUE +#else +#define BLE_L2CAP_COC_CLIENT_INCLUDED FALSE +#endif +#endif + +#ifndef BLE_L2CAP_COC_SERVER_INCLUDED +#if (BLE_L2CAP_COC_INCLUDED == TRUE) && (GATTS_INCLUDED == TRUE) +#define BLE_L2CAP_COC_SERVER_INCLUDED TRUE +#else +#define BLE_L2CAP_COC_SERVER_INCLUDED FALSE +#endif +#endif + +#ifndef BLE_L2CAP_ENHANCED_COC_INCLUDED +#if (UC_BT_BLE_L2CAP_ENHANCED_COC == TRUE) && (BLE_L2CAP_COC_INCLUDED == TRUE) +#define BLE_L2CAP_ENHANCED_COC_INCLUDED TRUE +#else +#define BLE_L2CAP_ENHANCED_COC_INCLUDED FALSE +#endif +#endif + +#ifndef BLE_EATT_INCLUDED +#if (UC_BT_BLE_EATT_ENABLE == TRUE) && (BLE_L2CAP_ENHANCED_COC_INCLUDED == TRUE) +#define BLE_EATT_INCLUDED TRUE +#else +#define BLE_EATT_INCLUDED FALSE +#endif +#endif + +#ifndef BLE_EATT_CLIENT_INCLUDED +#if (BLE_EATT_INCLUDED == TRUE) && (GATTC_INCLUDED == TRUE) +#define BLE_EATT_CLIENT_INCLUDED TRUE +#else +#define BLE_EATT_CLIENT_INCLUDED FALSE +#endif +#endif + +#ifndef BLE_EATT_SERVER_INCLUDED +#if (BLE_EATT_INCLUDED == TRUE) && (GATTS_INCLUDED == TRUE) +#define BLE_EATT_SERVER_INCLUDED TRUE +#else +#define BLE_EATT_SERVER_INCLUDED FALSE +#endif +#endif + +/* EATT bearer count and MTU from CONFIG_BT_BLE_EATT_CHAN_NUM / CONFIG_BT_BLE_EATT_MTU. */ +#ifndef GATT_EATT_MAX_CHAN +#define GATT_EATT_MAX_CHAN UC_BT_BLE_EATT_CHAN_NUM +#endif + +#ifndef GATT_EATT_MTU +#define GATT_EATT_MTU UC_BT_BLE_EATT_MTU #endif /* Support status of L2CAP connection-oriented dynamic channels over LE or BR/EDR transport with dynamic CID */ diff --git a/components/bt/host/bluedroid/hci/packet_fragmenter.c b/components/bt/host/bluedroid/hci/packet_fragmenter.c index 26671bfe657..3d8bdbd1f77 100644 --- a/components/bt/host/bluedroid/hci/packet_fragmenter.c +++ b/components/bt/host/bluedroid/hci/packet_fragmenter.c @@ -182,6 +182,10 @@ static void reassemble_and_dispatch(BT_HDR *packet) } STREAM_TO_UINT16(l2cap_length, stream); + /* A zero-length L2CAP information payload is valid per Core Spec v6.2 + * Vol 3 Part A 3.1 (B-frame payload is 0..65535 octets); do not drop + * it. The downstream length math handles l2cap_length == 0 correctly + * (full_length == header-only == 8). */ /* Check for integer overflow in length calculation */ if (l2cap_length > (UINT16_MAX - L2CAP_HEADER_SIZE - HCI_ACL_PREAMBLE_SIZE)) { HCI_TRACE_ERROR("L2CAP length too large: %u", l2cap_length); diff --git a/components/bt/host/bluedroid/stack/btm/btm_pm.c b/components/bt/host/bluedroid/stack/btm/btm_pm.c index 9391976a4e6..2b8b8d43703 100644 --- a/components/bt/host/bluedroid/stack/btm/btm_pm.c +++ b/components/bt/host/bluedroid/stack/btm/btm_pm.c @@ -971,4 +971,14 @@ static const char *mode_to_string(tBTM_PM_MODE mode) } #endif +#else /* CLASSIC_BT_INCLUDED != TRUE */ + +tBTM_STATUS BTM_SetPowerMode(UINT8 pm_id, BD_ADDR remote_bda, tBTM_PM_PWR_MD *p_mode) +{ + UNUSED(pm_id); + UNUSED(remote_bda); + UNUSED(p_mode); + return BTM_SUCCESS; +} + #endif // #if (CLASSIC_BT_INCLUDED == TRUE) diff --git a/components/bt/host/bluedroid/stack/btu/btu_init.c b/components/bt/host/bluedroid/stack/btu/btu_init.c index 14747a03fbc..67b48effa50 100644 --- a/components/bt/host/bluedroid/stack/btu/btu_init.c +++ b/components/bt/host/bluedroid/stack/btu/btu_init.c @@ -39,6 +39,9 @@ #if (BLE_INCLUDED == TRUE) #include "stack/gatt_api.h" #include "gatt_int.h" +#if (BLE_EATT_INCLUDED == TRUE) +#include "gatt_eatt_int.h" +#endif #if SMP_INCLUDED == TRUE #include "smp_int.h" #endif @@ -120,6 +123,14 @@ void btu_init_core(void) ******************************************************************************/ void btu_free_core(void) { +#if (BLE_INCLUDED == TRUE && defined(GATT_INCLUDED) && GATT_INCLUDED == true && BLE_EATT_INCLUDED == TRUE) + /* Tear down EATT before l2c_free(): gatt_eatt_deinit() deregisters the EATT + * LE CoC PSM (L2CA_DeregisterLECoc) and the EATT GATT interface + * (GATT_Deregister, which may disconnect open links). Both need live L2CAP + * state; running them after l2c_free() dereferences the freed l2c_cb_ptr. */ + gatt_eatt_deinit(); +#endif + // Free the mandatory core stack components l2c_free(); diff --git a/components/bt/host/bluedroid/stack/gatt/att_protocol.c b/components/bt/host/bluedroid/stack/gatt/att_protocol.c index 704e1e969f0..7dc3decc927 100644 --- a/components/bt/host/bluedroid/stack/gatt/att_protocol.c +++ b/components/bt/host/bluedroid/stack/gatt/att_protocol.c @@ -29,6 +29,9 @@ #include "gatt_int.h" #include "stack/l2c_api.h" +#if (BLE_EATT_INCLUDED == TRUE) +#include "gatt_eatt_int.h" +#endif #define GATT_HDR_FIND_TYPE_VALUE_LEN 21 #define GATT_OP_CODE_SIZE 1 @@ -387,9 +390,26 @@ BT_HDR *attp_build_value_cmd(UINT16 payload_size, UINT8 op_code, tGATT_STATUS attp_send_msg_to_l2cap(tGATT_TCB *p_tcb, BT_HDR *p_toL2CAP) { UINT16 l2cap_ret; + UINT16 lcid = p_tcb->att_lcid; +#if (BLE_EATT_INCLUDED == TRUE) + UINT8 op_code = *((UINT8 *)(p_toL2CAP + 1) + p_toL2CAP->offset); - if (p_tcb->att_lcid == L2CAP_ATT_CID) { + /* Exchange MTU is defined only on the legacy ATT bearer (Core Spec Vol 3 + * Part G 5.3): keep it on att_lcid even if eatt_tx_bearer/eatt_rx_bearer is + * set. Without this, an MTU PDU flushed while an EATT response is still being + * processed (eatt_rx_bearer not yet cleared) would be sent on an EATT bearer + * and the peer would reject it with REQ_NOT_SUPPORTED. */ + if (op_code != GATT_REQ_MTU && op_code != GATT_RSP_MTU) { + if (p_tcb->eatt_tx_bearer != 0) { + lcid = p_tcb->eatt_tx_bearer; + } else if (p_tcb->eatt_rx_bearer != 0) { + lcid = p_tcb->eatt_rx_bearer; + } + } +#endif + + if (lcid == L2CAP_ATT_CID) { /* L2CA_SendFixedChnlData() silently drops (osi_free) the buffer when the * ATT fixed channel is already in cong_sent state, yet still returns * L2CAP_DW_CONGESTED. Without distinguishing this from the post-enqueue @@ -404,11 +424,25 @@ tGATT_STATUS attp_send_msg_to_l2cap(tGATT_TCB *p_tcb, BT_HDR *p_toL2CAP) } l2cap_ret = L2CA_SendFixedChnlData (L2CAP_ATT_CID, p_tcb->peer_bda, p_toL2CAP); } else { +#if (BLE_L2CAP_COC_INCLUDED == TRUE) && (BLE_EATT_INCLUDED == TRUE) + if (gatt_eatt_is_bearer(lcid)) { + l2cap_ret = L2CA_LECocDataWrite(lcid, p_toL2CAP); + } else +#endif #if (CLASSIC_BT_INCLUDED == TRUE) - l2cap_ret = (UINT16) L2CA_DataWrite (p_tcb->att_lcid, p_toL2CAP); + { + l2cap_ret = (UINT16) L2CA_DataWrite(lcid, p_toL2CAP); + } #else - l2cap_ret = L2CAP_DW_FAILED; -#endif ///CLASSIC_BT_INCLUDED == TRUE + { + /* No L2CAP write consumed the buffer on this BLE-only path (e.g. an + * lcid that is neither the ATT fixed channel nor a known EATT + * bearer). Free it here so attp_send_msg_to_l2cap always consumes + * the buffer exactly once, matching every caller's assumption. */ + osi_free(p_toL2CAP); + l2cap_ret = L2CAP_DW_FAILED; + } +#endif } if (l2cap_ret == L2CAP_DW_FAILED) { @@ -470,7 +504,7 @@ BT_HDR *attp_build_sr_msg(tGATT_TCB *p_tcb, UINT8 op_code, tGATT_SR_MSG *p_msg) case GATT_HANDLE_VALUE_NOTIF: case GATT_HANDLE_VALUE_IND: case GATT_HANDLE_MULTI_VALUE_NOTIF: - p_cmd = attp_build_value_cmd(p_tcb->payload_size, + p_cmd = attp_build_value_cmd(gatt_get_att_mtu(p_tcb), op_code, p_msg->attr_value.handle, offset, @@ -552,6 +586,37 @@ tGATT_STATUS attp_cl_send_cmd(tGATT_TCB *p_tcb, UINT16 clcb_idx, UINT8 cmd_code, if (p_tcb != NULL) { cmd_code &= ~GATT_AUTH_SIGN_MASK; +#if (BLE_EATT_CLIENT_INCLUDED == TRUE) + UINT16 eatt_bearer = L2CAP_ATT_CID; + if (cmd_code != GATT_HANDLE_VALUE_CONF && cmd_code != GATT_CMD_WRITE && + cmd_code != GATT_REQ_MTU) { + eatt_bearer = gatt_eatt_get_available_bearer(p_tcb->peer_bda, cmd_code); + } + if (eatt_bearer != L2CAP_ATT_CID) { + p_tcb->eatt_tx_bearer = eatt_bearer; + att_ret = attp_send_msg_to_l2cap(p_tcb, p_cmd); + p_tcb->eatt_tx_bearer = 0; + if (att_ret == GATT_SUCCESS) { + gatt_eatt_mark_busy(p_tcb->peer_bda, eatt_bearer, cmd_code, clcb_idx); + gatt_start_rsp_timer(clcb_idx); + } else if (att_ret == GATT_CONGESTED) { + /* Buffer is queued at L2CAP; arm the response timer just like the + * legacy path so a lost response cannot hang the CLCB forever. */ + gatt_eatt_mark_busy(p_tcb->peer_bda, eatt_bearer, cmd_code, clcb_idx); + gatt_start_rsp_timer(clcb_idx); + /* Normalize to success: the buffer was accepted (queued for + * credit) so the operation is in progress. Returning + * GATT_CONGESTED would make gatt_act_discovery/gatt_act_read + * treat it as failure and free the CLCB via gatt_end_operation + * without releasing this EATT bearer, leaving it stuck busy. */ + att_ret = GATT_SUCCESS; + } else { + att_ret = GATT_INTERNAL_ERROR; + } + return att_ret; + } +#endif + /* no pending request or value confirmation */ if (p_tcb->pending_cl_req == p_tcb->next_slot_inq || cmd_code == GATT_HANDLE_VALUE_CONF) { @@ -598,6 +663,16 @@ tGATT_STATUS attp_send_cl_msg (tGATT_TCB *p_tcb, UINT16 clcb_idx, UINT8 op_code, UINT16 offset = 0, handle; if (p_tcb != NULL) { + /* Use the legacy ATT payload_size as the fallback MTU. gatt_get_att_mtu() + * would return the EATT rx-bearer MTU when eatt_rx_bearer is transiently + * set (re-entrant response handling), which could size a PDU for EATT but + * send it on the legacy bearer and exceed its MTU. */ + UINT16 att_mtu = p_tcb->payload_size; + +#if (BLE_EATT_CLIENT_INCLUDED == TRUE) + att_mtu = gatt_eatt_mtu_for_client_op(p_tcb->peer_bda, op_code, att_mtu); +#endif + switch (op_code) { case GATT_REQ_MTU: if (p_msg->mtu <= GATT_MAX_MTU_SIZE) { @@ -647,7 +722,7 @@ tGATT_STATUS attp_send_cl_msg (tGATT_TCB *p_tcb, UINT16 clcb_idx, UINT8 op_code, case GATT_CMD_WRITE: case GATT_SIGN_CMD_WRITE: if (GATT_HANDLE_IS_VALID (p_msg->attr_value.handle)) { - p_cmd = attp_build_value_cmd (p_tcb->payload_size, + p_cmd = attp_build_value_cmd (att_mtu, op_code, p_msg->attr_value.handle, offset, p_msg->attr_value.len, @@ -662,12 +737,12 @@ tGATT_STATUS attp_send_cl_msg (tGATT_TCB *p_tcb, UINT16 clcb_idx, UINT8 op_code, break; case GATT_REQ_FIND_TYPE_VALUE: - p_cmd = attp_build_read_by_type_value_cmd(p_tcb->payload_size, &p_msg->find_type_value); + p_cmd = attp_build_read_by_type_value_cmd(att_mtu, &p_msg->find_type_value); break; case GATT_REQ_READ_MULTI: case GATT_REQ_READ_MULTI_VAR: - p_cmd = attp_build_read_multi_cmd(op_code, p_tcb->payload_size, + p_cmd = attp_build_read_multi_cmd(op_code, att_mtu, p_msg->read_multi.num_handles, p_msg->read_multi.handles); break; diff --git a/components/bt/host/bluedroid/stack/gatt/gatt_api.c b/components/bt/host/bluedroid/stack/gatt/gatt_api.c index 3ade9f7f388..392c72d26aa 100644 --- a/components/bt/host/bluedroid/stack/gatt/gatt_api.c +++ b/components/bt/host/bluedroid/stack/gatt/gatt_api.c @@ -31,6 +31,9 @@ #include "stack/gatt_api.h" #include "gatt_int.h" #include "stack/l2c_api.h" +#if (BLE_EATT_INCLUDED == TRUE) +#include "gatt_eatt_int.h" +#endif #include "btm_int.h" #include "stack/sdpdefs.h" #include "stack/sdp_api.h" @@ -636,6 +639,13 @@ tGATT_STATUS GATTS_HandleValueIndication (UINT16 conn_id, UINT16 attr_handle, U return GATT_BUSY; } else { +#if (BLE_EATT_SERVER_INCLUDED == TRUE) + /* Route the indication over an EATT bearer (if any) so it uses the EATT + * MTU instead of the legacy 23-byte ATT MTU. Set transiently and cleared + * after the send; all GATT TX runs on the single BTU task. */ + UINT16 ind_bearer = gatt_eatt_get_server_tx_bearer(p_tcb->peer_bda); + p_tcb->eatt_tx_bearer = (ind_bearer != L2CAP_ATT_CID) ? ind_bearer : 0; +#endif if ( (p_msg = attp_build_sr_msg (p_tcb, GATT_HANDLE_VALUE_IND, (tGATT_SR_MSG *)&indication)) != NULL) { cmd_status = attp_send_sr_msg (p_tcb, p_msg); @@ -644,6 +654,9 @@ tGATT_STATUS GATTS_HandleValueIndication (UINT16 conn_id, UINT16 attr_handle, U gatt_start_conf_timer(p_tcb); } } +#if (BLE_EATT_SERVER_INCLUDED == TRUE) + p_tcb->eatt_tx_bearer = 0; +#endif } return cmd_status; } @@ -691,12 +704,22 @@ tGATT_STATUS GATTS_HandleValueNotification (UINT16 conn_id, UINT16 attr_handle, memcpy (notif.value, p_val, val_len); notif.auth_req = GATT_AUTH_REQ_NONE; +#if (BLE_EATT_SERVER_INCLUDED == TRUE) + /* Route the notification over an EATT bearer (if any) so it uses the EATT + * MTU instead of the legacy 23-byte ATT MTU. Set transiently and cleared + * after the send; all GATT TX runs on the single BTU task. */ + UINT16 notif_bearer = gatt_eatt_get_server_tx_bearer(p_tcb->peer_bda); + p_tcb->eatt_tx_bearer = (notif_bearer != L2CAP_ATT_CID) ? notif_bearer : 0; +#endif if ((p_buf = attp_build_sr_msg (p_tcb, GATT_HANDLE_VALUE_NOTIF, (tGATT_SR_MSG *)¬if)) != NULL) { cmd_sent = attp_send_sr_msg (p_tcb, p_buf); } else { cmd_sent = GATT_NO_RESOURCES; } +#if (BLE_EATT_SERVER_INCLUDED == TRUE) + p_tcb->eatt_tx_bearer = 0; +#endif } return cmd_sent; } @@ -1208,7 +1231,17 @@ tGATT_STATUS GATTC_SendHandleValueConfirm (UINT16 conn_id, UINT16 handle) GATT_TRACE_DEBUG ("notif_count=%d ", p_tcb->ind_count); /* send confirmation now */ +#if (BLE_EATT_INCLUDED == TRUE) + /* Route the confirmation back on the EATT bearer the indication came + * in on (0 == legacy ATT). eatt_rx_bearer was cleared after the + * indication was delivered, so use the saved eatt_ind_bearer. */ + p_tcb->eatt_tx_bearer = p_tcb->eatt_ind_bearer; ret = attp_send_cl_msg(p_tcb, 0, GATT_HANDLE_VALUE_CONF, (tGATT_CL_MSG *)&handle); + p_tcb->eatt_tx_bearer = 0; + p_tcb->eatt_ind_bearer = 0; +#else + ret = attp_send_cl_msg(p_tcb, 0, GATT_HANDLE_VALUE_CONF, (tGATT_CL_MSG *)&handle); +#endif p_tcb->ind_count = 0; @@ -1775,12 +1808,24 @@ tGATT_STATUS GATTS_HandleMultiValueNotification (UINT16 conn_id, tGATT_HLV *tupl notif.auth_req = GATT_AUTH_REQ_NONE; +#if (BLE_EATT_SERVER_INCLUDED == TRUE) + /* Route the multi-value notification over an EATT bearer (if any) so it uses + * the EATT MTU instead of the legacy 23-byte ATT MTU, and so gatt_get_att_mtu() + * (used for buffer sizing in attp_build_sr_msg) matches the bearer it is sent + * on. Set transiently and cleared after the send; all GATT TX runs on the + * single BTU task. Mirrors GATTS_HandleValueNotification. */ + UINT16 mv_bearer = gatt_eatt_get_server_tx_bearer(p_tcb->peer_bda); + p_tcb->eatt_tx_bearer = (mv_bearer != L2CAP_ATT_CID) ? mv_bearer : 0; +#endif p_buf = attp_build_sr_msg (p_tcb, GATT_HANDLE_MULTI_VALUE_NOTIF, (tGATT_SR_MSG *)¬if); if (p_buf != NULL) { cmd_sent = attp_send_sr_msg (p_tcb, p_buf); } else { cmd_sent = GATT_NO_RESOURCES; } +#if (BLE_EATT_SERVER_INCLUDED == TRUE) + p_tcb->eatt_tx_bearer = 0; +#endif return cmd_sent; } @@ -1791,4 +1836,22 @@ tGATT_STATUS GATTS_ShowLocalDatabase(void) return GATT_SUCCESS; } +#if (BLE_EATT_INCLUDED == TRUE) +void GATT_EattSetChanNum(UINT8 num_chan) +{ + gatt_eatt_set_chan_num(num_chan); +} + +BOOLEAN GATT_EattSetDefaultBearer(UINT16 conn_id, UINT16 lcid) +{ +#if (BLE_EATT_CLIENT_INCLUDED == TRUE) + return gatt_eatt_set_default_bearer(conn_id, lcid); +#else + UNUSED(conn_id); + UNUSED(lcid); + return FALSE; +#endif +} +#endif + #endif diff --git a/components/bt/host/bluedroid/stack/gatt/gatt_auth.c b/components/bt/host/bluedroid/stack/gatt/gatt_auth.c index 19ad2bc4cad..8db92907c46 100644 --- a/components/bt/host/bluedroid/stack/gatt/gatt_auth.c +++ b/components/bt/host/bluedroid/stack/gatt/gatt_auth.c @@ -28,6 +28,9 @@ #include #include "gatt_int.h" +#if (BLE_EATT_INCLUDED == TRUE) +#include "gatt_eatt_int.h" +#endif #include "stack/gatt_api.h" #include "btm_int.h" @@ -251,6 +254,9 @@ void gatt_notify_enc_cmpl(BD_ADDR bd_addr) } } } +#if (BLE_EATT_INCLUDED == TRUE) + gatt_eatt_on_encrypted(bd_addr); +#endif } else { GATT_TRACE_DEBUG("notify GATT for encryption completion of unknown device"); } diff --git a/components/bt/host/bluedroid/stack/gatt/gatt_cl.c b/components/bt/host/bluedroid/stack/gatt/gatt_cl.c index 1fc86945659..0483dcc0322 100644 --- a/components/bt/host/bluedroid/stack/gatt/gatt_cl.c +++ b/components/bt/host/bluedroid/stack/gatt/gatt_cl.c @@ -29,6 +29,9 @@ #include #include "osi/allocator.h" #include "gatt_int.h" +#if (BLE_EATT_INCLUDED == TRUE) +#include "gatt_eatt_int.h" +#endif #include "l2c_int.h" #define GATT_WRITE_LONG_HDR_SIZE 5 /* 1 opcode + 2 handle + 2 offset */ @@ -244,7 +247,7 @@ void gatt_act_write (tGATT_CLCB *p_clcb, UINT8 sec_act) break; case GATT_WRITE: - if (p_attr->len <= (p_tcb->payload_size - GATT_HDR_SIZE)) { + if (p_attr->len <= (GATT_CL_ATT_MTU(p_tcb, GATT_REQ_WRITE) - GATT_HDR_SIZE)) { p_clcb->s_handle = p_attr->handle; rt = gatt_send_write_msg(p_tcb, @@ -359,8 +362,8 @@ void gatt_send_prepare_write(tGATT_TCB *p_tcb, tGATT_CLCB *p_clcb) GATT_TRACE_DEBUG("gatt_send_prepare_write type=0x%x", type ); to_send = p_attr->len - p_attr->offset; - if (to_send > (p_tcb->payload_size - GATT_WRITE_LONG_HDR_SIZE)) { /* 2 = UINT16 offset bytes */ - to_send = p_tcb->payload_size - GATT_WRITE_LONG_HDR_SIZE; + if (to_send > (GATT_CL_ATT_MTU(p_tcb, GATT_REQ_PREPARE_WRITE) - GATT_WRITE_LONG_HDR_SIZE)) { /* 2 = UINT16 offset bytes */ + to_send = GATT_CL_ATT_MTU(p_tcb, GATT_REQ_PREPARE_WRITE) - GATT_WRITE_LONG_HDR_SIZE; } p_clcb->s_handle = p_attr->handle; @@ -722,6 +725,13 @@ void gatt_process_notification(tGATT_TCB *p_tcb, UINT8 op_code, /* start a timer for app confirmation */ if (p_tcb->ind_count > 0) { +#if (BLE_EATT_INCLUDED == TRUE) + /* Remember the bearer this indication arrived on (0 == legacy ATT) + * so the app's deferred confirmation is routed back to it; by the + * time GATTC_SendHandleValueConfirm() runs, eatt_rx_bearer is + * already cleared. */ + p_tcb->eatt_ind_bearer = p_tcb->eatt_rx_bearer; +#endif gatt_start_ind_ack_timer(p_tcb); } else { /* no app to indicate, or invalid handle */ attp_send_cl_msg(p_tcb, 0, GATT_HANDLE_VALUE_CONF, NULL); @@ -797,11 +807,11 @@ void gatt_process_read_by_type_rsp (tGATT_TCB *p_tcb, tGATT_CLCB *p_clcb, UINT8 STREAM_TO_UINT8(value_len, p); - if ((value_len > (p_tcb->payload_size - 2)) || (value_len > (len - 1)) ) { + if ((value_len > (gatt_get_att_mtu(p_tcb) - 2)) || (value_len > (len - 1)) ) { /* this is an error case that server's response containing a value length which is larger than MTU-2 or value_len > message total length -1 */ GATT_TRACE_ERROR("gatt_process_read_by_type_rsp: Discard response op_code=%d value_len=%d > (MTU-2=%d or msg_len-1=%d)", - op_code, value_len, (p_tcb->payload_size - 2), (len - 1)); + op_code, value_len, (gatt_get_att_mtu(p_tcb) - 2), (len - 1)); gatt_end_operation(p_clcb, GATT_ERROR, NULL); return; } @@ -891,7 +901,7 @@ void gatt_process_read_by_type_rsp (tGATT_TCB *p_tcb, tGATT_CLCB *p_clcb, UINT8 /* value_len is the length of current record's value; use it to avoid overread when multiple records present */ p_clcb->counter = value_len; p_clcb->s_handle = handle; - UINT16 max_rbtype_val_len = (p_clcb->p_tcb->payload_size - 4); + UINT16 max_rbtype_val_len = (gatt_get_att_mtu(p_clcb->p_tcb) - 4); if (max_rbtype_val_len > GATT_MAX_READ_BY_TYPE_VALUE_LEN) { max_rbtype_val_len = GATT_MAX_READ_BY_TYPE_VALUE_LEN; } @@ -1000,7 +1010,7 @@ void gatt_process_read_rsp(tGATT_TCB *p_tcb, tGATT_CLCB *p_clcb, UINT8 op_code, /* send next request if needed */ - if (len == (p_tcb->payload_size - 1) && /* full packet for read or read blob rsp */ + if (len == (gatt_get_att_mtu(p_tcb) - 1) && /* full packet for read or read blob rsp */ len + offset < GATT_MAX_ATTR_LEN) { GATT_TRACE_DEBUG("full pkt issue read blob for remaining bytes old offset=%d len=%d new offset=%d", offset, len, p_clcb->counter); @@ -1068,6 +1078,14 @@ void gatt_process_mtu_rsp(tGATT_TCB *p_tcb, tGATT_CLCB *p_clcb, UINT16 len, UINT UINT16 mtu; tGATT_STATUS status = GATT_SUCCESS; +#if (BLE_EATT_CLIENT_INCLUDED == TRUE) + if (p_tcb->eatt_rx_bearer != 0 && gatt_eatt_is_bearer(p_tcb->eatt_rx_bearer)) { + GATT_TRACE_ERROR("ignore MTU response on EATT bearer"); + gatt_end_operation(p_clcb, GATT_ERROR, NULL); + return; + } +#endif + if (len < GATT_MTU_RSP_MIN_LEN) { GATT_TRACE_ERROR("invalid MTU response PDU received, discard."); status = GATT_INVALID_PDU; @@ -1187,21 +1205,51 @@ BOOLEAN gatt_cl_send_next_cmd_inq(tGATT_TCB *p_tcb) ** *******************************************************************************/ void gatt_client_handle_server_rsp (tGATT_TCB *p_tcb, UINT8 op_code, - UINT16 len, UINT8 *p_data) + UINT16 len, UINT8 *p_data, UINT16 eatt_bearer_lcid) { tGATT_CLCB *p_clcb = NULL; - UINT8 rsp_code; + UINT8 rsp_code = 0; +#if (BLE_EATT_CLIENT_INCLUDED == TRUE) + UINT8 cmd_code = 0; + UINT16 clcb_idx = 0; +#else + UNUSED(eatt_bearer_lcid); +#endif if (op_code != GATT_HANDLE_VALUE_IND && op_code != GATT_HANDLE_VALUE_NOTIF && op_code != GATT_HANDLE_MULTI_VALUE_NOTIF) { - p_clcb = gatt_cmd_dequeue(p_tcb, &rsp_code); - - rsp_code = gatt_cmd_to_rsp_code(rsp_code); + p_clcb = NULL; +#if (BLE_EATT_CLIENT_INCLUDED == TRUE) + if (eatt_bearer_lcid != 0) { + if (gatt_eatt_release_bearer(p_tcb->peer_bda, eatt_bearer_lcid, &cmd_code, &clcb_idx)) { + p_clcb = gatt_clcb_find_by_idx(clcb_idx); + if (p_clcb != NULL) { + rsp_code = gatt_cmd_to_rsp_code(cmd_code); + } + } + } +#endif + if (p_clcb == NULL +#if (BLE_EATT_CLIENT_INCLUDED == TRUE) + && eatt_bearer_lcid == 0 +#endif + ) { + p_clcb = gatt_cmd_dequeue(p_tcb, &rsp_code); + rsp_code = gatt_cmd_to_rsp_code(rsp_code); + } if (p_clcb == NULL || (rsp_code != op_code && op_code != GATT_RSP_ERROR)) { GATT_TRACE_WARNING ("ATT - Ignore wrong response. Receives (%02x) \ Request(%02x) Ignored", op_code, rsp_code); - +#if (BLE_EATT_CLIENT_INCLUDED == TRUE) + /* On an EATT bearer the bearer was released above to locate the + * pending request. Since this response is wrong/unexpected, restore + * the bearer's busy state so the still-pending request keeps it and + * completes on the correct response or the response timer. */ + if (p_clcb != NULL && eatt_bearer_lcid != 0) { + gatt_eatt_mark_busy(p_tcb->peer_bda, eatt_bearer_lcid, cmd_code, clcb_idx); + } +#endif return; } else { btu_stop_timer (&p_clcb->rsp_timer_ent); @@ -1210,8 +1258,8 @@ void gatt_client_handle_server_rsp (tGATT_TCB *p_tcb, UINT8 op_code, } /* the size of the message may not be bigger than the local max PDU size*/ /* The message has to be smaller than the agreed MTU, len does not count op_code */ - if (len >= p_tcb->payload_size) { - GATT_TRACE_ERROR("invalid response/indicate pkt size: %d, PDU size: %d", len + 1, p_tcb->payload_size); + if (len >= gatt_get_att_mtu(p_tcb)) { + GATT_TRACE_ERROR("invalid response/indicate pkt size: %d, PDU size: %d", len + 1, gatt_get_att_mtu(p_tcb)); if (op_code != GATT_HANDLE_VALUE_NOTIF && op_code != GATT_HANDLE_VALUE_IND && op_code != GATT_HANDLE_MULTI_VALUE_NOTIF) { gatt_end_operation(p_clcb, GATT_ERROR, NULL); @@ -1272,7 +1320,12 @@ void gatt_client_handle_server_rsp (tGATT_TCB *p_tcb, UINT8 op_code, if (op_code != GATT_HANDLE_VALUE_IND && op_code != GATT_HANDLE_VALUE_NOTIF && op_code != GATT_HANDLE_MULTI_VALUE_NOTIF) { +#if (BLE_EATT_CLIENT_INCLUDED == TRUE) + if (eatt_bearer_lcid == 0) +#endif + { gatt_cl_send_next_cmd_inq(p_tcb); + } } } diff --git a/components/bt/host/bluedroid/stack/gatt/gatt_eatt.c b/components/bt/host/bluedroid/stack/gatt/gatt_eatt.c new file mode 100644 index 00000000000..96bb2c61959 --- /dev/null +++ b/components/bt/host/bluedroid/stack/gatt/gatt_eatt.c @@ -0,0 +1,1369 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* EATT (Enhanced ATT) over LE Enhanced CoC (PSM 0x0027). */ + +#include +#include "common/bt_target.h" +#include "stack/bt_types.h" +#include "stack/l2c_api.h" +#include "stack/l2cdefs.h" +#include "stack/gatt_api.h" +#include "stack/gattdefs.h" +#include "stack/sdpdefs.h" +#include "stack/btm_ble_api.h" +#include "stack/btm_api.h" +#include "btm_int.h" +#include "l2c_int.h" +#include "gatt_int.h" +#include "gatt_eatt_int.h" +#include "osi/allocator.h" + +#if (BLE_EATT_INCLUDED == TRUE) + +#define GATT_EATT_TRACE_API(fmt, ...) GATT_TRACE_API("EATT: " fmt, ##__VA_ARGS__) +#define GATT_EATT_TRACE_DEBUG(fmt, ...) GATT_TRACE_DEBUG("EATT: " fmt, ##__VA_ARGS__) +#define GATT_EATT_TRACE_ERROR(fmt, ...) GATT_TRACE_ERROR("EATT: " fmt, ##__VA_ARGS__) + +#define BLE_GATT_SR_SUPP_FEAT_EATT_BITMASK 0x01 +#define BLE_GATT_CL_SUPP_FEAT_EATT_BITMASK 0x02 + +typedef enum { + GATT_EATT_ST_IDLE = 0, + GATT_EATT_ST_READ_SR_FEAT, + GATT_EATT_ST_READ_CL_FEAT, + GATT_EATT_ST_WRITE_CL_FEAT, + GATT_EATT_ST_CONNECTING, +} tGATT_EATT_SETUP_ST; + +typedef struct { + BOOLEAN in_use; + BOOLEAN reported; /* TRUE once a connect event (status=0) was delivered for + * this bearer, so free_bearer only reports a symmetric + * disconnect for bearers the app actually saw connect. */ + UINT16 lcid; + UINT8 client_op; + UINT16 clcb_idx; +} tGATT_EATT_BEARER; + +typedef struct { + BOOLEAN in_use; + BD_ADDR peer_bda; + UINT16 conn_id; + UINT8 bearer_count; + UINT8 setup_target; /* number of bearers requested in the active setup */ + UINT8 setup_done; /* number of setup responses (ok or fail) received */ + UINT16 default_lcid; + tGATT_EATT_SETUP_ST setup_st; + UINT16 peer_cl_feat_handle; + UINT8 peer_cl_feat_val; /* current Client Supported Features value read back */ + UINT8 tx_rr; /* round-robin cursor for server-initiated PDUs */ + tGATT_EATT_BEARER bearers[GATT_EATT_MAX_CHAN]; +} tGATT_EATT_CONN; + +static tGATT_EATT_CONN s_eatt_conn[MAX_L2CAP_LINKS]; +static tL2CAP_APPL_INFO s_eatt_l2cap_appl; +static UINT16 s_eatt_reg_psm; +static UINT8 s_eatt_chan_num = GATT_EATT_MAX_CHAN; +static tGATT_EATT_EVT_CBACK *s_eatt_evt_cback; +static tGATT_IF s_eatt_gatt_if; + +static void gatt_eatt_connect_cfm(UINT16 lcid, UINT16 result); +static void gatt_eatt_disconnect_ind(UINT16 lcid, BOOLEAN local_init); +static void gatt_eatt_data_ind_l2c(UINT16 lcid, BT_HDR *p_buf); +static void gatt_eatt_congestion(UINT16 lcid, BOOLEAN congested); + +static void gatt_eatt_gatt_conn_cback(tGATT_IF gatt_if, BD_ADDR bda, UINT16 conn_id, + BOOLEAN connected, tGATT_DISCONN_REASON reason, + tBT_TRANSPORT transport); +#if (BLE_EATT_CLIENT_INCLUDED == TRUE) +static void gatt_eatt_gatt_cmpl_cback(UINT16 conn_id, tGATTC_OPTYPE op, tGATT_STATUS status, + tGATT_CL_COMPLETE *p_data); +#endif +#if (BLE_EATT_SERVER_INCLUDED == TRUE) +static void gatt_eatt_connect_ind(BD_ADDR bd_addr, UINT16 lcid, UINT16 psm, UINT8 id); +#endif + +static tGATT_EATT_CONN *gatt_eatt_find_conn_by_bda(BD_ADDR bd_addr) +{ + for (int i = 0; i < MAX_L2CAP_LINKS; i++) { + if (s_eatt_conn[i].in_use && + memcmp(s_eatt_conn[i].peer_bda, bd_addr, BD_ADDR_LEN) == 0) { + return &s_eatt_conn[i]; + } + } + return NULL; +} + +#if (BLE_EATT_CLIENT_INCLUDED == TRUE) +static tGATT_EATT_CONN *gatt_eatt_find_conn_for_setup(UINT16 stack_conn_id) +{ + UINT8 tcb_idx = GATT_GET_TCB_IDX(stack_conn_id); + tGATT_TCB *p_tcb = gatt_get_tcb_by_idx(tcb_idx); + + if (p_tcb == NULL) { + return NULL; + } + return gatt_eatt_find_conn_by_bda(p_tcb->peer_bda); +} +#endif + +static tGATT_EATT_CONN *gatt_eatt_alloc_conn(BD_ADDR bd_addr) +{ + tGATT_EATT_CONN *ec; + + ec = gatt_eatt_find_conn_by_bda(bd_addr); + if (ec != NULL) { + return ec; + } + + for (int i = 0; i < MAX_L2CAP_LINKS; i++) { + if (!s_eatt_conn[i].in_use) { + memset(&s_eatt_conn[i], 0, sizeof(s_eatt_conn[i])); + s_eatt_conn[i].in_use = TRUE; + memcpy(s_eatt_conn[i].peer_bda, bd_addr, BD_ADDR_LEN); + return &s_eatt_conn[i]; + } + } + GATT_EATT_TRACE_ERROR("alloc_conn failed: conn table full"); + return NULL; +} + +static tGATT_EATT_BEARER *gatt_eatt_find_bearer(tGATT_EATT_CONN *ec, UINT16 lcid) +{ + if (ec == NULL) { + return NULL; + } + for (int i = 0; i < GATT_EATT_MAX_CHAN; i++) { + if (ec->bearers[i].in_use && ec->bearers[i].lcid == lcid) { + return &ec->bearers[i]; + } + } + return NULL; +} + +static tGATT_EATT_BEARER *gatt_eatt_alloc_bearer(tGATT_EATT_CONN *ec, UINT16 lcid) +{ + tGATT_EATT_BEARER *b; + + b = gatt_eatt_find_bearer(ec, lcid); + if (b != NULL) { + return b; + } + + for (int i = 0; i < GATT_EATT_MAX_CHAN; i++) { + if (!ec->bearers[i].in_use) { + memset(&ec->bearers[i], 0, sizeof(ec->bearers[i])); + ec->bearers[i].in_use = TRUE; + ec->bearers[i].lcid = lcid; + ec->bearer_count++; + return &ec->bearers[i]; + } + } + GATT_EATT_TRACE_ERROR("alloc_bearer failed: slots full lcid=0x%04x bearer_count=%u", + lcid, ec->bearer_count); + return NULL; +} + +static void gatt_eatt_free_bearer(tGATT_EATT_CONN *ec, tGATT_EATT_BEARER *b) +{ + BD_ADDR peer_bda; + UINT16 freed_lcid; + tGATT_TCB *p_tcb; + + if (ec == NULL || b == NULL || !b->in_use) { + return; + } + memcpy(peer_bda, ec->peer_bda, BD_ADDR_LEN); + freed_lcid = b->lcid; + if (ec->default_lcid == b->lcid) { + ec->default_lcid = 0; + } + /* Do not guard on ec->conn_id: an app conn_id of 0 is the valid tcb_idx 0 + * (the first BLE connection), not a "not found" sentinel. Guarding on it + * would suppress the disconnect event for tcb_idx 0 while its connect event + * was reported, causing an asymmetry. + * + * Only report the disconnect if a matching connect event was delivered. A + * bearer allocated in connect_ind but torn down before connect_cfm succeeds + * (e.g. L2CA_ConnectLECocRsp failed, or the link dropped mid-setup) was never + * reported as connected, so emitting a disconnect for it would be spurious. */ + if (s_eatt_evt_cback && b->reported) { + GATT_EATT_TRACE_DEBUG("free_bearer report disconnect conn_id=%u lcid=0x%04x", + ec->conn_id, b->lcid); + (*s_eatt_evt_cback)(ec->conn_id, 1, b->lcid); + } + memset(b, 0, sizeof(*b)); + if (ec->bearer_count > 0) { + ec->bearer_count--; + } + if (ec->bearer_count == 0) { + GATT_EATT_TRACE_DEBUG("free_bearer last bearer gone conn_id=%u", ec->conn_id); + ec->setup_st = GATT_EATT_ST_IDLE; + } + p_tcb = gatt_find_tcb_by_addr(peer_bda, BT_TRANSPORT_LE); + if (p_tcb != NULL) { + if (p_tcb->eatt_ind_bearer == freed_lcid) { + p_tcb->eatt_ind_bearer = 0; + } +#if (GATTS_INCLUDED == TRUE) + if (p_tcb->sr_cmd.eatt_lcid == freed_lcid) { + /* The freed bearer owns the pending server command. Fully clear the + * sr_cmd slot (op_code, p_rsp_msg, multi_rsp_q, eatt_lcid) via + * gatt_dequeue_sr_cmd. Clearing only eatt_lcid would leave op_code + * non-zero, so gatt_sr_cmd_empty stays FALSE and every later server + * request from this peer is silently discarded until the ACL drops; + * any pending response buffer would also leak. */ + gatt_dequeue_sr_cmd(p_tcb); + } +#endif + if (ec->bearer_count == 0) { + p_tcb->eatt_att_mtu = 0; + } + } +} + +static void gatt_eatt_free_conn(tGATT_EATT_CONN *ec) +{ + if (ec == NULL) { + return; + } + + GATT_EATT_TRACE_DEBUG("free_conn conn_id=%u bearer_count=%u", ec->conn_id, ec->bearer_count); + for (int i = 0; i < GATT_EATT_MAX_CHAN; i++) { + if (ec->bearers[i].in_use) { + gatt_eatt_free_bearer(ec, &ec->bearers[i]); + } + } + memset(ec, 0, sizeof(*ec)); +} + +static UINT16 gatt_eatt_chan_mtu(UINT16 lcid) +{ + tL2C_CCB *p_ccb = l2cu_find_ccb_by_cid(NULL, lcid); + + if (p_ccb == NULL) { + return 0; + } + return MIN(p_ccb->local_conn_cfg.mtu, p_ccb->peer_conn_cfg.mtu); +} + +static void gatt_eatt_update_tcb_mtu(BD_ADDR bd_addr, UINT16 lcid) +{ + tGATT_TCB *p_tcb = gatt_find_tcb_by_addr(bd_addr, BT_TRANSPORT_LE); + UINT16 mtu; + + if (p_tcb == NULL) { + return; + } + mtu = gatt_eatt_chan_mtu(lcid); + if (mtu >= GATT_DEF_BLE_MTU_SIZE) { + p_tcb->eatt_att_mtu = mtu; + GATT_EATT_TRACE_API("sync att mtu=%u lcid=0x%04x", mtu, lcid); + } +} + +void gatt_eatt_on_chan_mtu_changed(BD_ADDR bd_addr, UINT16 lcid) +{ + if (!gatt_eatt_is_bearer(lcid)) { + return; + } + gatt_eatt_update_tcb_mtu(bd_addr, lcid); +} + +UINT16 gatt_get_att_mtu(tGATT_TCB *p_tcb) +{ + UINT16 mtu; + + if (p_tcb == NULL) { + return GATT_DEF_BLE_MTU_SIZE; + } + if (p_tcb->eatt_tx_bearer != 0) { + mtu = gatt_eatt_chan_mtu(p_tcb->eatt_tx_bearer); + if (mtu >= GATT_DEF_BLE_MTU_SIZE) { + return mtu; + } + } + if (p_tcb->eatt_rx_bearer != 0) { + mtu = gatt_eatt_chan_mtu(p_tcb->eatt_rx_bearer); + if (mtu >= GATT_DEF_BLE_MTU_SIZE) { + return mtu; + } + } + return p_tcb->payload_size; +} + +UINT16 gatt_eatt_mtu_for_client_op(BD_ADDR bd_addr, UINT8 op_code, UINT16 legacy_mtu) +{ +#if (BLE_EATT_CLIENT_INCLUDED == TRUE) + UINT16 lcid; + UINT16 mtu; + + if (op_code == GATT_REQ_MTU || op_code == GATT_CMD_WRITE || + op_code == GATT_SIGN_CMD_WRITE || op_code == GATT_HANDLE_VALUE_CONF) { + return legacy_mtu; + } + + lcid = gatt_eatt_get_available_bearer(bd_addr, op_code); + if (lcid == L2CAP_ATT_CID) { + return legacy_mtu; + } + + mtu = gatt_eatt_chan_mtu(lcid); + if (mtu >= GATT_DEF_BLE_MTU_SIZE) { + return mtu; + } + return legacy_mtu; +#else + UNUSED(bd_addr); + UNUSED(op_code); + return legacy_mtu; +#endif +} + +static UINT16 gatt_eatt_app_conn_id_from_bda(BD_ADDR bd_addr) +{ + tGATT_TCB *p_tcb = gatt_find_tcb_by_addr(bd_addr, BT_TRANSPORT_LE); + + /* ESP exposes conn_id as tcb_idx (see BTC_GATT_GET_CONN_ID / btc_gattc.c), + * so report tcb_idx here to stay consistent with esp_ble_gattc_* events and + * with esp_ble_eatt_set_default_bearer(). */ + if (p_tcb != NULL && gatt_get_ch_state(p_tcb) == GATT_CH_OPEN) { + return p_tcb->tcb_idx; + } + /* tcb_idx is a UINT8 starting at 0, so the first BLE connection legitimately + * owns tcb_idx 0. Return the dedicated invalid sentinel (0xFFFF) for the + * not-found/not-open case so a raced setup does not report an EATT event with + * conn_id 0 that the app would misroute to the real first connection. */ + return GATT_INVALID_CONN_ID; +} + +static UINT16 gatt_eatt_stack_conn_id_from_bda(BD_ADDR bd_addr) +{ + tGATT_TCB *p_tcb = gatt_find_tcb_by_addr(bd_addr, BT_TRANSPORT_LE); + + if (p_tcb != NULL && gatt_get_ch_state(p_tcb) == GATT_CH_OPEN && s_eatt_gatt_if != 0) { + return GATT_CREATE_CONN_ID(p_tcb->tcb_idx, s_eatt_gatt_if); + } + if (p_tcb != NULL) { + GATT_EATT_TRACE_DEBUG("stack_conn_id: tcb ch_state=%u eatt_gatt_if=%u", + gatt_get_ch_state(p_tcb), s_eatt_gatt_if); + } + return 0; +} + +static void gatt_eatt_log_peer(const char *tag, BD_ADDR bd_addr) +{ + GATT_EATT_TRACE_API("%s peer %02x:%02x:%02x:%02x:%02x:%02x", + tag, + bd_addr[0], bd_addr[1], bd_addr[2], + bd_addr[3], bd_addr[4], bd_addr[5]); +} + +static void gatt_eatt_report_evt(UINT16 conn_id, UINT8 status, UINT16 cid) +{ + if (s_eatt_evt_cback) { + GATT_EATT_TRACE_DEBUG("report evt conn_id=%u status=%u cid=0x%04x", conn_id, status, cid); + (*s_eatt_evt_cback)(conn_id, status, cid); + } else { + GATT_EATT_TRACE_DEBUG("report evt dropped (no cback) conn_id=%u status=%u cid=0x%04x", + conn_id, status, cid); + } +} + +static BOOLEAN gatt_eatt_is_central(BD_ADDR bd_addr) +{ + tL2C_LCB *p_lcb = l2cu_find_lcb_by_bd_addr(bd_addr, BT_TRANSPORT_LE); + + if (p_lcb != NULL && p_lcb->link_state == LST_CONNECTED) { + return (p_lcb->link_role == HCI_ROLE_MASTER); + } + + tBTM_SEC_DEV_REC *p_dev = btm_find_dev(bd_addr); + return (p_dev != NULL && p_dev->role_master); +} + +static BOOLEAN gatt_eatt_is_encrypted(BD_ADDR bd_addr) +{ + UINT8 sec_flag = 0; + + if (!BTM_GetSecurityFlagsByTransport(bd_addr, &sec_flag, BT_TRANSPORT_LE)) { + return FALSE; + } + return (sec_flag & BTM_SEC_FLAG_ENCRYPTED) != 0; +} + +#if (BLE_EATT_CLIENT_INCLUDED == TRUE) +static void gatt_eatt_fallback_legacy(tGATT_EATT_CONN *ec) +{ + if (ec == NULL) { + return; + } + GATT_EATT_TRACE_DEBUG("EATT setup aborted, fallback to Legacy ATT (CID 4)"); + gatt_eatt_free_conn(ec); +} + +static void gatt_eatt_start_ecoc_connect(BD_ADDR bd_addr) +{ + tGATT_EATT_CONN *ec = gatt_eatt_alloc_conn(bd_addr); + tL2CAP_LE_CFG_INFO cfg = {0}; + UINT16 lcids[GATT_EATT_MAX_CHAN]; + + if (ec == NULL) { + return; + } + + ec->conn_id = gatt_eatt_app_conn_id_from_bda(bd_addr); + ec->setup_st = GATT_EATT_ST_CONNECTING; + ec->setup_target = s_eatt_chan_num; + ec->setup_done = 0; + + cfg.mtu = GATT_EATT_MTU; + cfg.mps = L2CAP_LE_COC_MPS; + + GATT_EATT_TRACE_API("ConnectLEEcocReq psm=0x%04x n=%u", GATT_EATT_PSM, s_eatt_chan_num); + { + UINT8 num_started = L2CA_ConnectLEEcocReq(GATT_EATT_PSM, bd_addr, &cfg, s_eatt_chan_num, lcids); + if (num_started == 0) { + GATT_EATT_TRACE_ERROR("ConnectLEEcocReq failed (no RCB or link not ready?)"); + /* Release the conn slot so a stale in-use ec does not linger and + * occupy a MAX_L2CAP_LINKS entry (fall back to Legacy ATT). */ + gatt_eatt_free_conn(ec); + } else { + GATT_EATT_TRACE_API("ConnectLEEcocReq started n=%u lcids=0x%04x 0x%04x 0x%04x", + num_started, lcids[0], + s_eatt_chan_num > 1 ? lcids[1] : 0, + s_eatt_chan_num > 2 ? lcids[2] : 0); + } + } +} + +static void gatt_eatt_central_write_cl_feat(UINT16 conn_id, tGATT_EATT_CONN *ec) +{ + tGATT_VALUE wr; + /* OR the EATT bit into the previously-read value instead of overwriting it, + * so bits the client already set are preserved (Core Spec v6.2 Vol 3 Part G + * 7.2 forbids clearing set bits). */ + UINT8 feat = ec->peer_cl_feat_val | BLE_GATT_CL_SUPP_FEAT_EATT_BITMASK; + + if (ec->peer_cl_feat_handle == 0) { + gatt_eatt_start_ecoc_connect(ec->peer_bda); + return; + } + + memset(&wr, 0, sizeof(wr)); + wr.handle = ec->peer_cl_feat_handle; + wr.len = 1; + wr.value[0] = feat; + ec->setup_st = GATT_EATT_ST_WRITE_CL_FEAT; + + if (GATTC_Write(conn_id, GATT_WRITE, &wr) != GATT_SUCCESS) { + GATT_EATT_TRACE_ERROR("write cl supp feat failed"); + /* Writing Client Supported Features is not a prerequisite for the ECOC + * bearers. Fall back to the connect phase (matching the read paths and + * the async WRITE_CL_FEAT error handler) instead of silently abandoning + * setup with no completion callback to advance the state machine. */ + gatt_eatt_start_ecoc_connect(ec->peer_bda); + } +} + +static void gatt_eatt_central_read_cl_feat(UINT16 conn_id, tGATT_EATT_CONN *ec) +{ + tGATT_READ_PARAM rd; + tBT_UUID uuid; + + memset(&rd, 0, sizeof(rd)); + uuid.len = LEN_UUID_16; + uuid.uu.uuid16 = GATT_UUID_CLIENT_SUP_FEAT; + rd.char_type.s_handle = 0x0001; + rd.char_type.e_handle = 0xFFFF; + rd.char_type.uuid = uuid; + rd.char_type.auth_req = GATT_AUTH_REQ_NONE; + ec->setup_st = GATT_EATT_ST_READ_CL_FEAT; + + if (GATTC_Read(conn_id, GATT_READ_BY_TYPE, &rd) != GATT_SUCCESS) { + GATT_EATT_TRACE_ERROR("read cl supp feat failed, try ecoc"); + gatt_eatt_start_ecoc_connect(ec->peer_bda); + } +} + +#if GATTS_ROBUST_CACHING_ENABLED +static void gatt_eatt_central_read_sr_feat(UINT16 conn_id, tGATT_EATT_CONN *ec) +{ + tGATT_READ_PARAM rd; + tBT_UUID uuid; + + memset(&rd, 0, sizeof(rd)); + uuid.len = LEN_UUID_16; + uuid.uu.uuid16 = GATT_UUID_SERVER_SUP_FEAT; + rd.char_type.s_handle = 0x0001; + rd.char_type.e_handle = 0xFFFF; + rd.char_type.uuid = uuid; + rd.char_type.auth_req = GATT_AUTH_REQ_NONE; + ec->setup_st = GATT_EATT_ST_READ_SR_FEAT; + + { + tGATT_STATUS st = GATTC_Read(conn_id, GATT_READ_BY_TYPE, &rd); + if (st != GATT_SUCCESS) { + GATT_EATT_TRACE_API("read sr feat queue failed status=0x%x, fallback legacy", st); + gatt_eatt_fallback_legacy(ec); + } else { + GATT_EATT_TRACE_API("read sr feat queued conn_id=0x%04x", conn_id); + } + } +} +#endif /* GATTS_ROBUST_CACHING_ENABLED */ + +static void gatt_eatt_gatt_cmpl_cback(UINT16 conn_id, tGATTC_OPTYPE op, tGATT_STATUS status, + tGATT_CL_COMPLETE *p_data) +{ + tGATT_EATT_CONN *ec; + UINT8 sr_feat = 0; + + if (op != GATTC_OPTYPE_READ && op != GATTC_OPTYPE_WRITE) { + return; + } + + ec = gatt_eatt_find_conn_for_setup(conn_id); + if (ec == NULL || ec->setup_st == GATT_EATT_ST_IDLE) { + GATT_EATT_TRACE_DEBUG("gatt_cmpl: no active setup for conn_id=0x%04x", conn_id); + return; + } + + GATT_EATT_TRACE_API("gatt_cmpl op=%u status=0x%x setup_st=%u conn_id=0x%04x", + op, status, ec->setup_st, conn_id); + + if (status != GATT_SUCCESS) { + if (ec->setup_st == GATT_EATT_ST_READ_SR_FEAT) { + GATT_EATT_TRACE_API("read sr feat failed status=0x%x, fallback legacy", status); + gatt_eatt_fallback_legacy(ec); + } else if (ec->setup_st != GATT_EATT_ST_CONNECTING) { + GATT_EATT_TRACE_DEBUG("setup GATT op=%u st=%u, try ecoc", op, ec->setup_st); + gatt_eatt_start_ecoc_connect(ec->peer_bda); + } + return; + } + + switch (ec->setup_st) { + case GATT_EATT_ST_READ_SR_FEAT: + if (p_data && p_data->att_value.len >= 1) { + sr_feat = p_data->att_value.value[0]; + } + if (sr_feat & BLE_GATT_SR_SUPP_FEAT_EATT_BITMASK) { + GATT_EATT_TRACE_API("peer supports EATT (sr_feat=0x%02x), read cl feat", sr_feat); + gatt_eatt_central_read_cl_feat(conn_id, ec); + } else { + GATT_EATT_TRACE_API("peer does not support EATT (sr_feat=0x%02x), legacy ATT", sr_feat); + gatt_eatt_free_conn(ec); + } + break; + + case GATT_EATT_ST_READ_CL_FEAT: + if (p_data) { + ec->peer_cl_feat_handle = p_data->att_value.handle; + /* Preserve any bits already set in the Client Supported Features + * value so the subsequent write does not clear them (Core Spec v6.2 + * Vol 3 Part G 7.2: a client shall not clear bits it has set, else + * the server rejects the write with Value Not Allowed 0x13). */ + if (p_data->att_value.len >= 1) { + ec->peer_cl_feat_val = p_data->att_value.value[0]; + } + } + gatt_eatt_central_write_cl_feat(conn_id, ec); + break; + + case GATT_EATT_ST_WRITE_CL_FEAT: + gatt_eatt_start_ecoc_connect(ec->peer_bda); + break; + + default: + break; + } +} +#endif /* BLE_EATT_CLIENT_INCLUDED */ + +/* The EATT module registers a GATT interface only to receive connection / + * operation-complete callbacks for its own bearer setup; it is not a GATT + * server application. Server request indications (e.g. the legacy ATT Exchange + * MTU) are fanned out to every registered interface, so provide a no-op + * request callback to absorb them instead of tripping the + * "Call back not found for application" warning in gatt_sr_send_req_callback(). + * Mirrors bta_gattc_req_cback(). */ +static void gatt_eatt_gatt_req_cback(UINT16 conn_id, UINT32 trans_id, + tGATTS_REQ_TYPE type, tGATTS_DATA *p_data) +{ + UNUSED(conn_id); + UNUSED(trans_id); + UNUSED(type); + UNUSED(p_data); +} + +static void gatt_eatt_gatt_conn_cback(tGATT_IF gatt_if, BD_ADDR bda, UINT16 conn_id, + BOOLEAN connected, tGATT_DISCONN_REASON reason, + tBT_TRANSPORT transport) +{ + UNUSED(gatt_if); + UNUSED(conn_id); + UNUSED(reason); + + /* EATT state is BLE-only. A BR/EDR GATT disconnect for the same BDA must not + * tear down the BLE EATT connection (find_conn_by_bda matches on BDA only). */ + if (transport != BT_TRANSPORT_LE) { + return; + } + + if (!connected) { + tGATT_EATT_CONN *ec = gatt_eatt_find_conn_by_bda(bda); + if (ec) { + GATT_EATT_TRACE_DEBUG("gatt disconnect, free_conn conn_id=%u reason=0x%x", conn_id, reason); + gatt_eatt_free_conn(ec); + } + } +} + +#if (BLE_EATT_SERVER_INCLUDED == TRUE) +static void gatt_eatt_release_empty_conn(tGATT_EATT_CONN *ec) +{ + if (ec != NULL && ec->bearer_count == 0) { + gatt_eatt_free_conn(ec); + } +} + +static void gatt_eatt_connect_ind(BD_ADDR bd_addr, UINT16 lcid, UINT16 psm, UINT8 id) +{ + tL2CAP_LE_CFG_INFO cfg = {0}; + tGATT_EATT_CONN *ec; + tGATT_EATT_BEARER *bearer; + + UNUSED(psm); + + if (!gatt_eatt_is_encrypted(bd_addr)) { + GATT_EATT_TRACE_API("connect_ind: reject unencrypted lcid=0x%04x", lcid); + /* Per Core Spec v6.2 10.1/10.2, reject due to missing security with the + * insufficient-encryption result, not a generic no-resources code, so + * the peer knows to encrypt/pair rather than retry. */ + L2CA_ConnectLECocRsp(bd_addr, id, lcid, L2CAP_LE_RESULT_INSUFFICIENT_ENCRY, 0, NULL); + return; + } + + gatt_eatt_log_peer("connect_ind accept", bd_addr); + + ec = gatt_eatt_alloc_conn(bd_addr); + if (ec == NULL) { + GATT_EATT_TRACE_ERROR("connect_ind: no conn slot, reject lcid=0x%04x", lcid); + L2CA_ConnectLECocRsp(bd_addr, id, lcid, L2CAP_LE_RESULT_NO_RESOURCES, 0, NULL); + return; + } + + ec->conn_id = gatt_eatt_app_conn_id_from_bda(bd_addr); + bearer = gatt_eatt_alloc_bearer(ec, lcid); + if (bearer == NULL) { + /* No free bearer slot: reject so the L2CAP channel is not left up while + * GATT does not track it (which would break TX routing and reporting). */ + GATT_EATT_TRACE_ERROR("connect_ind: no bearer slot lcid=0x%04x", lcid); + L2CA_ConnectLECocRsp(bd_addr, id, lcid, L2CAP_LE_RESULT_NO_RESOURCES, 0, NULL); + gatt_eatt_release_empty_conn(ec); + return; + } + + cfg.mtu = GATT_EATT_MTU; + cfg.mps = L2CAP_LE_COC_MPS; + /* Grant the full initial RX credit window (L2CAP_LE_INIT_CREDITS) instead of + * a single credit. A window of 1 forces the peer into a one-K-frame-at-a-time + * ping-pong (send frame -> wait for credit return), which throttles the + * central -> peripheral GATT request throughput on EATT bearers. Leaving + * credits at 0 would let l2c_ble_ecfc_apply_default_cfg() fill the same + * value; set it explicitly for clarity and symmetry with the central path. */ + cfg.credits = L2CAP_LE_INIT_CREDITS; + if (!L2CA_ConnectLECocRsp(bd_addr, id, lcid, L2CAP_CONN_OK, 0, &cfg)) { + GATT_EATT_TRACE_ERROR("connect_ind: ConnectLECocRsp failed lcid=0x%04x", lcid); + gatt_eatt_free_bearer(ec, bearer); + gatt_eatt_release_empty_conn(ec); + (void)L2CA_LECocDisconnect(lcid); + return; + } + GATT_EATT_TRACE_DEBUG("connect_ind accepted lcid=0x%04x conn_id=%u", lcid, ec->conn_id); + gatt_eatt_update_tcb_mtu(bd_addr, lcid); +} +#endif /* BLE_EATT_SERVER_INCLUDED */ + +/* Account for one completed EATT setup response (success or failure) on a + * client-initiated setup, and release the setup state once every requested + * bearer has been answered. Without this, a partial failure (peer accepts + * fewer bearers than requested) would wedge setup_st at CONNECTING and block + * any future EATT setup on this connection. Server-side (setup_st != CONNECTING) + * is unaffected. */ +static void gatt_eatt_setup_mark_done(tGATT_EATT_CONN *ec) +{ + if (ec == NULL || ec->setup_st != GATT_EATT_ST_CONNECTING) { + return; + } + if (ec->setup_done < 0xFF) { + ec->setup_done++; + } + if (ec->bearer_count >= s_eatt_chan_num || + (ec->setup_target != 0 && ec->setup_done >= ec->setup_target)) { + ec->setup_st = GATT_EATT_ST_IDLE; + } +} + +/* Invoked from l2c_ble_le_coc_open_channel() for both originator and acceptor + * paths; do not trim with BLE_EATT_CLIENT_INCLUDED. */ +static void gatt_eatt_connect_cfm(UINT16 lcid, UINT16 result) +{ + tL2C_CCB *p_ccb = l2cu_find_ccb_by_cid(NULL, lcid); + tGATT_EATT_CONN *ec; + tGATT_EATT_BEARER *b; + + if (p_ccb == NULL || p_ccb->p_lcb == NULL) { + GATT_EATT_TRACE_ERROR("connect_cfm: no ccb/lcb lcid=0x%04x result=%u", lcid, result); + return; + } + + if (result != L2CAP_LE_RESULT_CONN_OK) { + /* Failure path: only clean up an EXISTING conn. Do not allocate one here. + * If the original EATT conn was already freed (e.g. a GATT disconnect + * raced ahead of this CoC failure callback), gatt_eatt_alloc_conn would + * create a fresh empty conn (no bearers, setup_st IDLE) that nothing ever + * frees, leaking a MAX_L2CAP_LINKS slot. */ + ec = gatt_eatt_find_conn_by_bda(p_ccb->p_lcb->remote_bd_addr); + if (ec == NULL) { + GATT_EATT_TRACE_DEBUG("connect_cfm fail, conn gone lcid=0x%04x result=%u", lcid, result); + return; + } + GATT_EATT_TRACE_ERROR("bearer connect failed lcid=0x%04x result=%u", lcid, result); + b = gatt_eatt_find_bearer(ec, lcid); + if (b) { + gatt_eatt_free_bearer(ec, b); + } + gatt_eatt_setup_mark_done(ec); + return; + } + + /* Only look up the EXISTING conn; do not allocate here. In both the client + * (gatt_eatt_start_ecoc_connect) and server (gatt_eatt_connect_ind) flows the + * conn is allocated before connect_cfm arrives, so find_conn_by_bda succeeds. + * If it is NULL the original conn was already freed by a racing GATT + * disconnect; allocating a fresh one here would create an orphan conn (no + * GATT conn cback to ever free it) and leak a MAX_L2CAP_LINKS slot. Tear the + * L2CAP channel down instead, mirroring the failure path (696-712) and the + * no-bearer-slot path below. */ + ec = gatt_eatt_find_conn_by_bda(p_ccb->p_lcb->remote_bd_addr); + if (ec == NULL) { + GATT_EATT_TRACE_ERROR("connect_cfm ok but conn gone, disconnect lcid=0x%04x", lcid); + L2CA_LECocDisconnect(lcid); + return; + } + ec->conn_id = gatt_eatt_app_conn_id_from_bda(p_ccb->p_lcb->remote_bd_addr); + + b = gatt_eatt_alloc_bearer(ec, lcid); + if (b == NULL) { + /* No free bearer slot: tear down the L2CAP channel instead of leaving it + * up untracked by GATT. */ + GATT_EATT_TRACE_ERROR("connect_cfm: no bearer slot lcid=0x%04x, disconnecting", lcid); + gatt_eatt_setup_mark_done(ec); + L2CA_LECocDisconnect(lcid); + return; + } + + GATT_EATT_TRACE_API("bearer connected lcid=0x%04x conn_id=%u (%u/%u)", + lcid, ec->conn_id, ec->bearer_count, s_eatt_chan_num); + gatt_eatt_update_tcb_mtu(p_ccb->p_lcb->remote_bd_addr, lcid); + b->reported = TRUE; + gatt_eatt_report_evt(ec->conn_id, 0, lcid); + + gatt_eatt_setup_mark_done(ec); +} + +static void gatt_eatt_disconnect_ind(UINT16 lcid, BOOLEAN local_init) +{ + tL2C_CCB *p_ccb = l2cu_find_ccb_by_cid(NULL, lcid); + tGATT_EATT_CONN *ec; + tGATT_EATT_BEARER *b; + + UNUSED(local_init); + + GATT_EATT_TRACE_DEBUG("disconnect_ind lcid=0x%04x local_init=%u", lcid, local_init); + + if (p_ccb == NULL || p_ccb->p_lcb == NULL) { + GATT_EATT_TRACE_ERROR("disconnect_ind: no ccb/lcb lcid=0x%04x", lcid); + return; + } + + ec = gatt_eatt_find_conn_by_bda(p_ccb->p_lcb->remote_bd_addr); + if (ec == NULL) { + GATT_EATT_TRACE_DEBUG("disconnect_ind: no conn for lcid=0x%04x", lcid); + return; + } + + b = gatt_eatt_find_bearer(ec, lcid); + if (b == NULL) { + GATT_EATT_TRACE_DEBUG("disconnect_ind: no bearer for lcid=0x%04x", lcid); + } + if (b) { +#if (GATTC_INCLUDED == TRUE) + if (b->client_op != 0 && b->clcb_idx != 0) { + tGATT_CLCB *p_clcb = gatt_clcb_find_by_idx(b->clcb_idx); + + if (p_clcb != NULL) { + btu_stop_timer(&p_clcb->rsp_timer_ent); + gatt_end_operation(p_clcb, GATT_ERROR, NULL); + } + } +#endif + gatt_eatt_free_bearer(ec, b); + } +} + +static void gatt_eatt_congestion(UINT16 lcid, BOOLEAN congested) +{ + /* Deliberately a no-op on EATT decongestion. EATT client commands are never + * queued in cl_cmd_q: attp_cl_send_cmd() sends them straight to L2CAP, which + * owns their credit-based flow control. cl_cmd_q only holds legacy commands + * bound to the ATT fixed channel, and its resend is driven solely by the ATT + * fixed-channel congestion path (gatt_channel_congestion) and the response + * handler (gatt_client_handle_server_rsp). Calling gatt_cl_send_next_cmd_inq + * here would flush that legacy queue onto the ATT fixed channel with + * eatt_tx/rx_bearer == 0; if that channel were still congested the commands + * would be dropped as GATT_BUSY (permanently dequeued + failed). */ + UNUSED(lcid); + UNUSED(congested); +} + +static void gatt_eatt_data_ind_l2c(UINT16 lcid, BT_HDR *p_buf) +{ + gatt_eatt_data_ind(lcid, p_buf); +} + +void gatt_eatt_init(void) +{ + tBT_UUID app_uuid = {LEN_UUID_16, {UUID_SERVCLASS_GATT_SERVER}}; + static const tGATT_CBACK s_eatt_gatt_cback = { + gatt_eatt_gatt_conn_cback, +#if (BLE_EATT_CLIENT_INCLUDED == TRUE) + gatt_eatt_gatt_cmpl_cback, +#else + NULL, +#endif + NULL, /* p_disc_res_cb */ + NULL, /* p_disc_cmpl_cb */ + gatt_eatt_gatt_req_cback, /* p_req_cb: no-op, absorbs server req fan-out */ + NULL, /* p_enc_cmpl_cb */ + NULL, /* p_congestion_cb */ + }; + + memset(s_eatt_conn, 0, sizeof(s_eatt_conn)); + memset(&s_eatt_l2cap_appl, 0, sizeof(s_eatt_l2cap_appl)); + +#if (BLE_EATT_SERVER_INCLUDED == TRUE) + s_eatt_l2cap_appl.pL2CA_ConnectInd_Cb = gatt_eatt_connect_ind; +#endif + s_eatt_l2cap_appl.pL2CA_ConnectCfm_Cb = gatt_eatt_connect_cfm; + s_eatt_l2cap_appl.pL2CA_DisconnectInd_Cb = gatt_eatt_disconnect_ind; + s_eatt_l2cap_appl.pL2CA_DataInd_Cb = gatt_eatt_data_ind_l2c; + s_eatt_l2cap_appl.pL2CA_CongestionStatus_Cb = gatt_eatt_congestion; + + s_eatt_reg_psm = L2CA_RegisterLECoc(GATT_EATT_PSM, &s_eatt_l2cap_appl); + if (s_eatt_reg_psm == 0) { + /* Bail out before GATT_Register so a failed EATT init does not consume a + * scarce GATT_MAX_APPS slot. With no PSM, gatt_eatt_on_encrypted() also + * returns early, so nothing allocates an EATT conn we could not free. */ + GATT_EATT_TRACE_ERROR("RegisterLECoc PSM 0x%04x failed, EATT disabled", GATT_EATT_PSM); + return; + } + GATT_EATT_TRACE_DEBUG("RegisterLECoc success reg_psm=0x%04x", s_eatt_reg_psm); + + BTM_SetSecurityLevel(TRUE, "GATT_EATT", BTM_SEC_SERVICE_GEN_NET, + BTM_SEC_NONE, s_eatt_reg_psm, BTM_SEC_PROTO_L2CAP, 0); + BTM_SetSecurityLevel(FALSE, "GATT_EATT", BTM_SEC_SERVICE_GEN_NET, + BTM_SEC_NONE, s_eatt_reg_psm, BTM_SEC_PROTO_L2CAP, 0); + GATT_EATT_TRACE_API("init BTM_SetSecurityLevel for PSM 0x%04x", s_eatt_reg_psm); + + s_eatt_gatt_if = GATT_Register(&app_uuid, &s_eatt_gatt_cback); + if (s_eatt_gatt_if != 0) { + GATT_EATT_TRACE_DEBUG("GATT_Register success gatt_if=%u", s_eatt_gatt_if); + GATT_StartIf(s_eatt_gatt_if); + } else { + GATT_EATT_TRACE_ERROR("GATT_Register failed for EATT module"); + /* Roll back the L2CAP PSM registration. Without a GATT interface the + * gatt_eatt_gatt_conn_cback that frees tGATT_EATT_CONN on BLE disconnect + * is never registered, so leaving the PSM (and its still-live callbacks) + * up would let the server accept EATT connections it can never clean up, + * leaking an s_eatt_conn[] slot per peer. */ + L2CA_DeregisterLECoc(s_eatt_reg_psm); + s_eatt_reg_psm = 0; + } + +#if GATTS_ROBUST_CACHING_ENABLED + if (s_eatt_reg_psm != 0) { + gatt_cb.gatt_sr_supported_feat_mask |= BLE_GATT_SR_SUPP_FEAT_EATT_BITMASK; + } +#endif + + GATT_EATT_TRACE_API("init reg_psm=0x%04x eatt_gatt_if=%u chan_num=%u mtu=%u robust_caching=%d", + s_eatt_reg_psm, s_eatt_gatt_if, s_eatt_chan_num, GATT_EATT_MTU, + GATTS_ROBUST_CACHING_ENABLED); +} + +void gatt_eatt_deinit(void) +{ + tGATT_EATT_EVT_CBACK *saved_cback = s_eatt_evt_cback; + + GATT_EATT_TRACE_DEBUG("deinit start reg_psm=0x%04x gatt_if=%u", s_eatt_reg_psm, s_eatt_gatt_if); + + /* Suppress app callbacks while tearing down bearers during stack disable. */ + s_eatt_evt_cback = NULL; + + /* Explicitly disconnect every EATT L2CAP channel first. gatt_eatt_free_bearer + * only clears internal state, and the L2CA_DeregisterLECoc call below walks + * each link but disconnects only the first CCB in its queue. With multiple + * bearers per link the rest would be orphaned (left open until the ACL drops). + * Send the disconnect here, before free_conn wipes the bearer LCIDs. */ + for (int i = 0; i < MAX_L2CAP_LINKS; i++) { + if (!s_eatt_conn[i].in_use) { + continue; + } + GATT_EATT_TRACE_DEBUG("deinit disconnect bearers for conn_id=%u count=%u", + s_eatt_conn[i].conn_id, s_eatt_conn[i].bearer_count); + for (int j = 0; j < GATT_EATT_MAX_CHAN; j++) { + if (s_eatt_conn[i].bearers[j].in_use) { + L2CA_LECocDisconnect(s_eatt_conn[i].bearers[j].lcid); + } + } + } + + for (int i = 0; i < MAX_L2CAP_LINKS; i++) { + if (s_eatt_conn[i].in_use) { + gatt_eatt_free_conn(&s_eatt_conn[i]); + } + } + + if (s_eatt_reg_psm != 0) { + GATT_EATT_TRACE_DEBUG("deinit DeregisterLECoc reg_psm=0x%04x", s_eatt_reg_psm); + L2CA_DeregisterLECoc(s_eatt_reg_psm); + s_eatt_reg_psm = 0; + } + + /* Mirror the GATT_Register/GATT_StartIf done in init: GATT_Deregister stops + * CLCB response timers, frees CLCBs, updates link-use flags and releases the + * cl_rcb slot. Skipping it would leak the registration slot and could leave a + * timer referencing a CLCB later freed by gatt_free(). */ + if (s_eatt_gatt_if != 0) { + GATT_EATT_TRACE_DEBUG("deinit GATT_Deregister gatt_if=%u", s_eatt_gatt_if); + GATT_Deregister(s_eatt_gatt_if); + s_eatt_gatt_if = 0; + } + +#if GATTS_ROBUST_CACHING_ENABLED + gatt_cb.gatt_sr_supported_feat_mask &= ~BLE_GATT_SR_SUPP_FEAT_EATT_BITMASK; +#endif + + UNUSED(saved_cback); + memset(&s_eatt_l2cap_appl, 0, sizeof(s_eatt_l2cap_appl)); + GATT_EATT_TRACE_DEBUG("deinit done"); +} + +void gatt_eatt_register_evt_cback(tGATT_EATT_EVT_CBACK *p_cback) +{ + s_eatt_evt_cback = p_cback; +} + +void gatt_eatt_set_chan_num(UINT8 num_chan) +{ + if (num_chan > 0 && num_chan <= GATT_EATT_MAX_CHAN) { + s_eatt_chan_num = num_chan; + } +} + +void gatt_eatt_on_encrypted(BD_ADDR bd_addr) +{ + tGATT_EATT_CONN *ec; + UINT16 stack_conn_id; + tL2C_LCB *p_lcb; + tBTM_SEC_DEV_REC *p_dev; + BOOLEAN is_central; + + gatt_eatt_log_peer("on_encrypted", bd_addr); + + if (!gatt_eatt_is_encrypted(bd_addr)) { + GATT_EATT_TRACE_API("on_encrypted: link not encrypted yet, skip"); + return; + } + + /* EATT is not operational unless its L2CAP PSM was registered in + * gatt_eatt_init(). Without it no bearer can ever be set up, and the GATT + * conn cback that frees tGATT_EATT_CONN on disconnect may not be registered + * either, so allocating a conn here would only leak an s_eatt_conn[] slot. */ + if (s_eatt_reg_psm == 0) { + GATT_EATT_TRACE_API("on_encrypted: EATT PSM not registered, skip"); + return; + } + + ec = gatt_eatt_find_conn_by_bda(bd_addr); + if (ec != NULL && (ec->bearer_count > 0 || ec->setup_st != GATT_EATT_ST_IDLE)) { + /* Skip if bearers are already up OR a setup is in progress, so a repeated + * encryption-complete notification cannot start a duplicate EATT setup. */ + GATT_EATT_TRACE_API("on_encrypted: setup in progress or %u bearers up, skip", + ec->bearer_count); + return; + } + + ec = gatt_eatt_alloc_conn(bd_addr); + if (ec == NULL) { + GATT_EATT_TRACE_ERROR("on_encrypted: alloc ec failed"); + return; + } + + ec->conn_id = gatt_eatt_app_conn_id_from_bda(bd_addr); + if (ec->conn_id == GATT_INVALID_CONN_ID) { + GATT_EATT_TRACE_ERROR("on_encrypted: conn_id invalid conn_id=0x%04x", ec->conn_id); + } + stack_conn_id = gatt_eatt_stack_conn_id_from_bda(bd_addr); + is_central = gatt_eatt_is_central(bd_addr); + + p_lcb = l2cu_find_lcb_by_bd_addr(bd_addr, BT_TRANSPORT_LE); + p_dev = btm_find_dev(bd_addr); + GATT_EATT_TRACE_DEBUG("on_encrypted: central=%u app_conn_id=%u stack_conn_id=0x%04x " + "eatt_gatt_if=%u lcb=%p state=%u role=%u btm_role_master=%u", + is_central, ec->conn_id, stack_conn_id, s_eatt_gatt_if, + (void *)p_lcb, + p_lcb ? p_lcb->link_state : 0xFF, + p_lcb ? p_lcb->link_role : 0xFF, + (p_dev && p_dev->role_master) ? 1 : 0); + + if (is_central) { +#if (BLE_EATT_CLIENT_INCLUDED == TRUE) +#if GATTS_ROBUST_CACHING_ENABLED + if (stack_conn_id != 0) { + gatt_eatt_central_read_sr_feat(stack_conn_id, ec); + } else { + GATT_EATT_TRACE_ERROR("on_encrypted: no stack conn_id, fallback legacy"); + gatt_eatt_fallback_legacy(ec); + } +#else + GATT_EATT_TRACE_API("on_encrypted: robust caching off, start ecoc directly"); + gatt_eatt_start_ecoc_connect(bd_addr); +#endif +#else + GATT_EATT_TRACE_API("on_encrypted: central but GATTC disabled, skip EATT setup"); + gatt_eatt_free_conn(ec); +#endif + } else { +#if (BLE_EATT_SERVER_INCLUDED == TRUE) + GATT_EATT_TRACE_API("on_encrypted: peripheral, wait central ECFC on PSM 0x%04x", + GATT_EATT_PSM); +#else + /* No ConnectInd cb is registered when the EATT server is disabled, so this + * pre-allocated conn can never receive an incoming bearer; free it instead + * of holding an s_eatt_conn[] slot until GATT disconnect. */ + gatt_eatt_free_conn(ec); +#endif + } +} + +BOOLEAN gatt_eatt_is_bearer(UINT16 lcid) +{ + tL2C_CCB *p_ccb = l2cu_find_ccb_by_cid(NULL, lcid); + if (p_ccb == NULL || p_ccb->p_lcb == NULL || p_ccb->p_rcb == NULL) { + return FALSE; + } + return (p_ccb->p_rcb->real_psm == GATT_EATT_PSM && p_ccb->le_coc_active); +} + +UINT16 gatt_eatt_get_available_bearer(BD_ADDR bd_addr, UINT8 op) +{ +#if (BLE_EATT_CLIENT_INCLUDED == TRUE) + tGATT_EATT_CONN *ec = gatt_eatt_find_conn_by_bda(bd_addr); + tGATT_EATT_BEARER *b; + + UNUSED(op); + + if (ec == NULL || ec->bearer_count == 0) { + return L2CAP_ATT_CID; + } + + if (ec->default_lcid != 0) { + b = gatt_eatt_find_bearer(ec, ec->default_lcid); + if (b != NULL && b->client_op == 0) { + GATT_EATT_TRACE_DEBUG("get_available_bearer op=0x%02x -> default lcid=0x%04x", op, ec->default_lcid); + return ec->default_lcid; + } + } + + for (int i = 0; i < GATT_EATT_MAX_CHAN; i++) { + if (ec->bearers[i].in_use && ec->bearers[i].client_op == 0) { + GATT_EATT_TRACE_DEBUG("get_available_bearer op=0x%02x -> lcid=0x%04x", op, ec->bearers[i].lcid); + return ec->bearers[i].lcid; + } + } + GATT_EATT_TRACE_DEBUG("no free EATT bearer op=0x%02x (%u busy), use ATT CID", op, ec->bearer_count); +#endif + UNUSED(bd_addr); + UNUSED(op); + return L2CAP_ATT_CID; +} + +#if (BLE_EATT_SERVER_INCLUDED == TRUE) +UINT16 gatt_eatt_get_server_tx_bearer(BD_ADDR bd_addr) +{ + tGATT_EATT_CONN *ec = gatt_eatt_find_conn_by_bda(bd_addr); + + if (ec == NULL || ec->bearer_count == 0) { + return L2CAP_ATT_CID; + } + + /* Honor an app-selected default bearer when present. */ + if (ec->default_lcid != 0 && + gatt_eatt_find_bearer(ec, ec->default_lcid) != NULL) { + GATT_EATT_TRACE_DEBUG("server_tx_bearer -> default lcid=0x%04x", ec->default_lcid); + return ec->default_lcid; + } + + /* Round-robin across in-use bearers so server-initiated notifications and + * indications are spread over all EATT channels rather than serialized on + * a single one. */ + for (int n = 0; n < GATT_EATT_MAX_CHAN; n++) { + int i = (ec->tx_rr + n) % GATT_EATT_MAX_CHAN; + if (ec->bearers[i].in_use) { + ec->tx_rr = (UINT8)((i + 1) % GATT_EATT_MAX_CHAN); + GATT_EATT_TRACE_DEBUG("server_tx_bearer -> rr lcid=0x%04x", ec->bearers[i].lcid); + return ec->bearers[i].lcid; + } + } + + return L2CAP_ATT_CID; +} +#endif /* BLE_EATT_SERVER_INCLUDED */ + +BOOLEAN gatt_eatt_set_default_bearer(UINT16 conn_id, UINT16 lcid) +{ +#if (BLE_EATT_CLIENT_INCLUDED == TRUE) + /* conn_id here is the application-level id, which ESP exposes as the raw + * tcb_idx (see gatt_eatt_app_conn_id_from_bda / esp_ble_eatt_set_default_bearer). + * Do NOT apply GATT_GET_TCB_IDX (a >>8 for stack-layer conn_ids), which would + * evaluate to 0 for every application conn_id. */ + tGATT_TCB *p_tcb = gatt_get_tcb_by_idx((UINT8)conn_id); + tGATT_EATT_CONN *ec; + tGATT_EATT_BEARER *b; + + if (p_tcb == NULL) { + GATT_EATT_TRACE_ERROR("set_default_bearer: no tcb for conn_id=%u", conn_id); + return FALSE; + } + + ec = gatt_eatt_find_conn_by_bda(p_tcb->peer_bda); + if (ec == NULL) { + GATT_EATT_TRACE_ERROR("set_default_bearer: no EATT conn for conn_id=%u", conn_id); + return FALSE; + } + + if (lcid == 0 || lcid == L2CAP_ATT_CID) { + GATT_EATT_TRACE_DEBUG("set_default_bearer: clear default for conn_id=%u", conn_id); + ec->default_lcid = 0; + return TRUE; + } + + b = gatt_eatt_find_bearer(ec, lcid); + if (b == NULL) { + GATT_EATT_TRACE_ERROR("set_default_bearer: lcid=0x%04x not an EATT bearer of conn_id=%u", lcid, conn_id); + return FALSE; + } + + GATT_EATT_TRACE_DEBUG("set_default_bearer conn_id=%u lcid=0x%04x", conn_id, lcid); + ec->default_lcid = lcid; + return TRUE; +#else + UNUSED(conn_id); + UNUSED(lcid); + return FALSE; +#endif +} + +BOOLEAN gatt_eatt_mark_busy(BD_ADDR bd_addr, UINT16 lcid, UINT8 op, UINT16 clcb_idx) +{ +#if (BLE_EATT_CLIENT_INCLUDED == TRUE) + tGATT_EATT_BEARER *b; + tGATT_EATT_CONN *ec = gatt_eatt_find_conn_by_bda(bd_addr); + + if (ec == NULL) { + return FALSE; + } + b = gatt_eatt_find_bearer(ec, lcid); + if (b == NULL || b->client_op != 0) { + return FALSE; + } + b->client_op = op; + b->clcb_idx = clcb_idx; + return TRUE; +#else + UNUSED(bd_addr); + UNUSED(lcid); + UNUSED(op); + UNUSED(clcb_idx); + return FALSE; +#endif +} + +BOOLEAN gatt_eatt_release_bearer(BD_ADDR bd_addr, UINT16 lcid, UINT8 *p_op, UINT16 *p_clcb_idx) +{ +#if (BLE_EATT_CLIENT_INCLUDED == TRUE) + tGATT_EATT_BEARER *b; + tGATT_EATT_CONN *ec = gatt_eatt_find_conn_by_bda(bd_addr); + + if (ec == NULL) { + return FALSE; + } + b = gatt_eatt_find_bearer(ec, lcid); + if (b == NULL || b->client_op == 0) { + return FALSE; + } + if (p_op) { + *p_op = b->client_op; + } + if (p_clcb_idx) { + *p_clcb_idx = b->clcb_idx; + } + b->client_op = 0; + b->clcb_idx = 0; + return TRUE; +#else + UNUSED(bd_addr); + UNUSED(lcid); + UNUSED(p_op); + UNUSED(p_clcb_idx); + return FALSE; +#endif +} + +BOOLEAN gatt_eatt_release_bearer_by_clcb(BD_ADDR bd_addr, UINT16 clcb_idx) +{ +#if (BLE_EATT_CLIENT_INCLUDED == TRUE) + tGATT_EATT_CONN *ec = gatt_eatt_find_conn_by_bda(bd_addr); + int i; + + if (ec == NULL || clcb_idx == 0) { + return FALSE; + } + /* bearers[] is a sparse slot array: freeing a bearer only decrements + * bearer_count without compacting, so an in-use bearer may live at any + * index. Iterate the full slot range (matches every other loop here). */ + for (i = 0; i < GATT_EATT_MAX_CHAN; i++) { + tGATT_EATT_BEARER *b = &ec->bearers[i]; + if (b->in_use && b->client_op != 0 && b->clcb_idx == clcb_idx) { + b->client_op = 0; + b->clcb_idx = 0; + return TRUE; + } + } + return FALSE; +#else + UNUSED(bd_addr); + UNUSED(clcb_idx); + return FALSE; +#endif +} + +void gatt_eatt_data_ind(UINT16 lcid, BT_HDR *p_buf) +{ + tL2C_CCB *p_ccb; + tGATT_TCB *p_tcb; + UINT8 *p; + UINT8 op_code; + + if (p_buf == NULL) { + return; + } + + p_ccb = l2cu_find_ccb_by_cid(NULL, lcid); + if (p_ccb == NULL || p_ccb->p_lcb == NULL) { + osi_free(p_buf); + return; + } + + if (!gatt_eatt_is_encrypted(p_ccb->p_lcb->remote_bd_addr)) { + GATT_EATT_TRACE_ERROR("ATT PDU on EATT before encryption, disconnect"); + L2CA_LECocDisconnect(lcid); + osi_free(p_buf); + return; + } + + if (p_buf->len < 1) { + L2CA_LECocDisconnect(lcid); + osi_free(p_buf); + return; + } + + p = (UINT8 *)(p_buf + 1) + p_buf->offset; + STREAM_TO_UINT8(op_code, p); + if (!gatt_is_valid_att_opcode(op_code)) { + GATT_EATT_TRACE_ERROR("invalid ATT opcode 0x%02x on EATT, disconnect", op_code); + L2CA_LECocDisconnect(lcid); + osi_free(p_buf); + return; + } + + if (op_code == GATT_SIGN_CMD_WRITE) { + GATT_EATT_TRACE_ERROR("signed write on EATT bearer, disconnect lcid=0x%04x", lcid); + L2CA_LECocDisconnect(lcid); + osi_free(p_buf); + return; + } + + p_tcb = gatt_find_tcb_by_addr(p_ccb->p_lcb->remote_bd_addr, BT_TRANSPORT_LE); + if (p_tcb == NULL || gatt_get_ch_state(p_tcb) < GATT_CH_OPEN) { + osi_free(p_buf); + return; + } + + p_tcb->eatt_rx_bearer = lcid; + + if ((op_code % 2) != 0) { +#if (GATTC_INCLUDED == TRUE) + /* ATT response on client bearer */ + gatt_client_handle_server_rsp(p_tcb, op_code, p_buf->len - 1, p, lcid); + osi_free(p_buf); +#else + GATT_EATT_TRACE_ERROR("ATT response on EATT but GATTC disabled, disconnect"); + L2CA_LECocDisconnect(lcid); + osi_free(p_buf); + p_tcb->eatt_rx_bearer = 0; + return; +#endif + } else { + /* Client->server request on an EATT bearer. Record the bearer so a + * deferred (GATT_PENDING) app response is routed back to it once + * eatt_rx_bearer is cleared below. Cleared on gatt_dequeue_sr_cmd. */ +#if (GATTS_INCLUDED == TRUE) + /* Only record the routing hint when the sr_cmd slot is free. If a prior + * request is still pending (op_code != 0), gatt_server_handle_client_req + * discards this PDU without touching sr_cmd, so overwriting eatt_lcid here + * would misroute the already-pending response to this bearer. */ + BOOLEAN sr_cmd_was_empty = (p_tcb->sr_cmd.op_code == 0); + if (sr_cmd_was_empty) { + p_tcb->sr_cmd.eatt_lcid = lcid; + } + gatt_data_process(p_tcb, p_buf); + /* If the request was fully handled synchronously (or needed no response, + * e.g. a write command), no sr_cmd is pending (op_code == 0). Clear the + * routing hint so it cannot misroute a later, unrelated response. */ + if (sr_cmd_was_empty && p_tcb->sr_cmd.op_code == 0) { + p_tcb->sr_cmd.eatt_lcid = 0; + } +#else + gatt_data_process(p_tcb, p_buf); +#endif + } + + p_tcb->eatt_rx_bearer = 0; + /* EATT bearers run in auto-credit mode, where l2c_ble_le_coc_data_ind() has + * already returned the RX credit for this K-frame. This call is therefore a + * no-op today (l2c_ble_le_coc_give_credits() ignores it while auto-credit is + * on); it is kept only as a safety net should EATT ever switch to manual + * credit management. */ + L2CA_LECocGiveCredits(lcid, 1); +} + +#endif /* BLE_EATT_INCLUDED == TRUE */ diff --git a/components/bt/host/bluedroid/stack/gatt/gatt_main.c b/components/bt/host/bluedroid/stack/gatt/gatt_main.c index 5d57534f6ca..ecf4f3bcb81 100644 --- a/components/bt/host/bluedroid/stack/gatt/gatt_main.c +++ b/components/bt/host/bluedroid/stack/gatt/gatt_main.c @@ -28,6 +28,9 @@ #include "gatt_int.h" #include "stack/l2c_api.h" +#if (BLE_EATT_INCLUDED == TRUE) +#include "gatt_eatt_int.h" +#endif #include "btm_int.h" #include "btm_ble_int.h" #include "osi/allocator.h" @@ -149,6 +152,10 @@ void gatt_init (void) #endif ///GATTS_INCLUDED == TRUE //init local MTU size gatt_default.local_mtu = GATT_MAX_MTU_SIZE; + +#if (BLE_EATT_INCLUDED == TRUE) + gatt_eatt_init(); +#endif } @@ -172,6 +179,11 @@ void gatt_free(void) gatt_cb.pending_new_srv_start_q = NULL; #endif // (GATTS_INCLUDED == TRUE) + /* Note: gatt_eatt_deinit() is intentionally invoked from btu_free_core() + * BEFORE l2c_free(), because it deregisters L2CAP/GATT resources that + * require live L2CAP state. Calling it here (gatt_free runs after l2c_free) + * would dereference the already-freed l2c_cb_ptr. */ + list_node_t *p_node = NULL; tGATT_TCB *p_tcb = NULL; for(p_node = list_begin(gatt_cb.p_tcb_list); p_node; p_node = list_next(p_node)) { @@ -986,7 +998,7 @@ static void gatt_send_conn_cback(tGATT_TCB *p_tcb) void gatt_data_process (tGATT_TCB *p_tcb, BT_HDR *p_buf) { UINT8 *p = (UINT8 *)(p_buf + 1) + p_buf->offset; - UINT8 op_code, pseudo_op_code; + UINT8 op_code; #if (GATTS_INCLUDED == TRUE) || (GATTC_INCLUDED == TRUE) UINT16 msg_len; #endif ///(GATTS_INCLUDED == TRUE) || (GATTC_INCLUDED == TRUE) @@ -998,10 +1010,7 @@ void gatt_data_process (tGATT_TCB *p_tcb, BT_HDR *p_buf) #endif ///(GATTS_INCLUDED == TRUE) || (GATTC_INCLUDED == TRUE) STREAM_TO_UINT8(op_code, p); - /* remove the two MSBs associated with sign write and write cmd */ - pseudo_op_code = op_code & (~GATT_WRITE_CMD_MASK); - - if (pseudo_op_code < GATT_OP_CODE_MAX) { + if (gatt_is_valid_att_opcode(op_code)) { #if (GATTS_INCLUDED == TRUE) || (GATTC_INCLUDED == TRUE) GATT_TRACE_DEBUG("%s opcode=%x msg_len=%u", __func__, op_code, msg_len); #endif ///(GATTS_INCLUDED == TRUE) || (GATTC_INCLUDED == TRUE) @@ -1017,7 +1026,7 @@ void gatt_data_process (tGATT_TCB *p_tcb, BT_HDR *p_buf) #endif ///GATTS_INCLUDED == TRUE } else { #if (GATTC_INCLUDED == TRUE) - gatt_client_handle_server_rsp (p_tcb, op_code, msg_len, p); + gatt_client_handle_server_rsp (p_tcb, op_code, msg_len, p, 0); #endif ///GATTC_INCLUDED == TRUE } } diff --git a/components/bt/host/bluedroid/stack/gatt/gatt_sr.c b/components/bt/host/bluedroid/stack/gatt/gatt_sr.c index 41b6db2d184..eb6bb9b9bc7 100644 --- a/components/bt/host/bluedroid/stack/gatt/gatt_sr.c +++ b/components/bt/host/bluedroid/stack/gatt/gatt_sr.c @@ -30,6 +30,9 @@ #include "gatt_int.h" #include "stack/l2c_api.h" #include "l2c_int.h" +#if (BLE_EATT_INCLUDED == TRUE) +#include "gatt_eatt_int.h" +#endif #define GATT_MTU_REQ_MIN_LEN 2 @@ -50,12 +53,13 @@ tGATT_STATUS gatt_send_packet (tGATT_TCB *p_tcb, UINT8 *p_data, UINT16 len) UINT8 *p_m = NULL; UINT16 buf_len; tGATT_STATUS status; + UINT16 att_mtu = gatt_get_att_mtu(p_tcb); - if (len > p_tcb->payload_size){ + if (len > att_mtu){ return GATT_ILLEGAL_PARAMETER; } - buf_len = (UINT16)(sizeof(BT_HDR) + p_tcb->payload_size + L2CAP_MIN_OFFSET); + buf_len = (UINT16)(sizeof(BT_HDR) + att_mtu + L2CAP_MIN_OFFSET); if ((p_msg = (BT_HDR *)osi_malloc(buf_len)) == NULL) { return GATT_NO_RESOURCES; } @@ -442,13 +446,38 @@ tGATT_STATUS gatt_sr_process_app_rsp (tGATT_TCB *p_tcb, tGATT_IF gatt_if, gatt_sr_update_cback_cnt(p_tcb, gatt_if, FALSE, FALSE); +#if (BLE_EATT_INCLUDED == TRUE) + /* If the request arrived on an EATT bearer and this response is deferred + * (GATT_PENDING) so eatt_rx_bearer was already cleared after synchronous + * handling, restore the TX bearer BEFORE the response is built. Otherwise + * gatt_get_att_mtu() below (and inside attp_build_sr_msg) would fall back to + * the legacy ATT MTU and truncate/mis-size the response. Cleared after send. */ + BOOLEAN eatt_routed = FALSE; + if (gatt_sr_is_cback_cnt_zero(p_tcb) && + p_tcb->eatt_tx_bearer == 0 && p_tcb->eatt_rx_bearer == 0 && + p_tcb->sr_cmd.eatt_lcid != 0) { + p_tcb->eatt_tx_bearer = p_tcb->sr_cmd.eatt_lcid; + eatt_routed = TRUE; + } +#endif + if (op_code == GATT_REQ_READ_MULTI) { /* If no error and still waiting, just return */ - if (!process_read_multi_rsp (&p_tcb->sr_cmd, status, p_msg, p_tcb->payload_size)) { + if (!process_read_multi_rsp (&p_tcb->sr_cmd, status, p_msg, gatt_get_att_mtu(p_tcb))) { +#if (BLE_EATT_INCLUDED == TRUE) + if (eatt_routed) { + p_tcb->eatt_tx_bearer = 0; + } +#endif return (GATT_SUCCESS); } } else if (op_code == GATT_REQ_READ_MULTI_VAR) { - if (!process_read_multi_var_rsp(&p_tcb->sr_cmd, status, p_msg, p_tcb->payload_size)) { + if (!process_read_multi_var_rsp(&p_tcb->sr_cmd, status, p_msg, gatt_get_att_mtu(p_tcb))) { +#if (BLE_EATT_INCLUDED == TRUE) + if (eatt_routed) { + p_tcb->eatt_tx_bearer = 0; + } +#endif return (GATT_SUCCESS); } } else { @@ -458,6 +487,19 @@ tGATT_STATUS gatt_sr_process_app_rsp (tGATT_TCB *p_tcb, tGATT_IF gatt_if, if (op_code == GATT_REQ_EXEC_WRITE && status != GATT_SUCCESS) { gatt_sr_reset_cback_cnt(p_tcb); +#if (BLE_EATT_INCLUDED == TRUE) + /* reset_cback_cnt() may have just forced the count to zero. If the + * EATT restore above was skipped because the count was still + * non-zero at that point (multi-app EXEC_WRITE), redo it now so the + * error response goes out on the originating EATT bearer instead of + * falling back to the legacy ATT fixed channel. */ + if (!eatt_routed && gatt_sr_is_cback_cnt_zero(p_tcb) && + p_tcb->eatt_tx_bearer == 0 && p_tcb->eatt_rx_bearer == 0 && + p_tcb->sr_cmd.eatt_lcid != 0) { + p_tcb->eatt_tx_bearer = p_tcb->sr_cmd.eatt_lcid; + eatt_routed = TRUE; + } +#endif } p_tcb->sr_cmd.status = status; @@ -472,6 +514,8 @@ tGATT_STATUS gatt_sr_process_app_rsp (tGATT_TCB *p_tcb, tGATT_IF gatt_if, } } if (gatt_sr_is_cback_cnt_zero(p_tcb)) { + /* eatt_tx_bearer was already restored above (before the response was + * built) so gatt_get_att_mtu() used the correct EATT MTU. */ if ( (p_tcb->sr_cmd.status == GATT_SUCCESS) && (p_tcb->sr_cmd.p_rsp_msg) ) { ret_code = attp_send_sr_msg (p_tcb, p_tcb->sr_cmd.p_rsp_msg); p_tcb->sr_cmd.p_rsp_msg = NULL; @@ -482,6 +526,11 @@ tGATT_STATUS gatt_sr_process_app_rsp (tGATT_TCB *p_tcb, tGATT_IF gatt_if, ret_code = gatt_send_error_rsp (p_tcb, status, op_code, p_tcb->sr_cmd.handle, FALSE); } +#if (BLE_EATT_INCLUDED == TRUE) + if (eatt_routed) { + p_tcb->eatt_tx_bearer = 0; + } +#endif gatt_dequeue_sr_cmd(p_tcb); } @@ -875,7 +924,7 @@ static tGATT_STATUS gatt_build_primary_service_rsp (BT_HDR *p_msg, tGATT_TCB *p_ } } - if (p_msg->len + p_msg->offset <= p_tcb->payload_size && + if (p_msg->len + p_msg->offset <= gatt_get_att_mtu(p_tcb) && handle_len == p_msg->offset) { if (op_code != GATT_REQ_FIND_TYPE_VALUE || gatt_uuid_compare(value, *p_uuid)) { @@ -1053,7 +1102,7 @@ void gatts_process_primary_service_req(tGATT_TCB *p_tcb, UINT8 op_code, UINT16 l UINT16 s_hdl = 0, e_hdl = 0; tBT_UUID uuid, value, primary_service = {LEN_UUID_16, {GATT_UUID_PRI_SERVICE}}; BT_HDR *p_msg = NULL; - UINT16 msg_len = (UINT16)(sizeof(BT_HDR) + p_tcb->payload_size + L2CAP_MIN_OFFSET); + UINT16 msg_len = (UINT16)(sizeof(BT_HDR) + gatt_get_att_mtu(p_tcb) + L2CAP_MIN_OFFSET); memset (&value, 0, sizeof(tBT_UUID)); reason = gatts_validate_packet_format(op_code, &len, &p_data, &uuid, &s_hdl, &e_hdl); @@ -1119,7 +1168,7 @@ static void gatts_process_find_info(tGATT_TCB *p_tcb, UINT8 op_code, UINT16 len, reason = gatts_validate_packet_format(op_code, &len, &p_data, NULL, &s_hdl, &e_hdl); if (reason == GATT_SUCCESS) { - buf_len = (UINT16)(sizeof(BT_HDR) + p_tcb->payload_size + L2CAP_MIN_OFFSET); + buf_len = (UINT16)(sizeof(BT_HDR) + gatt_get_att_mtu(p_tcb) + L2CAP_MIN_OFFSET); if ((p_msg = (BT_HDR *)osi_calloc(buf_len)) == NULL) { reason = GATT_NO_RESOURCES; @@ -1130,7 +1179,7 @@ static void gatts_process_find_info(tGATT_TCB *p_tcb, UINT8 op_code, UINT16 len, *p ++ = op_code + 1; p_msg->len = 2; - buf_len = p_tcb->payload_size - 2; + buf_len = gatt_get_att_mtu(p_tcb) - 2; p_srv = p_list->p_first; @@ -1182,6 +1231,14 @@ static void gatts_process_mtu_req (tGATT_TCB *p_tcb, UINT16 len, UINT8 *p_data) BT_HDR *p_buf; UINT16 conn_id; +#if (BLE_EATT_INCLUDED == TRUE) + /* Exchange MTU applies to Legacy ATT bearer only (Core Spec Vol 3 Part G 5.3). */ + if (p_tcb->eatt_rx_bearer != 0 && gatt_eatt_is_bearer(p_tcb->eatt_rx_bearer)) { + gatt_send_error_rsp (p_tcb, GATT_REQ_NOT_SUPPORTED, GATT_REQ_MTU, 0, FALSE); + return; + } +#endif + /* BR/EDR connection, send error response */ if (p_tcb->att_lcid != L2CAP_ATT_CID) { gatt_send_error_rsp (p_tcb, GATT_REQ_NOT_SUPPORTED, GATT_REQ_MTU, 0, FALSE); @@ -1241,7 +1298,12 @@ void gatts_process_read_by_type_req(tGATT_TCB *p_tcb, UINT8 op_code, UINT16 len, { tBT_UUID uuid; tGATT_SR_REG *p_rcb; - UINT16 msg_len = (UINT16)(sizeof(BT_HDR) + p_tcb->payload_size + L2CAP_MIN_OFFSET), + /* Cache the MTU once: gatt_get_att_mtu() reads dynamic EATT bearer state, so + * calling it separately for the allocation size and the write limit could + * (if it ever changed between calls) let buf_len exceed the allocated buffer. + * One read keeps both consistent, matching gatt_send_packet(). */ + UINT16 att_mtu = gatt_get_att_mtu(p_tcb); + UINT16 msg_len = (UINT16)(sizeof(BT_HDR) + att_mtu + L2CAP_MIN_OFFSET), buf_len, s_hdl, e_hdl, err_hdl = 0; BT_HDR *p_msg = NULL; @@ -1274,7 +1336,7 @@ void gatts_process_read_by_type_req(tGATT_TCB *p_tcb, UINT8 op_code, UINT16 len, *p ++ = op_code + 1; /* reserve length byte */ p_msg->len = 2; - buf_len = p_tcb->payload_size - 2; + buf_len = att_mtu - 2; reason = GATT_NOT_FOUND; @@ -1614,7 +1676,7 @@ void gatt_attr_process_prepare_write (tGATT_TCB *p_tcb, UINT8 i_rcb, UINT16 hand static void gatts_process_read_req(tGATT_TCB *p_tcb, tGATT_SR_REG *p_rcb, UINT8 op_code, UINT16 handle, UINT16 len, UINT8 *p_data) { - UINT16 buf_len = (UINT16)(sizeof(BT_HDR) + p_tcb->payload_size + L2CAP_MIN_OFFSET); + UINT16 buf_len = (UINT16)(sizeof(BT_HDR) + gatt_get_att_mtu(p_tcb) + L2CAP_MIN_OFFSET); tGATT_STATUS reason; BT_HDR *p_msg = NULL; UINT8 sec_flag, key_size, *p; @@ -1639,7 +1701,7 @@ static void gatts_process_read_req(tGATT_TCB *p_tcb, tGATT_SR_REG *p_rcb, UINT8 p = (UINT8 *)(p_msg + 1) + L2CAP_MIN_OFFSET; *p ++ = op_code + 1; p_msg->len = 1; - buf_len = p_tcb->payload_size - 1; + buf_len = gatt_get_att_mtu(p_tcb) - 1; gatt_sr_get_sec_info(p_tcb->peer_bda, p_tcb->transport, @@ -1958,8 +2020,8 @@ void gatt_server_handle_client_req (tGATT_TCB *p_tcb, UINT8 op_code, /* the size of the message may not be bigger than the local max PDU size*/ /* The message has to be smaller than the agreed MTU, len does not include op code */ - if (len >= p_tcb->payload_size) { - GATT_TRACE_ERROR("server receive invalid PDU size:%d pdu size:%d", len + 1, p_tcb->payload_size ); + if (len >= gatt_get_att_mtu(p_tcb)) { + GATT_TRACE_ERROR("server receive invalid PDU size:%d pdu size:%d", len + 1, gatt_get_att_mtu(p_tcb) ); /* for invalid request expecting response, send it now */ if (op_code != GATT_CMD_WRITE && op_code != GATT_SIGN_CMD_WRITE && diff --git a/components/bt/host/bluedroid/stack/gatt/gatt_utils.c b/components/bt/host/bluedroid/stack/gatt/gatt_utils.c index 552177df82c..fe43ccafc88 100644 --- a/components/bt/host/bluedroid/stack/gatt/gatt_utils.c +++ b/components/bt/host/bluedroid/stack/gatt/gatt_utils.c @@ -34,6 +34,9 @@ #include "stack/gattdefs.h" #include "stack/sdp_api.h" #include "btm_int.h" +#if (BLE_EATT_INCLUDED == TRUE) +#include "gatt_eatt_int.h" +#endif /* check if [x, y] and [a, b] have overlapping range */ #define GATT_VALIDATE_HANDLE_RANGE(x, y, a, b) (y >= a && x <= b) @@ -1301,6 +1304,20 @@ void gatt_rsp_timeout(TIMER_LIST_ENT *p_tle) p_clcb->retry_count < GATT_REQ_RETRY_LIMIT) { UINT8 rsp_code; GATT_TRACE_WARNING("gatt_rsp_timeout retry discovery primary service"); +#if (BLE_EATT_INCLUDED == TRUE) + /* Operations sent over an EATT bearer are tracked in the EATT bearer + * table, not the legacy cl_cmd_q. Calling gatt_cmd_dequeue for them would + * consume an unrelated legacy command and report "out of sync". Release + * the EATT bearer and retry directly (gatt_act_discovery re-acquires a + * bearer via attp_cl_send_cmd). */ + if (gatt_eatt_release_bearer_by_clcb(p_clcb->p_tcb->peer_bda, p_clcb->clcb_idx)) { + p_clcb->retry_count++; +#if (GATTC_INCLUDED == TRUE) + gatt_act_discovery(p_clcb); +#endif ///GATTC_INCLUDED == TRUE + return; + } +#endif ///BLE_EATT_INCLUDED == TRUE if (p_clcb != gatt_cmd_dequeue(p_clcb->p_tcb, &rsp_code)) { GATT_TRACE_ERROR("gatt_rsp_timeout command queue out of sync, disconnect"); } else { @@ -1336,6 +1353,16 @@ void gatt_ind_ack_timeout(TIMER_LIST_ENT *p_tle) p_tcb->ind_count = 0; } +#if (BLE_EATT_INCLUDED == TRUE) + if (p_tcb != NULL) { + /* Auto-ack on the bearer the indication arrived on (0 == legacy ATT). */ + p_tcb->eatt_tx_bearer = p_tcb->eatt_ind_bearer; + attp_send_cl_msg(p_tcb, 0, GATT_HANDLE_VALUE_CONF, NULL); + p_tcb->eatt_tx_bearer = 0; + p_tcb->eatt_ind_bearer = 0; + return; + } +#endif attp_send_cl_msg(((tGATT_TCB *)p_tle->param), 0, GATT_HANDLE_VALUE_CONF, NULL); } #endif // (GATTC_INCLUDED == TRUE) diff --git a/components/bt/host/bluedroid/stack/gatt/include/gatt_eatt_int.h b/components/bt/host/bluedroid/stack/gatt/include/gatt_eatt_int.h new file mode 100644 index 00000000000..3c438de0d37 --- /dev/null +++ b/components/bt/host/bluedroid/stack/gatt/include/gatt_eatt_int.h @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* EATT (Enhanced ATT) internal definitions. */ + +#ifndef GATT_EATT_INT_H +#define GATT_EATT_INT_H + +#include "common/bt_target.h" + +#if (BLE_EATT_INCLUDED == TRUE) + +#include "stack/bt_types.h" +#include "gatt_int.h" + +#define GATT_EATT_PSM 0x0027 + +typedef void (tGATT_EATT_EVT_CBACK)(UINT16 conn_id, UINT8 status, UINT16 cid); + +void gatt_eatt_init(void); +void gatt_eatt_deinit(void); +void gatt_eatt_register_evt_cback(tGATT_EATT_EVT_CBACK *p_cback); +void gatt_eatt_set_chan_num(UINT8 num_chan); +void gatt_eatt_on_encrypted(BD_ADDR bd_addr); + +BOOLEAN gatt_eatt_is_bearer(UINT16 lcid); +UINT16 gatt_eatt_get_available_bearer(BD_ADDR bd_addr, UINT8 op); +#if (BLE_EATT_SERVER_INCLUDED == TRUE) +/* Pick an EATT bearer for a server-initiated PDU (notification/indication), + * round-robin across the connection's bearers. Returns L2CAP_ATT_CID when no + * EATT bearer is available so the caller falls back to the legacy ATT channel. */ +UINT16 gatt_eatt_get_server_tx_bearer(BD_ADDR bd_addr); +#endif +BOOLEAN gatt_eatt_set_default_bearer(UINT16 conn_id, UINT16 lcid); +BOOLEAN gatt_eatt_mark_busy(BD_ADDR bd_addr, UINT16 lcid, UINT8 op, UINT16 clcb_idx); +BOOLEAN gatt_eatt_release_bearer(BD_ADDR bd_addr, UINT16 lcid, UINT8 *p_op, UINT16 *p_clcb_idx); +BOOLEAN gatt_eatt_release_bearer_by_clcb(BD_ADDR bd_addr, UINT16 clcb_idx); + +void gatt_eatt_data_ind(UINT16 lcid, BT_HDR *p_buf); +void gatt_eatt_on_chan_mtu_changed(BD_ADDR bd_addr, UINT16 lcid); +UINT16 gatt_eatt_mtu_for_client_op(BD_ADDR bd_addr, UINT8 op_code, UINT16 legacy_mtu); + +#endif /* BLE_EATT_INCLUDED == TRUE */ + +#endif /* GATT_EATT_INT_H */ diff --git a/components/bt/host/bluedroid/stack/gatt/include/gatt_int.h b/components/bt/host/bluedroid/stack/gatt/include/gatt_int.h index 0f2a7b08831..3c0dbd27f33 100644 --- a/components/bt/host/bluedroid/stack/gatt/include/gatt_int.h +++ b/components/bt/host/bluedroid/stack/gatt/include/gatt_int.h @@ -74,6 +74,20 @@ typedef UINT8 tGATT_SEC_ACTION; #define GATT_AUTH_SIGN_MASK 0x80 /*0x1000-0000*/ #define GATT_AUTH_SIGN_LEN 12 +/* Only Write Command (0x52) and Signed Write Command (0xD2) may set the + * command/signature bits in the top two MSBs; all other opcodes must be + * strictly below GATT_OP_CODE_MAX with those bits clear. */ +static inline BOOLEAN gatt_is_valid_att_opcode(UINT8 op_code) +{ + if (op_code == GATT_CMD_WRITE || op_code == GATT_SIGN_CMD_WRITE) { + return TRUE; + } + if (op_code & GATT_WRITE_CMD_MASK) { + return FALSE; + } + return op_code < GATT_OP_CODE_MAX; +} + #define GATT_HDR_SIZE 3 /* 1B opcode + 2B handle */ /* ATT Read By Type Response: Length field is 1 octet (max 255). */ @@ -301,6 +315,11 @@ typedef struct { UINT8 op_code; UINT8 status; UINT8 cback_cnt[GATT_MAX_APPS]; +#if (BLE_EATT_INCLUDED == TRUE) + UINT16 eatt_lcid; /* EATT bearer the request arrived on, so an + * async server response is routed back to it + * after eatt_rx_bearer has been cleared. */ +#endif } tGATT_SR_CMD; #define GATT_CH_CLOSE 0 @@ -388,6 +407,13 @@ typedef struct { UINT32 trans_id; UINT16 att_lcid; /* L2CAP channel ID for ATT */ +#if (BLE_EATT_INCLUDED == TRUE) + UINT16 eatt_rx_bearer; /* active EATT bearer for RX/response routing */ + UINT16 eatt_tx_bearer; /* transient TX bearer override */ + UINT16 eatt_ind_bearer; /* EATT bearer an indication arrived on, so a + * deferred app confirmation is sent back on it */ + UINT16 eatt_att_mtu; /* negotiated L2CAP MTU for EATT bearers */ +#endif UINT16 payload_size; tGATT_CH_STATE ch_state; @@ -635,6 +661,19 @@ extern UINT16 gatt_profile_find_conn_id_by_bd_addr(BD_ADDR bda); /* Functions provided by att_protocol.c */ +#if (BLE_EATT_INCLUDED == TRUE) +extern UINT16 gatt_get_att_mtu(tGATT_TCB *p_tcb); +#if (BLE_EATT_CLIENT_INCLUDED == TRUE) +extern UINT16 gatt_eatt_mtu_for_client_op(BD_ADDR bd_addr, UINT8 op_code, UINT16 legacy_mtu); +#define GATT_CL_ATT_MTU(p_tcb, op) \ + gatt_eatt_mtu_for_client_op((p_tcb)->peer_bda, (op), (p_tcb)->payload_size) +#else +#define GATT_CL_ATT_MTU(p_tcb, op) ((p_tcb)->payload_size) +#endif +#else +#define gatt_get_att_mtu(p_tcb) ((p_tcb)->payload_size) +#define GATT_CL_ATT_MTU(p_tcb, op) ((p_tcb)->payload_size) +#endif extern tGATT_STATUS attp_send_cl_msg (tGATT_TCB *p_tcb, UINT16 clcb_idx, UINT8 op_code, tGATT_CL_MSG *p_msg); extern BT_HDR *attp_build_sr_msg(tGATT_TCB *p_tcb, UINT8 op_code, tGATT_SR_MSG *p_msg); extern tGATT_STATUS attp_send_sr_msg (tGATT_TCB *p_tcb, BT_HDR *p_msg); @@ -753,7 +792,7 @@ extern UINT8 gatt_act_send_browse(tGATT_TCB *p_tcb, UINT16 index, UINT8 op, UINT extern tGATT_CLCB *gatt_cmd_dequeue(tGATT_TCB *p_tcb, UINT8 *p_opcode); extern BOOLEAN gatt_cmd_enq(tGATT_TCB *p_tcb, UINT16 clcb_idx, BOOLEAN to_send, UINT8 op_code, BT_HDR *p_buf); extern void gatt_client_handle_server_rsp (tGATT_TCB *p_tcb, UINT8 op_code, - UINT16 len, UINT8 *p_data); + UINT16 len, UINT8 *p_data, UINT16 eatt_bearer_lcid); extern void gatt_send_queue_write_cancel (tGATT_TCB *p_tcb, tGATT_CLCB *p_clcb, tGATT_EXEC_FLAG flag); /* gatt_auth.c */ diff --git a/components/bt/host/bluedroid/stack/include/stack/gatt_api.h b/components/bt/host/bluedroid/stack/include/stack/gatt_api.h index 2e1d2d54c2b..2eaab687601 100644 --- a/components/bt/host/bluedroid/stack/include/stack/gatt_api.h +++ b/components/bt/host/bluedroid/stack/include/stack/gatt_api.h @@ -1278,6 +1278,11 @@ extern tGATT_STATUS GATTS_HandleMultiValueNotification (UINT16 conn_id, tGATT_HL *******************************************************************************/ extern tGATT_STATUS GATTS_ShowLocalDatabase(void); +#if (BLE_EATT_INCLUDED == TRUE) +extern void GATT_EattSetChanNum(UINT8 num_chan); +extern BOOLEAN GATT_EattSetDefaultBearer(UINT16 conn_id, UINT16 lcid); +#endif + #ifdef __cplusplus } diff --git a/components/bt/host/bluedroid/stack/include/stack/l2c_api.h b/components/bt/host/bluedroid/stack/include/stack/l2c_api.h index 45538ff5347..e4e2c6e30c6 100644 --- a/components/bt/host/bluedroid/stack/include/stack/l2c_api.h +++ b/components/bt/host/bluedroid/stack/include/stack/l2c_api.h @@ -277,6 +277,15 @@ typedef void (tL2CA_ECHO_DATA_CB) (BD_ADDR, UINT16, UINT8 *); */ typedef void (tL2CA_CONGESTION_STATUS_CB) (UINT16, BOOLEAN); +#if (BLE_L2CAP_ENHANCED_COC_INCLUDED == TRUE) +/* LE CoC reconfiguration indication. Parameters are: +** Local CID +** Result (0 = L2CAP_LE_RECONFIG_OK) +** TRUE if peer initiated the reconfiguration +*/ +typedef void (tL2CA_LE_RECONFIG_IND_CB) (UINT16, UINT16, BOOLEAN); +#endif + /* Callback prototype for number of packets completed events. ** This callback notifies the application when Number of Completed Packets ** event has been received. @@ -312,6 +321,9 @@ typedef struct { tL2CA_DATA_IND_CB *pL2CA_DataInd_Cb; tL2CA_CONGESTION_STATUS_CB *pL2CA_CongestionStatus_Cb; tL2CA_TX_COMPLETE_CB *pL2CA_TxComplete_Cb; +#if (BLE_L2CAP_ENHANCED_COC_INCLUDED == TRUE) + tL2CA_LE_RECONFIG_IND_CB *pL2CA_LeReconfigInd_Cb; +#endif } tL2CAP_APPL_INFO; @@ -558,6 +570,12 @@ extern UINT16 L2CA_ConnectLECocReq (UINT16 psm, BD_ADDR p_bd_addr, tL2CAP_LE_CFG extern BOOLEAN L2CA_ConnectLECocRsp (BD_ADDR p_bd_addr, UINT8 id, UINT16 lcid, UINT16 result, UINT16 status, tL2CAP_LE_CFG_INFO *p_cfg); +#if (BLE_L2CAP_ENHANCED_COC_INCLUDED == TRUE) +extern UINT8 L2CA_ConnectLEEcocReq(UINT16 psm, BD_ADDR p_bd_addr, tL2CAP_LE_CFG_INFO *p_cfg, + UINT8 num_chan, UINT16 *p_lcids); +extern BOOLEAN L2CA_LEEcocReconfig(UINT16 lcids[], UINT8 num, UINT16 new_mtu, UINT16 new_mps); +#endif + /******************************************************************************* ** ** Function L2CA_GetPeerLECocConfig @@ -569,6 +587,12 @@ extern BOOLEAN L2CA_ConnectLECocRsp (BD_ADDR p_bd_addr, UINT8 id, UINT16 lcid, U *******************************************************************************/ extern BOOLEAN L2CA_GetPeerLECocConfig (UINT16 lcid, tL2CAP_LE_CFG_INFO* peer_cfg); +extern UINT8 L2CA_LECocDataWrite (UINT16 lcid, BT_HDR *p_data); +extern BOOLEAN L2CA_LECocIsCongested (UINT16 lcid); +extern BOOLEAN L2CA_LECocGiveCredits (UINT16 lcid, UINT16 credits); +extern BOOLEAN L2CA_LECocSetAutoCredit (UINT16 lcid, BOOLEAN enable); +extern BOOLEAN L2CA_LECocDisconnect (UINT16 lcid); + #endif // (BLE_L2CAP_COC_INCLUDED == TRUE) /******************************************************************************* diff --git a/components/bt/host/bluedroid/stack/include/stack/l2cdefs.h b/components/bt/host/bluedroid/stack/include/stack/l2cdefs.h index 1572a37d6d7..b64573e0d82 100644 --- a/components/bt/host/bluedroid/stack/include/stack/l2cdefs.h +++ b/components/bt/host/bluedroid/stack/include/stack/l2cdefs.h @@ -44,6 +44,10 @@ #define L2CAP_CMD_BLE_CREDIT_BASED_CONN_REQ 0x14 #define L2CAP_CMD_BLE_CREDIT_BASED_CONN_RES 0x15 #define L2CAP_CMD_BLE_FLOW_CTRL_CREDIT 0x16 +#define L2CAP_CMD_BLE_ENHANCED_CONN_REQ 0x17 +#define L2CAP_CMD_BLE_ENHANCED_CONN_RES 0x18 +#define L2CAP_CMD_BLE_CREDIT_RECONFIG_REQ 0x19 +#define L2CAP_CMD_BLE_CREDIT_RECONFIG_RSP 0x1A @@ -77,6 +81,10 @@ #define L2CAP_CMD_BLE_CREDIT_BASED_CONN_REQ_LEN 10 /* LE_PSM, SCID, MTU, MPS, Init Credit */ #define L2CAP_CMD_BLE_CREDIT_BASED_CONN_RES_LEN 10 /* DCID, MTU, MPS, Init credit, Result */ #define L2CAP_CMD_BLE_FLOW_CTRL_CREDIT_LEN 4 /* CID, Credit */ +#define L2CAP_CMD_BLE_ENHANCED_CONN_REQ_BASE_LEN 8 /* LE_PSM, MTU, MPS, Init Credit */ +#define L2CAP_CMD_BLE_ENHANCED_CONN_RES_BASE_LEN 8 /* MTU, MPS, Init credit, Result */ +#define L2CAP_CMD_BLE_CREDIT_RECONFIG_REQ_BASE_LEN 4 /* MTU, MPS */ +#define L2CAP_CMD_BLE_CREDIT_RECONFIG_RSP_LEN 2 /* Result */ @@ -288,7 +296,7 @@ /* SAR bits in the control word */ #define L2CAP_FCR_UNSEG_SDU 0x0000 /* Control word to begin with for unsegmented PDU*/ -#define L2CAP_FCR_START_SDU 0x4000 /* ...for Starting PDU of a semented SDU */ +#define L2CAP_FCR_START_SDU 0x4000 /* ...for Starting PDU of a segmented SDU */ #define L2CAP_FCR_END_SDU 0x8000 /* ...for ending PDU of a segmented SDU */ #define L2CAP_FCR_CONT_SDU 0xc000 /* ...for continuation PDU of a segmented SDU */ @@ -333,4 +341,10 @@ #define L2CAP_LE_RESULT_UNACCEPTABLE_PARAMETERS 0x0B #define L2CAP_LE_RESULT_INVALID_PARAMETERS 0x0C +#define L2CAP_LE_RECONFIG_OK 0 +#define L2CAP_LE_RECONFIG_REDUCTION_MTU_NOT_ALLOWED 1 +#define L2CAP_LE_RECONFIG_REDUCTION_MPS_NOT_ALLOWED 2 +#define L2CAP_LE_RECONFIG_INVALID_DCID 3 +#define L2CAP_LE_RECONFIG_UNACCEPTED_PARAM 4 + #endif diff --git a/components/bt/host/bluedroid/stack/l2cap/include/l2c_int.h b/components/bt/host/bluedroid/stack/l2cap/include/l2c_int.h index 705b11048bd..c8aa59f54be 100644 --- a/components/bt/host/bluedroid/stack/l2cap/include/l2c_int.h +++ b/components/bt/host/bluedroid/stack/l2cap/include/l2c_int.h @@ -38,6 +38,12 @@ #define L2CAP_LE_MIN_MTU 23 #define L2CAP_LE_MIN_MPS 23 #define L2CAP_LE_MAX_MPS 65533 +#define L2CAP_LE_CLAMP_MPS(m) \ + ((UINT16)(((m) < L2CAP_LE_MIN_MPS) ? L2CAP_LE_MIN_MPS : \ + (((m) > L2CAP_LE_MAX_MPS) ? L2CAP_LE_MAX_MPS : (m)))) +/* Enhanced Credit Based Flow Control minimums (Core Spec Vol 3 Part A 4.25). */ +#define L2CAP_LE_ECFC_MIN_MTU 64 +#define L2CAP_LE_ECFC_MIN_MPS 64 #define L2CAP_LE_MIN_CREDIT 0 #define L2CAP_LE_MAX_CREDIT 65535 #define L2CAP_LE_DEFAULT_MTU 512 @@ -285,8 +291,10 @@ typedef struct typedef struct t_l2c_ccb { BOOLEAN in_use; /* TRUE when in use, FALSE when not */ tL2C_CHNL_STATE chnl_state; /* Channel state */ - tL2CAP_LE_CFG_INFO local_conn_cfg; /* Our config for ble conn oriented channel */ - tL2CAP_LE_CFG_INFO peer_conn_cfg; /* Peer device config ble conn oriented channel */ +#if (BLE_INCLUDED == TRUE) + tL2CAP_LE_CFG_INFO local_conn_cfg; /* LE CoC local channel config */ + tL2CAP_LE_CFG_INFO peer_conn_cfg; /* LE CoC peer channel config */ +#endif struct t_l2c_ccb *p_next_ccb; /* Next CCB in the chain */ struct t_l2c_ccb *p_prev_ccb; /* Previous CCB in the chain */ @@ -347,6 +355,23 @@ typedef struct t_l2c_ccb { UINT16 fixed_chnl_idle_tout; /* Idle timeout to use for the fixed channel */ #endif UINT16 tx_data_len; +#if (BLE_L2CAP_COC_INCLUDED == TRUE) + BOOLEAN le_coc_active; + BOOLEAN le_ecfc_channel; + BOOLEAN le_coc_no_auto_credit; + UINT16 le_coc_rx_avail; + UINT16 le_coc_rx_credits_pending; + UINT16 le_coc_rx_manual_owed; /* manual mode: K-frame credits consumed, awaiting recv_ready return */ + BT_HDR *le_coc_rx_sdu; + UINT16 le_coc_rx_sdu_total; + UINT16 le_coc_rx_sdu_rcvd; + BOOLEAN le_coc_rx_have_len; + BT_HDR *le_coc_tx_sdu; + UINT16 le_coc_tx_offset; + BOOLEAN le_coc_tx_len_sent; + BOOLEAN le_coc_xmit_busy; /* try_xmit re-entrancy guard */ + BOOLEAN le_coc_xmit_rerun; /* re-entered: outer loop must re-run */ +#endif } tL2C_CCB; /*********************************************************************** @@ -449,7 +474,9 @@ typedef struct t_l2c_linkcb { tBLE_ADDR_TYPE open_addr_type; /* be set by open API */ tBLE_ADDR_TYPE ble_addr_type; UINT16 tx_data_len; /* tx data length used in data length extension */ +#if (BLE_L2CAP_COC_INCLUDED == TRUE) fixed_queue_t *le_sec_pending_q; /* LE coc channels waiting for security check completion */ +#endif UINT8 sec_act; #define L2C_BLE_CONN_UPDATE_DISABLE 0x1 /* disable update connection parameters */ #define L2C_BLE_NEW_CONN_PARAM 0x2 /* new connection parameter to be set */ @@ -720,6 +747,7 @@ extern tL2C_RCB *l2cu_find_rcb_by_psm (UINT16 psm); extern void l2cu_release_rcb (tL2C_RCB *p_rcb); extern tL2C_RCB *l2cu_allocate_ble_rcb (UINT16 psm); extern tL2C_RCB *l2cu_find_ble_rcb_by_psm (UINT16 psm); +extern tL2C_RCB *l2cu_find_ble_rcb_by_real_psm (UINT16 real_psm); #if (L2CAP_COC_INCLUDED == TRUE) extern UINT8 l2cu_process_peer_cfg_req (tL2C_CCB *p_ccb, tL2CAP_CFG_INFO *p_cfg); @@ -828,7 +856,84 @@ extern void l2cble_credit_based_conn_req (tL2C_CCB *p_ccb); extern void l2cble_credit_based_conn_res (tL2C_CCB *p_ccb, UINT16 result); extern void l2cble_send_peer_disc_req(tL2C_CCB *p_ccb); extern void l2cble_send_flow_control_credit(tL2C_CCB *p_ccb, UINT16 credit_value); +#if (BLE_L2CAP_COC_INCLUDED == TRUE) +#if (SMP_INCLUDED == TRUE) +/* Defined in l2c_ble.c under (SMP_INCLUDED && BLE_L2CAP_COC_INCLUDED); the LE + * CoC/ECFC security check has no meaning without SMP, so callers guard their + * use with #if (SMP_INCLUDED == TRUE) and fall back to an immediate success. */ extern BOOLEAN l2ble_sec_access_req(BD_ADDR bd_addr, UINT16 psm, BOOLEAN is_originator, tL2CAP_SEC_CBACK *p_callback, void *p_ref_data); +extern void l2ble_sec_flush_pending_req(tL2C_LCB *p_lcb, void *p_ref_data); +#endif + +extern BOOLEAN l2c_ble_le_coc_is_chan(tL2C_CCB *p_ccb); +/* Map a BTM security failure (tBTM_STATUS) to the matching LE CoC/ECFC L2CAP + * result code (0x0005-0x0008) so the peer learns the real reason (authorization + * / encryption) instead of always seeing "insufficient authentication". */ +extern UINT16 l2c_ble_coc_sec_status_to_result(BD_ADDR bd_addr, tBTM_STATUS status); +#if (BLE_L2CAP_COC_CLIENT_INCLUDED == TRUE) +extern void l2c_ble_le_coc_connect_req(tL2C_CCB *p_ccb); +extern void l2c_ble_le_coc_handle_credit_conn_res(tL2C_LCB *p_lcb, UINT8 *p, UINT8 id, UINT16 cmd_len); +/* Fail a pending base LE CoC (0x14) client request whose sig id was CMD_REJECTed. + * Returns TRUE if a matching pending CCB was found and torn down. */ +extern BOOLEAN l2c_ble_le_coc_abort_conn_req(tL2C_LCB *p_lcb, UINT8 id, UINT16 result); +#endif +#if (BLE_L2CAP_COC_SERVER_INCLUDED == TRUE) +extern void l2c_ble_le_coc_connect_rsp(tL2C_CCB *p_ccb, UINT16 result); +extern void l2c_ble_le_coc_handle_credit_conn_req(tL2C_LCB *p_lcb, UINT8 *p, UINT8 id, UINT16 cmd_len); +#endif +extern void l2c_ble_le_coc_on_link_up(tL2C_LCB *p_lcb); +extern void l2c_ble_le_coc_open_channel(tL2C_CCB *p_ccb, UINT16 result); +extern void l2c_ble_le_coc_cleanup_ccb(tL2C_CCB *p_ccb); +extern void l2c_ble_le_coc_apply_reconfig(tL2C_CCB *p_ccb, UINT16 new_mtu, UINT16 new_mps); +extern void l2c_ble_le_coc_handle_flow_ctrl_credit(tL2C_LCB *p_lcb, UINT8 *p, UINT16 cmd_len); +extern void l2c_ble_le_coc_handle_disc_req(tL2C_CCB *p_ccb, tL2C_LCB *p_lcb, UINT8 id, UINT16 lcid, UINT16 rcid); +extern void l2c_ble_le_coc_handle_disc_rsp(tL2C_LCB *p_lcb, UINT8 *p, UINT8 id, UINT16 cmd_len); +extern void l2c_ble_le_coc_data_ind(tL2C_CCB *p_ccb, BT_HDR *p_msg); +extern UINT8 l2c_ble_le_coc_data_write(UINT16 lcid, BT_HDR *p_data); +extern BOOLEAN l2c_ble_le_coc_is_congested(UINT16 lcid); +extern BOOLEAN l2c_ble_le_coc_give_credits(UINT16 lcid, UINT16 credits); +extern BOOLEAN l2c_ble_le_coc_set_auto_credit(UINT16 lcid, BOOLEAN enable); +#if (BLE_L2CAP_ENHANCED_COC_INCLUDED == TRUE) +extern void l2c_ble_le_coc_notify_reconfig(tL2C_CCB *p_ccb, UINT16 status, BOOLEAN peer_initiated); +#endif +extern BOOLEAN l2c_ble_le_coc_disconnect(UINT16 lcid); +/* Per-CCB signalling response timeout (BTU_TTYPE_L2CAP_CHNL on p_ccb->timer_entry): + * fires when a peer never answers a pending connect/reconfigure request. */ +extern void l2c_ble_le_coc_channel_timeout(tL2C_CCB *p_ccb); +extern void l2c_ble_le_coc_start_rsp_timer(tL2C_CCB *p_ccb, UINT16 timeout_sec); +extern void l2c_ble_le_coc_stop_rsp_timer(tL2C_CCB *p_ccb); + +#if (BLE_L2CAP_ENHANCED_COC_INCLUDED == TRUE) +#if (BLE_L2CAP_COC_SERVER_INCLUDED == TRUE) +extern void l2c_ble_ecfc_connect_rsp(tL2C_CCB *p_ccb, UINT16 result); +extern void l2c_ble_ecfc_handle_conn_req(tL2C_LCB *p_lcb, UINT8 *p, UINT8 id, UINT16 cmd_len); +#endif +#if (BLE_L2CAP_COC_CLIENT_INCLUDED == TRUE) +extern void l2c_ble_ecfc_handle_conn_res(tL2C_LCB *p_lcb, UINT8 *p, UINT8 id, UINT16 cmd_len); +extern void l2c_ble_ecfc_abort_cl_txn(tL2C_LCB *p_lcb, UINT8 sig_id, UINT16 result); +/* Abort the ECFC client connect transaction that owns p_ccb (0x18 timed out). */ +extern BOOLEAN l2c_ble_ecfc_on_conn_timeout(tL2C_CCB *p_ccb); +#endif +/* Reconfiguration is available regardless of the client/server flag. */ +extern void l2c_ble_ecfc_abort_reconfig_txn(tL2C_LCB *p_lcb, UINT8 sig_id); +/* Abort the ECFC reconfigure transaction that owns p_ccb (0x1A timed out). */ +extern BOOLEAN l2c_ble_ecfc_on_reconfig_timeout(tL2C_CCB *p_ccb); +extern void l2c_ble_ecfc_handle_reconfig_req(tL2C_LCB *p_lcb, UINT8 *p, UINT8 id, UINT16 cmd_len); +extern void l2c_ble_ecfc_handle_reconfig_res(tL2C_LCB *p_lcb, UINT8 *p, UINT8 id, UINT16 cmd_len); +extern void l2c_ble_ecfc_on_ccb_release(tL2C_CCB *p_ccb); +#if (BLE_L2CAP_COC_CLIENT_INCLUDED == TRUE) +extern void l2c_ble_ecfc_on_link_up(tL2C_LCB *p_lcb); +#endif +extern BOOLEAN l2cu_send_peer_ble_enhanced_credit_conn_req(tL2C_LCB *p_lcb, UINT8 sig_id, UINT16 psm, + UINT16 mtu, UINT16 mps, UINT16 credits, UINT8 num_chan, UINT16 *p_scids); +extern void l2cu_send_peer_ble_enhanced_credit_conn_res(tL2C_LCB *p_lcb, UINT8 rem_id, + UINT16 mtu, UINT16 mps, UINT16 credits, UINT16 result, UINT8 num_chan, UINT16 *p_dcids); +extern void l2cu_reject_ble_enhanced_connection(tL2C_LCB *p_lcb, UINT8 rem_id, UINT16 result, UINT8 num_scids); +extern BOOLEAN l2cu_send_peer_ble_credit_reconfig_req(tL2C_LCB *p_lcb, UINT8 sig_id, + UINT16 mtu, UINT16 mps, UINT8 num_chan, UINT16 *p_dcids); +extern void l2cu_send_peer_ble_credit_reconfig_rsp(tL2C_LCB *p_lcb, UINT8 rem_id, UINT16 result); +#endif /* BLE_L2CAP_ENHANCED_COC_INCLUDED == TRUE */ +#endif /* BLE_L2CAP_COC_INCLUDED == TRUE */ #if (defined BLE_LLT_INCLUDED) && (BLE_LLT_INCLUDED == TRUE) diff --git a/components/bt/host/bluedroid/stack/l2cap/l2c_api.c b/components/bt/host/bluedroid/stack/l2cap/l2c_api.c index c0487f16a5c..1c55c2566b0 100644 --- a/components/bt/host/bluedroid/stack/l2cap/l2c_api.c +++ b/components/bt/host/bluedroid/stack/l2cap/l2c_api.c @@ -37,6 +37,7 @@ #include "stack/btm_api.h" #include "osi/allocator.h" #include "gatt_int.h" +#include "device/controller.h" #if (CLASSIC_BT_INCLUDED == TRUE) /******************************************************************************* ** @@ -1439,6 +1440,12 @@ void L2CA_DeregisterLECoc(UINT16 psm) *******************************************************************************/ UINT16 L2CA_ConnectLECocReq(UINT16 psm, BD_ADDR p_bd_addr, tL2CAP_LE_CFG_INFO *p_cfg) { +#if (BLE_L2CAP_COC_CLIENT_INCLUDED != TRUE) + UNUSED(psm); + UNUSED(p_bd_addr); + UNUSED(p_cfg); + return 0; +#else L2CAP_TRACE_API("%s PSM: 0x%04x BDA: %02x:%02x:%02x:%02x:%02x:%02x", __func__, psm, p_bd_addr[0], p_bd_addr[1], p_bd_addr[2], p_bd_addr[3], p_bd_addr[4], p_bd_addr[5]); @@ -1449,6 +1456,17 @@ UINT16 L2CA_ConnectLECocReq(UINT16 psm, BD_ADDR p_bd_addr, tL2CAP_LE_CFG_INFO *p return 0; } + /* Bail out before allocating an LCB if the controller has no BLE support: + * l2cu_create_conn()'s !supports_ble() path returns FALSE WITHOUT releasing + * the LCB (it must not change its ownership contract), so allocating here and + * relying on that path would leak the LCB. Pre-check at the API entry as the + * function header of l2cu_create_conn recommends. */ + if (!controller_get_interface()->supports_ble()) + { + L2CAP_TRACE_WARNING("%s controller has no BLE support", __func__); + return 0; + } + /* Fail if the PSM is not registered */ tL2C_RCB *p_rcb = l2cu_find_ble_rcb_by_psm(psm); if (p_rcb == NULL) @@ -1483,6 +1501,13 @@ UINT16 L2CA_ConnectLECocReq(UINT16 psm, BD_ADDR p_bd_addr, tL2CAP_LE_CFG_INFO *p /* Save registration info */ p_ccb->p_rcb = p_rcb; + p_ccb->le_coc_active = TRUE; + /* A pooled CCB reused from a released non-CoC channel keeps its stale + * remote_cid (l2cu_allocate_ccb does not clear it, and l2cu_release_ccb only + * runs cleanup_ccb for le_coc_active CCBs). Clear it now so the DCID dedup + * check in l2c_ble_le_coc_handle_credit_conn_res cannot false-match this + * channel-in-setup before its real remote_cid is assigned. */ + p_ccb->remote_cid = 0; /* Save the configuration */ if (p_cfg) { @@ -1495,7 +1520,7 @@ UINT16 L2CA_ConnectLECocReq(UINT16 psm, BD_ADDR p_bd_addr, tL2CAP_LE_CFG_INFO *p if (p_ccb->p_lcb->transport == BT_TRANSPORT_LE) { L2CAP_TRACE_DEBUG("%s LE Link is up", __func__); - l2c_csm_execute(p_ccb, L2CEVT_L2CA_CONNECT_REQ, NULL); + l2c_ble_le_coc_connect_req(p_ccb); } } @@ -1517,6 +1542,7 @@ UINT16 L2CA_ConnectLECocReq(UINT16 psm, BD_ADDR p_bd_addr, tL2CAP_LE_CFG_INFO *p /* Return the local CID as our handle */ return p_ccb->local_cid; +#endif /* BLE_L2CAP_COC_CLIENT_INCLUDED */ } /******************************************************************************* @@ -1533,6 +1559,16 @@ UINT16 L2CA_ConnectLECocReq(UINT16 psm, BD_ADDR p_bd_addr, tL2CAP_LE_CFG_INFO *p BOOLEAN L2CA_ConnectLECocRsp (BD_ADDR p_bd_addr, UINT8 id, UINT16 lcid, UINT16 result, UINT16 status, tL2CAP_LE_CFG_INFO *p_cfg) { +#if (BLE_L2CAP_COC_SERVER_INCLUDED != TRUE) + UNUSED(p_bd_addr); + UNUSED(id); + UNUSED(lcid); + UNUSED(result); + UNUSED(status); + UNUSED(p_cfg); + return FALSE; +#else + UNUSED(status); L2CAP_TRACE_API("%s CID: 0x%04x Result: %d Status: %d BDA: %02x:%02x:%02x:%02x:%02x:%02x", __func__, lcid, result, status, p_bd_addr[0], p_bd_addr[1], p_bd_addr[2], p_bd_addr[3], p_bd_addr[4], p_bd_addr[5]); @@ -1566,18 +1602,77 @@ BOOLEAN L2CA_ConnectLECocRsp (BD_ADDR p_bd_addr, UINT8 id, UINT16 lcid, UINT16 r memcpy(&p_ccb->local_conn_cfg, p_cfg, sizeof(tL2CAP_LE_CFG_INFO)); } - if (result == L2CAP_CONN_OK) - l2c_csm_execute (p_ccb, L2CEVT_L2CA_CONNECT_RSP, NULL); - else - { - tL2C_CONN_INFO conn_info; - memcpy(conn_info.bd_addr, p_bd_addr, BD_ADDR_LEN); - conn_info.l2cap_result = result; - conn_info.l2cap_status = status; - l2c_csm_execute(p_ccb, L2CEVT_L2CA_CONNECT_RSP_NEG, &conn_info); +#if (BLE_L2CAP_ENHANCED_COC_INCLUDED == TRUE) + if (p_ccb->le_ecfc_channel) { + /* Forward the caller's specific result so a security reject + * (0x0005-0x0008) reaches the peer intact (Core Spec v6.2 Vol 3 Part A + * 10.2 mandates the exact "insufficient authentication/encryption" code). + * l2c_ble_ecfc_connect_rsp records it for the aggregate 0x18 response. */ + l2c_ble_ecfc_connect_rsp(p_ccb, result); + return TRUE; } +#endif + + /* Legacy single-channel LE CoC: forward the caller's specific result so the + * peer sees the real reject reason (mapped to a valid LE result code). */ + l2c_ble_le_coc_connect_rsp(p_ccb, result); return TRUE; +#endif /* BLE_L2CAP_COC_SERVER_INCLUDED */ +} + +/******************************************************************************* +** +** Function L2CA_LECocDataWrite +** +** Description Write an SDU on an LE CoC channel. +** +** Returns L2CAP_DW_SUCCESS, L2CAP_DW_CONGESTED, or L2CAP_DW_FAILED +** +*******************************************************************************/ +UINT8 L2CA_LECocDataWrite(UINT16 lcid, BT_HDR *p_data) +{ + L2CAP_TRACE_API("L2CA_LECocDataWrite() CID: 0x%04x", lcid); + return l2c_ble_le_coc_data_write(lcid, p_data); +} + +BOOLEAN L2CA_LECocIsCongested(UINT16 lcid) +{ + return l2c_ble_le_coc_is_congested(lcid); +} + +/******************************************************************************* +** +** Function L2CA_LECocGiveCredits +** +** Description Return RX credits to peer after processing an SDU. +** +** Returns TRUE if credits were sent +** +*******************************************************************************/ +BOOLEAN L2CA_LECocGiveCredits(UINT16 lcid, UINT16 credits) +{ + L2CAP_TRACE_API("L2CA_LECocGiveCredits() CID: 0x%04x credits: %u", lcid, credits); + return l2c_ble_le_coc_give_credits(lcid, credits); +} + +BOOLEAN L2CA_LECocSetAutoCredit(UINT16 lcid, BOOLEAN enable) +{ + L2CAP_TRACE_API("L2CA_LECocSetAutoCredit() CID: 0x%04x enable=%u", lcid, enable); + return l2c_ble_le_coc_set_auto_credit(lcid, enable); +} + +/******************************************************************************* +** +** Description Disconnect an LE CoC channel. +** +** Returns TRUE if disconnect request was sent +** +*******************************************************************************/ +BOOLEAN L2CA_LECocDisconnect(UINT16 lcid) +{ + L2CAP_TRACE_API("L2CA_LECocDisconnect() CID: 0x%04x", lcid); + return l2c_ble_le_coc_disconnect(lcid); } /******************************************************************************* diff --git a/components/bt/host/bluedroid/stack/l2cap/l2c_ble.c b/components/bt/host/bluedroid/stack/l2cap/l2c_ble.c index 3dfd210cc24..1b90e6b4ad2 100644 --- a/components/bt/host/bluedroid/stack/l2cap/l2c_ble.c +++ b/components/bt/host/bluedroid/stack/l2cap/l2c_ble.c @@ -312,6 +312,9 @@ void l2cble_notify_le_connection (BD_ADDR bda) /* update l2cap link status and send callback */ p_lcb->link_state = LST_CONNECTED; l2cu_process_fixed_chnl_resp (p_lcb); +#if (BLE_L2CAP_COC_INCLUDED == TRUE) + l2c_ble_le_coc_on_link_up(p_lcb); +#endif } } @@ -493,6 +496,9 @@ void l2cble_advertiser_conn_comp (UINT16 handle, BD_ADDR bda, tBLE_ADDR_TYPE typ if (!HCI_LE_SLAVE_INIT_FEAT_EXC_SUPPORTED(controller_get_interface()->get_features_ble()->as_array)) { p_lcb->link_state = LST_CONNECTED; l2cu_process_fixed_chnl_resp (p_lcb); +#if (BLE_L2CAP_COC_INCLUDED == TRUE) + l2c_ble_le_coc_on_link_up(p_lcb); +#endif } /* when adv and initiating are both active, cancel the direct connection */ @@ -738,8 +744,55 @@ void l2cble_process_sig_cmd (tL2C_LCB *p_lcb, UINT8 *p, UINT16 pkt_len) return; } +#if (BLE_L2CAP_ENHANCED_COC_INCLUDED == TRUE) + if (cmd_code >= L2CAP_CMD_BLE_ENHANCED_CONN_REQ && + cmd_code <= L2CAP_CMD_BLE_CREDIT_RECONFIG_RSP) { + L2CAP_TRACE_DEBUG("LE_ECFC sig rx cmd=0x%02x id=%u len=%u link_st=%u role=%u", + cmd_code, id, cmd_len, p_lcb->link_state, p_lcb->link_role); + } +#endif + switch (cmd_code) { +#if (BLE_L2CAP_ENHANCED_COC_INCLUDED == TRUE) + case L2CAP_CMD_REJECT: { + UINT16 rej_reason = 0; + + if (cmd_len < 2) { + L2CAP_TRACE_WARNING ("L2CAP - LE - short cmd: %d", cmd_len); + return; + } + STREAM_TO_UINT16(rej_reason, p); + L2CAP_TRACE_DEBUG("LE_ECFC rx CMD_REJECT sig_id=%u reason=%u", id, rej_reason); +#if (BLE_L2CAP_COC_CLIENT_INCLUDED == TRUE) + /* Peer explicitly rejected the request: "no/unsupported PSM" is the + * closest generic reason to report to the application. A CMD_REJECT may + * answer either an ECFC (0x18) or a base LE CoC (0x14) client request, so + * try both aborts; each only acts on its own matching pending state. */ + l2c_ble_ecfc_abort_cl_txn(p_lcb, id, L2CAP_CONN_NO_PSM); + l2c_ble_le_coc_abort_conn_req(p_lcb, id, L2CAP_CONN_NO_PSM); +#endif + /* Reconfiguration is compiled in regardless of the client/server flag, + * so a CMD_REJECT may be answering a pending reconfigure request. Abort + * it here too, otherwise its txn slot leaks (never freed). */ + l2c_ble_ecfc_abort_reconfig_txn(p_lcb, id); + break; + } +#endif +#if (BLE_L2CAP_ENHANCED_COC_INCLUDED != TRUE) case L2CAP_CMD_REJECT: + if (cmd_len < 2) { + L2CAP_TRACE_WARNING ("L2CAP - LE - short cmd: %d", cmd_len); + return; + } + L2CAP_TRACE_DEBUG("LE rx CMD_REJECT sig_id=%u", id); +#if (BLE_L2CAP_COC_CLIENT_INCLUDED == TRUE) + /* A CMD_REJECT may be answering a pending base LE CoC (0x14) client + * request; fail it now instead of waiting out the connect RTX timer. */ + l2c_ble_le_coc_abort_conn_req(p_lcb, id, L2CAP_CONN_NO_PSM); +#endif + p += 2; + break; +#endif case L2CAP_CMD_ECHO_RSP: case L2CAP_CMD_INFO_RSP: if (cmd_len < 2) { @@ -816,8 +869,12 @@ void l2cble_process_sig_cmd (tL2C_LCB *p_lcb, UINT8 *p, UINT16 pkt_len) break; } case L2CAP_CMD_BLE_CREDIT_BASED_CONN_REQ: { +#if (BLE_L2CAP_COC_SERVER_INCLUDED == TRUE) + l2c_ble_le_coc_handle_credit_conn_req(p_lcb, p, id, cmd_len); +#elif (BLE_L2CAP_COC_INCLUDED != TRUE) if (cmd_len < 10) { L2CAP_TRACE_WARNING ("L2CAP - LE - short cmd: %d", cmd_len); + l2cu_reject_ble_connection(p_lcb, id, L2CAP_LE_RESULT_UNACCEPTABLE_PARAMETERS); return; } tL2C_CCB *p_ccb = NULL; @@ -863,9 +920,25 @@ void l2cble_process_sig_cmd (tL2C_LCB *p_lcb, UINT8 *p, UINT16 pkt_len) p_ccb->peer_conn_cfg.credits = credits; l2cu_send_peer_ble_credit_based_conn_res(p_ccb, L2CAP_LE_RESULT_CONN_OK); +#else + if (cmd_len < 10) { + L2CAP_TRACE_WARNING ("L2CAP - LE - short cmd: %d", cmd_len); + l2cu_reject_ble_connection(p_lcb, id, L2CAP_LE_RESULT_UNACCEPTABLE_PARAMETERS); + return; + } + l2cu_reject_ble_connection(p_lcb, id, L2CAP_LE_RESULT_NO_RESOURCES); +#endif break; } +#if (BLE_L2CAP_COC_CLIENT_INCLUDED == TRUE) + case L2CAP_CMD_BLE_CREDIT_BASED_CONN_RES: + l2c_ble_le_coc_handle_credit_conn_res(p_lcb, p, id, cmd_len); + break; +#endif case L2CAP_CMD_BLE_FLOW_CTRL_CREDIT: { +#if (BLE_L2CAP_COC_INCLUDED == TRUE) + l2c_ble_le_coc_handle_flow_ctrl_credit(p_lcb, p, cmd_len); +#else if (cmd_len < L2CAP_CMD_BLE_FLOW_CTRL_CREDIT_LEN) { L2CAP_TRACE_WARNING ("L2CAP - LE - flow ctrl credit too short: %d", cmd_len); return; @@ -891,6 +964,7 @@ void l2cble_process_sig_cmd (tL2C_LCB *p_lcb, UINT8 *p, UINT16 pkt_len) p_ccb->peer_conn_cfg.credits, lcid); l2c_link_check_send_pkts(p_ccb->p_lcb, NULL, NULL); } +#endif break; } case L2CAP_CMD_DISC_REQ: { @@ -905,6 +979,12 @@ void l2cble_process_sig_cmd (tL2C_LCB *p_lcb, UINT8 *p, UINT16 pkt_len) STREAM_TO_UINT16(rcid, p); p_ccb = l2cu_find_ccb_by_cid(p_lcb, lcid); +#if (BLE_L2CAP_COC_INCLUDED == TRUE) + if (p_ccb && p_ccb->le_coc_active) { + l2c_ble_le_coc_handle_disc_req(p_ccb, p_lcb, id, lcid, rcid); + break; + } +#endif if (p_ccb) { p_ccb->remote_id = id; l2cu_send_peer_disc_rsp(p_lcb, id, lcid, rcid); @@ -914,6 +994,57 @@ void l2cble_process_sig_cmd (tL2C_LCB *p_lcb, UINT8 *p, UINT16 pkt_len) } break; } +#if (BLE_L2CAP_COC_INCLUDED == TRUE) + case L2CAP_CMD_DISC_RSP: + l2c_ble_le_coc_handle_disc_rsp(p_lcb, p, id, cmd_len); + break; +#endif +#if (BLE_L2CAP_ENHANCED_COC_INCLUDED == TRUE) +#if (BLE_L2CAP_COC_SERVER_INCLUDED == TRUE) + case L2CAP_CMD_BLE_ENHANCED_CONN_REQ: + l2c_ble_ecfc_handle_conn_req(p_lcb, p, id, cmd_len); + break; +#else + case L2CAP_CMD_BLE_ENHANCED_CONN_REQ: { + /* ECFC compiled without a server role (e.g. GATTS disabled): we still + * understand the ECFC command set (RECONFIG_REQ/RSP are handled below), + * so reply with a proper all-refused ECFC connection response instead of + * a CMD_REJECT "not understood". Mirrors the 0x14 #else path above. */ + if (cmd_len >= L2CAP_CMD_BLE_ENHANCED_CONN_REQ_BASE_LEN + sizeof(UINT16)) { + UINT16 n_scids = (UINT16)((cmd_len - L2CAP_CMD_BLE_ENHANCED_CONN_REQ_BASE_LEN) / sizeof(UINT16)); + /* The reject must carry one DCID per requested SCID (Core Spec v6.2 + * Vol 3 Part A 4.26: 1:1 positional mapping); do NOT clamp to the + * local channel budget as that desyncs the DCID count. Cap at 255 + * only to fit the UINT8 API argument. Mirror the server path + * (l2c_ble_ecfc_handle_conn_req): >5 SCIDs is malformed + * (INVALID_PARAMETERS), otherwise a plain resource refusal. */ + UINT8 reject_scids = (n_scids > 255) ? 255 : (UINT8)n_scids; + UINT16 reason = (n_scids > 5) ? L2CAP_LE_RESULT_INVALID_PARAMETERS + : L2CAP_LE_RESULT_NO_RESOURCES; + l2cu_reject_ble_enhanced_connection(p_lcb, id, reason, reject_scids); + } else { + /* Too short to parse the SCID list, but the peer still expects a + * response; mirror the server path (l2c_ble_ecfc_handle_conn_req) and + * reject with n_scids=1 so the peer does not hang until its signalling + * timer expires. */ + L2CAP_TRACE_WARNING("L2CAP - LE - short ECFC conn req: %d", cmd_len); + l2cu_reject_ble_enhanced_connection(p_lcb, id, L2CAP_LE_RESULT_INVALID_PARAMETERS, 1); + } + break; + } +#endif +#if (BLE_L2CAP_COC_CLIENT_INCLUDED == TRUE) + case L2CAP_CMD_BLE_ENHANCED_CONN_RES: + l2c_ble_ecfc_handle_conn_res(p_lcb, p, id, cmd_len); + break; +#endif + case L2CAP_CMD_BLE_CREDIT_RECONFIG_REQ: + l2c_ble_ecfc_handle_reconfig_req(p_lcb, p, id, cmd_len); + break; + case L2CAP_CMD_BLE_CREDIT_RECONFIG_RSP: + l2c_ble_ecfc_handle_reconfig_res(p_lcb, p, id, cmd_len); + break; +#endif default: L2CAP_TRACE_WARNING ("L2CAP - LE - unknown cmd code: %d", cmd_code); l2cu_send_peer_cmd_reject (p_lcb, L2CAP_CMD_REJ_NOT_UNDERSTOOD, id, 0, 0); @@ -952,6 +1083,10 @@ BOOLEAN l2cble_init_direct_conn (tL2C_LCB *p_lcb) /* There can be only one BLE connection request outstanding at a time */ if (p_dev_rec == NULL) { L2CAP_TRACE_WARNING ("unknown device, can not initiate connection"); + /* The caller allocated this LCB and expects this function to release it + * on failure (as the other error paths do); free it to avoid leaking the + * LCB and its queues / num_ble_links_active count. */ + l2cu_release_lcb (p_lcb); return (FALSE); } @@ -1675,7 +1810,43 @@ void l2cble_send_peer_disc_req(tL2C_CCB *p_ccb) return; } -#if (SMP_INCLUDED == TRUE) +#if (BLE_L2CAP_COC_INCLUDED == TRUE) +/******************************************************************************* +** +** Function l2c_ble_coc_sec_status_to_result +** +** Description Translate a BTM security failure into the LE CoC/ECFC L2CAP +** result code that best matches it, so a rejected peer learns +** the real reason instead of always "insufficient +** authentication" (Core Spec v6.2 Vol 3 Part A 4.26/10.2 make +** 0x0005-0x0008 mandatory per failure type). +** +** Returns One of L2CAP_LE_RESULT_INSUFFICIENT_* (0x0005-0x0008) +** +*******************************************************************************/ +UINT16 l2c_ble_coc_sec_status_to_result(BD_ADDR bd_addr, tBTM_STATUS status) +{ + UINT8 sec_flags = 0; + + if (status == BTM_NOT_AUTHORIZED) { + return L2CAP_LE_RESULT_INSUFFICIENT_AUTHORIZATION; /* 0x0006 */ + } + + /* If the link is not encrypted, tell the peer to encrypt (0x0008) rather + * than re-authenticate; only fall back to insufficient authentication + * (0x0005) when encryption is present but the required level was not met. + * Key-size (0x0007) needs the actual key length, which the flags API does + * not expose, so it is intentionally not distinguished here. */ + if (BTM_GetSecurityFlagsByTransport(bd_addr, &sec_flags, BT_TRANSPORT_LE) && + !(sec_flags & BTM_SEC_FLAG_ENCRYPTED)) { + return L2CAP_LE_RESULT_INSUFFICIENT_ENCRY; /* 0x0008 */ + } + + return L2CAP_LE_RESULT_INSUFFICIENT_AUTHENTICATION; /* 0x0005 */ +} +#endif /* BLE_L2CAP_COC_INCLUDED == TRUE */ + +#if (SMP_INCLUDED == TRUE) && (BLE_L2CAP_COC_INCLUDED == TRUE) /******************************************************************************* ** ** Function l2cble_sec_comp @@ -1762,6 +1933,53 @@ void l2cble_sec_comp(BD_ADDR p_bda, tBT_TRANSPORT transport, void *p_ref_data, } } +/******************************************************************************* +** +** Function l2ble_sec_flush_pending_req +** +** Description Drop any queued LE security requests whose p_ref_data matches +** |p_ref_data| (typically a CCB being released). Without this, +** l2cble_sec_comp() would later invoke the stored callback with +** a dangling or reused pointer once SMP completes. +** +** Returns void +** +*******************************************************************************/ +void l2ble_sec_flush_pending_req(tL2C_LCB *p_lcb, void *p_ref_data) +{ + if (p_lcb == NULL || p_lcb->le_sec_pending_q == NULL || p_ref_data == NULL) { + return; + } + + /* Removing mutates the underlying list, so re-scan from the head after each + * hit until no queued request references p_ref_data anymore. */ + for (;;) { + list_t *list = fixed_queue_get_list(p_lcb->le_sec_pending_q); + tL2CAP_SEC_DATA *match = NULL; + list_node_t *node; + + for (node = list_begin(list); node != list_end(list); node = list_next(node)) { + tL2CAP_SEC_DATA *p_buf = (tL2CAP_SEC_DATA *)list_node(node); + if (p_buf != NULL && p_buf->p_ref_data == p_ref_data) { + match = p_buf; + break; + } + } + if (match == NULL) { + break; + } + /* Only free once the node is actually detached. If removal fails (item + * gone / could not acquire the dequeue semaphore), freeing it here would + * leave a dangling node in the list, so the next scan would dereference + * freed memory (use-after-free) and could loop forever. Abort instead. */ + if (fixed_queue_try_remove_from_queue(p_lcb->le_sec_pending_q, match) != NULL) { + osi_free(match); + } else { + break; + } + } +} + /******************************************************************************* ** ** Function l2ble_sec_access_req @@ -1810,7 +2028,7 @@ BOOLEAN l2ble_sec_access_req(BD_ADDR bd_addr, UINT16 psm, BOOLEAN is_originator, return status; } -#endif /* #if (SMP_INCLUDED == TRUE) */ +#endif /* (SMP_INCLUDED == TRUE) && (BLE_L2CAP_COC_INCLUDED == TRUE) */ #endif /* (BLE_INCLUDED == TRUE) */ /******************************************************************************* ** diff --git a/components/bt/host/bluedroid/stack/l2cap/l2c_ble_ecfc.c b/components/bt/host/bluedroid/stack/l2cap/l2c_ble_ecfc.c new file mode 100644 index 00000000000..17b432d8682 --- /dev/null +++ b/components/bt/host/bluedroid/stack/l2cap/l2c_ble_ecfc.c @@ -0,0 +1,1547 @@ +/* + * 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 +#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 */ diff --git a/components/bt/host/bluedroid/stack/l2cap/l2c_ble_le_coc.c b/components/bt/host/bluedroid/stack/l2cap/l2c_ble_le_coc.c new file mode 100644 index 00000000000..f96af4172fc --- /dev/null +++ b/components/bt/host/bluedroid/stack/l2cap/l2c_ble_le_coc.c @@ -0,0 +1,1458 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* LE L2CAP Connection-Oriented Channel (Credit Based Flow Control Mode). */ +/* Independent from BR/EDR l2c_csm.c. */ + +#include +#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_COC_INCLUDED == TRUE) + +#define L2C_BLE_COC_DEFAULT_MTU 512 +#define L2C_BLE_COC_DEFAULT_CREDITS 10 +#define L2C_BLE_COC_SDU_LEN_SIZE 2 +/* LE Credit Based Flow Control minimums (Core Spec Vol 3 Part A 4.22/4.23). */ +#define L2C_BLE_COC_MIN_MTU 23 +#define L2C_BLE_COC_MIN_MPS 23 + +/* RX credit window used for throughput. This is the number of K-frame credits we + * keep granted to the peer. It must stay large enough to keep the sender's pipeline + * fed (otherwise it ping-pongs at 1 credit), and is decoupled from the minimal + * ceil(MTU/MPS) needed to reassemble a single SDU. Kept in sync with the ECFC + * initial credits so the window is consistent before and after a reconfig. */ +#define L2C_BLE_COC_RX_CREDIT_WINDOW L2CAP_LE_INIT_CREDITS + +#define L2C_BLE_COC_TRACE_DEBUG(fmt, ...) L2CAP_TRACE_DEBUG("LE_COC: " fmt, ##__VA_ARGS__) +#define L2C_BLE_COC_TRACE_WARN(fmt, ...) L2CAP_TRACE_WARNING("LE_COC: " fmt, ##__VA_ARGS__) +#define L2C_BLE_COC_TRACE_ERROR(fmt, ...) L2CAP_TRACE_ERROR("LE_COC: " fmt, ##__VA_ARGS__) + +static void l2c_ble_le_coc_sec_cback(BD_ADDR bd_addr, tBT_TRANSPORT transport, + void *p_ref_data, tBTM_STATUS result); +static void l2c_ble_le_coc_notify_disconnect(tL2C_CCB *p_ccb, BOOLEAN local_init); +void l2c_ble_le_coc_open_channel(tL2C_CCB *p_ccb, UINT16 result); +static void l2c_ble_le_coc_try_xmit(tL2C_CCB *p_ccb); +static BOOLEAN l2c_ble_le_coc_send_frame(tL2C_CCB *p_ccb, const UINT8 *data, UINT16 len); +static void l2c_ble_le_coc_apply_default_cfg(tL2CAP_LE_CFG_INFO *cfg); +static UINT16 l2c_ble_le_coc_effective_mps(tL2C_CCB *p_ccb); +static UINT16 l2c_ble_le_coc_calc_rx_credits(UINT16 mtu, UINT16 mps); + +#if (BLE_L2CAP_COC_SERVER_INCLUDED == TRUE) +/* Normalize an application/internal result code to a valid LE Credit Based + * Connection Response result. L2CAP_CONN_{OK,NO_PSM,NO_RESOURCES} share values + * with their L2CAP_LE_RESULT_* counterparts, and callers may also pass an + * LE result code directly, so any valid LE result code is forwarded as-is. + * Internal-only L2CAP_CONN_* codes with no LE encoding (e.g. NO_LINK, TIMEOUT) + * fall back to UNACCEPTABLE_PARAMETERS. */ +static UINT16 l2c_ble_le_coc_wire_result(UINT16 result) +{ + switch (result) { + case L2CAP_LE_RESULT_CONN_OK: + case L2CAP_LE_RESULT_NO_PSM: + case L2CAP_LE_RESULT_NO_RESOURCES: + case L2CAP_LE_RESULT_INSUFFICIENT_AUTHENTICATION: + case L2CAP_LE_RESULT_INSUFFICIENT_AUTHORIZATION: + case L2CAP_LE_RESULT_INSUFFICIENT_ENCRY_KEY_SIZE: + case L2CAP_LE_RESULT_INSUFFICIENT_ENCRY: + case L2CAP_LE_RESULT_INVALID_SOURCE_CID: + case L2CAP_LE_RESULT_SOURCE_CID_ALREADY_ALLOCATED: + case L2CAP_LE_RESULT_UNACCEPTABLE_PARAMETERS: + return result; + /* 0x0C (INVALID_PARAMETERS) is "Reserved for future use" for the LE Credit + * Based Connection Response (code 0x15) per Core Spec v6.2 Vol 3 Part A + * Table 4.16; fall through to the default so it maps to 0x0B, the last + * valid result code for this packet type. */ + default: + return L2CAP_LE_RESULT_UNACCEPTABLE_PARAMETERS; + } +} +#endif /* BLE_L2CAP_COC_SERVER_INCLUDED */ + +BOOLEAN l2c_ble_le_coc_is_chan(tL2C_CCB *p_ccb) +{ + return (p_ccb != NULL) && p_ccb->le_coc_active && p_ccb->p_lcb != NULL && + p_ccb->p_lcb->transport == BT_TRANSPORT_LE; +} + +static void l2c_ble_le_coc_apply_default_cfg(tL2CAP_LE_CFG_INFO *cfg) +{ + if (cfg->mtu == 0) { + cfg->mtu = L2C_BLE_COC_DEFAULT_MTU; + } + /* Enforce the spec minimum MTU (Core Spec v6.2 Vol 3 Part A 4.22/4.23: + * "shall support a minimum MTU size of 23 octets"). Mirrors the MPS clamp + * below so a caller-supplied non-zero MTU < 23 is not sent on the wire. */ + if (cfg->mtu < L2C_BLE_COC_MIN_MTU) { + cfg->mtu = L2C_BLE_COC_MIN_MTU; + } + if (cfg->mps == 0) { + cfg->mps = L2CAP_LE_COC_MPS; + } + cfg->mps = L2CAP_LE_CLAMP_MPS(cfg->mps); + if (cfg->credits == 0) { + cfg->credits = L2C_BLE_COC_RX_CREDIT_WINDOW; + } +} + +static UINT16 l2c_ble_le_coc_effective_mps(tL2C_CCB *p_ccb) +{ + UINT16 mps = p_ccb->peer_conn_cfg.mps; + UINT16 acl = controller_get_interface()->get_acl_data_size_ble(); + + if (mps == 0) { + mps = L2CAP_LE_COC_MPS; + } + if (acl > L2CAP_PKT_OVERHEAD && mps > (acl - L2CAP_PKT_OVERHEAD)) { + mps = acl - L2CAP_PKT_OVERHEAD; + } + return mps; +} + +static UINT16 l2c_ble_le_coc_calc_rx_credits(UINT16 mtu, UINT16 mps) +{ + UINT16 credits; + UINT32 total; + + if (mps == 0) { + mps = L2CAP_LE_COC_MPS; + } + /* The first K-frame of an SDU carries a 2-byte SDU Length header, so a full + * SDU of `mtu` bytes spans ceil((mtu + 2) / mps) K-frames. Ignoring the + * header under-counts by one credit whenever mtu is a multiple of mps. */ + total = (UINT32)mtu + L2C_BLE_COC_SDU_LEN_SIZE; + credits = (UINT16)(total / mps); + if (total % mps) { + credits++; + } + return (credits > 0) ? credits : 1; +} + +void l2c_ble_le_coc_apply_reconfig(tL2C_CCB *p_ccb, UINT16 new_mtu, UINT16 new_mps) +{ + UINT16 credits; + + if (p_ccb == NULL || !p_ccb->le_coc_active) { + return; + } + + p_ccb->local_conn_cfg.mtu = new_mtu; + if (new_mps != 0) { + p_ccb->local_conn_cfg.mps = L2CAP_LE_CLAMP_MPS(new_mps); + } + credits = l2c_ble_le_coc_calc_rx_credits(new_mtu, p_ccb->local_conn_cfg.mps); + if (credits < L2C_BLE_COC_RX_CREDIT_WINDOW) { + credits = L2C_BLE_COC_RX_CREDIT_WINDOW; + } + p_ccb->local_conn_cfg.credits = credits; + + /* Grant the peer the extra RX credits the enlarged window now allows, + * otherwise the widened window never takes effect: the auto-credit path only + * returns consumed credits and can never raise le_coc_rx_avail above the + * previously outstanding count. */ + { + /* Count every credit the peer will eventually hold again: what it still + * holds (le_coc_rx_avail) plus credits already consumed but not yet + * returned - batched in auto mode (le_coc_rx_credits_pending) or owed in + * manual mode (le_coc_rx_manual_owed). Omitting manual_owed under-counts + * the window, so the deficit sent here plus the same owed credits later + * returned by recv_ready would double-grant and inflate the peer's TX + * window beyond the configured window. Use a UINT32 sum and clamp to the + * max credit window, matching give_credits / set_auto_credit. */ + UINT32 outstanding = (UINT32)p_ccb->le_coc_rx_avail + + p_ccb->le_coc_rx_credits_pending + + p_ccb->le_coc_rx_manual_owed; + if (credits > outstanding && p_ccb->p_lcb != NULL) { + UINT16 deficit = (UINT16)(credits - outstanding); + if ((UINT32)p_ccb->le_coc_rx_avail + deficit > L2CAP_LE_MAX_CREDIT) { + deficit = L2CAP_LE_MAX_CREDIT - p_ccb->le_coc_rx_avail; + } + if (deficit > 0) { + p_ccb->le_coc_rx_avail += deficit; + l2cble_send_flow_control_credit(p_ccb, deficit); + } + } + } + + L2C_BLE_COC_TRACE_DEBUG("reconfig lcid=0x%04x mtu=%u mps=%u rx_cred=%u", + p_ccb->local_cid, new_mtu, p_ccb->local_conn_cfg.mps, credits); +#if (BLE_EATT_INCLUDED == TRUE) + if (p_ccb->p_lcb != NULL) { + gatt_eatt_on_chan_mtu_changed(p_ccb->p_lcb->remote_bd_addr, p_ccb->local_cid); + } +#endif +} + +#if (BLE_L2CAP_ENHANCED_COC_INCLUDED == TRUE) +void l2c_ble_le_coc_notify_reconfig(tL2C_CCB *p_ccb, UINT16 status, BOOLEAN peer_initiated) +{ + tL2CA_LE_RECONFIG_IND_CB *cb; + + if (p_ccb == NULL || p_ccb->p_rcb == NULL) { + return; + } + + cb = p_ccb->p_rcb->api.pL2CA_LeReconfigInd_Cb; + if (cb) { + (*cb)(p_ccb->local_cid, status, peer_initiated); + } +} +#endif /* BLE_L2CAP_ENHANCED_COC_INCLUDED == TRUE */ + +BOOLEAN l2c_ble_le_coc_set_auto_credit(UINT16 lcid, BOOLEAN enable) +{ + tL2C_CCB *p_ccb; + + p_ccb = l2cu_find_ccb_by_cid(NULL, lcid); + if (p_ccb == NULL || !p_ccb->le_coc_active) { + return FALSE; + } + + /* When leaving auto-credit mode, flush any RX credits batched by the + * auto-credit path. Otherwise they are stranded (the auto path stops running + * and give_credits only adds new credits), permanently shrinking the peer's + * TX window. */ + if (!enable && !p_ccb->le_coc_no_auto_credit && p_ccb->le_coc_rx_credits_pending > 0 && + p_ccb->p_lcb != NULL) { + UINT16 give = p_ccb->le_coc_rx_credits_pending; + p_ccb->le_coc_rx_credits_pending = 0; + p_ccb->le_coc_rx_avail += give; + l2cble_send_flow_control_credit(p_ccb, give); + } + + /* Symmetrically, when re-enabling auto-credit, flush any credits the manual + * path consumed but has not returned yet (le_coc_rx_manual_owed). The auto + * path only returns credits for frames it consumes from now on, so leftover + * owed credits would otherwise be stranded and shrink the peer's TX window. */ + if (enable && p_ccb->le_coc_no_auto_credit && p_ccb->le_coc_rx_manual_owed > 0 && + p_ccb->p_lcb != NULL) { + UINT16 give = p_ccb->le_coc_rx_manual_owed; + if ((UINT32)p_ccb->le_coc_rx_avail + give > L2CAP_LE_MAX_CREDIT) { + give = L2CAP_LE_MAX_CREDIT - p_ccb->le_coc_rx_avail; + } + if (give > 0) { + p_ccb->le_coc_rx_manual_owed -= give; + p_ccb->le_coc_rx_avail += give; + l2cble_send_flow_control_credit(p_ccb, give); + } + } + + /* Manual mode returns RX credits only after a complete SDU is delivered (via + * recv_ready). If a single SDU can span more K-frames than the whole RX + * window, the peer runs out of TX credit mid-SDU and the SDU never completes + * -> the app never calls recv_ready -> stall. Warn when entering manual mode + * so the misconfiguration is visible; the data_ind deadlock breaker keeps it + * working, but auto mode (or a larger MPS) is the proper fix. */ + if (!enable) { + UINT16 need = l2c_ble_le_coc_calc_rx_credits(p_ccb->local_conn_cfg.mtu, + p_ccb->local_conn_cfg.mps); + if (need > p_ccb->local_conn_cfg.credits) { + L2C_BLE_COC_TRACE_WARN("manual mode: SDU up to %u K-frames > RX window %u lcid=0x%04x, prefer auto mode", + need, p_ccb->local_conn_cfg.credits, lcid); + } + } + + p_ccb->le_coc_no_auto_credit = !enable; + return TRUE; +} + +/* Arm/disarm the per-CCB signalling response timer. Reuses p_ccb->timer_entry + * (unused by LE CoC otherwise) with the classic per-channel BTU timer type; the + * dispatcher in l2c_process_timeout() routes it to l2c_ble_le_coc_channel_timeout() + * for LE CoC channels. l2cu_release_ccb() frees the timer, so no explicit stop is + * needed on the teardown path. */ +void l2c_ble_le_coc_start_rsp_timer(tL2C_CCB *p_ccb, UINT16 timeout_sec) +{ + if (p_ccb != NULL) { + btu_start_timer(&p_ccb->timer_entry, BTU_TTYPE_L2CAP_CHNL, timeout_sec); + } +} + +void l2c_ble_le_coc_stop_rsp_timer(tL2C_CCB *p_ccb) +{ + if (p_ccb != NULL) { + btu_stop_timer(&p_ccb->timer_entry); + } +} + +/* Send an LE Credit Based DISC_REQ, move the channel to + * CST_W4_L2CAP_DISCONNECT_RSP and arm the RTX timer so an unresponsive peer + * cannot leave the CCB (and its CID) leaked (Core Spec v6.2 Vol 3 Part A + * 6.2.1). l2c_ble_le_coc_channel_timeout() releases the CCB on expiry. */ +static void l2c_ble_le_coc_initiate_disc(tL2C_CCB *p_ccb) +{ + /* Already awaiting a DISC_RSP: a second DISC_REQ would bump p_lcb->id and + * overwrite p_ccb->local_id (see l2cu_send_peer_ble_credit_based_disconn_req), + * so the peer's DISC_RSP to the first request would be rejected on the id + * mismatch in l2c_ble_le_coc_handle_disc_rsp and the channel would linger + * (with the RTX timer restarted, up to 20s) instead of closing. Guard here so + * every caller is safe, including l2c_ble_le_coc_handle_flow_ctrl_credit + * which does not check chnl_state. Mirrors l2c_ble_le_coc_disconnect. */ + if (p_ccb->chnl_state == CST_W4_L2CAP_DISCONNECT_RSP) { + L2C_BLE_COC_TRACE_DEBUG("DISC pending, skip dup DISC_REQ lcid=0x%04x", p_ccb->local_cid); + return; + } + + /* No NULL p_lcb guard is needed here even though the send path dereferences + * p_ccb->p_lcb: p_lcb is only ever set to NULL in l2cu_release_ccb(), which + * clears in_use in the same synchronous call immediately afterwards, and the + * BT stack is single-threaded. l2cu_find_ccb_by_cid() (used by the callers) + * only returns CCBs with in_use == TRUE, so a CCB reaching here always has a + * live p_lcb; there is no window where p_lcb == NULL while in_use == TRUE. */ + L2C_BLE_COC_TRACE_DEBUG("send DISC_REQ lcid=0x%04x rcid=0x%04x", p_ccb->local_cid, p_ccb->remote_cid); + l2cble_send_peer_disc_req(p_ccb); + p_ccb->chnl_state = CST_W4_L2CAP_DISCONNECT_RSP; + /* This is the disconnect RTX (waiting for DISC_RSP), not a connect timeout, + * so use the shorter 10s disconnect timeout rather than the 60s connect one + * (matches the classic BR/EDR disconnect path). */ + l2c_ble_le_coc_start_rsp_timer(p_ccb, L2CAP_CHNL_DISCONNECT_TOUT); +} + +void l2c_ble_le_coc_channel_timeout(tL2C_CCB *p_ccb) +{ + if (p_ccb == NULL || !p_ccb->le_coc_active) { + return; + } + + L2C_BLE_COC_TRACE_WARN("rsp timeout lcid=0x%04x state=%d", p_ccb->local_cid, p_ccb->chnl_state); + +#if (BLE_L2CAP_ENHANCED_COC_INCLUDED == TRUE) + /* Reconfigure (0x19) whose 0x1A never arrived: channel stays OPEN. */ + if (p_ccb->chnl_state == CST_OPEN) { + l2c_ble_ecfc_on_reconfig_timeout(p_ccb); + return; + } +#endif + + if (p_ccb->chnl_state == CST_W4_L2CAP_DISCONNECT_RSP) { + /* Peer never answered our DISC_REQ (RTX timeout, Core Spec v6.2 Vol 3 + * Part A 6.2.1): notify the upper layer of a local disconnect and free + * the CCB instead of leaking it and its CID. The DisconnectInd callback + * runs synchronously and may re-enter L2CAP and release/reuse this CCB + * slot; re-fetch by the saved lcid and only release if it is still the + * same, in-use CoC CCB (mirrors l2c_ble_le_coc_open_channel). */ + UINT16 saved_lcid = p_ccb->local_cid; + l2c_ble_le_coc_notify_disconnect(p_ccb, TRUE); + tL2C_CCB *p_now = l2cu_find_ccb_by_cid(NULL, saved_lcid); + if (p_now == p_ccb && p_ccb->in_use && p_ccb->le_coc_active) { + l2cu_release_ccb(p_ccb); + } else { + L2C_BLE_COC_TRACE_DEBUG("disc timeout: lcid=0x%04x freed in cb, skip release", saved_lcid); + } + return; + } + + if (p_ccb->chnl_state == CST_W4_L2CAP_CONNECT_RSP) { +#if (BLE_L2CAP_ENHANCED_COC_INCLUDED == TRUE) + if (p_ccb->le_ecfc_channel) { +#if (BLE_L2CAP_COC_CLIENT_INCLUDED == TRUE) + if (l2c_ble_ecfc_on_conn_timeout(p_ccb)) { + return; + } +#endif + /* No owning txn found: release the orphaned CCB directly. */ + l2cu_release_ccb(p_ccb); + return; + } +#endif + /* Basic LE CoC (0x14) with no 0x15: fail + release via open_channel. */ + l2c_ble_le_coc_open_channel(p_ccb, L2CAP_CONN_TIMEOUT); + } +} + +void l2c_ble_le_coc_cleanup_ccb(tL2C_CCB *p_ccb) +{ + if (p_ccb == NULL) { + return; + } + + L2C_BLE_COC_TRACE_DEBUG("cleanup lcid=0x%04x", p_ccb->local_cid); + +#if (SMP_INCLUDED == TRUE) + /* A CCB torn down while its LE security check is still outstanding would + * leave a queued request pointing at this (soon reused) memory. Drop it so + * the deferred SMP callback never lands on a stale/reused CCB. */ + l2ble_sec_flush_pending_req(p_ccb->p_lcb, p_ccb); +#endif + + if (p_ccb->le_coc_rx_sdu) { + osi_free(p_ccb->le_coc_rx_sdu); + p_ccb->le_coc_rx_sdu = NULL; + } + if (p_ccb->le_coc_tx_sdu) { + osi_free(p_ccb->le_coc_tx_sdu); + p_ccb->le_coc_tx_sdu = NULL; + } + + p_ccb->le_coc_active = FALSE; +#if (BLE_L2CAP_ENHANCED_COC_INCLUDED == TRUE) + p_ccb->le_ecfc_channel = FALSE; +#endif + p_ccb->le_coc_no_auto_credit = FALSE; + p_ccb->le_coc_rx_avail = 0; + p_ccb->le_coc_rx_credits_pending = 0; + p_ccb->le_coc_rx_manual_owed = 0; + p_ccb->le_coc_rx_sdu_total = 0; + p_ccb->le_coc_rx_sdu_rcvd = 0; + p_ccb->le_coc_rx_have_len = FALSE; + p_ccb->le_coc_tx_offset = 0; + p_ccb->le_coc_tx_len_sent = FALSE; + /* Reset the TX re-entrancy guard/flag too. l2cu_allocate_ccb does not memset + * a CCB reused from the pool, so a stale le_coc_xmit_busy == TRUE (left by a + * release that raced a try_xmit re-entrancy) would permanently deadlock TX on + * the recycled channel. */ + p_ccb->le_coc_xmit_busy = FALSE; + p_ccb->le_coc_xmit_rerun = FALSE; + /* Clear the negotiated config so a recycled CCB never inherits the previous + * channel's MTU/MPS/credits (apply_default_cfg only fills zero fields). */ + memset(&p_ccb->local_conn_cfg, 0, sizeof(p_ccb->local_conn_cfg)); + memset(&p_ccb->peer_conn_cfg, 0, sizeof(p_ccb->peer_conn_cfg)); + /* Clear the peer CID too. l2cu_allocate_ccb does not memset a CCB reused from + * the pool, so a stale remote_cid would let the duplicate-DCID check in + * l2c_ble_le_coc_handle_credit_conn_res (l2cu_find_ccb_by_remote_cid) match + * this very CCB and wrongly reject a valid new outgoing connection. */ + p_ccb->remote_cid = 0; +} + +static void l2c_ble_le_coc_notify_disconnect(tL2C_CCB *p_ccb, BOOLEAN local_init) +{ + tL2CA_DISCONNECT_IND_CB *cb; + + if (p_ccb == NULL || p_ccb->p_rcb == NULL) { + return; + } + + cb = p_ccb->p_rcb->api.pL2CA_DisconnectInd_Cb; + if (cb) { + L2C_BLE_COC_TRACE_DEBUG("DisconnectInd lcid=0x%04x local_init=%d", + p_ccb->local_cid, local_init); + (*cb)(p_ccb->local_cid, local_init); + } +} + +void l2c_ble_le_coc_open_channel(tL2C_CCB *p_ccb, UINT16 result) +{ + tL2CA_CONNECT_CFM_CB *cb; + + if (p_ccb == NULL) { + return; + } + + if (result == L2CAP_CONN_OK) { + p_ccb->chnl_state = CST_OPEN; + p_ccb->le_coc_rx_avail = p_ccb->local_conn_cfg.credits; + p_ccb->le_coc_rx_credits_pending = 0; + p_ccb->le_coc_rx_manual_owed = 0; + L2C_BLE_COC_TRACE_DEBUG("OPEN lcid=0x%04x rcid=0x%04x mtu=%u mps=%u tx_cred=%u rx_cred=%u", + p_ccb->local_cid, p_ccb->remote_cid, + p_ccb->peer_conn_cfg.mtu, p_ccb->peer_conn_cfg.mps, + p_ccb->peer_conn_cfg.credits, p_ccb->le_coc_rx_avail); + } else { + L2C_BLE_COC_TRACE_WARN("connect failed lcid=0x%04x result=%u", p_ccb->local_cid, result); + } + + UINT16 saved_lcid = p_ccb->local_cid; + + if (p_ccb->p_rcb && p_ccb->p_rcb->api.pL2CA_ConnectCfm_Cb) { + cb = p_ccb->p_rcb->api.pL2CA_ConnectCfm_Cb; + (*cb)(p_ccb->local_cid, result); + } + + if (result != L2CAP_CONN_OK) { + /* The ConnectCfm callback runs synchronously and may re-enter L2CAP (e.g. + * disconnect this channel and start a new connect), which could release + * this CCB and reallocate the same pool slot for another channel. In that + * case l2cu_release_ccb(p_ccb) would tear down the wrong (reused) CCB. + * Re-fetch by the saved lcid and only release if it is still the same, + * in-use, active CoC CCB. No registered ConnectCfm handler does this + * synchronously today; this just closes the dangling-pointer window. */ + tL2C_CCB *p_now = l2cu_find_ccb_by_cid(NULL, saved_lcid); + if (p_now == p_ccb && p_ccb->in_use && p_ccb->le_coc_active) { + l2cu_release_ccb(p_ccb); + } + } +} + +static void l2c_ble_le_coc_sec_cback(BD_ADDR bd_addr, tBT_TRANSPORT transport, + void *p_ref_data, tBTM_STATUS result) +{ + tL2C_CCB *p_ccb = (tL2C_CCB *)p_ref_data; + UNUSED(transport); + + if (p_ccb == NULL || !p_ccb->le_coc_active) { + return; + } + + L2C_BLE_COC_TRACE_DEBUG("sec_cback lcid=0x%04x status=%d state=%d", + p_ccb->local_cid, result, p_ccb->chnl_state); + + if (result != BTM_SUCCESS) { +#if (BLE_L2CAP_COC_SERVER_INCLUDED == TRUE) + if (p_ccb->chnl_state == CST_TERM_W4_SEC_COMP && p_ccb->p_lcb != NULL) { + l2cu_reject_ble_connection(p_ccb->p_lcb, p_ccb->remote_id, + l2c_ble_coc_sec_status_to_result(bd_addr, result)); + l2cu_release_ccb(p_ccb); + return; + } +#endif + l2c_ble_le_coc_open_channel(p_ccb, L2CAP_CONN_NO_LINK); + return; + } + + if (p_ccb->chnl_state == CST_ORIG_W4_SEC_COMP) { +#if (BLE_L2CAP_COC_CLIENT_INCLUDED == TRUE) + l2cble_credit_based_conn_req(p_ccb); + p_ccb->chnl_state = CST_W4_L2CAP_CONNECT_RSP; + l2c_ble_le_coc_start_rsp_timer(p_ccb, L2CAP_CHNL_CONNECT_TOUT); + L2C_BLE_COC_TRACE_DEBUG("sent 0x14 lcid=0x%04x", p_ccb->local_cid); +#else + L2C_BLE_COC_TRACE_WARN("orig sec complete but client path disabled lcid=0x%04x", p_ccb->local_cid); + l2c_ble_le_coc_open_channel(p_ccb, L2CAP_CONN_NO_LINK); +#endif + } else if (p_ccb->chnl_state == CST_TERM_W4_SEC_COMP) { +#if (BLE_L2CAP_COC_SERVER_INCLUDED == TRUE) + tL2CA_CONNECT_IND_CB *ind_cb; + + p_ccb->chnl_state = CST_W4_L2CA_CONNECT_RSP; + if (p_ccb->p_rcb && p_ccb->p_rcb->api.pL2CA_ConnectInd_Cb) { + ind_cb = p_ccb->p_rcb->api.pL2CA_ConnectInd_Cb; + L2C_BLE_COC_TRACE_DEBUG("ConnectInd lcid=0x%04x psm=0x%04x id=%u", + p_ccb->local_cid, p_ccb->p_rcb->real_psm, p_ccb->remote_id); + /* Use the bd_addr parameter instead of dereferencing p_ccb->p_lcb: + * it carries the same address (both callers pass p_lcb->remote_bd_addr, + * and the BTM security callback delivers the peer address), and this + * matches the defensive p_lcb-free failure path above. */ + (*ind_cb)(bd_addr, p_ccb->local_cid, + p_ccb->p_rcb->real_psm, p_ccb->remote_id); + } else { + L2C_BLE_COC_TRACE_WARN("no ConnectInd_Cb, auto-accept lcid=0x%04x", p_ccb->local_cid); + l2c_ble_le_coc_connect_rsp(p_ccb, L2CAP_CONN_OK); + } +#else + L2C_BLE_COC_TRACE_WARN("term sec complete but server path disabled lcid=0x%04x", p_ccb->local_cid); + l2c_ble_le_coc_open_channel(p_ccb, L2CAP_CONN_NO_LINK); +#endif + } +} + +#if (BLE_L2CAP_COC_CLIENT_INCLUDED == TRUE) +void l2c_ble_le_coc_connect_req(tL2C_CCB *p_ccb) +{ + if (p_ccb == NULL || p_ccb->p_lcb == NULL || p_ccb->p_rcb == NULL) { + return; + } + + l2c_ble_le_coc_apply_default_cfg(&p_ccb->local_conn_cfg); + + L2C_BLE_COC_TRACE_DEBUG("connect_req lcid=0x%04x psm=0x%04x mtu=%u mps=%u cred=%u", + p_ccb->local_cid, p_ccb->p_rcb->real_psm, + p_ccb->local_conn_cfg.mtu, p_ccb->local_conn_cfg.mps, + p_ccb->local_conn_cfg.credits); + + p_ccb->chnl_state = CST_ORIG_W4_SEC_COMP; +#if (SMP_INCLUDED == TRUE) + l2ble_sec_access_req(p_ccb->p_lcb->remote_bd_addr, p_ccb->p_rcb->real_psm, + TRUE, l2c_ble_le_coc_sec_cback, p_ccb); +#else + /* SMP disabled: there is no LE security procedure to run, so proceed as if + * the access check passed (l2ble_sec_access_req is only compiled with SMP). */ + l2c_ble_le_coc_sec_cback(p_ccb->p_lcb->remote_bd_addr, BT_TRANSPORT_LE, p_ccb, BTM_SUCCESS); +#endif +} +#endif /* BLE_L2CAP_COC_CLIENT_INCLUDED */ + +#if (BLE_L2CAP_COC_SERVER_INCLUDED == TRUE) +void l2c_ble_le_coc_connect_rsp(tL2C_CCB *p_ccb, UINT16 result) +{ + if (p_ccb == NULL) { + return; + } + + result = l2c_ble_le_coc_wire_result(result); + L2C_BLE_COC_TRACE_DEBUG("connect_rsp lcid=0x%04x result=%u", p_ccb->local_cid, result); + + if (result == L2CAP_LE_RESULT_CONN_OK) { + l2c_ble_le_coc_apply_default_cfg(&p_ccb->local_conn_cfg); + l2cble_credit_based_conn_res(p_ccb, L2CAP_LE_RESULT_CONN_OK); + l2c_ble_le_coc_open_channel(p_ccb, L2CAP_CONN_OK); + } else { + /* Forward the specific reject reason to the peer instead of collapsing + * every failure to UNACCEPTABLE_PARAMETERS. */ + l2cble_credit_based_conn_res(p_ccb, result); + l2cu_release_ccb(p_ccb); + } +} +#endif /* BLE_L2CAP_COC_SERVER_INCLUDED */ + +void l2c_ble_le_coc_on_link_up(tL2C_LCB *p_lcb) +{ + tL2C_CCB *p_ccb; + + if (p_lcb == NULL) { + return; + } + + for (p_ccb = p_lcb->ccb_queue.p_first_ccb; p_ccb; ) { + tL2C_CCB *p_next = p_ccb->p_next_ccb; + + if (p_ccb->le_coc_active && p_ccb->chnl_state == CST_CLOSED) { +#if (BLE_L2CAP_ENHANCED_COC_INCLUDED == TRUE) + if (!p_ccb->le_ecfc_channel) +#endif + { + L2C_BLE_COC_TRACE_DEBUG("link up, start pending lcid=0x%04x", p_ccb->local_cid); +#if (BLE_L2CAP_COC_CLIENT_INCLUDED == TRUE) + l2c_ble_le_coc_connect_req(p_ccb); +#endif + } + } + p_ccb = p_next; + } + +#if (BLE_L2CAP_ENHANCED_COC_INCLUDED == TRUE) && (BLE_L2CAP_COC_CLIENT_INCLUDED == TRUE) + l2c_ble_ecfc_on_link_up(p_lcb); +#endif +} + +#if (BLE_L2CAP_COC_SERVER_INCLUDED == TRUE) +void l2c_ble_le_coc_handle_credit_conn_req(tL2C_LCB *p_lcb, UINT8 *p, UINT8 id, UINT16 cmd_len) +{ + tL2C_CCB *p_ccb = NULL; + tL2C_RCB *p_rcb = NULL; + UINT16 spsm, scid, mtu, mps, credits; + + if (cmd_len < L2CAP_CMD_BLE_CREDIT_BASED_CONN_REQ_LEN) { + /* Owe the peer a response even for a malformed request, otherwise it + * waits out its RTX timer (Core Spec v6.2 Vol 3 Part A signalling + * rules). ECFC 0x17 already rejects short packets the same way. */ + L2C_BLE_COC_TRACE_WARN("short 0x14 len=%u", cmd_len); + l2cu_reject_ble_connection(p_lcb, id, L2CAP_LE_RESULT_UNACCEPTABLE_PARAMETERS); + return; + } + + STREAM_TO_UINT16(spsm, p); + STREAM_TO_UINT16(scid, p); + STREAM_TO_UINT16(mtu, p); + STREAM_TO_UINT16(mps, p); + STREAM_TO_UINT16(credits, p); + + L2C_BLE_COC_TRACE_DEBUG("rx 0x14 spsm=0x%04x scid=0x%04x mtu=%u mps=%u cred=%u id=%u", + spsm, scid, mtu, mps, credits, id); + + if (mtu < L2C_BLE_COC_MIN_MTU || mps < L2C_BLE_COC_MIN_MPS || + mps > L2CAP_LE_MAX_MPS) { + L2C_BLE_COC_TRACE_WARN("bad params mtu=%u mps=%u", mtu, mps); + l2cu_reject_ble_connection(p_lcb, id, L2CAP_LE_RESULT_UNACCEPTABLE_PARAMETERS); + return; + } + + /* The source CID must lie in the LE-U dynamically allocated range + * (0x0040-0x007F, Core Spec v6.2 Vol 3 Part A Table 2.3). Reject values + * such as 0x0000 or the fixed ATT CID 0x0004. */ + if (scid < L2CAP_BASE_APPL_CID || scid > L2CAP_BLE_CONN_MAX_CID) { + L2C_BLE_COC_TRACE_WARN("0x14 invalid scid=0x%04x", scid); + l2cu_reject_ble_connection(p_lcb, id, L2CAP_LE_RESULT_INVALID_SOURCE_CID); + return; + } + + if (l2cu_find_ccb_by_remote_cid(p_lcb, scid)) { + l2cu_reject_ble_connection(p_lcb, id, L2CAP_LE_RESULT_SOURCE_CID_ALREADY_ALLOCATED); + return; + } + + p_rcb = l2cu_find_ble_rcb_by_psm(spsm); + if (p_rcb == NULL) { + L2C_BLE_COC_TRACE_WARN("no RCB for psm=0x%04x", spsm); + l2cu_reject_ble_connection(p_lcb, id, L2CAP_LE_RESULT_NO_PSM); + return; + } + + p_ccb = l2cu_allocate_ccb(p_lcb, 0); + if (p_ccb == NULL) { + l2cu_reject_ble_connection(p_lcb, id, L2CAP_LE_RESULT_NO_RESOURCES); + return; + } + + p_ccb->le_coc_active = 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_le_coc_apply_default_cfg(&p_ccb->local_conn_cfg); + + p_ccb->chnl_state = CST_TERM_W4_SEC_COMP; +#if (SMP_INCLUDED == TRUE) + l2ble_sec_access_req(p_lcb->remote_bd_addr, p_rcb->real_psm, FALSE, + l2c_ble_le_coc_sec_cback, p_ccb); +#else + /* SMP disabled: no security procedure, treat the access check as passed. */ + l2c_ble_le_coc_sec_cback(p_lcb->remote_bd_addr, BT_TRANSPORT_LE, p_ccb, BTM_SUCCESS); +#endif +} +#endif /* BLE_L2CAP_COC_SERVER_INCLUDED */ + +#if (BLE_L2CAP_COC_CLIENT_INCLUDED == TRUE) +static tL2C_CCB *l2c_ble_le_coc_find_ccb_by_sig_id(tL2C_LCB *p_lcb, UINT8 id) +{ + tL2C_CCB *p_ccb; + + if (p_lcb == NULL) { + return NULL; + } + + for (p_ccb = p_lcb->ccb_queue.p_first_ccb; p_ccb; p_ccb = p_ccb->p_next_ccb) { + if (p_ccb->in_use && p_ccb->le_coc_active && +#if (BLE_L2CAP_ENHANCED_COC_INCLUDED == TRUE) + /* ECFC client CCBs also set le_coc_active and sit in + * CST_W4_L2CAP_CONNECT_RSP with local_id == txn sig_id. Exclude them + * so a base-CoC 0x15 response can never be applied to an ECFC channel + * on a signalling-id collision (8-bit wrap or a malformed peer). */ + !p_ccb->le_ecfc_channel && +#endif + p_ccb->local_id == id && + p_ccb->chnl_state == CST_W4_L2CAP_CONNECT_RSP) { + return p_ccb; + } + } + return NULL; +} + +void l2c_ble_le_coc_handle_credit_conn_res(tL2C_LCB *p_lcb, UINT8 *p, UINT8 id, UINT16 cmd_len) +{ + tL2C_CCB *p_ccb; + UINT16 dcid, mtu, mps, credits, result; + + /* Find and de-arm the pending CCB before validating length, mirroring the + * ECFC handler (l2c_ble_ecfc_handle_conn_res). The response id is consumed + * either way; returning on a short packet without cleanup would leave the + * CCB stuck in CST_W4_L2CAP_CONNECT_RSP until its response timer fires, + * needlessly delaying error recovery. */ + p_ccb = l2c_ble_le_coc_find_ccb_by_sig_id(p_lcb, id); + if (p_ccb == NULL || !p_ccb->le_coc_active) { + L2C_BLE_COC_TRACE_WARN("0x15 unknown id=%u", id); + return; + } + + /* Response received: cancel the connect-response timeout. */ + l2c_ble_le_coc_stop_rsp_timer(p_ccb); + + if (cmd_len < L2CAP_CMD_BLE_CREDIT_BASED_CONN_RES_LEN) { + L2C_BLE_COC_TRACE_WARN("short 0x15 len=%u", cmd_len); + l2c_ble_le_coc_open_channel(p_ccb, L2CAP_CONN_NO_PSM); + return; + } + + STREAM_TO_UINT16(dcid, p); + STREAM_TO_UINT16(mtu, p); + STREAM_TO_UINT16(mps, p); + STREAM_TO_UINT16(credits, p); + STREAM_TO_UINT16(result, p); + + L2C_BLE_COC_TRACE_DEBUG("rx 0x15 dcid=0x%04x mtu=%u mps=%u cred=%u result=%u id=%u", + dcid, mtu, mps, credits, result, id); + + if (result != L2CAP_LE_RESULT_CONN_OK) { + /* Forward the peer's specific reject reason (e.g. insufficient + * authentication/encryption, no resources) instead of a hardcoded + * L2CAP_CONN_NO_PSM, so the application can recover appropriately. + * L2CAP_LE_RESULT_CONN_OK (0) == L2CAP_CONN_OK (0), so the success + * check in l2c_ble_le_coc_open_channel still holds. Mirrors the ECFC + * handler (l2c_ble_ecfc_handle_conn_res). */ + l2c_ble_le_coc_open_channel(p_ccb, result); + return; + } + + /* Destination CID must be from the LE-U dynamic range (Core Spec v6.2 Vol 3 + * Part A 4.23) and not already assigned on this link; otherwise outgoing + * frames would target an invalid peer CID. Validate the DCID (and record + * remote_cid) BEFORE the MTU/MPS check so a subsequent teardown can address + * the peer's channel. An invalid/duplicate DCID cannot be cleanly torn down + * (no valid target, and a duplicate would disconnect the wrong channel), so + * that case still 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) != NULL) { + L2C_BLE_COC_TRACE_WARN("0x15 invalid/duplicate dcid=0x%04x", dcid); + l2c_ble_le_coc_open_channel(p_ccb, L2CAP_CONN_NO_PSM); + return; + } + p_ccb->remote_cid = dcid; + + if (mtu < L2C_BLE_COC_MIN_MTU || mps < L2CAP_LE_MIN_MPS || mps > L2CAP_LE_MAX_MPS) { + /* result==OK means the peer established the channel on its side; a bad + * MTU/MPS makes it unusable for us, but merely dropping our CCB would + * leave the peer's half orphaned until the ACL drops. Send a best-effort + * DISC_REQ (remote_cid is set) to tear it down on the air, then report + * the connect failure to the app (Core Spec v6.2 Vol 3 Part A 4.23 + * mandates a 23-byte minimum MTU/MPS). */ + L2C_BLE_COC_TRACE_WARN("0x15 bad mtu=%u mps=%u", mtu, mps); + l2cble_send_peer_disc_req(p_ccb); + l2c_ble_le_coc_open_channel(p_ccb, L2CAP_CONN_NO_PSM); + return; + } + + p_ccb->peer_conn_cfg.mtu = mtu; + p_ccb->peer_conn_cfg.mps = mps; + p_ccb->peer_conn_cfg.credits = credits; + l2c_ble_le_coc_open_channel(p_ccb, L2CAP_CONN_OK); +} + +BOOLEAN l2c_ble_le_coc_abort_conn_req(tL2C_LCB *p_lcb, UINT8 id, UINT16 result) +{ + /* A base LE CoC (0x14) client request whose signalling id was answered by a + * CMD_REJECT (rather than a 0x15 response): fail it now so the app is told + * immediately instead of waiting out the connect RTX timer. find_ccb_by_sig_id + * excludes ECFC channels, so this never collides with the ECFC abort path + * handling the same CMD_REJECT. */ + tL2C_CCB *p_ccb = l2c_ble_le_coc_find_ccb_by_sig_id(p_lcb, id); + + if (p_ccb == NULL) { + return FALSE; + } + + L2C_BLE_COC_TRACE_DEBUG("CMD_REJECT abort base CoC lcid=0x%04x id=%u result=%u", p_ccb->local_cid, id, result); + l2c_ble_le_coc_stop_rsp_timer(p_ccb); + l2c_ble_le_coc_open_channel(p_ccb, result); /* failure path releases the CCB */ + return TRUE; +} +#endif /* BLE_L2CAP_COC_CLIENT_INCLUDED */ + +void l2c_ble_le_coc_handle_flow_ctrl_credit(tL2C_LCB *p_lcb, UINT8 *p, UINT16 cmd_len) +{ + tL2C_CCB *p_ccb; + UINT16 lcid, credit; + + if (cmd_len < L2CAP_CMD_BLE_FLOW_CTRL_CREDIT_LEN) { + L2C_BLE_COC_TRACE_WARN("short 0x16 len=%u", cmd_len); + return; + } + + STREAM_TO_UINT16(lcid, p); + STREAM_TO_UINT16(credit, p); + + /* Per Core Spec v6.2 Vol 3 Part A 4.24 the CID in L2CAP_FLOW_CONTROL_CREDIT_IND + * is the sender's local (source) CID, i.e. our remote CID. Look up by remote + * CID so credits are routed to the correct channel when several CoC channels + * share the ACL link with non-matching CID values. */ + p_ccb = l2cu_find_ccb_by_remote_cid(p_lcb, lcid); + if (p_ccb == NULL || !p_ccb->le_coc_active) { + L2C_BLE_COC_TRACE_WARN("0x16 unknown lcid=0x%04x", lcid); + return; + } + + if (credit == 0) { + /* Core Spec v6.2 Vol 3 Part A 10.1: a device receiving a + * L2CAP_FLOW_CONTROL_CREDIT_IND with a credit value of zero shall ignore + * the packet. Handling it would falsely toggle the congestion state. */ + L2C_BLE_COC_TRACE_WARN("0x16 zero credits lcid=0x%04x, ignoring", lcid); + return; + } + + if ((p_ccb->peer_conn_cfg.credits + credit) > L2CAP_LE_MAX_CREDIT) { + L2C_BLE_COC_TRACE_ERROR("credit overflow lcid=0x%04x", lcid); + l2c_ble_le_coc_initiate_disc(p_ccb); + return; + } + + p_ccb->peer_conn_cfg.credits += credit; + L2C_BLE_COC_TRACE_DEBUG("0x16 lcid=0x%04x +%u tx_cred=%u", lcid, credit, p_ccb->peer_conn_cfg.credits); + + /* Only signal decongestion for a channel that is actually OPEN. Credits can + * still arrive after l2c_ble_le_coc_initiate_disc moved the channel to + * CST_W4_L2CAP_DISCONNECT_RSP; firing the callback then would tell the upper + * layer the channel is ready to send while it is being torn down. Mirrors the + * CST_OPEN guard in l2c_ble_le_coc_try_xmit. */ + if (p_ccb->chnl_state == CST_OPEN && p_ccb->p_rcb && + p_ccb->p_rcb->api.pL2CA_CongestionStatus_Cb && p_ccb->cong_sent) { + UINT16 saved_lcid = p_ccb->local_cid; + p_ccb->cong_sent = FALSE; + (*p_ccb->p_rcb->api.pL2CA_CongestionStatus_Cb)(p_ccb->local_cid, FALSE); + /* The callback runs synchronously and may re-enter L2CAP and release this + * CCB. l2cu_release_ccb() clears in_use/p_lcb but leaves chnl_state intact, + * so try_xmit's CST_OPEN check alone would not detect a freed slot; re-fetch + * by the saved lcid and bail if it is gone, mirroring handle_disc_req / + * handle_disc_rsp / open_channel / channel_timeout. */ + tL2C_CCB *p_now = l2cu_find_ccb_by_cid(NULL, saved_lcid); + if (p_now != p_ccb || !p_ccb->in_use || !p_ccb->le_coc_active) { + L2C_BLE_COC_TRACE_DEBUG("0x16: lcid=0x%04x freed in cong cb, skip xmit", saved_lcid); + return; + } + } + + l2c_ble_le_coc_try_xmit(p_ccb); + l2c_link_check_send_pkts(p_ccb->p_lcb, NULL, NULL); +} + +void l2c_ble_le_coc_handle_disc_req(tL2C_CCB *p_ccb, tL2C_LCB *p_lcb, UINT8 id, UINT16 lcid, UINT16 rcid) +{ + if (p_ccb == NULL || p_lcb == NULL || !p_ccb->le_coc_active) { + return; + } + + /* Core Spec v6.2 Vol 3 Part A 4.6: if the DCID matches but the SCID does not, + * silently discard the request. */ + if (p_ccb->remote_cid != 0 && rcid != p_ccb->remote_cid) { + L2C_BLE_COC_TRACE_WARN("rx DISC_REQ scid mismatch lcid=0x%04x rcid=0x%04x expect=0x%04x", + lcid, rcid, p_ccb->remote_cid); + return; + } + + L2C_BLE_COC_TRACE_DEBUG("rx DISC_REQ lcid=0x%04x", lcid); + p_ccb->remote_id = id; + l2cu_send_peer_disc_rsp(p_lcb, id, lcid, rcid); + /* The DisconnectInd callback runs synchronously and may re-enter L2CAP and + * release/reuse this CCB slot; re-fetch by the saved lcid and only release if + * it is still the same, in-use CoC CCB (mirrors l2c_ble_le_coc_open_channel). */ + UINT16 saved_lcid = p_ccb->local_cid; + l2c_ble_le_coc_notify_disconnect(p_ccb, FALSE); + tL2C_CCB *p_now = l2cu_find_ccb_by_cid(NULL, saved_lcid); + if (p_now == p_ccb && p_ccb->in_use && p_ccb->le_coc_active) { + l2cu_release_ccb(p_ccb); + } else { + L2C_BLE_COC_TRACE_DEBUG("DISC_REQ: lcid=0x%04x freed in cb, skip release", saved_lcid); + } +} + +void l2c_ble_le_coc_handle_disc_rsp(tL2C_LCB *p_lcb, UINT8 *p, UINT8 id, UINT16 cmd_len) +{ + tL2C_CCB *p_ccb; + UINT16 lcid, rcid; + + if (cmd_len < L2CAP_DISC_REQ_LEN) { + return; + } + + STREAM_TO_UINT16(rcid, p); + STREAM_TO_UINT16(lcid, p); + + p_ccb = l2cu_find_ccb_by_cid(p_lcb, lcid); + if (p_ccb == NULL || !p_ccb->le_coc_active) { + return; + } + + /* Only accept a DISC_RSP that actually answers a DISC_REQ we sent: the + * channel must be awaiting the response and the signalling Identifier must + * match the one used for the request (Core Spec v6.2 Vol 3 Part A 4.7). + * Otherwise an unsolicited/stale/cross DISC_RSP could tear down an OPEN + * channel that reused the same CID. */ + if (p_ccb->chnl_state != CST_W4_L2CAP_DISCONNECT_RSP || p_ccb->local_id != id) { + L2C_BLE_COC_TRACE_WARN("rx unexpected DISC_RSP lcid=0x%04x state=%d id=%u expect_id=%u", + lcid, p_ccb->chnl_state, id, p_ccb->local_id); + return; + } + + if (p_ccb->remote_cid != 0 && rcid != p_ccb->remote_cid) { + /* The signalling Identifier already confirmed this DISC_RSP answers our + * DISC_REQ, so no further response will arrive for this transaction. + * Complete the teardown below even though the echoed DCID is malformed; + * returning here would leave the CCB in CST_W4_L2CAP_DISCONNECT_RSP until + * the 10s RTX timer fires. l2cu_release_ccb() below stops that timer. */ + L2C_BLE_COC_TRACE_WARN("rx DISC_RSP rcid mismatch lcid=0x%04x rcid=0x%04x expect=0x%04x", + lcid, rcid, p_ccb->remote_cid); + } + + L2C_BLE_COC_TRACE_DEBUG("rx DISC_RSP lcid=0x%04x rcid=0x%04x", lcid, rcid); + /* The DisconnectInd callback runs synchronously and may re-enter L2CAP and + * release/reuse this CCB slot; re-fetch by the saved lcid and only release if + * it is still the same, in-use CoC CCB (mirrors l2c_ble_le_coc_open_channel). */ + UINT16 saved_lcid = p_ccb->local_cid; + l2c_ble_le_coc_notify_disconnect(p_ccb, TRUE); + tL2C_CCB *p_now = l2cu_find_ccb_by_cid(NULL, saved_lcid); + if (p_now == p_ccb && p_ccb->in_use && p_ccb->le_coc_active) { + l2cu_release_ccb(p_ccb); + } else { + L2C_BLE_COC_TRACE_DEBUG("DISC_RSP: lcid=0x%04x freed in cb, skip release", saved_lcid); + } +} + +static BOOLEAN l2c_ble_le_coc_send_frame(tL2C_CCB *p_ccb, const UINT8 *data, UINT16 len) +{ + BT_HDR *p_buf; + UINT8 *pkt; + + if (p_ccb->peer_conn_cfg.credits == 0) { + return FALSE; + } + + p_buf = (BT_HDR *)osi_malloc(sizeof(BT_HDR) + HCI_DATA_PREAMBLE_SIZE + + L2CAP_PKT_OVERHEAD + len); + if (p_buf == NULL) { + return FALSE; + } + + p_buf->offset = HCI_DATA_PREAMBLE_SIZE; + p_buf->len = L2CAP_PKT_OVERHEAD + len; + p_buf->event = 0; + p_buf->layer_specific = 0; + pkt = (UINT8 *)(p_buf + 1) + p_buf->offset; + UINT16_TO_STREAM(pkt, len); + UINT16_TO_STREAM(pkt, p_ccb->remote_cid); + memcpy(pkt, data, len); + + l2cu_set_acl_hci_header(p_buf, p_ccb); + l2c_link_check_send_pkts(p_ccb->p_lcb, p_ccb, p_buf); + p_ccb->peer_conn_cfg.credits--; + + L2C_BLE_COC_TRACE_DEBUG("tx frame lcid=0x%04x len=%u tx_cred=%u", + p_ccb->local_cid, len, p_ccb->peer_conn_cfg.credits); + return TRUE; +} + +static void l2c_ble_le_coc_try_xmit(tL2C_CCB *p_ccb) +{ + UINT16 mps, chunk, sdu_len, hdr_len; + const UINT8 *src; + BT_HDR *p_sdu; + BOOLEAN completed_sdu = FALSE; + + if (p_ccb == NULL || p_ccb->chnl_state != CST_OPEN) { + return; + } + + /* Re-entrancy guard. The uncongested CongestionStatus_Cb(FALSE) below can be + * invoked synchronously (e.g. the EATT congestion callback -> + * gatt_cl_send_next_cmd_inq -> data_write -> try_xmit), which would otherwise + * recurse once per credit and overflow the BTU task stack. Instead, a + * re-entrant call just flags a rerun and returns; the outermost invocation + * loops to drain the newly queued data. This preserves the self-clocked TX + * (UNSTALLED-after-each-SDU) behaviour without unbounded recursion. */ + if (p_ccb->le_coc_xmit_busy) { + p_ccb->le_coc_xmit_rerun = TRUE; + return; + } + p_ccb->le_coc_xmit_busy = TRUE; + +again: + /* Re-validate the state on every (re)entry, not just at function entry. The + * uncongested callback below can synchronously disconnect the channel + * (chnl_state -> CST_W4_L2CAP_DISCONNECT_RSP after DISC_REQ is sent) and set + * le_coc_xmit_rerun, so a plain "goto again" would otherwise resume the TX + * loop on a non-OPEN channel and emit K-frames after the DISC_REQ. Reset the + * busy flag so a recycled CCB is not left permanently blocked for TX. */ + if (p_ccb->chnl_state != CST_OPEN) { + p_ccb->le_coc_xmit_busy = FALSE; + return; + } + completed_sdu = FALSE; + while (p_ccb->peer_conn_cfg.credits > 0) { + if (p_ccb->le_coc_tx_sdu == NULL) { + if (fixed_queue_is_empty(p_ccb->xmit_hold_q)) { + break; + } + p_ccb->le_coc_tx_sdu = (BT_HDR *)fixed_queue_dequeue(p_ccb->xmit_hold_q, 0); + p_ccb->le_coc_tx_offset = 0; + p_ccb->le_coc_tx_len_sent = FALSE; + } + + p_sdu = p_ccb->le_coc_tx_sdu; + sdu_len = p_sdu->len; + mps = l2c_ble_le_coc_effective_mps(p_ccb); + src = (UINT8 *)(p_sdu + 1) + p_sdu->offset; + + if (!p_ccb->le_coc_tx_len_sent) { + UINT8 *frame_buf; + hdr_len = L2C_BLE_COC_SDU_LEN_SIZE; + if (mps < hdr_len) { + break; + } + frame_buf = (UINT8 *)osi_malloc(mps); + if (frame_buf == NULL) { + break; + } + frame_buf[0] = (UINT8)(sdu_len & 0xFF); + frame_buf[1] = (UINT8)((sdu_len >> 8) & 0xFF); + chunk = mps - hdr_len; + if (chunk > sdu_len) { + chunk = sdu_len; + } + memcpy(frame_buf + hdr_len, src, chunk); + if (!l2c_ble_le_coc_send_frame(p_ccb, frame_buf, hdr_len + chunk)) { + osi_free(frame_buf); + break; + } + osi_free(frame_buf); + p_ccb->le_coc_tx_offset += chunk; + if (p_ccb->le_coc_tx_offset >= sdu_len) { + osi_free(p_ccb->le_coc_tx_sdu); + p_ccb->le_coc_tx_sdu = NULL; + completed_sdu = TRUE; + } else { + p_ccb->le_coc_tx_len_sent = TRUE; + } + } else { + chunk = sdu_len - p_ccb->le_coc_tx_offset; + if (chunk > mps) { + chunk = mps; + } + if (!l2c_ble_le_coc_send_frame(p_ccb, src + p_ccb->le_coc_tx_offset, chunk)) { + break; + } + p_ccb->le_coc_tx_offset += chunk; + if (p_ccb->le_coc_tx_offset >= sdu_len) { + osi_free(p_ccb->le_coc_tx_sdu); + p_ccb->le_coc_tx_sdu = NULL; + completed_sdu = TRUE; + } + } + } + + if (p_ccb->peer_conn_cfg.credits == 0 && p_ccb->p_rcb && + p_ccb->p_rcb->api.pL2CA_CongestionStatus_Cb && !p_ccb->cong_sent) { + p_ccb->cong_sent = TRUE; + (*p_ccb->p_rcb->api.pL2CA_CongestionStatus_Cb)(p_ccb->local_cid, TRUE); + L2C_BLE_COC_TRACE_DEBUG("tx congested lcid=0x%04x", p_ccb->local_cid); + } else if (completed_sdu && p_ccb->peer_conn_cfg.credits > 0 && + p_ccb->le_coc_tx_sdu == NULL && + fixed_queue_is_empty(p_ccb->xmit_hold_q) && + p_ccb->p_rcb && p_ccb->p_rcb->api.pL2CA_CongestionStatus_Cb) { + /* SDU fully sent and pipeline drained with credits to spare: signal the + * app it may send the next SDU. This self-clocks TX without polling. The + * re-entrancy guard above turns any synchronous re-entry from this + * callback into an iterative rerun instead of deep recursion. */ + p_ccb->cong_sent = FALSE; + (*p_ccb->p_rcb->api.pL2CA_CongestionStatus_Cb)(p_ccb->local_cid, FALSE); + } + + /* A re-entrant call (typically triggered by the callback above) queued more + * data; drain it here in the outer frame rather than on a nested stack. */ + if (p_ccb->le_coc_xmit_rerun) { + p_ccb->le_coc_xmit_rerun = FALSE; + goto again; + } + p_ccb->le_coc_xmit_busy = FALSE; +} + +UINT8 l2c_ble_le_coc_data_write(UINT16 lcid, BT_HDR *p_data) +{ + tL2C_CCB *p_ccb; + + if (p_data == NULL) { + return L2CAP_DW_FAILED; + } + + p_ccb = l2cu_find_ccb_by_cid(NULL, lcid); + if (p_ccb == NULL || !p_ccb->le_coc_active || p_ccb->chnl_state != CST_OPEN) { + osi_free(p_data); + return L2CAP_DW_FAILED; + } + + if (p_data->len > p_ccb->peer_conn_cfg.mtu) { + L2C_BLE_COC_TRACE_WARN("SDU too large lcid=0x%04x len=%u mtu=%u", + lcid, p_data->len, p_ccb->peer_conn_cfg.mtu); + osi_free(p_data); + return L2CAP_DW_FAILED; + } + + L2C_BLE_COC_TRACE_DEBUG("data_write lcid=0x%04x len=%u", lcid, p_data->len); + /* fixed_queue_enqueue returns FALSE on OOM (list node alloc) without taking + * ownership of p_data; free it and report failure instead of leaking + losing + * the SDU while wrongly reporting success. */ + if (!fixed_queue_enqueue(p_ccb->xmit_hold_q, p_data, FIXED_QUEUE_MAX_TIMEOUT)) { + L2C_BLE_COC_TRACE_ERROR("xmit enqueue failed lcid=0x%04x", lcid); + osi_free(p_data); + return L2CAP_DW_FAILED; + } + l2c_ble_le_coc_try_xmit(p_ccb); + + if (p_ccb->peer_conn_cfg.credits == 0 && + (p_ccb->le_coc_tx_sdu != NULL || !fixed_queue_is_empty(p_ccb->xmit_hold_q))) { + return L2CAP_DW_CONGESTED; + } + return L2CAP_DW_SUCCESS; +} + +BOOLEAN l2c_ble_le_coc_is_congested(UINT16 lcid) +{ + tL2C_CCB *p_ccb; + + p_ccb = l2cu_find_ccb_by_cid(NULL, lcid); + if (p_ccb == NULL || !p_ccb->le_coc_active || p_ccb->chnl_state != CST_OPEN) { + return TRUE; + } + if (p_ccb->cong_sent) { + return TRUE; + } + if (p_ccb->le_coc_tx_sdu != NULL || !fixed_queue_is_empty(p_ccb->xmit_hold_q)) { + return TRUE; + } + return FALSE; +} + +BOOLEAN l2c_ble_le_coc_give_credits(UINT16 lcid, UINT16 credits) +{ + tL2C_CCB *p_ccb; + + if (credits == 0) { + return FALSE; + } + + p_ccb = l2cu_find_ccb_by_cid(NULL, lcid); + if (p_ccb == NULL || !p_ccb->le_coc_active || p_ccb->chnl_state != CST_OPEN) { + return FALSE; + } + + /* In auto-credit mode the stack returns credits per consumed frame, so a + * manual return (app recv_ready) would double-count and over-grant. Ignore. */ + if (!p_ccb->le_coc_no_auto_credit) { + return TRUE; + } + + if (p_ccb->p_lcb == NULL) { + return FALSE; + } + + /* Manual mode: return the credits actually consumed by the delivered SDU(s), + * not the caller's nominal count. data_ind() tracked one credit per consumed + * K-frame in le_coc_rx_manual_owed, so a multi-frame SDU returns >1 credit + * even though the app calls recv_ready once. The caller's `credits` argument + * is intentionally ignored here (recv_ready always means "return what the + * processed SDU consumed"). Nothing owed (e.g. a pre-auth recv_ready before + * any data) is a harmless no-op: the initial window was granted at open. */ + UINT16 give = p_ccb->le_coc_rx_manual_owed; + if (give == 0) { + return TRUE; + } + if ((UINT32)p_ccb->le_coc_rx_avail + give > L2CAP_LE_MAX_CREDIT) { + /* Clamp so we never advertise more than the peer's max window; keep the + * remainder owed to return on the next recv_ready. */ + give = L2CAP_LE_MAX_CREDIT - p_ccb->le_coc_rx_avail; + if (give == 0) { + L2C_BLE_COC_TRACE_ERROR("give_credits window full lcid=0x%04x", lcid); + return FALSE; + } + } + + p_ccb->le_coc_rx_manual_owed -= give; + p_ccb->le_coc_rx_avail += give; + L2C_BLE_COC_TRACE_DEBUG("give_credits lcid=0x%04x +%u owed=%u", lcid, give, p_ccb->le_coc_rx_manual_owed); + l2cble_send_flow_control_credit(p_ccb, give); + return TRUE; +} + +BOOLEAN l2c_ble_le_coc_disconnect(UINT16 lcid) +{ + tL2C_CCB *p_ccb; + + p_ccb = l2cu_find_ccb_by_cid(NULL, lcid); + if (p_ccb == NULL || !p_ccb->le_coc_active) { + return FALSE; + } + + /* Already tearing down: don't emit a duplicate disconnect request. */ + if (p_ccb->chnl_state == CST_W4_L2CAP_DISCONNECT_RSP) { + return TRUE; + } + +#if (BLE_L2CAP_COC_SERVER_INCLUDED == TRUE) + /* Incoming connection still awaiting the app's accept/reject decision: the + * peer sent an LE credit based connection request that we owe a response to. + * Reject it (sends 0x15 with a failure result) instead of silently dropping + * the CCB, which would leave the peer waiting for a response. */ + if (p_ccb->chnl_state == CST_W4_L2CA_CONNECT_RSP) { + L2C_BLE_COC_TRACE_DEBUG("reject pending incoming coc lcid=0x%04x", lcid); + l2c_ble_le_coc_connect_rsp(p_ccb, L2CAP_LE_RESULT_NO_RESOURCES); + return TRUE; + } +#endif + + /* A channel still connecting has remote_cid == 0. Emitting a disconnect + * request with a 0x0000 destination CID violates the spec (Core Spec v6.2 + * Vol 3 Part A 2.1) and the peer will not answer, leaving the CCB stuck. + * Cancel such a pending connection locally instead. */ + if (p_ccb->chnl_state != CST_OPEN || p_ccb->remote_cid == 0) { + L2C_BLE_COC_TRACE_DEBUG("cancel pending coc lcid=0x%04x state=%d", lcid, p_ccb->chnl_state); + l2cu_release_ccb(p_ccb); + return TRUE; + } + + L2C_BLE_COC_TRACE_DEBUG("disconnect lcid=0x%04x", lcid); + l2c_ble_le_coc_initiate_disc(p_ccb); + return TRUE; +} + +void l2c_ble_le_coc_data_ind(tL2C_CCB *p_ccb, BT_HDR *p_msg) +{ + UINT8 *p; + UINT16 frame_len, sdu_len, copy_len, hdr_need; + tL2CA_DATA_IND_CB *cb; + + if (p_ccb == NULL || p_msg == NULL || !p_ccb->le_coc_active) { + osi_free(p_msg); + return; + } + + if (p_ccb->chnl_state != CST_OPEN) { + L2C_BLE_COC_TRACE_WARN("data on non-open lcid=0x%04x state=%d", + p_ccb->local_cid, p_ccb->chnl_state); + osi_free(p_msg); + return; + } + + if (p_ccb->le_coc_rx_avail == 0) { + L2C_BLE_COC_TRACE_ERROR("rx credit exhausted lcid=0x%04x", p_ccb->local_cid); + l2c_ble_le_coc_initiate_disc(p_ccb); + osi_free(p_msg); + return; + } + + frame_len = p_msg->len; + p = (UINT8 *)(p_msg + 1) + p_msg->offset; + p_ccb->le_coc_rx_avail--; + + /* Core Spec v6.2 Vol 3 Part A 3.4.3: "If the payload size of any K-frame + * exceeds the receiver's MPS, the receiver shall disconnect the channel." + * frame_len is the K-frame information payload (basic L2CAP header already + * stripped), so it must not exceed the MPS we advertised for this channel. */ + if (frame_len > p_ccb->local_conn_cfg.mps) { + L2C_BLE_COC_TRACE_ERROR("K-frame %u > mps %u lcid=0x%04x", + frame_len, p_ccb->local_conn_cfg.mps, p_ccb->local_cid); + l2c_ble_le_coc_initiate_disc(p_ccb); + osi_free(p_msg); + return; + } + + if (!p_ccb->le_coc_rx_have_len) { + if (frame_len < L2C_BLE_COC_SDU_LEN_SIZE) { + L2C_BLE_COC_TRACE_ERROR("short first frame lcid=0x%04x", p_ccb->local_cid); + l2c_ble_le_coc_initiate_disc(p_ccb); + osi_free(p_msg); + return; + } + sdu_len = p[0] | (p[1] << 8); + if (sdu_len > p_ccb->local_conn_cfg.mtu) { + L2C_BLE_COC_TRACE_ERROR("SDU len %u > mtu %u", sdu_len, p_ccb->local_conn_cfg.mtu); + l2c_ble_le_coc_initiate_disc(p_ccb); + osi_free(p_msg); + return; + } + + p_ccb->le_coc_rx_sdu = (BT_HDR *)osi_malloc(sizeof(BT_HDR) + sdu_len); + if (p_ccb->le_coc_rx_sdu == NULL) { + /* Reassembly buffer OOM: disconnect instead of silently dropping. + * le_coc_rx_have_len is still FALSE here, so keeping the channel up + * would misparse the peer's subsequent continuation K-frames as new + * first frames and desync reassembly. */ + L2C_BLE_COC_TRACE_ERROR("rx SDU alloc failed lcid=0x%04x len=%u", p_ccb->local_cid, sdu_len); + l2c_ble_le_coc_initiate_disc(p_ccb); + osi_free(p_msg); + return; + } + p_ccb->le_coc_rx_sdu->offset = 0; + p_ccb->le_coc_rx_sdu->len = 0; + p_ccb->le_coc_rx_sdu_total = sdu_len; + p_ccb->le_coc_rx_sdu_rcvd = 0; + p_ccb->le_coc_rx_have_len = TRUE; + + copy_len = frame_len - L2C_BLE_COC_SDU_LEN_SIZE; + /* Core Spec v6.2 Vol 3 Part A 3.4.3: "If the sum of the payload sizes + * ... exceeds the specified SDU length, the receiver shall disconnect + * the channel." A first frame carrying more data than the declared SDU + * is malformed; disconnect instead of silently truncating (which would + * misalign reassembly against the peer's intended SDU boundaries). */ + if (copy_len > sdu_len) { + L2C_BLE_COC_TRACE_ERROR("first frame data %u > sdu %u lcid=0x%04x", + copy_len, sdu_len, p_ccb->local_cid); + l2c_ble_le_coc_initiate_disc(p_ccb); + osi_free(p_msg); + return; + } + memcpy((UINT8 *)(p_ccb->le_coc_rx_sdu + 1), p + L2C_BLE_COC_SDU_LEN_SIZE, copy_len); + p_ccb->le_coc_rx_sdu->len = copy_len; + p_ccb->le_coc_rx_sdu_rcvd = copy_len; + } else { + copy_len = frame_len; + hdr_need = p_ccb->le_coc_rx_sdu_total - p_ccb->le_coc_rx_sdu_rcvd; + /* Same 3.4.3 "shall disconnect" rule: a continuation frame that pushes + * the running total past the declared SDU length is malformed. */ + if (copy_len > hdr_need) { + L2C_BLE_COC_TRACE_ERROR("cont frame %u > remaining %u lcid=0x%04x", + copy_len, hdr_need, p_ccb->local_cid); + l2c_ble_le_coc_initiate_disc(p_ccb); + osi_free(p_msg); + return; + } + memcpy((UINT8 *)(p_ccb->le_coc_rx_sdu + 1) + p_ccb->le_coc_rx_sdu_rcvd, p, copy_len); + p_ccb->le_coc_rx_sdu->len += copy_len; + p_ccb->le_coc_rx_sdu_rcvd += copy_len; + } + + osi_free(p_msg); + + /* Return one RX credit for the K-frame just consumed. In auto-credit mode we + * batch the returns and flush when half the window has been used, or + * immediately if the window is empty, so the peer keeps a healthy credit + * pipeline and never ping-pongs at a single credit. */ + if (!p_ccb->le_coc_no_auto_credit) { + UINT16 window = p_ccb->local_conn_cfg.credits; + UINT16 flush_at = window ? ((window + 1) / 2) : 1; + + p_ccb->le_coc_rx_credits_pending++; + if (p_ccb->le_coc_rx_credits_pending >= flush_at || p_ccb->le_coc_rx_avail == 0) { + UINT16 give = p_ccb->le_coc_rx_credits_pending; + p_ccb->le_coc_rx_credits_pending = 0; + p_ccb->le_coc_rx_avail += give; + l2cble_send_flow_control_credit(p_ccb, give); + } + } else { + /* Manual mode: the stack does not return the credit now. Track each + * consumed K-frame so recv_ready (l2c_ble_le_coc_give_credits) can return + * the exact number of credits this SDU used. A multi-frame SDU consumes + * >1 credit while the app calls recv_ready once per SDU; returning a fixed + * 1 would leak (credit consumed per K-frame, returned per SDU). */ + p_ccb->le_coc_rx_manual_owed++; + + /* Deadlock breaker: if the peer's credit is now exhausted (avail == 0) + * while the current SDU is still incomplete, the app can never receive + * DATA_RECEIVED and thus never call recv_ready to replenish -> permanent + * stall. Return the owed credits now so the peer can finish this SDU. + * This only triggers for an SDU larger than the whole RX window; an SDU + * that fits the window never reaches avail == 0 mid-reassembly (it hits 0 + * only on its final frame, when it is already complete), so normal + * per-SDU backpressure is fully preserved for the supported range. */ + if (p_ccb->le_coc_rx_avail == 0 && + p_ccb->le_coc_rx_have_len && + p_ccb->le_coc_rx_sdu_rcvd < p_ccb->le_coc_rx_sdu_total) { + UINT16 give = p_ccb->le_coc_rx_manual_owed; + p_ccb->le_coc_rx_manual_owed = 0; + p_ccb->le_coc_rx_avail += give; + L2C_BLE_COC_TRACE_DEBUG("manual deadlock breaker lcid=0x%04x return %u", p_ccb->local_cid, give); + l2cble_send_flow_control_credit(p_ccb, give); + } + } + + if (p_ccb->le_coc_rx_sdu_rcvd < p_ccb->le_coc_rx_sdu_total) { + return; + } + + L2C_BLE_COC_TRACE_DEBUG("SDU complete lcid=0x%04x len=%u", p_ccb->local_cid, p_ccb->le_coc_rx_sdu_total); + + p_ccb->le_coc_rx_have_len = FALSE; + p_ccb->le_coc_rx_sdu_total = 0; + p_ccb->le_coc_rx_sdu_rcvd = 0; + + /* Transfer ownership of the completed SDU before invoking the callback: + * clear le_coc_rx_sdu first so that if the app synchronously tears the + * channel down from within DataInd_Cb, cleanup_ccb() cannot free the same + * buffer again (double free) and we never touch p_ccb after it may be gone. */ + BT_HDR *rx_sdu = p_ccb->le_coc_rx_sdu; + p_ccb->le_coc_rx_sdu = NULL; + if (p_ccb->p_rcb && p_ccb->p_rcb->api.pL2CA_DataInd_Cb) { + cb = p_ccb->p_rcb->api.pL2CA_DataInd_Cb; + (*cb)(p_ccb->local_cid, rx_sdu); + } else { + osi_free(rx_sdu); + } +} + +#endif /* BLE_L2CAP_COC_INCLUDED == TRUE */ diff --git a/components/bt/host/bluedroid/stack/l2cap/l2c_link.c b/components/bt/host/bluedroid/stack/l2cap/l2c_link.c index 1a74d33dcec..b1f06652963 100644 --- a/components/bt/host/bluedroid/stack/l2cap/l2c_link.c +++ b/components/bt/host/bluedroid/stack/l2cap/l2c_link.c @@ -478,6 +478,7 @@ BOOLEAN l2c_link_hci_disc_comp (UINT16 handle, UINT8 reason) while (!list_is_empty(p_lcb->link_xmit_data_q)) { p_buf = list_front(p_lcb->link_xmit_data_q); list_remove(p_lcb->link_xmit_data_q, p_buf); + p_buf->event = 0; osi_free(p_buf); } } else @@ -1629,6 +1630,11 @@ void l2c_link_segments_xmitted (BT_HDR *p_msg) /* Find the LCB based on the handle */ if ((p_lcb = l2cu_find_lcb_by_handle (handle)) == NULL) { L2CAP_TRACE_WARNING ("L2CAP - rcvd segment complete, unknown handle: %d\n", handle); + /* The partial segment being bounced back here was already removed from + * link_xmit_data_q before it was handed to the controller, so it is not + * freed by l2cu_release_lcb()/disc_comp when the link goes away. This + * function is its sole owner, so it must be freed here to avoid a leak. */ + p_msg->event = 0; osi_free (p_msg); return; } diff --git a/components/bt/host/bluedroid/stack/l2cap/l2c_main.c b/components/bt/host/bluedroid/stack/l2cap/l2c_main.c index 221109a1b66..f9b28cbc805 100644 --- a/components/bt/host/bluedroid/stack/l2cap/l2c_main.c +++ b/components/bt/host/bluedroid/stack/l2cap/l2c_main.c @@ -311,6 +311,13 @@ void l2c_rcv_acl_data (BT_HDR *p_msg) if (p_ccb == NULL) { osi_free (p_msg); } else { +#if (BLE_L2CAP_COC_INCLUDED == TRUE) + /* LE CoC data plane only; BR/EDR dynamic channels use l2c_csm / l2c_fcr below */ + if (p_lcb->transport == BT_TRANSPORT_LE && l2c_ble_le_coc_is_chan(p_ccb)) { + l2c_ble_le_coc_data_ind(p_ccb, p_msg); + return; + } +#endif if (p_lcb->transport == BT_TRANSPORT_LE) { l2c_link_check_send_pkts (p_ccb->p_lcb, NULL, NULL); } @@ -1147,11 +1154,41 @@ void l2c_process_timeout (TIMER_LIST_ENT *p_tle) * re-issue the connection attempt now. */ l2c_link_create_conn_retry ((tL2C_LCB *)p_tle->param); break; +#endif ///CLASSIC_BT_INCLUDED == TRUE - case BTU_TTYPE_L2CAP_CHNL: - l2c_csm_execute (((tL2C_CCB *)p_tle->param), L2CEVT_TIMEOUT, NULL); + case BTU_TTYPE_L2CAP_CHNL: { +#if (BLE_L2CAP_COC_INCLUDED == TRUE) + tL2C_CCB *p_ccb = (tL2C_CCB *)p_tle->param; + /* LE CoC/ECFC channels do not use the classic state machine; a per-CCB + * BTU_TTYPE_L2CAP_CHNL timer is their connect/reconfigure response + * timeout. Route it to the CoC handler. */ + if (p_ccb != NULL && p_ccb->le_coc_active) { + l2c_ble_le_coc_channel_timeout(p_ccb); + break; + } + /* Keep the NULL handling consistent with the CoC check above: the classic + * state machine dereferences p_ccb unconditionally, so bail out here + * instead of passing a NULL CCB down to l2c_csm_execute. */ + if (p_ccb == NULL) { + L2CAP_TRACE_WARNING("L2CAP channel timeout with NULL CCB"); + break; + } +#if (CLASSIC_BT_INCLUDED == TRUE) + l2c_csm_execute (p_ccb, L2CEVT_TIMEOUT, NULL); +#else + /* p_ccb may be unused when BT_STACK_NO_LOG strips the trace macro. */ + L2CAP_TRACE_WARNING("Unhandled L2CAP channel timeout for CCB %p", p_ccb); + UNUSED(p_ccb); +#endif +#elif (CLASSIC_BT_INCLUDED == TRUE) + l2c_csm_execute ((tL2C_CCB *)p_tle->param, L2CEVT_TIMEOUT, NULL); +#else + L2CAP_TRACE_WARNING("Unhandled L2CAP channel timeout"); +#endif break; + } +#if (CLASSIC_BT_INCLUDED == TRUE) case BTU_TTYPE_L2CAP_FCR_ACK: l2c_csm_execute (((tL2C_CCB *)p_tle->param), L2CEVT_ACK_TIMEOUT, NULL); break; diff --git a/components/bt/host/bluedroid/stack/l2cap/l2c_utils.c b/components/bt/host/bluedroid/stack/l2cap/l2c_utils.c index 2cf6443dccb..978b4c37a7e 100644 --- a/components/bt/host/bluedroid/stack/l2cap/l2c_utils.c +++ b/components/bt/host/bluedroid/stack/l2cap/l2c_utils.c @@ -108,7 +108,9 @@ tL2C_LCB *l2cu_allocate_lcb (BD_ADDR p_bd_addr, BOOLEAN is_bonding, tBT_TRANSPOR #if (BLE_INCLUDED == TRUE) p_lcb->transport = transport; p_lcb->tx_data_len = controller_get_interface()->get_ble_default_data_packet_length(); +#if (BLE_L2CAP_COC_INCLUDED == TRUE) p_lcb->le_sec_pending_q = fixed_queue_new(QUEUE_SIZE_MAX); +#endif if (transport == BT_TRANSPORT_LE) { l2cb.num_ble_links_active++; @@ -164,6 +166,16 @@ void l2cu_release_lcb (tL2C_LCB *p_lcb) { tL2C_CCB *p_ccb; + /* Make double-release harmless. Several failure paths (e.g. + * l2cble_init_direct_conn) release the LCB and return FALSE, after which the + * API-level caller (e.g. L2CA_ConnectFixedChnl) releases it again. Without + * this guard the second call would wrongly decrement num_ble_links_active + * and re-run l2cu_process_fixed_disc_cback on an already freed LCB. A valid + * LCB always has in_use == TRUE (set in l2cu_allocate_lcb). */ + if (p_lcb == NULL || !p_lcb->in_use) { + return; + } + L2CAP_TRACE_DEBUG("%s handle=%u bda="MACSTR"", __func__, p_lcb->handle, MAC2STR(p_lcb->remote_bd_addr)); @@ -253,6 +265,7 @@ void l2cu_release_lcb (tL2C_LCB *p_lcb) while (!list_is_empty(p_lcb->link_xmit_data_q)) { BT_HDR *p_buf = list_front(p_lcb->link_xmit_data_q); list_remove(p_lcb->link_xmit_data_q, p_buf); + p_buf->event = 0; osi_free(p_buf); } list_free(p_lcb->link_xmit_data_q); @@ -294,7 +307,7 @@ void l2cu_release_lcb (tL2C_LCB *p_lcb) (*p_cb) (L2CAP_PING_RESULT_NO_LINK); } -#if (BLE_INCLUDED == TRUE) +#if (BLE_INCLUDED == TRUE) && (BLE_L2CAP_COC_INCLUDED == TRUE) /* Check and release all the LE COC connections waiting for security */ if (p_lcb->le_sec_pending_q) { @@ -1721,8 +1734,18 @@ void l2cu_release_ccb (tL2C_CCB *p_ccb) if (!p_ccb->in_use) { return; } +#if (BLE_L2CAP_ENHANCED_COC_INCLUDED == TRUE) + if (p_lcb != NULL && p_lcb->transport == BT_TRANSPORT_LE) { + l2c_ble_ecfc_on_ccb_release(p_ccb); + } +#endif +#if (BLE_L2CAP_COC_INCLUDED == TRUE) + if (p_lcb != NULL && p_lcb->transport == BT_TRANSPORT_LE && p_ccb->le_coc_active) { + l2c_ble_le_coc_cleanup_ccb(p_ccb); + } +#endif #if BLE_INCLUDED == TRUE - if (p_lcb->transport == BT_TRANSPORT_LE) { + if (p_lcb != NULL && p_lcb->transport == BT_TRANSPORT_LE) { /* Take samephore to avoid race condition */ l2ble_update_att_acl_pkt_num(L2CA_BUFF_FREE, NULL); } @@ -1995,6 +2018,32 @@ tL2C_RCB *l2cu_find_ble_rcb_by_psm (UINT16 psm) /* If here, no match found */ return (NULL); } + +/******************************************************************************* +** +** Function l2cu_find_ble_rcb_by_real_psm +** +** Description Look through the BLE Registration Control Blocks to see if +** anyone registered to handle the application PSM in question +** +** Returns Pointer to the BLE RCB or NULL if not found +** +*******************************************************************************/ +tL2C_RCB *l2cu_find_ble_rcb_by_real_psm (UINT16 real_psm) +{ + tL2C_RCB *p_rcb = &l2cb.ble_rcb_pool[0]; + UINT16 xx; + + for (xx = 0; xx < BLE_MAX_L2CAP_CLIENTS; xx++, p_rcb++) + { + if ((p_rcb->in_use) && (p_rcb->real_psm == real_psm)) { + return (p_rcb); + } + } + + /* If here, no match found */ + return (NULL); +} #endif ///BLE_INCLUDED == TRUE #if (L2CAP_COC_INCLUDED == TRUE) @@ -2306,6 +2355,32 @@ void l2cu_device_reset (void) ** ** Returns TRUE if successful, FALSE if gki get buffer fails. ** +** LCB OWNERSHIP ON FAILURE - READ BEFORE "FIXING" A LEAK HERE: +** The release contract of this function is deliberately NOT uniform, and the +** callers rely on the current behaviour. Do NOT add an unconditional +** l2cu_release_lcb(p_lcb) around the FALSE returns below - it causes a +** use-after-free + double free (see l2c_link_hci_disc_comp). +** +** Per-path behaviour on a FALSE return: +** - BLE connect path (l2cble_create_conn -> l2cble_init_direct_conn) and the +** classic l2cu_create_conn_after_switch RELEASE p_lcb internally on their +** own failures. Callers must therefore NOT release again on those paths. +** - The "!supports_ble()" and the trailing "return false" paths do NOT +** release p_lcb (kept as-is on purpose). +** +** Caller expectations (all currently satisfied by the above): +** - l2c_link_hci_disc_comp() keeps using p_lcb after a FALSE return and +** releases it itself at the end via lcb_is_free (see the explicit +** "must not release the LCB on failure" note there). Releasing internally +** would UAF/double-free this hot disconnect+reconnect path. +** - L2CA_ConnectFixedChnl() releases p_lcb itself on FALSE. +** - The LE CoC/ECFC callers (L2CA_ConnectLECocReq / L2CA_ConnectLEEcocReq) +** do NOT release on FALSE; they rely on the BLE path having released. The +** only genuine leak is the (practically unreachable) !supports_ble() path +** for those callers - if that must be closed, do it at the CoC API entry +** (pre-check supports_ble and release the freshly-allocated LCB there), +** not by changing the contract of this function. +** *******************************************************************************/ BOOLEAN l2cu_create_conn (tL2C_LCB *p_lcb, tBT_TRANSPORT transport) { @@ -2327,6 +2402,9 @@ BOOLEAN l2cu_create_conn (tL2C_LCB *p_lcb, tBT_TRANSPORT transport) if (transport == BT_TRANSPORT_LE) { if (!controller_get_interface()->supports_ble()) { + /* Intentionally does NOT release p_lcb (see the ownership note in the + * function header). Practically unreachable for LE callers; close the + * CoC leak at the API entry, not here. */ return FALSE; } if(addr_type > BLE_ADDR_TYPE_MAX) { @@ -2384,6 +2462,9 @@ BOOLEAN l2cu_create_conn (tL2C_LCB *p_lcb, tBT_TRANSPORT transport) return (l2cu_create_conn_after_switch (p_lcb)); #endif // (CLASSIC_BT_INCLUDED == TRUE) + /* Fallthrough only in a BLE-only build reached with a non-LE transport + * (effectively dead). Intentionally does NOT release p_lcb - see the + * ownership note in the function header. */ return false; } @@ -3270,6 +3351,128 @@ void l2cu_send_peer_ble_credit_based_disconn_req(tL2C_CCB *p_ccb) l2c_link_check_send_pkts (p_lcb, NULL, p_buf); } +#if (BLE_L2CAP_ENHANCED_COC_INCLUDED == TRUE) +BOOLEAN l2cu_send_peer_ble_enhanced_credit_conn_req(tL2C_LCB *p_lcb, UINT8 sig_id, UINT16 psm, + UINT16 mtu, UINT16 mps, UINT16 credits, UINT8 num_chan, UINT16 *p_scids) +{ + BT_HDR *p_buf; + UINT8 *p; + UINT16 len = L2CAP_CMD_BLE_ENHANCED_CONN_REQ_BASE_LEN + num_chan * sizeof(UINT16); + + if (p_lcb == NULL || p_scids == NULL || num_chan == 0) { + return FALSE; + } + + if ((p_buf = l2cu_build_header(p_lcb, len, L2CAP_CMD_BLE_ENHANCED_CONN_REQ, sig_id)) == NULL) { + L2CAP_TRACE_WARNING("LE_ECFC tx 0x17 build_header failed sig_id=%u", sig_id); + return FALSE; + } + + p = (UINT8 *)(p_buf + 1) + L2CAP_SEND_CMD_OFFSET + HCI_DATA_PREAMBLE_SIZE + + L2CAP_PKT_OVERHEAD + L2CAP_CMD_OVERHEAD; + + UINT16_TO_STREAM(p, psm); + UINT16_TO_STREAM(p, mtu); + UINT16_TO_STREAM(p, mps); + UINT16_TO_STREAM(p, credits); + for (UINT8 i = 0; i < num_chan; i++) { + UINT16_TO_STREAM(p, p_scids[i]); + } + + l2c_link_check_send_pkts(p_lcb, NULL, p_buf); + return TRUE; +} + +void l2cu_send_peer_ble_enhanced_credit_conn_res(tL2C_LCB *p_lcb, UINT8 rem_id, + UINT16 mtu, UINT16 mps, UINT16 credits, UINT16 result, UINT8 num_chan, UINT16 *p_dcids) +{ + BT_HDR *p_buf; + UINT8 *p; + UINT16 len = L2CAP_CMD_BLE_ENHANCED_CONN_RES_BASE_LEN + num_chan * sizeof(UINT16); + + if (p_lcb == NULL) { + return; + } + + if ((p_buf = l2cu_build_header(p_lcb, len, L2CAP_CMD_BLE_ENHANCED_CONN_RES, rem_id)) == NULL) { + L2CAP_TRACE_WARNING("LE_ECFC tx 0x18 build_header failed rem_id=%u", rem_id); + return; + } + + p = (UINT8 *)(p_buf + 1) + L2CAP_SEND_CMD_OFFSET + HCI_DATA_PREAMBLE_SIZE + + L2CAP_PKT_OVERHEAD + L2CAP_CMD_OVERHEAD; + + UINT16_TO_STREAM(p, mtu); + UINT16_TO_STREAM(p, mps); + UINT16_TO_STREAM(p, credits); + UINT16_TO_STREAM(p, result); + for (UINT8 i = 0; i < num_chan; i++) { + UINT16 dcid = (p_dcids != NULL) ? p_dcids[i] : 0; + UINT16_TO_STREAM(p, dcid); + } + + l2c_link_check_send_pkts(p_lcb, NULL, p_buf); +} + +void l2cu_reject_ble_enhanced_connection(tL2C_LCB *p_lcb, UINT8 rem_id, UINT16 result, UINT8 num_scids) +{ + if (num_scids == 0) { + num_scids = 1; + } + l2cu_send_peer_ble_enhanced_credit_conn_res(p_lcb, rem_id, 0, 0, 0, result, num_scids, NULL); +} + +BOOLEAN l2cu_send_peer_ble_credit_reconfig_req(tL2C_LCB *p_lcb, UINT8 sig_id, + UINT16 mtu, UINT16 mps, UINT8 num_chan, UINT16 *p_dcids) +{ + BT_HDR *p_buf; + UINT8 *p; + UINT16 len = L2CAP_CMD_BLE_CREDIT_RECONFIG_REQ_BASE_LEN + num_chan * sizeof(UINT16); + + if (p_lcb == NULL || p_dcids == NULL || num_chan == 0) { + return FALSE; + } + + if ((p_buf = l2cu_build_header(p_lcb, len, L2CAP_CMD_BLE_CREDIT_RECONFIG_REQ, sig_id)) == NULL) { + L2CAP_TRACE_WARNING("LE_ECFC tx 0x19 build_header failed sig_id=%u", sig_id); + return FALSE; + } + + p = (UINT8 *)(p_buf + 1) + L2CAP_SEND_CMD_OFFSET + HCI_DATA_PREAMBLE_SIZE + + L2CAP_PKT_OVERHEAD + L2CAP_CMD_OVERHEAD; + + UINT16_TO_STREAM(p, mtu); + UINT16_TO_STREAM(p, mps); + for (UINT8 i = 0; i < num_chan; i++) { + UINT16_TO_STREAM(p, p_dcids[i]); + } + + l2c_link_check_send_pkts(p_lcb, NULL, p_buf); + return TRUE; +} + +void l2cu_send_peer_ble_credit_reconfig_rsp(tL2C_LCB *p_lcb, UINT8 rem_id, UINT16 result) +{ + BT_HDR *p_buf; + UINT8 *p; + + if (p_lcb == NULL) { + return; + } + + if ((p_buf = l2cu_build_header(p_lcb, L2CAP_CMD_BLE_CREDIT_RECONFIG_RSP_LEN, + L2CAP_CMD_BLE_CREDIT_RECONFIG_RSP, rem_id)) == NULL) { + return; + } + + p = (UINT8 *)(p_buf + 1) + L2CAP_SEND_CMD_OFFSET + HCI_DATA_PREAMBLE_SIZE + + L2CAP_PKT_OVERHEAD + L2CAP_CMD_OVERHEAD; + + UINT16_TO_STREAM(p, result); + l2c_link_check_send_pkts(p_lcb, NULL, p_buf); +} +#endif /* BLE_L2CAP_ENHANCED_COC_INCLUDED == TRUE */ + #endif /* BLE_INCLUDED == TRUE */ /*******************************************************************************