diff --git a/doc/connectivity/bluetooth/api/l2cap.rst b/doc/connectivity/bluetooth/api/l2cap.rst index 157f9684be131..6f9fd83ef4e5f 100644 --- a/doc/connectivity/bluetooth/api/l2cap.rst +++ b/doc/connectivity/bluetooth/api/l2cap.rst @@ -8,6 +8,53 @@ configuration option: :kconfig:option:`CONFIG_BT_L2CAP_DYNAMIC_CHANNEL`. These c support segmentation and reassembly transparently, they also support credit based flow control making it suitable for data streams. +Channels instances are represented by the :c:struct:`bt_l2cap_chan` struct which +contains the callbacks in the :c:struct:`bt_l2cap_chan_ops` struct to inform +when the channel has been connected, disconnected or when the encryption has +changed. +In addition to that it also contains the ``recv`` callback which is called +whenever an incoming data has been received. Data received this way can be +marked as processed by returning 0 or using +:c:func:`bt_l2cap_chan_recv_complete` API if processing is asynchronous. + +.. note:: + The ``recv`` callback is called directly from RX Thread thus it is not + recommended to block for long periods of time. + +For sending data the :c:func:`bt_l2cap_chan_send` API can be used noting that +it may block if no credits are available, and resuming as soon as more credits +are available. + +Servers can be registered using :c:func:`bt_l2cap_server_register` API passing +the :c:struct:`bt_l2cap_server` struct which informs what ``psm`` it should +listen to, the required security level ``sec_level``, and the callback +``accept`` which is called to authorize incoming connection requests and +allocate channel instances. + +Creating a simple L2CAP server +------------------------------- + +A complete working example demonstrating L2CAP dynamic channels is available +in the samples directory: + +- Acceptor (server) sample: :zephyr:code-sample:`bluetooth_l2cap_coc_acceptor` +- Initiator (client) sample: :zephyr:code-sample:`bluetooth_l2cap_coc_initiator` + +The acceptor sample shows how to register an L2CAP server and handle incoming +connections: + +.. literalinclude:: ../../../../samples/bluetooth/l2cap_coc_acceptor/src/main.c + :language: c + :linenos: + +.. note:: + The sample demonstrates allocating one channel per connection using + ``CONFIG_BT_MAX_CONN`` and ``bt_conn_index(conn)``. See the initiator sample + for how to open a channel and send data to the acceptor. + +Fixed Channels +-------------- + The user can also define fixed channels using the :c:macro:`BT_L2CAP_FIXED_CHANNEL_DEFINE` macro. Fixed channels are initialized upon connection, and do not support segmentation. An example of how to define a fixed channel is shown below. @@ -42,32 +89,12 @@ of how to define a fixed channel is shown below. .accept = l2cap_fixed_accept, }; -Channels instances are represented by the :c:struct:`bt_l2cap_chan` struct which -contains the callbacks in the :c:struct:`bt_l2cap_chan_ops` struct to inform -when the channel has been connected, disconnected or when the encryption has -changed. -In addition to that it also contains the ``recv`` callback which is called -whenever an incoming data has been received. Data received this way can be -marked as processed by returning 0 or using -:c:func:`bt_l2cap_chan_recv_complete` API if processing is asynchronous. - -.. note:: - The ``recv`` callback is called directly from RX Thread thus it is not - recommended to block for long periods of time. - -For sending data the :c:func:`bt_l2cap_chan_send` API can be used noting that -it may block if no credits are available, and resuming as soon as more credits -are available. - -Servers can be registered using :c:func:`bt_l2cap_server_register` API passing -the :c:struct:`bt_l2cap_server` struct which informs what ``psm`` it should -listen to, the required security level ``sec_level``, and the callback -``accept`` which is called to authorize incoming connection requests and -allocate channel instances. +Client Channels +--------------- Client channels can be initiated with use of :c:func:`bt_l2cap_chan_connect` API and can be disconnected with the :c:func:`bt_l2cap_chan_disconnect` API. -Note that the later can also disconnect channel instances created by servers. +Note that the latter can also disconnect channel instances created by servers. API Reference ************* diff --git a/samples/bluetooth/l2cap_coc_acceptor/CMakeLists.txt b/samples/bluetooth/l2cap_coc_acceptor/CMakeLists.txt new file mode 100644 index 0000000000000..3c2050e16609a --- /dev/null +++ b/samples/bluetooth/l2cap_coc_acceptor/CMakeLists.txt @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(l2cap_server_simple) + +target_sources(app PRIVATE src/main.c) diff --git a/samples/bluetooth/l2cap_coc_acceptor/README.rst b/samples/bluetooth/l2cap_coc_acceptor/README.rst new file mode 100644 index 0000000000000..c21fe2e75c594 --- /dev/null +++ b/samples/bluetooth/l2cap_coc_acceptor/README.rst @@ -0,0 +1,89 @@ +.. _bluetooth_l2cap_coc_acceptor: + +L2CAP Connection Oriented Channels (Acceptor) +############################################## + +Overview +******** + +This sample demonstrates how to create an L2CAP Connection Oriented Channel +(CoC) server that listens for incoming L2CAP connections on a fixed PSM +(Protocol/Service Multiplexer). When a client connects, the server accepts +the connection and receives data sent over the L2CAP channel. + +The sample uses a fixed allocation strategy where one L2CAP channel instance is +pre-allocated for each possible Bluetooth connection (based on +``CONFIG_BT_MAX_CONN``). This approach avoids dynamic memory allocation and +simplifies resource management. + +Requirements +************ + +* A board with Bluetooth Low Energy (BLE) support +* Bluetooth controller with L2CAP Connection Oriented Channels support + +Building and Running +******************** + +This sample can be built and run on any board with Bluetooth LE support. + +To build the sample for the nRF52840 DK: + +.. zephyr-app-commands:: + :zephyr-app: samples/bluetooth/l2cap_coc_acceptor + :board: nrf52840dk/nrf52840 + :goals: build flash + :compact: + +For testing with native_posix (simulation): + +.. zephyr-app-commands:: + :zephyr-app: samples/bluetooth/l2cap_coc_acceptor + :board: native_posix + :goals: build + :compact: + +After flashing, the device will register an L2CAP server on PSM 0x29 and wait +for incoming connections. Pair this sample with the +:zephyr:code-sample:`bluetooth_l2cap_coc_initiator` sample to establish a +connection and exchange data. + +Sample Output +============= + +Upon successful initialization: + +.. code-block:: console + + L2CAP server registered, PSM 41 + +When a client connects: + +.. code-block:: console + + L2CAP channel accepted, assigned chan[0] + L2CAP channel connected + +When data is received: + +.. code-block:: console + + L2CAP channel received 17 bytes + +Testing +******* + +This sample is designed to work with the +:zephyr:code-sample:`bluetooth_l2cap_coc_initiator` sample. Run the acceptor +sample on one device and the initiator sample on another device within BLE range. + +The initiator will scan, connect, and establish an L2CAP channel to send periodic +messages to the acceptor. + +References +********** + +* :ref:`bluetooth_api` +* :ref:`bluetooth-samples` +* `Bluetooth Core Specification v5.4, Vol 3, Part A (L2CAP) + `_ diff --git a/samples/bluetooth/l2cap_coc_acceptor/prj.conf b/samples/bluetooth/l2cap_coc_acceptor/prj.conf new file mode 100644 index 0000000000000..793cff6d43608 --- /dev/null +++ b/samples/bluetooth/l2cap_coc_acceptor/prj.conf @@ -0,0 +1,6 @@ +CONFIG_BT=y +CONFIG_BT_PERIPHERAL=y +CONFIG_BT_SMP=y +CONFIG_BT_L2CAP_DYNAMIC_CHANNEL=y +# Optional, adjust for sample: +CONFIG_BT_MAX_CONN=2 diff --git a/samples/bluetooth/l2cap_coc_acceptor/sample.yaml b/samples/bluetooth/l2cap_coc_acceptor/sample.yaml new file mode 100644 index 0000000000000..a3412c06a8390 --- /dev/null +++ b/samples/bluetooth/l2cap_coc_acceptor/sample.yaml @@ -0,0 +1,11 @@ +sample: + name: Bluetooth L2CAP dynamic channels sample (acceptor role) + description: Sample demonstrating L2CAP dynamic channels (acceptor role). +tests: + sample.bluetooth.l2cap_coc_acceptor: + harness: bluetooth + platform_allow: + - qemu_x86 + integration_platforms: + - qemu_x86 + tags: bluetooth \ No newline at end of file diff --git a/samples/bluetooth/l2cap_coc_acceptor/src/main.c b/samples/bluetooth/l2cap_coc_acceptor/src/main.c new file mode 100644 index 0000000000000..51419b683136d --- /dev/null +++ b/samples/bluetooth/l2cap_coc_acceptor/src/main.c @@ -0,0 +1,83 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright The Zephyr Project Contributors + */ + +#include +#include +#include +#include + +#define PSM 0x29 /* example PSM */ + +static int l2cap_recv(struct bt_l2cap_chan *chan, struct net_buf *buf); +static void l2cap_connected(struct bt_l2cap_chan *chan); +static void l2cap_disconnected(struct bt_l2cap_chan *chan); + +/* ops used for every channel instance */ +static struct bt_l2cap_chan_ops l2cap_ops = { + .recv = l2cap_recv, + .connected = l2cap_connected, + .disconnected = l2cap_disconnected, +}; + +/* Fixed array of channel instances, one per possible connection */ +static struct bt_l2cap_chan fixed_chan[CONFIG_BT_MAX_CONN]; + +static int accept_cb(struct bt_conn *conn, struct bt_l2cap_server *server, + struct bt_l2cap_chan **chan) +{ + uint8_t conn_index = bt_conn_index(conn); + + /* initialize the chosen entry */ + fixed_chan[conn_index] = (struct bt_l2cap_chan){ + .ops = &l2cap_ops, + }; + + *chan = &fixed_chan[conn_index]; + + printk("L2CAP channel accepted, assigned chan[%d]\n", conn_index); + + return 0; /* accept */ +} + +static struct bt_l2cap_server server = { + .psm = PSM, + .sec_level = BT_SECURITY_L1, + .accept = accept_cb, +}; + +static void l2cap_connected(struct bt_l2cap_chan *chan) +{ + printk("L2CAP channel connected\n"); +} + +static void l2cap_disconnected(struct bt_l2cap_chan *chan) +{ + printk("L2CAP channel disconnected\n"); +} + +static int l2cap_recv(struct bt_l2cap_chan *chan, struct net_buf *buf) +{ + printk("L2CAP channel received %u bytes\n", buf->len); + /* For synchronous processing, return 0 and the stack frees buf. */ + return 0; +} + +int main(void) +{ + int err = bt_enable(NULL); + if (err) { + printk("Bluetooth init failed: %d\n", err); + return err; + } + + err = bt_l2cap_server_register(&server); + if (err) { + printk("L2CAP server registration failed: %d\n", err); + return err; + } + + printk("L2CAP server registered, PSM %u\n", PSM); + return 0; +} \ No newline at end of file diff --git a/samples/bluetooth/l2cap_coc_initiator/CMakeLists.txt b/samples/bluetooth/l2cap_coc_initiator/CMakeLists.txt new file mode 100644 index 0000000000000..b8edd1f998fb7 --- /dev/null +++ b/samples/bluetooth/l2cap_coc_initiator/CMakeLists.txt @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(l2cap_client_simple) + +target_sources(app PRIVATE src/main.c) diff --git a/samples/bluetooth/l2cap_coc_initiator/README.rst b/samples/bluetooth/l2cap_coc_initiator/README.rst new file mode 100644 index 0000000000000..e88a0f0a861e6 --- /dev/null +++ b/samples/bluetooth/l2cap_coc_initiator/README.rst @@ -0,0 +1,98 @@ +.. _bluetooth_l2cap_coc_initiator: + +L2CAP Connection Oriented Channels (Initiator) +############################################### + +Overview +******** + +This sample demonstrates how to create an L2CAP Connection Oriented Channel +(CoC) client that scans for Bluetooth devices, connects to one, and establishes +an L2CAP channel to communicate with a server. + +The sample periodically sends short text messages over the L2CAP channel to +demonstrate basic data transmission. It uses a fixed allocation strategy for +the L2CAP channel to avoid dynamic memory allocation. + +Requirements +************ + +* A board with Bluetooth Low Energy (BLE) support +* Bluetooth controller with L2CAP Connection Oriented Channels support +* A peer device running an L2CAP server (such as the + :zephyr:code-sample:`bluetooth_l2cap_coc_acceptor` sample) + +Building and Running +******************** + +This sample can be built and run on any board with Bluetooth LE support. + +To build the sample for the nRF52840 DK: + +.. zephyr-app-commands:: + :zephyr-app: samples/bluetooth/l2cap_coc_initiator + :board: nrf52840dk/nrf52840 + :goals: build flash + :compact: + +For testing with native_posix (simulation): + +.. zephyr-app-commands:: + :zephyr-app: samples/bluetooth/l2cap_coc_initiator + :board: native_posix + :goals: build + :compact: + +After flashing, the device will scan for nearby Bluetooth devices, connect to +the first one it finds, and attempt to establish an L2CAP channel on PSM 0x29. +Once connected, it will send messages periodically. + +Sample Output +============= + +Upon successful connection and channel establishment: + +.. code-block:: console + + Scanning... + Found device: XX:XX:XX:XX:XX:XX (RSSI -45) + Connecting to XX:XX:XX:XX:XX:XX ... + Connected + L2CAP channel connection initiated + L2CAP channel connected + Sent: Hello from client + +The message "Hello from client" will be sent periodically to demonstrate +ongoing data transmission. + +Testing +******* + +This sample is designed to work with the +:zephyr:code-sample:`bluetooth_l2cap_coc_acceptor` sample: + +1. Flash the acceptor sample on one device +2. Flash the initiator sample on another device +3. Place both devices within BLE range +4. The initiator will automatically scan, connect, and establish the L2CAP channel +5. Monitor the serial output on both devices to observe the connection and data + exchange + +Notes +***** + +* This sample is intentionally simple and is designed for testing and + demonstration purposes +* The initiator connects to the first advertising device it finds - in a + production environment, you would want to filter devices by name, address, or + advertised services +* You may need to adjust Bluetooth settings in the ``prj.conf`` file to match + your platform's capabilities + +References +********** + +* :ref:`bluetooth_api` +* :ref:`bluetooth-samples` +* `Bluetooth Core Specification v5.4, Vol 3, Part A (L2CAP) + `_ diff --git a/samples/bluetooth/l2cap_coc_initiator/prj.conf b/samples/bluetooth/l2cap_coc_initiator/prj.conf new file mode 100644 index 0000000000000..d88b1bf3bd3e0 --- /dev/null +++ b/samples/bluetooth/l2cap_coc_initiator/prj.conf @@ -0,0 +1,8 @@ +# Minimal Bluetooth configuration +CONFIG_BT=y +CONFIG_BT_MAX_CONN=1 +CONFIG_BT_SMP=y +CONFIG_BT_CENTRAL=y +CONFIG_BT_L2CAP_DYNAMIC_CHANNEL=y +CONFIG_BT_PERIPHERAL=y + diff --git a/samples/bluetooth/l2cap_coc_initiator/sample.yaml b/samples/bluetooth/l2cap_coc_initiator/sample.yaml new file mode 100644 index 0000000000000..c17fdee4c19d5 --- /dev/null +++ b/samples/bluetooth/l2cap_coc_initiator/sample.yaml @@ -0,0 +1,11 @@ +sample: + name: Bluetooth L2CAP dynamic channels sample (initiator role) + description: Sample demonstrating L2CAP dynamic channels (initiator role). +tests: + sample.bluetooth.l2cap_coc_initiator: + harness: bluetooth + platform_allow: + - qemu_x86 + integration_platforms: + - qemu_x86 + tags: bluetooth diff --git a/samples/bluetooth/l2cap_coc_initiator/src/main.c b/samples/bluetooth/l2cap_coc_initiator/src/main.c new file mode 100644 index 0000000000000..e339b96c86a0e --- /dev/null +++ b/samples/bluetooth/l2cap_coc_initiator/src/main.c @@ -0,0 +1,184 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright The Zephyr Project Contributors + */ + +#include +#include +#include +#include +#include +#include + +#define PSM 0x29 +#define SEND_INTERVAL_MS 2000 +#define DATA_MTU 23 + +NET_BUF_POOL_FIXED_DEFINE(data_pool, 1, DATA_MTU, 8, NULL); + +K_THREAD_STACK_DEFINE(send_thread_stack, 1024); +static struct k_thread send_thread_data; +static struct bt_conn *default_conn; +static volatile bool channel_connected; + +static void start_scan(void); + +static int client_chan_recv(struct bt_l2cap_chan *chan, struct net_buf *buf); +static void client_chan_connected(struct bt_l2cap_chan *chan); +static void client_chan_disconnected(struct bt_l2cap_chan *chan); + +static struct bt_l2cap_chan_ops client_ops = { + .recv = client_chan_recv, + .connected = client_chan_connected, + .disconnected = client_chan_disconnected, +}; + +static struct bt_l2cap_le_chan client_chan = { + .chan.ops = &client_ops, +}; + +static void send_task(void *p1, void *p2, void *p3) +{ + struct bt_l2cap_chan *chan = p1; + + while (channel_connected) { + const char *msg = "Hello from client"; + struct net_buf *buf; + + buf = net_buf_alloc(&data_pool, K_NO_WAIT); + if (!buf) { + printk("Failed to allocate tx buffer\n"); + k_sleep(K_MSEC(SEND_INTERVAL_MS)); + continue; + } + + net_buf_reserve(buf, BT_L2CAP_SDU_CHAN_SEND_RESERVE); + net_buf_add_mem(buf, msg, strlen(msg)); + + if (bt_l2cap_chan_send(chan, buf) < 0) { + printk("Failed to send L2CAP data\n"); + net_buf_unref(buf); + } else { + printk("Sent: %s\n", msg); + } + + k_sleep(K_MSEC(SEND_INTERVAL_MS)); + } +} + +static int client_chan_recv(struct bt_l2cap_chan *chan, struct net_buf *buf) +{ + printk("L2CAP channel received %u bytes\n", buf->len); + return 0; +} + +static void client_chan_connected(struct bt_l2cap_chan *chan) +{ + printk("L2CAP channel connected\n"); + channel_connected = true; + + k_thread_create(&send_thread_data, send_thread_stack, + K_THREAD_STACK_SIZEOF(send_thread_stack), + send_task, chan, NULL, NULL, + K_PRIO_PREEMPT(7), 0, K_NO_WAIT); +} + +static void client_chan_disconnected(struct bt_l2cap_chan *chan) +{ + printk("L2CAP channel disconnected\n"); + channel_connected = false; +} + +static void connected(struct bt_conn *conn, uint8_t err) +{ + if (err) { + printk("Connection failed (err %u)\n", err); + bt_conn_unref(conn); + start_scan(); + return; + } + + printk("Connected\n"); + default_conn = bt_conn_ref(conn); + + int rc = bt_l2cap_chan_connect(default_conn, &client_chan.chan, PSM); + if (rc) { + printk("L2CAP channel connection failed: %d\n", rc); + } else { + printk("L2CAP channel connection initiated\n"); + } +} + +static void disconnected(struct bt_conn *conn, uint8_t reason) +{ + printk("Disconnected (reason %u)\n", reason); + if (default_conn) { + bt_conn_unref(default_conn); + default_conn = NULL; + } + channel_connected = false; + start_scan(); +} + +static struct bt_conn_cb conn_callbacks = { + .connected = connected, + .disconnected = disconnected, +}; + +static void device_found(const bt_addr_le_t *addr, int8_t rssi, uint8_t type, + struct net_buf_simple *ad) +{ + char addr_str[BT_ADDR_LE_STR_LEN]; + int err; + + bt_addr_le_to_str(addr, addr_str, sizeof(addr_str)); + printk("Found device: %s (RSSI %d)\n", addr_str, rssi); + + err = bt_le_scan_stop(); + if (err) { + printk("Failed to stop scanning: %d\n", err); + return; + } + + struct bt_le_conn_param *param = BT_LE_CONN_PARAM_DEFAULT; + int rc = bt_conn_le_create(addr, BT_CONN_LE_CREATE_CONN, param, &default_conn); + if (rc) { + printk("Failed to create connection: %d\n", rc); + start_scan(); + } else { + printk("Connecting to %s ...\n", addr_str); + } +} + +static void start_scan(void) +{ + int err; + struct bt_le_scan_param scan_param = { + .type = BT_HCI_LE_SCAN_ACTIVE, + .options = BT_LE_SCAN_OPT_NONE, + .interval = BT_GAP_SCAN_FAST_INTERVAL, + .window = BT_GAP_SCAN_FAST_WINDOW, + }; + + err = bt_le_scan_start(&scan_param, device_found); + if (err) { + printk("Starting scanning failed (err %d)\n", err); + } else { + printk("Scanning started\n"); + } +} + +int main(void) +{ + int err = bt_enable(NULL); + if (err) { + printk("Bluetooth init failed: %d\n", err); + return err; + } + printk("Bluetooth initialized\n"); + + bt_conn_cb_register(&conn_callbacks); + + start_scan(); + return 0; +}