blob: 086f27eb4416664c2d52d2d3b22f889038ecb40e [file] [log] [blame]
/** @file
* @brief Bluetooth shell module
*
* Provide some Bluetooth shell commands that can be useful to applications.
*/
/*
* Copyright (c) 2017 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/zephyr.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/rfcomm.h>
#include <zephyr/bluetooth/sdp.h>
#include <zephyr/shell/shell.h>
#include "bt.h"
#define CREDITS 10
#define DATA_MTU (23 * CREDITS)
#define L2CAP_POLICY_NONE 0x00
#define L2CAP_POLICY_ALLOWLIST 0x01
#define L2CAP_POLICY_16BYTE_KEY 0x02
NET_BUF_POOL_FIXED_DEFINE(data_tx_pool, 1,
BT_L2CAP_SDU_BUF_SIZE(DATA_MTU), 8, NULL);
NET_BUF_POOL_FIXED_DEFINE(data_rx_pool, 1, DATA_MTU, 8, NULL);
static uint8_t l2cap_policy;
static struct bt_conn *l2cap_allowlist[CONFIG_BT_MAX_CONN];
static uint32_t l2cap_rate;
static uint32_t l2cap_recv_delay_ms;
static K_FIFO_DEFINE(l2cap_recv_fifo);
struct l2ch {
struct k_work_delayable recv_work;
struct bt_l2cap_le_chan ch;
};
#define L2CH_CHAN(_chan) CONTAINER_OF(_chan, struct l2ch, ch.chan)
#define L2CH_WORK(_work) CONTAINER_OF(_work, struct l2ch, recv_work)
#define L2CAP_CHAN(_chan) _chan->ch.chan
static bool metrics;
static int l2cap_recv_metrics(struct bt_l2cap_chan *chan, struct net_buf *buf)
{
static uint32_t len;
static uint32_t cycle_stamp;
uint32_t delta;
delta = k_cycle_get_32() - cycle_stamp;
delta = (uint32_t)k_cyc_to_ns_floor64(delta);
/* if last data rx-ed was greater than 1 second in the past,
* reset the metrics.
*/
if (delta > 1000000000) {
len = 0U;
l2cap_rate = 0U;
cycle_stamp = k_cycle_get_32();
} else {
len += buf->len;
l2cap_rate = ((uint64_t)len << 3) * 1000000000U / delta;
}
return 0;
}
static void l2cap_recv_cb(struct k_work *work)
{
struct l2ch *c = L2CH_WORK(work);
struct net_buf *buf;
while ((buf = net_buf_get(&l2cap_recv_fifo, K_NO_WAIT))) {
shell_print(ctx_shell, "Confirming reception");
bt_l2cap_chan_recv_complete(&c->ch.chan, buf);
}
}
static int l2cap_recv(struct bt_l2cap_chan *chan, struct net_buf *buf)
{
struct l2ch *l2ch = L2CH_CHAN(chan);
if (metrics) {
return l2cap_recv_metrics(chan, buf);
}
shell_print(ctx_shell, "Incoming data channel %p len %u", chan,
buf->len);
if (buf->len) {
shell_hexdump(ctx_shell, buf->data, buf->len);
}
if (l2cap_recv_delay_ms > 0) {
/* Submit work only if queue is empty */
if (k_fifo_is_empty(&l2cap_recv_fifo)) {
shell_print(ctx_shell, "Delaying response in %u ms...",
l2cap_recv_delay_ms);
}
net_buf_put(&l2cap_recv_fifo, buf);
k_work_schedule(&l2ch->recv_work, K_MSEC(l2cap_recv_delay_ms));
return -EINPROGRESS;
}
return 0;
}
static void l2cap_sent(struct bt_l2cap_chan *chan)
{
shell_print(ctx_shell, "Outgoing data channel %p transmitted", chan);
}
static void l2cap_status(struct bt_l2cap_chan *chan, atomic_t *status)
{
shell_print(ctx_shell, "Channel %p status %u", chan, (uint32_t)*status);
}
static void l2cap_connected(struct bt_l2cap_chan *chan)
{
struct l2ch *c = L2CH_CHAN(chan);
k_work_init_delayable(&c->recv_work, l2cap_recv_cb);
shell_print(ctx_shell, "Channel %p connected", chan);
}
static void l2cap_disconnected(struct bt_l2cap_chan *chan)
{
shell_print(ctx_shell, "Channel %p disconnected", chan);
}
static struct net_buf *l2cap_alloc_buf(struct bt_l2cap_chan *chan)
{
/* print if metrics is disabled */
if (!metrics) {
shell_print(ctx_shell, "Channel %p requires buffer", chan);
}
return net_buf_alloc(&data_rx_pool, K_FOREVER);
}
static const struct bt_l2cap_chan_ops l2cap_ops = {
.alloc_buf = l2cap_alloc_buf,
.recv = l2cap_recv,
.sent = l2cap_sent,
.status = l2cap_status,
.connected = l2cap_connected,
.disconnected = l2cap_disconnected,
};
static struct l2ch l2ch_chan = {
.ch.chan.ops = &l2cap_ops,
.ch.rx.mtu = DATA_MTU,
};
static void l2cap_allowlist_remove(struct bt_conn *conn, uint8_t reason)
{
int i;
for (i = 0; i < ARRAY_SIZE(l2cap_allowlist); i++) {
if (l2cap_allowlist[i] == conn) {
bt_conn_unref(l2cap_allowlist[i]);
l2cap_allowlist[i] = NULL;
}
}
}
BT_CONN_CB_DEFINE(l2cap_conn_callbacks) = {
.disconnected = l2cap_allowlist_remove,
};
static int l2cap_accept_policy(struct bt_conn *conn)
{
int i;
if (l2cap_policy == L2CAP_POLICY_16BYTE_KEY) {
uint8_t enc_key_size = bt_conn_enc_key_size(conn);
if (enc_key_size && enc_key_size < BT_ENC_KEY_SIZE_MAX) {
return -EPERM;
}
} else if (l2cap_policy == L2CAP_POLICY_ALLOWLIST) {
for (i = 0; i < ARRAY_SIZE(l2cap_allowlist); i++) {
if (l2cap_allowlist[i] == conn) {
return 0;
}
}
return -EACCES;
}
return 0;
}
static int l2cap_accept(struct bt_conn *conn, struct bt_l2cap_chan **chan)
{
int err;
shell_print(ctx_shell, "Incoming conn %p", conn);
err = l2cap_accept_policy(conn);
if (err < 0) {
return err;
}
if (l2ch_chan.ch.chan.conn) {
shell_print(ctx_shell, "No channels available");
return -ENOMEM;
}
*chan = &l2ch_chan.ch.chan;
return 0;
}
static struct bt_l2cap_server server = {
.accept = l2cap_accept,
};
static int cmd_register(const struct shell *sh, size_t argc, char *argv[])
{
const char *policy;
if (server.psm) {
shell_error(sh, "Already registered");
return -ENOEXEC;
}
server.psm = strtoul(argv[1], NULL, 16);
if (argc > 2) {
server.sec_level = strtoul(argv[2], NULL, 10);
}
if (argc > 3) {
policy = argv[3];
if (!strcmp(policy, "allowlist")) {
l2cap_policy = L2CAP_POLICY_ALLOWLIST;
} else if (!strcmp(policy, "16byte_key")) {
l2cap_policy = L2CAP_POLICY_16BYTE_KEY;
} else {
return -EINVAL;
}
}
if (bt_l2cap_server_register(&server) < 0) {
shell_error(sh, "Unable to register psm");
server.psm = 0U;
return -ENOEXEC;
} else {
shell_print(sh, "L2CAP psm %u sec_level %u registered",
server.psm, server.sec_level);
}
return 0;
}
#if defined(CONFIG_BT_L2CAP_ECRED)
static int cmd_ecred_reconfigure(const struct shell *sh, size_t argc, char *argv[])
{
struct bt_l2cap_chan *l2cap_ecred_chans[1] = {&l2ch_chan.ch.chan};
uint16_t mtu;
int err = 0;
if (!default_conn) {
shell_error(sh, "Not connected");
return -ENOEXEC;
}
if (!l2ch_chan.ch.chan.conn) {
shell_error(sh, "Channel not connected");
return -ENOEXEC;
}
mtu = shell_strtoul(argv[1], 10, &err);
if (err) {
shell_error(sh, "Unable to parse MTU (err %d)", err);
return -ENOEXEC;
}
err = bt_l2cap_ecred_chan_reconfigure(l2cap_ecred_chans, mtu);
if (err < 0) {
shell_error(sh, "Unable to reconfigure channel (err %d)", err);
} else {
shell_print(sh, "L2CAP reconfiguration pending");
}
return err;
}
static int cmd_ecred_connect(const struct shell *sh, size_t argc, char *argv[])
{
struct bt_l2cap_chan *l2cap_ecred_chans[1] = {&l2ch_chan.ch.chan};
uint16_t psm;
int err = 0;
if (!default_conn) {
shell_error(sh, "Not connected");
return -ENOEXEC;
}
if (l2ch_chan.ch.chan.conn) {
shell_error(sh, "Channel already in use");
return -ENOEXEC;
}
psm = shell_strtoul(argv[1], 16, &err);
if (err) {
shell_error(sh, "Unable to parse PSM (err %d)", err);
return err;
}
if (argc > 2) {
int sec;
sec = shell_strtoul(argv[2], 10, &err);
if (err) {
shell_error(sh, "Unable to parse security level (err %d)", err);
return err;
}
l2ch_chan.ch.required_sec_level = sec;
}
err = bt_l2cap_ecred_chan_connect(default_conn, l2cap_ecred_chans, 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;
}
#endif /* CONFIG_BT_L2CAP_ECRED */
static int cmd_connect(const struct shell *sh, size_t argc, char *argv[])
{
uint16_t psm;
int err;
if (!default_conn) {
shell_error(sh, "Not connected");
return -ENOEXEC;
}
if (l2ch_chan.ch.chan.conn) {
shell_error(sh, "Channel already in use");
return -ENOEXEC;
}
psm = strtoul(argv[1], NULL, 16);
if (argc > 2) {
int sec;
sec = *argv[2] - '0';
l2ch_chan.ch.required_sec_level = sec;
}
err = bt_l2cap_chan_connect(default_conn, &l2ch_chan.ch.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_disconnect(const struct shell *sh, size_t argc, char *argv[])
{
int err;
err = bt_l2cap_chan_disconnect(&l2ch_chan.ch.chan);
if (err) {
shell_print(sh, "Unable to disconnect: %u", -err);
}
return err;
}
static int cmd_send(const struct shell *sh, size_t argc, char *argv[])
{
static uint8_t buf_data[DATA_MTU] = { [0 ... (DATA_MTU - 1)] = 0xff };
int ret, len = DATA_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_MTU) {
shell_print(sh,
"Length exceeds TX MTU for the channel");
return -ENOEXEC;
}
}
len = MIN(l2ch_chan.ch.tx.mtu, len);
while (count--) {
shell_print(sh, "Rem %d", count);
buf = net_buf_alloc(&data_tx_pool, K_SECONDS(2));
if (!buf) {
if (l2ch_chan.ch.state != BT_L2CAP_CONNECTED) {
shell_print(sh, "Channel disconnected, stopping TX");
return -EAGAIN;
}
shell_print(sh, "Allocation timeout, stopping TX");
return -EAGAIN;
}
net_buf_reserve(buf, BT_L2CAP_SDU_CHAN_SEND_RESERVE);
net_buf_add_mem(buf, buf_data, len);
ret = bt_l2cap_chan_send(&l2ch_chan.ch.chan, buf);
if (ret < 0) {
shell_print(sh, "Unable to send: %d", -ret);
net_buf_unref(buf);
return -ENOEXEC;
}
}
return 0;
}
static int cmd_recv(const struct shell *sh, size_t argc, char *argv[])
{
if (argc > 1) {
l2cap_recv_delay_ms = strtoul(argv[1], NULL, 10);
} else {
shell_print(sh, "l2cap receive delay: %u ms",
l2cap_recv_delay_ms);
}
return 0;
}
static int cmd_metrics(const struct shell *sh, size_t argc, char *argv[])
{
const char *action;
if (argc < 2) {
shell_print(sh, "l2cap rate: %u bps.", l2cap_rate);
return 0;
}
action = argv[1];
if (!strcmp(action, "on")) {
metrics = true;
} else if (!strcmp(action, "off")) {
metrics = false;
} else {
shell_help(sh);
return 0;
}
shell_print(sh, "l2cap metrics %s.", action);
return 0;
}
static int cmd_allowlist_add(const struct shell *sh, size_t argc, char *argv[])
{
int i;
if (!default_conn) {
shell_error(sh, "Not connected");
return 0;
}
for (i = 0; i < ARRAY_SIZE(l2cap_allowlist); i++) {
if (l2cap_allowlist[i] == NULL) {
l2cap_allowlist[i] = bt_conn_ref(default_conn);
return 0;
}
}
return -ENOMEM;
}
static int cmd_allowlist_remove(const struct shell *sh, size_t argc, char *argv[])
{
if (!default_conn) {
shell_error(sh, "Not connected");
return 0;
}
l2cap_allowlist_remove(default_conn, 0);
return 0;
}
#define HELP_NONE "[none]"
SHELL_STATIC_SUBCMD_SET_CREATE(allowlist_cmds,
SHELL_CMD_ARG(add, NULL, HELP_NONE, cmd_allowlist_add, 1, 0),
SHELL_CMD_ARG(remove, NULL, HELP_NONE, cmd_allowlist_remove, 1, 0),
SHELL_SUBCMD_SET_END
);
SHELL_STATIC_SUBCMD_SET_CREATE(l2cap_cmds,
SHELL_CMD_ARG(connect, NULL, "<psm> [sec_level]", cmd_connect, 2, 1),
SHELL_CMD_ARG(disconnect, NULL, HELP_NONE, cmd_disconnect, 1, 0),
SHELL_CMD_ARG(metrics, NULL, "<value on, off>", cmd_metrics, 2, 0),
SHELL_CMD_ARG(recv, NULL, "[delay (in milliseconds)", cmd_recv, 1, 1),
SHELL_CMD_ARG(register, NULL, "<psm> [sec_level] "
"[policy: allowlist, 16byte_key]", cmd_register, 2, 2),
SHELL_CMD_ARG(send, NULL, "[number of packets] [length of packet(s)]",
cmd_send, 1, 2),
SHELL_CMD_ARG(allowlist, &allowlist_cmds, HELP_NONE, NULL, 1, 0),
#if defined(CONFIG_BT_L2CAP_ECRED)
SHELL_CMD_ARG(ecred-connect, NULL, "<psm (hex)> [sec_level (dec)]",
cmd_ecred_connect, 2, 1),
SHELL_CMD_ARG(ecred-reconfigure, NULL, "<mtu (dec)>",
cmd_ecred_reconfigure, 1, 1),
#endif /* CONFIG_BT_L2CAP_ECRED */
SHELL_SUBCMD_SET_END
);
static int cmd_l2cap(const struct shell *sh, size_t argc, char **argv)
{
if (argc == 1) {
shell_help(sh);
/* shell returns 1 when help is printed */
return 1;
}
shell_error(sh, "%s unknown parameter: %s", argv[0], argv[1]);
return -ENOEXEC;
}
SHELL_CMD_ARG_REGISTER(l2cap, &l2cap_cmds, "Bluetooth L2CAP shell commands",
cmd_l2cap, 1, 1);