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;
+}