* [PATCH BlueZ v5 3/6] bap: Release cached ASEs on ClearConfiguration of an endpoint
2026-11-12 20:37 [PATCH BlueZ v5 0/6] bap: Release cached ASEs on ClearConfiguration Luiz Augusto von Dentz
2026-11-12 20:37 ` [PATCH BlueZ v5 1/6] shared/bap: Add bt_bap_foreach_stream and bt_bap_stream_get_rpac Luiz Augusto von Dentz
2026-11-12 20:37 ` [PATCH BlueZ v5 2/6] shared/bap: Update the remote PAC when reusing an ASE Luiz Augusto von Dentz
@ 2026-11-12 20:37 ` Luiz Augusto von Dentz
2026-11-12 20:37 ` [PATCH BlueZ v5 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-12 20: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 | 178 ++++++++++++++++++++++++++++++++++++++++---
1 file changed, 169 insertions(+), 9 deletions(-)
diff --git a/profiles/audio/bap.c b/profiles/audio/bap.c
index a0e0ba60d817..45314b499325 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,144 @@ 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;
+};
+
+/* The release of a cached stream, done once the ASE has left Releasing */
+struct cached_release {
+ struct bap_data *data;
+ struct bt_bap_stream *stream;
+ struct ep_close_data *epdata;
+ int refs;
+ bool done;
+};
+
+static void cached_release_unref(struct cached_release *cr)
+{
+ if (--cr->refs)
+ return;
+
+ free(cr);
+}
+
+static void cached_release_done(struct cached_release *cr, int err)
+{
+ if (cr->done)
+ return;
+
+ cr->done = true;
+
+ if (err)
+ cr->epdata->err = err;
+
+ ep_close_setup_cb(NULL, cr->epdata);
+}
+
+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 bap_cached_release_done(struct bap_data *data,
+ struct bt_bap_stream *stream)
+{
+ 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.
+ */
+ while ((cr = queue_remove_if(data->cached_releases,
+ match_cached_release, stream))) {
+ cached_release_done(cr, 0);
+ cached_release_unref(cr);
+ }
+}
+
+static void cached_release_cancel(void *data)
+{
+ struct cached_release *cr = data;
+
+ cr->data = NULL;
+ cached_release_done(cr, -ECANCELED);
+ 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, so only a failure completes
+ * the release here.
+ */
+ if (code != BT_ASCS_RSP_SUCCESS) {
+ if (cr->data && queue_remove(cr->data->cached_releases, cr))
+ cached_release_unref(cr);
+
+ cached_release_done(cr, -EIO);
+ }
+
+ 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;
+
+ /* The server may have cached the codec configuration of a released
+ * ASE, which has no setup anymore, so release it again.
+ */
+ if (bt_bap_stream_get_rpac(stream) != ep->rpac ||
+ bt_bap_stream_get_state(stream) !=
+ BT_BAP_STREAM_STATE_CONFIG ||
+ bap_find_setup_by_stream(ep->data, stream))
+ return;
+
+ DBG("stream %p", stream);
+
+ /* Don't release it again while an earlier release is pending, just
+ * wait for it as well.
+ */
+ pending = queue_find(ep->data->cached_releases, match_cached_release,
+ stream);
+
+ cr = new0(struct cached_release, 1);
+ cr->data = ep->data;
+ cr->stream = stream;
+ cr->epdata = data->epdata;
+ cr->refs = pending ? 1 : 2;
+
+ 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;
+
+ if (!bt_bap_stream_release(stream, ep_release_cached_cb, cr)) {
+ queue_remove(ep->data->cached_releases, cr);
+ data->epdata->remaining--;
+ free(cr);
+ }
+}
+
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 +1193,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 +1453,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 +1491,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 +1557,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 +1578,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 +1618,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 +3169,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 +4211,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 v5 4/6] client/player: Add endpoint.clear command
2026-11-12 20:37 [PATCH BlueZ v5 0/6] bap: Release cached ASEs on ClearConfiguration Luiz Augusto von Dentz
` (2 preceding siblings ...)
2026-11-12 20:37 ` [PATCH BlueZ v5 3/6] bap: Release cached ASEs on ClearConfiguration of an endpoint Luiz Augusto von Dentz
@ 2026-11-12 20:37 ` Luiz Augusto von Dentz
2026-11-12 20:37 ` [PATCH BlueZ v5 5/6] doc/functional-bap: Document clearing a cached configuration Luiz Augusto von Dentz
2026-11-12 20:37 ` [PATCH BlueZ v5 6/6] test: functional: Cover " Luiz Augusto von Dentz
5 siblings, 0 replies; 7+ messages in thread
From: Luiz Augusto von Dentz @ 2026-11-12 20: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