Linux bluetooth development
 help / color / mirror / Atom feed
From: Naga Bhavani Akella <naga.akella@oss.qualcomm.com>
To: linux-bluetooth@vger.kernel.org
Cc: luiz.dentz@gmail.com, quic_mohamull@quicinc.com,
	quic_hbandi@quicinc.com, quic_anubhavg@quicinc.com,
	Naga Bhavani Akella <naga.akella@oss.qualcomm.com>
Subject: [PATCH BlueZ v2 4/4] client: Add Channel Sounding shell submenu
Date: Fri, 10 Jul 2026 15:19:19 +0530	[thread overview]
Message-ID: <20260710094919.4047204-5-naga.akella@oss.qualcomm.com> (raw)
In-Reply-To: <20260710094919.4047204-1-naga.akella@oss.qualcomm.com>

Add a cs bt_shell submenu for controlling
org.bluez.ChannelSounding1, supporting measurement start/stop,
default-setting configuration, and session status display.
Maintain reusable validated CS parameters and
track active sessions via D-Bus Active state,
replies, and disconnects.
Integrate with client/main.c to manage per-adapter
ChannelSounding proxies, device resolution,
property updates, and session cleanup.
---
 Makefile.tools |    3 +-
 client/cs.c    | 1157 ++++++++++++++++++++++++++++++++++++++++++++++++
 client/cs.h    |   18 +
 client/main.c  |   27 +-
 4 files changed, 1203 insertions(+), 2 deletions(-)
 create mode 100644 client/cs.c
 create mode 100644 client/cs.h

diff --git a/Makefile.tools b/Makefile.tools
index 6188449f1..b5f7ef8da 100644
--- a/Makefile.tools
+++ b/Makefile.tools
@@ -16,7 +16,8 @@ client_bluetoothctl_SOURCES = client/main.c \
 					client/mgmt.h client/mgmt.c \
 					client/assistant.h client/assistant.c \
 					client/hci.h client/hci.c \
-					client/telephony.h client/telephony.c
+					client/telephony.h client/telephony.c \
+					client/cs.h client/cs.c
 client_bluetoothctl_LDADD = lib/libbluetooth-internal.la \
 			gdbus/libgdbus-internal.la src/libshared-glib.la \
 			$(GLIB_LIBS) $(DBUS_LIBS) -lreadline
