From 6c403da3cd167f0850fb83c04251a2cae52ea99e Mon Sep 17 00:00:00 2001
From: Emil Gydesen <emil.gydesen@nordicsemi.no>
Date: Mon, 4 Dec 2023 11:12:33 +0100
Subject: [PATCH] Bluetooth: Audio: Shell: CAP change volume offset command

Add sthe change volume offset command to the CAP commander shell.

Signed-off-by: Emil Gydesen <emil.gydesen@nordicsemi.no>
---
 doc/connectivity/bluetooth/api/shell/cap.rst |  39 ++++++-
 subsys/bluetooth/audio/shell/cap_commander.c | 102 ++++++++++++++++++-
 2 files changed, 135 insertions(+), 6 deletions(-)

diff --git a/doc/connectivity/bluetooth/api/shell/cap.rst b/doc/connectivity/bluetooth/api/shell/cap.rst
index 32d752d71fc8..51a42c90260f 100644
--- a/doc/connectivity/bluetooth/api/shell/cap.rst
+++ b/doc/connectivity/bluetooth/api/shell/cap.rst
@@ -159,8 +159,10 @@ the optionally included CSIS instance by calling (:code:`cap_commander discover`
    cap_commander --help
    cap_commander - Bluetooth CAP commander shell commands
    Subcommands:
-     discover       :Discover CAS
-     change_volume  :Change volume on all connections <volume>
+     discover              :Discover CAS
+     change_volume         :Change volume on all connections <volume>
+     change_volume_offset  :Change volume offset per connection <volume_offset
+                            [volume_offset [...]]>
 
 
 Before being able to perform any stream operation, the device must also perform the
@@ -193,3 +195,36 @@ Setting the volume on all connected devices:
    VCP flags 0x01
    VCP vol_set done
    Volume change completed
+
+
+Setting the volume offset on one or more connected devices. The offsets are set by connection index,
+so connection index 0 gets the first offset, and index 1 gets the second offset, etc.:
+
+.. code-block:: console
+
+   uart:~$ bt connect <device A>
+   Connected: <device A>
+   uart:~$ cap_commander discover
+   discovery completed with CSIS
+   uart:~$ vcp_vol_ctlr discover
+   VCP discover done with 1 VOCS and 1 AICS
+   uart:~$
+   uart:~$ bt connect <device B>
+   Connected: <device B>
+   uart:~$ cap_commander discover
+   discovery completed with CSIS
+   uart:~$ vcp_vol_ctlr discover
+   VCP discover done with 1 VOCS and 1 AICS
+   uart:~$
+   uart:~$ cap_commander change_volume_offset 10
+   Setting volume offset on 1 connections
+   VOCS inst 0x200140a4 offset 10
+   Offset set for inst 0x200140a4
+   Volume offset change completed
+   uart:~$
+   uart:~$ cap_commander change_volume_offset 10 15
+   Setting volume offset on 2 connections
+   Offset set for inst 0x200140a4
+   VOCS inst 0x20014188 offset 15
+   Offset set for inst 0x20014188
+   Volume offset change completed
diff --git a/subsys/bluetooth/audio/shell/cap_commander.c b/subsys/bluetooth/audio/shell/cap_commander.c
index 01675f8fc6ca..6e4f85eba014 100644
--- a/subsys/bluetooth/audio/shell/cap_commander.c
+++ b/subsys/bluetooth/audio/shell/cap_commander.c
@@ -8,10 +8,12 @@
  */
 #include <stdlib.h>
 
-#include <zephyr/types.h>
-#include <zephyr/shell/shell.h>
 #include <zephyr/bluetooth/conn.h>
 #include <zephyr/bluetooth/audio/cap.h>
+#include <zephyr/bluetooth/audio/vocs.h>
+#include <zephyr/shell/shell.h>
+#include <zephyr/sys/util.h>
+#include <zephyr/types.h>
 
 #include "shell/bt.h"
 #include "audio.h"
@@ -37,12 +39,27 @@ static void cap_volume_changed_cb(struct bt_conn *conn, int err)
 
 	shell_print(ctx_shell, "Volume change completed");
 }
+
+#if defined(CONFIG_BT_VCP_VOL_CTLR_VOCS)
+static void cap_volume_offset_changed_cb(struct bt_conn *conn, int err)
+{
+	if (err != 0) {
+		shell_error(ctx_shell, "Volume offset change failed (%d)", err);
+		return;
+	}
+
+	shell_print(ctx_shell, "Volume offset change completed");
+}
+#endif /* CONFIG_BT_VCP_VOL_CTLR_VOCS */
 #endif /* CONFIG_BT_VCP_VOL_CTLR */
 
 static struct bt_cap_commander_cb cbs = {
 	.discovery_complete = cap_discover_cb,
 #if defined(CONFIG_BT_VCP_VOL_CTLR)
 	.volume_changed = cap_volume_changed_cb,
+#if defined(CONFIG_BT_VCP_VOL_CTLR_VOCS)
+	.volume_offset_changed = cap_volume_offset_changed_cb,
+#endif /* CONFIG_BT_VCP_VOL_CTLR_VOCS */
 #endif /* CONFIG_BT_VCP_VOL_CTLR */
 };
 
