* [PATCH BlueZ v6 3/6] bap: Release cached ASEs on ClearConfiguration of an endpoint
2026-11-13 16:37 [PATCH BlueZ v6 0/6] bap: Release cached ASEs on ClearConfiguration Luiz Augusto von Dentz
2026-11-13 16:37 ` [PATCH BlueZ v6 1/6] shared/bap: Add bt_bap_foreach_stream and bt_bap_stream_get_rpac Luiz Augusto von Dentz
2026-11-13 16:37 ` [PATCH BlueZ v6 2/6] shared/bap: Update the remote PAC when reusing an ASE Luiz Augusto von Dentz
@ 2026-11-13 16:37 ` Luiz Augusto von Dentz
2026-11-13 16:37 ` [PATCH BlueZ v6 4/6] client/player: Add endpoint.clear command Luiz Augusto von Dentz
` (2 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Luiz Augusto von Dentz @ 2026-11-13 16:37 UTC (permalink / raw)
To: linux-bluetooth
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
Once a stream is released, the server may cache the codec configuration
of its ASE, leaving it in Codec Configured state, while the setup of
the stream is freed. ClearConfiguration only closed the streams that
have a setup, so it could not release such an ASE anymore.
When ClearConfiguration is given the path of the endpoint, also release
its streams in Codec Configured state that have no setup, as a client
may release an ASE in that state (ASCS Table 3.2, BAP 5.6.6).
Assisted-by: Claude:claude-opus-5-5
---
profiles/audio/bap.c | 221 +++++++++++++++++++++++++++++++++++++++++--
1 file changed, 212 insertions(+), 9 deletions(-)
diff --git a/profiles/audio/bap.c b/profiles/audio/bap.c
index a0e0ba60d817..457ba5de8fd0 100644
--- a/profiles/audio/bap.c
+++ b/profiles/audio/bap.c
@@ -46,6 +46,7 @@
#include "src/shared/gatt-client.h"
#include "src/shared/gatt-server.h"
#include "src/shared/bap.h"
+#include "src/shared/ascs.h"
#include "src/shared/tmap.h"
#include "src/shared/gmap.h"
@@ -138,6 +139,7 @@ struct bap_data {
struct queue *bcast;
struct queue *bcast_snks;
struct queue *server_streams;
+ struct queue *cached_releases;
GIOChannel *listen_io;
bool pa_synced;
unsigned int pa_timer;
@@ -149,6 +151,8 @@ struct bap_data {
static struct queue *sessions;
+static void cached_release_cancel(void *data);
+
static int setup_config(struct bap_setup *setup, bap_setup_ready_func_t cb,
void *user_data);
static int bap_select_all(struct bap_data *data, bool reconfigure,
@@ -196,6 +200,7 @@ static void bap_data_free(struct bap_data *data)
queue_destroy(data->srcs, ep_unregister);
queue_destroy(data->bcast, ep_unregister);
queue_destroy(data->server_streams, NULL);
+ queue_destroy(data->cached_releases, cached_release_cancel);
data->bcast_snks = NULL;
queue_destroy(bcast_snks, setup_free);
@@ -996,8 +1001,9 @@ static int setup_close(struct bap_setup *setup, bap_setup_close_func_t cb,
struct ep_close_data {
int remaining;
int count;
+ int err;
const char *path;
- void (*cb)(int count, void *user_data);
+ void (*cb)(int count, int err, void *user_data);
void *user_data;
};
@@ -1013,7 +1019,7 @@ static void ep_close_setup_cb(struct bap_setup *setup, void *user_data)
return;
if (epdata->cb)
- epdata->cb(epdata->count, epdata->user_data);
+ epdata->cb(epdata->count, epdata->err, epdata->user_data);
free(epdata);
}
@@ -1035,8 +1041,187 @@ static void ep_close_setup(void *data, void *user_data)
epdata->count++;
}
+static struct bap_setup *bap_find_setup_by_stream(struct bap_data *data,
+ struct bt_bap_stream *stream);
+
+struct cached_release_data {
+ struct bap_ep *ep;
+ struct ep_close_data *epdata;
+};
+
+#define CACHED_RELEASE_TIMEOUT 10
+
+/* A ClearConfiguration waiting for a cached ASE to leave Releasing */
+struct cached_release {
+ struct bap_data *data;
+ struct bt_bap_stream *stream;
+ struct ep_close_data *epdata;
+ unsigned int timer;
+ int refs;
+};
+
+static void cached_release_unref(struct cached_release *cr)
+{
+ if (--cr->refs)
+ return;
+
+ free(cr);
+}
+
+static bool match_cached_release(const void *data, const void *match_data)
+{
+ const struct cached_release *cr = data;
+
+ return cr->stream == match_data;
+}
+
+static void cached_release_timer_destroy(void *user_data)
+{
+ struct cached_release *cr = user_data;
+
+ cr->timer = 0;
+ cached_release_unref(cr);
+}
+
+static void bap_cached_release_finish(struct bap_data *data,
+ struct bt_bap_stream *stream, int err)
+{
+ struct cached_release *cr;
+
+ /* A server may keep the codec configuration of the ASE when released,
+ * moving it back to Codec Configured instead of Idle, which can't be
+ * cleared, so it is done once the ASE has left Releasing either way.
+ * All the ClearConfiguration waiting for the stream are completed
+ * with the same result.
+ */
+ while ((cr = queue_remove_if(data->cached_releases,
+ match_cached_release, stream))) {
+ /* The request may outlive the queue */
+ cr->data = NULL;
+
+ if (cr->timer)
+ g_source_remove(cr->timer);
+
+ if (err)
+ cr->epdata->err = err;
+
+ ep_close_setup_cb(NULL, cr->epdata);
+ cached_release_unref(cr);
+ }
+}
+
+static gboolean cached_release_timeout(gpointer user_data)
+{
+ struct cached_release *cr = user_data;
+
+ /* The source is removed by returning, so don't remove it again */
+ cr->timer = 0;
+
+ if (cr->data) {
+ DBG("stream %p", cr->stream);
+ bap_cached_release_finish(cr->data, cr->stream, -ETIMEDOUT);
+ }
+
+ return FALSE;
+}
+
+static void bap_cached_release_done(struct bap_data *data,
+ struct bt_bap_stream *stream)
+{
+ bap_cached_release_finish(data, stream, 0);
+}
+
+static void cached_release_cancel(void *data)
+{
+ struct cached_release *cr = data;
+
+ cr->data = NULL;
+
+ if (cr->timer)
+ g_source_remove(cr->timer);
+
+ cr->epdata->err = -ECANCELED;
+ ep_close_setup_cb(NULL, cr->epdata);
+ cached_release_unref(cr);
+}
+
+static void ep_release_cached_cb(struct bt_bap_stream *stream, uint8_t code,
+ uint8_t reason, void *user_data)
+{
+ struct cached_release *cr = user_data;
+
+ /* The ASE leaves Releasing once notified, which completes the release,
+ * so only a failure completes it here, or an ASE still in Codec
+ * Configured, as the shared layer calls it when the notification
+ * didn't come in time.
+ */
+ if (cr->data && (code != BT_ASCS_RSP_SUCCESS ||
+ bt_bap_stream_get_state(stream) ==
+ BT_BAP_STREAM_STATE_CONFIG))
+ bap_cached_release_finish(cr->data, stream,
+ code != BT_ASCS_RSP_SUCCESS ? -EIO :
+ -ETIMEDOUT);
+
+ cached_release_unref(cr);
+}
+
+static void ep_release_cached(struct bt_bap_stream *stream, void *user_data)
+{
+ struct cached_release_data *data = user_data;
+ struct bap_ep *ep = data->ep;
+ struct cached_release *cr, *pending;
+
+ if (bt_bap_stream_get_rpac(stream) != ep->rpac ||
+ bap_find_setup_by_stream(ep->data, stream))
+ return;
+
+ /* Wait for an earlier release as well instead of releasing again,
+ * also when the ASE is already Releasing.
+ */
+ pending = queue_find(ep->data->cached_releases, match_cached_release,
+ stream);
+
+ /* The server may have cached the codec configuration of a released
+ * ASE, which has no setup anymore, so release it again.
+ */
+ if (!pending && bt_bap_stream_get_state(stream) !=
+ BT_BAP_STREAM_STATE_CONFIG)
+ return;
+
+ DBG("stream %p", stream);
+
+ cr = new0(struct cached_release, 1);
+ cr->data = ep->data;
+ cr->stream = stream;
+ cr->epdata = data->epdata;
+ cr->refs = 1;
+
+ data->epdata->remaining++;
+
+ if (!ep->data->cached_releases)
+ ep->data->cached_releases = queue_new();
+
+ queue_push_tail(ep->data->cached_releases, cr);
+
+ if (pending)
+ return;
+
+ /* One reference for the request, and one for the timer */
+ cr->refs += 2;
+ cr->timer = g_timeout_add_seconds_full(G_PRIORITY_DEFAULT,
+ CACHED_RELEASE_TIMEOUT, cached_release_timeout,
+ cr, cached_release_timer_destroy);
+
+ if (!bt_bap_stream_release(stream, ep_release_cached_cb, cr)) {
+ cr->refs--;
+ bap_cached_release_finish(ep->data, stream, -EIO);
+ }
+}
+
static void ep_close(struct bap_ep *ep, const char *transport_path,
- void (*cb)(int count, void *user_data), void *user_data)
+ bool cached,
+ void (*cb)(int count, int err, void *user_data),
+ void *user_data)
{
struct ep_close_data *epdata;
@@ -1051,6 +1236,15 @@ static void ep_close(struct bap_ep *ep, const char *transport_path,
if (ep)
queue_foreach(ep->setups, ep_close_setup, epdata);
+ /* The cached streams have to be released before replying, as an ASE
+ * still being released can't be configured again.
+ */
+ if (cached && ep && ep->rpac) {
+ struct cached_release_data data = { ep, epdata };
+
+ bt_bap_foreach_stream(ep->data->bap, ep_release_cached, &data);
+ }
+
epdata->path = NULL;
ep_close_setup_cb(NULL, epdata);
}
@@ -1302,14 +1496,16 @@ struct clear_configuration_data {
bool all;
};
-static void clear_configuration_cb(int count, void *user_data)
+static void clear_configuration_cb(int count, int err, void *user_data)
{
struct clear_configuration_data *data = user_data;
DBusMessage *reply;
DBG("%p", data);
- if (!data->all && count == 0)
+ if (err)
+ reply = btd_error_failed(data->msg, strerror(-err));
+ else if (!data->all && count == 0)
reply = btd_error_invalid_args(data->msg);
else
reply = dbus_message_new_method_return(data->msg);
@@ -1338,7 +1534,8 @@ static DBusMessage *clear_configuration(DBusConnection *conn, DBusMessage *msg,
cbdata->all = (path == NULL);
DBG("%p %s %s", cbdata, ep->path, path ? path : "NULL");
- ep_close(ep, path, clear_configuration_cb, cbdata);
+
+ ep_close(ep, path, !path, clear_configuration_cb, cbdata);
return NULL;
}
@@ -1403,7 +1600,7 @@ static void reconfigure_select_cb(int err, void *user_data)
free(data);
}
-static void reconfigure_close_cb(int count, void *user_data)
+static void reconfigure_close_cb(int count, int err, void *user_data)
{
struct reconfigure_data *data = user_data;
@@ -1424,7 +1621,7 @@ static void ep_close_if_reconfigure(void *obj, void *user_data)
if (ep->reconfigure) {
data->remaining++;
- ep_close(ep, NULL, reconfigure_close_cb, data);
+ ep_close(ep, NULL, false, reconfigure_close_cb, data);
}
}
@@ -1464,7 +1661,7 @@ static DBusMessage *reconfigure(DBusConnection *conn, DBusMessage *msg,
queue_foreach(data->snks, ep_close_if_reconfigure, cbdata);
queue_foreach(data->srcs, ep_close_if_reconfigure, cbdata);
- reconfigure_close_cb(0, cbdata);
+ reconfigure_close_cb(0, 0, cbdata);
return NULL;
}
@@ -3015,6 +3212,9 @@ static void bap_state(struct bt_bap_stream *stream, uint8_t old_state,
if (new_state == old_state && new_state != BT_BAP_STREAM_STATE_CONFIG)
return;
+ if (old_state == BT_BAP_STREAM_STATE_RELEASING)
+ bap_cached_release_done(data, stream);
+
setup = bap_find_setup_by_stream(data, stream);
if (setup && setup->closing) {
@@ -4054,6 +4254,9 @@ static int bap_disconnect(struct btd_service *service)
queue_destroy(data->server_streams, NULL);
data->server_streams = NULL;
+ queue_destroy(data->cached_releases, cached_release_cancel);
+ data->cached_releases = NULL;
+
bt_bap_detach(data->bap);
btd_service_disconnecting_complete(service, 0);
--
2.56.0
^ permalink raw reply related [flat|nested] 7+ messages in thread* [PATCH BlueZ v6 4/6] client/player: Add endpoint.clear command
2026-11-13 16:37 [PATCH BlueZ v6 0/6] bap: Release cached ASEs on ClearConfiguration Luiz Augusto von Dentz
` (2 preceding siblings ...)
2026-11-13 16:37 ` [PATCH BlueZ v6 3/6] bap: Release cached ASEs on ClearConfiguration of an endpoint Luiz Augusto von Dentz
@ 2026-11-13 16:37 ` Luiz Augusto von Dentz
2026-11-13 16:37 ` [PATCH BlueZ v6 5/6] doc/functional-bap: Document clearing a cached configuration Luiz Augusto von Dentz
2026-11-13 16:37 ` [PATCH BlueZ v6 6/6] test: functional: Cover " Luiz Augusto von Dentz
5 siblings, 0 replies; 7+ messages in thread
From: Luiz Augusto von Dentz @ 2026-11-13 16:37 UTC (permalink / raw)
To: linux-bluetooth
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
Add a command calling ClearConfiguration on a remote endpoint, to close
the stream of a given transport or all the streams of the endpoint.
Assisted-by: Claude:claude-opus-5-5
---
client/player.c | 57 +++++++++++++++++++++++++++++++++++
doc/bluetoothctl-endpoint.rst | 20 ++++++++++++
2 files changed, 77 insertions(+)
diff --git a/client/player.c b/client/player.c
index 88221076c1e0..932338ef53a2 100644
--- a/client/player.c
+++ b/client/player.c
@@ -4030,6 +4030,60 @@ static void endpoint_set_config_bcast(struct endpoint_config *cfg)
config_endpoint_iso_group, cfg);
}
+static void clear_endpoint_setup(DBusMessageIter *iter, void *user_data)
+{
+ const char *path = user_data;
+
+ dbus_message_iter_append_basic(iter, DBUS_TYPE_OBJECT_PATH, &path);
+}
+
+static void clear_endpoint_reply(DBusMessage *message, void *user_data)
+{
+ DBusError error;
+
+ dbus_error_init(&error);
+
+ if (dbus_set_error_from_message(&error, message)) {
+ bt_shell_printf("Failed to clear configuration: %s\n",
+ error.name);
+ dbus_error_free(&error);
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+ }
+
+ bt_shell_printf("Configuration cleared\n");
+
+ return bt_shell_noninteractive_quit(EXIT_SUCCESS);
+}
+
+static void cmd_clear_endpoint(int argc, char *argv[])
+{
+ GDBusProxy *proxy;
+ const char *path;
+
+ proxy = g_dbus_proxy_lookup(endpoints, NULL, argv[1],
+ BLUEZ_MEDIA_ENDPOINT_INTERFACE);
+ if (!proxy) {
+ bt_shell_printf("Endpoint %s not found\n", argv[1]);
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+ }
+
+ /* The path of the endpoint itself clears all its transports */
+ path = argc > 2 ? argv[2] : g_dbus_proxy_get_path(proxy);
+
+ if (!dbus_validate_path(path, NULL)) {
+ bt_shell_printf("Invalid transport path %s\n", path);
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+ }
+
+ if (!g_dbus_proxy_method_call(proxy, "ClearConfiguration",
+ clear_endpoint_setup,
+ clear_endpoint_reply,
+ (void *) path, NULL)) {
+ bt_shell_printf("Failed to clear configuration\n");
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+ }
+}
+
static void cmd_config_endpoint(int argc, char *argv[])
{
struct endpoint_config *cfg;
@@ -4625,6 +4679,9 @@ static const struct bt_shell_menu endpoint_menu = {
cmd_config_endpoint,
"Configure Endpoint",
endpoint_generator },
+ { "clear", "<endpoint> [transport]", cmd_clear_endpoint,
+ "Clear Endpoint Configuration",
+ endpoint_generator },
{ "presets", "<endpoint>/<UUID> [codec[:company]] [preset] "
"[codec config] [metadata]",
cmd_presets_endpoint,
diff --git a/doc/bluetoothctl-endpoint.rst b/doc/bluetoothctl-endpoint.rst
index 019e64e38452..ba30ef769ec4 100644
--- a/doc/bluetoothctl-endpoint.rst
+++ b/doc/bluetoothctl-endpoint.rst
@@ -154,6 +154,26 @@ Note:
If the endpoint was registered by an audio server, it can't be configured from bluetoothctl.
This must be done by the audio server as well.
+clear
+-----
+
+Clear Endpoint Configuration.
+
+:Usage: **> clear <endpoint> [transport]**
+:Uses: **org.bluez.MediaEndpoint(5)** method **ClearConfiguration**
+:<endpoint>: Path to the remote endpoint to clear
+:[transport]: Path to the transport whose stream to close (optional, all the
+ streams of the endpoint are closed if omitted)
+:Example Release all the streams of a remote endpoint:
+ | **> clear /org/bluez/hci0/dev_00_11_22_33_44_55/pac_sink0**
+:Example Release the stream of a single transport:
+ | **> clear /org/bluez/hci0/dev_00_11_22_33_44_55/pac_sink0 /org/bluez/hci0/dev_00_11_22_33_44_55/pac_sink0/fd0**
+
+Note:
+
+Clearing an endpoint whose codec configuration was cached by the remote, after
+its streams were released, releases them again.
+
presets
-------
--
2.56.0
^ permalink raw reply related [flat|nested] 7+ messages in thread