diff --git a/client/cs.c b/client/cs.c
new file mode 100644
index 000000000..89251b502
--- /dev/null
+++ b/client/cs.c
@@ -0,0 +1,1157 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ *  BlueZ - Bluetooth protocol stack for Linux
+ *
+ * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <string.h>
+
+#include <glib.h>
+
+#include "gdbus/gdbus.h"
+#include "src/shared/shell.h"
+#include "src/shared/util.h"
+#include "cs.h"
+
+static GList * cs_proxies;
+static GList **cs_device_list;
+static DBusConnection *dbus_conn;
+
+#define RANGING_INTERFACE "org.bluez.ChannelSoundingRanging1"
+#define RANGE_ESTIMATE     "RangeEstimate"
+
+/* One signal watch per device proxy, so each device's RangeEstimate
+ * watch can be independently added/removed instead of clobbering a
+ * single shared handle.
+ */
+struct cs_watch {
+	GDBusProxy *proxy;
+	guint watch_id;
+};
+
+static GList *cs_watches;
+
+/* ---- Per-device active session ---- */
+
+struct cs_session {
+	GDBusProxy *proxy;
+};
+
+static GList *cs_sessions;
+
+static struct cs_session *cs_find_session(GDBusProxy *proxy);
+
+/* ---- Parameter state (used to build the StartMeasurement dict) ---- */
+
+static uint8_t cs_role        = 0x03;
+static uint8_t cs_sync_ant    = 0xFF;
+static int8_t  cs_max_tx_power = 0x14;
+
+static struct {
+	uint8_t config_id;
+	uint8_t main_mode_type;
+	uint8_t sub_mode_type;
+	uint8_t main_mode_min_steps;
+	uint8_t main_mode_max_steps;
+	uint8_t main_mode_repetition;
+	uint8_t mode0_steps;
+	uint8_t role;
+	uint8_t rtt_types;
+	uint8_t cs_sync_phy;
+	uint8_t channel_map[10];
+	uint8_t channel_map_repetition;
+	uint8_t channel_selection_type;
+	uint8_t channel_shape;
+	uint8_t channel_jump;
+	uint8_t companion_signal_enable;
+} cs_cfg = {
+	.config_id               = 0x00,
+	.main_mode_type          = 0x01,
+	.sub_mode_type           = 0xFF,
+	.main_mode_min_steps     = 0x02,
+	.main_mode_max_steps     = 0x03,
+	.main_mode_repetition    = 0x01,
+	.mode0_steps             = 0x02,
+	.role                    = 0x00,
+	.rtt_types               = 0x00,
+	.cs_sync_phy             = 0x01,
+	.channel_map             = { 0xFC, 0xFF, 0x7F, 0xFC, 0xFF,
+				     0xFF, 0xFF, 0xFF, 0xFF, 0x1F },
+	.channel_map_repetition  = 0x01,
+	.channel_selection_type  = 0x00,
+	.channel_shape           = 0x00,
+	.channel_jump            = 0x02,
+	.companion_signal_enable = 0x00,
+};
+
+static struct {
+	uint16_t max_procedure_duration;
+	uint16_t min_period_between_procedures;
+	uint16_t max_period_between_procedures;
+	uint16_t max_procedure_count;
+	uint8_t  min_sub_event_len[3];
+	uint8_t  max_sub_event_len[3];
+	uint8_t  tone_antenna_config_selection;
+	uint8_t  phy;
+	uint8_t  tx_power_delta;
+	uint8_t  preferred_peer_antenna;
+	uint8_t  snr_control_initiator;
+	uint8_t  snr_control_reflector;
+} cs_freq = {
+	.max_procedure_duration        = 0x0640,
+	.min_period_between_procedures = 0x001E,
+	.max_period_between_procedures = 0x0096,
+	.max_procedure_count           = 0x0000,
+	.min_sub_event_len             = { 0x00, 0x20, 0x00 },
+	.max_sub_event_len             = { 0x03, 0x20, 0x00 },
+	.tone_antenna_config_selection = 0x07,
+	.phy                           = 0x01,
+	.tx_power_delta                = 0x80,
+	.preferred_peer_antenna        = 0x03,
+	.snr_control_initiator         = 0xFF,
+	.snr_control_reflector         = 0xFF,
+};
+
+enum cs_param_type {
+	CS_PARAM_U8,
+	CS_PARAM_U8_RANGE,
+	CS_PARAM_U16,
+};
+
+struct cs_param_desc {
+	const char *name;
+	enum cs_param_type type;
+	void *field;
+	unsigned long min;
+	unsigned long max;
+	const char *range_error;
+};
+
+static const struct cs_param_desc cs_param_table[] = {
+	{ .name = "sync_ant_sel", .type = CS_PARAM_U8, .field = &cs_sync_ant },
+	{ .name = "config_id", .type = CS_PARAM_U8,
+		.field = &cs_cfg.config_id },
+	{ .name = "main_mode_type", .type = CS_PARAM_U8,
+		.field = &cs_cfg.main_mode_type },
+	{ .name = "sub_mode_type", .type = CS_PARAM_U8,
+		.field = &cs_cfg.sub_mode_type },
+	{ .name = "main_mode_min_steps", .type = CS_PARAM_U8,
+		.field = &cs_cfg.main_mode_min_steps },
+	{ .name = "main_mode_max_steps", .type = CS_PARAM_U8,
+		.field = &cs_cfg.main_mode_max_steps },
+	{ .name = "main_mode_repetition", .type = CS_PARAM_U8,
+		.field = &cs_cfg.main_mode_repetition },
+	{ .name = "mode0_steps", .type = CS_PARAM_U8,
+		.field = &cs_cfg.mode0_steps },
+	{ .name = "rtt_types", .type = CS_PARAM_U8,
+		.field = &cs_cfg.rtt_types },
+	{ .name = "role", .type = CS_PARAM_U8_RANGE, .field = &cs_role,
+		.min = 0x01, .max = 0x03,
+		.range_error = "role: 01=Initiator 02=Reflector 03=Both\n" },
+	{ .name = "sync_phy", .type = CS_PARAM_U8_RANGE,
+		.field = &cs_cfg.cs_sync_phy, .min = 0x01, .max = 0x02,
+		.range_error = "sync_phy: 0x01=LE 1M 0x02=LE 2M\n" },
+	{ .name = "channel_map_repetition", .type = CS_PARAM_U8,
+		.field = &cs_cfg.channel_map_repetition },
+	{ .name = "channel_selection_type", .type = CS_PARAM_U8,
+		.field = &cs_cfg.channel_selection_type },
+	{ .name = "channel_shape", .type = CS_PARAM_U8,
+		.field = &cs_cfg.channel_shape },
+	{ .name = "channel_jump", .type = CS_PARAM_U8,
+		.field = &cs_cfg.channel_jump },
+	{ .name = "companion_signal_enable", .type = CS_PARAM_U8_RANGE,
+		.field = &cs_cfg.companion_signal_enable, .min = 0, .max = 1,
+		.range_error = "companion_signal_enable: 0 or 1\n" },
+	{ .name = "max_procedure_duration", .type = CS_PARAM_U16,
+		.field = &cs_freq.max_procedure_duration },
+	{ .name = "min_period_between_procedures", .type = CS_PARAM_U16,
+		.field = &cs_freq.min_period_between_procedures },
+	{ .name = "max_period_between_procedures", .type = CS_PARAM_U16,
+		.field = &cs_freq.max_period_between_procedures },
+	{ .name = "max_procedure_count", .type = CS_PARAM_U16,
+		.field = &cs_freq.max_procedure_count },
+	{ .name = "tone_antenna_config_selection", .type = CS_PARAM_U8,
+		.field = &cs_freq.tone_antenna_config_selection },
+	{ .name = "phy", .type = CS_PARAM_U8_RANGE, .field = &cs_freq.phy,
+		.min = 0x01, .max = 0x02,
+		.range_error = "phy: 0x01=LE 1M  0x02=LE 2M\n" },
+	{ .name = "tx_power_delta", .type = CS_PARAM_U8,
+		.field = &cs_freq.tx_power_delta },
+	{ .name = "preferred_peer_antenna", .type = CS_PARAM_U8,
+		.field = &cs_freq.preferred_peer_antenna },
+	{ .name = "snr_control_initiator", .type = CS_PARAM_U8,
+		.field = &cs_freq.snr_control_initiator },
+	{ .name = "snr_control_reflector", .type = CS_PARAM_U8,
+		.field = &cs_freq.snr_control_reflector },
+};
+
+static gboolean range_estimate_cb(DBusConnection *connection,
+					DBusMessage *msg, void *user_data)
+{
+	GDBusProxy *proxy = user_data;
+	DBusMessageIter iter;
+	double distance;
+	uint8_t confidence;
+
+	if (!dbus_message_iter_init(msg, &iter))
+		return TRUE;
+
+	if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_DOUBLE)
+		return TRUE;
+
+	dbus_message_iter_get_basic(&iter, &distance);
+	dbus_message_iter_next(&iter);
+
+	if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_BYTE)
+		return TRUE;
+
+	dbus_message_iter_get_basic(&iter, &confidence);
+
+	bt_shell_printf("[CS] %s Distance: %.3f m  Confidence: %u%%\n",
+			g_dbus_proxy_get_path(proxy), distance, confidence);
+
+	return TRUE;
+}
+
+static struct cs_watch *cs_find_watch(GDBusProxy *proxy)
+{
+	struct cs_watch *w;
+	GList *list;
+
+	for (list = g_list_first(cs_watches); list; list = g_list_next(list)) {
+		w = list->data;
+
+		if (w->proxy == proxy)
+			return w;
+	}
+
+	return NULL;
+}
+
+void cs_proxy_added(GDBusProxy *proxy)
+{
+	struct cs_watch *w;
+
+	cs_proxies = g_list_append(cs_proxies, proxy);
+
+	if (!dbus_conn)
+		dbus_conn = bt_shell_get_env("DBUS_CONNECTION");
+
+	if (!dbus_conn)
+		return;
+
+	w = g_new0(struct cs_watch, 1);
+	w->proxy = proxy;
+	w->watch_id = g_dbus_add_signal_watch(dbus_conn, NULL,
+					g_dbus_proxy_get_path(proxy),
+					RANGING_INTERFACE, RANGE_ESTIMATE,
+					range_estimate_cb, proxy, NULL);
+	cs_watches = g_list_append(cs_watches, w);
+}
+
+void cs_proxy_removed(GDBusProxy *proxy)
+{
+	struct cs_session *s;
+	struct cs_watch *w;
+	GList *list;
+
+	cs_proxies = g_list_remove(cs_proxies, proxy);
+
+	for (list = g_list_first(cs_sessions); list;
+	     list = g_list_next(list)) {
+		s = list->data;
+
+		if (s->proxy == proxy) {
+			cs_sessions = g_list_remove(cs_sessions, s);
+			g_free(s);
+			break;
+		}
+	}
+
+	w = cs_find_watch(proxy);
+	if (w) {
+		g_dbus_remove_watch(dbus_conn, w->watch_id);
+		cs_watches = g_list_remove(cs_watches, w);
+		g_free(w);
+	}
+}
+
+/* Drop any active CS session for a device proxy path on disconnect. */
+void cs_device_disconnected(const char *dev_path)
+{
+	struct cs_session *s;
+	GList *list;
+
+	for (list = g_list_first(cs_sessions); list;
+	     list = g_list_next(list)) {
+		s = list->data;
+
+		if (!strcmp(g_dbus_proxy_get_path(s->proxy), dev_path)) {
+			bt_shell_printf("Measurement stopped (disconnected)"
+					" on %s\n", dev_path);
+			cs_sessions = g_list_remove(cs_sessions, s);
+			g_free(s);
+			break;
+		}
+	}
+}
+
+/* Drop active CS session when the daemon reports the measurement stopped. */
+void cs_measurement_stopped(GDBusProxy *proxy)
+{
+	struct cs_session *s = cs_find_session(proxy);
+
+	if (!s)
+		return;
+
+	bt_shell_printf("Measurement stopped (timer expired) on %s\n",
+			g_dbus_proxy_get_path(proxy));
+	cs_sessions = g_list_remove(cs_sessions, s);
+	g_free(s);
+}
+
+/* Record a measurement started by the remote side (Reflector role). */
+void cs_measurement_started(GDBusProxy *proxy)
+{
+	struct cs_session *s;
+
+	if (cs_find_session(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));
+}
+
+void cs_set_device_list(GList **devices)
+{
+	cs_device_list = devices;
+}
+
+/* Resolve a Bluetooth address to the device's D-Bus object path. */
+static const char *cs_resolve_address(const char *address)
+{
+	GDBusProxy *proxy;
+	DBusMessageIter iter;
+	const char *str;
+	GList *list;
+
+	if (!cs_device_list) {
+		bt_shell_printf("DBG: cs_device_list is NULL\n");
+		return NULL;
+	}
+
+	if (!*cs_device_list) {
+		bt_shell_printf("DBG: *cs_device_list is empty\n");
+		return NULL;
+	}
+
+	for (list = g_list_first(*cs_device_list); list;
+	     list = g_list_next(list)) {
+		proxy = list->data;
+
+		if (!g_dbus_proxy_get_property(proxy, "Address", &iter)) {
+			bt_shell_printf("DBG: proxy %s has no Address\n",
+					g_dbus_proxy_get_path(proxy));
+			continue;
+		}
+
+		dbus_message_iter_get_basic(&iter, &str);
+
+		if (!strcasecmp(str, address))
+			return g_dbus_proxy_get_path(proxy);
+	}
+
+	return NULL;
+}
+
+/* Find the ChannelSounding1 proxy for the device at the given object path. */
+static GDBusProxy *cs_find_proxy(const char *dev_path)
+{
+	GDBusProxy *proxy;
+	const char *path;
+	GList *list;
+
+	if (!cs_proxies) {
+		bt_shell_printf("DBG: cs_proxies is empty\n");
+		return NULL;
+	}
+
+	for (list = g_list_first(cs_proxies); list;
+	     list = g_list_next(list)) {
+		proxy = list->data;
+		path = g_dbus_proxy_get_path(proxy);
+
+		if (!strcmp(path, dev_path))
+			return proxy;
+	}
+
+	return NULL;
+}
+
+static struct cs_session *cs_find_session(GDBusProxy *proxy)
+{
+	struct cs_session *s;
+	GList *list;
+
+	for (list = g_list_first(cs_sessions); list;
+	     list = g_list_next(list)) {
+		s = list->data;
+
+		if (s->proxy == proxy)
+			return s;
+	}
+
+	return NULL;
+}
+
+/* ---- dict helpers ---- */
+
+static void dict_append_byte(DBusMessageIter *dict, const char *key,
+			     uint8_t val)
+{
+	DBusMessageIter entry, variant;
+
+	dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY,
+					 NULL, &entry);
+	dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &key);
+	dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT,
+					 DBUS_TYPE_BYTE_AS_STRING, &variant);
+	dbus_message_iter_append_basic(&variant, DBUS_TYPE_BYTE, &val);
+	dbus_message_iter_close_container(&entry, &variant);
+	dbus_message_iter_close_container(dict, &entry);
+}
+
+static void dict_append_uint16(DBusMessageIter *dict, const char *key,
+			       uint16_t val)
+{
+	DBusMessageIter entry, variant;
+
+	dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY,
+					 NULL, &entry);
+	dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &key);
+	dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT,
+					 "q", &variant);
+	dbus_message_iter_append_basic(&variant, DBUS_TYPE_UINT16, &val);
+	dbus_message_iter_close_container(&entry, &variant);
+	dbus_message_iter_close_container(dict, &entry);
+}
+
+static void dict_append_uint32(DBusMessageIter *dict, const char *key,
+			       uint32_t val)
+{
+	DBusMessageIter entry, variant;
+
+	dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY,
+					 NULL, &entry);
+	dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &key);
+	dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT,
+					 "u", &variant);
+	dbus_message_iter_append_basic(&variant, DBUS_TYPE_UINT32, &val);
+	dbus_message_iter_close_container(&entry, &variant);
+	dbus_message_iter_close_container(dict, &entry);
+}
+
+static void dict_append_byte_array(DBusMessageIter *dict, const char *key,
+				   const uint8_t *bytes, int len)
+{
+	DBusMessageIter entry, variant, array;
+
+	dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY,
+					 NULL, &entry);
+	dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &key);
+	dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT,
+					 "ay", &variant);
+	dbus_message_iter_open_container(&variant, DBUS_TYPE_ARRAY,
+					 DBUS_TYPE_BYTE_AS_STRING, &array);
+	dbus_message_iter_append_fixed_array(&array, DBUS_TYPE_BYTE,
+					     &bytes, len);
+	dbus_message_iter_close_container(&variant, &array);
+	dbus_message_iter_close_container(&entry, &variant);
+	dbus_message_iter_close_container(dict, &entry);
+}
+
+/* ---- start ---- */
+
+static uint32_t pending_duration_secs;
+static const char *pending_dev_path;
+
+static void start_measurement_setup(DBusMessageIter *iter, void *user_data)
+{
+	DBusMessageIter dict;
+	uint8_t tx = (uint8_t)cs_max_tx_power;
+
+	dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY,
+					 "{sv}", &dict);
+
+	/* timing */
+	dict_append_uint32(&dict, "duration_secs", pending_duration_secs);
+
+	/* default settings */
+	dict_append_byte(&dict, "role",            cs_role);
+	dict_append_byte(&dict, "sync_ant_sel", cs_sync_ant);
+	dict_append_byte(&dict, "max_tx_power",    tx);
+
+	/* CS config */
+	dict_append_byte(&dict, "config_id",
+			 cs_cfg.config_id);
+	dict_append_byte(&dict, "main_mode_type",
+			 cs_cfg.main_mode_type);
+	dict_append_byte(&dict, "sub_mode_type",
+			 cs_cfg.sub_mode_type);
+	dict_append_byte(&dict, "main_mode_min_steps",
+			 cs_cfg.main_mode_min_steps);
+	dict_append_byte(&dict, "main_mode_max_steps",
+			 cs_cfg.main_mode_max_steps);
+	dict_append_byte(&dict, "main_mode_repetition",
+			 cs_cfg.main_mode_repetition);
+	dict_append_byte(&dict, "mode0_steps",
+			 cs_cfg.mode0_steps);
+	dict_append_byte(&dict, "rtt_types",
+			 cs_cfg.rtt_types);
+	dict_append_byte(&dict, "sync_phy",
+			 cs_cfg.cs_sync_phy);
+	dict_append_byte_array(&dict, "channel_map",
+			       cs_cfg.channel_map, 10);
+	dict_append_byte(&dict, "channel_map_repetition",
+			 cs_cfg.channel_map_repetition);
+	dict_append_byte(&dict, "channel_selection_type",
+			 cs_cfg.channel_selection_type);
+	dict_append_byte(&dict, "channel_shape",
+			 cs_cfg.channel_shape);
+	dict_append_byte(&dict, "channel_jump",
+			 cs_cfg.channel_jump);
+	dict_append_byte(&dict, "companion_signal_enable",
+			 cs_cfg.companion_signal_enable);
+
+	/* CS frequency */
+	dict_append_uint16(&dict, "max_procedure_duration",
+			   cs_freq.max_procedure_duration);
+	dict_append_uint16(&dict, "min_period_between_procedures",
+			   cs_freq.min_period_between_procedures);
+	dict_append_uint16(&dict, "max_period_between_procedures",
+			   cs_freq.max_period_between_procedures);
+	dict_append_uint16(&dict, "max_procedure_count",
+			   cs_freq.max_procedure_count);
+	dict_append_byte_array(&dict, "min_sub_event_len",
+			       cs_freq.min_sub_event_len, 3);
+	dict_append_byte_array(&dict, "max_sub_event_len",
+			       cs_freq.max_sub_event_len, 3);
+	dict_append_byte(&dict, "tone_antenna_config_selection",
+			 cs_freq.tone_antenna_config_selection);
+	dict_append_byte(&dict, "phy",
+			 cs_freq.phy);
+	dict_append_byte(&dict, "tx_power_delta",
+			 cs_freq.tx_power_delta);
+	dict_append_byte(&dict, "preferred_peer_antenna",
+			 cs_freq.preferred_peer_antenna);
+	dict_append_byte(&dict, "snr_control_initiator",
+			 cs_freq.snr_control_initiator);
+	dict_append_byte(&dict, "snr_control_reflector",
+			 cs_freq.snr_control_reflector);
+
+	dbus_message_iter_close_container(iter, &dict);
+}
+
+/* ---- cs_set_param: apply name=value to the local parameter state ---- */
+static bool parse_hex_bytes(const char *name, const char *valstr,
+					uint8_t *out, unsigned int count)
+{
+	const char *str = valstr;
+	char *endptr;
+	unsigned long uval;
+	unsigned int i = 0;
+
+	while (i < count) {
+		uval = strtoul(str, &endptr, 16);
+		if (endptr == str || uval > 0xFF) {
+			bt_shell_printf("%s: invalid byte at index %u\n",
+					name, i);
+			return false;
+		}
+		out[i++] = (uint8_t)uval;
+		if (*endptr == '\0')
+			break;
+		if (*endptr != ':') {
+			bt_shell_printf("%s: expected ':' separator\n", name);
+			return false;
+		}
+		str = endptr + 1;
+	}
+
+	if (i != count) {
+		bt_shell_printf("%s: need exactly %u colon-separated hex"
+				" bytes\n", name, count);
+		return false;
+	}
+
+	return true;
+}
+
+static const struct cs_param_desc *cs_find_param_desc(const char *name)
+{
+	size_t i;
+
+	for (i = 0; i < ARRAY_SIZE(cs_param_table); i++) {
+		if (!strcmp(cs_param_table[i].name, name))
+			return &cs_param_table[i];
+	}
+
+	return NULL;
+}
+
+static bool cs_set_param(const char *name, const char *valstr)
+{
+	const struct cs_param_desc *desc;
+	char *endptr;
+	unsigned long uval;
+	long sval;
+
+	/* Byte-array: channel_map — 10 colon-separated hex bytes */
+	if (strcmp(name, "channel_map") == 0) {
+		if (!parse_hex_bytes(name, valstr, cs_cfg.channel_map, 10))
+			return false;
+		return true;
+	}
+
+	/* Byte-array: min/max_sub_event_len — 3 colon-separated hex bytes */
+	if (strcmp(name, "min_sub_event_len") == 0)
+		return parse_hex_bytes(name, valstr,
+					cs_freq.min_sub_event_len, 3);
+	if (strcmp(name, "max_sub_event_len") == 0)
+		return parse_hex_bytes(name, valstr,
+					cs_freq.max_sub_event_len, 3);
+
+	/* max_tx_power is signed (-127 to +20 dBm) */
+	if (strcmp(name, "max_tx_power") == 0) {
+		sval = strtol(valstr, &endptr, 0);
+
+		if (*endptr != '\0' || sval < -127 || sval > 20) {
+			bt_shell_printf("max_tx_power: valid range -127"
+					" to 20 dBm\n");
+			return false;
+		}
+		cs_max_tx_power = (int8_t)sval;
+		return true;
+	}
+
+	desc = cs_find_param_desc(name);
+	if (!desc) {
+		bt_shell_printf("Unknown parameter: %s\n", name);
+		return false;
+	}
+
+	/* All remaining parameters are unsigned */
+	uval = strtoul(valstr, &endptr, 0);
+	if (*endptr != '\0') {
+		bt_shell_printf("%s: invalid value '%s'\n", name, valstr);
+		return false;
+	}
+
+	if (desc->type == CS_PARAM_U8_RANGE &&
+			(uval < desc->min || uval > desc->max)) {
+		bt_shell_printf("%s", desc->range_error);
+		return false;
+	}
+
+	if (desc->type == CS_PARAM_U16)
+		*(uint16_t *)desc->field = (uint16_t)uval;
+	else
+		*(uint8_t *)desc->field = (uint8_t)uval;
+
+	return true;
+}
+
+static void start_measurement_reply(DBusMessage *message, void *user_data)
+{
+	GDBusProxy *proxy = user_data;
+	struct cs_session *s;
+	DBusError error;
+
+	dbus_error_init(&error);
+	if (dbus_set_error_from_message(&error, message)) {
+		bt_shell_printf("StartMeasurement failed: %s\n", error.message);
+		dbus_error_free(&error);
+		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));
+}
+
+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");
+	else
+		bt_shell_printf("Note: Initiator role requires dev_addr"
+				" and duration_secs; CS config/frequency"
+				" parameters configure the measurement"
+				" procedure.\n");
+}
+
+/* Generic handler for the per-parameter set commands (cs.role,
+ * cs.main_mode_type, ...); argv[0] is the parameter name (the command
+ * itself), argv[1] is the value.
+ */
+static void cmd_cs_set(int argc, char *argv[])
+{
+	if (argc < 2) {
+		bt_shell_printf("Usage: %s <value>\n", argv[0]);
+		return;
+	}
+
+	if (!cs_set_param(argv[0], argv[1]))
+		return;
+
+	if (!strcmp(argv[0], "role"))
+		cs_print_role_hint(cs_role);
+}
+
+/* Tab completion for params whose only legal values are a small fixed
+ * enum, so users don't need to consult documentation to know what's
+ * valid.
+ */
+static char *cs_value_generator(const char *text, int state,
+					const char * const values[])
+{
+	static unsigned int index, len;
+	const char *value;
+
+	if (!state) {
+		index = 0;
+		len = strlen(text);
+	}
+
+	while ((value = values[index])) {
+		index++;
+
+		if (!strncmp(value, text, len))
+			return strdup(value);
+	}
+
+	return NULL;
+}
+
+static const char * const cs_role_values[] = { "0x01", "0x02", "0x03", NULL };
+
+static char *cs_role_generator(const char *text, int state)
+{
+	return cs_value_generator(text, state, cs_role_values);
+}
+
+static const char * const cs_main_mode_type_values[] = { "1", "2", "3", NULL };
+
+static char *cs_main_mode_type_generator(const char *text, int state)
+{
+	return cs_value_generator(text, state, cs_main_mode_type_values);
+}
+
+static const char * const cs_phy_values[] = { "0x01", "0x02", NULL };
+
+static char *cs_phy_generator(const char *text, int state)
+{
+	return cs_value_generator(text, state, cs_phy_values);
+}
+
+static const char * const cs_bool_values[] = { "0", "1", NULL };
+
+static char *cs_bool_generator(const char *text, int state)
+{
+	return cs_value_generator(text, state, cs_bool_values);
+}
+
+static const char * const cs_sub_mode_type_values[] = { "0x01", "0x02",
+							"0x03", "0xFF", NULL };
+
+static char *cs_sub_mode_type_generator(const char *text, int state)
+{
+	return cs_value_generator(text, state, cs_sub_mode_type_values);
+}
+
+static void cmd_cs_start(int argc, char *argv[])
+{
+	GDBusProxy *proxy;
+	char *endptr;
+	unsigned long val;
+
+	pending_duration_secs = 0;
+	pending_dev_path = NULL;
+
+	if (argc >= 2) {
+		pending_dev_path = cs_resolve_address(argv[1]);
+		if (!pending_dev_path) {
+			bt_shell_printf("Device %s not found\n", argv[1]);
+			return;
+		}
+	}
+
+	if (argc >= 3) {
+		val = strtoul(argv[2], &endptr, 0);
+
+		if (*endptr != '\0') {
+			bt_shell_printf("Invalid duration: %s\n", argv[2]);
+			return;
+		}
+		pending_duration_secs = (uint32_t)val;
+	}
+
+	if (argc > 3) {
+		bt_shell_printf("Too many arguments\n");
+		return;
+	}
+
+	cs_print_role_hint(cs_role);
+
+	if (pending_dev_path) {
+		proxy = cs_find_proxy(pending_dev_path);
+		if (!proxy) {
+			bt_shell_printf("No ChannelSounding1 interface for"
+					" that device\n");
+			return;
+		}
+	} else {
+		if (!cs_proxies) {
+			bt_shell_printf("No ChannelSounding1 interface"
+					" available\n");
+			return;
+		}
+		proxy = cs_proxies->data;
+	}
+
+	if (cs_find_session(proxy)) {
+		bt_shell_printf("Measurement already active on %s\n",
+				g_dbus_proxy_get_path(proxy));
+		return;
+	}
+
+	if (!g_dbus_proxy_method_call(proxy, "StartMeasurement",
+				      start_measurement_setup,
+				      start_measurement_reply, proxy, NULL))
+		bt_shell_printf("Failed to send StartMeasurement\n");
+}
+
+/* ---- stop ---- */
+
+static void stop_measurement_reply(DBusMessage *message, void *user_data)
+{
+	struct cs_session *s = user_data;
+	DBusError error;
+
+	dbus_error_init(&error);
+	if (dbus_set_error_from_message(&error, message)) {
+		bt_shell_printf("StopMeasurement failed: %s\n", error.message);
+		dbus_error_free(&error);
+		return;
+	}
+
+	bt_shell_printf("Measurement stopped on %s\n",
+			g_dbus_proxy_get_path(s->proxy));
+	cs_sessions = g_list_remove(cs_sessions, s);
+	g_free(s);
+}
+
+static void cmd_cs_stop(int argc, char *argv[])
+{
+	const char *dev_path;
+	struct cs_session *s;
+	GDBusProxy *proxy;
+
+	if (argc >= 2) {
+		dev_path = cs_resolve_address(argv[1]);
+
+		if (!dev_path) {
+			bt_shell_printf("Device %s not found\n", argv[1]);
+			return;
+		}
+		proxy = cs_find_proxy(dev_path);
+		if (!proxy) {
+			bt_shell_printf("No ChannelSounding1 interface for"
+					" that device\n");
+			return;
+		}
+		s = cs_find_session(proxy);
+		if (!s) {
+			bt_shell_printf("No active measurement on %s\n",
+					argv[1]);
+			return;
+		}
+	} else {
+		if (!cs_sessions) {
+			bt_shell_printf("No active measurements\n");
+			return;
+		}
+		if (g_list_length(cs_sessions) > 1) {
+			bt_shell_printf("Multiple active measurements —"
+					" specify device address\n");
+			return;
+		}
+		s = cs_sessions->data;
+	}
+
+	if (!g_dbus_proxy_method_call(s->proxy, "StopMeasurement",
+				      NULL,
+				      stop_measurement_reply, s, NULL))
+		bt_shell_printf("Failed to send StopMeasurement\n");
+}
+
+/* ---- show ---- */
+
+static void cmd_cs_show(int argc, char *argv[])
+{
+	struct cs_session *s;
+	size_t j;
+	GList *list;
+
+	bt_shell_printf("Active measurements:\n");
+	if (!cs_sessions) {
+		bt_shell_printf("  none\n");
+	} else {
+		for (list = g_list_first(cs_sessions); list;
+		     list = g_list_next(list)) {
+			s = list->data;
+
+			bt_shell_printf("  %s\n",
+					g_dbus_proxy_get_path(s->proxy));
+		}
+	}
+
+	bt_shell_printf("\n=== Default Settings ===\n");
+	bt_shell_printf("  role           : %u"
+			" (1=Initiator 2=Reflector 3=Both)\n", cs_role);
+	bt_shell_printf("  sync_ant_sel   : 0x%02x\n", cs_sync_ant);
+	bt_shell_printf("  max_tx_power   : %d dBm\n", cs_max_tx_power);
+
+	bt_shell_printf("\n=== CS Config Params ===\n");
+	bt_shell_printf("  config_id               : %u\n",
+			cs_cfg.config_id);
+	bt_shell_printf("  main_mode_type          : 0x%02x\n",
+			cs_cfg.main_mode_type);
+	bt_shell_printf("  sub_mode_type           : 0x%02x\n",
+			cs_cfg.sub_mode_type);
+	bt_shell_printf("  main_mode_min_steps     : %u\n",
+			cs_cfg.main_mode_min_steps);
+	bt_shell_printf("  main_mode_max_steps     : %u\n",
+			cs_cfg.main_mode_max_steps);
+	bt_shell_printf("  main_mode_repetition    : %u\n",
+			cs_cfg.main_mode_repetition);
+	bt_shell_printf("  mode0_steps             : %u\n",
+			cs_cfg.mode0_steps);
+	bt_shell_printf("  configured role         : %u\n",
+			cs_cfg.role);
+	bt_shell_printf("  rtt_types               : %u\n",
+			cs_cfg.rtt_types);
+	bt_shell_printf("  sync_phy                : %u\n",
+			cs_cfg.cs_sync_phy);
+	bt_shell_printf("  channel_map             :");
+	for (j = 0; j < 10; j++)
+		bt_shell_printf(" %02x", cs_cfg.channel_map[j]);
+	bt_shell_printf("\n");
+	bt_shell_printf("  channel_map_repetition  : %u\n",
+			cs_cfg.channel_map_repetition);
+	bt_shell_printf("  channel_selection_type  : %u\n",
+			cs_cfg.channel_selection_type);
+	bt_shell_printf("  channel_shape           : %u\n",
+			cs_cfg.channel_shape);
+	bt_shell_printf("  channel_jump            : %u\n",
+			cs_cfg.channel_jump);
+	bt_shell_printf("  companion_signal_enable : %u\n",
+			cs_cfg.companion_signal_enable);
+
+	bt_shell_printf("\n=== CS Frequency Params ===\n");
+	bt_shell_printf("  max_procedure_duration        : %u\n",
+			cs_freq.max_procedure_duration);
+	bt_shell_printf("  min_period_between_procedures : %u\n",
+			cs_freq.min_period_between_procedures);
+	bt_shell_printf("  max_period_between_procedures : %u\n",
+			cs_freq.max_period_between_procedures);
+	bt_shell_printf("  max_procedure_count           : %u\n",
+			cs_freq.max_procedure_count);
+	bt_shell_printf("  min_sub_event_len             : %02x %02x %02x\n",
+			cs_freq.min_sub_event_len[0],
+			cs_freq.min_sub_event_len[1],
+			cs_freq.min_sub_event_len[2]);
+	bt_shell_printf("  max_sub_event_len             : %02x %02x %02x\n",
+			cs_freq.max_sub_event_len[0],
+			cs_freq.max_sub_event_len[1],
+			cs_freq.max_sub_event_len[2]);
+	bt_shell_printf("  tone_antenna_config_selection : 0x%02x\n",
+			cs_freq.tone_antenna_config_selection);
+	bt_shell_printf("  phy                           : 0x%02x\n",
+			cs_freq.phy);
+	bt_shell_printf("  tx_power_delta                : 0x%02x\n",
+			cs_freq.tx_power_delta);
+	bt_shell_printf("  preferred_peer_antenna        : 0x%02x\n",
+			cs_freq.preferred_peer_antenna);
+	bt_shell_printf("  snr_control_initiator         : 0x%02x\n",
+			cs_freq.snr_control_initiator);
+	bt_shell_printf("  snr_control_reflector         : 0x%02x\n",
+			cs_freq.snr_control_reflector);
+}
+
+static const struct bt_shell_menu cs_menu = {
+	.name = "cs",
+	.desc = "Channel Sounding Submenu",
+	.entries = {
+	{ "start",  "[dev_addr] [duration_secs]",
+				cmd_cs_start,
+				"Start distance measurement using the"
+				" current parameters (see cs.<param>"
+				" commands below).\n"
+				"\t\t\t\t\t\tInitiator (role 0x01/0x03): requires"
+				" dev_addr and duration_secs.\n"
+				"\t\t\t\t\t\tReflector (role 0x02): dev_addr/"
+				"duration_secs are ignored, and no local"
+				" measurement is started.",
+				NULL },
+	{ "stop",   "[dev_addr]",
+				cmd_cs_stop,
+				"Stop the active distance measurement;"
+				" address required when multiple are active" },
+	{ "show",   NULL,
+				cmd_cs_show,
+				"Show active session id and current"
+				" CS parameters" },
+	{ "role", "<0x01|0x02|0x03>", cmd_cs_set,
+				"CS role: 0x01 Initiator, 0x02 Reflector,"
+				" 0x03 Both (default 0x03)",
+				cs_role_generator },
+	{ "sync_ant_sel", "<value>", cmd_cs_set,
+				"CS sync antenna selection; 0xFE/0xFF"
+				" reserved (default 0xFF)" },
+	{ "max_tx_power", "<-127..20>", cmd_cs_set,
+				"Max TX power in dBm, signed (default 20)" },
+	{ "config_id", "<value>", cmd_cs_set,
+				"CS configuration identifier (default 0)" },
+	{ "main_mode_type", "<1|2|3>", cmd_cs_set,
+				"1 Mode 1 (RTT), 2 Mode 2 (PBR), 3 Both"
+				" (default 1)",
+				cs_main_mode_type_generator },
+	{ "sub_mode_type", "<0x01|0x02|0x03|0xFF>", cmd_cs_set,
+				"Sub-mode within main mode; 0xFF = unused"
+				" (default 0xFF)",
+				cs_sub_mode_type_generator },
+	{ "main_mode_min_steps", "<value>", cmd_cs_set,
+				"Min CS main mode steps per subevent"
+				" (default 2)" },
+	{ "main_mode_max_steps", "<value>", cmd_cs_set,
+				"Max CS main mode steps per subevent"
+				" (default 3)" },
+	{ "main_mode_repetition", "<value>", cmd_cs_set,
+				"Times main mode steps are repeated in a"
+				" subevent (default 1)" },
+	{ "mode0_steps", "<value>", cmd_cs_set,
+				"CS Mode 0 steps at the beginning of each"
+				" subevent (default 2)" },
+	{ "rtt_types", "<value>", cmd_cs_set,
+				"RTT measurement types bitmask (default 0)" },
+	{ "sync_phy", "<0x01|0x02>", cmd_cs_set,
+				"PHY for CS sync: 0x01 LE 1M, 0x02 LE 2M"
+				" (default 0x01)",
+				cs_phy_generator },
+	{ "channel_map", "<10 colon-separated hex bytes>", cmd_cs_set,
+				"10-byte channel map bitmap"
+				" (default FC:FF:7F:FC:FF:FF:FF:FF:FF:1F)" },
+	{ "channel_map_repetition", "<value>", cmd_cs_set,
+				"Consecutive repetitions of the channel map"
+				" (default 1)" },
+	{ "channel_selection_type", "<value>", cmd_cs_set,
+				"CS channel selection algorithm (default 0)" },
+	{ "channel_shape", "<value>", cmd_cs_set,
+				"Shape used in channel selection algorithm"
+				" (default 0)" },
+	{ "channel_jump", "<value>", cmd_cs_set,
+				"Channel jump size (default 2)" },
+	{ "companion_signal_enable", "<0|1>", cmd_cs_set,
+				"1 to transmit companion signal, 0 to disable"
+				" (default 0)",
+				cs_bool_generator },
+	{ "min_sub_event_len", "<3 colon-separated hex bytes>", cmd_cs_set,
+				"Min CS subevent length, 3-byte LE"
+				" (default 00:20:00)" },
+	{ "max_sub_event_len", "<3 colon-separated hex bytes>", cmd_cs_set,
+				"Max CS subevent length, 3-byte LE"
+				" (default 03:20:00)" },
+	{ "max_procedure_duration", "<value>", cmd_cs_set,
+				"Maximum duration of one CS measurement"
+				" procedure (default 1600)" },
+	{ "min_period_between_procedures", "<value>", cmd_cs_set,
+				"Minimum time between consecutive procedures"
+				" (default 30)" },
+	{ "max_period_between_procedures", "<value>", cmd_cs_set,
+				"Maximum time between consecutive procedures"
+				" (default 150)" },
+	{ "max_procedure_count", "<value>", cmd_cs_set,
+				"Max number of procedures; 0 = no limit"
+				" (default 0)" },
+	{ "tone_antenna_config_selection", "<value>", cmd_cs_set,
+				"Antenna config for CS tone exchanges"
+				" (default 0x07)" },
+	{ "phy", "<0x01|0x02>", cmd_cs_set,
+				"PHY for CS procedures: 0x01 LE 1M, 0x02 LE 2M"
+				" (default 0x01)",
+				cs_phy_generator },
+	{ "tx_power_delta", "<value>", cmd_cs_set,
+				"Remote vs local TX power delta; 0x80 = not"
+				" applicable (default 0x80)" },
+	{ "preferred_peer_antenna", "<value>", cmd_cs_set,
+				"Preferred antenna for the peer device"
+				" (default 0x03)" },
+	{ "snr_control_initiator", "<value>", cmd_cs_set,
+				"SNR control for initiator; 0xFF = no"
+				" preference (default 0xFF)" },
+	{ "snr_control_reflector", "<value>", cmd_cs_set,
+				"SNR control for reflector; 0xFF = no"
+				" preference (default 0xFF)" },
+	{ NULL } },
+};
+
+void cs_add_submenu(void)
+{
+	bt_shell_add_submenu(&cs_menu);
+}
+
+void cs_remove_submenu(void)
+{
+	g_list_free_full(cs_sessions, g_free);
+	cs_sessions = NULL;
+
+	while (cs_watches) {
+		struct cs_watch *w = cs_watches->data;
+
+		g_dbus_remove_watch(dbus_conn, w->watch_id);
+		cs_watches = g_list_remove(cs_watches, w);
+		g_free(w);
+	}
+
+	g_list_free(cs_proxies);
+	cs_proxies = NULL;
+}
diff --git a/client/cs.h b/client/cs.h
new file mode 100644
index 000000000..8902c8143
--- /dev/null
+++ b/client/cs.h
@@ -0,0 +1,18 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ *  BlueZ - Bluetooth protocol stack for Linux
+ *
+ * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
+ */
+
+#include "gdbus/gdbus.h"
+
+void cs_add_submenu(void);
+void cs_remove_submenu(void);
+
+void cs_proxy_added(GDBusProxy *proxy);
+void cs_proxy_removed(GDBusProxy *proxy);
+void cs_device_disconnected(const char *dev_path);
+void cs_measurement_started(GDBusProxy *proxy);
+void cs_measurement_stopped(GDBusProxy *proxy);
+void cs_set_device_list(GList **devices);
diff --git a/client/main.c b/client/main.c
index 9d9ac8b14..b5cad8afc 100644
--- a/client/main.c
+++ b/client/main.c
@@ -39,6 +39,7 @@
 #include "assistant.h"
 #include "hci.h"
 #include "telephony.h"
