From: Luiz Augusto von Dentz <luiz.dentz@gmail.com>
To: linux-bluetooth@vger.kernel.org
Subject: [PATCH BlueZ v1 6/6] test: functional: add the true wireless broadcast assistant test
Date: Mon, 21 Sep 2026 17:01:23 -0400 [thread overview]
Message-ID: <20260921210123.3690449-7-luiz.dentz@gmail.com> (raw)
In-Reply-To: <20260921210123.3690449-1-luiz.dentz@gmail.com>
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
Assisted-by: opencode:claude-opus-5
---
client/scripts/broadcast-delegator-left.bt | 32 ++++
client/scripts/broadcast-delegator-right.bt | 32 ++++
doc/functional-bap.rst | 6 +
test/functional/test_bap.py | 181 ++++++++++++++++++--
4 files changed, 239 insertions(+), 12 deletions(-)
create mode 100644 client/scripts/broadcast-delegator-left.bt
create mode 100644 client/scripts/broadcast-delegator-right.bt
diff --git a/client/scripts/broadcast-delegator-left.bt b/client/scripts/broadcast-delegator-left.bt
new file mode 100644
index 000000000000..818b30567be6
--- /dev/null
+++ b/client/scripts/broadcast-delegator-left.bt
@@ -0,0 +1,32 @@
+power on
+#
+#
+# Register a Broadcast Sink endpoint with LC3 taking a single channel,
+# to act as the left member of a coordinated set of Scan Delegators.
+# The members of the set are programmed with the same SIRK, see [CSIS]
+# in main.conf.
+#
+endpoint.register 00001851-0000-1000-8000-00805f9b34fb 0x06
+# Auto Accept: accept the configuration chosen by the peer
+y
+# Max Transports: no limit
+a
+# Locations: Front Left, so only the BIS carrying that channel is synced
+1
+# Supported Context: Media
+4
+#
+#
+# Select and acquire automatically the transports of the streams the
+# Broadcast Assistant adds.
+#
+transport.select auto
+transport.acquire auto
+#
+#
+# Advertise the Resolvable Set Identifier, so the assistant is able to
+# resolve this device as a member of the set, and connecting one member
+# connects the other.
+#
+advertise.rsi on
+advertise on
diff --git a/client/scripts/broadcast-delegator-right.bt b/client/scripts/broadcast-delegator-right.bt
new file mode 100644
index 000000000000..bfd1888987bc
--- /dev/null
+++ b/client/scripts/broadcast-delegator-right.bt
@@ -0,0 +1,32 @@
+power on
+#
+#
+# Register a Broadcast Sink endpoint with LC3 taking a single channel,
+# to act as the right member of a coordinated set of Scan Delegators.
+# The members of the set are programmed with the same SIRK, see [CSIS]
+# in main.conf.
+#
+endpoint.register 00001851-0000-1000-8000-00805f9b34fb 0x06
+# Auto Accept: accept the configuration chosen by the peer
+y
+# Max Transports: no limit
+a
+# Locations: Front Right, so only the BIS carrying that channel is synced
+2
+# Supported Context: Media
+4
+#
+#
+# Select and acquire automatically the transports of the streams the
+# Broadcast Assistant adds.
+#
+transport.select auto
+transport.acquire auto
+#
+#
+# Advertise the Resolvable Set Identifier, so the assistant is able to
+# resolve this device as a member of the set, and connecting one member
+# connects the other.
+#
+advertise.rsi on
+advertise on
diff --git a/doc/functional-bap.rst b/doc/functional-bap.rst
index 7059e0c735f1..74a6604ad285 100644
--- a/doc/functional-bap.rst
+++ b/doc/functional-bap.rst
@@ -648,6 +648,12 @@ test_bass_past_earbuds_transport_acquire
characteristic of each member is read over its own connection,
which is why the services of both are waited for.
+ The hosts are reused between tests, so the members may be bonded
+ already, in which case ``pair`` connects them instead, see
+ **bluetoothctl(1)**. What is already in place is then checked over
+ D-Bus rather than waited for on the output, as a device whose
+ services are already resolved does not report it again.
+
Each member gets a push of its own: BASS has no notion of a set,
so the assistant adds the source to the Broadcast Receive State
of each delegator separately, and each of them receives the
diff --git a/test/functional/test_bap.py b/test/functional/test_bap.py
index bfcb43c0ad67..8d0b4489d766 100644
--- a/test/functional/test_bap.py
+++ b/test/functional/test_bap.py
@@ -5,6 +5,7 @@ Tests for BAP (LE Audio) using bluetoothctl in VM instances
"""
import threading
+import time
import warnings
import dbus
@@ -113,7 +114,7 @@ def pair_le(host0, ctl0, host1, ctl1, advertise=True, services=False):
ctl0.expect(f"Device {host1.bdaddr.upper()}")
ctl0.send(f"pair {host1.bdaddr.upper()}\n")
- pending = ["Pairing successful"]
+ pending = [PAIRED]
if services:
pending.append(f"Device {host1.bdaddr.upper()} ServicesResolved: yes")
@@ -170,6 +171,11 @@ def remove_device(ctl0, host):
)
+# A device that is already paired is connected instead, see cmd_pair in
+# client/main.c, so a pairing reports one or the other
+PAIRED = r"Pairing successful|Connection successful"
+
+
def pair_wait(ctl0, ctl1, pending):
"""
Wait for the given events while answering the pairing requests.
@@ -326,6 +332,41 @@ def transport_paths():
)
+@mainloop_wrap
+def device_properties(paths):
+ """Properties of the given devices, empty for those that are unknown."""
+ objects = dbus.Interface(
+ get_dbus().get_object("org.bluez", "/"),
+ "org.freedesktop.DBus.ObjectManager",
+ ).GetManagedObjects()
+ return {
+ path: {
+ str(key): bool(value)
+ for key, value in objects.get(path, {}).get("org.bluez.Device1", {}).items()
+ if key in ("Paired", "Connected", "ServicesResolved")
+ }
+ for path in paths
+ }
+
+
+def wait_properties(host, paths, name):
+ """
+ Wait for the given property of the given devices to be set.
+
+ Polled rather than waited for on the output, as a device that is
+ already in that state does not report it again.
+ """
+ deadline = time.monotonic() + DEFAULT_TIMEOUT
+
+ while True:
+ props = host.call(device_properties, paths)
+ if all(props[path].get(name) for path in paths):
+ return
+
+ assert time.monotonic() < deadline, f"{name} not set on {paths}: {props}"
+ time.sleep(0.2)
+
+
def add_preset(ctl, name, props, metadata):
"""Make a custom preset from a transport's codec configuration and QoS."""
@@ -571,6 +612,28 @@ past_host_config = host_config(
LOCAL_ASSISTANT_RE = r"Assistant (/org/bluez/\S+/sid\d+/bis\d+)"
+# Local MediaAssistant object of a given BIS of the local broadcast
+LOCAL_ASSISTANT = r"Assistant (/org/bluez/hci0/sid\d+/bis{})"
+
+
+def push_assistant(source, assistant_path, host):
+ """
+ Share the local stream of the given MediaAssistant object with the
+ given delegator, which receives the periodic advertising sync over
+ the connection (PAST).
+ """
+ source.send(f"assistant.push {assistant_path}\n")
+ source.expect(r"Enter Device \(path\):")
+ source.send(f"/org/bluez/hci0/dev_{dev_addr(host)}\n")
+
+ # The local stream may already know the broadcast code
+ idx, _ = source.expect(
+ [r"Enter Broadcast Code \(auto/value\):", r"Assistant \S+ pushed"]
+ )
+ if idx == 0:
+ source.send(f"{BCAST_CODE}\n")
+ source.expect(r"Assistant \S+ pushed")
+
@past_host_config
def test_bass_past_transport_acquire(hosts):
@@ -602,17 +665,7 @@ def test_bass_past_transport_acquire(hosts):
# Share the local broadcast: the delegator receives the periodic
# advertising sync over the connection (PAST)
- source.send(f"assistant.push {assistant_path}\n")
- source.expect(r"Enter Device \(path\):")
- source.send(f"/org/bluez/hci0/dev_{dev_addr(delegator_host)}\n")
-
- # The local stream may already know the broadcast code
- idx, _ = source.expect(
- [r"Enter Broadcast Code \(auto/value\):", r"Assistant \S+ pushed"]
- )
- if idx == 0:
- source.send(f"{BCAST_CODE}\n")
- source.expect(r"Assistant \S+ pushed")
+ push_assistant(source, assistant_path, delegator_host)
# A transport is created on the delegator, selected and acquired
# automatically
@@ -795,3 +848,107 @@ def test_bap_unicast_set_transport_acquire(set_hosts):
[acquired, acquired]
+ [f"Transport {transport} State: active" for transport in transports],
)
+
+
+def discover_set(source_host, source, left_host, right_host):
+ """
+ Scan until both members of the set have been found.
+
+ The set is resolved from the RSI of the members that are already
+ known, so connecting before both have been found would leave the
+ one found later out of it.
+ """
+ source.send("scan on\n")
+ source.expect(f"Controller {source_host.bdaddr.upper()} Discovering: yes")
+
+ expect_all(
+ source,
+ [
+ f"Device {left_host.bdaddr.upper()}",
+ f"Device {right_host.bdaddr.upper()}",
+ ],
+ )
+
+ # Stop scanning before connecting, so the discovery does not
+ # interfere with the connections to the members
+ source.send("scan off\n")
+ source.expect(f"Controller {source_host.bdaddr.upper()} Discovering: no")
+
+
+bass_set_host_config = host_config(
+ [Bluetoothd(conf=BAP_CONF), Pexpect()],
+ [Bluetoothd(conf=csip_conf(1)), Pexpect()],
+ [Bluetoothd(conf=csip_conf(2)), Pexpect()],
+)
+
+
+@bass_set_host_config
+def test_bass_past_earbuds_transport_acquire(hosts):
+ source_host, left_host, right_host = hosts
+
+ # Source broadcasting one BIS per channel, with its own streams
+ # exposed as local MediaAssistant objects, one per BIS
+ source = start_bluetoothctl(source_host, "broadcast-source-2bis.bt")
+ groups = expect_all(
+ source,
+ [LOCAL_ASSISTANT.format(1), LOCAL_ASSISTANT.format(2), ACQUIRED, ACQUIRED],
+ )
+ assistants = [m[0].decode("utf-8") for m in groups[:2]]
+
+ # Delegators advertising, selecting and acquiring automatically
+ left = start_bluetoothctl(left_host, "broadcast-delegator-left.bt")
+ right = start_bluetoothctl(right_host, "broadcast-delegator-right.bt")
+
+ # Every member has to be advertising before connecting, so the set
+ # can be resolved and the remaining members found
+ left.expect("Advertising object registered")
+ right.expect("Advertising object registered")
+
+ paths = [
+ f"/org/bluez/hci0/dev_{dev_addr(host)}" for host in (left_host, right_host)
+ ]
+
+ # The hosts are reused, so a previous test may have left the
+ # members bonded, in which case they are known already and the
+ # discovery below would report nothing for them, as only what
+ # changes is reported: they are connected directly instead.
+ props = source_host.call(device_properties, paths)
+
+ if not all(props[path].get("Paired") for path in paths):
+ discover_set(source_host, source, left_host, right_host)
+
+ # Connecting one member connects the rest of the set, so a single
+ # request covers both, and pairing a member that is bonded already
+ # connects it, see cmd_pair in client/main.c
+ source.send(f"pair {left_host.bdaddr.upper()}\n")
+ pair_wait(source, left, [PAIRED])
+
+ # The Broadcast Receive State of each member is read over its own
+ # connection, so the services of both have to be resolved, and an
+ # encrypted link is required for the read to succeed.
+ #
+ # Checked over D-Bus rather than waited for on the output, as a
+ # device whose services are already resolved does not report it
+ # again, e.g. when the members were bonded already.
+ wait_properties(source_host, paths, "ServicesResolved")
+
+ # One push per member, each carrying the BIS of its own channel:
+ # BASS has no notion of a set, so the source is added to the
+ # Broadcast Receive State of each delegator separately, and each of
+ # them receives the periodic advertising sync over its own ACL
+ for assistant, host in zip(assistants, (left_host, right_host)):
+ push_assistant(source, assistant, host)
+
+ # A transport is created on each side for the BIS that was pushed
+ # to it, and selected and acquired automatically
+ for ctl, host, bis in ((left, left_host, 1), (right, right_host, 2)):
+ transport = expect_bis_transport(ctl, host, bis)
+
+ expect_all(
+ ctl,
+ [
+ ACQUIRED,
+ f"Transport {transport} State: broadcasting",
+ f"Transport {transport} State: active",
+ ],
+ )
--
2.55.0
next prev parent reply other threads:[~2026-09-21 21:01 UTC|newest]
Thread overview: 9+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-21 21:01 [PATCH BlueZ v1 0/6] Add true wireless BAP broadcast tests Luiz Augusto von Dentz
2026-09-21 21:01 ` [PATCH BlueZ v1 1/6] doc: describe the true wireless broadcast test cases Luiz Augusto von Dentz
2026-09-21 23:47 ` Add true wireless BAP broadcast tests bluez.test.bot
2026-09-21 21:01 ` [PATCH BlueZ v1 2/6] doc: describe the true wireless broadcast assistant test case Luiz Augusto von Dentz
2026-09-21 21:01 ` [PATCH BlueZ v1 3/6] test: functional: add the true wireless broadcast tests Luiz Augusto von Dentz
2026-09-21 21:01 ` [PATCH BlueZ v1 4/6] bass: keep the push target per MediaAssistant object Luiz Augusto von Dentz
2026-09-21 21:01 ` [PATCH BlueZ v1 5/6] client: connect a device that is already paired Luiz Augusto von Dentz
2026-09-21 21:01 ` Luiz Augusto von Dentz [this message]
2026-09-22 15:00 ` [PATCH BlueZ v1 0/6] Add true wireless BAP broadcast tests patchwork-bot+bluetooth
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260921210123.3690449-7-luiz.dentz@gmail.com \
--to=luiz.dentz@gmail.com \
--cc=linux-bluetooth@vger.kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox