Linux bluetooth development
 help / color / mirror / Atom feed
* [PATCH BlueZ v3 0/5] Add Role property to org.bluez.Bearer.LE1
@ 2026-10-09 18:04 Kevin Dewald
  2026-10-09 18:04 ` [PATCH BlueZ v3 1/5] org.bluez.Bearer: Add Role property Kevin Dewald
                   ` (5 more replies)
  0 siblings, 6 replies; 8+ messages in thread
From: Kevin Dewald @ 2026-10-09 18:04 UTC (permalink / raw)
  To: linux-bluetooth; +Cc: Luiz Augusto von Dentz, Kevin Dewald

Applications acting as a peripheral, or in both roles, cannot tell
whether a connected device is a central that connected to the adapter
or a peripheral the adapter connected to. When several applications
share the adapter, a device object can appear because of any of them,
so the order of events does not answer it either.

bluetoothd already tracks this per bearer from the Device Connected
event. This series exposes it as a Role property on
org.bluez.Bearer.LE1, "central" or "peripheral", present only while
the device is connected over LE, and prints it as LE.Role in
bluetoothctl info. The documentation states that it is the link layer
role and says nothing about GATT client and server roles.

As a step towards marking the bearer interfaces stable, the series also
adds functional tests for them, test/functional/test_bearer.py,
described in doc/functional-bearer.rst. Two hosts connect or pair over
LE or BR/EDR, and each checks the bearer interfaces of the device
object it has for the other: Connect and Disconnect, Connected and the
Disconnected signal with its reason, Paired and Bonded, Connectable for
a connectable advertiser and for a broadcaster, and Role on both sides
of an LE connection.

The five tests take about 45 seconds and passed in five consecutive
runs, and the rest of test-functional still passes.

If these tests cover what you had in mind, I can follow up with a patch
marking the bearer interfaces stable.

Changes in v3:
- Add functional tests for the bearer interfaces, with their
  documentation (Luiz)

Changes in v2:
- Move the property from org.bluez.Device1 to org.bluez.Bearer.LE1, so
  it is clearly per connection (Luiz)
- State in the documentation that it says nothing about GATT roles
  (Luiz)

Kevin Dewald (5):
  org.bluez.Bearer: Add Role property
  bearer: Add Role property
  client: Print LE.Role in info
  doc: Add functional-bearer documentation
  test: functional: add bearer tests

 Makefile.am                    |   1 +
 client/main.c                  |   1 +
 doc/functional-bearer.rst      | 141 +++++++++++++++
 doc/functional-testing.rst     |   1 +
 doc/org.bluez.Bearer.LE.rst    |  24 +++
 src/bearer.c                   |  42 +++++
 src/device.c                   |   5 +
 src/device.h                   |   1 +
 test/functional/test_bearer.py | 302 +++++++++++++++++++++++++++++++++
 9 files changed, 518 insertions(+)
 create mode 100644 doc/functional-bearer.rst
 create mode 100644 test/functional/test_bearer.py

-- 
2.54.0 (Apple Git-157)


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [PATCH BlueZ v3 1/5] org.bluez.Bearer: Add Role property
  2026-10-09 18:04 [PATCH BlueZ v3 0/5] Add Role property to org.bluez.Bearer.LE1 Kevin Dewald
@ 2026-10-09 18:04 ` Kevin Dewald
  2026-10-09 21:01   ` Add Role property to org.bluez.Bearer.LE1 bluez.test.bot
  2026-10-09 18:04 ` [PATCH BlueZ v3 2/5] bearer: Add Role property Kevin Dewald
                   ` (4 subsequent siblings)
  5 siblings, 1 reply; 8+ messages in thread
From: Kevin Dewald @ 2026-10-09 18:04 UTC (permalink / raw)
  To: linux-bluetooth; +Cc: Luiz Augusto von Dentz, Kevin Dewald

This adds Role property to org.bluez.Bearer.LE1 which indicates whether
the remote device is the central or the peripheral of the current LE
connection, so applications can tell a central that connected to the
adapter from a peripheral the adapter connected to.

Assisted-by: Claude:claude-opus-5.5
---
 doc/org.bluez.Bearer.LE.rst | 24 ++++++++++++++++++++++++
 1 file changed, 24 insertions(+)

diff --git a/doc/org.bluez.Bearer.LE.rst b/doc/org.bluez.Bearer.LE.rst
index ec92a5ce5..dfd809739 100644
--- a/doc/org.bluez.Bearer.LE.rst
+++ b/doc/org.bluez.Bearer.LE.rst
@@ -153,3 +153,27 @@ The property is only present once the device has been discovered or connected
 over LE bearer.
 
 A PropertiesChanged signal indicate changes to this status.
+
+string Role [readonly, optional, experimental]
+``````````````````````````````````````````````
+
+Indicates the role of the remote device in the current connection on LE bearer.
+It is only present while the remote device is connected over LE bearer.
+
+It is the link layer role of the connection. It does not imply which side acts
+as GATT client or server, as GATT roles are independent of it and both may be
+used at the same time.
+
+Possible values:
+
+:"central":
+
+	The remote device initiated the connection, and the adapter is the
+	peripheral.
+
+:"peripheral":
+
+	The adapter initiated the connection, and the remote device is the
+	peripheral.
+
+A PropertiesChanged signal indicate changes to this status.
-- 
2.54.0 (Apple Git-157)


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH BlueZ v3 2/5] bearer: Add Role property
  2026-10-09 18:04 [PATCH BlueZ v3 0/5] Add Role property to org.bluez.Bearer.LE1 Kevin Dewald
  2026-10-09 18:04 ` [PATCH BlueZ v3 1/5] org.bluez.Bearer: Add Role property Kevin Dewald
@ 2026-10-09 18:04 ` Kevin Dewald
  2026-10-09 18:04 ` [PATCH BlueZ v3 3/5] client: Print LE.Role in info Kevin Dewald
                   ` (3 subsequent siblings)
  5 siblings, 0 replies; 8+ messages in thread
From: Kevin Dewald @ 2026-10-09 18:04 UTC (permalink / raw)
  To: linux-bluetooth; +Cc: Luiz Augusto von Dentz, Kevin Dewald

This implements Role property for the LE bearer using the initiator
flag of the Device Connected event, which the kernel sets when the
adapter created the connection as central, and emits PropertiesChanged
along with Connected.

Assisted-by: Claude:claude-opus-5.5
---
 src/bearer.c | 42 ++++++++++++++++++++++++++++++++++++++++++
 src/device.c |  5 +++++
 src/device.h |  1 +
 3 files changed, 48 insertions(+)

diff --git a/src/bearer.c b/src/bearer.c
index f3b7a2337..25224dcc7 100644
--- a/src/bearer.c
+++ b/src/bearer.c
@@ -274,6 +274,34 @@ static gboolean bearer_connectable_exists(const GDBusPropertyTable *property,
 	return btd_device_bdaddr_type_seen(bearer->device, bearer->type);
 }
 
