| /** @file |
| * @brief Bluetooth BR/EDR shell module |
| * |
| * Provide some Bluetooth shell commands that can be useful to applications. |
| */ |
| |
| /* |
| * Copyright (c) 2018 Intel Corporation |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #include <errno.h> |
| #include <zephyr/types.h> |
| #include <stddef.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <zephyr/sys/byteorder.h> |
| #include <zephyr/kernel.h> |
| |
| #include <zephyr/settings/settings.h> |
| |
| #include <zephyr/bluetooth/hci.h> |
| #include <zephyr/bluetooth/bluetooth.h> |
| #include <zephyr/bluetooth/conn.h> |
| #include <zephyr/bluetooth/l2cap.h> |
| #include <zephyr/bluetooth/classic/rfcomm.h> |
| #include <zephyr/bluetooth/classic/sdp.h> |
| #include <zephyr/bluetooth/classic/l2cap_br.h> |
| |
| #include <zephyr/shell/shell.h> |
| |
| #include "host/shell/bt.h" |
| #include "common/bt_shell_private.h" |
| |
| #if defined(CONFIG_BT_CONN) |
| /* Connection context for BR/EDR legacy pairing in sec mode 3 */ |
| static struct bt_conn *pairing_conn; |
| #endif /* CONFIG_BT_CONN */ |
| |
| #define DATA_BREDR_MTU 200 |
| |
| NET_BUF_POOL_FIXED_DEFINE(data_tx_pool, 1, BT_L2CAP_SDU_BUF_SIZE(DATA_BREDR_MTU), |
| CONFIG_BT_CONN_TX_USER_DATA_SIZE, NULL); |
| NET_BUF_POOL_FIXED_DEFINE(data_rx_pool, 1, DATA_BREDR_MTU, 8, NULL); |
| |
| #define SDP_CLIENT_USER_BUF_LEN 512 |
| NET_BUF_POOL_FIXED_DEFINE(sdp_client_pool, CONFIG_BT_MAX_CONN, SDP_CLIENT_USER_BUF_LEN, 8, NULL); |
| |
| static int cmd_auth_pincode(const struct shell *sh, size_t argc, char *argv[]) |
| { |
| struct bt_conn *conn; |
| uint8_t max = 16U; |
| |
| if (default_conn) { |
| conn = default_conn; |
| } else if (pairing_conn) { |
| conn = pairing_conn; |
| } else { |
| conn = NULL; |
| } |
| |
| if (!conn) { |
| shell_print(sh, "Not connected"); |
| return -ENOEXEC; |
| } |
| |
| if (strlen(argv[1]) > max) { |
| shell_print(sh, "PIN code value invalid - enter max %u digits", max); |
| return -ENOEXEC; |
| } |
| |
| shell_print(sh, "PIN code \"%s\" applied", argv[1]); |
| |
| bt_conn_auth_pincode_entry(conn, argv[1]); |
| |
| return 0; |
| } |
| |
| static int cmd_connect(const struct shell *sh, size_t argc, char *argv[]) |
| { |
| struct bt_conn *conn; |
| bt_addr_t addr; |
| int err; |
| |
| err = bt_addr_from_str(argv[1], &addr); |
| if (err) { |
| shell_print(sh, "Invalid peer address (err %d)", err); |
| return -ENOEXEC; |
| } |
| |
| conn = bt_conn_create_br(&addr, BT_BR_CONN_PARAM_DEFAULT); |
| if (!conn) { |
| shell_print(sh, "Connection failed"); |
| return -ENOEXEC; |
| } |
| |
| shell_print(sh, "Connection pending"); |
| |
| /* unref connection obj in advance as app user */ |
| bt_conn_unref(conn); |
| |
| return 0; |
| } |
| |
| static void br_device_found(const bt_addr_t *addr, int8_t rssi, const uint8_t cod[3], |
| const uint8_t eir[240]) |
| { |
| char br_addr[BT_ADDR_STR_LEN]; |
| char name[239]; |
| int len = 240; |
| |
| (void)memset(name, 0, sizeof(name)); |
| |
| while (len) { |
| if (len < 2) { |
| break; |
| } |
| |
| /* Look for early termination */ |
| if (!eir[0]) { |
| break; |
| } |
| |
| /* Check if field length is correct */ |
| if (eir[0] > len - 1) { |
| break; |
| } |
| |
| switch (eir[1]) { |
| case BT_DATA_NAME_SHORTENED: |
| case BT_DATA_NAME_COMPLETE: |
| if (eir[0] > sizeof(name) - 1) { |
| memcpy(name, &eir[2], sizeof(name) - 1); |
| } else { |
| memcpy(name, &eir[2], eir[0] - 1); |
| } |
| break; |
| default: |
| break; |
| } |
| |
| /* Parse next AD Structure */ |
| len -= eir[0] + 1; |
| eir += eir[0] + 1; |
| } |
| |
| bt_addr_to_str(addr, br_addr, sizeof(br_addr)); |
| |
| bt_shell_print("[DEVICE]: %s, RSSI %i %s", br_addr, rssi, name); |
| } |
| |
| static struct bt_br_discovery_result br_discovery_results[5]; |
| |
| static void br_discovery_complete(const struct bt_br_discovery_result *results, size_t count) |
| { |
| size_t i; |
| |
| bt_shell_print("BR/EDR discovery complete"); |
| |
| for (i = 0; i < count; i++) { |
| br_device_found(&results[i].addr, results[i].rssi, results[i].cod, results[i].eir); |
| } |
| } |
| |
| static struct bt_br_discovery_cb discovery_cb = { |
| .recv = NULL, |
| .timeout = br_discovery_complete, |
| }; |
| |
| static int cmd_discovery(const struct shell *sh, size_t argc, char *argv[]) |
| { |
| const char *action; |
| |
| action = argv[1]; |
| if (!strcmp(action, "on")) { |
| static bool reg_cb = true; |
| struct bt_br_discovery_param param; |
| |
| param.limited = false; |
| param.length = 8U; |
| |
| if (argc > 2) { |
| param.length = atoi(argv[2]); |
| } |
| |
| if (argc > 3 && !strcmp(argv[3], "limited")) { |
| param.limited = true; |
| } |
| |
| if (reg_cb) { |
| reg_cb = false; |
| bt_br_discovery_cb_register(&discovery_cb); |
| } |
| |
| if (bt_br_discovery_start(¶m, br_discovery_results, |
| ARRAY_SIZE(br_discovery_results)) < 0) { |
| shell_print(sh, "Failed to start discovery"); |
| return -ENOEXEC; |
| } |
| |
| shell_print(sh, "Discovery started"); |
| } else if (!strcmp(action, "off")) { |
| if (bt_br_discovery_stop()) { |
| shell_print(sh, "Failed to stop discovery"); |
| return -ENOEXEC; |
| } |
| |
| shell_print(sh, "Discovery stopped"); |
| } else { |
| shell_help(sh); |
| return SHELL_CMD_HELP_PRINTED; |
| } |
| |
| return 0; |
| } |
| |
| struct bt_l2cap_br_server { |
| struct bt_l2cap_server server; |
| #if defined(CONFIG_BT_L2CAP_RET_FC) |
| uint8_t options; |
| #endif /* CONFIG_BT_L2CAP_RET_FC */ |
| }; |
| |
| struct l2cap_br_chan { |
| struct bt_l2cap_br_chan chan; |
| #if defined(CONFIG_BT_L2CAP_RET_FC) |
| struct k_fifo l2cap_recv_fifo; |
| bool hold_credit; |
| #endif /* CONFIG_BT_L2CAP_RET_FC */ |
| }; |
| |
| static int l2cap_recv(struct bt_l2cap_chan *chan, struct net_buf *buf) |
| { |
| struct l2cap_br_chan *br_chan = CONTAINER_OF(chan, struct l2cap_br_chan, chan.chan); |
| |
| bt_shell_print("Incoming data channel %p len %u", chan, buf->len); |
| |
| if (buf->len) { |
| bt_shell_hexdump(buf->data, buf->len); |
| } |
| |
| #if defined(CONFIG_BT_L2CAP_RET_FC) |
| if (br_chan->hold_credit) { |
| k_fifo_put(&br_chan->l2cap_recv_fifo, buf); |
| return -EINPROGRESS; |
| } |
| #endif /* CONFIG_BT_L2CAP_RET_FC */ |
| (void)br_chan; |
| |
| return 0; |
| } |
| |
| static void l2cap_connected(struct bt_l2cap_chan *chan) |
| { |
| struct l2cap_br_chan *br_chan = CONTAINER_OF(chan, struct l2cap_br_chan, chan.chan); |
| |
| bt_shell_print("Channel %p connected", chan); |
| |
| #if defined(CONFIG_BT_L2CAP_RET_FC) |
| switch (br_chan->chan.rx.mode) { |
| case BT_L2CAP_BR_LINK_MODE_BASIC: |
| bt_shell_print("It is basic mode"); |
| if (br_chan->hold_credit) { |
| br_chan->hold_credit = false; |
| bt_shell_warn("hold_credit is unsupported in basic mode"); |
| } |
| break; |
| case BT_L2CAP_BR_LINK_MODE_RET: |
| bt_shell_print("It is retransmission mode"); |
| break; |
| case BT_L2CAP_BR_LINK_MODE_FC: |
| bt_shell_print("It is flow control mode"); |
| break; |
| case BT_L2CAP_BR_LINK_MODE_ERET: |
| bt_shell_print("It is enhance retransmission mode"); |
| break; |
| case BT_L2CAP_BR_LINK_MODE_STREAM: |
| bt_shell_print("It is streaming mode"); |
| break; |
| default: |
| bt_shell_error("It is unknown mode"); |
| break; |
| } |
| #endif /* CONFIG_BT_L2CAP_RET_FC */ |
| (void)br_chan; |
| } |
| |
| static void l2cap_disconnected(struct bt_l2cap_chan *chan) |
| { |
| #if defined(CONFIG_BT_L2CAP_RET_FC) |
| struct net_buf *buf; |
| struct l2cap_br_chan *br_chan = CONTAINER_OF(chan, struct l2cap_br_chan, chan.chan); |
| #endif /* CONFIG_BT_L2CAP_RET_FC */ |
| |
| bt_shell_print("Channel %p disconnected", chan); |
| |
| #if defined(CONFIG_BT_L2CAP_RET_FC) |
| do { |
| buf = k_fifo_get(&br_chan->l2cap_recv_fifo, K_NO_WAIT); |
| if (buf != NULL) { |
| net_buf_unref(buf); |
| } |
| } while (buf != NULL); |
| #endif /* CONFIG_BT_L2CAP_RET_FC */ |
| } |
| |
| static struct net_buf *l2cap_alloc_buf(struct bt_l2cap_chan *chan) |
| { |
| bt_shell_print("Channel %p requires buffer", chan); |
| |
| return net_buf_alloc(&data_rx_pool, K_NO_WAIT); |
| } |
| |
| #if defined(CONFIG_BT_L2CAP_SEG_RECV) |
| static void seg_recv(struct bt_l2cap_chan *chan, size_t sdu_len, off_t seg_offset, |
| struct net_buf_simple *seg) |
| { |
| bt_shell_print("Incoming data channel %p SDU len %u offset %lu len %u", chan, sdu_len, |
| seg_offset, seg->len); |
| |
| if (seg->len) { |
| bt_shell_hexdump(seg->data, seg->len); |
| } |
| } |
| #endif /* CONFIG_BT_L2CAP_SEG_RECV */ |
| |
| static const struct bt_l2cap_chan_ops l2cap_ops = { |
| .alloc_buf = l2cap_alloc_buf, |
| .recv = l2cap_recv, |
| .connected = l2cap_connected, |
| .disconnected = l2cap_disconnected, |
| #if defined(CONFIG_BT_L2CAP_SEG_RECV) |
| .seg_recv = seg_recv, |
| #endif /* CONFIG_BT_L2CAP_SEG_RECV */ |
| }; |
| |
| #define BT_L2CAP_BR_SERVER_OPT_RET BIT(0) |
| #define BT_L2CAP_BR_SERVER_OPT_FC BIT(1) |
| #define BT_L2CAP_BR_SERVER_OPT_ERET BIT(2) |
| #define BT_L2CAP_BR_SERVER_OPT_STREAM BIT(3) |
| #define BT_L2CAP_BR_SERVER_OPT_MODE_OPTIONAL BIT(4) |
| #define BT_L2CAP_BR_SERVER_OPT_EXT_WIN_SIZE BIT(5) |
| #define BT_L2CAP_BR_SERVER_OPT_HOLD_CREDIT BIT(6) |
| |
| static struct l2cap_br_chan l2cap_chan = { |
| .chan = { |
| .chan.ops = &l2cap_ops, |
| /* Set for now min. MTU */ |
| .rx.mtu = DATA_BREDR_MTU, |
| }, |
| #if defined(CONFIG_BT_L2CAP_RET_FC) |
| .l2cap_recv_fifo = Z_FIFO_INITIALIZER(l2cap_chan.l2cap_recv_fifo), |
| #endif /* CONFIG_BT_L2CAP_RET_FC */ |
| }; |
| |
| static int l2cap_accept(struct bt_conn *conn, struct bt_l2cap_server *server, |
| struct bt_l2cap_chan **chan) |
| { |
| struct bt_l2cap_br_server *br_server; |
| |
| br_server = CONTAINER_OF(server, struct bt_l2cap_br_server, server); |
| |
| bt_shell_print("Incoming BR/EDR conn %p", conn); |
| |
| if (l2cap_chan.chan.chan.conn) { |
| bt_shell_error("No channels available"); |
| return -ENOMEM; |
| } |
| |
| *chan = &l2cap_chan.chan.chan; |
| |
| #if defined(CONFIG_BT_L2CAP_RET_FC) |
| if (br_server->options & BT_L2CAP_BR_SERVER_OPT_HOLD_CREDIT) { |
| l2cap_chan.hold_credit = true; |
| } else { |
| l2cap_chan.hold_credit = false; |
| } |
| |
| if (br_server->options & BT_L2CAP_BR_SERVER_OPT_EXT_WIN_SIZE) { |
| l2cap_chan.chan.rx.extended_control = true; |
| } else { |
| l2cap_chan.chan.rx.extended_control = false; |
| } |
| |
| if (br_server->options & BT_L2CAP_BR_SERVER_OPT_MODE_OPTIONAL) { |
| l2cap_chan.chan.rx.optional = true; |
| } else { |
| l2cap_chan.chan.rx.optional = false; |
| } |
| |
| l2cap_chan.chan.rx.fcs = BT_L2CAP_BR_FCS_16BIT; |
| |
| if (br_server->options & BT_L2CAP_BR_SERVER_OPT_STREAM) { |
| l2cap_chan.chan.rx.mode = BT_L2CAP_BR_LINK_MODE_STREAM; |
| l2cap_chan.chan.rx.max_window = CONFIG_BT_L2CAP_MAX_WINDOW_SIZE; |
| l2cap_chan.chan.rx.max_transmit = 0; |
| } else if (br_server->options & BT_L2CAP_BR_SERVER_OPT_ERET) { |
| l2cap_chan.chan.rx.mode = BT_L2CAP_BR_LINK_MODE_ERET; |
| l2cap_chan.chan.rx.max_window = CONFIG_BT_L2CAP_MAX_WINDOW_SIZE; |
| l2cap_chan.chan.rx.max_transmit = 3; |
| } else if (br_server->options & BT_L2CAP_BR_SERVER_OPT_FC) { |
| l2cap_chan.chan.rx.mode = BT_L2CAP_BR_LINK_MODE_FC; |
| l2cap_chan.chan.rx.max_window = CONFIG_BT_L2CAP_MAX_WINDOW_SIZE; |
| l2cap_chan.chan.rx.max_transmit = 3; |
| } else if (br_server->options & BT_L2CAP_BR_SERVER_OPT_RET) { |
| l2cap_chan.chan.rx.mode = BT_L2CAP_BR_LINK_MODE_RET; |
| l2cap_chan.chan.rx.max_window = CONFIG_BT_L2CAP_MAX_WINDOW_SIZE; |
| l2cap_chan.chan.rx.max_transmit = 3; |
| } |
| #endif /* CONFIG_BT_L2CAP_RET_FC */ |
| (void)br_server; |
| return 0; |
| } |
| |
| static struct bt_l2cap_br_server l2cap_server = { |
| .server = { |
| .accept = l2cap_accept, |
| }, |
| }; |
| |
| static int cmd_l2cap_register(const struct shell *sh, size_t argc, char *argv[]) |
| { |
| if (l2cap_server.server.psm) { |
| shell_print(sh, "Already registered"); |
| return -ENOEXEC; |
| } |
| |
| l2cap_server.server.psm = strtoul(argv[1], NULL, 16); |
| |
| #if defined(CONFIG_BT_L2CAP_RET_FC) |
| l2cap_server.options = 0; |
| |
| if (!strcmp(argv[2], "none")) { |
| /* Support mode: None */ |
| } else if (!strcmp(argv[2], "ret")) { |
| l2cap_server.options |= BT_L2CAP_BR_SERVER_OPT_RET; |
| } else if (!strcmp(argv[2], "fc")) { |
| l2cap_server.options |= BT_L2CAP_BR_SERVER_OPT_FC; |
| } else if (!strcmp(argv[2], "eret")) { |
| l2cap_server.options |= BT_L2CAP_BR_SERVER_OPT_ERET; |
| } else if (!strcmp(argv[2], "stream")) { |
| l2cap_server.options |= BT_L2CAP_BR_SERVER_OPT_STREAM; |
| } else { |
| l2cap_server.server.psm = 0; |
| shell_help(sh); |
| return SHELL_CMD_HELP_PRINTED; |
| } |
| |
| for (size_t index = 3; index < argc; index++) { |
| if (!strcmp(argv[index], "hold_credit")) { |
| l2cap_server.options |= BT_L2CAP_BR_SERVER_OPT_HOLD_CREDIT; |
| } else if (!strcmp(argv[index], "mode_optional")) { |
| l2cap_server.options |= BT_L2CAP_BR_SERVER_OPT_MODE_OPTIONAL; |
| } else if (!strcmp(argv[index], "extended_control")) { |
| l2cap_server.options |= BT_L2CAP_BR_SERVER_OPT_EXT_WIN_SIZE; |
| } else { |
| l2cap_server.server.psm = 0; |
| shell_help(sh); |
| return SHELL_CMD_HELP_PRINTED; |
| } |
| } |
| |
| if ((l2cap_server.options & BT_L2CAP_BR_SERVER_OPT_EXT_WIN_SIZE) && |
| (!(l2cap_server.options & |
| (BT_L2CAP_BR_SERVER_OPT_ERET | BT_L2CAP_BR_SERVER_OPT_STREAM)))) { |
| shell_error(sh, "[extended_control] only supports mode eret and stream"); |
| l2cap_server.server.psm = 0U; |
| return -ENOEXEC; |
| } |
| #endif /* CONFIG_BT_L2CAP_RET_FC */ |
| |
| if (bt_l2cap_br_server_register(&l2cap_server.server) < 0) { |
| shell_error(sh, "Unable to register psm"); |
| l2cap_server.server.psm = 0U; |
| return -ENOEXEC; |
| } |
| |
| shell_print(sh, "L2CAP psm %u registered", l2cap_server.server.psm); |
| |
| return 0; |
| } |
| |
| static int cmd_l2cap_connect(const struct shell *sh, size_t argc, char *argv[]) |
| { |
| uint16_t psm; |
| struct bt_conn_info info; |
| int err; |
| |
| if (!default_conn) { |
| shell_error(sh, "Not connected"); |
| return -ENOEXEC; |
| } |
| |
| if (l2cap_chan.chan.chan.conn) { |
| bt_shell_error("No channels available"); |
| return -ENOMEM; |
| } |
| |
| err = bt_conn_get_info(default_conn, &info); |
| if ((err < 0) || (info.type != BT_CONN_TYPE_BR)) { |
| shell_error(sh, "Invalid conn type"); |
| return -ENOEXEC; |
| } |
| |
| psm = strtoul(argv[1], NULL, 16); |
| |
| #if defined(CONFIG_BT_L2CAP_RET_FC) |
| if (!strcmp(argv[2], "none")) { |
| l2cap_chan.chan.rx.mode = BT_L2CAP_BR_LINK_MODE_BASIC; |
| } else if (!strcmp(argv[2], "ret")) { |
| l2cap_chan.chan.rx.mode = BT_L2CAP_BR_LINK_MODE_RET; |
| l2cap_chan.chan.rx.max_transmit = 3; |
| } else if (!strcmp(argv[2], "fc")) { |
| l2cap_chan.chan.rx.mode = BT_L2CAP_BR_LINK_MODE_FC; |
| l2cap_chan.chan.rx.max_transmit = 3; |
| } else if (!strcmp(argv[2], "eret")) { |
| l2cap_chan.chan.rx.mode = BT_L2CAP_BR_LINK_MODE_ERET; |
| l2cap_chan.chan.rx.max_transmit = 3; |
| } else if (!strcmp(argv[2], "stream")) { |
| l2cap_chan.chan.rx.mode = BT_L2CAP_BR_LINK_MODE_STREAM; |
| l2cap_chan.chan.rx.max_transmit = 0; |
| } else { |
| shell_help(sh); |
| return SHELL_CMD_HELP_PRINTED; |
| } |
| |
| l2cap_chan.hold_credit = false; |
| l2cap_chan.chan.rx.optional = false; |
| l2cap_chan.chan.rx.extended_control = false; |
| |
| for (size_t index = 3; index < argc; index++) { |
| if (!strcmp(argv[index], "hold_credit")) { |
| l2cap_chan.hold_credit = true; |
| } else if (!strcmp(argv[index], "mode_optional")) { |
| l2cap_chan.chan.rx.optional = true; |
| } else if (!strcmp(argv[index], "extended_control")) { |
| l2cap_chan.chan.rx.extended_control = true; |
| } else { |
| shell_help(sh); |
| return SHELL_CMD_HELP_PRINTED; |
| } |
| } |
| |
| if ((l2cap_chan.chan.rx.extended_control) && |
| ((l2cap_chan.chan.rx.mode != BT_L2CAP_BR_LINK_MODE_ERET) && |
| (l2cap_chan.chan.rx.mode != BT_L2CAP_BR_LINK_MODE_STREAM))) { |
| shell_error(sh, "[extended_control] only supports mode eret and stream"); |
| return -ENOEXEC; |
| } |
| |
| if (l2cap_chan.hold_credit && (l2cap_chan.chan.rx.mode == BT_L2CAP_BR_LINK_MODE_BASIC)) { |
| shell_error(sh, "[hold_credit] cannot support basic mode"); |
| return -ENOEXEC; |
| } |
| |
| l2cap_chan.chan.rx.max_window = CONFIG_BT_L2CAP_MAX_WINDOW_SIZE; |
| #endif /* CONFIG_BT_L2CAP_RET_FC */ |
| |
| err = bt_l2cap_chan_connect(default_conn, &l2cap_chan.chan.chan, psm); |
| if (err < 0) { |
| shell_error(sh, "Unable to connect to psm %u (err %d)", psm, err); |
| } else { |
| shell_print(sh, "L2CAP connection pending"); |
| } |
| |
| return err; |
| } |
| |
| static int cmd_l2cap_disconnect(const struct shell *sh, size_t argc, char *argv[]) |
| { |
| int err; |
| |
| err = bt_l2cap_chan_disconnect(&l2cap_chan.chan.chan); |
| if (err) { |
| shell_error(sh, "Unable to disconnect: %u", -err); |
| } |
| |
| return err; |
| } |
| |
| static int cmd_l2cap_send(const struct shell *sh, size_t argc, char *argv[]) |
| { |
| static uint8_t buf_data[DATA_BREDR_MTU]; |
| int err, len = DATA_BREDR_MTU, count = 1; |
| struct net_buf *buf; |
| |
| if (argc > 1) { |
| count = strtoul(argv[1], NULL, 10); |
| } |
| |
| if (argc > 2) { |
| len = strtoul(argv[2], NULL, 10); |
| if (len > DATA_BREDR_MTU) { |
| shell_error(sh, "Length exceeds TX MTU for the channel"); |
| return -ENOEXEC; |
| } |
| } |
| |
| len = MIN(l2cap_chan.chan.tx.mtu, len); |
| |
| while (count--) { |
| shell_print(sh, "Rem %d", count); |
| buf = net_buf_alloc(&data_tx_pool, K_SECONDS(2)); |
| if (!buf) { |
| if (l2cap_chan.chan.state != BT_L2CAP_CONNECTED) { |
| shell_error(sh, "Channel disconnected, stopping TX"); |
| |
| return -EAGAIN; |
| } |
| shell_error(sh, "Allocation timeout, stopping TX"); |
| |
| return -EAGAIN; |
| } |
| net_buf_reserve(buf, BT_L2CAP_CHAN_SEND_RESERVE); |
| memset(buf_data, count, sizeof(buf_data)); |
| |
| net_buf_add_mem(buf, buf_data, len); |
| err = bt_l2cap_chan_send(&l2cap_chan.chan.chan, buf); |
| if (err < 0) { |
| shell_error(sh, "Unable to send: %d", -err); |
| net_buf_unref(buf); |
| return -ENOEXEC; |
| } |
| } |
| |
| return 0; |
| } |
| |
| #if defined(CONFIG_BT_L2CAP_RET_FC) |
| static int cmd_l2cap_credits(const struct shell *sh, size_t argc, char *argv[]) |
| { |
| int err; |
| struct net_buf *buf; |
| |
| buf = k_fifo_get(&l2cap_chan.l2cap_recv_fifo, K_NO_WAIT); |
| if (buf != NULL) { |
| err = bt_l2cap_chan_recv_complete(&l2cap_chan.chan.chan, buf); |
| if (err < 0) { |
| shell_error(sh, "Unable to set recv_complete: %d", -err); |
| } |
| } else { |
| shell_warn(sh, "No pending recv buffer"); |
| } |
| |
| return 0; |
| } |
| #endif /* CONFIG_BT_L2CAP_RET_FC */ |
| |
| static void l2cap_br_echo_req(struct bt_conn *conn, uint8_t identifier, struct net_buf *buf) |
| { |
| bt_shell_print("Incoming ECHO REQ data identifier %u len %u", identifier, buf->len); |
| |
| if (buf->len > 0) { |
| bt_shell_hexdump(buf->data, buf->len); |
| } |
| } |
| |
| static void l2cap_br_echo_rsp(struct bt_conn *conn, struct net_buf *buf) |
| { |
| bt_shell_print("Incoming ECHO RSP data len %u", buf->len); |
| |
| if (buf->len > 0) { |
| bt_shell_hexdump(buf->data, buf->len); |
| } |
| } |
| |
| static struct bt_l2cap_br_echo_cb echo_cb = { |
| .req = l2cap_br_echo_req, |
| .rsp = l2cap_br_echo_rsp, |
| }; |
| |
| static int cmd_l2cap_echo_reg(const struct shell *sh, size_t argc, char *argv[]) |
| { |
| int err; |
| |
| err = bt_l2cap_br_echo_cb_register(&echo_cb); |
| if (err) { |
| shell_error(sh, "Failed to register echo callback: %d", -err); |
| return err; |
| } |
| |
| return 0; |
| } |
| |
| static int cmd_l2cap_echo_unreg(const struct shell *sh, size_t argc, char *argv[]) |
| { |
| int err; |
| |
| err = bt_l2cap_br_echo_cb_unregister(&echo_cb); |
| if (err) { |
| shell_error(sh, "Failed to unregister echo callback: %d", -err); |
| return err; |
| } |
| |
| return 0; |
| } |
| |
| static int cmd_l2cap_echo_req(const struct shell *sh, size_t argc, char *argv[]) |
| { |
| static uint8_t buf_data[DATA_BREDR_MTU]; |
| int err, len = DATA_BREDR_MTU; |
| struct net_buf *buf; |
| |
| len = strtoul(argv[1], NULL, 10); |
| if (len > DATA_BREDR_MTU) { |
| shell_error(sh, "Length exceeds TX MTU for the channel"); |
| return -ENOEXEC; |
| } |
| |
| buf = net_buf_alloc(&data_tx_pool, K_SECONDS(2)); |
| if (!buf) { |
| shell_error(sh, "Allocation timeout, stopping TX"); |
| return -EAGAIN; |
| } |
| net_buf_reserve(buf, BT_L2CAP_BR_ECHO_REQ_RESERVE); |
| for (int i = 0; i < len; i++) { |
| buf_data[i] = (uint8_t)i; |
| } |
| |
| net_buf_add_mem(buf, buf_data, len); |
| err = bt_l2cap_br_echo_req(default_conn, buf); |
| if (err < 0) { |
| shell_error(sh, "Unable to send ECHO REQ: %d", -err); |
| net_buf_unref(buf); |
| return -ENOEXEC; |
| } |
| |
| return 0; |
| } |
| |
| static int cmd_l2cap_echo_rsp(const struct shell *sh, size_t argc, char *argv[]) |
| { |
| static uint8_t buf_data[DATA_BREDR_MTU]; |
| int err, len = DATA_BREDR_MTU; |
| uint8_t identifier; |
| struct net_buf *buf; |
| |
| identifier = (uint8_t)strtoul(argv[1], NULL, 10); |
| |
| len = strtoul(argv[2], NULL, 10); |
| if (len > DATA_BREDR_MTU) { |
| shell_error(sh, "Length exceeds TX MTU for the channel"); |
| return -ENOEXEC; |
| } |
| |
| buf = net_buf_alloc(&data_tx_pool, K_SECONDS(2)); |
| if (!buf) { |
| shell_error(sh, "Allocation timeout, stopping TX"); |
| return -EAGAIN; |
| } |
| net_buf_reserve(buf, BT_L2CAP_BR_ECHO_RSP_RESERVE); |
| for (int i = 0; i < len; i++) { |
| buf_data[i] = (uint8_t)i; |
| } |
| |
| net_buf_add_mem(buf, buf_data, len); |
| err = bt_l2cap_br_echo_rsp(default_conn, identifier, buf); |
| if (err < 0) { |
| shell_error(sh, "Unable to send ECHO RSP: %d", -err); |
| net_buf_unref(buf); |
| return -ENOEXEC; |
| } |
| |
| return 0; |
| } |
| |
| static int cmd_discoverable(const struct shell *sh, size_t argc, char *argv[]) |
| { |
| int err = 0; |
| bool enable; |
| bool limited = false; |
| |
| enable = shell_strtobool(argv[1], 10, &err); |
| if (err) { |
| shell_help(sh); |
| return SHELL_CMD_HELP_PRINTED; |
| } |
| |
| if (argc > 2 && !strcmp(argv[2], "limited")) { |
| limited = true; |
| } |
| |
| err = bt_br_set_discoverable(enable, limited); |
| if (err) { |
| shell_print(sh, "BR/EDR set/reset discoverable failed (err %d)", err); |
| return -ENOEXEC; |
| } |
| |
| shell_print(sh, "BR/EDR set/reset discoverable done"); |
| |
| return 0; |
| } |
| |
| static int cmd_connectable(const struct shell *sh, size_t argc, char *argv[]) |
| { |
| int err; |
| const char *action; |
| |
| action = argv[1]; |
| |
| if (!strcmp(action, "on")) { |
| err = bt_br_set_connectable(true); |
| } else if (!strcmp(action, "off")) { |
| err = bt_br_set_connectable(false); |
| } else { |
| shell_help(sh); |
| return SHELL_CMD_HELP_PRINTED; |
| } |
| |
| if (err) { |
| shell_print(sh, "BR/EDR set/rest connectable failed (err %d)", err); |
| return -ENOEXEC; |
| } |
| |
| shell_print(sh, "BR/EDR set/reset connectable done"); |
| |
| return 0; |
| } |
| |
| static int cmd_oob(const struct shell *sh, size_t argc, char *argv[]) |
| { |
| char addr[BT_ADDR_STR_LEN]; |
| struct bt_br_oob oob; |
| int err; |
| |
| err = bt_br_oob_get_local(&oob); |
| if (err) { |
| shell_print(sh, "BR/EDR OOB data failed"); |
| return -ENOEXEC; |
| } |
| |
| bt_addr_to_str(&oob.addr, addr, sizeof(addr)); |
| |
| shell_print(sh, "BR/EDR OOB data:"); |
| shell_print(sh, " addr %s", addr); |
| return 0; |
| } |
| |
| static uint8_t sdp_hfp_ag_user(struct bt_conn *conn, struct bt_sdp_client_result *result, |
| const struct bt_sdp_discover_params *params) |
| { |
| char addr[BT_ADDR_STR_LEN]; |
| uint16_t param, version; |
| uint16_t features; |
| int err; |
| |
| conn_addr_str(conn, addr, sizeof(addr)); |
| |
| if (result && result->resp_buf) { |
| bt_shell_print("SDP HFPAG data@%p (len %u) hint %u from remote %s", |
| result->resp_buf, result->resp_buf->len, result->next_record_hint, |
| addr); |
| |
| /* |
| * Focus to get BT_SDP_ATTR_PROTO_DESC_LIST attribute item to |
| * get HFPAG Server Channel Number operating on RFCOMM protocol. |
| */ |
| err = bt_sdp_get_proto_param(result->resp_buf, BT_SDP_PROTO_RFCOMM, ¶m); |
| if (err < 0) { |
| bt_shell_error("Error getting Server CN, err %d", err); |
| goto done; |
| } |
| bt_shell_print("HFPAG Server CN param 0x%04x", param); |
| |
| err = bt_sdp_get_profile_version(result->resp_buf, BT_SDP_HANDSFREE_SVCLASS, |
| &version); |
| if (err < 0) { |
| bt_shell_error("Error getting profile version, err %d", err); |
| goto done; |
| } |
| bt_shell_print("HFP version param 0x%04x", version); |
| |
| /* |
| * Focus to get BT_SDP_ATTR_SUPPORTED_FEATURES attribute item to |
| * get profile Supported Features mask. |
| */ |
| err = bt_sdp_get_features(result->resp_buf, &features); |
| if (err < 0) { |
| bt_shell_error("Error getting HFPAG Features, err %d", err); |
| goto done; |
| } |
| bt_shell_print("HFPAG Supported Features param 0x%04x", features); |
| } else { |
| bt_shell_print("No SDP HFPAG data from remote %s", addr); |
| } |
| done: |
| return BT_SDP_DISCOVER_UUID_CONTINUE; |
| } |
| |
| static uint8_t sdp_hfp_hf_user(struct bt_conn *conn, |
| struct bt_sdp_client_result *result, |
| const struct bt_sdp_discover_params *params) |
| { |
| char addr[BT_ADDR_STR_LEN]; |
| uint16_t param, version; |
| uint16_t features; |
| int err; |
| |
| conn_addr_str(conn, addr, sizeof(addr)); |
| |
| if (result && result->resp_buf) { |
| bt_shell_print("SDP HFPHF data@%p (len %u) hint %u from remote %s", |
| result->resp_buf, result->resp_buf->len, result->next_record_hint, |
| addr); |
| |
| /* |
| * Focus to get BT_SDP_ATTR_PROTO_DESC_LIST attribute item to |
| * get HFPHF Server Channel Number operating on RFCOMM protocol. |
| */ |
| err = bt_sdp_get_proto_param(result->resp_buf, BT_SDP_PROTO_RFCOMM, ¶m); |
| if (err < 0) { |
| bt_shell_error("Error getting Server CN, err %d", err); |
| goto done; |
| } |
| bt_shell_print("HFPHF Server CN param 0x%04x", param); |
| |
| err = bt_sdp_get_profile_version(result->resp_buf, BT_SDP_HANDSFREE_SVCLASS, |
| &version); |
| if (err < 0) { |
| bt_shell_error("Error getting profile version, err %d", err); |
| goto done; |
| } |
| bt_shell_print("HFP version param 0x%04x", version); |
| |
| /* |
| * Focus to get BT_SDP_ATTR_SUPPORTED_FEATURES attribute item to |
| * get profile Supported Features mask. |
| */ |
| err = bt_sdp_get_features(result->resp_buf, &features); |
| if (err < 0) { |
| bt_shell_error("Error getting HFPHF Features, err %d", err); |
| goto done; |
| } |
| bt_shell_print("HFPHF Supported Features param 0x%04x", features); |
| } else { |
| bt_shell_print("No SDP HFPHF data from remote %s", addr); |
| } |
| done: |
| return BT_SDP_DISCOVER_UUID_CONTINUE; |
| } |
| |
| static uint8_t sdp_a2src_user(struct bt_conn *conn, struct bt_sdp_client_result *result, |
| const struct bt_sdp_discover_params *params) |
| { |
| char addr[BT_ADDR_STR_LEN]; |
| uint16_t param, version; |
| uint16_t features; |
| int err; |
| |
| conn_addr_str(conn, addr, sizeof(addr)); |
| |
| if (result && result->resp_buf) { |
| bt_shell_print("SDP A2SRC data@%p (len %u) hint %u from remote %s", |
| result->resp_buf, result->resp_buf->len, result->next_record_hint, |
| addr); |
| |
| /* |
| * Focus to get BT_SDP_ATTR_PROTO_DESC_LIST attribute item to |
| * get A2SRC Server PSM Number. |
| */ |
| err = bt_sdp_get_proto_param(result->resp_buf, BT_SDP_PROTO_L2CAP, ¶m); |
| if (err < 0) { |
| bt_shell_error("A2SRC PSM Number not found, err %d", err); |
| goto done; |
| } |
| |
| bt_shell_print("A2SRC Server PSM Number param 0x%04x", param); |
| |
| /* |
| * Focus to get BT_SDP_ATTR_PROFILE_DESC_LIST attribute item to |
| * get profile version number. |
| */ |
| err = bt_sdp_get_profile_version(result->resp_buf, BT_SDP_ADVANCED_AUDIO_SVCLASS, |
| &version); |
| if (err < 0) { |
| bt_shell_error("A2SRC version not found, err %d", err); |
| goto done; |
| } |
| bt_shell_print("A2SRC version param 0x%04x", version); |
| |
| /* |
| * Focus to get BT_SDP_ATTR_SUPPORTED_FEATURES attribute item to |
| * get profile supported features mask. |
| */ |
| err = bt_sdp_get_features(result->resp_buf, &features); |
| if (err < 0) { |
| bt_shell_error("A2SRC Features not found, err %d", err); |
| goto done; |
| } |
| bt_shell_print("A2SRC Supported Features param 0x%04x", features); |
| } else { |
| bt_shell_print("No SDP A2SRC data from remote %s", addr); |
| } |
| done: |
| return BT_SDP_DISCOVER_UUID_CONTINUE; |
| } |
| |
| static struct bt_sdp_discover_params discov_hfpag = { |
| .type = BT_SDP_DISCOVER_SERVICE_SEARCH_ATTR, |
| .uuid = BT_UUID_DECLARE_16(BT_SDP_HANDSFREE_AGW_SVCLASS), |
| .func = sdp_hfp_ag_user, |
| .pool = &sdp_client_pool, |
| }; |
| |
| static struct bt_sdp_discover_params discov_hfphf = { |
| .type = BT_SDP_DISCOVER_SERVICE_SEARCH_ATTR, |
| .uuid = BT_UUID_DECLARE_16(BT_SDP_HANDSFREE_SVCLASS), |
| .func = sdp_hfp_hf_user, |
| .pool = &sdp_client_pool, |
| }; |
| |
| static struct bt_sdp_discover_params discov_a2src = { |
| .type = BT_SDP_DISCOVER_SERVICE_SEARCH_ATTR, |
| .uuid = BT_UUID_DECLARE_16(BT_SDP_AUDIO_SOURCE_SVCLASS), |
| .func = sdp_a2src_user, |
| .pool = &sdp_client_pool, |
| }; |
| |
| static struct bt_sdp_discover_params discov; |
| |
| static int cmd_sdp_find_record(const struct shell *sh, size_t argc, char *argv[]) |
| { |
| int err; |
| const char *action; |
| |
| if (!default_conn) { |
| shell_print(sh, "Not connected"); |
| return -ENOEXEC; |
| } |
| |
| action = argv[1]; |
| |
| if (!strcmp(action, "HFPAG")) { |
| discov = discov_hfpag; |
| } else if (!strcmp(action, "HFPHF")) { |
| discov = discov_hfphf; |
| } else if (!strcmp(action, "A2SRC")) { |
| discov = discov_a2src; |
| } else { |
| shell_help(sh); |
| return SHELL_CMD_HELP_PRINTED; |
| } |
| |
| shell_print(sh, "SDP UUID \'%s\' gets applied", action); |
| |
| err = bt_sdp_discover(default_conn, &discov); |
| if (err) { |
| shell_error(sh, "SDP discovery failed: err %d", err); |
| return -ENOEXEC; |
| } |
| |
| shell_print(sh, "SDP discovery started"); |
| |
| return 0; |
| } |
| |
| static void bond_info(const struct bt_br_bond_info *info, void *user_data) |
| { |
| char addr[BT_ADDR_STR_LEN]; |
| int *bond_count = user_data; |
| |
| bt_addr_to_str(&info->addr, addr, sizeof(addr)); |
| bt_shell_print("Remote Identity: %s", addr); |
| (*bond_count)++; |
| } |
| |
| static int cmd_bonds(const struct shell *sh, size_t argc, char *argv[]) |
| { |
| int bond_count = 0; |
| |
| shell_print(sh, "Bonded devices:"); |
| bt_br_foreach_bond(bond_info, &bond_count); |
| shell_print(sh, "Total %d", bond_count); |
| |
| return 0; |
| } |
| |
| static int cmd_clear(const struct shell *sh, size_t argc, char *argv[]) |
| { |
| bt_addr_t addr; |
| int err; |
| |
| if (strcmp(argv[1], "all") == 0) { |
| err = bt_br_unpair(NULL); |
| if (err) { |
| shell_error(sh, "Failed to clear pairings (err %d)", err); |
| return err; |
| } |
| |
| shell_print(sh, "Pairings successfully cleared"); |
| return 0; |
| } |
| |
| err = bt_addr_from_str(argv[1], &addr); |
| if (err) { |
| shell_print(sh, "Invalid address"); |
| return err; |
| } |
| |
| err = bt_br_unpair(&addr); |
| if (err) { |
| shell_error(sh, "Failed to clear pairing (err %d)", err); |
| } else { |
| shell_print(sh, "Pairing successfully cleared"); |
| } |
| |
| return err; |
| } |
| |
| static int cmd_select(const struct shell *sh, size_t argc, char *argv[]) |
| { |
| char addr_str[BT_ADDR_STR_LEN]; |
| struct bt_conn *conn; |
| bt_addr_t addr; |
| int err; |
| |
| err = bt_addr_from_str(argv[1], &addr); |
| if (err) { |
| shell_error(sh, "Invalid peer address (err %d)", err); |
| return err; |
| } |
| |
| conn = bt_conn_lookup_addr_br(&addr); |
| if (!conn) { |
| shell_error(sh, "No matching connection found"); |
| return -ENOEXEC; |
| } |
| |
| if (default_conn != NULL) { |
| bt_conn_unref(default_conn); |
| } |
| |
| default_conn = conn; |
| |
| bt_addr_to_str(&addr, addr_str, sizeof(addr_str)); |
| shell_print(sh, "Selected conn is now: %s", addr_str); |
| |
| return 0; |
| } |
| |
| static const char *get_conn_type_str(uint8_t type) |
| { |
| switch (type) { |
| case BT_CONN_TYPE_LE: return "LE"; |
| case BT_CONN_TYPE_BR: return "BR/EDR"; |
| case BT_CONN_TYPE_SCO: return "SCO"; |
| default: return "Invalid"; |
| } |
| } |
| |
| static const char *get_conn_role_str(uint8_t role) |
| { |
| switch (role) { |
| case BT_CONN_ROLE_CENTRAL: return "central"; |
| case BT_CONN_ROLE_PERIPHERAL: return "peripheral"; |
| default: return "Invalid"; |
| } |
| } |
| |
| static int cmd_info(const struct shell *sh, size_t argc, char *argv[]) |
| { |
| struct bt_conn *conn = NULL; |
| struct bt_conn_info info; |
| bt_addr_t addr; |
| int err; |
| |
| if (argc > 1) { |
| err = bt_addr_from_str(argv[1], &addr); |
| if (err) { |
| shell_error(sh, "Invalid peer address (err %d)", err); |
| return err; |
| } |
| conn = bt_conn_lookup_addr_br(&addr); |
| } else { |
| if (default_conn) { |
| conn = bt_conn_ref(default_conn); |
| } |
| } |
| |
| if (!conn) { |
| shell_error(sh, "Not connected"); |
| return -ENOEXEC; |
| } |
| |
| err = bt_conn_get_info(conn, &info); |
| if (err) { |
| shell_print(sh, "Failed to get info"); |
| goto done; |
| } |
| |
| shell_print(sh, "Type: %s, Role: %s, Id: %u", |
| get_conn_type_str(info.type), |
| get_conn_role_str(info.role), |
| info.id); |
| |
| if (info.type == BT_CONN_TYPE_BR) { |
| char addr_str[BT_ADDR_STR_LEN]; |
| |
| bt_addr_to_str(info.br.dst, addr_str, sizeof(addr_str)); |
| shell_print(sh, "Peer address %s", addr_str); |
| } |
| |
| done: |
| bt_conn_unref(conn); |
| |
| return err; |
| } |
| |
| static int cmd_default_handler(const struct shell *sh, size_t argc, char **argv) |
| { |
| if (argc == 1) { |
| shell_help(sh); |
| return SHELL_CMD_HELP_PRINTED; |
| } |
| |
| shell_error(sh, "%s unknown parameter: %s", argv[0], argv[1]); |
| |
| return -EINVAL; |
| } |
| |
| #define HELP_NONE "[none]" |
| #define HELP_ADDR "<address: XX:XX:XX:XX:XX:XX>" |
| #define HELP_REG \ |
| "<psm> <mode: none, ret, fc, eret, stream> [hold_credit] " \ |
| "[mode_optional] [extended_control]" |
| |
| #define HELP_CONN \ |
| "<psm> <mode: none, ret, fc, eret, stream> [hold_credit] " \ |
| "[mode_optional] [extended_control]" |
| |
| SHELL_STATIC_SUBCMD_SET_CREATE(echo_cmds, |
| SHELL_CMD_ARG(register, NULL, HELP_NONE, cmd_l2cap_echo_reg, 1, 0), |
| SHELL_CMD_ARG(unregister, NULL, HELP_NONE, cmd_l2cap_echo_unreg, 1, 0), |
| SHELL_CMD_ARG(req, NULL, "<length of data>", cmd_l2cap_echo_req, 2, 0), |
| SHELL_CMD_ARG(rsp, NULL, "<identifier> <length of data>", cmd_l2cap_echo_rsp, 3, 0), |
| SHELL_SUBCMD_SET_END |
| ); |
| |
| SHELL_STATIC_SUBCMD_SET_CREATE(l2cap_cmds, |
| #if defined(CONFIG_BT_L2CAP_RET_FC) |
| SHELL_CMD_ARG(register, NULL, HELP_REG, cmd_l2cap_register, 3, 3), |
| SHELL_CMD_ARG(connect, NULL, HELP_CONN, cmd_l2cap_connect, 3, 3), |
| #else |
| SHELL_CMD_ARG(register, NULL, "<psm>", cmd_l2cap_register, 2, 0), |
| SHELL_CMD_ARG(connect, NULL, "<psm>", cmd_l2cap_connect, 2, 0), |
| #endif /* CONFIG_BT_L2CAP_RET_FC */ |
| SHELL_CMD_ARG(disconnect, NULL, HELP_NONE, cmd_l2cap_disconnect, 1, 0), |
| SHELL_CMD_ARG(send, NULL, "[number of packets] [length of packet(s)]", |
| cmd_l2cap_send, 1, 2), |
| #if defined(CONFIG_BT_L2CAP_RET_FC) |
| SHELL_CMD_ARG(credits, NULL, HELP_NONE, cmd_l2cap_credits, 1, 0), |
| #endif /* CONFIG_BT_L2CAP_RET_FC */ |
| SHELL_CMD(echo, &echo_cmds, "L2CAP BR ECHO commands", cmd_default_handler), |
| SHELL_SUBCMD_SET_END |
| ); |
| |
| SHELL_STATIC_SUBCMD_SET_CREATE(br_cmds, |
| SHELL_CMD_ARG(auth-pincode, NULL, "<pincode>", cmd_auth_pincode, 2, 0), |
| SHELL_CMD_ARG(connect, NULL, "<address>", cmd_connect, 2, 0), |
| SHELL_CMD_ARG(bonds, NULL, HELP_NONE, cmd_bonds, 1, 0), |
| SHELL_CMD_ARG(clear, NULL, "[all] ["HELP_ADDR"]", cmd_clear, 2, 0), |
| SHELL_CMD_ARG(select, NULL, HELP_ADDR, cmd_select, 2, 0), |
| SHELL_CMD_ARG(info, NULL, HELP_ADDR, cmd_info, 1, 1), |
| SHELL_CMD_ARG(discovery, NULL, "<value: on, off> [length: 1-48] [mode: limited]", |
| cmd_discovery, 2, 2), |
| SHELL_CMD_ARG(iscan, NULL, "<value: on, off> [mode: limited]", |
| cmd_discoverable, 2, 1), |
| SHELL_CMD(l2cap, &l2cap_cmds, HELP_NONE, cmd_default_handler), |
| SHELL_CMD_ARG(oob, NULL, NULL, cmd_oob, 1, 0), |
| SHELL_CMD_ARG(pscan, NULL, "<value: on, off>", cmd_connectable, 2, 0), |
| SHELL_CMD_ARG(sdp-find, NULL, "<HFPAG, HFPHF>", cmd_sdp_find_record, 2, 0), |
| SHELL_SUBCMD_SET_END |
| ); |
| |
| SHELL_CMD_ARG_REGISTER(br, &br_cmds, "Bluetooth BR/EDR shell commands", cmd_default_handler, 1, 1); |