+#include "cs.h"
 
 /* String display constants */
 #define COLORED_NEW	COLOR_GREEN "NEW" COLOR_OFF
@@ -124,6 +125,7 @@ static void disconnect_handler(DBusConnection *connection, void *user_data)
 	battery_proxies = NULL;
 
 	default_ctrl = NULL;
+	cs_set_device_list(NULL);
 }
 
 static void print_adapter(GDBusProxy *proxy, const char *description)
@@ -391,8 +393,10 @@ static struct adapter *adapter_new(GDBusProxy *proxy)
 
 	ctrl_list = g_list_append(ctrl_list, adapter);
 
-	if (!default_ctrl)
+	if (!default_ctrl) {
 		default_ctrl = adapter;
+		cs_set_device_list(&adapter->devices);
+	}
 
 	return adapter;
 }
@@ -525,6 +529,8 @@ static void proxy_added(GDBusProxy *proxy, void *user_data)
 		bearer_added(proxy);
 	} else if (!strcmp(interface, "org.bluez.Bearer.LE1")) {
 		bearer_added(proxy);
+	} else if (!strcmp(interface, "org.bluez.ChannelSounding1")) {
+		cs_proxy_added(proxy);
 	}
 }
 
@@ -656,6 +662,8 @@ static void proxy_removed(GDBusProxy *proxy, void *user_data)
 		bearer_removed(proxy);
 	} else if (!strcmp(interface, "org.bluez.Bearer.LE1")) {
 		bearer_removed(proxy);
+	} else if (!strcmp(interface, "org.bluez.ChannelSounding1")) {
+		cs_proxy_removed(proxy);
 	}
 }
 