@@ -145,6 +162,79 @@ static int cmd_cap_commander_change_volume(const struct shell *sh, size_t argc,
 
 	return 0;
 }
+
+#if defined(CONFIG_BT_VCP_VOL_CTLR_VOCS)
+static int cmd_cap_commander_change_volume_offset(const struct shell *sh, size_t argc, char *argv[])
+{
+	struct bt_cap_commander_change_volume_offset_member_param member_params[CONFIG_BT_MAX_CONN];
+	const size_t cap_args = argc - 1; /* First argument is the command itself */
+	struct bt_cap_commander_change_volume_offset_param param = {
+		.type = BT_CAP_SET_TYPE_AD_HOC,
+		.param = member_params,
+	};
+	struct bt_conn *connected_conns[CONFIG_BT_MAX_CONN] = {0};
+	size_t conn_cnt = 0U;
+	int err = 0;
+
+	if (default_conn == NULL) {
+		shell_error(sh, "Not connected");
+		return -ENOEXEC;
+	}
+
+	/* Populate the array of connected connections */
+	bt_conn_foreach(BT_CONN_TYPE_LE, populate_connected_conns, (void *)connected_conns);
+	for (size_t i = 0; i < ARRAY_SIZE(connected_conns); i++) {
+		struct bt_conn *conn = connected_conns[i];
+
+		if (conn == NULL) {
+			break;
+		}
+
+		conn_cnt++;
+	}
+
+	if (cap_args > conn_cnt) {
+		shell_error(sh, "Cannot use %zu arguments for %zu connections", argc, conn_cnt);
+
+		return -ENOEXEC;
+	}
+
+	/* TODO: Add support for coordinated sets */
+
+	for (size_t i = 0U; i < cap_args; i++) {
+		const char *arg = argv[i + 1];
+		long volume_offset;
+
+		volume_offset = shell_strtol(arg, 10, &err);
+		if (err != 0) {
+			shell_error(sh, "Failed to parse volume offset from %s", arg);
+
+			return -ENOEXEC;
+		}
+
+		if (!IN_RANGE(volume_offset, BT_VOCS_MIN_OFFSET, BT_VOCS_MAX_OFFSET)) {
+			shell_error(sh, "Invalid volume_offset %lu", volume_offset);
+
+			return -ENOEXEC;
+		}
+
+		member_params[i].offset = (int16_t)volume_offset;
+		member_params[i].member.member = connected_conns[i];
+		param.count++;
+	}
+
+	shell_print(sh, "Setting volume offset on %zu connections", param.count);
+
+	err = bt_cap_commander_change_volume_offset(&param);
+	if (err != 0) {
+		shell_print(sh, "Failed to change volume offset: %d", err);
+
+		return -ENOEXEC;
+	}
+
+	return 0;
+}
+#endif /* CONFIG_BT_VCP_VOL_CTLR_VOCS */
 #endif /* CONFIG_BT_VCP_VOL_CTLR */
 
 static int cmd_cap_commander(const struct shell *sh, size_t argc, char **argv)
@@ -164,9 +254,13 @@ SHELL_STATIC_SUBCMD_SET_CREATE(
 #if defined(CONFIG_BT_VCP_VOL_CTLR)
 	SHELL_CMD_ARG(change_volume, NULL, "Change volume on all connections <volume>",
 		      cmd_cap_commander_change_volume, 2, 0),
+#if defined(CONFIG_BT_VCP_VOL_CTLR_VOCS)
+	SHELL_CMD_ARG(change_volume_offset, NULL,
+		      "Change volume offset per connection <volume_offset [volume_offset [...]]>",
+		      cmd_cap_commander_change_volume_offset, 2, CONFIG_BT_MAX_CONN - 1),
+#endif /* CONFIG_BT_VCP_VOL_CTLR_VOCS */
 #endif /* CONFIG_BT_VCP_VOL_CTLR */
-	SHELL_SUBCMD_SET_END
-);
+	SHELL_SUBCMD_SET_END);
 
 SHELL_CMD_ARG_REGISTER(cap_commander, &cap_commander_cmds, "Bluetooth CAP commander shell commands",
 		       cmd_cap_commander, 1, 1);