* [PATCH BlueZ 01/15] gatt-database: Drop stale device state for removed bonds
2026-11-02 8:18 [PATCH BlueZ 00/15] Channel Sounding and RAP fixes Cheng Jiang
@ 2026-11-02 8:18 ` Cheng Jiang
2026-11-02 9:08 ` Channel Sounding and RAP fixes bluez.test.bot
2026-11-02 8:18 ` [PATCH BlueZ 02/15] shared/rap: Reset the readable Ranging Counters per connection Cheng Jiang
` (14 subsequent siblings)
15 siblings, 1 reply; 18+ messages in thread
From: Cheng Jiang @ 2026-11-02 8:18 UTC (permalink / raw)
To: linux-bluetooth
Cc: jinwang.li, wei.deng, xiuzhuo.shang, shuai.zhang, Cheng Jiang
When a device is removed while disconnected, its cached CCC state
stays in the GATT database and is restored on the next connection.
Drop that state from device_remove() so a re-paired device starts
clean.
Assisted-by: Claude:claude-haiku-5-5
---
src/device.c | 3 +++
src/gatt-database.c | 25 +++++++++++++++++++++++++
src/gatt-database.h | 2 ++
3 files changed, 30 insertions(+)
diff --git a/src/device.c b/src/device.c
index 4567188d6..6179c4777 100644
--- a/src/device.c
+++ b/src/device.c
@@ -5629,6 +5629,9 @@ void device_remove(struct btd_device *device, gboolean remove_stored)
{
DBG("Removing device %s", device->path);
+ btd_gatt_database_remove_device(
+ btd_adapter_get_database(device->adapter), device);
+
if (device->auth_retry_id) {
device->auth_retry_id = 0;
timeout_remove(device->auth_retry_id);
diff --git a/src/gatt-database.c b/src/gatt-database.c
index d8e45581a..a7480a7f8 100644
--- a/src/gatt-database.c
+++ b/src/gatt-database.c
@@ -4251,6 +4251,31 @@ void btd_gatt_database_att_disconnected(struct btd_gatt_database *database,
att_disconnected(0, state);
}
+void btd_gatt_database_remove_device(struct btd_gatt_database *database,
+ struct btd_device *device)
+{
+ struct device_state *state;
+ const bdaddr_t *addr;
+ uint8_t type;
+
+ addr = device_get_address(device);
+ type = btd_device_get_bdaddr_type(device);
+
+ state = find_device_state(database, addr, type);
+ if (!state)
+ return;
+
+ /* While connected, gatt_server_cleanup() still reaches the state
+ * through the disconnect callback, so leave it alone here.
+ */
+ if (state->disc_id)
+ return;
+
+ queue_foreach(state->ccc_states, clear_ccc_state, state->db);
+ queue_remove(database->device_states, state);
+ device_state_free(state);
+}
+
static void restore_ccc(struct btd_gatt_database *database,
const bdaddr_t *addr, uint8_t addr_type, uint16_t value)
{
diff --git a/src/gatt-database.h b/src/gatt-database.h
index 998e3c452..94469a707 100644
--- a/src/gatt-database.h
+++ b/src/gatt-database.h
@@ -20,6 +20,8 @@ btd_gatt_database_get_adapter(struct btd_gatt_database *database);
void btd_gatt_database_att_disconnected(struct btd_gatt_database *database,
struct btd_device *device);
+void btd_gatt_database_remove_device(struct btd_gatt_database *database,
+ struct btd_device *device);
void btd_gatt_database_server_connected(struct btd_gatt_database *database,
struct bt_gatt_server *server);
--
2.43.0
^ permalink raw reply related [flat|nested] 18+ messages in thread* RE: Channel Sounding and RAP fixes
2026-11-02 8:18 ` [PATCH BlueZ 01/15] gatt-database: Drop stale device state for removed bonds Cheng Jiang
@ 2026-11-02 9:08 ` bluez.test.bot
0 siblings, 0 replies; 18+ messages in thread
From: bluez.test.bot @ 2026-11-02 9:08 UTC (permalink / raw)
To: linux-bluetooth, cheng.jiang
[-- Attachment #1: Type: text/plain, Size: 1736 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/1198568/
---Test result---
Test Summary:
CheckPatch PASS 7.35 seconds
GitLint FAIL 4.51 seconds
BuildEll PASS 13.67 seconds
BluezMake PASS 179.90 seconds
MakeCheck PASS 13.01 seconds
MakeDistcheck PASS 99.32 seconds
CheckValgrind PASS 157.65 seconds
CheckSmatch PASS 179.66 seconds
bluezmakeextell PASS 63.53 seconds
TestFunctional PASS 845.97 seconds
IncrementalBuild PASS 317.45 seconds
ScanBuild PASS 637.91 seconds
Details
##############################
Test: GitLint - FAIL
Desc: Run gitlint
Output:
[BlueZ,08/15] profiles/ranging: Let StartMeasurement() pick the Initiator for Both
1: T1 Title exceeds max length (82>80): "[BlueZ,08/15] profiles/ranging: Let StartMeasurement() pick the Initiator for Both"
[BlueZ,12/15] profiles/ranging: Defer CS Security Enable until the link is encrypted
1: T1 Title exceeds max length (84>80): "[BlueZ,12/15] profiles/ranging: Defer CS Security Enable until the link is encrypted"
[BlueZ,13/15] profiles/ranging: Choose the CS antenna configuration from both ends
1: T1 Title exceeds max length (82>80): "[BlueZ,13/15] profiles/ranging: Choose the CS antenna configuration from both ends"
https://github.com/bluez/bluez/pull/2747
---
Regards,
Linux Bluetooth
^ permalink raw reply [flat|nested] 18+ messages in thread
* [PATCH BlueZ 02/15] shared/rap: Reset the readable Ranging Counters per connection
2026-11-02 8:18 [PATCH BlueZ 00/15] Channel Sounding and RAP fixes Cheng Jiang
2026-11-02 8:18 ` [PATCH BlueZ 01/15] gatt-database: Drop stale device state for removed bonds Cheng Jiang
@ 2026-11-02 8:18 ` Cheng Jiang
2026-11-02 8:19 ` [PATCH BlueZ 03/15] shared/rap: Subscribe on-demand data with the requested transport Cheng Jiang
` (13 subsequent siblings)
15 siblings, 0 replies; 18+ messages in thread
From: Cheng Jiang @ 2026-11-02 8:18 UTC (permalink / raw)
To: linux-bluetooth
Cc: jinwang.li, wei.deng, xiuzhuo.shang, shuai.zhang, Cheng Jiang
A read of Ranging Data Ready reports the Ranging Counter of the most
recent CS procedure completed during the connection, and 0 if none
completed; Ranging Data Overwritten reports the counter of the most
recent procedure whose data was overwritten during the connection, and 0
if none was (RAS v1.0, 3.4 and 3.5). Both are scoped to the connection.
Neither counter was cleared when the link went away, and the local RAS
service outlives it, so the value counted on one connection was still
reported on the next. After any run that produced ranging data, a read
on a fresh connection returned a stale non-zero counter, which is what
RAS/SR/RCO/BV-07-C checks in its first step.
Clear both on the local RAS service in bt_rap_detach(), next to the
other per-connection state it already drops.
Assisted-by: Claude:claude-opus-5-5
---
src/shared/rap.c | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/src/shared/rap.c b/src/shared/rap.c
index ebf12a6bb..e0aa965e5 100644
--- a/src/shared/rap.c
+++ b/src/shared/rap.c
@@ -1018,6 +1018,17 @@ void bt_rap_detach(struct bt_rap *rap)
*/
ras_ondemand_cache_clear(rap);
+ /* Reads of Ranging Data Ready and Ranging Data Overwritten report the
+ * Ranging Counter of the most recent CS procedure completed,
+ * respectively overwritten, *during the connection*, and 0 when there
+ * was none (RAS v1.0, 3.4 and 3.5). Neither counter may survive the
+ * link it was counted on.
+ */
+ if (rap->lrapdb && rap->lrapdb->ras) {
+ rap->lrapdb->ras->ready_counter = 0;
+ rap->lrapdb->ras->overwritten_counter = 0;
+ }
+
if (rap->rrapdb && rap->rrapdb->ras) {
struct ras *ras = rap->rrapdb->ras;
--
2.43.0
^ permalink raw reply related [flat|nested] 18+ messages in thread* [PATCH BlueZ 03/15] shared/rap: Subscribe on-demand data with the requested transport
2026-11-02 8:18 [PATCH BlueZ 00/15] Channel Sounding and RAP fixes Cheng Jiang
2026-11-02 8:18 ` [PATCH BlueZ 01/15] gatt-database: Drop stale device state for removed bonds Cheng Jiang
2026-11-02 8:18 ` [PATCH BlueZ 02/15] shared/rap: Reset the readable Ranging Counters per connection Cheng Jiang
@ 2026-11-02 8:19 ` Cheng Jiang
2026-11-02 8:19 ` [PATCH BlueZ 04/15] shared/rap: Subscribe to Ranging Data when a measurement starts Cheng Jiang
` (12 subsequent siblings)
15 siblings, 0 replies; 18+ messages in thread
From: Cheng Jiang @ 2026-11-02 8:19 UTC (permalink / raw)
To: linux-bluetooth
Cc: jinwang.li, wei.deng, xiuzhuo.shang, shuai.zhang, Cheng Jiang
The On-demand Ranging Data characteristic was always subscribed with
notifications, so an indication request from the Upper Tester was
ignored for the data characteristic, while the RAS Control Point,
Ranging Data Ready and Ranging Data Overwritten followed it.
RAS v1.0 Table 3.1 has it the other way around: On-demand Ranging Data
must support both notifications and indications, the RAS Control Point
supports only indications, and notifications are optional for Ranging
Data Ready and Ranging Data Overwritten. With notifications selected
the Control Point subscription was rejected, so no On-demand transfer
could be requested.
Use the requested transport for On-demand Ranging Data, always
subscribe to Control Point indications, and subscribe Ready and
Overwritten with indications when the server supports them, falling
back to notifications only when it does not. A server may send Ready
only as indications: PTS does so in RAP/REQ/ORD/BV-01-C.
Assisted-by: Claude:claude-opus-5-5
---
doc/org.bluez.ChannelSounding1.rst | 5 +++--
src/shared/rap.c | 32 +++++++++++++++++++++++-------
2 files changed, 28 insertions(+), 9 deletions(-)
diff --git a/doc/org.bluez.ChannelSounding1.rst b/doc/org.bluez.ChannelSounding1.rst
index 32c83e36f..e75535b97 100644
--- a/doc/org.bluez.ChannelSounding1.rst
+++ b/doc/org.bluez.ChannelSounding1.rst
@@ -275,8 +275,9 @@ CCCD subscriptions, then enables the selected mode.
``transport`` selects the CCCD value for Real-time or On-demand mode. It is
ignored for Disabled mode. Use ``0x01`` for notifications or ``0x02`` for
-indications. For On-demand mode, this applies to Control Point, Ready, and
-Overwritten; segmented On-demand Data always uses notifications.
+indications. For On-demand mode, this applies to On-demand Data. Control
+Point always uses indications, and Ready and Overwritten use indications
+if the peer supports them, notifications otherwise.
Raises ``org.bluez.Error.Failed`` when remote RAS discovery and Features read
have not completed, or the selected mode is unsupported by the peer.
diff --git a/src/shared/rap.c b/src/shared/rap.c
index e0aa965e5..f275f5835 100644
--- a/src/shared/rap.c
+++ b/src/shared/rap.c
@@ -4556,6 +4556,21 @@ static void ras_cp_response_notify_cb(struct bt_rap *rap,
}
}
+/* Indicate Ranging Data Ready and Ranging Data Overwritten when the server
+ * supports it; otherwise let the client fall back to notifications.
+ */
+static uint16_t rap_status_ccc(struct gatt_db_attribute *chrc)
+{
+ uint8_t props;
+
+ if (gatt_db_attribute_get_char_data(chrc, NULL, NULL, &props, NULL,
+ NULL) &&
+ !(props & BT_GATT_CHRC_PROP_INDICATE))
+ return 0;
+
+ return BT_RAP_RANGING_INDICATE;
+}
+
/* Initiator role: subscribe to a single on-demand-related characteristic,
* logging (not failing) on any missing attribute/handle so a
* partially-capable peer doesn't block subscription to the others.
@@ -4637,21 +4652,24 @@ static bool rap_subscribe_ranging_data(struct bt_rap *rap,
if (mode == BT_RAP_RANGING_DATA_ONDEMAND) {
uint16_t value_handle;
- /* Data is segmented and can be large, so keep it on
- * notifications. The control and status characteristics use
- * the requested transport.
+ /* Only the data characteristic follows the requested
+ * transport (RAS v1.0 Table 3.1). The RAS Control Point can
+ * only be indicated, and Ranging Data Ready and Ranging Data
+ * Overwritten are indicated whenever the server supports it,
+ * since a server may send them only as indications.
*/
rap_subscribe_ondemand_chrc(rap, ras->ondemand_chrc,
- BT_RAP_RANGING_NOTIFY, ras_segment_notify_cb,
+ rap->ondemand_ccc_value, ras_segment_notify_cb,
"on-demand", &rap->ondemand_notify_id);
rap_subscribe_ondemand_chrc(rap, ras->cp_chrc,
- rap->ondemand_ccc_value, ras_cp_response_notify_cb,
+ BT_RAP_RANGING_INDICATE, ras_cp_response_notify_cb,
"RAS Control Point", &rap->cp_notify_id);
rap_subscribe_ondemand_chrc(rap, ras->ready_chrc,
- rap->ondemand_ccc_value, ras_data_ready_notify_cb,
+ rap_status_ccc(ras->ready_chrc),
+ ras_data_ready_notify_cb,
"Ranging Data Ready", &rap->ready_notify_id);
rap_subscribe_ondemand_chrc(rap, ras->overwritten_chrc,
- rap->ondemand_ccc_value,
+ rap_status_ccc(ras->overwritten_chrc),
ras_data_overwritten_notify_cb,
"Ranging Data Overwritten",
&rap->overwritten_notify_id);
--
2.43.0
^ permalink raw reply related [flat|nested] 18+ messages in thread* [PATCH BlueZ 04/15] shared/rap: Subscribe to Ranging Data when a measurement starts
2026-11-02 8:18 [PATCH BlueZ 00/15] Channel Sounding and RAP fixes Cheng Jiang
` (2 preceding siblings ...)
2026-11-02 8:19 ` [PATCH BlueZ 03/15] shared/rap: Subscribe on-demand data with the requested transport Cheng Jiang
@ 2026-11-02 8:19 ` Cheng Jiang
2026-11-02 8:19 ` [PATCH BlueZ 05/15] client/cs: Fix use-after-free in stop_measurement_reply Cheng Jiang
` (11 subsequent siblings)
15 siblings, 0 replies; 18+ messages in thread
From: Cheng Jiang @ 2026-11-02 8:19 UTC (permalink / raw)
To: linux-bluetooth
Cc: jinwang.li, wei.deng, xiuzhuo.shang, shuai.zhang, Cheng Jiang
Reading RAS Features subscribed to Real-time Ranging Data whenever
On-demand ranging was not configured. The subscription also arms the
5 s first segment timer, and at connection time there is normally no
CS procedure running yet, so the timer fires and writes 0x0000 back to
the CCCD. The connection-time subscription therefore only adds an
enable and a disable write that no application asked for, and a test
that expects the IUT to enable Real-time Ranging Data only when told
to (RAP/REQ/RRD/BV-01-C) sees both. It also subscribed to nothing when
the peer does not support Real-time Ranging Data, although the RREQ
has to enable On-demand Ranging Data then (RAP v1.0 4.4.2).
Subscribe at connection time only for the configured On-demand mode.
When StartMeasurement() is called and SetRangingDataMode() has not
subscribed to either mode, subscribe to the configured one, using
On-demand if the peer lacks Real-time Ranging Data.
Assisted-by: Claude:claude-opus-5-5
---
profiles/ranging/rap.c | 6 ++++++
src/shared/rap.c | 28 +++++++++++++++++++++++++---
src/shared/rap.h | 5 +++++
3 files changed, 36 insertions(+), 3 deletions(-)
diff --git a/profiles/ranging/rap.c b/profiles/ranging/rap.c
index f1717d932..2d8b1a7a1 100644
--- a/profiles/ranging/rap.c
+++ b/profiles/ranging/rap.c
@@ -843,6 +843,12 @@ bad_type:
if (settings.role == 0x02)
return dbus_message_new_method_return(msg);
+ /* Nothing is subscribed at connection time unless On-demand is
+ * configured, so subscribe now if SetRangingDataMode() was not called.
+ */
+ if (!bt_rap_ensure_ranging_data(data->rap))
+ DBG("Unable to subscribe to Ranging Data");
+
if (!bt_rap_start_measurement(data->hci_sm, data->conn_handle,
duration_secs))
return g_dbus_create_error(msg, DBUS_ERROR_FAILED,
diff --git a/src/shared/rap.c b/src/shared/rap.c
index f275f5835..85cf8111f 100644
--- a/src/shared/rap.c
+++ b/src/shared/rap.c
@@ -4712,6 +4712,28 @@ bool bt_rap_set_ranging_data_mode(struct bt_rap *rap,
return rap_subscribe_ranging_data(rap, mode);
}
+bool bt_rap_ensure_ranging_data(struct bt_rap *rap)
+{
+ enum bt_rap_ranging_data_mode mode;
+
+ if (!rap || !rap->client)
+ return false;
+
+ if (rap->realtime_notify_id || rap->ondemand_notify_id)
+ return true;
+
+ /* The RREQ has to enable one of the two (RAP v1.0 4.4). Real-time
+ * Ranging Data is optional for the RRSP, so fall back to On-demand
+ * when it is not supported (RAP v1.0 4.4.2).
+ */
+ if (rap->ondemand_mode || !(rap->ras_features & 0x01))
+ mode = BT_RAP_RANGING_DATA_ONDEMAND;
+ else
+ mode = BT_RAP_RANGING_DATA_REALTIME;
+
+ return rap_subscribe_ranging_data(rap, mode);
+}
+
static void read_ras_features(struct bt_rap *rap, bool success,
uint8_t att_ecode,
const uint8_t *value, uint16_t length,
@@ -4771,9 +4793,9 @@ static void read_ras_features(struct bt_rap *rap, bool success,
return;
}
- if (!rap_subscribe_ranging_data(rap, rap->ondemand_mode ?
- BT_RAP_RANGING_DATA_ONDEMAND :
- BT_RAP_RANGING_DATA_REALTIME))
+ if (rap->ondemand_mode &&
+ !rap_subscribe_ranging_data(rap,
+ BT_RAP_RANGING_DATA_ONDEMAND))
DBG(rap, "Failed to subscribe to requested Ranging Data mode");
}
diff --git a/src/shared/rap.h b/src/shared/rap.h
index af4fab057..7ee51143c 100644
--- a/src/shared/rap.h
+++ b/src/shared/rap.h
@@ -322,6 +322,11 @@ bool bt_rap_set_ranging_data_mode(struct bt_rap *rap,
enum bt_rap_ranging_data_mode mode,
enum bt_rap_ranging_transport transport);
+/* Subscribe to the configured Ranging Data mode unless one is already
+ * subscribed, falling back to On-demand if the peer lacks Real-time.
+ */
+bool bt_rap_ensure_ranging_data(struct bt_rap *rap);
+
/* Initiator role: stop receiving Real-time Ranging Data. The GATT client
* disables the characteristic CCCD when this is its final subscription.
*/
--
2.43.0
^ permalink raw reply related [flat|nested] 18+ messages in thread* [PATCH BlueZ 05/15] client/cs: Fix use-after-free in stop_measurement_reply
2026-11-02 8:18 [PATCH BlueZ 00/15] Channel Sounding and RAP fixes Cheng Jiang
` (3 preceding siblings ...)
2026-11-02 8:19 ` [PATCH BlueZ 04/15] shared/rap: Subscribe to Ranging Data when a measurement starts Cheng Jiang
@ 2026-11-02 8:19 ` Cheng Jiang
2026-11-02 8:19 ` [PATCH BlueZ 06/15] shared/hci: Look up the connection handle on the right link Cheng Jiang
` (10 subsequent siblings)
15 siblings, 0 replies; 18+ messages in thread
From: Cheng Jiang @ 2026-11-02 8:19 UTC (permalink / raw)
To: linux-bluetooth
Cc: jinwang.li, wei.deng, xiuzhuo.shang, shuai.zhang, Cheng Jiang
The daemon emits the Active=false property change before the
StopMeasurement reply, so cs_measurement_stopped() frees the session
first and the reply callback then read and freed it again.
Pass the proxy as user_data and look the session up again in the reply
callback, returning if it is already gone.
Assisted-by: Claude:claude-sonnet-5-5
---
client/cs.c | 15 ++++++++++++---
1 file changed, 12 insertions(+), 3 deletions(-)
diff --git a/client/cs.c b/client/cs.c
index 4c181e743..e029da881 100644
--- a/client/cs.c
+++ b/client/cs.c
@@ -1369,7 +1369,8 @@ static void cmd_cs_start(int argc, char *argv[])
static void stop_measurement_reply(DBusMessage *message, void *user_data)
{
- struct cs_session *s = user_data;
+ GDBusProxy *proxy = user_data;
+ struct cs_session *s;
DBusError error;
dbus_error_init(&error);
@@ -1380,7 +1381,15 @@ static void stop_measurement_reply(DBusMessage *message, void *user_data)
}
bt_shell_printf("Measurement stopped on %s\n",
- g_dbus_proxy_get_path(s->proxy));
+ g_dbus_proxy_get_path(proxy));
+
+ /* The Active=false property change may already have dropped the
+ * session, since the daemon emits it before this reply.
+ */
+ s = cs_find_session(proxy);
+ if (!s)
+ return;
+
cs_sessions = g_list_remove(cs_sessions, s);
g_free(s);
}
@@ -1425,7 +1434,7 @@ static void cmd_cs_stop(int argc, char *argv[])
if (!g_dbus_proxy_method_call(s->proxy, "StopMeasurement",
NULL,
- stop_measurement_reply, s, NULL))
+ stop_measurement_reply, s->proxy, NULL))
bt_shell_printf("Failed to send StopMeasurement\n");
}
--
2.43.0
^ permalink raw reply related [flat|nested] 18+ messages in thread* [PATCH BlueZ 06/15] shared/hci: Look up the connection handle on the right link
2026-11-02 8:18 [PATCH BlueZ 00/15] Channel Sounding and RAP fixes Cheng Jiang
` (4 preceding siblings ...)
2026-11-02 8:19 ` [PATCH BlueZ 05/15] client/cs: Fix use-after-free in stop_measurement_reply Cheng Jiang
@ 2026-11-02 8:19 ` Cheng Jiang
2026-11-02 8:19 ` [PATCH BlueZ 07/15] profiles/ranging: Let the Reflector run the CS setup phase Cheng Jiang
` (9 subsequent siblings)
15 siblings, 0 replies; 18+ messages in thread
From: Cheng Jiang @ 2026-11-02 8:19 UTC (permalink / raw)
To: linux-bluetooth
Cc: jinwang.li, wei.deng, xiuzhuo.shang, shuai.zhang, Cheng Jiang
bt_hci_get_conn_handle() set dev_id to 0 in its HCIGETCONNLIST request,
expecting the ioctl to fill it in. The kernel reads it instead, to pick
the controller whose connections it lists, so the lookup always went to
hci0 whatever adapter the channel was opened for.
The list also holds every link of that controller, and the first one
with a matching address was taken. With a dual-mode peer connected over
BR/EDR first, that is the BR/EDR ACL, and after a quick reconnection it
can be the old link that is still being torn down.
Remember the index the raw channel was opened for and use it in the
request. Take the link type from the caller and only accept a connected
link of that type, so that the function works for any link type. Add
LE_LINK next to the other link types for that, which also replaces the
local copy in hcitool.
Assisted-by: Claude:claude-opus-5-5
---
lib/bluetooth/hci.h | 1 +
profiles/ranging/rap.c | 3 ++-
src/shared/hci.c | 12 +++++++-----
src/shared/hci.h | 2 +-
tools/hcitool.c | 3 ---
5 files changed, 11 insertions(+), 10 deletions(-)
diff --git a/lib/bluetooth/hci.h b/lib/bluetooth/hci.h
index 2e4aa394e..6f51a2e10 100644
--- a/lib/bluetooth/hci.h
+++ b/lib/bluetooth/hci.h
@@ -210,6 +210,7 @@ enum {
#define SCO_LINK 0x00
#define ACL_LINK 0x01
#define ESCO_LINK 0x02
+#define LE_LINK 0x80
/* LMP features */
#define LMP_3SLOT 0x01
diff --git a/profiles/ranging/rap.c b/profiles/ranging/rap.c
index 2d8b1a7a1..d23c3f8e2 100644
--- a/profiles/ranging/rap.c
+++ b/profiles/ranging/rap.c
@@ -18,6 +18,7 @@
#include "gdbus/gdbus.h"
#include "bluetooth/bluetooth.h"
+#include "bluetooth/hci.h"
#include "bluetooth/l2cap.h"
#include "bluetooth/uuid.h"
@@ -1099,7 +1100,7 @@ static int rap_setup_reflector(struct rap_data *data)
if (att && data->adapter_data->hci) {
if (bt_hci_get_conn_handle(data->adapter_data->hci,
- (const uint8_t *)bdaddr, &handle)) {
+ (const uint8_t *)bdaddr, LE_LINK, &handle)) {
DBG("conn handle 0x%04X for %s", handle, addr);
data->conn_handle = handle;
bt_rap_set_conn_hndl(data->hci_sm,
diff --git a/src/shared/hci.c b/src/shared/hci.c
index e8586ab50..f1d663be1 100644
--- a/src/shared/hci.c
+++ b/src/shared/hci.c
@@ -34,9 +34,9 @@
#include "src/shared/timeout.h"
#include "src/shared/hci.h"
-
struct bt_hci {
int ref_count;
+ uint16_t index;
struct io *io;
bool is_stream;
bool writer_active;
@@ -463,6 +463,7 @@ struct bt_hci *bt_hci_new_raw_device(uint16_t index)
}
hci->is_stream = false;
+ hci->index = index;
bt_hci_set_close_on_unref(hci, true);
@@ -972,7 +973,7 @@ bool bt_hci_unregister_subevent(struct bt_hci *hci, unsigned int id)
}
bool bt_hci_get_conn_handle(struct bt_hci *hci, const uint8_t *bdaddr,
- uint16_t *handle)
+ uint8_t link_type, uint16_t *handle)
{
struct hci_conn_list_req *cl;
struct hci_conn_info *ci;
@@ -992,7 +993,7 @@ bool bt_hci_get_conn_handle(struct bt_hci *hci, const uint8_t *bdaddr,
return false;
memset(cl, 0, 10 * sizeof(*ci) + sizeof(*cl));
- cl->dev_id = 0; /* Will be filled by ioctl */
+ cl->dev_id = hci->index;
cl->conn_num = 10;
/* Get connection list via ioctl */
@@ -1001,10 +1002,11 @@ bool bt_hci_get_conn_handle(struct bt_hci *hci, const uint8_t *bdaddr,
return false;
}
- /* Search for the connection with matching bdaddr */
+ /* The list holds every link of the controller, so match the type too */
ci = cl->conn_info;
for (i = 0; i < cl->conn_num; i++, ci++) {
- if (memcmp(&ci->bdaddr, bdaddr, 6) == 0) {
+ if (ci->type == link_type && ci->state == BT_CONNECTED &&
+ memcmp(&ci->bdaddr, bdaddr, 6) == 0) {
*handle = ci->handle;
found = true;
break;
diff --git a/src/shared/hci.h b/src/shared/hci.h
index 5be48577f..a91d24523 100644
--- a/src/shared/hci.h
+++ b/src/shared/hci.h
@@ -49,4 +49,4 @@ unsigned int bt_hci_register_subevent(struct bt_hci *hci,
bool bt_hci_unregister_subevent(struct bt_hci *hci, unsigned int id);
bool bt_hci_get_conn_handle(struct bt_hci *hci, const uint8_t *bdaddr,
- uint16_t *handle);
+ uint8_t link_type, uint16_t *handle);
diff --git a/tools/hcitool.c b/tools/hcitool.c
index 1903d859b..fc4fc1709 100644
--- a/tools/hcitool.c
+++ b/tools/hcitool.c
@@ -34,9 +34,6 @@
#include "src/oui.h"
-/* Unofficial value, might still change */
-#define LE_LINK 0x80
-
#define FLAGS_AD_TYPE 0x01
#define FLAGS_LIMITED_MODE_BIT 0x01
#define FLAGS_GENERAL_MODE_BIT 0x02
--
2.43.0
^ permalink raw reply related [flat|nested] 18+ messages in thread* [PATCH BlueZ 07/15] profiles/ranging: Let the Reflector run the CS setup phase
2026-11-02 8:18 [PATCH BlueZ 00/15] Channel Sounding and RAP fixes Cheng Jiang
` (5 preceding siblings ...)
2026-11-02 8:19 ` [PATCH BlueZ 06/15] shared/hci: Look up the connection handle on the right link Cheng Jiang
@ 2026-11-02 8:19 ` Cheng Jiang
2026-11-02 8:19 ` [PATCH BlueZ 08/15] profiles/ranging: Let StartMeasurement() pick the Initiator for Both Cheng Jiang
` (8 subsequent siblings)
15 siblings, 0 replies; 18+ messages in thread
From: Cheng Jiang @ 2026-11-02 8:19 UTC (permalink / raw)
To: linux-bluetooth
Cc: jinwang.li, wei.deng, xiuzhuo.shang, shuai.zhang, Cheng Jiang
StartMeasurement() returned early for a Reflector, so the local Host
never read the CS capabilities or sent LE CS Set Default Settings, and
the state machine never left its idle state. The events driven by the
remote Initiator (Config, Security Enable, Procedure Enable) were then
dropped as out-of-state, and the Reflector role could not be used.
Run the setup phase for the Reflector as well. The FAE Table paths
skipped Set Default Settings for a Reflector, and for Both on the
Peripheral; send it on every path, and leave Create Config to the
Initiator, which rap_def_settings_done_cb() already decides from the
role.
The early return also skipped the Ranging Data subscription. The RAP
roles are independent of the CS roles (RAP v1.0 2), so a Reflector that
is the Ranging Requester still needs the peer's Ranging Data, and it is
now subscribed the same way as for an Initiator.
A Reflector can now reach StopMeasurement() before any procedure has
started, and the Procedure Enable it then sent was rejected by the
Controller; skip it while no procedure is running or being enabled.
bluetoothctl now keeps a session for a Reflector too, so that stop and
the Active property work for it, and the ChannelSounding1 documentation
describes the setup phase.
Assisted-by: Claude:claude-opus-5-5
---
client/cs.c | 25 ++++++-------
doc/org.bluez.ChannelSounding1.rst | 16 ++++-----
profiles/ranging/rap.c | 11 +++---
profiles/ranging/rap_hci.c | 58 +++++++++++-------------------
4 files changed, 43 insertions(+), 67 deletions(-)
diff --git a/client/cs.c b/client/cs.c
index e029da881..01d92cd9b 100644
--- a/client/cs.c
+++ b/client/cs.c
@@ -1120,29 +1120,26 @@ static void start_measurement_reply(DBusMessage *message, void *user_data)
return;
}
- if (cs_role == 0x02) {
- bt_shell_printf("Default settings applied on %s;"
- " Reflector role does not start a local"
- " measurement\n",
- g_dbus_proxy_get_path(proxy));
- return;
- }
-
s = g_new0(struct cs_session, 1);
s->proxy = proxy;
cs_sessions = g_list_append(cs_sessions, s);
- bt_shell_printf("Measurement started on %s\n",
- g_dbus_proxy_get_path(proxy));
+ if (cs_role == 0x02)
+ bt_shell_printf("Reflector armed on %s; waiting for the remote"
+ " Initiator\n",
+ g_dbus_proxy_get_path(proxy));
+ else
+ bt_shell_printf("Measurement started on %s\n",
+ g_dbus_proxy_get_path(proxy));
}
static void cs_print_role_hint(uint8_t role)
{
if (role == 0x02)
- bt_shell_printf("Note: Reflector role only uses"
- " role/cs_sync_ant_sel/max_tx_power;"
- " other CS config/frequency parameters"
- " are ignored.\n");
+ bt_shell_printf("Note: Reflector role runs the setup phase and"
+ " then waits for the remote Initiator to create"
+ " the config; it does not start a procedure"
+ " itself.\n");
else
bt_shell_printf("Note: Initiator role requires dev_addr"
" and duration_secs; CS config/frequency"
diff --git a/doc/org.bluez.ChannelSounding1.rst b/doc/org.bluez.ChannelSounding1.rst
index e75535b97..6e61ea4c1 100644
--- a/doc/org.bluez.ChannelSounding1.rst
+++ b/doc/org.bluez.ChannelSounding1.rst
@@ -37,12 +37,11 @@ Only one measurement per device object may be active at a time. Calling
``org.bluez.Error.InProgress``.
When ``role`` is Reflector (``0x02``), this method does not start a CS
-distance measurement: a Reflector never initiates a procedure locally,
-so it cannot start one via this call. Instead, all given configuration
-is applied and stored so the controller is ready to respond once a
-remote Initiator begins a procedure, and the method returns success
-without arming a local measurement session. The ``Active`` property
-only transitions to ``true`` when a remote-initiated procedure
+procedure: a Reflector never initiates one locally. It runs the setup
+phase (reading the capabilities of both sides and applying the default
+settings) so the controller is ready to respond once a remote Initiator
+creates the configuration and begins a procedure. The ``Active``
+property only transitions to ``true`` when a remote-initiated procedure
actually starts, which may happen well after this method returns (or
not at all, if the remote never initiates one). Clients that watch
``PropertiesChanged`` for ``Active`` (as **bluetoothctl-cs(1)** does)
@@ -53,7 +52,8 @@ path are the parameters that matter to start a measurement; every
other key below is optional configuration with a usable default. For
Reflector role, only ``role``, ``sync_ant_sel`` and ``max_tx_power``
are required or have any effect — the remaining keys are accepted but
-otherwise unused, since no local procedure is armed.
+otherwise unused, since the configuration comes from the remote
+Initiator.
Supported dictionary keys:
@@ -229,7 +229,7 @@ Examples:
| [cs] > start AA:BB:CC:DD:EE:FF
:bluetoothctl start with defaults:
| [cs] > start [dev_addr] [duration_secs]
-:bluetoothctl configure as Reflector (applies settings, does not start a measurement):
+:bluetoothctl configure as Reflector (runs the setup phase, does not start a procedure):
| [cs] > role 0x02
| [cs] > start
diff --git a/profiles/ranging/rap.c b/profiles/ranging/rap.c
index d23c3f8e2..a7fc95168 100644
--- a/profiles/ranging/rap.c
+++ b/profiles/ranging/rap.c
@@ -837,19 +837,16 @@ bad_type:
return g_dbus_create_error(msg, DBUS_ERROR_FAILED,
"Set CS freq params failed");
- /* Reflector never initiates a CS procedure locally: apply the
- * settings so the controller is ready to respond to the remote
- * Initiator, but do not arm a local measurement session.
- */
- if (settings.role == 0x02)
- return dbus_message_new_method_return(msg);
-
/* Nothing is subscribed at connection time unless On-demand is
* configured, so subscribe now if SetRangingDataMode() was not called.
*/
if (!bt_rap_ensure_ranging_data(data->rap))
DBG("Unable to subscribe to Ranging Data");
+ /* The Reflector runs the setup phase too, so that the state machine
+ * acts on the remote Initiator's events; rap_def_settings_done_cb()
+ * leaves Create Config to the Initiator.
+ */
if (!bt_rap_start_measurement(data->hci_sm, data->conn_handle,
duration_secs))
return g_dbus_create_error(msg, DBUS_ERROR_FAILED,
diff --git a/profiles/ranging/rap_hci.c b/profiles/ranging/rap_hci.c
index 62760b5c0..0eea018cc 100644
--- a/profiles/ranging/rap_hci.c
+++ b/profiles/ranging/rap_hci.c
@@ -716,7 +716,6 @@ static void rap_rd_rem_fae_cmplt_evt(const void *data, uint8_t size,
struct cs_state_machine *sm = (struct cs_state_machine *) user_data;
const struct bt_hci_evt_le_cs_rd_rem_fae_complete *evt;
struct bt_hci_evt_le_cs_rd_rem_supp_cap_complete tmp_ev;
- struct rap_conn_mapping *m;
struct iovec iov;
int i;
@@ -742,19 +741,10 @@ static void rap_rd_rem_fae_cmplt_evt(const void *data, uint8_t size,
* to the Default Settings step.
*/
if (evt->status == 0x11) {
- m = find_mapping_by_handle(sm, evt->handle);
-
DBG("Remote FAE=0 (No_FAE), proceed to Def Settings");
- if (is_initiator_role(sm) ||
- (is_both_role(sm) && m && m->is_central)) {
- memset(&tmp_ev, 0, sizeof(tmp_ev));
- tmp_ev.handle = evt->handle;
- DBG("Init/Both: send def settings (No_FAE)");
- rap_send_hci_def_settings_command(sm, &tmp_ev);
- } else {
- DBG("Reflector role: continuing after No_FAE");
- cs_set_state(sm, CS_STATE_INIT);
- }
+ memset(&tmp_ev, 0, sizeof(tmp_ev));
+ tmp_ev.handle = evt->handle;
+ rap_send_hci_def_settings_command(sm, &tmp_ev);
return;
}
error("Remote FAE Table read failed with status 0x%02X",
@@ -774,18 +764,9 @@ static void rap_rd_rem_fae_cmplt_evt(const void *data, uint8_t size,
}
/* After receiving FAE Table, send default settings */
- m = find_mapping_by_handle(sm, evt->handle);
-
- if (is_initiator_role(sm) ||
- (is_both_role(sm) && m && m->is_central)) {
- memset(&tmp_ev, 0, sizeof(tmp_ev));
- tmp_ev.handle = evt->handle;
- DBG("Init/Both: send def settings after FAE");
- rap_send_hci_def_settings_command(sm, &tmp_ev);
- } else {
- DBG("Reflector role: Proceeding after FAE Table");
- cs_set_state(sm, CS_STATE_INIT);
- }
+ memset(&tmp_ev, 0, sizeof(tmp_ev));
+ tmp_ev.handle = evt->handle;
+ rap_send_hci_def_settings_command(sm, &tmp_ev);
}
static bool bt_rap_read_remote_fae_table(void *hci_sm, uint16_t handle)
@@ -821,7 +802,6 @@ static void rap_rd_rmt_supp_cap_cmplt_evt(const void *data, uint8_t size,
{
struct cs_state_machine *sm = user_data;
const struct bt_hci_evt_le_cs_rd_rem_supp_cap_complete *evt;
- struct rap_conn_mapping *m;
struct iovec iov;
uint16_t subfeatures_supported;
@@ -871,18 +851,8 @@ static void rap_rd_rmt_supp_cap_cmplt_evt(const void *data, uint8_t size,
return;
}
- m = find_mapping_by_handle(sm, evt->handle);
-
- if (is_initiator_role(sm) ||
- (is_both_role(sm) && m && m->is_central)) {
- DBG("Init/Both: send def settings for 0x%04X",
- evt->handle);
- rap_send_hci_def_settings_command(sm, evt);
- } else {
- DBG("Reflector role: send def settings cmd");
- cs_set_state(sm, CS_STATE_INIT);
- rap_send_hci_def_settings_command(sm, evt);
- }
+ DBG("send def settings for 0x%04X", evt->handle);
+ rap_send_hci_def_settings_command(sm, evt);
}
static void rap_cs_config_cmplt_evt(const void *data, uint8_t size,
@@ -1844,6 +1814,18 @@ bool bt_rap_stop_measurement(void *hci_sm)
return false;
}
+ /* With no procedure running - a Reflector still waiting for the
+ * Initiator, or a timeout left over from an earlier measurement -
+ * there is nothing to disable, and the Controller answers Procedure
+ * Enable with Invalid HCI Command Parameters. A procedure that is
+ * being enabled has not reported yet, so still disable that one.
+ */
+ if (!sm->procedure_active &&
+ cs_get_current_state(sm) != CS_STATE_WAIT_PROC_CMPLT) {
+ DBG("No local CS procedure active, nothing to disable");
+ return true;
+ }
+
return rap_send_hci_cs_procedure_enable(sm, sm->active_conn_handle,
false);
}
--
2.43.0
^ permalink raw reply related [flat|nested] 18+ messages in thread* [PATCH BlueZ 08/15] profiles/ranging: Let StartMeasurement() pick the Initiator for Both
2026-11-02 8:18 [PATCH BlueZ 00/15] Channel Sounding and RAP fixes Cheng Jiang
` (6 preceding siblings ...)
2026-11-02 8:19 ` [PATCH BlueZ 07/15] profiles/ranging: Let the Reflector run the CS setup phase Cheng Jiang
@ 2026-11-02 8:19 ` Cheng Jiang
2026-11-02 8:19 ` [PATCH BlueZ 09/15] profiles/ranging: Take the CS role from Config Complete Cheng Jiang
` (7 subsequent siblings)
15 siblings, 0 replies; 18+ messages in thread
From: Cheng Jiang @ 2026-11-02 8:19 UTC (permalink / raw)
To: linux-bluetooth
Cc: jinwang.li, wei.deng, xiuzhuo.shang, shuai.zhang, Cheng Jiang
Role_Enable is a bitmask of the CS roles the Host permits, and the CS
roles are independent of the GAP roles. When Role_Enable permitted both
roles, the setup code let the GAP role decide, so a Peripheral that
called StartMeasurement() still waited as Reflector and took the
Initiator role only when Role_Enable permitted nothing else.
The setup phase only runs from StartMeasurement(), so the local Host is
always the one asking to measure: take the Initiator role whenever
Role_Enable permits it.
Both sides may now ask for a configuration at the same time. The
collision rules let the Central's request complete and reject the
Peripheral's with LL Procedure Collision (Core v6.2 Vol 6 Part B 5.1.25
and 5.3). Ignore that rejection instead of removing the config ID, which
could remove the configuration the Central has just created.
The Config Complete role check compared Role_Enable with the two
removed helpers. Check the reported role against the Role_Enable bits
instead, which accepts the same roles for every valid Role_Enable.
Assisted-by: Claude:claude-opus-5-5
---
profiles/ranging/rap_hci.c | 59 +++++++++++++++++++++++---------------
1 file changed, 36 insertions(+), 23 deletions(-)
diff --git a/profiles/ranging/rap_hci.c b/profiles/ranging/rap_hci.c
index 0eea018cc..f23e89cfe 100644
--- a/profiles/ranging/rap_hci.c
+++ b/profiles/ranging/rap_hci.c
@@ -348,17 +348,30 @@ static enum cs_state cs_get_current_state(struct cs_state_machine *sm)
return sm ? sm->current_state : CS_STATE_UNSPECIFIED;
}
-static bool is_initiator_role(const struct cs_state_machine *sm)
+/* Role_Enable is a bitmask of the CS roles the Host permits for this link
+ * (Core v6.2 Vol 4 Part E 7.8.134), not a selector for one of them.
+ */
+#define CS_ROLE_ENABLE_INITIATOR 0x01
+#define CS_ROLE_ENABLE_REFLECTOR 0x02
+
+/* Whether Role_Enable permits the CS role (CS_INITIATOR or CS_REFLECTOR)
+ * that LE CS Config Complete reports for the local Controller.
+ */
+static bool cs_role_permitted(const struct cs_state_machine *sm, uint8_t role)
{
- return sm->role_enable == 0x01;
+ if (role == CS_INITIATOR)
+ return sm->role_enable & CS_ROLE_ENABLE_INITIATOR;
+
+ return sm->role_enable & CS_ROLE_ENABLE_REFLECTOR;
}
-/* For role_enable=0x03 (Both), Central acts as Initiator and Peripheral as
- * Reflector — the link layer topology determines the CS role assignment.
+/* The setup phase only runs from StartMeasurement(), that is when the local
+ * Host asked to measure this peer, so take the Initiator role whenever
+ * Role_Enable permits it. The CS roles are independent of the GAP roles.
*/
-static bool is_both_role(const struct cs_state_machine *sm)
+static bool cs_local_is_initiator(const struct cs_state_machine *sm)
{
- return sm->role_enable == 0x03;
+ return sm->role_enable & CS_ROLE_ENABLE_INITIATOR;
}
/* Helper function to send read remote capabilities for all connections */
@@ -503,7 +516,6 @@ static void rap_def_settings_done_cb(const void *data, uint8_t size,
{
const struct bt_hci_rsp_le_cs_set_def_settings *rp;
struct cs_state_machine *sm = user_data;
- struct rap_conn_mapping *m;
if (!sm || !data || size < sizeof(*rp))
return;
@@ -521,22 +533,13 @@ static void rap_def_settings_done_cb(const void *data, uint8_t size,
if (rp->status == 0) {
cs_set_state(sm, CS_STATE_WAIT_CONFIG_CMPLT);
- /* Send CS Create Config if we are (or will act as) Initiator.
- * For role_enable=Both, Central acts as Initiator.
+ /* Only the Initiator creates the config; the Reflector waits
+ * for its own CS Config Complete.
*/
- if (is_initiator_role(sm)) {
+ if (cs_local_is_initiator(sm))
rap_send_hci_cs_create_config_command(sm, rp->handle);
- } else if (is_both_role(sm)) {
- m = find_mapping_by_handle(sm, rp->handle);
-
- if (m && m->is_central)
- rap_send_hci_cs_create_config_command(sm,
- rp->handle);
- else
- DBG("Both role: Wait for CS Config Cmpl");
- } else {
+ else
DBG("Reflector role: Waiting for CS Config Completed");
- }
} else {
/* Error - transition to hold */
error("CS Set default setting failed with status 0x%02X",
@@ -889,6 +892,18 @@ static void rap_cs_config_cmplt_evt(const void *data, uint8_t size,
DBG("status=0x%02X, handle=0x%04X", evt->status, evt->handle);
+ /* Both sides asked for a configuration at the same time. The Central
+ * rejects the Peripheral's request with LL Procedure Collision (0x23)
+ * and completes its own, which the Peripheral handles instead (Core
+ * v6.2 Vol 6 Part B 5.3). That configuration arrives in an event of
+ * its own, so there is nothing to undo here: removing the config ID
+ * could remove the one the Central has just created.
+ */
+ if (evt->status == 0x23) {
+ DBG("Config collided with the peer's, using the peer's");
+ return;
+ }
+
/* Check status */
if (evt->status != 0) {
if (evt->action != 0x00) {
@@ -934,9 +949,7 @@ static void rap_cs_config_cmplt_evt(const void *data, uint8_t size,
* (CS_INITIATOR=0x00, CS_REFLECTOR=0x01)
* which is different from the role_enable value sent in HCI command
*/
- if ((is_initiator_role(sm) && rap_ev.role != CS_INITIATOR) ||
- (!is_initiator_role(sm) && !is_both_role(sm) &&
- rap_ev.role != CS_REFLECTOR)) {
+ if (!cs_role_permitted(sm, rap_ev.role)) {
error("CS Config role 0x%02X doesn't match role enable 0x%02X",
rap_ev.role, sm->role_enable);
cs_set_state(sm, CS_STATE_HOLD);
--
2.43.0
^ permalink raw reply related [flat|nested] 18+ messages in thread* [PATCH BlueZ 09/15] profiles/ranging: Take the CS role from Config Complete
2026-11-02 8:18 [PATCH BlueZ 00/15] Channel Sounding and RAP fixes Cheng Jiang
` (7 preceding siblings ...)
2026-11-02 8:19 ` [PATCH BlueZ 08/15] profiles/ranging: Let StartMeasurement() pick the Initiator for Both Cheng Jiang
@ 2026-11-02 8:19 ` Cheng Jiang
2026-11-02 8:19 ` [PATCH BlueZ 10/15] profiles/ranging: Act on a CS configuration created by the peer Cheng Jiang
` (6 subsequent siblings)
15 siblings, 0 replies; 18+ messages in thread
From: Cheng Jiang @ 2026-11-02 8:19 UTC (permalink / raw)
To: linux-bluetooth
Cc: jinwang.li, wei.deng, xiuzhuo.shang, shuai.zhang, Cheng Jiang
The LE CS Config Complete handler stored the CS role it reports in
cs_opt.role, overwriting the Role_Enable value set by StartMeasurement().
StartMeasurement() seeds its settings from that field, so a second call
without a role key sent Set Default Settings with the negotiated role
instead: 0x00 for an Initiator, which is not a valid Role_Enable, and
0x01, Initiator only, for a Reflector.
Keep the negotiated role in its own field, known only once Config
Complete has arrived, and report Role_Enable back to StartMeasurement().
With the Reflector now running the setup phase, StopMeasurement() can
also be reached on a Reflector whose procedure is running. Only the
Initiator enables a procedure, so do not send Procedure Enable from a
Reflector to stop it.
Assisted-by: Claude:claude-opus-5-5
---
profiles/ranging/rap_hci.c | 29 ++++++++++++++++++++---------
src/shared/rap.h | 1 -
2 files changed, 20 insertions(+), 10 deletions(-)
diff --git a/profiles/ranging/rap_hci.c b/profiles/ranging/rap_hci.c
index f23e89cfe..ef01e60c5 100644
--- a/profiles/ranging/rap_hci.c
+++ b/profiles/ranging/rap_hci.c
@@ -75,6 +75,13 @@ struct cs_state_machine {
bool procedure_active;
struct bt_rap_hci_cs_options cs_opt; /* Per-instance CS options */
uint8_t role_enable; /* Role value for HCI commands (1, 2, or 3) */
+ /* Negotiated CS role for this config (CS_INITIATOR/CS_REFLECTOR), taken
+ * from LE CS Config Complete. Unknown until that event arrives, which
+ * is why it is kept apart from role_enable: the latter is the bitmask
+ * of roles the host permits, not the role actually in use.
+ */
+ uint8_t cs_role;
+ bool cs_role_known;
struct queue *conn_mappings; /* Per-instance connection mappings */
uint16_t active_conn_handle; /* current measurement handle */
struct timespec last_chan_class_time; /* For 1-second rate limit */
@@ -129,7 +136,6 @@ bool bt_rap_set_default_settings_params(void *hci_sm,
return false;
sm->role_enable = settings->role;
- sm->cs_opt.role = settings->role;
sm->cs_opt.cs_sync_ant_sel = settings->cs_sync_ant_sel;
sm->cs_opt.max_tx_power = settings->max_tx_power;
return true;
@@ -167,7 +173,7 @@ bool bt_rap_get_default_settings_params(void *hci_sm,
if (!sm || !settings)
return false;
- settings->role = sm->cs_opt.role;
+ settings->role = sm->role_enable;
settings->cs_sync_ant_sel = sm->cs_opt.cs_sync_ant_sel;
settings->max_tx_power = sm->cs_opt.max_tx_power;
return true;
@@ -277,7 +283,7 @@ static void cs_state_machine_init(struct cs_state_machine *sm,
/* Store role_enable for HCI commands (1, 2, or 3 from config) */
sm->role_enable = role;
- sm->cs_opt.role = role;
+ sm->cs_role_known = false;
sm->cs_opt.cs_sync_ant_sel = cs_sync_ant_sel;
sm->cs_opt.max_tx_power = max_tx_power;
sm->cs_opt.rtt_type = 0;
@@ -956,7 +962,8 @@ static void rap_cs_config_cmplt_evt(const void *data, uint8_t size,
return;
}
- sm->cs_opt.role = rap_ev.role;
+ sm->cs_role = rap_ev.role;
+ sm->cs_role_known = true;
sm->cs_opt.rtt_type = rap_ev.rtt_type;
DBG("config_id=%u, action=%u, ",
@@ -1042,7 +1049,7 @@ static void rap_cs_sec_enable_cmplt_evt(const void *data, uint8_t size,
cs_set_state(sm, CS_STATE_WAIT_PROC_CMPLT);
/* Check if role is initiator */
- if (sm->cs_opt.role == CS_INITIATOR) {
+ if (sm->cs_role_known && sm->cs_role == CS_INITIATOR) {
DBG("Initiator role: Sending CS Set Procedure Params");
if (!rap_send_hci_cs_set_procedure_parameters(
sm, handle)) {
@@ -1516,7 +1523,7 @@ static void rap_cs_subevt_result_evt(const void *data, uint8_t size,
return;
}
- cs_role = sm->cs_opt.role;
+ cs_role = sm->cs_role;
cs_rtt_type = sm->cs_opt.rtt_type;
max_paths = MIN((num_ant_paths + 1), CS_MAX_ANT_PATHS);
steps = MIN(num_steps_reported, CS_MAX_STEPS);
@@ -1608,7 +1615,7 @@ static void rap_cs_subevt_result_cont_evt(const void *data, uint8_t size,
return;
}
- cs_role = sm->cs_opt.role;
+ cs_role = sm->cs_role;
cs_rtt_type = sm->cs_opt.rtt_type;
max_paths = MIN((num_ant_paths + 1), CS_MAX_ANT_PATHS);
steps = MIN(num_steps_reported, CS_MAX_STEPS);
@@ -1832,9 +1839,12 @@ bool bt_rap_stop_measurement(void *hci_sm)
* there is nothing to disable, and the Controller answers Procedure
* Enable with Invalid HCI Command Parameters. A procedure that is
* being enabled has not reported yet, so still disable that one.
+ * Only the Initiator enables a procedure, so the same holds for a
+ * Reflector once the Initiator's procedure is running.
*/
- if (!sm->procedure_active &&
- cs_get_current_state(sm) != CS_STATE_WAIT_PROC_CMPLT) {
+ if ((!sm->procedure_active &&
+ cs_get_current_state(sm) != CS_STATE_WAIT_PROC_CMPLT) ||
+ !sm->cs_role_known || sm->cs_role != CS_INITIATOR) {
DBG("No local CS procedure active, nothing to disable");
return true;
}
@@ -1891,6 +1901,7 @@ void bt_rap_clear_conn_handle(void *hci_sm, uint16_t handle)
DBG("Clearing connection mapping: handle=0x%04X", handle);
remove_conn_mapping(sm, handle);
sm->procedure_active = false;
+ sm->cs_role_known = false;
sm->current_state = CS_STATE_UNSPECIFIED;
}
diff --git a/src/shared/rap.h b/src/shared/rap.h
index 7ee51143c..7ff5de227 100644
--- a/src/shared/rap.h
+++ b/src/shared/rap.h
@@ -19,7 +19,6 @@ struct bt_att;
/* Channel Sounding Events */
struct bt_rap_hci_cs_options {
- uint8_t role;
uint8_t cs_sync_ant_sel;
int8_t max_tx_power;
int rtt_type;
--
2.43.0
^ permalink raw reply related [flat|nested] 18+ messages in thread* [PATCH BlueZ 10/15] profiles/ranging: Act on a CS configuration created by the peer
2026-11-02 8:18 [PATCH BlueZ 00/15] Channel Sounding and RAP fixes Cheng Jiang
` (8 preceding siblings ...)
2026-11-02 8:19 ` [PATCH BlueZ 09/15] profiles/ranging: Take the CS role from Config Complete Cheng Jiang
@ 2026-11-02 8:19 ` Cheng Jiang
2026-11-02 8:19 ` [PATCH BlueZ 11/15] profiles/ranging: Keep a peer-driven setup from being rewound Cheng Jiang
` (5 subsequent siblings)
15 siblings, 0 replies; 18+ messages in thread
From: Cheng Jiang @ 2026-11-02 8:19 UTC (permalink / raw)
To: linux-bluetooth
Cc: jinwang.li, wei.deng, xiuzhuo.shang, shuai.zhang, Cheng Jiang
LE CS Config Complete is also generated when the local Controller has
responded to a configuration request from the remote one (Core v6.2
Vol 4 Part E 7.7.65.42). The handler required CS_STATE_WAIT_CONFIG_CMPLT
before parsing the event, which is only reached between the Set Default
Settings response and Config Complete of a StartMeasurement(). A remote
Initiator does not wait for that: it can create the configuration as
soon as the link is up, before the local setup phase has run, or again
after an earlier procedure. The event was then dropped along with the
role it carries, Security Enable Complete and Procedure Enable Complete
failed their own state checks, and no ranging data was produced.
Parse Config Complete in any state. Every state machine on the adapter
receives the CS events of all links, which the state check used to
hide, so ignore Config Complete and Security Enable Complete events
whose handle is not mapped to this state machine, so that they cannot
overwrite this link's role or advance its state.
A successful removal (action 0x00) leaves the remaining parameters
undefined, yet it went on to the role check and, if the undefined role
happened to be permitted, to Security Enable. Ignore it whatever the
state.
Assisted-by: Claude:claude-opus-5-5
---
profiles/ranging/rap_hci.c | 42 +++++++++++++++++++++++++++++++-------
1 file changed, 35 insertions(+), 7 deletions(-)
diff --git a/profiles/ranging/rap_hci.c b/profiles/ranging/rap_hci.c
index ef01e60c5..39b167ba8 100644
--- a/profiles/ranging/rap_hci.c
+++ b/profiles/ranging/rap_hci.c
@@ -882,13 +882,6 @@ static void rap_cs_config_cmplt_evt(const void *data, uint8_t size,
DBG("size=0x%02X", size);
- /* State Check */
- if (cs_get_current_state(sm) != CS_STATE_WAIT_CONFIG_CMPLT) {
- DBG("Event received in Wrong State!! ");
- DBG("Expected : CS_STATE_WAIT_CONFIG_CMPLT");
- return;
- }
-
/* Pull the entire structure at once */
evt = util_iov_pull_mem(&iov, sizeof(*evt));
if (!evt) {
@@ -898,6 +891,35 @@ static void rap_cs_config_cmplt_evt(const void *data, uint8_t size,
DBG("status=0x%02X, handle=0x%04X", evt->status, evt->handle);
+ /* Every state machine on this adapter sees every CS event, so ignore
+ * the ones for other links: acting on them would overwrite this
+ * link's negotiated role and advance its state.
+ */
+ if (!find_mapping_by_handle(sm, evt->handle)) {
+ DBG("Not for this link, ignoring");
+ return;
+ }
+
+ /* Action 0x00 reports that the configuration was removed, and leaves
+ * the remaining parameters undefined (Core v6.2 Vol 4 Part E
+ * 7.7.65.42), so there is no role or configuration to act on.
+ */
+ if (evt->status == 0 && evt->action == 0x00) {
+ DBG("Config %u removed", evt->config_id);
+ return;
+ }
+
+ /* This event is also generated when the local Controller responded to a
+ * configuration request from the remote one, which is how a Reflector
+ * learns a procedure is being set up for it. The peer can do that
+ * before the setup phase has run here, or after an earlier procedure,
+ * so do not require WAIT_CONFIG_CMPLT, which only the local setup
+ * phase reaches.
+ */
+ if (cs_get_current_state(sm) != CS_STATE_WAIT_CONFIG_CMPLT)
+ DBG("Config created by the peer while in %s",
+ state_names[cs_get_current_state(sm)]);
+
/* Both sides asked for a configuration at the same time. The Central
* rejects the Peripheral's request with LL Procedure Collision (0x23)
* and completes its own, which the Peripheral handles instead (Core
@@ -1044,6 +1066,12 @@ static void rap_cs_sec_enable_cmplt_evt(const void *data, uint8_t size,
DBG("status=0x%02X, handle=0x%04X",
rap_ev.status, handle);
+ /* Shared HCI channel: drop events belonging to another link. */
+ if (!find_mapping_by_handle(sm, handle)) {
+ DBG("Not for this link, ignoring");
+ return;
+ }
+
if (rap_ev.status == 0) {
/* Success - proceed to configuration */
cs_set_state(sm, CS_STATE_WAIT_PROC_CMPLT);
--
2.43.0
^ permalink raw reply related [flat|nested] 18+ messages in thread* [PATCH BlueZ 11/15] profiles/ranging: Keep a peer-driven setup from being rewound
2026-11-02 8:18 [PATCH BlueZ 00/15] Channel Sounding and RAP fixes Cheng Jiang
` (9 preceding siblings ...)
2026-11-02 8:19 ` [PATCH BlueZ 10/15] profiles/ranging: Act on a CS configuration created by the peer Cheng Jiang
@ 2026-11-02 8:19 ` Cheng Jiang
2026-11-02 8:19 ` [PATCH BlueZ 12/15] profiles/ranging: Defer CS Security Enable until the link is encrypted Cheng Jiang
` (4 subsequent siblings)
15 siblings, 0 replies; 18+ messages in thread
From: Cheng Jiang @ 2026-11-02 8:19 UTC (permalink / raw)
To: linux-bluetooth
Cc: jinwang.li, wei.deng, xiuzhuo.shang, shuai.zhang, Cheng Jiang
The Set Default Settings response moved the state machine to
CS_STATE_WAIT_CONFIG_CMPLT unconditionally. A remote Initiator does
not wait for the local setup phase, so its configuration can complete
while the capabilities are still being read over the air or while
this response is pending, for instance when the local Controller still
has a CS role enabled from an earlier StartMeasurement(). Its Config
Complete now advances the state on its own, so the response put the
state back and, on an Initiator, created a second configuration.
Only advance from CS_STATE_INIT, the state the setup phase is in until
then.
Assisted-by: Claude:claude-opus-5-5
---
profiles/ranging/rap_hci.c | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/profiles/ranging/rap_hci.c b/profiles/ranging/rap_hci.c
index 39b167ba8..8ce7f32f1 100644
--- a/profiles/ranging/rap_hci.c
+++ b/profiles/ranging/rap_hci.c
@@ -537,6 +537,16 @@ static void rap_def_settings_done_cb(const void *data, uint8_t size,
}
if (rp->status == 0) {
+ /* A configuration created by the peer may have completed
+ * while the capabilities were read or this response was
+ * pending. Do not rewind its state.
+ */
+ if (cs_get_current_state(sm) != CS_STATE_INIT) {
+ DBG("Setup already advanced to %s",
+ state_names[cs_get_current_state(sm)]);
+ return;
+ }
+
cs_set_state(sm, CS_STATE_WAIT_CONFIG_CMPLT);
/* Only the Initiator creates the config; the Reflector waits
--
2.43.0
^ permalink raw reply related [flat|nested] 18+ messages in thread* [PATCH BlueZ 12/15] profiles/ranging: Defer CS Security Enable until the link is encrypted
2026-11-02 8:18 [PATCH BlueZ 00/15] Channel Sounding and RAP fixes Cheng Jiang
` (10 preceding siblings ...)
2026-11-02 8:19 ` [PATCH BlueZ 11/15] profiles/ranging: Keep a peer-driven setup from being rewound Cheng Jiang
@ 2026-11-02 8:19 ` Cheng Jiang
2026-11-02 8:19 ` [PATCH BlueZ 13/15] profiles/ranging: Choose the CS antenna configuration from both ends Cheng Jiang
` (3 subsequent siblings)
15 siblings, 0 replies; 18+ messages in thread
From: Cheng Jiang @ 2026-11-02 8:19 UTC (permalink / raw)
To: linux-bluetooth
Cc: jinwang.li, wei.deng, xiuzhuo.shang, shuai.zhang, Cheng Jiang
LE CS Security Enable is only valid on an encrypted link (Core v6.2
Vol 4 Part E 7.8.133, which fails it with Insufficient Security
otherwise). On the Central, the Config Complete handler checked the ATT
security level once and put the state machine on hold if the link was
not yet encrypted, so a configuration completing before encryption
never got its Security Enable.
Defer the command instead and send it when the HCI Encryption Change
event reports the link as encrypted. Track the encryption in the
connection mapping, so the check no longer depends only on an ATT
instance being attached, and keep the ATT security level as a fallback
for a link that was encrypted before the state machine was attached.
Assisted-by: Claude:claude-opus-5-5
---
profiles/ranging/rap_hci.c | 78 +++++++++++++++++++++++++++++++-------
1 file changed, 65 insertions(+), 13 deletions(-)
diff --git a/profiles/ranging/rap_hci.c b/profiles/ranging/rap_hci.c
index 8ce7f32f1..2e7f216aa 100644
--- a/profiles/ranging/rap_hci.c
+++ b/profiles/ranging/rap_hci.c
@@ -75,6 +75,8 @@ struct cs_state_machine {
bool procedure_active;
struct bt_rap_hci_cs_options cs_opt; /* Per-instance CS options */
uint8_t role_enable; /* Role value for HCI commands (1, 2, or 3) */
+ bool sec_enable_pending; /* Security Enable deferred until encrypted */
+ unsigned int encrypt_evt_id; /* Encryption Change registration */
/* Negotiated CS role for this config (CS_INITIATOR/CS_REFLECTOR), taken
* from LE CS Config Complete. Unknown until that event arrives, which
* is why it is kept apart from role_enable: the latter is the bitmask
@@ -99,6 +101,7 @@ struct rap_conn_mapping {
uint8_t bdaddr[6];
uint8_t bdaddr_type;
bool is_central; /* true if local device is BLE Central on this link */
+ bool encrypted; /* set from the HCI Encryption Change event */
struct bt_att *att;
struct bt_rap *rap;
};
@@ -905,7 +908,8 @@ static void rap_cs_config_cmplt_evt(const void *data, uint8_t size,
* the ones for other links: acting on them would overwrite this
* link's negotiated role and advance its state.
*/
- if (!find_mapping_by_handle(sm, evt->handle)) {
+ mapping = find_mapping_by_handle(sm, evt->handle);
+ if (!mapping) {
DBG("Not for this link, ignoring");
return;
}
@@ -1013,26 +1017,62 @@ static void rap_cs_config_cmplt_evt(const void *data, uint8_t size,
/* Success - proceed to Security enable complete */
cs_set_state(sm, CS_STATE_WAIT_SEC_CMPLT);
- /* CS Security Enable may only be issued by the BLE Central */
- mapping = find_mapping_by_handle(sm, evt->handle);
- if (mapping && mapping->is_central) {
- if (bt_att_get_security(mapping->att, NULL) <
- BT_ATT_SECURITY_MEDIUM) {
- error("CS Security Enable skipped: not encrypted");
- cs_set_state(sm, CS_STATE_HOLD);
- return;
- }
-
+ /* CS Security Enable may only be issued by the BLE Central, and only on
+ * an encrypted link. The LE role comes from the connection mapping
+ * (set from the LE connection event), not from the CS role in rap_ev.
+ * If the link is not encrypted yet, defer: the Encryption Change event
+ * retries it (rap_encrypt_change_evt).
+ */
+ if (!mapping->is_central) {
+ /* Peripheral link: the Central's Link Layer starts CS Security
+ * Start (Core v6.2 Vol 6 Part B 5.1.23). The local controller
+ * reports completion with LE CS Security Enable Complete when
+ * it responds to the peer's request (Vol 4 Part E 7.7.65.41),
+ * and that event continues the procedure. Do not stop here.
+ */
+ DBG("Peripheral link: waiting for peer CS Security Start");
+ sm->sec_enable_pending = false;
+ } else if (mapping->encrypted ||
+ (mapping->att && bt_att_get_security(mapping->att,
+ NULL) >= BT_ATT_SECURITY_MEDIUM)) {
DBG("Central,encrypted: Sending CS Security Enable command");
rap_send_hci_cs_security_enable_command(sm, evt->handle);
} else {
- DBG("Peripheral: Waiting for security enable event...");
+ DBG("Central, not yet encrypted: deferring CS Security Enable");
+ sm->sec_enable_pending = true;
}
/* Send callback to RAP Profile */
bt_rap_hci_cs_config_complete_callback(size, &rap_ev, sm->rap);
}
+static void rap_encrypt_change_evt(const void *data, uint8_t size,
+ void *user_data)
+{
+ struct cs_state_machine *sm = user_data;
+ const struct bt_hci_evt_encrypt_change *evt = data;
+ struct rap_conn_mapping *mapping;
+ uint16_t handle;
+
+ if (!sm || size < sizeof(*evt) || evt->status != 0)
+ return;
+
+ handle = le16_to_cpu(evt->handle);
+ mapping = find_mapping_by_handle(sm, handle);
+ if (!mapping)
+ return;
+
+ mapping->encrypted = (evt->encr_mode != 0x00);
+
+ if (!sm->sec_enable_pending || !mapping->is_central ||
+ !mapping->encrypted)
+ return;
+
+ DBG("Encrypted: sending deferred CS Security Enable");
+ sm->sec_enable_pending = false;
+ rap_send_hci_cs_security_enable_command(sm, handle);
+}
+
static void rap_cs_sec_enable_cmplt_evt(const void *data, uint8_t size,
void *user_data)
{
@@ -1802,6 +1842,14 @@ void *bt_rap_attach_hci(struct bt_rap *rap, struct bt_hci *hci,
queue_push_tail(sm->event_ids, UINT_TO_PTR(id));
}
+ /* Encryption Change is a plain HCI event; it lets a deferred CS
+ * Security Enable be sent once the link is encrypted.
+ */
+ sm->encrypt_evt_id = bt_hci_register(hci, BT_HCI_EVT_ENCRYPT_CHANGE,
+ rap_encrypt_change_evt, sm, NULL);
+ if (!sm->encrypt_evt_id)
+ error("Failed to register Encryption Change handler");
+
DBG("HCI state machine attached (role=%u, ant=0x%02x, power=%d)",
sm->role_enable, sm->cs_opt.cs_sync_ant_sel,
sm->cs_opt.max_tx_power);
@@ -1940,6 +1988,7 @@ void bt_rap_clear_conn_handle(void *hci_sm, uint16_t handle)
remove_conn_mapping(sm, handle);
sm->procedure_active = false;
sm->cs_role_known = false;
+ sm->sec_enable_pending = false;
sm->current_state = CS_STATE_UNSPECIFIED;
}
@@ -1988,9 +2037,12 @@ void bt_rap_detach_hci(struct bt_rap *rap, void *hci_sm)
/* Cleanup the per-instance state machine */
if (sm) {
/* Unregister HCI events */
- if (sm->hci)
+ if (sm->hci) {
queue_foreach(sm->event_ids, unregister_event_id,
sm->hci);
+ if (sm->encrypt_evt_id)
+ bt_hci_unregister(sm->hci, sm->encrypt_evt_id);
+ }
if (sm->measurement_timeout_id) {
timeout_remove(sm->measurement_timeout_id);
--
2.43.0
^ permalink raw reply related [flat|nested] 18+ messages in thread* [PATCH BlueZ 13/15] profiles/ranging: Choose the CS antenna configuration from both ends
2026-11-02 8:18 [PATCH BlueZ 00/15] Channel Sounding and RAP fixes Cheng Jiang
` (11 preceding siblings ...)
2026-11-02 8:19 ` [PATCH BlueZ 12/15] profiles/ranging: Defer CS Security Enable until the link is encrypted Cheng Jiang
@ 2026-11-02 8:19 ` Cheng Jiang
2026-11-02 8:19 ` [PATCH BlueZ 14/15] profiles/ranging: Do not start a procedure after StopMeasurement() Cheng Jiang
` (2 subsequent siblings)
15 siblings, 0 replies; 18+ messages in thread
From: Cheng Jiang @ 2026-11-02 8:19 UTC (permalink / raw)
To: linux-bluetooth
Cc: jinwang.li, wei.deng, xiuzhuo.shang, shuai.zhang, Cheng Jiang
Set Procedure Parameters sent the configured antenna configuration as
is, ACI 7 unless the application gave another. ACI 7 is the 2:2
configuration and needs two antennas and four antenna paths on both
ends. Against a single-antenna peer, or on a single-antenna local
Controller, the command was rejected with Rejected Due to Limited
Resources and no procedure was started.
The capabilities of both ends are read during the setup phase, but the
antenna counts were only logged. Keep the antenna count and antenna path
limit from both, and check the requested configuration against them and
the eight configurations of Core v6.2 Vol 6 Part A, Table 5.3. If it
fits, use it as given; otherwise use the first one that fits, with a
Preferred_Peer_Antenna that has at least as many bits set as the peer has
antenna elements in it.
Table 5.3 is written as "device A:device B" where device A is the
initiator (Vol 6 Part B, 2.4.2.47), so a configuration is asymmetric:
one antenna locally and four on the peer is ACI 6 as the Initiator but
ACI 3 as the Reflector. Use the negotiated CS role to decide which end
contributes which count, and refuse to send the command until both
capability reads and the role are known.
Assisted-by: Claude:claude-sonnet-5-5
---
profiles/ranging/rap_hci.c | 125 +++++++++++++++++++++++++++++++++++++
1 file changed, 125 insertions(+)
diff --git a/profiles/ranging/rap_hci.c b/profiles/ranging/rap_hci.c
index 2e7f216aa..4a1a2ad5e 100644
--- a/profiles/ranging/rap_hci.c
+++ b/profiles/ranging/rap_hci.c
@@ -89,6 +89,12 @@ struct cs_state_machine {
struct timespec last_chan_class_time; /* For 1-second rate limit */
struct bt_rap_le_cs_config cs_config;
struct bt_rap_le_cs_frequency cs_frequency;
+ uint8_t remote_num_antennas; /* from Read Remote Supported Caps */
+ uint8_t remote_max_antenna_paths;
+ bool remote_caps_known;
+ uint8_t local_num_antennas; /* from LE CS Read Local Supported Caps */
+ uint8_t local_max_antenna_paths;
+ bool local_caps_known;
cs_timeout_func_t timeout_func;
void *timeout_data;
void (*proc_active_func)(bool active, void *user_data);
@@ -441,6 +447,13 @@ static void rap_rd_loc_supp_cap_done_cb(const void *data, uint8_t size,
rsp->max_consecutive_procedures_supported);
DBG(" Num Antennas: %u", rsp->num_antennas_supported);
DBG(" Max Antenna Paths: %u", rsp->max_antenna_paths_supported);
+
+ /* Needed to pick the antenna configuration, which has to fit both
+ * ends (Core v6.2 Vol 6 Part A, Table 5.3).
+ */
+ sm->local_num_antennas = rsp->num_antennas_supported;
+ sm->local_max_antenna_paths = rsp->max_antenna_paths_supported;
+ sm->local_caps_known = true;
DBG(" Roles Supported: 0x%02X", rsp->roles_supported);
DBG(" Modes Supported: 0x%02X", rsp->modes_supported);
DBG(" RTT Capability: 0x%02X", rsp->rtt_capability);
@@ -624,6 +637,104 @@ static void rap_send_hci_cs_security_enable_command(
DBG("CS Security Enable command sent successfully");
}
+/*
+ * Pick the antenna configuration for Set Procedure Parameters.
+ *
+ * The ACI values of Core v6.2 Vol 6 Part A, Table 5.3 are "device A:device B"
+ * antenna counts, where device A is the initiator (Vol 6 Part B, 2.4.2.47),
+ * so which end contributes which count depends on the local CS role. The
+ * entries are ordered so that the first match uses as many antenna paths as
+ * both ends allow.
+ */
+static const struct {
+ uint8_t aci;
+ uint8_t paths;
+ uint8_t init_ant;
+ uint8_t refl_ant;
+} cs_antenna_configs[] = {
+ { 0x07, 4, 2, 2 }, /* 2:2 */
+ { 0x03, 4, 4, 1 }, /* N_AP:1 */
+ { 0x06, 4, 1, 4 }, /* 1:N_AP */
+ { 0x02, 3, 3, 1 },
+ { 0x05, 3, 1, 3 },
+ { 0x01, 2, 2, 1 },
+ { 0x04, 2, 1, 2 },
+ { 0x00, 1, 1, 1 }, /* 1:1, always usable */
+};
+
+static bool cs_select_antenna_config(struct cs_state_machine *sm)
+{
+ uint8_t local_ant, peer_ant, max_paths, req, sel_peer = 0;
+ bool initiator;
+ size_t i, sel = ARRAY_SIZE(cs_antenna_configs);
+
+ /* Both capability reads have to be in before the configuration can be
+ * known to fit: ACI 7 (2:2), the default, is rejected with Rejected
+ * Due to Limited Resources if either end has a single antenna. The
+ * role decides which end of the configuration is ours, so it has to
+ * be settled as well.
+ */
+ if (!sm->local_caps_known || !sm->remote_caps_known ||
+ !sm->cs_role_known) {
+ error("CS Set Proc Params: capabilities or role unknown");
+ return false;
+ }
+
+ initiator = sm->cs_role == CS_INITIATOR;
+ local_ant = sm->local_num_antennas;
+ peer_ant = sm->remote_num_antennas;
+ max_paths = MIN(sm->local_max_antenna_paths,
+ sm->remote_max_antenna_paths);
+ req = sm->cs_frequency.tone_antenna_config_selection;
+
+ for (i = 0; i < ARRAY_SIZE(cs_antenna_configs); i++) {
+ uint8_t need_local = initiator ?
+ cs_antenna_configs[i].init_ant :
+ cs_antenna_configs[i].refl_ant;
+ uint8_t need_peer = initiator ?
+ cs_antenna_configs[i].refl_ant :
+ cs_antenna_configs[i].init_ant;
+
+ if (cs_antenna_configs[i].paths > max_paths ||
+ need_local > local_ant || need_peer > peer_ant)
+ continue;
+
+ /* The requested configuration fits, so keep it as given */
+ if (cs_antenna_configs[i].aci == req) {
+ DBG("ACI 0x%02X fits both ends", req);
+ return true;
+ }
+
+ if (sel == ARRAY_SIZE(cs_antenna_configs)) {
+ sel = i;
+ sel_peer = need_peer;
+ }
+ }
+
+ /* The 1:1 entry needs one antenna on each side, so this only happens
+ * if a side reported none.
+ */
+ if (sel == ARRAY_SIZE(cs_antenna_configs)) {
+ error("No usable CS antenna configuration: local %u ant, "
+ "peer %u ant", local_ant, peer_ant);
+ return false;
+ }
+
+ sm->cs_frequency.tone_antenna_config_selection =
+ cs_antenna_configs[sel].aci;
+ /* Preferred_Peer_Antenna needs at least as many bits set as the peer
+ * has antenna elements in this configuration.
+ */
+ sm->cs_frequency.preferred_peer_antenna = (1 << sel_peer) - 1;
+
+ DBG("ACI 0x%02X does not fit, using 0x%02X: local %u ant, peer %u ant, "
+ "%u paths, peer mask 0x%02X, initiator=%d", req,
+ cs_antenna_configs[sel].aci, local_ant, peer_ant, max_paths,
+ sm->cs_frequency.preferred_peer_antenna, initiator);
+
+ return true;
+}
+
static bool rap_send_hci_cs_set_procedure_parameters(
struct cs_state_machine *sm, uint16_t handle)
{
@@ -635,6 +746,9 @@ static bool rap_send_hci_cs_set_procedure_parameters(
return false;
}
+ if (!cs_select_antenna_config(sm))
+ return false;
+
DBG("Sending CS Set Procedure Parameters for handle 0x%04X", handle);
memset(&cmd, 0, sizeof(cmd));
@@ -864,6 +978,16 @@ static void rap_rd_rmt_supp_cap_cmplt_evt(const void *data, uint8_t size,
subfeatures_supported = le16_to_cpu(evt->subfeatures_supported);
DBG("subfeatures_supported=0x%04X", subfeatures_supported);
+ /* Remember the peer's antenna count: the antenna configuration used in
+ * Set Procedure Parameters must fit both ends (Core v6.2 Vol 4 Part E
+ * 7.8.140).
+ */
+ sm->remote_num_antennas = evt->num_antennas_supported;
+ sm->remote_max_antenna_paths = evt->max_antenna_paths_supported;
+ sm->remote_caps_known = true;
+ DBG("remote antennas=%u paths=%u", sm->remote_num_antennas,
+ sm->remote_max_antenna_paths);
+
bt_rap_set_remote_sw_time(sm->rap, evt->t_sw_time_supported);
/* Check Bit 1 of subfeatures_supported (0x0002) */
@@ -1989,6 +2113,7 @@ void bt_rap_clear_conn_handle(void *hci_sm, uint16_t handle)
sm->procedure_active = false;
sm->cs_role_known = false;
sm->sec_enable_pending = false;
+ sm->remote_caps_known = false;
sm->current_state = CS_STATE_UNSPECIFIED;
}
--
2.43.0
^ permalink raw reply related [flat|nested] 18+ messages in thread* [PATCH BlueZ 14/15] profiles/ranging: Do not start a procedure after StopMeasurement()
2026-11-02 8:18 [PATCH BlueZ 00/15] Channel Sounding and RAP fixes Cheng Jiang
` (12 preceding siblings ...)
2026-11-02 8:19 ` [PATCH BlueZ 13/15] profiles/ranging: Choose the CS antenna configuration from both ends Cheng Jiang
@ 2026-11-02 8:19 ` Cheng Jiang
2026-11-02 8:19 ` [PATCH BlueZ 15/15] shared/rap: Start the Real-time timeout when the procedure starts Cheng Jiang
2026-11-03 20:38 ` [PATCH BlueZ 00/15] Channel Sounding and RAP fixes Luiz Augusto von Dentz
15 siblings, 0 replies; 18+ messages in thread
From: Cheng Jiang @ 2026-11-02 8:19 UTC (permalink / raw)
To: linux-bluetooth
Cc: jinwang.li, wei.deng, xiuzhuo.shang, shuai.zhang, Cheng Jiang
StopMeasurement() during the setup phase, or the measurement duration
expiring there, found no procedure to disable and returned without
touching the setup. The pending Set Default Settings response, Config
Complete and Security Enable Complete still arrived, and the Initiator
went on to create a configuration and enable a procedure that nobody
was waiting for any more.
Remember that a stop was requested until the next StartMeasurement(),
and while it is set do not send Create Config, and do not send Set
Procedure Parameters and Procedure Enable as the Initiator.
Assisted-by: Claude:claude-opus-5-5
---
profiles/ranging/rap_hci.c | 21 ++++++++++++++++++++-
1 file changed, 20 insertions(+), 1 deletion(-)
diff --git a/profiles/ranging/rap_hci.c b/profiles/ranging/rap_hci.c
index 4a1a2ad5e..04ea0d7a4 100644
--- a/profiles/ranging/rap_hci.c
+++ b/profiles/ranging/rap_hci.c
@@ -73,6 +73,7 @@ struct cs_state_machine {
unsigned int measurement_timeout_id;
bool initiator;
bool procedure_active;
+ bool stop_requested; /* StopMeasurement() since StartMeasurement() */
struct bt_rap_hci_cs_options cs_opt; /* Per-instance CS options */
uint8_t role_enable; /* Role value for HCI commands (1, 2, or 3) */
bool sec_enable_pending; /* Security Enable deferred until encrypted */
@@ -568,7 +569,9 @@ static void rap_def_settings_done_cb(const void *data, uint8_t size,
/* Only the Initiator creates the config; the Reflector waits
* for its own CS Config Complete.
*/
- if (cs_local_is_initiator(sm))
+ if (sm->stop_requested)
+ DBG("Measurement stopped during setup, no config");
+ else if (cs_local_is_initiator(sm))
rap_send_hci_cs_create_config_command(sm, rp->handle);
else
DBG("Reflector role: Waiting for CS Config Completed");
@@ -1252,6 +1255,15 @@ static void rap_cs_sec_enable_cmplt_evt(const void *data, uint8_t size,
/* Check if role is initiator */
if (sm->cs_role_known && sm->cs_role == CS_INITIATOR) {
+ /* StopMeasurement() during the setup phase had no
+ * procedure to disable, so do not enable one now.
+ */
+ if (sm->stop_requested) {
+ DBG("Measurement stopped during setup");
+ cs_set_state(sm, CS_STATE_STOPPED);
+ return;
+ }
+
DBG("Initiator role: Sending CS Set Procedure Params");
if (!rap_send_hci_cs_set_procedure_parameters(
sm, handle)) {
@@ -2010,6 +2022,7 @@ bool bt_rap_start_measurement(void *hci_sm, uint16_t conn_handle,
}
sm->active_conn_handle = conn_handle;
+ sm->stop_requested = false;
if (sm->measurement_timeout_id) {
timeout_remove(sm->measurement_timeout_id);
@@ -2033,6 +2046,11 @@ bool bt_rap_stop_measurement(void *hci_sm)
if (!sm || !sm->hci)
return false;
+ /* The setup phase may still be running, with nothing to disable yet:
+ * keep it from creating a config or enabling a procedure.
+ */
+ sm->stop_requested = true;
+
if (sm->measurement_timeout_id) {
timeout_remove(sm->measurement_timeout_id);
sm->measurement_timeout_id = 0;
@@ -2114,6 +2132,7 @@ void bt_rap_clear_conn_handle(void *hci_sm, uint16_t handle)
sm->cs_role_known = false;
sm->sec_enable_pending = false;
sm->remote_caps_known = false;
+ sm->stop_requested = false;
sm->current_state = CS_STATE_UNSPECIFIED;
}
--
2.43.0
^ permalink raw reply related [flat|nested] 18+ messages in thread* [PATCH BlueZ 15/15] shared/rap: Start the Real-time timeout when the procedure starts
2026-11-02 8:18 [PATCH BlueZ 00/15] Channel Sounding and RAP fixes Cheng Jiang
` (13 preceding siblings ...)
2026-11-02 8:19 ` [PATCH BlueZ 14/15] profiles/ranging: Do not start a procedure after StopMeasurement() Cheng Jiang
@ 2026-11-02 8:19 ` Cheng Jiang
2026-11-03 20:38 ` [PATCH BlueZ 00/15] Channel Sounding and RAP fixes Luiz Augusto von Dentz
15 siblings, 0 replies; 18+ messages in thread
From: Cheng Jiang @ 2026-11-02 8:19 UTC (permalink / raw)
To: linux-bluetooth
Cc: jinwang.li, wei.deng, xiuzhuo.shang, shuai.zhang, Cheng Jiang
RAP v1.0 4.4.1.1 requires the Requester to disable Real-time Ranging
Data if none arrives within 5 seconds after starting the CS procedure.
The timer was started when the CCCD was written instead, which can be
seconds before the procedure starts, since the CS configuration and
security setup come first. The CCCD could then be disabled before the
procedure had a chance to produce any data.
Start the timer from CS Procedure Enable Complete when the procedure
is enabled and Real-time Ranging Data is subscribed.
Assisted-by: Claude:claude-opus-5-5
---
src/shared/rap.c | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/src/shared/rap.c b/src/shared/rap.c
index 85cf8111f..cb44d73c9 100644
--- a/src/shared/rap.c
+++ b/src/shared/rap.c
@@ -413,6 +413,7 @@ struct ras_ccc_state {
static void ras_ondemand_client_reset(struct ras *ras);
static void ras_realtime_segments_clear(struct bt_rap *rap);
static void ras_ondemand_cache_clear(struct bt_rap *rap);
+static void ras_realtime_arm_timeout(struct bt_rap *rap, unsigned int timeout);
static bool ras_cp_write_op(struct bt_rap *rap, struct ras *ras,
uint8_t opcode, uint16_t counter);
@@ -3352,6 +3353,13 @@ void bt_rap_hci_cs_procedure_enable_complete_callback(uint16_t length,
/* Store procedure enable config in the session-level template */
reqtracker->bcs_proc_data.proc_enable_config = *data;
+
+ /* RAP v1.0 4.4.1.1: the first Real-time Ranging Data is due within
+ * 5 seconds after the CS procedure starts.
+ */
+ if (data->state == 0x01 && rap->realtime_notify_id)
+ ras_realtime_arm_timeout(rap,
+ RAS_REALTIME_FIRST_SEGMENT_TIMEOUT);
}
void bt_rap_hci_cs_sec_enable_complete_callback(uint16_t length,
@@ -4643,9 +4651,6 @@ static bool rap_subscribe_ranging_data(struct bt_rap *rap,
rap->realtime_notify_id = bt_rap_register_notify(rap,
value_handle, rap->realtime_ccc_value,
ras_segment_notify_cb, "real-time");
- if (rap->realtime_notify_id)
- ras_realtime_arm_timeout(rap,
- RAS_REALTIME_FIRST_SEGMENT_TIMEOUT);
return rap->realtime_notify_id != 0;
}
--
2.43.0
^ permalink raw reply related [flat|nested] 18+ messages in thread* Re: [PATCH BlueZ 00/15] Channel Sounding and RAP fixes
2026-11-02 8:18 [PATCH BlueZ 00/15] Channel Sounding and RAP fixes Cheng Jiang
` (14 preceding siblings ...)
2026-11-02 8:19 ` [PATCH BlueZ 15/15] shared/rap: Start the Real-time timeout when the procedure starts Cheng Jiang
@ 2026-11-03 20:38 ` Luiz Augusto von Dentz
15 siblings, 0 replies; 18+ messages in thread
From: Luiz Augusto von Dentz @ 2026-11-03 20:38 UTC (permalink / raw)
To: Cheng Jiang
Cc: linux-bluetooth, jinwang.li, wei.deng, xiuzhuo.shang, shuai.zhang
Hi Cheng,
On Mon, Nov 2, 2026 at 3:21 AM Cheng Jiang <cheng.jiang@oss.qualcomm.com> wrote:
>
> This series fixes problems found while running the RAP Requester (RREQ),
> RAS Server (RAS/SR) and GAP CS test cases against PTS. So far the
> Channel Sounding code had mostly been used between two BlueZ devices,
> with the local side creating the configuration as the Initiator. The
> test cases also cover the Reflector role, a configuration created by
> the peer, peers with different antenna counts and every Ranging Data
> mode, and most of the fixes are for those paths.
>
> The patches do not depend on how the CS session is created; they apply
> to the existing session set up from the RAP profile probe. Creating a
> session for a peer without the Ranging Service is a separate question,
> which I sent to the list earlier and is not part of this series.
>
> GATT database and RAS (patches 1-4 and 15)
> ==========================================
>
> 1. A device removed while disconnected kept its CCC state in the GATT
> database, and it was restored when the device paired again. Drop it
> from device_remove().
>
> 2. The Ranging Counters read from Ranging Data Ready and Ranging Data
> Overwritten are scoped to the connection (RAS v1.0 3.4 and 3.5),
> but were never cleared, so a new connection read the counter of the
> previous one. Clear them in bt_rap_detach().
>
> 3. On-demand Ranging Data was always subscribed with notifications,
> while the RAS Control Point, which only supports indications,
> followed the requested transport; with notifications no On-demand
> transfer could be requested. Follow RAS v1.0 Table 3.1: the
> requested transport for On-demand Ranging Data, indications for the
> Control Point, and indications for Ready and Overwritten whenever
> the server supports them.
>
> 4. Reading RAS Features subscribed to Real-time Ranging Data at
> connection time, when no procedure is running yet, so the 5 s timer
> disabled it again. Subscribe when StartMeasurement() is called
> instead, unless SetRangingDataMode() already did, and use On-demand
> when the peer lacks Real-time Ranging Data (RAP v1.0 4.4.2).
>
> 15. RAP v1.0 4.4.1.1 counts the 5 s Real-time timeout from the start of
> the CS procedure, but the timer started when the CCCD was written,
> possibly seconds earlier. Start it from CS Procedure Enable
> Complete.
>
> bluetoothctl and shared/hci (patches 5-6)
> =========================================
>
> 5. Fix a use-after-free in the StopMeasurement() reply of
> bluetoothctl, where the Active=false property change had already
> freed the session.
>
> 6. bt_hci_get_conn_handle() always listed the connections of hci0 and
> took the first link matching the address, which could be a BR/EDR
> ACL or an old link being torn down. Use the right controller index
> and match the link type; this adds LE_LINK to lib/bluetooth/hci.h,
> replacing the local copy in hcitool.
>
> CS role and setup in profiles/ranging (patches 7-14)
> ====================================================
>
> These depend on each other and are best applied in order.
>
> 7. StartMeasurement() returned early for a Reflector, so it never ran
> the setup phase and dropped every event the remote Initiator
> drove. Run the setup phase for the Reflector too, subscribe to
> Ranging Data as for an Initiator (the RAP roles are independent of
> the CS roles), and do not send Procedure Enable on stop when no
> procedure runs.
>
> 8. With Role_Enable permitting both roles, the GAP role decided which
> one was used. Since the setup phase only runs from
> StartMeasurement(), take the Initiator role, and tolerate the LL
> Procedure Collision that two simultaneous configuration requests
> can cause.
>
> 9. Config Complete overwrote the Role_Enable value with the negotiated
> role, so a second StartMeasurement() sent an invalid Role_Enable.
> Keep the two in separate fields, and do not send Procedure Enable
> from a Reflector.
>
> 10. Config Complete was only parsed in the state reached during a local
> setup, so a configuration created by the peer was dropped. Parse it
> in any state, ignore events for other links, and ignore a removal
> whose parameters are undefined.
>
> 11. The Set Default Settings response rewound a setup the peer had
> already advanced, and an Initiator then created a second
> configuration. Only advance from the initial state.
>
> 12. CS Security Enable needs an encrypted link (Core v6.2 Vol 4 Part E
> 7.8.133). When the configuration completed before encryption, the
> command was never sent. Defer it to the Encryption Change event.
>
> 13. The antenna configuration was always ACI 7 (2:2) unless the
> application set another, which a single-antenna Controller on
> either end rejects. Choose one that fits both ends' capabilities
> and the negotiated role (Core v6.2 Vol 6 Part A, Table 5.3).
>
> 14. A StopMeasurement() during the setup phase did not stop it, and the
> Initiator went on to create a configuration and enable a procedure.
> Remember the stop until the next StartMeasurement().
>
> Testing
> =======
>
> Each patch builds with --enable-maintainer-mode and unit/test-rap
> passes. The PTS runs (PTS 8.14.0, Qualcomm controller) used a
> BlueZ 5.86 based tree carrying these fixes.
We really need to start testing with test-functional though, since
unit/test-rap only test bt_rap itself, it is very tricky to do a
proper verification if these changes don't break anything since we
don't have any functional testing coverage yet.
> Cheng Jiang (15):
> gatt-database: Drop stale device state for removed bonds
> shared/rap: Reset the readable Ranging Counters per connection
> shared/rap: Subscribe on-demand data with the requested transport
> shared/rap: Subscribe to Ranging Data when a measurement starts
> client/cs: Fix use-after-free in stop_measurement_reply
> shared/hci: Look up the connection handle on the right link
> profiles/ranging: Let the Reflector run the CS setup phase
> profiles/ranging: Let StartMeasurement() pick the Initiator for Both
> profiles/ranging: Take the CS role from Config Complete
> profiles/ranging: Act on a CS configuration created by the peer
> profiles/ranging: Keep a peer-driven setup from being rewound
> profiles/ranging: Defer CS Security Enable until the link is encrypted
> profiles/ranging: Choose the CS antenna configuration from both ends
> profiles/ranging: Do not start a procedure after StopMeasurement()
> shared/rap: Start the Real-time timeout when the procedure starts
>
> client/cs.c | 40 +--
> doc/org.bluez.ChannelSounding1.rst | 21 +-
> lib/bluetooth/hci.h | 1 +
> profiles/ranging/rap.c | 16 +-
> profiles/ranging/rap_hci.c | 414 +++++++++++++++++++++++------
> src/device.c | 3 +
> src/gatt-database.c | 25 ++
> src/gatt-database.h | 2 +
> src/shared/hci.c | 12 +-
> src/shared/hci.h | 2 +-
> src/shared/rap.c | 82 +++++-
> src/shared/rap.h | 6 +-
> tools/hcitool.c | 3 -
> 13 files changed, 484 insertions(+), 143 deletions(-)
>
> --
> 2.43.0
>
>
--
Luiz Augusto von Dentz
^ permalink raw reply [flat|nested] 18+ messages in thread