diff --git a/doc/releases/release-notes-3.7.rst b/doc/releases/release-notes-3.7.rst index ddbf8a3e98c4..4a684cdff270 100644 --- a/doc/releases/release-notes-3.7.rst +++ b/doc/releases/release-notes-3.7.rst @@ -37,6 +37,10 @@ Architectures Bluetooth ********* + * Added Nordic UART Service (NUS), enabled by the :kconfig:option:`CONFIG_BT_NUS`. + This Service exposes the ability to declare multiple instances of the GATT service, + allowing multiple serial endpoints to be used for different purposes. + Boards & SoC Support ******************** @@ -146,6 +150,10 @@ Drivers and Sensors * Serial + * Added driver to support UART over Bluetooth LE using NUS (Nordic UART Service). This driver + enables using Bluetooth as a transport to all the subsystems that are currently supported by + UART (e.g: Console, Shell, Logging). + * SPI * USB @@ -212,3 +220,8 @@ LVGL Tests and Samples ***************** + + * Added snippet for easily enabling UART over Bluetooth LE by passing ``-S nus-console`` during + ``west build``. This snippet sets the :kconfig:option:`CONFIG_BT_NUS_AUTO_START_BLUETOOTH` + which allows non-Bluetooth samples that use the UART APIs to run without modifications + (e.g: Console and Logging examples). diff --git a/drivers/serial/CMakeLists.txt b/drivers/serial/CMakeLists.txt index 1648f3a70817..27c8e16ec3cc 100644 --- a/drivers/serial/CMakeLists.txt +++ b/drivers/serial/CMakeLists.txt @@ -97,6 +97,8 @@ if(CONFIG_UART_NATIVE_TTY) endif() endif() +zephyr_library_sources_ifdef(CONFIG_UART_BT uart_bt.c) + zephyr_library_sources_ifdef(CONFIG_SERIAL_TEST serial_test.c) zephyr_library_sources_ifdef(CONFIG_UART_ASYNC_RX_HELPER uart_async_rx.c) zephyr_library_sources_ifdef(CONFIG_UART_ASYNC_TO_INT_DRIVEN_API uart_async_to_irq.c) diff --git a/drivers/serial/Kconfig b/drivers/serial/Kconfig index a3041b2cae61..64b719b6842c 100644 --- a/drivers/serial/Kconfig +++ b/drivers/serial/Kconfig @@ -218,6 +218,8 @@ source "drivers/serial/Kconfig.litex" source "drivers/serial/Kconfig.rtt" +source "drivers/serial/Kconfig.bt" + source "drivers/serial/Kconfig.xlnx" source "drivers/serial/Kconfig.xmc4xxx" diff --git a/drivers/serial/Kconfig.bt b/drivers/serial/Kconfig.bt new file mode 100644 index 000000000000..a650615f2196 --- /dev/null +++ b/drivers/serial/Kconfig.bt @@ -0,0 +1,13 @@ +# Copyright (c) 2024 Croxel, Inc. +# SPDX-License-Identifier: Apache-2.0 + +config UART_BT + bool "UART over NUS Bluetooth LE" + depends on BT_NUS + depends on DT_HAS_ZEPHYR_NUS_UART_ENABLED + select UART_INTERRUPT_DRIVEN + select RING_BUFFER + select EXPERIMENTAL + help + Enable the UART over NUS Bluetooth driver, which can be used to pipe + serial data over Bluetooth LE GATT using NUS (Nordic UART Service). diff --git a/drivers/serial/uart_bt.c b/drivers/serial/uart_bt.c new file mode 100644 index 000000000000..79353fe7307c --- /dev/null +++ b/drivers/serial/uart_bt.c @@ -0,0 +1,321 @@ +/* + * Copyright (c) 2024 Croxel, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +#define DT_DRV_COMPAT zephyr_nus_uart + +#include +LOG_MODULE_REGISTER(uart_nus, CONFIG_UART_LOG_LEVEL); + +struct uart_bt_data { + struct { + struct bt_nus_inst *inst; + struct bt_nus_cb cb; + atomic_t enabled; + } bt; + struct { + struct ring_buf *rx_ringbuf; + struct ring_buf *tx_ringbuf; + struct k_work cb_work; + struct k_work_delayable tx_work; + bool rx_irq_ena; + bool tx_irq_ena; + struct { + const struct device *dev; + uart_irq_callback_user_data_t cb; + void *cb_data; + } callback; + } uart; +}; + +static void bt_notif_enabled(bool enabled, void *ctx) +{ + __ASSERT_NO_MSG(ctx); + + const struct device *dev = (const struct device *)ctx; + struct uart_bt_data *dev_data = (struct uart_bt_data *)dev->data; + + (void)atomic_set(&dev_data->bt.enabled, enabled ? 1 : 0); + + LOG_DBG("%s() - %s", __func__, enabled ? "enabled" : "disabled"); + + if (!ring_buf_is_empty(dev_data->uart.tx_ringbuf)) { + k_work_reschedule(&dev_data->uart.tx_work, K_NO_WAIT); + } +} + +static void bt_received(struct bt_conn *conn, const void *data, uint16_t len, void *ctx) +{ + __ASSERT_NO_MSG(conn); + __ASSERT_NO_MSG(ctx); + __ASSERT_NO_MSG(data); + __ASSERT_NO_MSG(len > 0); + + const struct device *dev = (const struct device *)ctx; + struct uart_bt_data *dev_data = (struct uart_bt_data *)dev->data; + struct ring_buf *ringbuf = dev_data->uart.rx_ringbuf; + uint32_t put_len; + + LOG_DBG("%s() - len: %d, rx_ringbuf space %d", __func__, len, ring_buf_space_get(ringbuf)); + LOG_HEXDUMP_DBG(data, len, "data"); + + put_len = ring_buf_put(ringbuf, (const uint8_t *)data, len); + if (put_len < len) { + LOG_ERR("RX Ring buffer full. received: %d, added to queue: %d", len, put_len); + } + + k_work_submit(&dev_data->uart.cb_work); +} + +static void cb_work_handler(struct k_work *work) +{ + struct uart_bt_data *dev_data = CONTAINER_OF(work, struct uart_bt_data, uart.cb_work); + + if (dev_data->uart.callback.cb) { + dev_data->uart.callback.cb( + dev_data->uart.callback.dev, + dev_data->uart.callback.cb_data); + } +} + +static void tx_work_handler(struct k_work *work) +{ + struct k_work_delayable *dwork = k_work_delayable_from_work(work); + struct uart_bt_data *dev_data = CONTAINER_OF(dwork, struct uart_bt_data, uart.tx_work); + uint8_t *data = NULL; + size_t len; + int err; + + __ASSERT_NO_MSG(dev_data); + + do { + /** Using Minimum MTU at this point to guarantee all connected + * peers will receive the data, without keeping track of MTU + * size per-connection. This has the trade-off of limiting + * throughput but allows multi-connection support. + */ + len = ring_buf_get_claim(dev_data->uart.tx_ringbuf, &data, 23); + if (len > 0) { + err = bt_nus_inst_send(NULL, dev_data->bt.inst, data, len); + if (err) { + LOG_ERR("Failed to send data over BT: %d", err); + } + } + + ring_buf_get_finish(dev_data->uart.tx_ringbuf, len); + } while (len > 0 && !err); + + if ((ring_buf_space_get(dev_data->uart.tx_ringbuf) > 0) && dev_data->uart.tx_irq_ena) { + k_work_submit(&dev_data->uart.cb_work); + } +} + +static int uart_bt_fifo_fill(const struct device *dev, const uint8_t *tx_data, int len) +{ + struct uart_bt_data *dev_data = (struct uart_bt_data *)dev->data; + size_t wrote; + + wrote = ring_buf_put(dev_data->uart.tx_ringbuf, tx_data, len); + if (wrote < len) { + LOG_WRN("Ring buffer full, drop %zd bytes", len - wrote); + } + + if (atomic_get(&dev_data->bt.enabled)) { + k_work_reschedule(&dev_data->uart.tx_work, K_NO_WAIT); + } + + return wrote; +} + +static int uart_bt_fifo_read(const struct device *dev, uint8_t *rx_data, const int size) +{ + struct uart_bt_data *dev_data = (struct uart_bt_data *)dev->data; + + return ring_buf_get(dev_data->uart.rx_ringbuf, rx_data, size); +} + +static int uart_bt_poll_in(const struct device *dev, unsigned char *c) +{ + int err = uart_bt_fifo_read(dev, c, 1); + + return err == 1 ? 0 : -1; +} + +static void uart_bt_poll_out(const struct device *dev, unsigned char c) +{ + struct uart_bt_data *dev_data = (struct uart_bt_data *)dev->data; + struct ring_buf *ringbuf = dev_data->uart.tx_ringbuf; + + /** Right now we're discarding data if ring-buf is full. */ + while (!ring_buf_put(ringbuf, &c, 1)) { + if (k_is_in_isr() || !atomic_get(&dev_data->bt.enabled)) { + LOG_INF("Ring buffer full, discard %c", c); + break; + } + + k_sleep(K_MSEC(1)); + } + + /** Don't flush the data until notifications are enabled. */ + if (atomic_get(&dev_data->bt.enabled)) { + /** Delay will allow buffering some characters before transmitting + * data, so more than one byte is transmitted (e.g: when poll_out is + * called inside a for-loop). + */ + k_work_reschedule(&dev_data->uart.tx_work, K_MSEC(1)); + } +} + +static int uart_bt_irq_tx_ready(const struct device *dev) +{ + struct uart_bt_data *dev_data = (struct uart_bt_data *)dev->data; + + if ((ring_buf_space_get(dev_data->uart.tx_ringbuf) > 0) && dev_data->uart.tx_irq_ena) { + return 1; + } + + return 0; +} + +static void uart_bt_irq_tx_enable(const struct device *dev) +{ + struct uart_bt_data *dev_data = (struct uart_bt_data *)dev->data; + + dev_data->uart.tx_irq_ena = true; + + if (uart_bt_irq_tx_ready(dev)) { + k_work_submit(&dev_data->uart.cb_work); + } +} + +static void uart_bt_irq_tx_disable(const struct device *dev) +{ + struct uart_bt_data *dev_data = (struct uart_bt_data *)dev->data; + + dev_data->uart.tx_irq_ena = false; +} + +static int uart_bt_irq_rx_ready(const struct device *dev) +{ + struct uart_bt_data *dev_data = (struct uart_bt_data *)dev->data; + + if (!ring_buf_is_empty(dev_data->uart.rx_ringbuf) && dev_data->uart.rx_irq_ena) { + return 1; + } + + return 0; +} + +static void uart_bt_irq_rx_enable(const struct device *dev) +{ + struct uart_bt_data *dev_data = (struct uart_bt_data *)dev->data; + + dev_data->uart.rx_irq_ena = true; + + k_work_submit(&dev_data->uart.cb_work); +} + +static void uart_bt_irq_rx_disable(const struct device *dev) +{ + struct uart_bt_data *dev_data = (struct uart_bt_data *)dev->data; + + dev_data->uart.rx_irq_ena = false; +} + +static int uart_bt_irq_is_pending(const struct device *dev) +{ + return uart_bt_irq_rx_ready(dev); +} + +static int uart_bt_irq_update(const struct device *dev) +{ + ARG_UNUSED(dev); + + return 1; +} + +static void uart_bt_irq_callback_set(const struct device *dev, + uart_irq_callback_user_data_t cb, + void *cb_data) +{ + struct uart_bt_data *dev_data = (struct uart_bt_data *)dev->data; + + dev_data->uart.callback.cb = cb; + dev_data->uart.callback.cb_data = cb_data; +} + +static const struct uart_driver_api uart_bt_driver_api = { + .poll_in = uart_bt_poll_in, + .poll_out = uart_bt_poll_out, + .fifo_fill = uart_bt_fifo_fill, + .fifo_read = uart_bt_fifo_read, + .irq_tx_enable = uart_bt_irq_tx_enable, + .irq_tx_disable = uart_bt_irq_tx_disable, + .irq_tx_ready = uart_bt_irq_tx_ready, + .irq_rx_enable = uart_bt_irq_rx_enable, + .irq_rx_disable = uart_bt_irq_rx_disable, + .irq_rx_ready = uart_bt_irq_rx_ready, + .irq_is_pending = uart_bt_irq_is_pending, + .irq_update = uart_bt_irq_update, + .irq_callback_set = uart_bt_irq_callback_set, +}; + +static int uart_bt_init(const struct device *dev) +{ + int err; + struct uart_bt_data *dev_data = (struct uart_bt_data *)dev->data; + + /** As a way to backtrace the device handle from uart_bt_data. + * Used in cb_work_handler. + */ + dev_data->uart.callback.dev = dev; + + k_work_init_delayable(&dev_data->uart.tx_work, tx_work_handler); + k_work_init(&dev_data->uart.cb_work, cb_work_handler); + + err = bt_nus_inst_cb_register(dev_data->bt.inst, &dev_data->bt.cb, (void *)dev); + if (err) { + return err; + } + + return 0; +} + +#define UART_BT_RX_FIFO_SIZE(inst) (DT_INST_PROP(inst, rx_fifo_size)) +#define UART_BT_TX_FIFO_SIZE(inst) (DT_INST_PROP(inst, tx_fifo_size)) + +#define UART_BT_INIT(n) \ + \ + BT_NUS_INST_DEFINE(bt_nus_inst_##n); \ + \ + RING_BUF_DECLARE(bt_nus_rx_rb_##n, UART_BT_RX_FIFO_SIZE(n)); \ + RING_BUF_DECLARE(bt_nus_tx_rb_##n, UART_BT_TX_FIFO_SIZE(n)); \ + \ + static struct uart_bt_data uart_bt_data_##n = { \ + .bt = { \ + .inst = &bt_nus_inst_##n, \ + .enabled = ATOMIC_INIT(0), \ + .cb = { \ + .notif_enabled = bt_notif_enabled, \ + .received = bt_received, \ + }, \ + }, \ + .uart = { \ + .rx_ringbuf = &bt_nus_rx_rb_##n, \ + .tx_ringbuf = &bt_nus_tx_rb_##n, \ + }, \ + }; \ + \ + DEVICE_DT_INST_DEFINE(n, uart_bt_init, NULL, &uart_bt_data_##n, \ + NULL, PRE_KERNEL_1, \ + CONFIG_SERIAL_INIT_PRIORITY, \ + &uart_bt_driver_api); + +DT_INST_FOREACH_STATUS_OKAY(UART_BT_INIT) diff --git a/dts/bindings/serial/zephyr,nus-uart.yaml b/dts/bindings/serial/zephyr,nus-uart.yaml new file mode 100644 index 000000000000..0c07121ea30c --- /dev/null +++ b/dts/bindings/serial/zephyr,nus-uart.yaml @@ -0,0 +1,19 @@ +# Copyright (c) 2024 Croxel, Inc. +# SPDX-License-Identifier: Apache-2.0 + +description: UART over NUS (Bluetooth LE) + +compatible: "zephyr,nus-uart" + +properties: + tx-fifo-size: + type: int + default: 1024 + description: | + Size of the virtual UART TX FIFO + + rx-fifo-size: + type: int + default: 1024 + description: | + Size of the virtual UART RX FIFO diff --git a/include/zephyr/bluetooth/services/nus.h b/include/zephyr/bluetooth/services/nus.h new file mode 100644 index 000000000000..595637f72750 --- /dev/null +++ b/include/zephyr/bluetooth/services/nus.h @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2024 Croxel, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_BLUETOOTH_SERVICES_NUS_H_ +#define ZEPHYR_INCLUDE_BLUETOOTH_SERVICES_NUS_H_ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** @brief UUIDs of Nordic UART GATT Service. + * Service: 6e400001-b5a3-f393-e0a9-e50e24dcca9e + * RX Char: 6e400002-b5a3-f393-e0a9-e50e24dcca9e + * TX Char: 6e400003-b5a3-f393-e0a9-e50e24dcca9e + */ +#define BT_UUID_NUS_SRV_VAL \ + BT_UUID_128_ENCODE(0x6e400001, 0xb5a3, 0xf393, 0xe0a9, 0xe50e24dcca9e) +#define BT_UUID_NUS_RX_CHAR_VAL \ + BT_UUID_128_ENCODE(0x6e400002, 0xb5a3, 0xf393, 0xe0a9, 0xe50e24dcca9e) +#define BT_UUID_NUS_TX_CHAR_VAL \ + BT_UUID_128_ENCODE(0x6e400003, 0xb5a3, 0xf393, 0xe0a9, 0xe50e24dcca9e) + +/** @brief Macro to define instance of NUS Service. It allows users to + * define multiple NUS instances, analogous to Serial endpoints, and use each + * one for different purposes. A default NUS instance may be defined through + * Kconfig. + */ +#define BT_NUS_INST_DEFINE(_name) \ + Z_INTERNAL_BT_NUS_INST_DEFINE(_name) + +/** @brief Callbacks for getting notified on NUS Service occurrences */ +struct bt_nus_cb { + /** @brief Notifications subscription changed + * + * @param enabled Flag that is true if notifications were enabled, false + * if they were disabled. + * @param ctx User context provided in the callback structure. + */ + void (*notif_enabled)(bool enabled, void *ctx); + + /** @brief Received Data + * + * @param conn Peer Connection object. + * @param data Pointer to buffer with data received. + * @param len Size in bytes of data received. + * @param ctx User context provided in the callback structure. + */ + void (*received)(struct bt_conn *conn, const void *data, uint16_t len, void *ctx); + + /** Internal member. Provided as a callback argument for user context */ + void *ctx; + + /** Internal member to form a list of callbacks */ + sys_snode_t _node; +}; + +/** @brief NUS server Instance callback register + * + * This function registers callbacks that will be called in + * certain events related to NUS. + * + * @param inst Pointer to instance of NUS service. NULL if using default instance. + * @param cb Pointer to callbacks structure. Must be valid throughout the + * lifetime of the application. + * @param ctx User context to be provided through the callback. + * + * @return 0 on success + * @return -EINVAL in case @p cb is NULL + */ +int bt_nus_inst_cb_register(struct bt_nus_inst *inst, struct bt_nus_cb *cb, void *ctx); + +/** @brief Send Data to NUS Instance + * + * @note This API sends the data to the specified peer. + * + * @param conn Connection object to send data to. NULL if notifying all peers. + * @param inst Pointer to instance of NUS service. NULL if using default instance. + * @param data Pointer to buffer with bytes to send. + * @param len Length in bytes of data to send. + * + * @return 0 on success, negative error code if failed. + * @return -EAGAIN when Bluetooth stack has not been enabled. + * @return -ENOTCONN when either no connection has been established or no peers + * have subscribed. + */ +int bt_nus_inst_send(struct bt_conn *conn, + struct bt_nus_inst *inst, + const void *data, + uint16_t len); + +/** @brief NUS server callback register + * + * @param cb Pointer to callbacks structure. Must be valid throughout the + * lifetime of the application. + * @param ctx User context to be provided through the callback. + * + * @return 0 on success, negative error code if failed. + * @return -EINVAL in case @p cb is NULL + */ +static inline int bt_nus_cb_register(struct bt_nus_cb *cb, void *ctx) +{ + return bt_nus_inst_cb_register(NULL, cb, ctx); +} + +/** @brief Send Data over NUS + * + * @note This API sends the data to the specified peer. + * + * @param conn Connection object to send data to. NULL if notifying all peers. + * @param data Pointer to buffer with bytes to send. + * @param len Length in bytes of data to send. + * + * @return 0 on success, negative error code if failed. + * @return -EAGAIN when Bluetooth stack has not been enabled. + * @return -ENOTCONN when either no connection has been established or no peers + * have subscribed. + */ +static inline int bt_nus_send(struct bt_conn *conn, const void *data, uint16_t len) +{ + return bt_nus_inst_send(conn, NULL, data, len); +} + +#ifdef __cplusplus +} +#endif + + +#endif /* ZEPHYR_INCLUDE_BLUETOOTH_SERVICES_NUS_H_ */ diff --git a/include/zephyr/bluetooth/services/nus/inst.h b/include/zephyr/bluetooth/services/nus/inst.h new file mode 100644 index 000000000000..de70495e829f --- /dev/null +++ b/include/zephyr/bluetooth/services/nus/inst.h @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2024 Croxel, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_BLUETOOTH_SERVICES_NUS_INST_H_ +#define ZEPHYR_INCLUDE_BLUETOOTH_SERVICES_NUS_INST_H_ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct bt_nus_inst { + /** Pointer to the NUS Service Instance */ + const struct bt_gatt_service_static *svc; + + /** List of subscribers to invoke callbacks on asynchronous events */ + sys_slist_t *cbs; +}; + +#define BT_UUID_NUS_SERVICE BT_UUID_DECLARE_128(BT_UUID_NUS_SRV_VAL) +#define BT_UUID_NUS_TX_CHAR BT_UUID_DECLARE_128(BT_UUID_NUS_TX_CHAR_VAL) +#define BT_UUID_NUS_RX_CHAR BT_UUID_DECLARE_128(BT_UUID_NUS_RX_CHAR_VAL) + +/** Required as the service may be instantiated outside of the module */ +ssize_t nus_bt_chr_write(struct bt_conn *conn, const struct bt_gatt_attr *attr, + const void *buf, uint16_t len, uint16_t offset, uint8_t flags); +void nus_ccc_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value); + +#define Z_INTERNAL_BT_NUS_INST_DEFINE(_name) \ + \ +BT_GATT_SERVICE_DEFINE(_name##_svc, \ + BT_GATT_PRIMARY_SERVICE(BT_UUID_NUS_SERVICE), \ + BT_GATT_CHARACTERISTIC(BT_UUID_NUS_TX_CHAR, \ + BT_GATT_CHRC_NOTIFY, \ + BT_GATT_PERM_NONE, \ + NULL, NULL, NULL), \ + BT_GATT_CCC(nus_ccc_cfg_changed, \ + BT_GATT_PERM_READ | BT_GATT_PERM_WRITE), \ + BT_GATT_CHARACTERISTIC(BT_UUID_NUS_RX_CHAR, \ + BT_GATT_CHRC_WRITE | \ + BT_GATT_CHRC_WRITE_WITHOUT_RESP, \ + BT_GATT_PERM_WRITE, \ + NULL, nus_bt_chr_write, NULL), \ +); \ + \ +sys_slist_t _name##_cbs = SYS_SLIST_STATIC_INIT(&_name##_cbs); \ + \ +STRUCT_SECTION_ITERABLE(bt_nus_inst, _name) = { \ + .svc = &_name##_svc, \ + .cbs = &_name##_cbs, \ +} + +#ifdef __cplusplus +} +#endif + +#endif /* ZEPHYR_INCLUDE_BLUETOOTH_SERVICES_NUS_INST_H_ */ diff --git a/include/zephyr/linker/common-ram.ld b/include/zephyr/linker/common-ram.ld index df70b13ca73d..847c8373548e 100644 --- a/include/zephyr/linker/common-ram.ld +++ b/include/zephyr/linker/common-ram.ld @@ -140,6 +140,10 @@ ITERABLE_SECTION_RAM(device_mutable, 4) #endif +#if defined(CONFIG_BT_NUS) + ITERABLE_SECTION_RAM(bt_nus_inst, 4) +#endif + #ifdef CONFIG_USERSPACE _static_kernel_objects_end = .; #endif diff --git a/samples/bluetooth/peripheral_nus/CMakeLists.txt b/samples/bluetooth/peripheral_nus/CMakeLists.txt new file mode 100644 index 000000000000..2da8d777ccb8 --- /dev/null +++ b/samples/bluetooth/peripheral_nus/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(peripheral_nus) + +target_sources(app PRIVATE + src/main.c +) diff --git a/samples/bluetooth/peripheral_nus/README.rst b/samples/bluetooth/peripheral_nus/README.rst new file mode 100644 index 000000000000..2cbf68f80caf --- /dev/null +++ b/samples/bluetooth/peripheral_nus/README.rst @@ -0,0 +1,26 @@ +.. _peripheral_nus: + +Bluetooth: Peripheral NUS +######################### + +Overview +******** + +This sample demonstrates the usage of the NUS service (Nordic UART Service) as a serial +endpoint to exchange data. In this case, the sample assumes the data is UTF-8 encoded, +but it may be binary data. Once the user connects to the device and subscribes to the TX +characteristic, it will start receiving periodic notifications with "Hello World!\n". + +Requirements +************ + +* BlueZ running on the host, or +* A board with BLE support + +Building and Running +******************** + +This sample can be found under :zephyr_file:`samples/bluetooth/peripheral_nus` in the +Zephyr tree. + +See :ref:`bluetooth samples section ` for details. diff --git a/samples/bluetooth/peripheral_nus/prj.conf b/samples/bluetooth/peripheral_nus/prj.conf new file mode 100644 index 000000000000..8eafb75f19cd --- /dev/null +++ b/samples/bluetooth/peripheral_nus/prj.conf @@ -0,0 +1,3 @@ +CONFIG_BT=y +CONFIG_BT_PERIPHERAL=y +CONFIG_BT_NUS=y diff --git a/samples/bluetooth/peripheral_nus/sample.yaml b/samples/bluetooth/peripheral_nus/sample.yaml new file mode 100644 index 000000000000..af70193ec1e0 --- /dev/null +++ b/samples/bluetooth/peripheral_nus/sample.yaml @@ -0,0 +1,13 @@ +sample: + name: Bluetooth Peripheral NUS + description: Demonstrates the NUS GATT Service (Nordic UART Service) +tests: + sample.bluetooth.peripheral_nus: + harness: bluetooth + platform_allow: + - qemu_cortex_m3 + - qemu_x86 + - nrf52840dk/nrf52840 + integration_platforms: + - qemu_cortex_m3 + tags: bluetooth diff --git a/samples/bluetooth/peripheral_nus/src/main.c b/samples/bluetooth/peripheral_nus/src/main.c new file mode 100644 index 000000000000..e94a45a0e083 --- /dev/null +++ b/samples/bluetooth/peripheral_nus/src/main.c @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2024 Croxel, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#define DEVICE_NAME CONFIG_BT_DEVICE_NAME +#define DEVICE_NAME_LEN (sizeof(DEVICE_NAME) - 1) + +static const struct bt_data ad[] = { + BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)), + BT_DATA(BT_DATA_NAME_COMPLETE, DEVICE_NAME, DEVICE_NAME_LEN), +}; + +static const struct bt_data sd[] = { + BT_DATA_BYTES(BT_DATA_UUID128_ALL, BT_UUID_NUS_SRV_VAL), +}; + +static void notif_enabled(bool enabled, void *ctx) +{ + ARG_UNUSED(ctx); + + printk("%s() - %s\n", __func__, (enabled ? "Enabled" : "Disabled")); +} + +static void received(struct bt_conn *conn, const void *data, uint16_t len, void *ctx) +{ + char message[CONFIG_BT_L2CAP_TX_MTU + 1] = ""; + + ARG_UNUSED(conn); + ARG_UNUSED(ctx); + + memcpy(message, data, MIN(sizeof(message) - 1, len)); + printk("%s() - Len: %d, Message: %s\n", __func__, len, message); +} + +struct bt_nus_cb nus_listener = { + .notif_enabled = notif_enabled, + .received = received, +}; + +int main(void) +{ + int err; + + printk("Sample - Bluetooth Peripheral NUS\n"); + + err = bt_nus_cb_register(&nus_listener, NULL); + if (err) { + printk("Failed to register NUS callback: %d\n", err); + return err; + } + + err = bt_enable(NULL); + if (err) { + printk("Failed to enable bluetooth: %d\n", err); + return err; + } + + err = bt_le_adv_start(BT_LE_ADV_CONN, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd)); + if (err) { + printk("Failed to start advertising: %d\n", err); + return err; + } + + printk("Initialization complete\n"); + + while (true) { + const char *hello_world = "Hello World!\n"; + + k_sleep(K_SECONDS(3)); + + err = bt_nus_send(NULL, hello_world, strlen(hello_world)); + printk("Data send - Result: %d\n", err); + + if (err < 0 && (err != -EAGAIN) && (err != -ENOTCONN)) { + return err; + } + } + + return 0; +} diff --git a/samples/subsys/logging/logger/bt.overlay b/samples/subsys/logging/logger/bt.overlay new file mode 100644 index 000000000000..29a2d9c5f4d6 --- /dev/null +++ b/samples/subsys/logging/logger/bt.overlay @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2024 Croxel, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + / { + chosen { + zephyr,console = &bt_nus_console_uart; + }; + + bt_nus_console_uart: bt_nus_console_uart { + compatible = "zephyr,nus-uart"; + rx-fifo-size = <1024>; + tx-fifo-size = <1024>; + }; +}; diff --git a/samples/subsys/logging/logger/overlay-bt.conf b/samples/subsys/logging/logger/overlay-bt.conf new file mode 100644 index 000000000000..bae817ce5623 --- /dev/null +++ b/samples/subsys/logging/logger/overlay-bt.conf @@ -0,0 +1,16 @@ +CONFIG_SERIAL=y +CONFIG_CONSOLE=y +CONFIG_UART_CONSOLE=y + +CONFIG_BT=y +CONFIG_BT_PERIPHERAL=y +CONFIG_BT_NUS=y +CONFIG_UART_BT=y +CONFIG_BT_NUS_AUTO_START_BLUETOOTH=y + +# Bluetooth optimizations to allow larger data packets. +CONFIG_BT_RX_STACK_SIZE=2048 +CONFIG_BT_L2CAP_TX_MTU=512 +CONFIG_BT_BUF_ACL_RX_SIZE=502 +CONFIG_BT_BUF_ACL_TX_SIZE=502 +CONFIG_BT_CTLR_DATA_LENGTH_MAX=251 diff --git a/samples/subsys/logging/logger/sample.yaml b/samples/subsys/logging/logger/sample.yaml index 3d8220766854..413be4f190d7 100644 --- a/samples/subsys/logging/logger/sample.yaml +++ b/samples/subsys/logging/logger/sample.yaml @@ -29,6 +29,21 @@ tests: - CONFIG_LOG_BACKEND_RTT=y - CONFIG_USE_SEGGER_RTT=y + sample.logger.bt: + platform_allow: + - nrf52840dk/nrf52840 + integration_platforms: + - nrf52840dk/nrf52840 + tags: + - logging + - bluetooth + filter: CONFIG_DT_HAS_ZEPHYR_NUS_UART_ENABLED + arch_exclude: + - posix + extra_args: + - OVERLAY_CONFIG="overlay-bt.conf" + - DTC_OVERLAY_FILE="bt.overlay" + sample.logger.usermode: integration_platforms: - mps2/an385 diff --git a/samples/subsys/shell/shell_module/bt.overlay b/samples/subsys/shell/shell_module/bt.overlay new file mode 100644 index 000000000000..da4f46066f53 --- /dev/null +++ b/samples/subsys/shell/shell_module/bt.overlay @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2024 Croxel, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + / { + chosen { + zephyr,console = &bt_nus_console_uart; + zephyr,shell-uart = &bt_nus_console_uart; + }; + + bt_nus_console_uart: bt_nus_console_uart { + compatible = "zephyr,nus-uart"; + rx-fifo-size = <1024>; + tx-fifo-size = <1024>; + }; +}; diff --git a/samples/subsys/shell/shell_module/overlay-bt.conf b/samples/subsys/shell/shell_module/overlay-bt.conf new file mode 100644 index 000000000000..bae817ce5623 --- /dev/null +++ b/samples/subsys/shell/shell_module/overlay-bt.conf @@ -0,0 +1,16 @@ +CONFIG_SERIAL=y +CONFIG_CONSOLE=y +CONFIG_UART_CONSOLE=y + +CONFIG_BT=y +CONFIG_BT_PERIPHERAL=y +CONFIG_BT_NUS=y +CONFIG_UART_BT=y +CONFIG_BT_NUS_AUTO_START_BLUETOOTH=y + +# Bluetooth optimizations to allow larger data packets. +CONFIG_BT_RX_STACK_SIZE=2048 +CONFIG_BT_L2CAP_TX_MTU=512 +CONFIG_BT_BUF_ACL_RX_SIZE=502 +CONFIG_BT_BUF_ACL_TX_SIZE=502 +CONFIG_BT_CTLR_DATA_LENGTH_MAX=251 diff --git a/samples/subsys/shell/shell_module/sample.yaml b/samples/subsys/shell/shell_module/sample.yaml index ceba72d68422..fff5553b63d9 100644 --- a/samples/subsys/shell/shell_module/sample.yaml +++ b/samples/subsys/shell/shell_module/sample.yaml @@ -44,6 +44,20 @@ tests: extra_args: CONF_FILE="prj_minimal_rtt.conf" integration_platforms: - nrf52833dk/nrf52833 + sample.shell.shell_module.bt: + platform_allow: + - nrf52840dk/nrf52840 + integration_platforms: + - nrf52840dk/nrf52840 + tags: + - shell + - bluetooth + filter: CONFIG_DT_HAS_ZEPHYR_NUS_UART_ENABLED + arch_exclude: + - posix + extra_args: + - OVERLAY_CONFIG="overlay-bt.conf" + - DTC_OVERLAY_FILE="bt.overlay" sample.shell.shell_module.login: filter: CONFIG_SERIAL and dt_chosen_enabled("zephyr,shell-uart") tags: shell diff --git a/snippets/nus-console/README.rst b/snippets/nus-console/README.rst new file mode 100644 index 000000000000..5d84343094ba --- /dev/null +++ b/snippets/nus-console/README.rst @@ -0,0 +1,37 @@ +.. _snippet-nus-console: + +NUS Console Snippet (nus-console) +######################################## + +.. code-block:: console + + west build -S nus-console [...] + +Overview +******** + +This snippet redirects serial console output to a UART over NUS (Bluetooth LE) instance. +The Bluetooth Serial device used shall be configured using :ref:`devicetree`. + +Requirements +************ + +Hardware support for: + +- :kconfig:option:`CONFIG_BT` +- :kconfig:option:`CONFIG_BT_PERIPHERAL` +- :kconfig:option:`CONFIG_BT_NUS` +- :kconfig:option:`CONFIG_SERIAL` +- :kconfig:option:`CONFIG_CONSOLE` +- :kconfig:option:`CONFIG_UART_CONSOLE` + +A devicetree node with node label ``bt_nus_console_uart`` that points to an enabled +device node with nus-uart support. This should look roughly like this in +:ref:`your devicetree `: + +.. code-block:: DTS + + bt_nus_console_uart: bt_nus_console_uart { + compatible = "zephyr,nus-uart"; + /* ... */ + }; diff --git a/snippets/nus-console/nus-console.conf b/snippets/nus-console/nus-console.conf new file mode 100644 index 000000000000..bae817ce5623 --- /dev/null +++ b/snippets/nus-console/nus-console.conf @@ -0,0 +1,16 @@ +CONFIG_SERIAL=y +CONFIG_CONSOLE=y +CONFIG_UART_CONSOLE=y + +CONFIG_BT=y +CONFIG_BT_PERIPHERAL=y +CONFIG_BT_NUS=y +CONFIG_UART_BT=y +CONFIG_BT_NUS_AUTO_START_BLUETOOTH=y + +# Bluetooth optimizations to allow larger data packets. +CONFIG_BT_RX_STACK_SIZE=2048 +CONFIG_BT_L2CAP_TX_MTU=512 +CONFIG_BT_BUF_ACL_RX_SIZE=502 +CONFIG_BT_BUF_ACL_TX_SIZE=502 +CONFIG_BT_CTLR_DATA_LENGTH_MAX=251 diff --git a/snippets/nus-console/nus-console.overlay b/snippets/nus-console/nus-console.overlay new file mode 100644 index 000000000000..71b1b32079a1 --- /dev/null +++ b/snippets/nus-console/nus-console.overlay @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2024 Croxel, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/ { + chosen { + zephyr,console = &bt_nus_console_uart; + }; + + bt_nus_console_uart: bt_nus_console_uart { + compatible = "zephyr,nus-uart"; + rx-fifo-size = <1024>; + tx-fifo-size = <1024>; + }; +}; diff --git a/snippets/nus-console/snippet.yml b/snippets/nus-console/snippet.yml new file mode 100644 index 000000000000..3271a4774283 --- /dev/null +++ b/snippets/nus-console/snippet.yml @@ -0,0 +1,4 @@ +name: nus-console +append: + EXTRA_CONF_FILE: nus-console.conf + EXTRA_DTC_OVERLAY_FILE: nus-console.overlay diff --git a/subsys/bluetooth/services/CMakeLists.txt b/subsys/bluetooth/services/CMakeLists.txt index 771f293dad21..bb219891f0fd 100644 --- a/subsys/bluetooth/services/CMakeLists.txt +++ b/subsys/bluetooth/services/CMakeLists.txt @@ -16,3 +16,7 @@ endif() if(CONFIG_BT_IAS OR CONFIG_BT_IAS_CLIENT) add_subdirectory(ias) endif() + +if(CONFIG_BT_NUS) + add_subdirectory(nus) +endif() diff --git a/subsys/bluetooth/services/Kconfig b/subsys/bluetooth/services/Kconfig index ecdbe4d15b1d..f4a42ac37a44 100644 --- a/subsys/bluetooth/services/Kconfig +++ b/subsys/bluetooth/services/Kconfig @@ -14,6 +14,8 @@ rsource "Kconfig.hrs" rsource "Kconfig.tps" +rsource "nus/Kconfig.nus" + rsource "ias/Kconfig.ias" rsource "ots/Kconfig" diff --git a/subsys/bluetooth/services/nus/CMakeLists.txt b/subsys/bluetooth/services/nus/CMakeLists.txt new file mode 100644 index 000000000000..a15271cba3c5 --- /dev/null +++ b/subsys/bluetooth/services/nus/CMakeLists.txt @@ -0,0 +1,14 @@ +# Copyright (c) 2024 Croxel, Inc. +# SPDX-License-Identifier: Apache-2.0 + +zephyr_library() + +zephyr_library_include_directories(.) +zephyr_library_sources( + nus.c + nus_inst.c +) + +zephyr_library_sources_ifdef(CONFIG_BT_NUS_AUTO_START_BLUETOOTH + bt_nus_auto_start_bt.c +) diff --git a/subsys/bluetooth/services/nus/Kconfig.nus b/subsys/bluetooth/services/nus/Kconfig.nus new file mode 100644 index 000000000000..1841226d7c78 --- /dev/null +++ b/subsys/bluetooth/services/nus/Kconfig.nus @@ -0,0 +1,26 @@ +# Copyright (c) 2024 Croxel, Inc. +# SPDX-License-Identifier: Apache-2.0 + +menuconfig BT_NUS + bool "GATT Nordic UART Service" + +if BT_NUS + +config BT_NUS_DEFAULT_INSTANCE + bool "Use default NUS Service instance" + default y if !UART_BT + help + Enable default Nordic UART Service Instance. Allows using the NUS as + the other services, where the Service is hosted by the subsystem itself. + If the user wishes to declare NUS instances externally by using + BT_NUS_INST_DEFINE(), it may not be beneficial having an internal + instance as well. + +config BT_NUS_AUTO_START_BLUETOOTH + bool "Auto-enable Bluetooth stack and start LE advertisements" + help + Auto-Enable the Bluetooth stack and start advertising with the NUS + UUID. Useful to run applications that inherently do not deal with + Bluetooth (e.g: Non-Bluetooth samples using UART over Bluetooth LE). + +endif # BT_NUS diff --git a/subsys/bluetooth/services/nus/bt_nus_auto_start_bt.c b/subsys/bluetooth/services/nus/bt_nus_auto_start_bt.c new file mode 100644 index 000000000000..be7b16998a03 --- /dev/null +++ b/subsys/bluetooth/services/nus/bt_nus_auto_start_bt.c @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024 Croxel, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#define DEVICE_NAME CONFIG_BT_DEVICE_NAME +#define DEVICE_NAME_LEN (sizeof(DEVICE_NAME) - 1) + +static const struct bt_data ad[] = { + BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)), + BT_DATA(BT_DATA_NAME_COMPLETE, DEVICE_NAME, DEVICE_NAME_LEN), +}; + +static const struct bt_data sd[] = { + BT_DATA_BYTES(BT_DATA_UUID128_ALL, BT_UUID_NUS_SRV_VAL), +}; + +static int bt_nus_auto_start(void) +{ + int err; + + err = bt_enable(NULL); + __ASSERT_NO_MSG(!err); + + err = bt_le_adv_start(BT_LE_ADV_CONN, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd)); + __ASSERT_NO_MSG(!err); + + return 0; +} + +SYS_INIT(bt_nus_auto_start, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY); diff --git a/subsys/bluetooth/services/nus/nus.c b/subsys/bluetooth/services/nus/nus.c new file mode 100644 index 000000000000..3dc2fbe6ca12 --- /dev/null +++ b/subsys/bluetooth/services/nus/nus.c @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2024 Croxel, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include "nus_internal.h" + +ssize_t nus_bt_chr_write(struct bt_conn *conn, const struct bt_gatt_attr *attr, + const void *buf, uint16_t len, uint16_t offset, uint8_t flags) +{ + struct bt_nus_cb *listener = NULL; + struct bt_nus_inst *instance = NULL; + + instance = bt_nus_inst_get_from_attr(attr); + __ASSERT_NO_MSG(instance); + + SYS_SLIST_FOR_EACH_CONTAINER(instance->cbs, listener, _node) { + if (listener->received) { + listener->received(conn, buf, len, listener->ctx); + } + } + + return len; +} + +void nus_ccc_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value) +{ + struct bt_nus_cb *listener = NULL; + struct bt_nus_inst *instance = NULL; + + instance = bt_nus_inst_get_from_attr(attr); + __ASSERT_NO_MSG(instance); + + SYS_SLIST_FOR_EACH_CONTAINER(instance->cbs, listener, _node) { + if (listener->notif_enabled) { + listener->notif_enabled((value == 1), listener->ctx); + } + } +} + +int bt_nus_inst_cb_register(struct bt_nus_inst *instance, struct bt_nus_cb *cb, void *ctx) +{ + if (!cb) { + return -EINVAL; + } + + if (!instance) { + if (IS_ENABLED(CONFIG_BT_NUS_DEFAULT_INSTANCE)) { + instance = bt_nus_inst_default(); + } else { + return -ENOTSUP; + } + } + + cb->ctx = ctx; + sys_slist_append(instance->cbs, &cb->_node); + + return 0; +} + +int bt_nus_inst_send(struct bt_conn *conn, + struct bt_nus_inst *instance, + const void *data, + uint16_t len) +{ + if (!data || !len) { + return -EINVAL; + } + + if (!instance) { + if (IS_ENABLED(CONFIG_BT_NUS_DEFAULT_INSTANCE)) { + instance = bt_nus_inst_default(); + } else { + return -ENOTSUP; + } + } + + return bt_gatt_notify(conn, &instance->svc->attrs[1], data, len); +} diff --git a/subsys/bluetooth/services/nus/nus_inst.c b/subsys/bluetooth/services/nus/nus_inst.c new file mode 100644 index 000000000000..0f88bb8dda8a --- /dev/null +++ b/subsys/bluetooth/services/nus/nus_inst.c @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024 Croxel, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "nus_internal.h" + +#if defined(CONFIG_BT_NUS_DEFAULT_INSTANCE) +BT_NUS_INST_DEFINE(nus_def); +struct bt_nus_inst *bt_nus_inst_default(void) +{ + return &nus_def; +} +#else +struct bt_nus_inst *bt_nus_inst_default(void) { return NULL; } +#endif + +struct bt_nus_inst *bt_nus_inst_get_from_attr(const struct bt_gatt_attr *attr) +{ + STRUCT_SECTION_FOREACH(bt_nus_inst, instance) { + for (size_t i = 0 ; i < instance->svc->attr_count ; i++) { + if (attr == &instance->svc->attrs[i]) { + return instance; + } + } + } + + return NULL; +} diff --git a/subsys/bluetooth/services/nus/nus_internal.h b/subsys/bluetooth/services/nus/nus_internal.h new file mode 100644 index 000000000000..3e23d55929e0 --- /dev/null +++ b/subsys/bluetooth/services/nus/nus_internal.h @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2024 Croxel, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_BLUETOOTH_SERVICES_NUS_INTERNAL_H_ +#define ZEPHYR_BLUETOOTH_SERVICES_NUS_INTERNAL_H_ + +#include +#include + +struct bt_nus_inst *bt_nus_inst_default(void); +struct bt_nus_inst *bt_nus_inst_get_from_attr(const struct bt_gatt_attr *attr); + +#endif /* ZEPHYR_BLUETOOTH_SERVICES_NUS_INTERNAL_H_ */