Skip to content

Commit 3c6cba8

Browse files
committed
Bluetooth: CAP: Shell: Add handover shell commands
Add shell commands for the CAP handover procedures. Signed-off-by: Emil Gydesen <[email protected]>
1 parent b6909b4 commit 3c6cba8

File tree

6 files changed

+380
-43
lines changed

6 files changed

+380
-43
lines changed

subsys/bluetooth/audio/shell/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ zephyr_library_sources_ifdef(
7171
CONFIG_BT_CAP_COMMANDER
7272
cap_commander.c
7373
)
74+
zephyr_library_sources_ifdef(
75+
CONFIG_BT_CAP_HANDOVER
76+
cap_handover.c
77+
)
7478
zephyr_library_sources_ifdef(
7579
CONFIG_BT_HAS_CLIENT
7680
has_client.c

subsys/bluetooth/audio/shell/audio.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,8 @@ void bap_usb_get_frame(struct shell_stream *sh_stream, enum bt_audio_location ch
170170
size_t bap_usb_get_frame_size(const struct shell_stream *sh_stream);
171171

172172
struct broadcast_source {
173-
bool is_cap;
173+
bool is_cap: 1;
174+
bool handover_in_progress: 1;
174175
union {
175176
struct bt_bap_broadcast_source *bap_source;
176177
struct bt_cap_broadcast_source *cap_source;

subsys/bluetooth/audio/shell/bap.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3218,6 +3218,12 @@ static int cmd_create_broadcast(const struct shell *sh, size_t argc,
32183218
return -ENOEXEC;
32193219
}
32203220

3221+
if (IS_ENABLED(CONFIG_BT_CAP_HANDOVER) && default_source.handover_in_progress) {
3222+
shell_info(sh, "CAP Handover in progress");
3223+
3224+
return -ENOEXEC;
3225+
}
3226+
32213227
named_preset = &default_broadcast_source_preset;
32223228

32233229
for (size_t i = 1U; i < argc; i++) {
Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
/**
2+
* @file
3+
* @brief Shell APIs for Bluetooth CAP handover
4+
*
5+
* Copyright (c) 2025 Nordic Semiconductor ASA
6+
*
7+
* SPDX-License-Identifier: Apache-2.0
8+
*/
9+
#include <errno.h>
10+
#include <stdbool.h>
11+
#include <stddef.h>
12+
#include <stdint.h>
13+
#include <stdlib.h>
14+
#include <string.h>
15+
#include <sys/types.h>
16+
17+
#include <zephyr/autoconf.h>
18+
#include <zephyr/bluetooth/audio/audio.h>
19+
#include <zephyr/bluetooth/audio/bap.h>
20+
#include <zephyr/bluetooth/audio/csip.h>
21+
#include <zephyr/bluetooth/crypto.h>
22+
#include <zephyr/bluetooth/gap.h>
23+
#include <zephyr/bluetooth/iso.h>
24+
#include <zephyr/bluetooth/uuid.h>
25+
#include <zephyr/net_buf.h>
26+
#include <zephyr/shell/shell_string_conv.h>
27+
#include <zephyr/sys/__assert.h>
28+
#include <zephyr/sys/byteorder.h>
29+
#include <zephyr/sys/printk.h>
30+
#include <zephyr/sys/util.h>
31+
#include <zephyr/types.h>
32+
#include <zephyr/shell/shell.h>
33+
#include <zephyr/bluetooth/conn.h>
34+
#include <zephyr/bluetooth/gatt.h>
35+
#include <zephyr/bluetooth/bluetooth.h>
36+
#include <zephyr/bluetooth/audio/cap.h>
37+
38+
#include "common/bt_shell_private.h"
39+
#include "host/shell/bt.h"
40+
#include "audio.h"
41+
42+
static void unicast_to_broadcast_complete_cb(int err, struct bt_conn *conn,
43+
struct bt_cap_unicast_group *unicast_group,
44+
struct bt_cap_broadcast_source *broadcast_source)
45+
{
46+
if (err == -ECANCELED) {
47+
bt_shell_print("Unicast to broadcast handover was cancelled for conn %p", conn);
48+
49+
default_source.broadcast_id = BT_BAP_INVALID_BROADCAST_ID;
50+
} else if (err != 0) {
51+
bt_shell_error("Unicast to broadcast handover failed for conn %p (%d)", conn, err);
52+
53+
default_source.broadcast_id = BT_BAP_INVALID_BROADCAST_ID;
54+
} else {
55+
bt_shell_print(
56+
"Unicast to broadcast handover completed with new broadcast source %p",
57+
(void *)broadcast_source);
58+
59+
default_source.cap_source = broadcast_source;
60+
default_source.is_cap = true;
61+
}
62+
63+
default_source.handover_in_progress = false;
64+
}
65+
66+
static void broadcast_to_unicast_complete_cb(int err, struct bt_conn *conn,
67+
struct bt_cap_broadcast_source *broadcast_source,
68+
struct bt_cap_unicast_group *unicast_group)
69+
{
70+
if (err == -ECANCELED) {
71+
bt_shell_print("Broadcast to unicast handover was cancelled for conn %p", conn);
72+
} else if (err != 0) {
73+
bt_shell_error("Broadcast to unicast handover failed for conn %p (%d)", conn, err);
74+
} else {
75+
bt_shell_print("Broadcast to unicast handover completed with new group %p",
76+
(void *)unicast_group);
77+
}
78+
}
79+
80+
static struct bt_cap_handover_cb cbs = {
81+
.unicast_to_broadcast_complete = unicast_to_broadcast_complete_cb,
82+
.broadcast_to_unicast_complete = broadcast_to_unicast_complete_cb,
83+
};
84+
85+
struct cap_unicast_group_stream_lookup {
86+
struct bt_cap_stream *active_sink_streams[CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT];
87+
size_t active_sink_streams_cnt;
88+
};
89+
90+
static bool unicast_group_foreach_stream_cb(struct bt_cap_stream *cap_stream, void *user_data)
91+
{
92+
struct cap_unicast_group_stream_lookup *data = user_data;
93+
const struct bt_bap_stream *bap_stream = &cap_stream->bap_stream;
94+
95+
__ASSERT_NO_MSG(data->active_sink_streams_cnt < ARRAY_SIZE(data->active_sink_streams));
96+
97+
if (bap_stream->ep != NULL) {
98+
struct bt_bap_ep_info ep_info;
99+
int err;
100+
101+
err = bt_bap_ep_get_info(bap_stream->ep, &ep_info);
102+
__ASSERT_NO_MSG(err == 0);
103+
104+
/* Only consider sink streams for handover to broadcast */
105+
if (ep_info.state == BT_BAP_EP_STATE_STREAMING &&
106+
ep_info.dir == BT_AUDIO_DIR_SINK) {
107+
data->active_sink_streams[data->active_sink_streams_cnt++] = cap_stream;
108+
}
109+
}
110+
111+
return false;
112+
}
113+
114+
static int register_callbacks(void)
115+
{
116+
static bool registered;
117+
118+
if (!registered) {
119+
const int err = bt_cap_handover_register_cb(&cbs);
120+
121+
if (err != 0) {
122+
return err;
123+
}
124+
125+
registered = true;
126+
}
127+
128+
return 0;
129+
}
130+
131+
static int cmd_cap_handover_unicast_to_broadcast(const struct shell *sh, size_t argc, char *argv[])
132+
{
133+
static struct bt_cap_initiator_broadcast_stream_param
134+
stream_params[CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT];
135+
static struct bt_cap_initiator_broadcast_subgroup_param subgroup_param = {0};
136+
static struct bt_cap_initiator_broadcast_create_param broadcast_create_param;
137+
/* Struct containing the converted unicast group configuration */
138+
struct bt_cap_handover_unicast_to_broadcast_param param = {0};
139+
struct cap_unicast_group_stream_lookup lookup_data = {0};
140+
struct bt_le_ext_adv *adv = adv_sets[selected_adv];
141+
const struct named_lc3_preset *named_preset;
142+
uint32_t broadcast_id = 0U;
143+
int err;
144+
145+
if (adv == NULL) {
146+
shell_error(sh, "Extended advertising set is NULL");
147+
148+
return -ENOEXEC;
149+
}
150+
151+
if (default_unicast_group.cap_group == NULL) {
152+
shell_error(sh, "CAP unicast group not created");
153+
154+
return -ENOEXEC;
155+
}
156+
157+
if (!default_unicast_group.is_cap) {
158+
shell_error(sh, "Unicast group is not CAP");
159+
160+
return -ENOEXEC;
161+
}
162+
163+
if (default_source.cap_source != NULL) {
164+
shell_error(sh, "CAP Broadcast source already created");
165+
166+
return -ENOEXEC;
167+
}
168+
169+
if (default_source.handover_in_progress) {
170+
shell_info(sh, "CAP Handover in progress");
171+
172+
return -ENOEXEC;
173+
}
174+
175+
named_preset = &default_broadcast_source_preset;
176+
177+
for (size_t i = 1U; i < argc; i++) {
178+
char *arg = argv[i];
179+
180+
if (strcmp(arg, "enc") == 0) {
181+
if (argc > i) {
182+
size_t bcode_len;
183+
184+
i++;
185+
arg = argv[i];
186+
187+
bcode_len = hex2bin(arg, strlen(arg),
188+
broadcast_create_param.broadcast_code,
189+
sizeof(broadcast_create_param.broadcast_code));
190+
191+
if (bcode_len != sizeof(broadcast_create_param.broadcast_code)) {
192+
shell_error(sh, "Invalid Broadcast Code Length: %zu",
193+
bcode_len);
194+
195+
return -ENOEXEC;
196+
}
197+
198+
broadcast_create_param.encryption = true;
199+
} else {
200+
shell_help(sh);
201+
202+
return SHELL_CMD_HELP_PRINTED;
203+
}
204+
} else if (strcmp(arg, "preset") == 0) {
205+
if (argc > i) {
206+
207+
i++;
208+
arg = argv[i];
209+
210+
named_preset =
211+
bap_get_named_preset(false, BT_AUDIO_DIR_SOURCE, arg);
212+
if (named_preset == NULL) {
213+
shell_error(sh, "Unable to parse named_preset %s", arg);
214+
215+
return -ENOEXEC;
216+
}
217+
} else {
218+
shell_help(sh);
219+
220+
return SHELL_CMD_HELP_PRINTED;
221+
}
222+
}
223+
}
224+
225+
err = register_callbacks();
226+
if (err != 0) {
227+
shell_error(sh, "Failed to register callbacks: %d", err);
228+
229+
return -ENOEXEC;
230+
}
231+
232+
/* Lookup all active sink streams */
233+
err = bt_cap_unicast_group_foreach_stream(default_unicast_group.cap_group,
234+
unicast_group_foreach_stream_cb, &lookup_data);
235+
__ASSERT_NO_MSG(err == 0);
236+
237+
if (lookup_data.active_sink_streams_cnt == 0U) {
238+
shell_error(sh, "No active sink streams in group, cannot handover");
239+
240+
return -ENOEXEC;
241+
}
242+
243+
/* Generate the broadcast_id here even though the broadcast source is created later -
244+
* We need to provide the broadcast ID that we expect to use when it is started
245+
*/
246+
err = bt_rand(&broadcast_id, BT_AUDIO_BROADCAST_ID_SIZE);
247+
if (err != 0) {
248+
shell_error(sh, "Unable to generate broadcast ID: %d\n", err);
249+
250+
return -ENOEXEC;
251+
}
252+
253+
shell_print(sh, "Generated broadcast_id 0x%06X", broadcast_id);
254+
255+
copy_broadcast_source_preset(&default_source, named_preset);
256+
257+
subgroup_param.stream_count = lookup_data.active_sink_streams_cnt;
258+
subgroup_param.stream_params = stream_params;
259+
subgroup_param.codec_cfg = &default_source.codec_cfg;
260+
for (size_t i = 0U; i < lookup_data.active_sink_streams_cnt; i++) {
261+
subgroup_param.stream_params[i].stream = lookup_data.active_sink_streams[i];
262+
stream_params[i].data_len = 0U;
263+
stream_params[i].data = NULL;
264+
}
265+
266+
(void)memset(&broadcast_create_param, 0, sizeof(broadcast_create_param));
267+
broadcast_create_param.subgroup_count = 1U;
268+
broadcast_create_param.subgroup_params = &subgroup_param;
269+
broadcast_create_param.qos = &default_source.qos;
270+
broadcast_create_param.packing = BT_ISO_PACKING_SEQUENTIAL;
271+
broadcast_create_param.encryption = false;
272+
273+
param.type = BT_CAP_SET_TYPE_AD_HOC;
274+
param.unicast_group = default_unicast_group.cap_group;
275+
param.broadcast_create_param = &broadcast_create_param;
276+
param.ext_adv = adv;
277+
param.pa_interval = BT_BAP_PA_INTERVAL_UNKNOWN;
278+
param.broadcast_id = broadcast_id;
279+
280+
err = bt_cap_handover_unicast_to_broadcast(&param);
281+
if (err != 0) {
282+
shell_error(sh, "Failed to handover unicast audio to broadcast: %d", err);
283+
284+
return -ENOEXEC;
285+
}
286+
287+
default_source.handover_in_progress = true;
288+
default_source.broadcast_id = broadcast_id;
289+
290+
return 0;
291+
}
292+
293+
static int cmd_cap_handover_broadcast_to_unicast(const struct shell *sh, size_t argc, char *argv[])
294+
{
295+
int err;
296+
297+
err = register_callbacks();
298+
if (err != 0) {
299+
shell_error(sh, "Failed to register callbacks: %d", err);
300+
301+
return -ENOEXEC;
302+
}
303+
304+
return 0;
305+
}
306+
307+
static int cmd_cap_handover(const struct shell *sh, size_t argc, char **argv)
308+
{
309+
if (argc > 1) {
310+
shell_error(sh, "%s unknown parameter: %s", argv[0], argv[1]);
311+
} else {
312+
shell_error(sh, "%s Missing subcommand", argv[0]);
313+
}
314+
315+
return -ENOEXEC;
316+
}
317+
318+
SHELL_STATIC_SUBCMD_SET_CREATE(
319+
cap_handover_cmds,
320+
SHELL_CMD_ARG(unicast_to_broadcast, NULL,
321+
"Handover current unicast group to broadcast (unicast group will be deleted)",
322+
cmd_cap_handover_unicast_to_broadcast, 1, 0),
323+
SHELL_CMD_ARG(
324+
broadcast_to_unicast, NULL,
325+
"Handover current broadcast source to unicast (broadcast source will be deleted)",
326+
cmd_cap_handover_broadcast_to_unicast, 1, 0),
327+
SHELL_SUBCMD_SET_END);
328+
329+
SHELL_CMD_ARG_REGISTER(cap_handover, &cap_handover_cmds, "Bluetooth CAP handover shell commands",
330+
cmd_cap_handover, 1, 1);

0 commit comments

Comments
 (0)