+static gboolean bearer_get_role(const GDBusPropertyTable *property,
+					DBusMessageIter *iter, void *data)
+{
+	struct btd_bearer *bearer = data;
+	const char *role;
+
+	/* The initiator of an LE connection is always the central */
+	if (btd_device_bdaddr_type_initiator(bearer->device, bearer->type))
+		role = "peripheral";
+	else
+		role = "central";
+
+	dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &role);
+
+	return TRUE;
+}
+
+static gboolean bearer_role_exists(const GDBusPropertyTable *property,
+								void *data)
+{
+	struct btd_bearer *bearer = data;
+
+	if (bearer->type == BDADDR_BREDR)
+		return FALSE;
+
+	return btd_device_bdaddr_type_connected(bearer->device, bearer->type);
+}
+
 static const GDBusSignalTable bearer_signals[] = {
 	{ GDBUS_SIGNAL("Disconnected",
 			GDBUS_ARGS({ "name", "s" }, { "message", "s" })) },
@@ -292,6 +320,8 @@ static const GDBusPropertyTable bearer_properties[] = {
 	{ "Connectable", "b", bearer_get_connectable, NULL,
 			bearer_connectable_exists,
 			G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
+	{ "Role", "s", bearer_get_role, NULL, bearer_role_exists,
+			G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
 	{}
 };
 
@@ -416,6 +446,12 @@ void btd_bearer_connected(struct btd_bearer *bearer, int err)
 	g_dbus_emit_property_changed(btd_get_dbus_connection(), bearer->path,
 					bearer_interface(bearer->type),
 					"Connected");
+
+	if (!err && bearer->type != BDADDR_BREDR)
+		g_dbus_emit_property_changed(btd_get_dbus_connection(),
+					bearer->path,
+					bearer_interface(bearer->type),
+					"Role");
 }
 
 void btd_bearer_disconnected(struct btd_bearer *bearer, uint8_t reason)
@@ -444,6 +480,12 @@ void btd_bearer_disconnected(struct btd_bearer *bearer, uint8_t reason)
 					bearer_interface(bearer->type),
 					"Connected");
 
+	if (bearer->type != BDADDR_BREDR)
+		g_dbus_emit_property_changed(btd_get_dbus_connection(),
+					bearer->path,
+					bearer_interface(bearer->type),
+					"Role");
+
 	switch (reason) {
 	case MGMT_DEV_DISCONN_UNKNOWN:
 		name = "org.bluez.Reason.Unknown";
diff --git a/src/device.c b/src/device.c
index 1ab4e5342..4ccafd15a 100644
--- a/src/device.c
+++ b/src/device.c
@@ -3869,6 +3869,11 @@ bool btd_device_bdaddr_type_seen(struct btd_device *dev, uint8_t type)
 	return get_state(dev, type)->last_seen != 0;
 }
 
+bool btd_device_bdaddr_type_initiator(struct btd_device *dev, uint8_t type)
+{
+	return get_state(dev, type)->initiator;
+}
+
 static void clear_temporary_timer(struct btd_device *dev)
 {
 	if (dev->temporary_timer) {
diff --git a/src/device.h b/src/device.h
index 756540056..5ead73958 100644
--- a/src/device.h
+++ b/src/device.h
@@ -129,6 +129,7 @@ bool btd_device_bearer_is_connected(struct btd_device *dev);
 bool btd_device_bdaddr_type_connected(struct btd_device *dev, uint8_t type);
 bool btd_device_bdaddr_type_connectable(struct btd_device *dev, uint8_t type);
 bool btd_device_bdaddr_type_seen(struct btd_device *dev, uint8_t type);
+bool btd_device_bdaddr_type_initiator(struct btd_device *dev, uint8_t type);
 uint8_t btd_device_get_bdaddr_type(struct btd_device *dev);
 bool device_is_retrying(struct btd_device *device);
 void device_bonding_complete(struct btd_device *device, uint8_t bdaddr_type,
-- 
2.54.0 (Apple Git-157)


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH BlueZ v3 3/5] client: Print LE.Role in info
  2026-10-09 18:04 [PATCH BlueZ v3 0/5] Add Role property to org.bluez.Bearer.LE1 Kevin Dewald
  2026-10-09 18:04 ` [PATCH BlueZ v3 1/5] org.bluez.Bearer: Add Role property Kevin Dewald
  2026-10-09 18:04 ` [PATCH BlueZ v3 2/5] bearer: Add Role property Kevin Dewald
@ 2026-10-09 18:04 ` Kevin Dewald
  2026-10-09 18:04 ` [PATCH BlueZ v3 4/5] doc: Add functional-bearer documentation Kevin Dewald
                   ` (2 subsequent siblings)
  5 siblings, 0 replies; 8+ messages in thread
From: Kevin Dewald @ 2026-10-09 18:04 UTC (permalink / raw)
  To: linux-bluetooth; +Cc: Luiz Augusto von Dentz, Kevin Dewald

This prints the Role property of the LE bearer with the likes of info
command:

bluetoothctl> info <addr>
...
        LE.Connected: yes
        LE.Connectable: yes
        LE.Role: central

Assisted-by: Claude:claude-opus-5.5
---
 client/main.c | 1 +
 1 file changed, 1 insertion(+)

diff --git a/client/main.c b/client/main.c
index fff75056e..01ed00073 100644
--- a/client/main.c
+++ b/client/main.c
@@ -2097,6 +2097,7 @@ static void cmd_info(int argc, char *argv[])
 		print_property_with_label(bearer, "Connected", "LE.Connected");
 		print_property_with_label(bearer, "Connectable",
 							"LE.Connectable");
+		print_property_with_label(bearer, "Role", "LE.Role");
 	}
 
 	return bt_shell_noninteractive_quit(EXIT_SUCCESS);
-- 
2.54.0 (Apple Git-157)


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH BlueZ v3 4/5] doc: Add functional-bearer documentation
  2026-10-09 18:04 [PATCH BlueZ v3 0/5] Add Role property to org.bluez.Bearer.LE1 Kevin Dewald
                   ` (2 preceding siblings ...)
  2026-10-09 18:04 ` [PATCH BlueZ v3 3/5] client: Print LE.Role in info Kevin Dewald
@ 2026-10-09 18:04 ` Kevin Dewald
  2026-10-09 18:04 ` [PATCH BlueZ v3 5/5] test: functional: add bearer tests Kevin Dewald
  2026-10-09 20:50 ` [PATCH BlueZ v3 0/5] Add Role property to org.bluez.Bearer.LE1 patchwork-bot+bluetooth
  5 siblings, 0 replies; 8+ messages in thread
From: Kevin Dewald @ 2026-10-09 18:04 UTC (permalink / raw)
  To: linux-bluetooth; +Cc: Luiz Augusto von Dentz, Kevin Dewald

Document the bearer functional tests, where two hosts connect or pair
over LE or BR/EDR and each checks the org.bluez.Bearer.LE1 and
org.bluez.Bearer.BREDR1 properties of the device object it has for the
other:

- Connected and Role on both sides of an LE connection, and the
  Disconnected signal with its reason on both sides
- Connectable for a connectable advertiser and for a broadcaster
- Paired, Bonded and Connected after LE and BR/EDR pairing, Role being
  only reported for LE

Assisted-by: Claude:claude-opus-5.5
---
 Makefile.am                |   1 +
 doc/functional-bearer.rst  | 141 +++++++++++++++++++++++++++++++++++++
 doc/functional-testing.rst |   1 +
 3 files changed, 143 insertions(+)
 create mode 100644 doc/functional-bearer.rst

diff --git a/Makefile.am b/Makefile.am
index 71f93d2f1..f742458d1 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -499,6 +499,7 @@ EXTRA_DIST += doc/assigned-numbers.rst doc/supported-features.txt \
 				doc/functional-a2dp.rst \
 				doc/functional-avrcp.rst \
 				doc/functional-bap.rst \
+				doc/functional-bearer.rst \
 				doc/functional-hog.rst \
 				doc/functional-mpris-proxy.rst \
 				doc/functional-obex.rst \
diff --git a/doc/functional-bearer.rst b/doc/functional-bearer.rst
new file mode 100644
index 000000000..79fed9db3
--- /dev/null
+++ b/doc/functional-bearer.rst
@@ -0,0 +1,141 @@
+=================
+functional-bearer
+=================
+
+DESCRIPTION
+===========
+
+Bearer functional tests, `test/functional/test_bearer.py`, covering the
+**org.bluez.Bearer.LE(5)** and **org.bluez.Bearer.BREDR(5)** interfaces
+of the device objects. See **functional-testing(7)** for the conventions
+used here, and **test-functional(1)** for how to run the suite.
+
+SETUP
+=====
+
+Two hosts, both running **bluetoothd(8)** with ``Experimental = true``,
+as the bearer interfaces are experimental, and with the MIDI plugin
+disabled (``-P midi``), as the MIDI profile would ask to pair as soon as
+it finds the MIDI service the other host exports:
+
+.. code-block::
+
+	+------------------------+                 +------------------------+
+	| host0                  |   LE or BR/EDR  | host1                  |
+	| bluetoothd, agent      | --------------> | bluetoothd, agent      |
+	| connects or pairs      |                 | advertises (LE) or is  |
+	|                        |                 | discoverable (BR/EDR)  |
+	+------------------------+                 +------------------------+
+
+	--> connection is initiated by
+
+The LE tests run **bluetoothd(8)** with ``ControllerMode = le`` and have
+host1 register a connectable advertisement, as bluetoothd does not
+advertise on its own in that mode. Both hosts register an agent with
+the ``NoInputNoOutput`` capability, so pairing is Just Works.
+
+Each host reads the bearer properties of the device object it has for
+the other host, and records the ``Disconnected`` signals of the bearer
+interfaces.
+
+TEST CASES
+==========
+
+test_bearer_le_connect
+----------------------
+
+:Setup: As above, over LE.
+
+:Steps:
+	1. host0 calls ``org.bluez.Adapter1.StartDiscovery`` and waits for a
+	   device object for host1.
+	2. host0 calls ``org.bluez.Bearer.LE1.Connect`` on it.
+	3. Both hosts read ``Connected`` and ``Role`` on the LE bearer.
+	4. host0 calls ``org.bluez.Bearer.LE1.Disconnect``.
+
+:Expected:
+	1. host1 is discovered.
+	2. ``Connect`` replies.
+	3. ``Connected`` is true on both hosts. ``Role`` is ``peripheral`` on
+	   host0, which initiated the connection, and ``central`` on host1.
+	4. ``Disconnect`` replies, and the LE bearer emits ``Disconnected``
+	   with ``org.bluez.Reason.Local`` on host0 and
+	   ``org.bluez.Reason.Remote`` on host1. ``Connected`` is false on
+	   both hosts and ``Role`` is no longer present.
+
+test_bearer_le_connectable
+--------------------------
+
+:Setup: As above, over LE.
+
+:Steps: host0 discovers host1, as in test_bearer_le_connect, and reads
+	``Connectable`` on the LE bearer.
+
+:Expected: ``Connectable`` is true, as host1 advertises connectable.
+
+test_bearer_le_not_connectable
+------------------------------
+
+:Setup: As above, over LE, except host1 registers an advertisement of
+	type ``broadcast`` that is not discoverable, i.e. it advertises as a
+	broadcaster, with the service UUID
+	``6e2c1d84-7a3b-4f50-9c1e-5b8d2a7f0e13``, and runs no agent. host0
+	runs **bluetoothd(8)** with ``FilterDiscoverable = false``, as
+	otherwise it only creates device objects for discoverable
+	advertisers.
+
+:Steps:
+	1. host0 calls ``org.bluez.Adapter1.StartDiscovery`` and waits for a
+	   device object advertising the service UUID.
+	2. host0 reads ``Connectable`` on its LE bearer.
+
+:Expected:
+	1. The device is discovered.
+	2. ``Connectable`` is false.
+
+:Notes: The kernel sends non-connectable advertising from a
+	non-resolvable private address, so the device object has that
+	address rather than host1's, and host0 finds it by the advertised
+	service UUID instead.
+
+test_bearer_le_pair
+-------------------
+
+:Setup: As above, over LE.
+
+:Steps:
+	1. host0 pairs with host1, which authorizes the pairing.
+	2. Both hosts read ``Paired``, ``Bonded`` and ``Connected`` on the LE
+	   bearer, and ``Paired`` on the BR/EDR bearer.
+
+:Expected:
+	1. ``org.bluez.Device1.Pair`` replies.
+	2. ``Paired``, ``Bonded`` and ``Connected`` are true on the LE bearer
+	   of both hosts. The BR/EDR bearer is not present, as bluetoothd
+	   runs with ``ControllerMode = le``.
+
+test_bearer_bredr_pair
+----------------------
+
+:Setup: As above, over BR/EDR.
+
+:Steps:
+	1. host0 pairs with host1 as in test_agent_pair_bredr[accept], see
+	   **functional-testing(7)**.
+	2. Both hosts read ``Paired``, ``Bonded``, ``Connected``,
+	   ``Connectable`` and ``Role`` on the BR/EDR bearer.
+	3. host0 calls ``org.bluez.Bearer.BREDR1.Disconnect``.
+
+:Expected:
+	1. Both agents confirm the same passkey and ``Pair`` replies.
+	2. ``Paired``, ``Bonded``, ``Connected`` and ``Connectable`` are true
+	   on both hosts, and ``Role`` is not present, as it is only
+	   reported for LE.
+	3. ``Disconnect`` replies, and the BR/EDR bearer emits
+	   ``Disconnected`` with ``org.bluez.Reason.Local`` on host0 and
+	   ``org.bluez.Reason.Remote`` on host1. ``Connected`` is false on
+	   both hosts.
+
+:Notes: The ACL link is terminated a short time after the profiles are
+	disconnected, so the ``Disconnected`` signal follows the reply to
+	``Disconnect``.
diff --git a/doc/functional-testing.rst b/doc/functional-testing.rst
index 7b3cad1c6..371258261 100644
--- a/doc/functional-testing.rst
+++ b/doc/functional-testing.rst
@@ -15,6 +15,7 @@ are documented separately:
 - **functional-a2dp(7)**: `test/functional/test_a2dp.py`
 - **functional-avrcp(7)**: `test/functional/test_avrcp.py`
 - **functional-bap(7)**: `test/functional/test_bap.py`
+- **functional-bearer(7)**: `test/functional/test_bearer.py`
 - **functional-hog(7)**: `test/functional/test_hog.py`
 - **functional-mpris-proxy(7)**: `test/functional/test_mpris_proxy.py`
 - **functional-obex(7)**: `test/functional/test_obex.py`
-- 
2.54.0 (Apple Git-157)


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH BlueZ v3 5/5] test: functional: add bearer tests
  2026-10-09 18:04 [PATCH BlueZ v3 0/5] Add Role property to org.bluez.Bearer.LE1 Kevin Dewald
                   ` (3 preceding siblings ...)
  2026-10-09 18:04 ` [PATCH BlueZ v3 4/5] doc: Add functional-bearer documentation Kevin Dewald
@ 2026-10-09 18:04 ` Kevin Dewald
  2026-10-09 20:50 ` [PATCH BlueZ v3 0/5] Add Role property to org.bluez.Bearer.LE1 patchwork-bot+bluetooth
  5 siblings, 0 replies; 8+ messages in thread
From: Kevin Dewald @ 2026-10-09 18:04 UTC (permalink / raw)
  To: linux-bluetooth; +Cc: Luiz Augusto von Dentz, Kevin Dewald

Add tests where host0 connects to or pairs with host1 over LE or
BR/EDR, and both hosts check the properties and the Disconnected
signal of the bearer interfaces of the device object they have for the
other, including the LE Role of each side of the connection.

See doc/functional-bearer.rst for details.

Assisted-by: Claude:claude-opus-5.5
---
 test/functional/test_bearer.py | 302 +++++++++++++++++++++++++++++++++
 1 file changed, 302 insertions(+)
 create mode 100644 test/functional/test_bearer.py

diff --git a/test/functional/test_bearer.py b/test/functional/test_bearer.py
new file mode 100644
index 000000000..9d2f3346d
--- /dev/null
+++ b/test/functional/test_bearer.py
@@ -0,0 +1,302 @@
+# -*- coding: utf-8; mode: python; eval: (blacken-mode); -*-
+# SPDX-License-Identifier: GPL-2.0-or-later
+"""
+Tests for the org.bluez.Bearer.LE1 and org.bluez.Bearer.BREDR1 interfaces
+
+host0 connects to or pairs with host1, and each host checks the bearer
+interfaces of the device object it has for the other.
+"""
+
+import logging
+
+import dbus
+import pytest
+
+from pytest_bluezenv import (
+    Agent,
+    Bluetoothd,
+    EventPluginMixin,
+    HostPlugin,
+    get_dbus,
+    host_config,
+    mainloop_wrap,
+    wait_until,
+)
+
+from .le_utils import LeAdvertiser, pair_le
+from .test_agent import test_agent_pair_bredr as pair_bredr
+
+pytestmark = [pytest.mark.vm]
+
+BUS_NAME = "org.bluez"
+PROPS_INTERFACE = "org.freedesktop.DBus.Properties"
+DEVICE_INTERFACE = "org.bluez.Device1"
+LE_INTERFACE = "org.bluez.Bearer.LE1"
+BREDR_INTERFACE = "org.bluez.Bearer.BREDR1"
+
+# The bearer interfaces are experimental
+CONF = "[General]\nExperimental = true\n"
+LE_CONF = CONF + "ControllerMode = le\n"
+
+# bluetoothd only creates device objects for advertisers that are
+# discoverable, which a broadcaster is not
+SCANNER_CONF = LE_CONF + "FilterDiscoverable = false\n"
+
+# Identifies the broadcast advertisement of host1 on host0, as it is sent
+# from a non-resolvable private address rather than from host1's address
+BROADCAST_UUID = "6e2c1d84-7a3b-4f50-9c1e-5b8d2a7f0e13"
+
+# The MIDI profile asks to pair as soon as it finds the MIDI service the
+# other host exports, which would need an agent reply in every test
+ARGS = ("-P", "midi")
+
+
+class Bearers(HostPlugin, EventPluginMixin):
+    """
+    Host plugin calling the bearer interfaces of remote devices, reading
+    their properties and recording their Disconnected signals.
+    """
+
+    name = "bearers"
+    depends = [Bluetoothd()]
+
+    @mainloop_wrap
+    def setup(self, impl):
+        EventPluginMixin.setup(self, impl)
+
+        self.log = logging.getLogger(self.name)
+        self.bus = get_dbus(private=True)
+        self.disconnected = []
+
+        self.bus.add_signal_receiver(
+            self._disconnected,
+            signal_name="Disconnected",
+            bus_name=BUS_NAME,
+            path_keyword="path",
+            interface_keyword="interface",
+        )
+
+    def _disconnected(self, reason, message, path=None, interface=None):
+        if interface not in (LE_INTERFACE, BREDR_INTERFACE):
+            return
+
+        self.log.info(f"{path} {interface}.Disconnected: {reason}")
+        self.disconnected.append((str(path), str(interface), str(reason)))
+
+    def _devices(self):
+        manager = dbus.Interface(
+            self.bus.get_object(BUS_NAME, "/"), "org.freedesktop.DBus.ObjectManager"
+        )
+
+        for path, ifaces in manager.GetManagedObjects().items():
+            device = ifaces.get(DEVICE_INTERFACE)
+            if device is not None:
+                yield str(path), device
+
+    def _device_path(self, address):
+        for path, device in self._devices():
+            if device["Address"].lower() == address.lower():
+                return path
+
+        return None
+
+    @mainloop_wrap
+    def find_address(self, uuid):
+        """
+        Return the address of the device advertising the given service
+        UUID, or None if there is none.
+        """
+        for path, device in self._devices():
+            if uuid in [str(u).lower() for u in device.get("UUIDs", [])]:
+                return str(device["Address"])
+
+        return None
+
+    @mainloop_wrap
+    def get(self, address, interface, name):
+        """
+        Return a bearer property of the device with the given address, or
+        None if the device, the bearer or the property is not there.
+        """
+        path = self._device_path(address)
+        if path is None:
+            return None
+
+        props = dbus.Interface(self.bus.get_object(BUS_NAME, path), PROPS_INTERFACE)
+        try:
+            value = props.Get(interface, name)
+        except dbus.exceptions.DBusException:
+            return None
+
+        if isinstance(value, dbus.Boolean):
+            return bool(value)
+
+        return str(value)
+
+    @mainloop_wrap
+    def call(self, address, interface, method):
+        """
+        Call a bearer method of the device with the given address.
+
+        Events:
+            Event(kind="{interface}.{method}:reply")
+        """
+        path = self._device_path(address)
+        bearer = dbus.Interface(self.bus.get_object(BUS_NAME, path), interface)
+        self._object_method(bearer, method)
+
+    @mainloop_wrap
+    def disconnect_reasons(self, address, interface):
+        """
+        Return the reasons of the Disconnected signals received on the
+        given bearer of the device with the given address, in order.
+        """
+        path = f"/org/bluez/hci0/dev_{address.upper().replace(':', '_')}"
+        return [r for p, i, r in self.disconnected if p == path and i == interface]
+
+
+le_config = host_config(
+    [
+        Bluetoothd(conf=LE_CONF, args=ARGS),
+        Agent(capability="NoInputNoOutput"),
+        Bearers(),
+    ],
+    [
+        Bluetoothd(conf=LE_CONF, args=ARGS),
+        LeAdvertiser(),
+        Agent(capability="NoInputNoOutput"),
+        Bearers(),
+    ],
+)
+
+bredr_config = host_config(
+    [Bluetoothd(conf=CONF, args=ARGS), Agent(), Bearers()],
+    [Bluetoothd(conf=CONF, args=ARGS), Agent(), Bearers()],
+)
+
+
+def start_discovery(host):
+    host.agent.adapter_method("StartDiscovery")
+    host.agent.expect("org.bluez.Adapter1.StartDiscovery:reply")
+
+
+def discover(host, remote):
+    start_discovery(host)
+    wait_until(host.agent.has_device, remote.bdaddr)
+
+
+def peers(host0, host1):
+    """Both hosts, each with the other"""
+    return ((host0, host1), (host1, host0))
+
+
+def disconnect(host0, host1, interface):
+    """
+    Disconnect the bearer from host0, and check it is reported as
+    disconnected on both hosts with the reason seen from each side.
+    """
+    host0.bearers.call(host1.bdaddr, interface, "Disconnect")
+    host0.bearers.expect(f"{interface}.Disconnect:reply")
+
+    for host, remote, reason in (
+        (host0, host1, "org.bluez.Reason.Local"),
+        (host1, host0, "org.bluez.Reason.Remote"),
+    ):
+        wait_until(
+            lambda: host.bearers.disconnect_reasons(remote.bdaddr, interface)
+            == [reason]
+        )
+        assert host.bearers.get(remote.bdaddr, interface, "Connected") is False
+
+
+@le_config
+def test_bearer_le_connect(hosts):
+    host0, host1 = hosts
+
+    discover(host0, host1)
+
+    host0.bearers.call(host1.bdaddr, LE_INTERFACE, "Connect")
+    host0.bearers.expect(f"{LE_INTERFACE}.Connect:reply")
+
+    for host, remote in peers(host0, host1):
+        wait_until(
+            lambda: host.bearers.get(remote.bdaddr, LE_INTERFACE, "Connected") is True
+        )
+
+    assert host0.bearers.get(host1.bdaddr, LE_INTERFACE, "Role") == "peripheral"
+    assert host1.bearers.get(host0.bdaddr, LE_INTERFACE, "Role") == "central"
+
+    disconnect(host0, host1, LE_INTERFACE)
+
+    for host, remote in peers(host0, host1):
+        assert host.bearers.get(remote.bdaddr, LE_INTERFACE, "Role") is None
+
+
+@host_config(
+    [Bluetoothd(conf=SCANNER_CONF, args=ARGS), Agent(), Bearers()],
+    # A broadcaster, so neither connectable nor discoverable
+    [
+        Bluetoothd(conf=LE_CONF, args=ARGS),
+        LeAdvertiser(
+            adv_type="broadcast",
+            service_uuids=[BROADCAST_UUID],
+            discoverable=False,
+        ),
+    ],
+)
+def test_bearer_le_not_connectable(hosts):
+    host0, host1 = hosts
+
+    start_discovery(host0)
+    wait_until(lambda: host0.bearers.find_address(BROADCAST_UUID) is not None)
+    address = host0.bearers.find_address(BROADCAST_UUID)
+
+    wait_until(
+        lambda: host0.bearers.get(address, LE_INTERFACE, "Connectable") is not None
+    )
+    assert host0.bearers.get(address, LE_INTERFACE, "Connectable") is False
+
+
+@le_config
+def test_bearer_le_connectable(hosts):
+    host0, host1 = hosts
+
+    discover(host0, host1)
+
+    wait_until(
+        lambda: host0.bearers.get(host1.bdaddr, LE_INTERFACE, "Connectable") is not None
+    )
+    assert host0.bearers.get(host1.bdaddr, LE_INTERFACE, "Connectable") is True
+
+
+@le_config
+def test_bearer_le_pair(hosts):
+    host0, host1 = hosts
+
+    pair_le(host0, host1)
+
+    for host, remote in peers(host0, host1):
+        for name in ("Paired", "Bonded", "Connected"):
+            wait_until(
+                lambda: host.bearers.get(remote.bdaddr, LE_INTERFACE, name) is True
+            )
+
+        assert host.bearers.get(remote.bdaddr, BREDR_INTERFACE, "Paired") is None
+
+
+@bredr_config
+def test_bearer_bredr_pair(hosts):
+    host0, host1 = hosts
+
+    pair_bredr(hosts, True)
+
+    for host, remote in peers(host0, host1):
+        for name in ("Paired", "Bonded", "Connected", "Connectable"):
+            wait_until(
+                lambda: host.bearers.get(remote.bdaddr, BREDR_INTERFACE, name) is True
+            )
+
+        # Role is only reported for LE
+        assert host.bearers.get(remote.bdaddr, BREDR_INTERFACE, "Role") is None
+
+    disconnect(host0, host1, BREDR_INTERFACE)
-- 
2.54.0 (Apple Git-157)


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* Re: [PATCH BlueZ v3 0/5] Add Role property to org.bluez.Bearer.LE1
  2026-10-09 18:04 [PATCH BlueZ v3 0/5] Add Role property to org.bluez.Bearer.LE1 Kevin Dewald
                   ` (4 preceding siblings ...)
  2026-10-09 18:04 ` [PATCH BlueZ v3 5/5] test: functional: add bearer tests Kevin Dewald
@ 2026-10-09 20:50 ` patchwork-bot+bluetooth
  5 siblings, 0 replies; 8+ messages in thread
From: patchwork-bot+bluetooth @ 2026-10-09 20:50 UTC (permalink / raw)
  To: Kevin Dewald; +Cc: linux-bluetooth, luiz.dentz

Hello:

This series was applied to bluetooth/bluez.git (master)
by Luiz Augusto von Dentz <luiz.von.dentz@intel.com>:

On Fri,  9 Oct 2026 11:04:43 -0700 you wrote:
> Applications acting as a peripheral, or in both roles, cannot tell
> whether a connected device is a central that connected to the adapter
> or a peripheral the adapter connected to. When several applications
> share the adapter, a device object can appear because of any of them,
> so the order of events does not answer it either.
> 
> bluetoothd already tracks this per bearer from the Device Connected
> event. This series exposes it as a Role property on
> org.bluez.Bearer.LE1, "central" or "peripheral", present only while
> the device is connected over LE, and prints it as LE.Role in
> bluetoothctl info. The documentation states that it is the link layer
> role and says nothing about GATT client and server roles.
> 
> [...]

Here is the summary with links:
  - [BlueZ,v3,1/5] org.bluez.Bearer: Add Role property
    https://git.kernel.org/pub/scm/bluetooth/bluez.git/?id=c31e9091e646
  - [BlueZ,v3,2/5] bearer: Add Role property
    https://git.kernel.org/pub/scm/bluetooth/bluez.git/?id=76890c8fd325
  - [BlueZ,v3,3/5] client: Print LE.Role in info
    https://git.kernel.org/pub/scm/bluetooth/bluez.git/?id=2515706490c2
  - [BlueZ,v3,4/5] doc: Add functional-bearer documentation
    https://git.kernel.org/pub/scm/bluetooth/bluez.git/?id=926bbc5dfe29
  - [BlueZ,v3,5/5] test: functional: add bearer tests
    https://git.kernel.org/pub/scm/bluetooth/bluez.git/?id=ce448a8a7a3b

You are awesome, thank you!
-- 
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html



^ permalink raw reply	[flat|nested] 8+ messages in thread

* RE: Add Role property to org.bluez.Bearer.LE1
  2026-10-09 18:04 ` [PATCH BlueZ v3 1/5] org.bluez.Bearer: Add Role property Kevin Dewald
@ 2026-10-09 21:01   ` bluez.test.bot
  0 siblings, 0 replies; 8+ messages in thread
From: bluez.test.bot @ 2026-10-09 21:01 UTC (permalink / raw)
  To: linux-bluetooth, kevin

[-- Attachment #1: Type: text/plain, Size: 1022 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/1182613/

---Test result---

Test Summary:
CheckPatch                    PASS      2.34 seconds
GitLint                       PASS      1.64 seconds
BuildEll                      PASS      20.03 seconds
BluezMake                     PASS      388.62 seconds
MakeCheck                     PASS      14.38 seconds
MakeDistcheck                 PASS      145.70 seconds
CheckValgrind                 PASS      234.83 seconds
CheckSmatch                   PASS      296.08 seconds
bluezmakeextell               PASS      95.20 seconds
TestFunctional                PASS      1219.26 seconds
IncrementalBuild              PASS      440.58 seconds
ScanBuild                     PASS      1152.57 seconds



https://github.com/bluez/bluez/pull/2645

---
Regards,
Linux Bluetooth


^ permalink raw reply	[flat|nested] 8+ messages in thread

end of thread, other threads:[~2026-10-09 21:01 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-09 18:04 [PATCH BlueZ v3 0/5] Add Role property to org.bluez.Bearer.LE1 Kevin Dewald
2026-10-09 18:04 ` [PATCH BlueZ v3 1/5] org.bluez.Bearer: Add Role property Kevin Dewald
2026-10-09 21:01   ` Add Role property to org.bluez.Bearer.LE1 bluez.test.bot
2026-10-09 18:04 ` [PATCH BlueZ v3 2/5] bearer: Add Role property Kevin Dewald
2026-10-09 18:04 ` [PATCH BlueZ v3 3/5] client: Print LE.Role in info Kevin Dewald
2026-10-09 18:04 ` [PATCH BlueZ v3 4/5] doc: Add functional-bearer documentation Kevin Dewald
2026-10-09 18:04 ` [PATCH BlueZ v3 5/5] test: functional: add bearer tests Kevin Dewald
2026-10-09 20:50 ` [PATCH BlueZ v3 0/5] Add Role property to org.bluez.Bearer.LE1 patchwork-bot+bluetooth

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox