Bluetooth: audio: ascs: Retry ASE state notifications on error

This adds retry logic for ASE state notifications if failed due to
insufficient number of buffers to send ATT PDU. The state transition is
retried after connection interval delay.

Fixes: #64574
Signed-off-by: Mariusz Skamra <mariusz.skamra@codecoup.pl>
This commit is contained in:
Mariusz Skamra 2023-11-13 10:44:15 +01:00 committed by Carles Cufí
commit ba61420504
2 changed files with 150 additions and 104 deletions

View file

@ -14,7 +14,3 @@ CONFIG_BT_ATT_PREPARE_COUNT=1
CONFIG_BT_EXT_ADV=y
CONFIG_BT_DEVICE_NAME="Unicast Audio Server"
# Due to https://github.com/zephyrproject-rtos/zephyr/issues/64574 we need to increase the number
# of L2CAP buffers
CONFIG_BT_L2CAP_TX_BUF_COUNT=4

View file

@ -61,7 +61,7 @@ static struct bt_ascs_ase {
struct bt_bap_ep ep;
const struct bt_gatt_attr *attr;
struct k_work_delayable disconnect_work;
struct k_work state_transition_work;
struct k_work_delayable state_transition_work;
enum bt_bap_ep_state state_pending;
bool unexpected_iso_link_loss;
} ase_pool[CONFIG_BT_ASCS_MAX_ACTIVE_ASES];
@ -124,56 +124,51 @@ static void ase_free(struct bt_ascs_ase *ase)
bt_conn_unref(ase->conn);
ase->conn = NULL;
(void)k_work_cancel(&ase->state_transition_work);
(void)k_work_cancel_delayable(&ase->disconnect_work);
(void)k_work_cancel_delayable(&ase->state_transition_work);
}
static void ase_status_changed(struct bt_ascs_ase *ase, uint8_t state)
static int ase_state_notify(struct bt_ascs_ase *ase)
{
const uint8_t att_ntf_header_size = 3; /* opcode (1) + handle (2) */
struct bt_conn *conn = ase->conn;
struct bt_conn_info conn_info;
uint16_t max_ntf_size;
uint16_t ntf_size;
int err;
LOG_DBG("ase %p id 0x%02x %s -> %s", ase, ase->ep.status.id,
bt_bap_ep_state_str(ascs_ep_get_state(&ase->ep)), bt_bap_ep_state_str(state));
__ASSERT_NO_MSG(conn != NULL);
ase->ep.status.state = state;
err = bt_conn_get_info(conn, &conn_info);
__ASSERT_NO_MSG(err == 0);
if (conn != NULL) {
struct bt_conn_info conn_info;
int err;
err = bt_conn_get_info(conn, &conn_info);
if (err != 0) {
LOG_ERR("Failed to get conn %p info: %d", (void *)conn, err);
return;
}
if (conn_info.state == BT_CONN_STATE_CONNECTED &&
bt_gatt_is_subscribed(conn, ase->attr, BT_GATT_CCC_NOTIFY)) {
const uint8_t att_ntf_header_size = 3; /* opcode (1) + handle (2) */
const uint16_t max_ntf_size = bt_gatt_get_mtu(conn) - att_ntf_header_size;
uint16_t ntf_size;
err = k_sem_take(&ase_buf_sem, ASE_BUF_SEM_TIMEOUT);
if (err != 0) {
LOG_DBG("Failed to take ase_buf_sem: %d", err);
return;
}
ascs_ep_get_status(&ase->ep, &ase_buf);
ntf_size = MIN(max_ntf_size, ase_buf.len);
if (ntf_size < ase_buf.len) {
LOG_DBG("Sending truncated notification (%u / %u)",
ntf_size, ase_buf.len);
}
err = bt_gatt_notify(conn, ase->attr, ase_buf.data, ntf_size);
__ASSERT_NO_MSG(err == 0);
k_sem_give(&ase_buf_sem);
}
if (conn_info.state != BT_CONN_STATE_CONNECTED ||
!bt_gatt_is_subscribed(conn, ase->attr, BT_GATT_CCC_NOTIFY)) {
return 0;
}
err = k_sem_take(&ase_buf_sem, ASE_BUF_SEM_TIMEOUT);
if (err != 0) {
LOG_WRN("Failed to take ase_buf_sem: %d", err);
return err;
}
ascs_ep_get_status(&ase->ep, &ase_buf);
max_ntf_size = bt_gatt_get_mtu(conn) - att_ntf_header_size;
ntf_size = MIN(max_ntf_size, ase_buf.len);
if (ntf_size < ase_buf.len) {
LOG_DBG("Sending truncated notification (%u / %u)",
ntf_size, ase_buf.len);
}
err = bt_gatt_notify(conn, ase->attr, ase_buf.data, ntf_size);
k_sem_give(&ase_buf_sem);
return err;
}
static void ascs_disconnect_stream_work_handler(struct k_work *work)
@ -243,7 +238,7 @@ static int ascs_disconnect_stream(struct bt_bap_stream *stream)
K_MSEC(CONFIG_BT_ASCS_ISO_DISCONNECT_DELAY));
}
static void ase_set_state_idle(struct bt_ascs_ase *ase)
static void ase_enter_state_idle(struct bt_ascs_ase *ase)
{
struct bt_bap_stream *stream = ase->ep.stream;
struct bt_bap_stream_ops *ops;
@ -252,8 +247,6 @@ static void ase_set_state_idle(struct bt_ascs_ase *ase)
ase->ep.receiver_ready = false;
ase_status_changed(ase, BT_BAP_EP_STATE_IDLE);
if (stream->conn != NULL) {
bt_conn_unref(stream->conn);
stream->conn = NULL;
@ -267,7 +260,7 @@ static void ase_set_state_idle(struct bt_ascs_ase *ase)
ase_free(ase);
}
static void ase_set_state_codec_configured(struct bt_ascs_ase *ase)
static void ase_enter_state_codec_configured(struct bt_ascs_ase *ase)
{
struct bt_bap_stream *stream = ase->ep.stream;
struct bt_bap_stream_ops *ops;
@ -276,15 +269,13 @@ static void ase_set_state_codec_configured(struct bt_ascs_ase *ase)
ase->ep.receiver_ready = false;
ase_status_changed(ase, BT_BAP_EP_STATE_CODEC_CONFIGURED);
ops = stream->ops;
if (ops != NULL && ops->configured != NULL) {
ops->configured(stream, &ase->ep.qos_pref);
}
}
static void ase_set_state_qos_configured(struct bt_ascs_ase *ase)
static void ase_enter_state_qos_configured(struct bt_ascs_ase *ase)
{
struct bt_bap_stream *stream = ase->ep.stream;
struct bt_bap_stream_ops *ops;
@ -293,29 +284,22 @@ static void ase_set_state_qos_configured(struct bt_ascs_ase *ase)
ase->ep.receiver_ready = false;
ase_status_changed(ase, BT_BAP_EP_STATE_QOS_CONFIGURED);
ops = stream->ops;
if (ops != NULL && ops->qos_set != NULL) {
ops->qos_set(stream);
}
}
static void ase_set_state_enabling(struct bt_ascs_ase *ase)
static void ase_enter_state_enabling(struct bt_ascs_ase *ase)
{
const bool state_changed = ascs_ep_get_state(&ase->ep) != BT_BAP_EP_STATE_ENABLING;
struct bt_bap_stream *stream = ase->ep.stream;
struct bt_bap_stream_ops *ops;
__ASSERT_NO_MSG(stream != NULL);
ase_status_changed(ase, BT_BAP_EP_STATE_ENABLING);
ops = stream->ops;
if (state_changed && ops != NULL && ops->enabled != NULL) {
if (ops != NULL && ops->enabled != NULL) {
ops->enabled(stream);
} else if (!state_changed && ops != NULL && ops->metadata_updated != NULL) {
ops->metadata_updated(stream);
}
/* SINK ASEs can autonomously go into the streaming state if the CIS is connected */
@ -325,25 +309,54 @@ static void ase_set_state_enabling(struct bt_ascs_ase *ase)
}
}
static void ase_set_state_streaming(struct bt_ascs_ase *ase)
static void ase_enter_state_streaming(struct bt_ascs_ase *ase)
{
const bool state_changed = ascs_ep_get_state(&ase->ep) != BT_BAP_EP_STATE_STREAMING;
struct bt_bap_stream *stream = ase->ep.stream;
struct bt_bap_stream_ops *ops;
__ASSERT_NO_MSG(stream != NULL);
ase_status_changed(ase, BT_BAP_EP_STATE_STREAMING);
ops = stream->ops;
if (ops != NULL && ops->started != NULL) {
ops->started(stream);
}
}
static void ase_metadata_updated(struct bt_ascs_ase *ase)
{
struct bt_bap_stream *stream = ase->ep.stream;
struct bt_bap_stream_ops *ops;
__ASSERT_NO_MSG(stream != NULL);
ops = stream->ops;
if (state_changed && ops != NULL && ops->started != NULL) {
ops->started(stream);
} else if (!state_changed && ops != NULL && ops->metadata_updated != NULL) {
if (ops != NULL && ops->metadata_updated != NULL) {
ops->metadata_updated(stream);
}
}
static void ase_set_state_disabling(struct bt_ascs_ase *ase)
static void ase_exit_state_streaming(struct bt_ascs_ase *ase)
{
struct bt_bap_stream *stream = ase->ep.stream;
struct bt_bap_stream_ops *ops;
uint8_t reason = ase->ep.reason;
__ASSERT_NO_MSG(stream != NULL);
if (reason == BT_HCI_ERR_SUCCESS) {
/* Default to BT_HCI_ERR_UNSPECIFIED if no other reason is set */
reason = BT_HCI_ERR_UNSPECIFIED;
}
ops = stream->ops;
if (ops != NULL && ops->stopped != NULL) {
ops->stopped(stream, reason);
} else {
LOG_WRN("No callback for stopped set");
}
}
static void ase_enter_state_disabling(struct bt_ascs_ase *ase)
{
struct bt_bap_stream *stream = ase->ep.stream;
struct bt_bap_stream_ops *ops;
@ -352,15 +365,13 @@ static void ase_set_state_disabling(struct bt_ascs_ase *ase)
ase->ep.receiver_ready = false;
ase_status_changed(ase, BT_BAP_EP_STATE_DISABLING);
ops = stream->ops;
if (ops != NULL && ops->disabled != NULL) {
ops->disabled(stream);
}
}
static void ase_set_state_releasing(struct bt_ascs_ase *ase)
static void ase_enter_state_releasing(struct bt_ascs_ase *ase)
{
struct bt_bap_stream *stream = ase->ep.stream;
@ -368,8 +379,6 @@ static void ase_set_state_releasing(struct bt_ascs_ase *ase)
ase->ep.receiver_ready = false;
ase_status_changed(ase, BT_BAP_EP_STATE_RELEASING);
/* Either the client or the server may disconnect the CISes when entering the releasing
* state.
*/
@ -387,52 +396,88 @@ static void ase_set_state_releasing(struct bt_ascs_ase *ase)
static void state_transition_work_handler(struct k_work *work)
{
struct bt_ascs_ase *ase = CONTAINER_OF(work, struct bt_ascs_ase, state_transition_work);
struct k_work_delayable *d_work = k_work_delayable_from_work(work);
struct bt_ascs_ase *ase = CONTAINER_OF(d_work, struct bt_ascs_ase, state_transition_work);
const enum bt_bap_ep_state old_state = ascs_ep_get_state(&ase->ep);
const enum bt_bap_ep_state new_state = ase->state_pending;
struct bt_bap_stream *stream = ase->ep.stream;
struct bt_bap_ep *ep = &ase->ep;
int err;
__ASSERT_NO_MSG(stream != NULL);
ase->ep.status.state = new_state;
/* We left the streaming state, let the upper layers know that the stream is stopped */
if (old_state != new_state && old_state == BT_BAP_EP_STATE_STREAMING) {
struct bt_bap_stream_ops *ops = stream->ops;
uint8_t reason = ep->reason;
/* Notify ASE state */
if (ase->conn != NULL) {
err = ase_state_notify(ase);
if (err == -ENOMEM) {
struct bt_conn_info info;
uint32_t retry_delay_ms;
if (reason == BT_HCI_ERR_SUCCESS) {
/* Default to BT_HCI_ERR_UNSPECIFIED if no other reason is set */
reason = BT_HCI_ERR_UNSPECIFIED;
/* Revert back to old state */
ase->ep.status.state = old_state;
err = bt_conn_get_info(ase->conn, &info);
__ASSERT_NO_MSG(err == 0);
retry_delay_ms = BT_CONN_INTERVAL_TO_MS(info.le.interval);
/* Reschedule the state transition */
err = k_work_reschedule(d_work, K_MSEC(retry_delay_ms));
if (err >= 0) {
LOG_WRN("Out of buffers for ase state notification. "
"Will retry in %dms", retry_delay_ms);
return;
}
}
if (ops != NULL && ops->stopped != NULL) {
ops->stopped(stream, reason);
} else {
LOG_WRN("No callback for stopped set");
if (err < 0) {
LOG_ERR("Failed to notify ASE state (err %d)", err);
}
}
LOG_DBG("ase %p id 0x%02x %s -> %s", ase, ase->ep.status.id,
bt_bap_ep_state_str(old_state), bt_bap_ep_state_str(new_state));
if (old_state == new_state) {
switch (new_state) {
case BT_BAP_EP_STATE_ENABLING:
case BT_BAP_EP_STATE_STREAMING:
ase_metadata_updated(ase);
return;
default:
break;
}
}
/* Actions needed for exiting the old state */
switch (old_state) {
case BT_BAP_EP_STATE_STREAMING:
ase_exit_state_streaming(ase);
break;
default:
break;
}
/* Actions needed for entering the new state */
switch (new_state) {
case BT_BAP_EP_STATE_IDLE:
ase_set_state_idle(ase);
ase_enter_state_idle(ase);
break;
case BT_BAP_EP_STATE_CODEC_CONFIGURED:
ase_set_state_codec_configured(ase);
ase_enter_state_codec_configured(ase);
break;
case BT_BAP_EP_STATE_QOS_CONFIGURED:
ase_set_state_qos_configured(ase);
ase_enter_state_qos_configured(ase);
break;
case BT_BAP_EP_STATE_ENABLING:
ase_set_state_enabling(ase);
ase_enter_state_enabling(ase);
break;
case BT_BAP_EP_STATE_STREAMING:
ase_set_state_streaming(ase);
ase_enter_state_streaming(ase);
break;
case BT_BAP_EP_STATE_DISABLING:
ase_set_state_disabling(ase);
ase_enter_state_disabling(ase);
break;
case BT_BAP_EP_STATE_RELEASING:
ase_set_state_releasing(ase);
ase_enter_state_releasing(ase);
break;
default:
__ASSERT_PRINT("Invalid state %d", new_state);
@ -533,9 +578,9 @@ int ascs_ep_set_state(struct bt_bap_ep *ep, uint8_t state)
ase->state_pending = state;
err = k_work_submit(&ase->state_transition_work);
err = k_work_schedule(&ase->state_transition_work, K_NO_WAIT);
if (err < 0) {
LOG_ERR("Failed to submit state transition work err %d", err);
LOG_ERR("Failed to schedule state transition work err %d", err);
return err;
}
@ -1020,6 +1065,12 @@ static int ase_release(struct bt_ascs_ase *ase, uint8_t reason, struct bt_bap_as
int bt_ascs_release_ase(struct bt_bap_ep *ep)
{
struct bt_ascs_ase *ase = CONTAINER_OF(ep, struct bt_ascs_ase, ep);
const enum bt_bap_ep_state state = ascs_ep_get_state(&ase->ep);
if (state == BT_BAP_EP_STATE_IDLE) {
ase_free(ase);
return 0;
}
return ase_release(ase, BT_HCI_ERR_LOCALHOST_TERM_CONN, BT_BAP_ASCS_RSP_NULL);
}
@ -1103,12 +1154,12 @@ static void disconnected(struct bt_conn *conn, uint8_t reason)
* should expect there to be only a single reference to the bt_conn pointer
* from the stack.
* We trigger the work handler directly rather than e.g. calling
* ase_set_state_idle to trigger "regular" state change behavior (such) as
* ase_enter_state_idle to trigger "regular" state change behavior (such) as
* calling stream->stopped when leaving the streaming state.
*/
ase->ep.reason = reason;
ase->state_pending = BT_BAP_EP_STATE_IDLE;
state_transition_work_handler(&ase->state_transition_work);
state_transition_work_handler(&ase->state_transition_work.work);
/* At this point, `ase` object have been free'd */
}
}
@ -1205,9 +1256,8 @@ static void ase_init(struct bt_ascs_ase *ase, struct bt_conn *conn, uint8_t id)
__ASSERT(ase->attr, "ASE characteristic not found\n");
k_work_init_delayable(&ase->disconnect_work,
ascs_disconnect_stream_work_handler);
k_work_init(&ase->state_transition_work, state_transition_work_handler);
k_work_init_delayable(&ase->disconnect_work, ascs_disconnect_stream_work_handler);
k_work_init_delayable(&ase->state_transition_work, state_transition_work_handler);
}
static struct bt_ascs_ase *ase_new(struct bt_conn *conn, uint8_t id)