* [PATCH BlueZ v1 1/6] client/scripts: Add bap-duplex-lc3.bt
2026-11-02 18:12 [PATCH BlueZ v1 0/6] Fix linked transports of an ASE not enabled yet Luiz Augusto von Dentz
@ 2026-11-02 18:12 ` Luiz Augusto von Dentz
2026-11-02 19:43 ` Fix linked transports of an ASE not enabled yet bluez.test.bot
2026-11-02 18:12 ` [PATCH BlueZ v1 2/6] doc/functional-bap: Document the duplex unicast test cases Luiz Augusto von Dentz
` (4 subsequent siblings)
5 siblings, 1 reply; 9+ messages in thread
From: Luiz Augusto von Dentz @ 2026-11-02 18:12 UTC (permalink / raw)
To: linux-bluetooth
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
Register a PAC Sink and a PAC Source endpoint with LC3, both for Front
Left only, so a single CIS carries both directions, e.g. for a voice
call to a headset with a microphone.
Assisted-by: Claude:claude-opus-5-5
---
client/scripts/bap-duplex-lc3.bt | 41 ++++++++++++++++++++++++++++++++
1 file changed, 41 insertions(+)
create mode 100644 client/scripts/bap-duplex-lc3.bt
diff --git a/client/scripts/bap-duplex-lc3.bt b/client/scripts/bap-duplex-lc3.bt
new file mode 100644
index 000000000000..f1f20fc44e02
--- /dev/null
+++ b/client/scripts/bap-duplex-lc3.bt
@@ -0,0 +1,41 @@
+power on
+#
+#
+# Register a PAC Sink and a PAC Source endpoint with LC3, both for Front
+# Left only, e.g. a headset with a microphone in a voice call. Run on
+# both sides: the device that pairs is the initiator.
+#
+endpoint.register 00002bc9-0000-1000-8000-00805f9b34fb 0x06
+# Auto Accept: accept the configuration chosen by the peer
+y
+# Max Transports: no limit
+a
+# Locations: Front Left
+1
+# Supported Context: same as the contexts bluetoothctl registers with
+0x0fff
+# Context
+0x0fff
+# CIG: assigned automatically
+a
+# CIS: assigned automatically
+a
+endpoint.register 00002bcb-0000-1000-8000-00805f9b34fb 0x06
+# Auto Accept: accept the configuration chosen by the peer
+y
+# Max Transports: no limit
+a
+# Locations: Front Left
+1
+# Supported Context: same as the contexts bluetoothctl registers with
+0x000f
+# Context
+0x000f
+# CIG: assigned automatically
+a
+# CIS: assigned automatically
+a
+#
+#
+# With a single location, the stream of each direction uses the same
+# CIS, so the two transports are linked.
--
2.56.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* RE: Fix linked transports of an ASE not enabled yet
2026-11-02 18:12 ` [PATCH BlueZ v1 1/6] client/scripts: Add bap-duplex-lc3.bt Luiz Augusto von Dentz
@ 2026-11-02 19:43 ` bluez.test.bot
0 siblings, 0 replies; 9+ messages in thread
From: bluez.test.bot @ 2026-11-02 19:43 UTC (permalink / raw)
To: linux-bluetooth, luiz.dentz
[-- Attachment #1: Type: text/plain, Size: 2290 bytes --]
This is automated email and please do not reply to this email!
Dear submitter,
Thank you for submitting the patches to the linux bluetooth mailing list.
This is a CI test results with your patch series:
PW Link:https://patchwork.kernel.org/series/1199382/
---Test result---
Test Summary:
CheckPatch PASS 2.68 seconds
GitLint FAIL 1.33 seconds
BuildEll PASS 20.72 seconds
BluezMake PASS 301.89 seconds
MakeCheck PASS 14.36 seconds
MakeDistcheck PASS 145.70 seconds
CheckValgrind PASS 239.45 seconds
CheckSmatch WARNING 299.23 seconds
bluezmakeextell PASS 98.84 seconds
TestFunctional FAIL 1162.90 seconds
IncrementalBuild PASS 347.69 seconds
ScanBuild PASS 1155.95 seconds
Details
##############################
Test: GitLint - FAIL
Desc: Run gitlint
Output:
[BlueZ,v1,5/6] transport: Don't set linked transports pending before they are enabled
1: T1 Title exceeds max length (85>80): "[BlueZ,v1,5/6] transport: Don't set linked transports pending before they are enabled"
##############################
Test: CheckSmatch - WARNING
Desc: Run smatch tool with source
Output:
src/shared/bap.c:309:25: warning: array of flexible structuressrc/shared/bap.c: note: in included file:./src/shared/ascs.h:88:25: warning: array of flexible structuressrc/shared/bap.c:309:25: warning: array of flexible structuressrc/shared/bap.c: note: in included file:./src/shared/ascs.h:88:25: warning: array of flexible structuressrc/shared/bap.c:309:25: warning: array of flexible structuressrc/shared/bap.c: note: in included file:./src/shared/ascs.h:88:25: warning: array of flexible structures
##############################
Test: TestFunctional - FAIL
Desc: Run test-functional
Output:
FAIL functional.test_bap::test_bap_unicast_desync[hosts6-vm2]: failed on setup with "NameError: name 'CONFIGURED' is not defined"
FAIL functional.test_bap::test_bap_unicast_duplex_enable_one[hosts6-vm2]: failed on setup with "NameError: name 'CONFIGURED' is not defined"
https://github.com/bluez/bluez/pull/2748
---
Regards,
Linux Bluetooth
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH BlueZ v1 2/6] doc/functional-bap: Document the duplex unicast test cases
2026-11-02 18:12 [PATCH BlueZ v1 0/6] Fix linked transports of an ASE not enabled yet Luiz Augusto von Dentz
2026-11-02 18:12 ` [PATCH BlueZ v1 1/6] client/scripts: Add bap-duplex-lc3.bt Luiz Augusto von Dentz
@ 2026-11-02 18:12 ` Luiz Augusto von Dentz
2026-11-02 18:12 ` [PATCH BlueZ v1 3/6] test: functional: Cover enabling one ASE of a bidirectional CIS Luiz Augusto von Dentz
` (3 subsequent siblings)
5 siblings, 0 replies; 9+ messages in thread
From: Luiz Augusto von Dentz @ 2026-11-02 18:12 UTC (permalink / raw)
To: linux-bluetooth
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
Describe the topology with a stream for each direction over the same
CIS, the test of the Desynchronized property used to enable each
direction separately, and the test of the acceptor when only one of
its ASEs is enabled.
Assisted-by: Claude:claude-opus-5-5
---
doc/functional-bap.rst | 105 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 105 insertions(+)
diff --git a/doc/functional-bap.rst b/doc/functional-bap.rst
index 42fa7fda8006..ea0054ecff5c 100644
--- a/doc/functional-bap.rst
+++ b/doc/functional-bap.rst
@@ -157,6 +157,111 @@ test_bap_unicast_reconfigure_metadata[empty|media]
streams; it does not exercise microphone streams or simultaneous
playback and capture.
+DUPLEX UNICAST
+==============
+
+Two hosts, connected over LE, with a stream for each direction sharing
+a single CIS, e.g. a voice call to a headset with a microphone:
+
+.. code-block::
+
+ +------------------------+ +------------------------+
+ | host0 | LE ACL | host1 |
+ | initiator (central) | --------------> | acceptor (peripheral) |
+ | bluetoothd, testing | | bluetoothctl -a auto |
+ | bluetoothctl -a auto | | bap-duplex-lc3.bt |
+ | bap-duplex-lc3.bt | CIS 0x00 (LC3) | PAC Sink, Front Left |
+ | PAC Source, Front Left | ==============> | |
+ | PAC Sink, Front Left | <============== | PAC Source, Front Left |
+ +------------------------+ +------------------------+
+
+ one CIG holding one bidirectional CIS: the Sink ASE and the Source
+ ASE of the acceptor use the same CIS, so the transports of each
+ host are linked
+
+ --> connection is initiated by ==> audio flows towards
+
+The initiator runs `bluetoothd` in testing mode:
+
+.. code-block::
+
+ [General]
+ Testing = true
+
+which makes the ``Desynchronized`` property of
+**org.bluez.MediaTransport(5)** writable, so each direction can be
+acquired, and so its ASE enabled, on its own. The acceptor does not run
+in testing mode.
+
+``client/scripts/bap-duplex-lc3.bt`` on both hosts
+ Registers a PAC Sink endpoint
+ (``00002bc9-0000-1000-8000-00805f9b34fb``) and a PAC Source endpoint
+ (``00002bcb-0000-1000-8000-00805f9b34fb``) with LC3, both with
+ Front Left as their only location, so a single CIS carries both
+ directions. The host that pairs is the initiator.
+
+test_bap_unicast_desync
+-----------------------
+
+:Setup: As above, with both transports created and linked.
+
+:Steps:
+ 1. Initiator: set ``Desynchronized`` on the transport of the remote
+ Sink ASE.
+ 2. Initiator: ``Acquire`` the transport of the remote Source ASE.
+ 3. Initiator: ``Acquire`` the transport of the remote Sink ASE.
+
+:Expected:
+ 1. ``Desynchronized`` is set on both transports, as it applies to
+ the links of a transport as well.
+ 2. The acquired transport moves to ``State: active``, while its link
+ stays in ``State: idle``.
+ 3. The link is acquired on its own and moves to ``State: active``.
+
+:Notes: Checks the mechanism the next test relies on to enable a
+ single ASE of a CIS, so a failure of that one can be told apart
+ from a failure of the initiator to enable the ASEs separately.
+
+test_bap_unicast_duplex_enable_one
+----------------------------------
+
+:Setup: As above, with both transports created and linked, and
+ ``Desynchronized`` set on the initiator.
+
+:Steps:
+ 1. Initiator: ``Acquire`` the transport of the remote Source ASE
+ only, so the acceptor's Source ASE is enabled and the CIS is
+ established, while its Sink ASE stays in the QoS Configured
+ state.
+ 2. Acceptor: check the state of the transport of its Sink ASE.
+ 3. Initiator: ``Acquire`` the transport of the remote Sink ASE.
+ 4. Initiator: ``Release`` the transport of the remote Sink ASE.
+
+:Expected:
+ 1. The acceptor's Source ASE transport moves to ``State: pending``,
+ and is then acquired by `bluetoothctl`.
+ 2. The Sink ASE transport stays in ``State: idle``: its ASE has not
+ been enabled, so it is neither pending nor acquired along with
+ its link.
+ 3. The Sink ASE transport moves to ``State: active``, acquired on
+ its own once the initiator enables its ASE.
+ 4. The Sink ASE transport moves back to ``State: idle`` while the
+ Source ASE transport stays in ``State: active``, on both hosts:
+ transports acquired on their own are released on their own.
+
+:Notes: Reproduces a remote initiator, e.g. a phone in a voice call,
+ enabling the Source ASE of a headset before its Sink ASE. As an
+ acceptor cannot enable an ASE itself, acquiring a transport whose
+ ASE is still in QoS is rejected with ``NotAuthorized``. An audio
+ server acquiring every pending transport then fails, and with it
+ the linked transport that was working.
+
+ `bluetoothctl` with ``-a auto`` acquires a transport as soon as it
+ is pending, as an audio server would. Depending on which transport
+ it acquires first, the Sink ASE transport is either rejected or
+ made active through its link, so the test checks the state of the
+ transport rather than the result of the acquisition.
+
BROADCAST
=========
--
2.56.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH BlueZ v1 3/6] test: functional: Cover enabling one ASE of a bidirectional CIS
2026-11-02 18:12 [PATCH BlueZ v1 0/6] Fix linked transports of an ASE not enabled yet Luiz Augusto von Dentz
2026-11-02 18:12 ` [PATCH BlueZ v1 1/6] client/scripts: Add bap-duplex-lc3.bt Luiz Augusto von Dentz
2026-11-02 18:12 ` [PATCH BlueZ v1 2/6] doc/functional-bap: Document the duplex unicast test cases Luiz Augusto von Dentz
@ 2026-11-02 18:12 ` Luiz Augusto von Dentz
2026-11-02 18:12 ` [PATCH BlueZ v1 4/6] shared/bap: Add bt_bap_stream_is_client Luiz Augusto von Dentz
` (2 subsequent siblings)
5 siblings, 0 replies; 9+ messages in thread
From: Luiz Augusto von Dentz @ 2026-11-02 18:12 UTC (permalink / raw)
To: linux-bluetooth
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
Have the initiator, in testing mode, desynchronize its linked
transports and enable the Source ASE of the acceptor alone, as a phone
may do in a voice call, then check that the transport of the Sink ASE,
still in QoS Configured, is neither pending nor acquired along with its
link.
Also check the Desynchronized property on its own, so a failure to
enable the ASEs separately can be told apart.
Assisted-by: Claude:claude-opus-5-5
---
test/functional/test_bap.py | 170 ++++++++++++++++++++++++++++++++++++
1 file changed, 170 insertions(+)
diff --git a/test/functional/test_bap.py b/test/functional/test_bap.py
index 221efd982071..5465615e8135 100644
--- a/test/functional/test_bap.py
+++ b/test/functional/test_bap.py
@@ -583,6 +583,176 @@ def test_bap_unicast_reconfigure_metadata(unicast_hosts, metadata):
)
+# The initiator runs in testing mode, so the Desynchronized property of
+# its transports can be set and each direction enabled on its own
+duplex_host_config = host_config(
+ [
+ Bluetoothd(conf=BAP_CONF.replace("[General]\n", "[General]\nTesting = true\n")),
+ Pexpect(),
+ ],
+ [Bluetoothd(conf=BAP_CONF), Pexpect()],
+)
+
+# Sockets of the acquired transports, held by the host process until
+# the test ends, as closing them releases the stream
+held_fds = []
+
+
+@mainloop_wrap
+def transports_by_uuid():
+ """Transports of the host, with their properties, keyed by UUID."""
+ objects = dbus.Interface(
+ get_dbus().get_object("org.bluez", "/"),
+ "org.freedesktop.DBus.ObjectManager",
+ ).GetManagedObjects()
+ return {
+ str(props["org.bluez.MediaTransport1"]["UUID"]): (
+ str(path),
+ {
+ str(key): value
+ for key, value in props["org.bluez.MediaTransport1"].items()
+ },
+ )
+ for path, props in objects.items()
+ if "org.bluez.MediaTransport1" in props
+ }
+
+
+@mainloop_wrap
+def desynchronize(path):
+ dbus.Interface(
+ get_dbus().get_object("org.bluez", path),
+ "org.freedesktop.DBus.Properties",
+ ).Set("org.bluez.MediaTransport1", "Desynchronized", dbus.Boolean(True))
+
+
+@mainloop_wrap
+def acquire(path):
+ """Acquire a transport, returning the D-Bus error name if it fails."""
+ try:
+ fd, _, _ = dbus.Interface(
+ get_dbus().get_object("org.bluez", path),
+ "org.bluez.MediaTransport1",
+ ).Acquire(timeout=DEFAULT_TIMEOUT)
+ except dbus.exceptions.DBusException as e:
+ return e.get_dbus_name()
+
+ held_fds.append(fd.take())
+ return None
+
+
+@mainloop_wrap
+def release(path):
+ """Release a transport, returning the D-Bus error name if it fails."""
+ try:
+ dbus.Interface(
+ get_dbus().get_object("org.bluez", path),
+ "org.bluez.MediaTransport1",
+ ).Release(timeout=DEFAULT_TIMEOUT)
+ except dbus.exceptions.DBusException as e:
+ return e.get_dbus_name()
+
+ return None
+
+
+def wait_state(host, uuid, states):
+ """Wait for the transport with the given UUID to reach one of states."""
+ deadline = time.monotonic() + DEFAULT_TIMEOUT
+
+ while True:
+ path, props = host.call(transports_by_uuid)[uuid]
+ if props["State"] in states:
+ return
+
+ assert time.monotonic() < deadline, f"{path} not {states}: {props['State']}"
+ time.sleep(0.1)
+
+
+@pytest.fixture
+def duplex_hosts(hosts):
+ """
+ Initiator (host0) and acceptor (host1) each with a local PAC Sink and
+ PAC Source endpoint for a single location, paired over LE, with a
+ stream configured for each direction over the same CIS.
+
+ Initiator transports are keyed by the UUID of the local endpoint: the
+ local PAC Source streams to the remote Sink ASE, and the local PAC
+ Sink from the remote Source ASE. Acceptor transports carry the UUID
+ of the local PAC of their ASE.
+ """
+ host0, host1 = hosts
+
+ initiator = start_bluetoothctl(host0, "bap-duplex-lc3.bt")
+ acceptor = start_bluetoothctl(host1, "bap-duplex-lc3.bt")
+
+ pair_le(host0, initiator, host1, acceptor)
+
+ expect_all(initiator, [CONFIGURED + TRANSPORT_RE] * 2)
+ expect_all(acceptor, [TRANSPORT_RE] * 2)
+
+ transports = host0.call(transports_by_uuid)
+ to_sink, props = transports[PAC_SOURCE_UUID]
+ from_source, _ = transports[PAC_SINK_UUID]
+
+ # Both directions share the CIS, so their transports are linked
+ assert [str(link) for link in props.get("Links", [])] == [from_source]
+
+ yield host0, host1, to_sink, from_source
+
+
+@duplex_host_config
+def test_bap_unicast_desync(duplex_hosts):
+ host0, host1, to_sink, from_source = duplex_hosts
+
+ # Desynchronizing a transport desynchronizes its links as well
+ host0.call(desynchronize, to_sink)
+ transports = host0.call(transports_by_uuid)
+ for uuid in (PAC_SOURCE_UUID, PAC_SINK_UUID):
+ path, props = transports[uuid]
+ assert props["Desynchronized"], f"{path} not desynchronized"
+
+ # Acquiring one transport leaves its link alone
+ assert host0.call(acquire, from_source) is None
+ wait_state(host0, PAC_SINK_UUID, ("active",))
+ path, props = host0.call(transports_by_uuid)[PAC_SOURCE_UUID]
+ assert props["State"] == "idle", f"{path} {props['State']} not acquired"
+
+ # The link is acquired on its own, over the CIS already established
+ assert host0.call(acquire, to_sink) is None
+ wait_state(host0, PAC_SOURCE_UUID, ("active",))
+
+
+@duplex_host_config
+def test_bap_unicast_duplex_enable_one(duplex_hosts):
+ host0, host1, to_sink, from_source = duplex_hosts
+
+ host0.call(desynchronize, to_sink)
+
+ # Enable the remote Source ASE only, leaving its Sink ASE in QoS,
+ # while the CIS they share is established. bluetoothctl on the
+ # acceptor acquires a transport as soon as it is pending, as an
+ # audio server would.
+ assert host0.call(acquire, from_source) is None
+ wait_state(host1, PAC_SOURCE_UUID, ("pending", "active"))
+
+ # The Sink ASE has not been enabled, so its transport must not be
+ # reported as pending, nor acquired along with the Source one
+ path, props = host1.call(transports_by_uuid)[PAC_SINK_UUID]
+ assert props["State"] == "idle", f"{path} {props['State']} while in QoS"
+
+ # Once the initiator enables it, it is acquired on its own
+ assert host0.call(acquire, to_sink) is None
+ wait_state(host1, PAC_SINK_UUID, ("active",))
+
+ # Disabling the Sink ASE alone leaves the Source one streaming
+ assert host0.call(release, to_sink) is None
+ wait_state(host1, PAC_SINK_UUID, ("idle",))
+ time.sleep(1)
+ for host, uuid in ((host0, PAC_SINK_UUID), (host1, PAC_SOURCE_UUID)):
+ path, props = host.call(transports_by_uuid)[uuid]
+ assert props["State"] == "active", f"{path} {props['State']}"
+
+
# Broadcast code used by the broadcast scripts, see BCAST_CODE in
# client/player.c
BCAST_CODE = (
--
2.56.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH BlueZ v1 4/6] shared/bap: Add bt_bap_stream_is_client
2026-11-02 18:12 [PATCH BlueZ v1 0/6] Fix linked transports of an ASE not enabled yet Luiz Augusto von Dentz
` (2 preceding siblings ...)
2026-11-02 18:12 ` [PATCH BlueZ v1 3/6] test: functional: Cover enabling one ASE of a bidirectional CIS Luiz Augusto von Dentz
@ 2026-11-02 18:12 ` Luiz Augusto von Dentz
2026-11-02 18:12 ` [PATCH BlueZ v1 5/6] transport: Don't set linked transports pending before they are enabled Luiz Augusto von Dentz
2026-11-02 18:12 ` [PATCH BlueZ v1 6/6] transport: Don't disable desynchronized links on release Luiz Augusto von Dentz
5 siblings, 0 replies; 9+ messages in thread
From: Luiz Augusto von Dentz @ 2026-11-02 18:12 UTC (permalink / raw)
To: linux-bluetooth
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
Tell whether the stream is handled by the local client, which enables
its ASEs, or by the server, whose ASEs are enabled by the remote.
Assisted-by: Claude:claude-opus-5-5
---
src/shared/bap.c | 8 ++++++++
src/shared/bap.h | 2 ++
2 files changed, 10 insertions(+)
diff --git a/src/shared/bap.c b/src/shared/bap.c
index 10cc81a51c3e..b1180b532b8d 100644
--- a/src/shared/bap.c
+++ b/src/shared/bap.c
@@ -4549,6 +4549,14 @@ struct iovec *bt_bap_pac_get_metadata(struct bt_bap_pac *pac)
return pac->metadata;
}
+bool bt_bap_stream_is_client(struct bt_bap_stream *stream)
+{
+ if (!stream)
+ return false;
+
+ return stream->client;
+}
+
uint8_t bt_bap_stream_get_type(struct bt_bap_stream *stream)
{
if (!stream)
diff --git a/src/shared/bap.h b/src/shared/bap.h
index 5bdc5f8dbc34..242bfbb39b30 100644
--- a/src/shared/bap.h
+++ b/src/shared/bap.h
@@ -122,6 +122,8 @@ struct iovec *bt_bap_pac_get_metadata(struct bt_bap_pac *pac);
uint8_t bt_bap_stream_get_type(struct bt_bap_stream *stream);
+bool bt_bap_stream_is_client(struct bt_bap_stream *stream);
+
struct bt_bap_stream *bt_bap_pac_get_stream(struct bt_bap_pac *pac);
/* Session related function */
--
2.56.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH BlueZ v1 5/6] transport: Don't set linked transports pending before they are enabled
2026-11-02 18:12 [PATCH BlueZ v1 0/6] Fix linked transports of an ASE not enabled yet Luiz Augusto von Dentz
` (3 preceding siblings ...)
2026-11-02 18:12 ` [PATCH BlueZ v1 4/6] shared/bap: Add bt_bap_stream_is_client Luiz Augusto von Dentz
@ 2026-11-02 18:12 ` Luiz Augusto von Dentz
2026-11-02 18:12 ` [PATCH BlueZ v1 6/6] transport: Don't disable desynchronized links on release Luiz Augusto von Dentz
5 siblings, 0 replies; 9+ messages in thread
From: Luiz Augusto von Dentz @ 2026-11-02 18:12 UTC (permalink / raw)
To: linux-bluetooth
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
As a server, when the client enables one ASE of a bidirectional CIS
before the other, the transport of the enabled one becomes pending once
the CIS is established, and so did its link, still in QoS Configured.
An audio server would then acquire the link, which was rejected with
NotAuthorized as a server cannot enable an ASE, or acquire the enabled
one and make the link active along with it:
bap.c:ep_enable() ep 0x5e382ebaba40 id 0x03 dir 0x02
transport.c:transport_set_state() fd1: IDLE -> PENDING
transport.c:transport_set_state() fd0: IDLE -> PENDING
> org.bluez.MediaTransport1.Acquire fd0
< org.bluez.Error.NotAuthorized
As a server, only update the state and owner of a link once its stream
is enabled, only make it active along with the transport if it was
given its owner, keep the owner of a link acquired on its own, and
don't set a link that is acquired already back to pending.
A link acquired on its own is also released on its own: don't propagate
the state of the transport to it, nor disable it along with the stream,
or disabling one ASE would stop the other direction as well.
Fixes: 47ba235f5a6f ("profiles: Add initial code for bap plugin")
Assisted-by: Claude:claude-opus-5-5
---
profiles/audio/transport.c | 95 ++++++++++++++++++++++++++++++++++++--
1 file changed, 92 insertions(+), 3 deletions(-)
diff --git a/profiles/audio/transport.c b/profiles/audio/transport.c
index e001f73151d9..c6b675f0b6ec 100644
--- a/profiles/audio/transport.c
+++ b/profiles/audio/transport.c
@@ -727,6 +727,44 @@ static void media_owner_exit(DBusConnection *connection, void *user_data)
media_transport_remove_owner(owner->transport);
}
+static bool bap_link_enabled(struct bt_bap_stream *link)
+{
+ /* A client enables its links along with the stream, while a server
+ * has to wait for the client to enable each of them.
+ */
+ if (bt_bap_stream_is_client(link))
+ return true;
+
+ switch (bt_bap_stream_get_state(link)) {
+ case BT_BAP_STREAM_STATE_ENABLING:
+ case BT_BAP_STREAM_STATE_STREAMING:
+ return true;
+ }
+
+ return false;
+}
+
+static bool bap_link_independent(struct bt_bap_stream *link,
+ struct media_owner *owner)
+{
+ struct media_transport *transport;
+
+ /* A server has its links enabled separately by the client, so they
+ * may have been acquired by a different owner.
+ */
+ if (bt_bap_stream_is_client(link))
+ return false;
+
+ transport = find_transport_by_bap_stream(link);
+
+ return transport && transport->owner && transport->owner != owner;
+}
+
+static bool match_link_independent(const void *data, const void *user_data)
+{
+ return bap_link_independent((void *)data, (void *)user_data);
+}
+
static void linked_transport_set_owner(void *data, void *user_data)
{
struct bt_bap_stream *stream = data;
@@ -739,6 +777,19 @@ static void linked_transport_set_owner(void *data, void *user_data)
return;
}
+ /* A link that is not enabled cannot be acquired yet, and once it is
+ * it will be acquired on its own.
+ */
+ if (!bap_link_enabled(stream))
+ return;
+
+ /* As a server, links are enabled separately by the client, so a link
+ * acquired already keeps its own owner.
+ */
+ if (!bt_bap_stream_is_client(stream) && transport->owner &&
+ transport->owner != owner)
+ return;
+
/* If the linked transport already has a different owner (e.g. it
* was Acquired separately), tear it down properly instead of
* silently overwriting it. Otherwise the previous owner is
@@ -2209,6 +2260,7 @@ static guint transport_bap_suspend(struct media_transport *transport,
struct bap_transport *bap = transport->data;
struct queue *links = bt_bap_stream_io_get_links(bap->stream);
bt_bap_stream_func_t func = NULL;
+ bool disable_links;
guint id;
if (!bap->stream)
@@ -2225,7 +2277,13 @@ static guint transport_bap_suspend(struct media_transport *transport,
bap_update_links(transport);
- id = bt_bap_stream_disable(bap->stream, bap->linked, func, owner);
+ /* Don't disable links acquired on their own, they are disabled
+ * when they are released.
+ */
+ disable_links = bap->linked &&
+ !queue_find(links, match_link_independent, owner);
+
+ id = bt_bap_stream_disable(bap->stream, disable_links, func, owner);
if (bt_bap_stream_get_type(bap->stream) == BT_BAP_STREAM_TYPE_BCAST) {
if (transport->owner == owner)
@@ -2262,10 +2320,16 @@ static void transport_bap_cancel(struct media_transport *transport, guint id)
bt_bap_stream_cancel(bap->stream, id);
}
+struct link_state {
+ transport_state_t state;
+ struct media_owner *owner;
+};
+
static void link_set_state(void *data, void *user_data)
{
struct bt_bap_stream *stream = data;
- transport_state_t state = PTR_TO_UINT(user_data);
+ struct link_state *ls = user_data;
+ transport_state_t state = ls->state;
struct media_transport *transport;
transport = find_transport_by_bap_stream(stream);
@@ -2274,6 +2338,30 @@ static void link_set_state(void *data, void *user_data)
return;
}
+ /* A link acquired on its own keeps its state */
+ if (bap_link_independent(stream, ls->owner))
+ return;
+
+ /* Don't bring back a link that is acquired already */
+ if (state == TRANSPORT_STATE_PENDING &&
+ transport->state >= TRANSPORT_STATE_REQUESTING)
+ return;
+
+ /* A link whose stream is not enabled cannot be acquired */
+ if ((state == TRANSPORT_STATE_PENDING ||
+ state == TRANSPORT_STATE_ACTIVE) &&
+ !bap_link_enabled(stream))
+ return;
+
+ /* As a server, a link only becomes active along with the transport
+ * if it was given its owner, otherwise it may have been enabled in
+ * the meantime and has to be acquired on its own.
+ */
+ if (state == TRANSPORT_STATE_ACTIVE &&
+ !bt_bap_stream_is_client(stream) &&
+ transport->owner != ls->owner)
+ return;
+
transport_set_state(transport, state);
}
@@ -2281,13 +2369,14 @@ static void transport_bap_set_state(struct media_transport *transport,
transport_state_t state)
{
struct bap_transport *bap = transport->data;
+ struct link_state ls = { state, transport->owner };
if (!bap->linked || bap->desync)
return;
/* Update links */
queue_foreach(bt_bap_stream_io_get_links(bap->stream), link_set_state,
- UINT_TO_PTR(state));
+ &ls);
}
static void bap_metadata_changed(struct media_transport *transport)
--
2.56.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH BlueZ v1 6/6] transport: Don't disable desynchronized links on release
2026-11-02 18:12 [PATCH BlueZ v1 0/6] Fix linked transports of an ASE not enabled yet Luiz Augusto von Dentz
` (4 preceding siblings ...)
2026-11-02 18:12 ` [PATCH BlueZ v1 5/6] transport: Don't set linked transports pending before they are enabled Luiz Augusto von Dentz
@ 2026-11-02 18:12 ` Luiz Augusto von Dentz
5 siblings, 0 replies; 9+ messages in thread
From: Luiz Augusto von Dentz @ 2026-11-02 18:12 UTC (permalink / raw)
To: linux-bluetooth
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
Releasing a desynchronized transport also disabled its links, which
are acquired and released on their own, so the other direction of a
CIS stopped while still acquired.
Only disable the links along with the stream when they are
synchronized, as is done when enabling them.
Fixes: 1db06505df5a ("audio: Add ability to desynchronized linked transport")
Assisted-by: Claude:claude-opus-5-5
---
profiles/audio/transport.c | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/profiles/audio/transport.c b/profiles/audio/transport.c
index c6b675f0b6ec..285db0d37554 100644
--- a/profiles/audio/transport.c
+++ b/profiles/audio/transport.c
@@ -2277,10 +2277,10 @@ static guint transport_bap_suspend(struct media_transport *transport,
bap_update_links(transport);
- /* Don't disable links acquired on their own, they are disabled
- * when they are released.
+ /* Don't disable links that are desynchronized or acquired on their
+ * own, they are disabled when they are released.
*/
- disable_links = bap->linked &&
+ disable_links = bap->linked && !bap->desync &&
!queue_find(links, match_link_independent, owner);
id = bt_bap_stream_disable(bap->stream, disable_links, func, owner);
--
2.56.0
^ permalink raw reply related [flat|nested] 9+ messages in thread