@@ -729,6 +737,10 @@ static void property_changed(GDBusProxy *proxy, const char *name,
 				else if (!connected && default_dev == proxy)
 					set_default_device(NULL, NULL);
 
+				if (!connected)
+					cs_device_disconnected(
+						g_dbus_proxy_get_path(proxy));
+
 				/* If the device is connected and the filter
 				 * is auto_connect and it matches the pattern,
 				 * stop discovery.
@@ -821,6 +833,16 @@ static void property_changed(GDBusProxy *proxy, const char *name,
 			print_iter(str, name, iter);
 			g_free(str);
 		}
+	} else if (!strcmp(interface, "org.bluez.ChannelSounding1")) {
+		if (!strcmp(name, "Active")) {
+			dbus_bool_t active;
+
+			dbus_message_iter_get_basic(iter, &active);
+			if (active)
+				cs_measurement_started(proxy);
+			else
+				cs_measurement_stopped(proxy);
+		}
 	}
 }
 
@@ -1090,6 +1112,7 @@ static void cmd_select(int argc, char *argv[])
 		return bt_shell_noninteractive_quit(EXIT_SUCCESS);
 
 	default_ctrl = adapter;
+	cs_set_device_list(&adapter->devices);
 	print_adapter(adapter->proxy, NULL);
 
 	return bt_shell_noninteractive_quit(EXIT_SUCCESS);
@@ -4003,6 +4026,7 @@ int main(int argc, char *argv[])
 	assistant_add_submenu();
 	hci_add_submenu();
 	telephony_add_submenu();
+	cs_add_submenu();
 	bt_shell_set_prompt(PROMPT_OFF, NULL);
 
 	bt_shell_handle_non_interactive_help();
@@ -4044,6 +4068,7 @@ int main(int argc, char *argv[])
 	assistant_remove_submenu();
 	hci_remove_submenu();
 	telephony_remove_submenu();
+	cs_remove_submenu();
 
 	g_dbus_client_unref(client);
 
-- 


      parent reply	other threads:[~2026-07-10  9:49 UTC|newest]

Thread overview: 6+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-10  9:49 [PATCH BlueZ v2 0/4] Add Channel Sounding Dbus support and Naga Bhavani Akella
2026-07-10  9:49 ` [PATCH BlueZ v2 1/4] rap: Add Channel Sounding parameter types and APIs Naga Bhavani Akella
2026-07-10 11:57   ` Add Channel Sounding Dbus support and bluez.test.bot
2026-07-10  9:49 ` [PATCH BlueZ v2 2/4] src: Register GATT profiles for Channel Sounding reflector Naga Bhavani Akella
2026-07-10  9:49 ` [PATCH BlueZ v2 3/4] profiles: Add D-Bus Channel Sounding control APIs Naga Bhavani Akella
2026-07-10  9:49 ` Naga Bhavani Akella [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260710094919.4047204-5-naga.akella@oss.qualcomm.com \
    --to=naga.akella@oss.qualcomm.com \
    --cc=linux-bluetooth@vger.kernel.org \
    --cc=luiz.dentz@gmail.com \
    --cc=quic_anubhavg@quicinc.com \
    --cc=quic_hbandi@quicinc.com \
    --cc=quic_mohamull@quicinc.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox