From 62dbb947240fb50e5261c253391f28d05863ad04 Mon Sep 17 00:00:00 2001 From: pstruebi Date: Mon, 30 Jun 2025 09:28:01 +0200 Subject: [PATCH] Initial commit for nrf5340_audio --- .gitignore | 6 + CMakeLists.txt | 79 + Kconfig | 66 + Kconfig.defaults | 128 ++ Kconfig.sysbuild | 10 + LICENSE | 36 + boards/nrf5340_audio_dk_nrf5340_cpuapp.conf | 9 + ...f5340_audio_dk_nrf5340_cpuapp_fota.overlay | 39 + ...f5340_audio_dk_nrf5340_cpuapp_release.conf | 8 + boards/nrf5340dk_nrf5340_cpuapp.overlay | 18 + broadcast_sink/CMakeLists.txt | 8 + broadcast_sink/README.rst | 107 + broadcast_sink/main.c | 614 ++++++ broadcast_sink/overlay-broadcast_sink.conf | 60 + broadcast_source/CMakeLists.txt | 8 + broadcast_source/Kconfig.defaults | 45 + broadcast_source/README.rst | 96 + broadcast_source/main.c | 606 ++++++ .../overlay-broadcast_source.conf | 28 + doc/adapting_application.rst | 82 + doc/building.rst | 327 +++ doc/configuration.rst | 87 + doc/feature_support.rst | 44 + doc/firmware_architecture.rst | 290 +++ doc/fota.rst | 84 + doc/requirements.rst | 67 + doc/user_interface.rst | 160 ++ include/zbus_common.h | 102 + index.rst | 56 + pm_static_fota.yml | 55 + prj.conf | 72 + prj_fota.conf | 84 + prj_release.conf | 34 + sample.yaml | 47 + src/audio/CMakeLists.txt | 12 + src/audio/Kconfig | 388 ++++ src/audio/Kconfig.defaults | 13 + src/audio/audio_datapath.c | 1218 +++++++++++ src/audio/audio_datapath.h | 90 + src/audio/audio_system.c | 530 +++++ src/audio/audio_system.h | 111 + src/audio/le_audio_rx.c | 204 ++ src/audio/le_audio_rx.h | 31 + src/audio/streamctrl.h | 35 + src/audio/sw_codec_select.c | 464 +++++ src/audio/sw_codec_select.h | 131 ++ src/bluetooth/CMakeLists.txt | 17 + src/bluetooth/Kconfig | 129 ++ src/bluetooth/Kconfig.defaults | 25 + .../bt_content_control/CMakeLists.txt | 17 + src/bluetooth/bt_content_control/Kconfig | 19 + .../bt_content_control/bt_content_ctrl.c | 149 ++ .../bt_content_control/bt_content_ctrl.h | 74 + .../bt_content_control/media/Kconfig | 17 + .../media/bt_content_ctrl_media.c | 527 +++++ .../media/bt_content_ctrl_media_internal.h | 92 + src/bluetooth/bt_management/CMakeLists.txt | 44 + src/bluetooth/bt_management/Kconfig | 43 + .../bt_management/advertising/Kconfig | 78 + .../bt_management/advertising/Kconfig.default | 8 + .../bt_management/advertising/bt_mgmt_adv.c | 476 +++++ .../advertising/bt_mgmt_adv_internal.h | 27 + src/bluetooth/bt_management/bt_mgmt.c | 415 ++++ src/bluetooth/bt_management/bt_mgmt.h | 214 ++ .../bt_management/controller_config/Kconfig | 83 + .../controller_config/bt_mgmt_ctlr_cfg.c | 135 ++ .../bt_mgmt_ctlr_cfg_internal.h | 33 + src/bluetooth/bt_management/dfu/Kconfig | 40 + src/bluetooth/bt_management/dfu/bt_mgmt_dfu.c | 132 ++ .../bt_management/dfu/bt_mgmt_dfu_internal.h | 17 + src/bluetooth/bt_management/scanning/Kconfig | 59 + .../bt_management/scanning/Kconfig.defaults | 18 + .../bt_management/scanning/bt_mgmt_scan.c | 81 + .../scanning/bt_mgmt_scan_for_broadcast.c | 477 +++++ .../bt_mgmt_scan_for_broadcast_internal.h | 24 + .../scanning/bt_mgmt_scan_for_conn.c | 363 ++++ .../scanning/bt_mgmt_scan_for_conn_internal.h | 22 + .../bt_rendering_and_capture/CMakeLists.txt | 26 + .../bt_rendering_and_capture/Kconfig | 19 + .../bt_rendering_and_capture.c | 184 ++ .../bt_rendering_and_capture.h | 81 + .../bt_rendering_and_capture/volume/Kconfig | 29 + .../volume/bt_vol_ctlr.c | 254 +++ .../volume/bt_vol_ctlr_internal.h | 79 + .../volume/bt_vol_rend.c | 109 + .../volume/bt_vol_rend_internal.h | 65 + src/bluetooth/bt_stream/CMakeLists.txt | 36 + src/bluetooth/bt_stream/broadcast/Kconfig | 259 +++ .../bt_stream/broadcast/Kconfig.defaults | 11 + .../bt_stream/broadcast/broadcast_sink.c | 849 ++++++++ .../bt_stream/broadcast/broadcast_sink.h | 110 + .../bt_stream/broadcast/broadcast_source.c | 800 ++++++++ .../bt_stream/broadcast/broadcast_source.h | 266 +++ .../bt_stream/bt_le_audio_tx/CMakeLists.txt | 10 + .../bt_stream/bt_le_audio_tx/bt_le_audio_tx.c | 368 ++++ .../bt_stream/bt_le_audio_tx/bt_le_audio_tx.h | 62 + src/bluetooth/bt_stream/le_audio.c | 318 +++ src/bluetooth/bt_stream/le_audio.h | 201 ++ src/bluetooth/bt_stream/unicast/Kconfig | 161 ++ .../bt_stream/unicast/Kconfig.defaults | 57 + .../bt_stream/unicast/unicast_client.c | 1827 +++++++++++++++++ .../bt_stream/unicast/unicast_client.h | 142 ++ .../bt_stream/unicast/unicast_server.c | 766 +++++++ .../bt_stream/unicast/unicast_server.h | 80 + src/drivers/CMakeLists.txt | 9 + src/drivers/Kconfig | 32 + src/drivers/cs47l63_comm.c | 432 ++++ src/drivers/cs47l63_comm.h | 21 + src/drivers/cs47l63_reg_conf.h | 156 ++ src/modules/CMakeLists.txt | 24 + src/modules/Kconfig | 233 +++ src/modules/Kconfig.defaults | 45 + src/modules/audio_i2s.c | 158 ++ src/modules/audio_i2s.h | 90 + src/modules/audio_sync_timer.c | 289 +++ src/modules/audio_sync_timer.h | 37 + src/modules/audio_usb.c | 214 ++ src/modules/audio_usb.h | 49 + src/modules/board.h | 62 + src/modules/button_assignments.h | 29 + src/modules/button_handler.c | 305 +++ src/modules/button_handler.h | 37 + src/modules/hw_codec.c | 487 +++++ src/modules/hw_codec.h | 96 + src/modules/lc3_file.c | 154 ++ src/modules/lc3_file.h | 90 + src/modules/lc3_streamer.c | 545 +++++ src/modules/lc3_streamer.h | 138 ++ src/modules/led.c | 300 +++ src/modules/led.h | 102 + src/modules/power_meas.c | 231 +++ src/modules/sd_card.c | 490 +++++ src/modules/sd_card.h | 156 ++ src/modules/sd_card_playback.c | 586 ++++++ src/modules/sd_card_playback.h | 76 + src/utils/CMakeLists.txt | 15 + src/utils/Kconfig | 60 + src/utils/Kconfig.defaults | 9 + src/utils/board_version.c | 168 ++ src/utils/board_version.h | 37 + src/utils/channel_assignment.c | 54 + src/utils/channel_assignment.h | 43 + src/utils/error_handler.c | 63 + src/utils/fw_info_app.c.in | 69 + src/utils/fw_info_app.h | 18 + src/utils/macros/macros_common.h | 72 + src/utils/nrf5340_audio_dk.c | 146 ++ src/utils/nrf5340_audio_dk.h | 19 + src/utils/uicr.c | 43 + src/utils/uicr.h | 36 + sysbuild/ipc_radio/prj.conf | 50 + sysbuild/ipc_radio/prj_release.conf | 56 + sysbuild/mcuboot/app_fota.overlay | 5 + ...f5340_audio_dk_nrf5340_cpuapp_fota.overlay | 7 + sysbuild/mcuboot/prj_fota.conf | 62 + sysbuild_fota.conf | 15 + tools/buildprog/buildprog.py | 418 ++++ tools/buildprog/fw_info_data.py | 96 + tools/buildprog/nrf5340_audio_dk_devices.json | 17 + tools/buildprog/nrf5340_audio_dk_devices.py | 107 + tools/buildprog/program.py | 135 ++ .../uart_terminal/scripts/get_serial_ports.py | 28 + .../scripts/linux_terminator_config | 62 + tools/uart_terminal/scripts/open_putty.py | 15 + .../uart_terminal/scripts/open_terminator.py | 16 + tools/uart_terminal/uart_terminal.py | 19 + unicast_client/CMakeLists.txt | 8 + unicast_client/README.rst | 144 ++ unicast_client/main.c | 583 ++++++ unicast_client/overlay-unicast_client.conf | 43 + unicast_server/CMakeLists.txt | 8 + unicast_server/README.rst | 150 ++ unicast_server/main.c | 570 +++++ unicast_server/overlay-unicast_server.conf | 49 + 174 files changed, 27001 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 Kconfig create mode 100644 Kconfig.defaults create mode 100644 Kconfig.sysbuild create mode 100644 LICENSE create mode 100644 boards/nrf5340_audio_dk_nrf5340_cpuapp.conf create mode 100644 boards/nrf5340_audio_dk_nrf5340_cpuapp_fota.overlay create mode 100644 boards/nrf5340_audio_dk_nrf5340_cpuapp_release.conf create mode 100644 boards/nrf5340dk_nrf5340_cpuapp.overlay create mode 100644 broadcast_sink/CMakeLists.txt create mode 100644 broadcast_sink/README.rst create mode 100644 broadcast_sink/main.c create mode 100644 broadcast_sink/overlay-broadcast_sink.conf create mode 100644 broadcast_source/CMakeLists.txt create mode 100644 broadcast_source/Kconfig.defaults create mode 100644 broadcast_source/README.rst create mode 100644 broadcast_source/main.c create mode 100644 broadcast_source/overlay-broadcast_source.conf create mode 100644 doc/adapting_application.rst create mode 100644 doc/building.rst create mode 100644 doc/configuration.rst create mode 100644 doc/feature_support.rst create mode 100644 doc/firmware_architecture.rst create mode 100644 doc/fota.rst create mode 100644 doc/requirements.rst create mode 100644 doc/user_interface.rst create mode 100644 include/zbus_common.h create mode 100644 index.rst create mode 100644 pm_static_fota.yml create mode 100644 prj.conf create mode 100644 prj_fota.conf create mode 100644 prj_release.conf create mode 100644 sample.yaml create mode 100644 src/audio/CMakeLists.txt create mode 100644 src/audio/Kconfig create mode 100644 src/audio/Kconfig.defaults create mode 100644 src/audio/audio_datapath.c create mode 100644 src/audio/audio_datapath.h create mode 100644 src/audio/audio_system.c create mode 100644 src/audio/audio_system.h create mode 100644 src/audio/le_audio_rx.c create mode 100644 src/audio/le_audio_rx.h create mode 100644 src/audio/streamctrl.h create mode 100644 src/audio/sw_codec_select.c create mode 100644 src/audio/sw_codec_select.h create mode 100644 src/bluetooth/CMakeLists.txt create mode 100644 src/bluetooth/Kconfig create mode 100644 src/bluetooth/Kconfig.defaults create mode 100644 src/bluetooth/bt_content_control/CMakeLists.txt create mode 100644 src/bluetooth/bt_content_control/Kconfig create mode 100644 src/bluetooth/bt_content_control/bt_content_ctrl.c create mode 100644 src/bluetooth/bt_content_control/bt_content_ctrl.h create mode 100644 src/bluetooth/bt_content_control/media/Kconfig create mode 100644 src/bluetooth/bt_content_control/media/bt_content_ctrl_media.c create mode 100644 src/bluetooth/bt_content_control/media/bt_content_ctrl_media_internal.h create mode 100644 src/bluetooth/bt_management/CMakeLists.txt create mode 100644 src/bluetooth/bt_management/Kconfig create mode 100644 src/bluetooth/bt_management/advertising/Kconfig create mode 100644 src/bluetooth/bt_management/advertising/Kconfig.default create mode 100644 src/bluetooth/bt_management/advertising/bt_mgmt_adv.c create mode 100644 src/bluetooth/bt_management/advertising/bt_mgmt_adv_internal.h create mode 100644 src/bluetooth/bt_management/bt_mgmt.c create mode 100644 src/bluetooth/bt_management/bt_mgmt.h create mode 100644 src/bluetooth/bt_management/controller_config/Kconfig create mode 100644 src/bluetooth/bt_management/controller_config/bt_mgmt_ctlr_cfg.c create mode 100644 src/bluetooth/bt_management/controller_config/bt_mgmt_ctlr_cfg_internal.h create mode 100644 src/bluetooth/bt_management/dfu/Kconfig create mode 100644 src/bluetooth/bt_management/dfu/bt_mgmt_dfu.c create mode 100644 src/bluetooth/bt_management/dfu/bt_mgmt_dfu_internal.h create mode 100644 src/bluetooth/bt_management/scanning/Kconfig create mode 100644 src/bluetooth/bt_management/scanning/Kconfig.defaults create mode 100644 src/bluetooth/bt_management/scanning/bt_mgmt_scan.c create mode 100644 src/bluetooth/bt_management/scanning/bt_mgmt_scan_for_broadcast.c create mode 100644 src/bluetooth/bt_management/scanning/bt_mgmt_scan_for_broadcast_internal.h create mode 100644 src/bluetooth/bt_management/scanning/bt_mgmt_scan_for_conn.c create mode 100644 src/bluetooth/bt_management/scanning/bt_mgmt_scan_for_conn_internal.h create mode 100644 src/bluetooth/bt_rendering_and_capture/CMakeLists.txt create mode 100644 src/bluetooth/bt_rendering_and_capture/Kconfig create mode 100644 src/bluetooth/bt_rendering_and_capture/bt_rendering_and_capture.c create mode 100644 src/bluetooth/bt_rendering_and_capture/bt_rendering_and_capture.h create mode 100644 src/bluetooth/bt_rendering_and_capture/volume/Kconfig create mode 100644 src/bluetooth/bt_rendering_and_capture/volume/bt_vol_ctlr.c create mode 100644 src/bluetooth/bt_rendering_and_capture/volume/bt_vol_ctlr_internal.h create mode 100644 src/bluetooth/bt_rendering_and_capture/volume/bt_vol_rend.c create mode 100644 src/bluetooth/bt_rendering_and_capture/volume/bt_vol_rend_internal.h create mode 100644 src/bluetooth/bt_stream/CMakeLists.txt create mode 100644 src/bluetooth/bt_stream/broadcast/Kconfig create mode 100644 src/bluetooth/bt_stream/broadcast/Kconfig.defaults create mode 100644 src/bluetooth/bt_stream/broadcast/broadcast_sink.c create mode 100644 src/bluetooth/bt_stream/broadcast/broadcast_sink.h create mode 100644 src/bluetooth/bt_stream/broadcast/broadcast_source.c create mode 100644 src/bluetooth/bt_stream/broadcast/broadcast_source.h create mode 100644 src/bluetooth/bt_stream/bt_le_audio_tx/CMakeLists.txt create mode 100644 src/bluetooth/bt_stream/bt_le_audio_tx/bt_le_audio_tx.c create mode 100644 src/bluetooth/bt_stream/bt_le_audio_tx/bt_le_audio_tx.h create mode 100644 src/bluetooth/bt_stream/le_audio.c create mode 100644 src/bluetooth/bt_stream/le_audio.h create mode 100644 src/bluetooth/bt_stream/unicast/Kconfig create mode 100644 src/bluetooth/bt_stream/unicast/Kconfig.defaults create mode 100644 src/bluetooth/bt_stream/unicast/unicast_client.c create mode 100644 src/bluetooth/bt_stream/unicast/unicast_client.h create mode 100644 src/bluetooth/bt_stream/unicast/unicast_server.c create mode 100644 src/bluetooth/bt_stream/unicast/unicast_server.h create mode 100644 src/drivers/CMakeLists.txt create mode 100644 src/drivers/Kconfig create mode 100644 src/drivers/cs47l63_comm.c create mode 100644 src/drivers/cs47l63_comm.h create mode 100644 src/drivers/cs47l63_reg_conf.h create mode 100644 src/modules/CMakeLists.txt create mode 100644 src/modules/Kconfig create mode 100644 src/modules/Kconfig.defaults create mode 100644 src/modules/audio_i2s.c create mode 100644 src/modules/audio_i2s.h create mode 100644 src/modules/audio_sync_timer.c create mode 100644 src/modules/audio_sync_timer.h create mode 100644 src/modules/audio_usb.c create mode 100644 src/modules/audio_usb.h create mode 100644 src/modules/board.h create mode 100644 src/modules/button_assignments.h create mode 100644 src/modules/button_handler.c create mode 100644 src/modules/button_handler.h create mode 100644 src/modules/hw_codec.c create mode 100644 src/modules/hw_codec.h create mode 100644 src/modules/lc3_file.c create mode 100644 src/modules/lc3_file.h create mode 100644 src/modules/lc3_streamer.c create mode 100644 src/modules/lc3_streamer.h create mode 100644 src/modules/led.c create mode 100644 src/modules/led.h create mode 100644 src/modules/power_meas.c create mode 100644 src/modules/sd_card.c create mode 100644 src/modules/sd_card.h create mode 100644 src/modules/sd_card_playback.c create mode 100644 src/modules/sd_card_playback.h create mode 100644 src/utils/CMakeLists.txt create mode 100644 src/utils/Kconfig create mode 100644 src/utils/Kconfig.defaults create mode 100644 src/utils/board_version.c create mode 100644 src/utils/board_version.h create mode 100644 src/utils/channel_assignment.c create mode 100644 src/utils/channel_assignment.h create mode 100644 src/utils/error_handler.c create mode 100644 src/utils/fw_info_app.c.in create mode 100644 src/utils/fw_info_app.h create mode 100644 src/utils/macros/macros_common.h create mode 100644 src/utils/nrf5340_audio_dk.c create mode 100644 src/utils/nrf5340_audio_dk.h create mode 100644 src/utils/uicr.c create mode 100644 src/utils/uicr.h create mode 100644 sysbuild/ipc_radio/prj.conf create mode 100644 sysbuild/ipc_radio/prj_release.conf create mode 100644 sysbuild/mcuboot/app_fota.overlay create mode 100644 sysbuild/mcuboot/boards/nrf5340_audio_dk_nrf5340_cpuapp_fota.overlay create mode 100644 sysbuild/mcuboot/prj_fota.conf create mode 100644 sysbuild_fota.conf create mode 100644 tools/buildprog/buildprog.py create mode 100644 tools/buildprog/fw_info_data.py create mode 100644 tools/buildprog/nrf5340_audio_dk_devices.json create mode 100644 tools/buildprog/nrf5340_audio_dk_devices.py create mode 100644 tools/buildprog/program.py create mode 100644 tools/uart_terminal/scripts/get_serial_ports.py create mode 100644 tools/uart_terminal/scripts/linux_terminator_config create mode 100644 tools/uart_terminal/scripts/open_putty.py create mode 100644 tools/uart_terminal/scripts/open_terminator.py create mode 100644 tools/uart_terminal/uart_terminal.py create mode 100644 unicast_client/CMakeLists.txt create mode 100644 unicast_client/README.rst create mode 100644 unicast_client/main.c create mode 100644 unicast_client/overlay-unicast_client.conf create mode 100644 unicast_server/CMakeLists.txt create mode 100644 unicast_server/README.rst create mode 100644 unicast_server/main.c create mode 100644 unicast_server/overlay-unicast_server.conf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..635a99b --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# editors +*.swp +*~ + +# build +/build*/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..94e075f --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,79 @@ +# +# Copyright (c) 2022 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +cmake_minimum_required(VERSION 3.20.0) + +# Flag which defines whether application is compiled as gateway/dongle or headset +add_compile_definitions(HEADSET=1) +add_compile_definitions(GATEWAY=2) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) + +# Test a configuration file option has been given +if(NOT DEFINED EXTRA_CONF_FILE) + message(FATAL_ERROR "No configuration file specified, set -- -DEXTRA_CONF_FILE=") +endif() + +project(nrf5340_audio) + +string(TIMESTAMP NRF5340_AUDIO_CORE_APP_COMP_DATE "%a %b %d %H:%M:%S %Y") + +# Generate fw_info_app.c +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/src/utils/fw_info_app.c.in" + "${CMAKE_BINARY_DIR}/fw_info_app.c" + @ONLY) + +# Target sources below are specific to the nRF5340 Audio DK HW +target_sources(app PRIVATE + ${CMAKE_BINARY_DIR}/fw_info_app.c + ) + +if (CONFIG_BT_BAP_BROADCAST_SINK) + add_subdirectory(broadcast_sink) +endif() + +if (CONFIG_BT_BAP_BROADCAST_SOURCE) + add_subdirectory(broadcast_source) +endif() + +if (CONFIG_BT_BAP_UNICAST_CLIENT) + add_subdirectory(unicast_client) +endif() + +if (CONFIG_BT_BAP_UNICAST_SERVER) + add_subdirectory(unicast_server) +endif() + + +# Include application events and configuration headers +zephyr_library_include_directories( + include + src/audio + src/bluetooth + src/drivers + src/modules + src/utils + src/utils/macros +) + +zephyr_library_include_directories(app PRIVATE + ${ZEPHYR_NRF_MODULE_DIR}/boards/arm/nrf5340_audio_dk_nrf5340) + +# Application sources +add_subdirectory(src/audio) +add_subdirectory(src/bluetooth) +add_subdirectory(src/drivers) +add_subdirectory(src/modules) +add_subdirectory(src/utils) + +## Cirrus Logic +if (CONFIG_HW_CODEC_CIRRUS_LOGIC) + if (ZEPHYR_CIRRUS_LOGIC_MODULE_DIR) + add_subdirectory(${ZEPHYR_CIRRUS_LOGIC_MODULE_DIR} cirrus_logic_bin_dir) + else() + message(FATAL_ERROR "Cirrus Logic/sdk-mcu-drivers repository not found\n") + endif() +endif() diff --git a/Kconfig b/Kconfig new file mode 100644 index 0000000..6997b4f --- /dev/null +++ b/Kconfig @@ -0,0 +1,66 @@ +# +# Copyright (c) 2018 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +menuconfig NRF5340_AUDIO + bool "nRF5340 Audio [EXPERIMENTAL]" + select EXPERIMENTAL + +if NRF5340_AUDIO + +config CUSTOM_BROADCASTER + bool "Allow custom broadcasters" + default n + help + Incomplete feature for now, but will introduce support for multiple BIGs and subgroups. + +config AUDIO_DEV + int "Select which device type to compile for. 1=HEADSET or 2=GATEWAY" + range 1 2 + default 1 + help + Setting this variable to 1 selects that the project is compiled + as a HEADSET device. + Setting to 2 will compile as a GATEWAY. + +choice NRF5340_AUDIO_TRANSPORT_MODE + prompt "Choose BIS or CIS for ISO transport" + default TRANSPORT_CIS if WALKIE_TALKIE_DEMO + default TRANSPORT_CIS + +config TRANSPORT_BIS + bool "Use BIS (Broadcast Isochronous Stream)" + +config TRANSPORT_CIS + bool "Use CIS (Connected Isochronous Stream)" + +endchoice + +#----------------------------------------------------------------------------# +rsource "Kconfig.defaults" +rsource "src/audio/Kconfig" +rsource "src/bluetooth/Kconfig" +rsource "src/drivers/Kconfig" +rsource "src/modules/Kconfig" +rsource "src/utils/Kconfig" + +#----------------------------------------------------------------------------# +menu "Logging" + +module = MAIN +module-str = main +source "subsys/logging/Kconfig.template.log_config" + +config PRINT_STACK_USAGE_MS + depends on THREAD_ANALYZER && INIT_STACKS + int "Print stack usage every x milliseconds" + default 5000 + +endmenu # Log levels + +#----------------------------------------------------------------------------# +endif # NRF5340_AUDIO + +source "Kconfig.zephyr" diff --git a/Kconfig.defaults b/Kconfig.defaults new file mode 100644 index 0000000..3e98206 --- /dev/null +++ b/Kconfig.defaults @@ -0,0 +1,128 @@ +# +# Copyright (c) 2022 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +config REBOOT + default y + +config MAIN_THREAD_PRIORITY + default 10 + +config MAIN_STACK_SIZE + default 1800 if SD_CARD_PLAYBACK + default 1600 + +config SYSTEM_WORKQUEUE_STACK_SIZE + default 1200 + +# As long as thread names are used, config must be set to "y" +config THREAD_NAME + default y + +# Workaround to not use fatal_error.c in NCS. Note that the system may still +# reset on error depending on the build configuraion +config RESET_ON_FATAL_ERROR + default n + +# Default Config for Debug and Release build +config BT + default y + +config ZBUS + default y + +config ZBUS_RUNTIME_OBSERVERS + default y + +config ZBUS_MSG_SUBSCRIBER + default y + +config SENSOR + default y + +config REGULATOR + default y + +config CONTIN_ARRAY + default y + +config NRFX_I2S0 + default y + +config PCM_MIX + default y + +config PSCM + default y + +config DATA_FIFO + default y + +# Enable NRFX_CLOCK for ACLK control +config NRFX_CLOCK + default y + +config I2C + default y + +choice LIBC_IMPLEMENTATION + # NOTE: Since we are not using minimal libc, error codes from + # minimal libc are not used + default NEWLIB_LIBC +endchoice + +# Audio codec LC3 related defines +# FPU_SHARING enables preservation of the hardware floating point registers +# across context switches to allow multiple threads to perform concurrent +# floating point operations. +config FPU + default y + +config FPU_SHARING + default y + +# Enable SDHC interface +config DISK_DRIVERS + default y + +config DISK_DRIVER_SDMMC + default y + +# Enable SPI interface +config SPI + default y + +# Enable ADC for board version readback +config ADC + default y + +# Allocate buffer on RAM for transferring chunck of data +# from Flash to SPI +config SPI_NRFX_RAM_BUFFER_SIZE + default 8 + +# Config the file system +config FILE_SYSTEM + default y + +config FAT_FILESYSTEM_ELM + default y + +config FS_FATFS_LFN + default y +choice FS_FATFS_LFN_MODE + # Using stack for LFN work queue + default FS_FATFS_LFN_MODE_STACK +endchoice + +# exFAT enabled to support longer file names and higher transfer speed +config FS_FATFS_EXFAT + default y + +config WATCHDOG + default y + +config TASK_WDT + default y diff --git a/Kconfig.sysbuild b/Kconfig.sysbuild new file mode 100644 index 0000000..a5679fa --- /dev/null +++ b/Kconfig.sysbuild @@ -0,0 +1,10 @@ +# +# Copyright (c) 2024 Nordic Semiconductor +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +config NRF_DEFAULT_IPC_RADIO + default y + +source "share/sysbuild/Kconfig" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fc9684f --- /dev/null +++ b/LICENSE @@ -0,0 +1,36 @@ +LicenseID: LicenseRef-PCFT + +ExtractedText: +Redistribution and use of the Audio subsystem for nRF5340 Software, in binary +and source code forms, with or without modification, are permitted provided that +the following conditions are met: + +1. Redistributions of source code form must retain the above copyright notice, + this list of conditions, and the following disclaimer. + +2. Redistributions in binary code form, except as embedded into a Nordic Semiconductor ASA + nRF53 chip or a software update for such product, must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or other + materials provided with the distribution. + +3. Neither the name of Packetcraft, Inc. nor Nordic Semiconductor ASA nor the names of its + contributors may be used to endorse or promote products derived from this software without + specific prior written permission. + +4. This software, with or without modification, must only be used with a Nordic Semiconductor ASA + nRF53 chip. + +5. Any software provided in binary or source code form under this license must not be reverse + engineered, decompiled, modified and/or disassembled. + +THIS SOFTWARE IS PROVIDED BY PACKETCRAFT, INC. AND NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS +OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, +NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE HEREBY DISCLAIMED. IN NO EVENT SHALL +PACKETCRAFT, INC., NORDIC SEMICONDUCTOR ASA, OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED +AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY +OF SUCH DAMAGE. + + diff --git a/boards/nrf5340_audio_dk_nrf5340_cpuapp.conf b/boards/nrf5340_audio_dk_nrf5340_cpuapp.conf new file mode 100644 index 0000000..ccddc56 --- /dev/null +++ b/boards/nrf5340_audio_dk_nrf5340_cpuapp.conf @@ -0,0 +1,9 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +CONFIG_NRF5340_AUDIO_CS47L63_DRIVER=y +CONFIG_NRF5340_AUDIO_POWER_MEASUREMENT=y +CONFIG_NRF5340_AUDIO_SD_CARD_MODULE=y diff --git a/boards/nrf5340_audio_dk_nrf5340_cpuapp_fota.overlay b/boards/nrf5340_audio_dk_nrf5340_cpuapp_fota.overlay new file mode 100644 index 0000000..731274b --- /dev/null +++ b/boards/nrf5340_audio_dk_nrf5340_cpuapp_fota.overlay @@ -0,0 +1,39 @@ +/ { + chosen { + nordic,pm-ext-flash = &mx25r64; + }; +}; + +&qspi { + status = "disabled"; +}; + +&spi4 { + cs-gpios = <&gpio1 10 GPIO_ACTIVE_LOW>, <&gpio0 11 GPIO_ACTIVE_LOW>, <&gpio0 17 GPIO_ACTIVE_LOW>; + status = "okay"; + mx25r64: mx25r6435f@0 { + + compatible = "jedec,spi-nor"; + reg = <0>; + spi-max-frequency = <8000000>; + + jedec-id = [c2 28 17]; + sfdp-bfp = [ + e5 20 f1 ff ff ff ff 03 44 eb 08 6b 08 3b 04 bb + ee ff ff ff ff ff 00 ff ff ff 00 ff 0c 20 0f 52 + 10 d8 00 ff 23 72 f5 00 82 ed 04 cc 44 83 68 44 + 30 b0 30 b0 f7 c4 d5 5c 00 be 29 ff f0 d0 ff ff + ]; + size = <67108864>; + has-dpd; + t-enter-dpd = <10000>; + t-exit-dpd = <5000>; + dpd-wakeup-sequence = <30000 20 45000>; + }; +}; + +&gpio_fwd { + uart { + gpios = < &gpio1 0x9 0x0 >, < &gpio1 0x8 0x0 >, < &gpio1 0xb 0x0 >; + }; +}; diff --git a/boards/nrf5340_audio_dk_nrf5340_cpuapp_release.conf b/boards/nrf5340_audio_dk_nrf5340_cpuapp_release.conf new file mode 100644 index 0000000..068697c --- /dev/null +++ b/boards/nrf5340_audio_dk_nrf5340_cpuapp_release.conf @@ -0,0 +1,8 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +CONFIG_NRF5340_AUDIO_CS47L63_DRIVER=y +CONFIG_NRF5340_AUDIO_SD_CARD_MODULE=y diff --git a/boards/nrf5340dk_nrf5340_cpuapp.overlay b/boards/nrf5340dk_nrf5340_cpuapp.overlay new file mode 100644 index 0000000..9c36cd5 --- /dev/null +++ b/boards/nrf5340dk_nrf5340_cpuapp.overlay @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +&usbd { + hs_0: hs_0 { + compatible = "usb-audio-hs"; + mic-feature-mute; + mic-channel-l; + mic-channel-r; + + hp-feature-mute; + hp-channel-l; + hp-channel-r; + }; +}; diff --git a/broadcast_sink/CMakeLists.txt b/broadcast_sink/CMakeLists.txt new file mode 100644 index 0000000..5c48ca6 --- /dev/null +++ b/broadcast_sink/CMakeLists.txt @@ -0,0 +1,8 @@ +# +# Copyright (c) 2023 Nordic Semiconductor +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +target_sources(app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/main.c) diff --git a/broadcast_sink/README.rst b/broadcast_sink/README.rst new file mode 100644 index 0000000..46dbce5 --- /dev/null +++ b/broadcast_sink/README.rst @@ -0,0 +1,107 @@ +.. _nrf53_audio_broadcast_sink_app: + +nRF5340 Audio: Broadcast sink +############################# + +.. contents:: + :local: + :depth: 2 + +The nRF5340 Audio broadcast sink application implements the :ref:`BIS headset mode `. +In this mode, receiving broadcast audio happens using Broadcast Isochronous Stream (BIS) and Broadcast Isochronous Group (BIG). + +The following limitations apply to this application: + +* One BIG, one of the two BIS streams (selectable). +* Audio output: I2S/Analog headset output. +* Configuration: 16 bit, several bit rates ranging from 32 kbps to 124 kbps. + +.. _nrf53_audio_broadcast_sink_app_requirements: + +Requirements +************ + +The application shares the :ref:`requirements common to all nRF5340 Audio application `. + +.. _nrf53_audio_broadcast_sink_app_ui: + +User interface +************** + +Most of the user interface mappings are common across all nRF5340 Audio applications. +See the :ref:`nrf53_audio_app_ui` page for detailed overview. + +This application uses specific mapping for the following user interface elements: + +* Long-pressed on the broadcast sink device during startup: + + * **VOL-** - Changes the headset to the left channel one. + * **VOL+** - Changes the headset to the right channel one. + +* Pressed on the broadcast sink device during playback: + + * **PLAY/PAUSE** - Starts or pauses listening to the stream. + * **VOL-** - Turns the playback volume down. + * **VOL+** - Turns the playback volume up. + * **BTN 4** - Changes audio stream (different BIS), if more than one is available. + * **BTN 5** - Changes the gateway, if more than one is available. + +* **LED1**: + + * Solid blue - Devices have synchronized with a broadcasted stream. + * Blinking blue - Devices have started streaming audio (BIS mode). + +* **LED2** - Solid green - Sync achieved (both drift and presentation compensation are in the ``LOCKED`` state). +* **RGB**: + + * Solid blue - The device is programmed as the left headset. + * Solid magenta - The device is programmed as the right headset. + +.. _nrf53_audio_broadcast_sink_app_configuration: + +Configuration +************* + +The application requires the ``CONFIG_TRANSPORT_BIS`` Kconfig option to be set to ``y`` in the :file:`applications/nrf5340_audio/prj.conf` file for `Building and running`_ to succeed. + +For other configuration options, see :ref:`nrf53_audio_app_configuration` and :ref:`nrf53_audio_app_fota`. + +For information about how to configure applications in the |NCS|, see :ref:`configure_application`. + +.. _nrf53_audio_broadcast_sink_app_building: + +Building and running +******************** + +This application can be found under :file:`applications/nrf5340_audio/broadcast_sink` in the nRF Connect SDK folder structure, but it uses :file:`.conf` files at :file:`applications/nrf5340_audio/`. + +The nRF5340 Audio DK comes preprogrammed with basic firmware that indicates if the kit is functional. +See :ref:`nrf53_audio_app_dk_testing_out_of_the_box` for more information. + +To build the application, complete the following steps: + +1. Select the BIS mode by setting the ``CONFIG_TRANSPORT_BIS`` Kconfig option to ``y`` in the :file:`applications/nrf5340_audio/prj.conf` file for the debug version and in the :file:`applications/nrf5340_audio/prj_release.conf` file for the release version. +#. Complete the steps for building and programming common to all audio applications using one of the following methods: + + * :ref:`nrf53_audio_app_building_script` + * :ref:`nrf53_audio_app_building_standard` + +.. _nrf53_audio_broadcast_sink_app_testing: + +Testing +******* + +.. note:: + |nrf5340_audio_external_devices_note| + +To test the broadcast sink application, complete the following steps: + +1. Make sure you have another nRF5340 Audio DK for testing purposes. +#. Program the other DK with the :ref:`broadcast source ` application. + The broadcast sink device automatically synchronizes with the broadcast source after programming. +#. Proceed to testing the devices using the :ref:`nrf53_audio_broadcast_sink_app_ui` buttons and LEDs. + +Dependencies +************ + +For the list of dependencies, check the application's source files under :file:`applications/nrf5340_audio/broadcast_sink`. diff --git a/broadcast_sink/main.c b/broadcast_sink/main.c new file mode 100644 index 0000000..f1d8266 --- /dev/null +++ b/broadcast_sink/main.c @@ -0,0 +1,614 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "streamctrl.h" + +#include +#include + +#include "broadcast_sink.h" +#include "zbus_common.h" +#include "nrf5340_audio_dk.h" +#include "led.h" +#include "button_assignments.h" +#include "macros_common.h" +#include "audio_system.h" +#include "bt_mgmt.h" +#include "bt_rendering_and_capture.h" +#include "audio_datapath.h" +#include "le_audio_rx.h" +#include "fw_info_app.h" + +#include +LOG_MODULE_REGISTER(main, CONFIG_MAIN_LOG_LEVEL); + +struct ble_iso_data { + uint8_t data[CONFIG_BT_ISO_RX_MTU]; + size_t data_size; + bool bad_frame; + uint32_t sdu_ref; + uint32_t recv_frame_ts; +} __packed; + +static uint32_t last_broadcast_id = BRDCAST_ID_NOT_USED; + +ZBUS_SUBSCRIBER_DEFINE(button_evt_sub, CONFIG_BUTTON_MSG_SUB_QUEUE_SIZE); + +ZBUS_MSG_SUBSCRIBER_DEFINE(le_audio_evt_sub); +ZBUS_MSG_SUBSCRIBER_DEFINE(bt_mgmt_evt_sub); + +ZBUS_CHAN_DECLARE(button_chan); +ZBUS_CHAN_DECLARE(le_audio_chan); +ZBUS_CHAN_DECLARE(bt_mgmt_chan); +ZBUS_CHAN_DECLARE(volume_chan); + +ZBUS_OBS_DECLARE(volume_evt_sub); + +static struct k_thread button_msg_sub_thread_data; +static struct k_thread le_audio_msg_sub_thread_data; +static struct k_thread bt_mgmt_msg_sub_thread_data; + +static k_tid_t button_msg_sub_thread_id; +static k_tid_t le_audio_msg_sub_thread_id; +static k_tid_t bt_mgmt_msg_sub_thread_id; + +K_THREAD_STACK_DEFINE(button_msg_sub_thread_stack, CONFIG_BUTTON_MSG_SUB_STACK_SIZE); +K_THREAD_STACK_DEFINE(le_audio_msg_sub_thread_stack, CONFIG_LE_AUDIO_MSG_SUB_STACK_SIZE); +K_THREAD_STACK_DEFINE(bt_mgmt_msg_sub_thread_stack, CONFIG_BT_MGMT_MSG_SUB_STACK_SIZE); + +static enum stream_state strm_state = STATE_PAUSED; + +/* Function for handling all stream state changes */ +static void stream_state_set(enum stream_state stream_state_new) +{ + strm_state = stream_state_new; +} + +/** + * @brief Handle button activity. + */ +static void button_msg_sub_thread(void) +{ + int ret; + const struct zbus_channel *chan; + bool broadcast_alt = true; + + while (1) { + ret = zbus_sub_wait(&button_evt_sub, &chan, K_FOREVER); + ERR_CHK(ret); + + struct button_msg msg; + + ret = zbus_chan_read(chan, &msg, ZBUS_READ_TIMEOUT_MS); + ERR_CHK(ret); + + LOG_DBG("Got btn evt from queue - id = %d, action = %d", msg.button_pin, + msg.button_action); + + if (msg.button_action != BUTTON_PRESS) { + LOG_WRN("Unhandled button action"); + continue; + } + + switch (msg.button_pin) { + case BUTTON_PLAY_PAUSE: + if (strm_state == STATE_STREAMING) { + ret = broadcast_sink_stop(); + if (ret) { + LOG_WRN("Failed to stop broadcast sink: %d", ret); + } + } else if (strm_state == STATE_PAUSED) { + ret = broadcast_sink_start(); + if (ret) { + LOG_WRN("Failed to start broadcast sink: %d", ret); + } + } else { + LOG_WRN("In invalid state: %d", strm_state); + } + + break; + + case BUTTON_VOLUME_UP: + ret = bt_r_and_c_volume_up(); + if (ret) { + LOG_WRN("Failed to increase volume: %d", ret); + } + + break; + + case BUTTON_VOLUME_DOWN: + ret = bt_r_and_c_volume_down(); + if (ret) { + LOG_WRN("Failed to decrease volume: %d", ret); + } + + break; + + case BUTTON_4: + ret = broadcast_sink_change_active_audio_stream(); + if (ret) { + LOG_WRN("Failed to change active audio stream: %d", ret); + } + + break; + + case BUTTON_5: + if (IS_ENABLED(CONFIG_AUDIO_MUTE)) { + ret = bt_r_and_c_volume_mute(false); + if (ret) { + LOG_WRN("Failed to mute, ret: %d", ret); + } + + break; + } + + ret = broadcast_sink_disable(); + if (ret) { + LOG_ERR("Failed to disable the broadcast sink: %d", ret); + break; + } + + if (broadcast_alt) { + ret = bt_mgmt_scan_start(0, 0, BT_MGMT_SCAN_TYPE_BROADCAST, + CONFIG_BT_AUDIO_BROADCAST_NAME_ALT, + BRDCAST_ID_NOT_USED); + broadcast_alt = false; + } else { + ret = bt_mgmt_scan_start(0, 0, BT_MGMT_SCAN_TYPE_BROADCAST, + CONFIG_BT_AUDIO_BROADCAST_NAME, + BRDCAST_ID_NOT_USED); + broadcast_alt = true; + } + + if (ret) { + LOG_WRN("Failed to start scanning for broadcaster: %d", ret); + } + + break; + + default: + LOG_WRN("Unexpected/unhandled button id: %d", msg.button_pin); + } + + STACK_USAGE_PRINT("button_msg_thread", &button_msg_sub_thread_data); + } +} + +/** + * @brief Handle Bluetooth LE audio events. + */ +static void le_audio_msg_sub_thread(void) +{ + int ret; + uint32_t pres_delay_us; + uint32_t bitrate_bps; + uint32_t sampling_rate_hz; + + const struct zbus_channel *chan; + + while (1) { + struct le_audio_msg msg; + + ret = zbus_sub_wait_msg(&le_audio_evt_sub, &chan, &msg, K_FOREVER); + ERR_CHK(ret); + + LOG_DBG("Received event = %d, current state = %d", msg.event, strm_state); + + switch (msg.event) { + case LE_AUDIO_EVT_STREAMING: + LOG_DBG("LE audio evt streaming"); + + if (strm_state == STATE_STREAMING) { + LOG_DBG("Got streaming event in streaming state"); + break; + } + + audio_system_start(); + stream_state_set(STATE_STREAMING); + ret = led_blink(LED_APP_1_BLUE); + ERR_CHK(ret); + + break; + + case LE_AUDIO_EVT_NOT_STREAMING: + LOG_DBG("LE audio evt not_streaming"); + + if (strm_state == STATE_PAUSED) { + LOG_DBG("Got not_streaming event in paused state"); + break; + } + + stream_state_set(STATE_PAUSED); + audio_system_stop(); + ret = led_on(LED_APP_1_BLUE); + ERR_CHK(ret); + + break; + + case LE_AUDIO_EVT_CONFIG_RECEIVED: + LOG_DBG("LE audio config received"); + + ret = broadcast_sink_config_get(&bitrate_bps, &sampling_rate_hz, + &pres_delay_us); + if (ret) { + LOG_WRN("Failed to get config: %d", ret); + break; + } + + LOG_DBG("\tSampling rate: %d Hz", sampling_rate_hz); + LOG_DBG("\tBitrate (compressed): %d bps", bitrate_bps); + + ret = audio_system_config_set(VALUE_NOT_SET, VALUE_NOT_SET, + sampling_rate_hz); + ERR_CHK(ret); + + ret = audio_datapath_pres_delay_us_set(pres_delay_us); + if (ret) { + break; + } + + LOG_INF("Presentation delay %d us is set", pres_delay_us); + + break; + + case LE_AUDIO_EVT_SYNC_LOST: + LOG_INF("Sync lost"); + + ret = bt_mgmt_pa_sync_delete(msg.pa_sync); + if (ret) { + LOG_WRN("Failed to delete PA sync"); + } + + if (strm_state == STATE_STREAMING) { + stream_state_set(STATE_PAUSED); + audio_system_stop(); + ret = led_on(LED_APP_1_BLUE); + ERR_CHK(ret); + } + + if (IS_ENABLED(CONFIG_BT_OBSERVER)) { + ret = bt_mgmt_scan_start(0, 0, BT_MGMT_SCAN_TYPE_BROADCAST, NULL, + last_broadcast_id); + if (ret) { + if (ret == -EALREADY) { + break; + } + + LOG_ERR("Failed to restart scanning: %d", ret); + break; + } + + /* NOTE: The string below is used by the Nordic CI system */ + LOG_INF("Restarted scanning for broadcaster"); + } + + break; + + case LE_AUDIO_EVT_NO_VALID_CFG: + LOG_WRN("No valid configurations found, disabling the broadcast sink"); + + ret = broadcast_sink_disable(); + if (ret) { + LOG_ERR("Failed to disable the broadcast sink: %d", ret); + break; + } + + break; + + case LE_AUDIO_EVT_STREAM_SENT: + /* Nothing to do. */ + break; + + default: + LOG_WRN("Unexpected/unhandled le_audio event: %d", msg.event); + + break; + } + + STACK_USAGE_PRINT("le_audio_msg_thread", &le_audio_msg_sub_thread_data); + } +} + +/** + * @brief Handle bt_mgmt events. + */ +static void bt_mgmt_msg_sub_thread(void) +{ + int ret; + static uint8_t *broadcast_code; + const struct zbus_channel *chan; + + while (1) { + struct bt_mgmt_msg msg; + + ret = zbus_sub_wait_msg(&bt_mgmt_evt_sub, &chan, &msg, K_FOREVER); + ERR_CHK(ret); + + switch (msg.event) { + case BT_MGMT_CONNECTED: + LOG_DBG("Connected"); + break; + + case BT_MGMT_DISCONNECTED: + LOG_DBG("Disconnected"); + break; + + case BT_MGMT_SECURITY_CHANGED: + LOG_DBG("Security changed"); + break; + + case BT_MGMT_PA_SYNCED: + LOG_DBG("PA synced"); + + ret = broadcast_sink_pa_sync_set(msg.pa_sync, msg.broadcast_id); + if (ret) { + last_broadcast_id = BRDCAST_ID_NOT_USED; + LOG_WRN("Failed to set PA sync"); + } else { + last_broadcast_id = msg.broadcast_id; + } + + break; + + case BT_MGMT_PA_SYNC_LOST: + LOG_INF("PA sync lost, reason: %d", msg.pa_sync_term_reason); + + if (IS_ENABLED(CONFIG_BT_OBSERVER) && + msg.pa_sync_term_reason != BT_HCI_ERR_LOCALHOST_TERM_CONN) { + ret = bt_mgmt_scan_start(0, 0, BT_MGMT_SCAN_TYPE_BROADCAST, NULL, + BRDCAST_ID_NOT_USED); + if (ret) { + if (ret == -EALREADY) { + LOG_ERR("Failed to restart scanning: %d", ret); + } + + break; + } + + /* NOTE: The string below is used by the Nordic CI system */ + LOG_INF("Restarted scanning for broadcaster"); + } + + break; + + case BT_MGMT_BROADCAST_SINK_DISABLE: + LOG_DBG("Broadcast sink disabled"); + + ret = broadcast_sink_disable(); + if (ret) { + LOG_ERR("Failed to disable the broadcast sink: %d", ret); + } + + break; + + case BT_MGMT_BROADCAST_CODE_RECEIVED: + LOG_DBG("Broadcast code received"); + + bt_mgmt_broadcast_code_ptr_get(&broadcast_code); + + ret = broadcast_sink_broadcast_code_set(broadcast_code); + if (ret) { + LOG_ERR("Failed to set broadcast code: %d", ret); + } + + break; + + default: + LOG_WRN("Unexpected/unhandled bt_mgmt event: %d", msg.event); + + break; + } + } +} + +/** + * @brief Create zbus subscriber threads. + * + * @return 0 for success, error otherwise. + */ +static int zbus_subscribers_create(void) +{ + int ret; + + button_msg_sub_thread_id = k_thread_create( + &button_msg_sub_thread_data, button_msg_sub_thread_stack, + CONFIG_BUTTON_MSG_SUB_STACK_SIZE, (k_thread_entry_t)button_msg_sub_thread, NULL, + NULL, NULL, K_PRIO_PREEMPT(CONFIG_BUTTON_MSG_SUB_THREAD_PRIO), 0, K_NO_WAIT); + ret = k_thread_name_set(button_msg_sub_thread_id, "BUTTON_MSG_SUB"); + if (ret) { + LOG_ERR("Failed to create button_msg thread"); + return ret; + } + + le_audio_msg_sub_thread_id = k_thread_create( + &le_audio_msg_sub_thread_data, le_audio_msg_sub_thread_stack, + CONFIG_LE_AUDIO_MSG_SUB_STACK_SIZE, (k_thread_entry_t)le_audio_msg_sub_thread, NULL, + NULL, NULL, K_PRIO_PREEMPT(CONFIG_LE_AUDIO_MSG_SUB_THREAD_PRIO), 0, K_NO_WAIT); + ret = k_thread_name_set(le_audio_msg_sub_thread_id, "LE_AUDIO_MSG_SUB"); + if (ret) { + LOG_ERR("Failed to create le_audio_msg thread"); + return ret; + } + + bt_mgmt_msg_sub_thread_id = k_thread_create( + &bt_mgmt_msg_sub_thread_data, bt_mgmt_msg_sub_thread_stack, + CONFIG_BT_MGMT_MSG_SUB_STACK_SIZE, (k_thread_entry_t)bt_mgmt_msg_sub_thread, NULL, + NULL, NULL, K_PRIO_PREEMPT(CONFIG_BT_MGMT_MSG_SUB_THREAD_PRIO), 0, K_NO_WAIT); + ret = k_thread_name_set(bt_mgmt_msg_sub_thread_id, "BT_MGMT_MSG_SUB"); + if (ret) { + LOG_ERR("Failed to create le_audio_msg thread"); + return ret; + } + + return 0; +} + +/** + * @brief Link zbus producers and observers. + * + * @return 0 for success, error otherwise. + */ +static int zbus_link_producers_observers(void) +{ + int ret; + + if (!IS_ENABLED(CONFIG_ZBUS)) { + return -ENOTSUP; + } + + ret = zbus_chan_add_obs(&button_chan, &button_evt_sub, ZBUS_ADD_OBS_TIMEOUT_MS); + if (ret) { + LOG_ERR("Failed to add button sub"); + return ret; + } + + ret = zbus_chan_add_obs(&le_audio_chan, &le_audio_evt_sub, ZBUS_ADD_OBS_TIMEOUT_MS); + if (ret) { + LOG_ERR("Failed to add le_audio sub"); + return ret; + } + + ret = zbus_chan_add_obs(&volume_chan, &volume_evt_sub, ZBUS_ADD_OBS_TIMEOUT_MS); + if (ret) { + LOG_ERR("Failed to add add volume sub"); + return ret; + } + + ret = zbus_chan_add_obs(&bt_mgmt_chan, &bt_mgmt_evt_sub, ZBUS_ADD_OBS_TIMEOUT_MS); + if (ret) { + LOG_ERR("Failed to add bt_mgmt sub"); + return ret; + } + + return 0; +} + +/* + * @brief The following configures the data for the extended advertising. + * + * @param ext_adv_buf Pointer to the bt_data used for extended advertising. + * @param ext_adv_buf_size Size of @p ext_adv_buf. + * @param ext_adv_count Pointer to the number of elements added to @p adv_buf. + * + * @return 0 for success, error otherwise. + */ +static int ext_adv_populate(struct bt_data *ext_adv_buf, size_t ext_adv_buf_size, + size_t *ext_adv_count) +{ + int ret; + size_t ext_adv_buf_cnt = 0; + + NET_BUF_SIMPLE_DEFINE_STATIC(uuid_buf, CONFIG_EXT_ADV_UUID_BUF_MAX); + + ext_adv_buf[ext_adv_buf_cnt].type = BT_DATA_UUID16_ALL; + ext_adv_buf[ext_adv_buf_cnt].data = uuid_buf.data; + ext_adv_buf_cnt++; + + ret = bt_mgmt_manufacturer_uuid_populate(&uuid_buf, CONFIG_BT_DEVICE_MANUFACTURER_ID); + if (ret) { + LOG_ERR("Failed to add adv data with manufacturer ID: %d", ret); + return ret; + } + + ret = broadcast_sink_uuid_populate(&uuid_buf); + if (ret < 0) { + LOG_ERR("Failed to add UUID with broadcast sink: %d", ret); + return ret; + } + + ret = bt_r_and_c_uuid_populate(&uuid_buf); + if (ret) { + LOG_ERR("Failed to add adv data from renderer: %d", ret); + return ret; + } + + ret = broadcast_sink_adv_populate(&ext_adv_buf[ext_adv_buf_cnt], + ext_adv_buf_size - ext_adv_buf_cnt); + + if (ret < 0) { + LOG_ERR("Failed to add adv data from broadcast sink: %d", ret); + return ret; + } + + ext_adv_buf_cnt += ret; + + /* Add the number of UUIDs */ + ext_adv_buf[0].data_len = uuid_buf.len; + + LOG_DBG("Size of adv data: %d, num_elements: %d", sizeof(struct bt_data) * ext_adv_buf_cnt, + ext_adv_buf_cnt); + + *ext_adv_count = ext_adv_buf_cnt; + + return 0; +} + +uint8_t stream_state_get(void) +{ + return strm_state; +} + +void streamctrl_send(void const *const data, size_t size, uint8_t num_ch) +{ + ARG_UNUSED(data); + ARG_UNUSED(size); + ARG_UNUSED(num_ch); + + LOG_WRN("Sending is not possible for broadcast sink"); +} + +int main(void) +{ + int ret; + + LOG_DBG("Main started"); + + ret = nrf5340_audio_dk_init(); + ERR_CHK(ret); + + ret = fw_info_app_print(); + ERR_CHK(ret); + + ret = bt_mgmt_init(); + ERR_CHK(ret); + + ret = audio_system_init(); + ERR_CHK(ret); + + ret = zbus_subscribers_create(); + ERR_CHK_MSG(ret, "Failed to create zbus subscriber threads"); + + ret = zbus_link_producers_observers(); + ERR_CHK_MSG(ret, "Failed to link zbus producers and observers"); + + ret = le_audio_rx_init(); + ERR_CHK_MSG(ret, "Failed to initialize rx path"); + + ret = broadcast_sink_enable(le_audio_rx_data_handler); + ERR_CHK_MSG(ret, "Failed to enable broadcast sink"); + + if (IS_ENABLED(CONFIG_BT_AUDIO_SCAN_DELEGATOR)) { + static struct bt_data ext_adv_buf[CONFIG_EXT_ADV_BUF_MAX]; + size_t ext_adv_buf_cnt = 0; + + bt_mgmt_scan_delegator_init(); + + ret = bt_r_and_c_init(); + ERR_CHK(ret); + + ret = ext_adv_populate(ext_adv_buf, ARRAY_SIZE(ext_adv_buf), &ext_adv_buf_cnt); + ERR_CHK(ret); + + ret = bt_mgmt_adv_start(0, ext_adv_buf, ext_adv_buf_cnt, NULL, 0, true); + ERR_CHK(ret); + } else { + ret = bt_mgmt_scan_start(0, 0, BT_MGMT_SCAN_TYPE_BROADCAST, + CONFIG_BT_AUDIO_BROADCAST_NAME, BRDCAST_ID_NOT_USED); + ERR_CHK_MSG(ret, "Failed to start scanning"); + } + + return 0; +} diff --git a/broadcast_sink/overlay-broadcast_sink.conf b/broadcast_sink/overlay-broadcast_sink.conf new file mode 100644 index 0000000..b6adef1 --- /dev/null +++ b/broadcast_sink/overlay-broadcast_sink.conf @@ -0,0 +1,60 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +CONFIG_TRANSPORT_BIS=y +CONFIG_AUDIO_DEV=1 +CONFIG_BT_DEVICE_NAME="NRF5340_BIS_HEADSET" + +## ACL related configs ## +CONFIG_BT_OBSERVER=y +CONFIG_BT_PERIPHERAL=y +CONFIG_BT_SMP=y +CONFIG_BT_AUDIO=y +CONFIG_BT_GATT_DYNAMIC_DB=y +CONFIG_BT_GATT_CACHING=n + +CONFIG_SETTINGS=y +CONFIG_BT_SETTINGS=y +CONFIG_FLASH=y +CONFIG_FLASH_MAP=y +CONFIG_NVS=y + +CONFIG_MBEDTLS_ENABLE_HEAP=y +CONFIG_MBEDTLS_HEAP_SIZE=2048 + +CONFIG_BT_BUF_ACL_TX_COUNT=18 + +CONFIG_BT_PERIPHERAL_PREF_MIN_INT=64 +CONFIG_BT_PERIPHERAL_PREF_MAX_INT=69 +CONFIG_BT_PERIPHERAL_PREF_LATENCY=0 +CONFIG_BT_PERIPHERAL_PREF_TIMEOUT=200 + +# Generic Audio Sink - 0x0840 +CONFIG_BT_DEVICE_APPEARANCE=2112 +CONFIG_BT_PER_ADV_SYNC_MAX=2 + +## ISO related configs ## +CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT=2 +CONFIG_BT_BAP_BROADCAST_SNK_COUNT=2 +CONFIG_BT_ISO_MAX_CHAN=2 +CONFIG_BT_ISO_MAX_BIG=2 + +## PACS related configs ## +CONFIG_BT_PAC_SNK_NOTIFIABLE=y +CONFIG_BT_PAC_SNK=y +CONFIG_BT_PAC_SRC_NOTIFIABLE=y +CONFIG_BT_PAC_SRC=y + +## Audio related configs ## +CONFIG_AUDIO_MUTE=n +CONFIG_AUDIO_TEST_TONE=n + +CONFIG_BT_ISO_SYNC_RECEIVER=y +CONFIG_BT_BAP_SCAN_DELEGATOR=y +CONFIG_BT_BAP_BROADCAST_SINK=y + +## LC3 related configs ## +CONFIG_LC3_DEC_CHAN_MAX=1 diff --git a/broadcast_source/CMakeLists.txt b/broadcast_source/CMakeLists.txt new file mode 100644 index 0000000..5c48ca6 --- /dev/null +++ b/broadcast_source/CMakeLists.txt @@ -0,0 +1,8 @@ +# +# Copyright (c) 2023 Nordic Semiconductor +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +target_sources(app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/main.c) diff --git a/broadcast_source/Kconfig.defaults b/broadcast_source/Kconfig.defaults new file mode 100644 index 0000000..0792b08 --- /dev/null +++ b/broadcast_source/Kconfig.defaults @@ -0,0 +1,45 @@ +# +# Copyright (c) 2023 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +## ISO related configs ## +config BT_CAP_INITIATOR + default y + +config BT_MAX_CONN + default 1 + +# Broadcasting Device - 0x0885 +config BT_DEVICE_APPEARANCE + default 2181 + +config BT_ISO_BROADCASTER + default y + +config BT_BAP_BROADCAST_SOURCE + default y + +config BT_ISO_TX_BUF_COUNT + default 2 + +config BT_BAP_BROADCAST_SRC_STREAM_COUNT + default 2 + +config BT_ISO_MAX_CHAN + default 2 + +config BT_ISO_MAX_BIG + default 2 + +config BT_AUDIO_TX + default y + + +## LC3 related configs ## +config LC3_BITRATE + default BT_AUDIO_BITRATE_BROADCAST_SRC + +config LC3_ENC_CHAN_MAX + default 2 diff --git a/broadcast_source/README.rst b/broadcast_source/README.rst new file mode 100644 index 0000000..0772de3 --- /dev/null +++ b/broadcast_source/README.rst @@ -0,0 +1,96 @@ +.. _nrf53_audio_broadcast_source_app: + +nRF5340 Audio: Broadcast source +############################### + +.. contents:: + :local: + :depth: 2 + +The nRF5340 Audio broadcast source application implements the :ref:`BIS gateway mode `. + +In this mode, transmitting broadcast audio happens using Broadcast Isochronous Stream (BIS) and Broadcast Isochronous Group (BIG). +Play and pause are emulated by enabling and disabling stream, respectively. + +The following limitations apply to this application: + +* One BIG with two BIS streams. +* Audio input: USB or I2S (Line in or using Pulse Density Modulation). +* Configuration: 16 bit, several bit rates ranging from 32 kbps to 124 kbps. + +.. _nrf53_audio_broadcast_source_app_requirements: + +Requirements +************ + +The application shares the :ref:`requirements common to all nRF5340 Audio application `. + +.. _nrf53_audio_broadcast_source_app_ui: + +User interface +************** + +Most of the user interface mappings are common across all nRF5340 Audio applications. +See the :ref:`nrf53_audio_app_ui` page for detailed overview. + +This application uses specific mapping for the following user interface elements: + +* Pressed on the broadcast source device during playback: + + * **PLAY/PAUSE** - Starts or pauses the playback of the stream. + * **BTN 4** - Toggles between the normal audio stream and different test tones generated on the device. + Use this tone to check the synchronization of headsets. + +* **LED1** - Blinking blue - Device has started broadcasting audio. +* **RGB** - Solid green - The device is programmed as the gateway. + +.. _nrf53_audio_broadcast_source_app_configuration: + +Configuration +************* + +The application requires the ``CONFIG_TRANSPORT_BIS`` Kconfig option to be set to ``y`` in the :file:`applications/nrf5340_audio/prj.conf` file for `Building and running`_ to succeed. + +For other configuration options, see :ref:`nrf53_audio_app_configuration` and :ref:`nrf53_audio_app_fota`. + +For information about how to configure applications in the |NCS|, see :ref:`configure_application`. + +.. _nrf53_audio_broadcast_source_app_building: + +Building and running +******************** + +This application can be found under :file:`applications/nrf5340_audio/broadcast_source` in the nRF Connect SDK folder structure, but it uses :file:`.conf` files at :file:`applications/nrf5340_audio/`. + +The nRF5340 Audio DK comes preprogrammed with basic firmware that indicates if the kit is functional. +See :ref:`nrf53_audio_app_dk_testing_out_of_the_box` for more information. + +To build the application, complete the following steps: + +1. Select the BIS mode by setting the ``CONFIG_TRANSPORT_BIS`` Kconfig option to ``y`` in the :file:`applications/nrf5340_audio/prj.conf` file for the debug version and in the :file:`applications/nrf5340_audio/prj_release.conf` file for the release version. +#. Complete the steps for building and programming common to all audio applications using one of the following methods: + + * :ref:`nrf53_audio_app_building_script` + * :ref:`nrf53_audio_app_building_standard` + +After programming, the broadcast source automatically starts broadcasting the default 48-kHz audio stream. + +.. _nrf53_audio_broadcast_source_app_testing: + +Testing +******* + +.. note:: + |nrf5340_audio_external_devices_note| + +To test the broadcast source application, complete the following steps: + +1. Make sure you have another nRF5340 Audio DK for testing purposes. +#. Program the other DK with the :ref:`broadcast sink ` application. + The broadcast sink device automatically synchronizes with the broadcast source after programming. +#. Proceed to testing the broadcast source using the :ref:`nrf53_audio_broadcast_source_app_ui` buttons and LEDs. + +Dependencies +************ + +For the list of dependencies, check the application's source files. diff --git a/broadcast_source/main.c b/broadcast_source/main.c new file mode 100644 index 0000000..a920257 --- /dev/null +++ b/broadcast_source/main.c @@ -0,0 +1,606 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "streamctrl.h" + +#include +#include +#include +#include + +#include "broadcast_source.h" +#include "zbus_common.h" +#include "nrf5340_audio_dk.h" +#include "led.h" +#include "button_assignments.h" +#include "macros_common.h" +#include "audio_system.h" +#include "bt_mgmt.h" +#include "fw_info_app.h" + +#include +LOG_MODULE_REGISTER(main, CONFIG_MAIN_LOG_LEVEL); + +ZBUS_SUBSCRIBER_DEFINE(button_evt_sub, CONFIG_BUTTON_MSG_SUB_QUEUE_SIZE); + +ZBUS_MSG_SUBSCRIBER_DEFINE(le_audio_evt_sub); + +ZBUS_CHAN_DECLARE(button_chan); +ZBUS_CHAN_DECLARE(le_audio_chan); +ZBUS_CHAN_DECLARE(bt_mgmt_chan); +ZBUS_CHAN_DECLARE(sdu_ref_chan); + +ZBUS_OBS_DECLARE(sdu_ref_msg_listen); + +static struct k_thread button_msg_sub_thread_data; +static struct k_thread le_audio_msg_sub_thread_data; + +static k_tid_t button_msg_sub_thread_id; +static k_tid_t le_audio_msg_sub_thread_id; + +struct bt_le_ext_adv *ext_adv; + +K_THREAD_STACK_DEFINE(button_msg_sub_thread_stack, CONFIG_BUTTON_MSG_SUB_STACK_SIZE); +K_THREAD_STACK_DEFINE(le_audio_msg_sub_thread_stack, CONFIG_LE_AUDIO_MSG_SUB_STACK_SIZE); + +static enum stream_state strm_state = STATE_PAUSED; + +/* Buffer for the UUIDs. */ +#define EXT_ADV_UUID_BUF_SIZE (128) +NET_BUF_SIMPLE_DEFINE_STATIC(uuid_data, EXT_ADV_UUID_BUF_SIZE); +NET_BUF_SIMPLE_DEFINE_STATIC(uuid_data2, EXT_ADV_UUID_BUF_SIZE); + +/* Buffer for periodic advertising BASE data. */ +NET_BUF_SIMPLE_DEFINE_STATIC(base_data, 128); +NET_BUF_SIMPLE_DEFINE_STATIC(base_data2, 128); + +/* Extended advertising buffer. */ +static struct bt_data ext_adv_buf[CONFIG_BT_ISO_MAX_BIG][CONFIG_EXT_ADV_BUF_MAX]; + +/* Periodic advertising buffer. */ +static struct bt_data per_adv_buf[CONFIG_BT_ISO_MAX_BIG]; + +#if (CONFIG_AURACAST) +/* Total size of the PBA buffer includes the 16-bit UUID, 8-bit features and the + * meta data size. + */ +#define BROADCAST_SRC_PBA_BUF_SIZE \ + (BROADCAST_SOURCE_PBA_HEADER_SIZE + CONFIG_BT_AUDIO_BROADCAST_PBA_METADATA_SIZE) + +/* Number of metadata items that can be assigned. */ +#define BROADCAST_SOURCE_PBA_METADATA_VACANT \ + (CONFIG_BT_AUDIO_BROADCAST_PBA_METADATA_SIZE / (sizeof(struct bt_data))) + +/* Make sure pba_buf is large enough for a 16bit UUID and meta data + * (any addition to pba_buf requires an increase of this value) + */ +uint8_t pba_data[CONFIG_BT_ISO_MAX_BIG][BROADCAST_SRC_PBA_BUF_SIZE]; + +/** + * @brief Broadcast source static extended advertising data. + */ +static struct broadcast_source_ext_adv_data ext_adv_data[] = { + {.uuid_buf = &uuid_data, + .pba_metadata_vacant_cnt = BROADCAST_SOURCE_PBA_METADATA_VACANT, + .pba_buf = pba_data[0]}, + {.uuid_buf = &uuid_data2, + .pba_metadata_vacant_cnt = BROADCAST_SOURCE_PBA_METADATA_VACANT, + .pba_buf = pba_data[1]}}; +#else +/** + * @brief Broadcast source static extended advertising data. + */ +static struct broadcast_source_ext_adv_data ext_adv_data[] = {{.uuid_buf = &uuid_data}, + {.uuid_buf = &uuid_data2}}; +#endif /* (CONFIG_AURACAST) */ + +/** + * @brief Broadcast source static periodic advertising data. + */ +static struct broadcast_source_per_adv_data per_adv_data[] = {{.base_buf = &base_data}, + {.base_buf = &base_data2}}; + +/* Function for handling all stream state changes */ +static void stream_state_set(enum stream_state stream_state_new) +{ + strm_state = stream_state_new; +} + +/** + * @brief Handle button activity. + */ +static void button_msg_sub_thread(void) +{ + int ret; + const struct zbus_channel *chan; + + while (1) { + ret = zbus_sub_wait(&button_evt_sub, &chan, K_FOREVER); + ERR_CHK(ret); + + struct button_msg msg; + + ret = zbus_chan_read(chan, &msg, ZBUS_READ_TIMEOUT_MS); + ERR_CHK(ret); + + LOG_DBG("Got btn evt from queue - id = %d, action = %d", msg.button_pin, + msg.button_action); + + if (msg.button_action != BUTTON_PRESS) { + LOG_WRN("Unhandled button action"); + return; + } + + switch (msg.button_pin) { + case BUTTON_PLAY_PAUSE: + if (strm_state == STATE_STREAMING) { + ret = broadcast_source_stop(0); + if (ret) { + LOG_WRN("Failed to stop broadcaster: %d", ret); + } + } else if (strm_state == STATE_PAUSED) { + ret = broadcast_source_start(0, ext_adv); + if (ret) { + LOG_WRN("Failed to start broadcaster: %d", ret); + } + } else { + LOG_WRN("In invalid state: %d", strm_state); + } + + break; + + case BUTTON_4: + if (IS_ENABLED(CONFIG_AUDIO_TEST_TONE)) { + if (strm_state != STATE_STREAMING) { + LOG_WRN("Not in streaming state"); + break; + } + + ret = audio_system_encode_test_tone_step(); + if (ret) { + LOG_WRN("Failed to play test tone, ret: %d", ret); + } + + break; + } + + break; + + default: + LOG_WRN("Unexpected/unhandled button id: %d", msg.button_pin); + } + + STACK_USAGE_PRINT("button_msg_thread", &button_msg_sub_thread_data); + } +} + +/** + * @brief Handle Bluetooth LE audio events. + */ +static void le_audio_msg_sub_thread(void) +{ + int ret; + const struct zbus_channel *chan; + + while (1) { + struct le_audio_msg msg; + + ret = zbus_sub_wait_msg(&le_audio_evt_sub, &chan, &msg, K_FOREVER); + ERR_CHK(ret); + + LOG_DBG("Received event = %d, current state = %d", msg.event, strm_state); + + switch (msg.event) { + case LE_AUDIO_EVT_STREAMING: + LOG_DBG("LE audio evt streaming"); + + audio_system_encoder_start(); + + if (strm_state == STATE_STREAMING) { + LOG_DBG("Got streaming event in streaming state"); + break; + } + + audio_system_start(); + stream_state_set(STATE_STREAMING); + ret = led_blink(LED_APP_1_BLUE); + ERR_CHK(ret); + + break; + + case LE_AUDIO_EVT_NOT_STREAMING: + LOG_DBG("LE audio evt not_streaming"); + + audio_system_encoder_stop(); + + if (strm_state == STATE_PAUSED) { + LOG_DBG("Got not_streaming event in paused state"); + break; + } + + stream_state_set(STATE_PAUSED); + audio_system_stop(); + ret = led_on(LED_APP_1_BLUE); + ERR_CHK(ret); + + break; + + case LE_AUDIO_EVT_STREAM_SENT: + /* Nothing to do. */ + break; + + default: + LOG_WRN("Unexpected/unhandled le_audio event: %d", msg.event); + + break; + } + + STACK_USAGE_PRINT("le_audio_msg_thread", &le_audio_msg_sub_thread_data); + } +} + +/** + * @brief Create zbus subscriber threads. + * + * @return 0 for success, error otherwise. + */ +static int zbus_subscribers_create(void) +{ + int ret; + + button_msg_sub_thread_id = k_thread_create( + &button_msg_sub_thread_data, button_msg_sub_thread_stack, + CONFIG_BUTTON_MSG_SUB_STACK_SIZE, (k_thread_entry_t)button_msg_sub_thread, NULL, + NULL, NULL, K_PRIO_PREEMPT(CONFIG_BUTTON_MSG_SUB_THREAD_PRIO), 0, K_NO_WAIT); + ret = k_thread_name_set(button_msg_sub_thread_id, "BUTTON_MSG_SUB"); + if (ret) { + LOG_ERR("Failed to create button_msg thread"); + return ret; + } + + le_audio_msg_sub_thread_id = k_thread_create( + &le_audio_msg_sub_thread_data, le_audio_msg_sub_thread_stack, + CONFIG_LE_AUDIO_MSG_SUB_STACK_SIZE, (k_thread_entry_t)le_audio_msg_sub_thread, NULL, + NULL, NULL, K_PRIO_PREEMPT(CONFIG_LE_AUDIO_MSG_SUB_THREAD_PRIO), 0, K_NO_WAIT); + ret = k_thread_name_set(le_audio_msg_sub_thread_id, "LE_AUDIO_MSG_SUB"); + if (ret) { + LOG_ERR("Failed to create le_audio_msg thread"); + return ret; + } + + ret = zbus_chan_add_obs(&sdu_ref_chan, &sdu_ref_msg_listen, ZBUS_ADD_OBS_TIMEOUT_MS); + if (ret) { + LOG_ERR("Failed to add timestamp listener"); + return ret; + } + + return 0; +} + +/** + * @brief Zbus listener to receive events from bt_mgmt. + * + * @param[in] chan Zbus channel. + * + * @note Will in most cases be called from BT_RX context, + * so there should not be too much processing done here. + */ +static void bt_mgmt_evt_handler(const struct zbus_channel *chan) +{ + int ret; + const struct bt_mgmt_msg *msg; + + msg = zbus_chan_const_msg(chan); + + switch (msg->event) { + case BT_MGMT_EXT_ADV_WITH_PA_READY: + LOG_INF("Ext adv ready"); + + ext_adv = msg->ext_adv; + + ret = broadcast_source_start(msg->index, ext_adv); + if (ret) { + LOG_ERR("Failed to start broadcaster: %d", ret); + } + + break; + + default: + LOG_WRN("Unexpected/unhandled bt_mgmt event: %d", msg->event); + break; + } +} + +ZBUS_LISTENER_DEFINE(bt_mgmt_evt_listen, bt_mgmt_evt_handler); + +/** + * @brief Link zbus producers and observers. + * + * @return 0 for success, error otherwise. + */ +static int zbus_link_producers_observers(void) +{ + int ret; + + if (!IS_ENABLED(CONFIG_ZBUS)) { + return -ENOTSUP; + } + + ret = zbus_chan_add_obs(&button_chan, &button_evt_sub, ZBUS_ADD_OBS_TIMEOUT_MS); + if (ret) { + LOG_ERR("Failed to add button sub"); + return ret; + } + + ret = zbus_chan_add_obs(&le_audio_chan, &le_audio_evt_sub, ZBUS_ADD_OBS_TIMEOUT_MS); + if (ret) { + LOG_ERR("Failed to add le_audio sub"); + return ret; + } + + ret = zbus_chan_add_obs(&bt_mgmt_chan, &bt_mgmt_evt_listen, ZBUS_ADD_OBS_TIMEOUT_MS); + if (ret) { + LOG_ERR("Failed to add bt_mgmt listener"); + return ret; + } + + return 0; +} + +/* + * @brief The following configures the data for the extended advertising. + * This includes the Broadcast Audio Announcements [BAP 3.7.2.1] and Broadcast_ID + * [BAP 3.7.2.1.1] in the AUX_ADV_IND Extended Announcements. + * + * @param big_index Index of the Broadcast Isochronous Group (BIG) to get + * advertising data for. + * @param ext_adv_data Pointer to the extended advertising buffers. + * @param ext_adv_buf Pointer to the bt_data used for extended advertising. + * @param ext_adv_buf_size Size of @p ext_adv_buf. + * @param ext_adv_count Pointer to the number of elements added to @p adv_buf. + * + * @return 0 for success, error otherwise. + */ +static int ext_adv_populate(uint8_t big_index, struct broadcast_source_ext_adv_data *ext_adv_data, + struct bt_data *ext_adv_buf, size_t ext_adv_buf_size, + size_t *ext_adv_count) +{ + int ret; + size_t ext_adv_buf_cnt = 0; + + if (IS_ENABLED(CONFIG_BT_AUDIO_USE_BROADCAST_NAME_ALT)) { + if (sizeof(CONFIG_BT_AUDIO_BROADCAST_NAME_ALT) > + ARRAY_SIZE(ext_adv_data->brdcst_name_buf)) { + LOG_ERR("CONFIG_BT_AUDIO_BROADCAST_NAME_ALT is too long"); + return -EINVAL; + } + + size_t brdcst_name_size = sizeof(CONFIG_BT_AUDIO_BROADCAST_NAME_ALT) - 1; + + memcpy(ext_adv_data->brdcst_name_buf, CONFIG_BT_AUDIO_BROADCAST_NAME_ALT, + brdcst_name_size); + } else { + if (sizeof(CONFIG_BT_AUDIO_BROADCAST_NAME) > + ARRAY_SIZE(ext_adv_data->brdcst_name_buf)) { + LOG_ERR("CONFIG_BT_AUDIO_BROADCAST_NAME is too long"); + return -EINVAL; + } + + size_t brdcst_name_size = sizeof(CONFIG_BT_AUDIO_BROADCAST_NAME) - 1; + + memcpy(ext_adv_data->brdcst_name_buf, CONFIG_BT_AUDIO_BROADCAST_NAME, + brdcst_name_size); + } + + ext_adv_buf[ext_adv_buf_cnt].type = BT_DATA_UUID16_ALL; + ext_adv_buf[ext_adv_buf_cnt].data = ext_adv_data->uuid_buf->data; + ext_adv_buf_cnt++; + + ret = bt_mgmt_manufacturer_uuid_populate(ext_adv_data->uuid_buf, + CONFIG_BT_DEVICE_MANUFACTURER_ID); + if (ret) { + LOG_ERR("Failed to add adv data with manufacturer ID: %d", ret); + return ret; + } + + bool fixed_id = !IS_ENABLED(CONFIG_BT_AUDIO_USE_BROADCAST_ID_RANDOM); + + uint32_t broadcast_id = CONFIG_BT_AUDIO_BROADCAST_ID_FIXED; + + ret = broadcast_source_ext_adv_populate(big_index, fixed_id, broadcast_id, ext_adv_data, + &ext_adv_buf[ext_adv_buf_cnt], + ext_adv_buf_size - ext_adv_buf_cnt); + if (ret < 0) { + LOG_ERR("Failed to add ext adv data for broadcast source: %d", ret); + return ret; + } + + ext_adv_buf_cnt += ret; + + /* Add the number of UUIDs */ + ext_adv_buf[0].data_len = ext_adv_data->uuid_buf->len; + + LOG_DBG("Size of adv data: %d, num_elements: %d", sizeof(struct bt_data) * ext_adv_buf_cnt, + ext_adv_buf_cnt); + + *ext_adv_count = ext_adv_buf_cnt; + + return 0; +} + +/* + * @brief The following configures the data for the periodic advertising. + * This includes the Basic Audio Announcement, including the + * BASE [BAP 3.7.2.2] and BIGInfo. + * + * @param big_index Index of the Broadcast Isochronous Group (BIG) to get + * advertising data for. + * @param pre_adv_data Pointer to the periodic advertising buffers. + * @param per_adv_buf Pointer to the bt_data used for periodic advertising. + * @param per_adv_buf_size Size of @p ext_adv_buf. + * @param per_adv_count Pointer to the number of elements added to @p adv_buf. + * + * @return 0 for success, error otherwise. + */ +static int per_adv_populate(uint8_t big_index, struct broadcast_source_per_adv_data *pre_adv_data, + struct bt_data *per_adv_buf, size_t per_adv_buf_size, + size_t *per_adv_count) +{ + int ret; + size_t per_adv_buf_cnt = 0; + + ret = broadcast_source_per_adv_populate(big_index, pre_adv_data, per_adv_buf, + per_adv_buf_size - per_adv_buf_cnt); + if (ret < 0) { + LOG_ERR("Failed to add per adv data for broadcast source: %d", ret); + return ret; + } + + per_adv_buf_cnt += ret; + + LOG_DBG("Size of per adv data: %d, num_elements: %d", + sizeof(struct bt_data) * per_adv_buf_cnt, per_adv_buf_cnt); + + *per_adv_count = per_adv_buf_cnt; + + return 0; +} + +uint8_t stream_state_get(void) +{ + return strm_state; +} + +void streamctrl_send(void const *const data, size_t size, uint8_t num_ch) +{ + int ret; + static int prev_ret; + + struct le_audio_encoded_audio enc_audio = {.data = data, .size = size, .num_ch = num_ch}; + + if (strm_state == STATE_STREAMING) { + ret = broadcast_source_send(0, 0, enc_audio); + + if (ret != 0 && ret != prev_ret) { + if (ret == -ECANCELED) { + LOG_WRN("Sending operation cancelled"); + } else { + LOG_WRN("Problem with sending LE audio data, ret: %d", ret); + } + } + + prev_ret = ret; + } +} + +#if CONFIG_CUSTOM_BROADCASTER +/* Example of how to create a custom broadcaster */ +/** + * Remember to increase: + * CONFIG_BT_BAP_BROADCAST_SRC_SUBGROUP_COUNT + * CONFIG_BT_CTLR_ADV_ISO_STREAM_COUNT (set in hci_ipc.conf) + * CONFIG_BT_ISO_TX_BUF_COUNT + * CONFIG_BT_BAP_BROADCAST_SRC_STREAM_COUNT + * CONFIG_BT_ISO_MAX_CHAN + */ +#error Feature is incomplete and should only be used as a guideline for now +static struct bt_bap_lc3_preset lc3_preset_48 = BT_BAP_LC3_BROADCAST_PRESET_48_4_1( + BT_AUDIO_LOCATION_FRONT_LEFT | BT_AUDIO_LOCATION_FRONT_RIGHT, BT_AUDIO_CONTEXT_TYPE_MEDIA); + +static void broadcast_create(struct broadcast_source_big *broadcast_param) +{ + static enum bt_audio_location location[2] = {BT_AUDIO_LOCATION_FRONT_LEFT, + BT_AUDIO_LOCATION_FRONT_RIGHT}; + static struct subgroup_config subgroups[2]; + + subgroups[0].group_lc3_preset = lc3_preset_48; + subgroups[0].num_bises = 2; + subgroups[0].context = BT_AUDIO_CONTEXT_TYPE_MEDIA; + subgroups[0].location = location; + + subgroups[1].group_lc3_preset = lc3_preset_48; + subgroups[1].num_bises = 2; + subgroups[1].context = BT_AUDIO_CONTEXT_TYPE_MEDIA; + subgroups[1].location = location; + + broadcast_param->packing = BT_ISO_PACKING_INTERLEAVED; + + broadcast_param->encryption = false; + + bt_audio_codec_cfg_meta_set_bcast_audio_immediate_rend_flag( + &subgroups[0].group_lc3_preset.codec_cfg); + bt_audio_codec_cfg_meta_set_bcast_audio_immediate_rend_flag( + &subgroups[1].group_lc3_preset.codec_cfg); + + uint8_t spanish_src[3] = "spa"; + uint8_t english_src[3] = "eng"; + + bt_audio_codec_cfg_meta_set_stream_lang(&subgroups[0].group_lc3_preset.codec_cfg, + (uint32_t)sys_get_le24(english_src)); + bt_audio_codec_cfg_meta_set_stream_lang(&subgroups[1].group_lc3_preset.codec_cfg, + (uint32_t)sys_get_le24(spanish_src)); + + broadcast_param->subgroups = subgroups; + broadcast_param->num_subgroups = 2; +} +#endif /* CONFIG_CUSTOM_BROADCASTER */ + +int main(void) +{ + int ret; + static struct broadcast_source_big broadcast_param; + + LOG_DBG("Main started"); + + size_t ext_adv_buf_cnt = 0; + size_t per_adv_buf_cnt = 0; + + ret = nrf5340_audio_dk_init(); + ERR_CHK(ret); + + ret = fw_info_app_print(); + ERR_CHK(ret); + + ret = bt_mgmt_init(); + ERR_CHK(ret); + + ret = audio_system_init(); + ERR_CHK(ret); + + ret = zbus_subscribers_create(); + ERR_CHK_MSG(ret, "Failed to create zbus subscriber threads"); + + ret = zbus_link_producers_observers(); + ERR_CHK_MSG(ret, "Failed to link zbus producers and observers"); + + broadcast_source_default_create(&broadcast_param); + + /* Only one BIG supported at the moment */ + ret = broadcast_source_enable(&broadcast_param, 0); + ERR_CHK_MSG(ret, "Failed to enable broadcaster(s)"); + + ret = audio_system_config_set( + bt_audio_codec_cfg_freq_to_freq_hz(CONFIG_BT_AUDIO_PREF_SAMPLE_RATE_VALUE), + CONFIG_BT_AUDIO_BITRATE_BROADCAST_SRC, VALUE_NOT_SET); + ERR_CHK_MSG(ret, "Failed to set sample- and bitrate"); + + /* Get advertising set for BIG0 */ + ret = ext_adv_populate(0, &ext_adv_data[0], ext_adv_buf[0], ARRAY_SIZE(ext_adv_buf[0]), + &ext_adv_buf_cnt); + ERR_CHK(ret); + + ret = per_adv_populate(0, &per_adv_data[0], &per_adv_buf[0], 1, &per_adv_buf_cnt); + ERR_CHK(ret); + + /* Start broadcaster */ + ret = bt_mgmt_adv_start(0, ext_adv_buf[0], ext_adv_buf_cnt, &per_adv_buf[0], + per_adv_buf_cnt, false); + ERR_CHK_MSG(ret, "Failed to start first advertiser"); + + LOG_INF("Broadcast source: %s started", CONFIG_BT_AUDIO_BROADCAST_NAME); + + return 0; +} diff --git a/broadcast_source/overlay-broadcast_source.conf b/broadcast_source/overlay-broadcast_source.conf new file mode 100644 index 0000000..cb2bf56 --- /dev/null +++ b/broadcast_source/overlay-broadcast_source.conf @@ -0,0 +1,28 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +CONFIG_TRANSPORT_BIS=y +CONFIG_AUDIO_DEV=2 + +CONFIG_BT_CAP_INITIATOR=y +CONFIG_BT_AUDIO=y + +CONFIG_BT_DEVICE_APPEARANCE=2181 + +CONFIG_BT_ISO_BROADCASTER=y + +CONFIG_BT_BAP_BROADCAST_SOURCE=y + +CONFIG_BT_ISO_TX_BUF_COUNT=2 + +CONFIG_BT_BAP_BROADCAST_SRC_STREAM_COUNT=2 + +CONFIG_BT_ISO_MAX_CHAN=2 + +CONFIG_BT_ISO_MAX_BIG=2 + +CONFIG_LC3_ENC_CHAN_MAX=2 +CONFIG_ENTROPY_GENERATOR=y diff --git a/doc/adapting_application.rst b/doc/adapting_application.rst new file mode 100644 index 0000000..a812ee7 --- /dev/null +++ b/doc/adapting_application.rst @@ -0,0 +1,82 @@ +.. _nrf53_audio_app_adapting: + +Adapting nRF5340 Audio applications for end products +#################################################### + +.. contents:: + :local: + :depth: 2 + +This page describes the relevant configuration sources and lists the steps required for adapting the :ref:`nrf53_audio_app` to end products. + +Board configuration sources +*************************** + +The nRF5340 Audio applications use the following files as board configuration sources: + +* Devicetree Specification (DTS) files - These reflect the hardware configuration. + See :ref:`zephyr:dt-guide` for more information about the DTS data structure. +* Kconfig files - These reflect the hardware-related software configuration. + See :ref:`kconfig_tips_and_tricks` for information about how to configure them. +* Memory layout configuration files - These define the memory layout of the application. + +You can see the :file:`zephyr/boards/nordic/nrf5340_audio_dk` directory as an example of how these files are structured. + +For information about differences between DTS and Kconfig, see :ref:`zephyr:dt_vs_kconfig`. +For detailed instructions for adding Zephyr support to a custom board, see Zephyr's :ref:`zephyr:board_porting_guide`. + +.. _nrf53_audio_app_porting_guide_app_configuration: + +Application configuration sources +********************************* + +The application configuration source file defines a set of options used by the given nRF5340 Audio application. +This is a :file:`.conf` file that modifies the default Kconfig values defined in the Kconfig files. + +Only one :file:`.conf` file is included at a time. +The :file:`prj.conf` file is the default configuration file and it implements the debug application version. +For the release application version, you need to include the :file:`prj_release.conf` configuration file. +In the release application version no debug features should be enabled. + +Each nRF5340 Audio application also uses its own :file:`Kconfig.default` file to change configuration defaults automatically. + +You need to edit :file:`prj.conf` and :file:`prj_release.conf` if you want to add new functionalities to your application, but editing these files when adding a new board is not required. + +.. _nrf53_audio_app_porting_guide_adding_board: + +Adding a new board +****************** + +.. note:: + The first three steps of the configuration procedure are identical to the steps described in Zephyr's :ref:`zephyr:board_porting_guide`. + +To use the nRF5340 Audio application with your custom board: + +1. Define the board files for your custom board: + + a. Create a new directory in the :file:`nrf/boards//` directory with the name of the new board. + #. Copy the nRF5340 Audio board files from the :file:`nrf5340_audio_dk` directory located in the :file:`zephyr/boards/nordic/` folder to the newly created directory. + +#. Edit the DTS files to make sure they match the hardware configuration. + Pay attention to the following elements: + + * Pins that are used. + * Interrupt priority that might be different. + +#. Edit the board's Kconfig files to make sure they match the required system configuration. + For example, disable the drivers that will not be used by your device. +#. Build the application by selecting the name of the new board (for example, ``new_audio_board_name``) in your build system. + For example, when building from the command line, add ``-b new_audio_board_name`` to your build command. + +FOTA for end products +********************* + +Do not use the default MCUboot key for end products. +See :ref:`ug_fw_update` and :ref:`west-sign` for more information. + +To create your own app that supports DFU, you can use the `nRF Connect Device Manager`_ libraries for Android and iOS. + +Changing default values +*********************** + +Given the requirements for the Coordinated Set Identification Service (CSIS), make sure to change the Set Identity Resolving Key (SIRK) value when adapting the application. diff --git a/doc/building.rst b/doc/building.rst new file mode 100644 index 0000000..2f30044 --- /dev/null +++ b/doc/building.rst @@ -0,0 +1,327 @@ +.. _nrf53_audio_app_building: + +Building and running nRF5340 Audio applications +############################################### + +.. contents:: + :local: + :depth: 2 + +This nRF5340 Audio application source files can be found in their respective folders under :file:`applications/nrf5340_audio` in the nRF Connect SDK folder structure. + +You can build and program the applications in one of the following ways: + +* :ref:`nrf53_audio_app_building_script` - This is the suggested method. + Using this method allows you to build and program multiple development kits at the same time. +* :ref:`nrf53_audio_app_building_standard` - Using this method requires building and programming each development kit separately. + +.. important:: + Building and programming using the |nRFVSC| is currently not supported. + +.. note:: + You might want to check the :ref:`nRF5340 Audio application known issues ` before building and programming the applications. + +.. _nrf53_audio_app_dk_testing_out_of_the_box: + +Testing out of the box +********************** + +Each development kit comes preprogrammed with basic firmware that indicates if the kit is functional. +Before building the application, you can verify if the kit is working by completing the following steps: + +1. Plug the device into the USB port. +#. Turn on the development kit using the On/Off switch. +#. Observe **RGB** (bottom side LEDs around the center opening that illuminate the Nordic Semiconductor logo) turn solid yellow, **OB/EXT** turn solid green, and **LED3** start blinking green. + +You can now program the development kit. + +.. _nrf53_audio_app_building_script: + +Building and programming using script +************************************* + +The suggested method for building each of the applications and programming it to the development kit is running the :file:`buildprog.py` Python script. +The script automates the process of selecting :ref:`configuration files ` and building different applications. +This eases the process of building and programming images for multiple development kits. + +The script is located in the :file:`applications/nrf5340_audio/tools/buildprog` directory. + + .. note:: + The :file:`buildprog.py` script is an app-specific script for building and programming multiple kits and cores with various audio application configurations. The script will be deprecated in a future release. The audio applications will gradually shift only to using standard tools for building and programming development kits. + +Preparing the JSON file +======================= + +The script depends on the settings defined in the :file:`nrf5340_audio_dk_devices.json` file. +Before using the script, make sure to update this file with the following information for each development kit you want to use. +This is how the file looks by default: + +.. literalinclude:: ../tools/buildprog/nrf5340_audio_dk_devices.json + :language: json + +When preparing the JSON file, update the following fields: + +* ``nrf5340_audio_dk_snr`` - This field lists the SEGGER serial number. + You can check this ten-digit number on the sticker on the nRF5340 Audio development kit. + Alternatively, connect the development kit to your PC and run ``nrfutil device list`` in a command window to print the SEGGER serial number of all connected kits. +* ``nrf5340_audio_dk_dev`` - This field assigns the specific nRF5340 Audio development kit to be ``headset`` or ``gateway``. +* ``channel`` - This field is valid only for headsets. + It sets the channels on which the headset is meant to work. + When no channel is set, the headset is programmed as a left channel one. + +.. _nrf53_audio_app_building_script_running: + +Running the script +================== + +The script handles building and parallel programming of multiple kits. +The following sections explain these two steps separately. + +Script parameters for building +------------------------------ + +After editing the :file:`nrf5340_audio_dk_devices.json` file, run :file:`buildprog.py` to build the firmware for the development kits. +The building command for running the script requires providing the following parameters: + +.. list-table:: Parameters for the script + :header-rows: 1 + + * - Parameter + - Description + - Options + - More information + * - Core type (``-c``) + - Specifies the core type. + - ``app``, ``net``, ``both`` + - :ref:`nrf53_audio_app_overview_architecture` + * - Application version (``-b``) + - Specifies the application version. + - ``release``, ``debug`` + - | :ref:`nrf53_audio_app_configuration_files` + | **Note:** For FOTA DFU, you must use :ref:`nrf53_audio_app_building_standard`. + * - Transport type (``-t``) + - Specifies the transport type. + - ``broadcast``, ``unicast`` + - :ref:`nrf53_audio_app_overview_architecture` + * - Device type (``-d``) + - Specifies the device type. + - ``headset``, ``gateway``, ``both`` + - :ref:`nrf53_audio_app_overview_gateway_headsets` + +For example, the following command builds headset and gateway applications using the script for the application core with the ``debug`` application version: + +.. code-block:: console + + python buildprog.py -c app -b debug -d both -t unicast + +The command can be run from any location, as long as the correct path to :file:`buildprog.py` is given. + +The build files are saved in separate subdirectories in the :file:`applications/nrf5340_audio/tools/build` directory. +The script creates a directory for each transport, device type, core, and version combination. +For example, when running the command above, the script creates the :file:`unicast/gateway/app/debug`, :file:`unicast/gateway/net/debug`, :file:`unicast/headset/app/debug`, :file:`unicast/headset/net/debug` files and directories. + +Script parameters for programming +--------------------------------- + +The script can program the build files as part of the same `python buildprog.py` command used for building. +Use one of the following programming parameters: + +* Programming (``-p`` parameter) - If you run the ``buildprog`` script with this parameter, you can program one or both of the cores after building the files. +* Sequential programming (``-s`` parameter) - If you encounter problems while programming, include this parameter alongside other parameters to program sequentially. + +.. note:: + The development kits are programmed according to the serial numbers set in the JSON file. + Make sure to connect the development kits to your PC using USB and turn them on using the **POWER** switch before you run the script with the programming parameter. + +The command for programming can look as follows: + +.. code-block:: console + + python buildprog.py -c both -b debug -d both -t unicast -p + +This command builds the unicast headset and the gateway applications with ``debug`` version of both the application core binary and the network core binary - and programs each to its respective core. +If you want to rebuild from scratch, you can add the ``--pristine`` parameter to the command (west's ``-p`` for cannot be used for a pristine build with the script). + +.. note:: + If the programming command fails because of a :ref:`readback protection error `, run :file:`buildprog.py` with the ``--recover_on_fail`` or ``-f`` parameter to recover and re-program automatically when programming fails. + For example, using the programming command example above: + + .. code-block:: console + + python buildprog.py -c both -b debug -d both -t unicast -p --recover_on_fail + +Getting help +------------ + +Run ``python buildprog.py -h`` for information about all available script parameters. + +Configuration table overview +---------------------------- + +When running the script command, a table similar to the following one is displayed to provide an overview of the selected options and parameter values: + +.. code-block:: console + + +------------+----------+---------+--------------+---------------------+---------------------+ + | snr | snr conn | device | only reboot | core app programmed | core net programmed | + +------------+----------+---------+--------------+---------------------+---------------------+ + | 1010101010 | True | headset | Not selected | Selected TBD | Not selected | + | 2020202020 | True | gateway | Not selected | Selected TBD | Not selected | + | 3030303030 | True | headset | Not selected | Selected TBD | Not selected | + +------------+----------+---------+--------------+---------------------+---------------------+ + +See the following table for the meaning of each column and the list of possible values: + ++-----------------------+-----------------------------------------------------------------------------------------------------+-------------------------------------------------+ +| Column | Indication | Possible values | ++=======================+=====================================================================================================+=================================================+ +| ``snr`` | Serial number of the device, as provided in the :file:`nrf5340_audio_dk_devices.json` file. | Serial number. | ++-----------------------+-----------------------------------------------------------------------------------------------------+-------------------------------------------------+ +| ``snr conn`` | Whether the device with the provided serial number is connected to the PC with a serial connection. | ``True`` - Connected. | +| | +-------------------------------------------------+ +| | | ``False`` - Not connected. | ++-----------------------+-----------------------------------------------------------------------------------------------------+-------------------------------------------------+ +| ``device`` | Device type, as provided in the :file:`nrf5340_audio_dk_devices.json` file. | ``headset`` - Headset. | +| | +-------------------------------------------------+ +| | | ``gateway`` - Gateway. | ++-----------------------+-----------------------------------------------------------------------------------------------------+-------------------------------------------------+ +| ``only reboot`` | Whether the device is to be only reset and not programmed. | ``Not selected`` - No reset. | +| | This depends on the ``-r`` parameter in the command, which overrides other parameters. +-------------------------------------------------+ +| | | ``Selected TBD`` - Only reset requested. | +| | +-------------------------------------------------+ +| | | ``Done`` - Reset done. | +| | +-------------------------------------------------+ +| | | ``Failed`` - Reset failed. | ++-----------------------+-----------------------------------------------------------------------------------------------------+-------------------------------------------------+ +|``core app programmed``| Whether the application core is to be programmed. | ``Not selected`` - Core will not be programmed. | +| | This depends on the value provided to the ``-c`` parameter (see above). +-------------------------------------------------+ +| | | ``Selected TBD`` - Programming requested. | +| | +-------------------------------------------------+ +| | | ``Done`` - Programming done. | +| | +-------------------------------------------------+ +| | | ``Failed`` - Programming failed. | ++-----------------------+-----------------------------------------------------------------------------------------------------+-------------------------------------------------+ +|``core net programmed``| Whether the network core is to be programmed. | ``Not selected`` - Core will not be programmed. | +| | This depends on the value provided to the ``-c`` parameter (see above). +-------------------------------------------------+ +| | | ``Selected TBD`` - Programming requested. | +| | +-------------------------------------------------+ +| | | ``Done`` - Programming done. | +| | +-------------------------------------------------+ +| | | ``Failed`` - Programming failed. | ++-----------------------+-----------------------------------------------------------------------------------------------------+-------------------------------------------------+ + +.. _nrf53_audio_app_building_standard: + +Building and programming using command line +******************************************* + +You can also build the nRF5340 Audio applications using the standard |NCS| :ref:`build steps ` for the command line. + +.. _nrf53_audio_app_building_config_files: + +Application configuration files +=============================== + +The application uses a :file:`prj.conf` configuration file located in the sample root directory for the default configuration. +It also provides additional files for different custom configurations. +When you build the sample, you can select one of these configurations using the :makevar:`FILE_SUFFIX` variable. + +See :ref:`app_build_file_suffixes` and :ref:`cmake_options` for more information. + +The application supports the following custom configurations: + +.. list-table:: Application custom configurations + :widths: auto + :header-rows: 1 + + * - Configuration + - File name + - FILE_SUFFIX + - Description + * - Debug (default) + - :file:`prj.conf` + - No suffix + - Debug version of the application. Provides full logging capabilities and debug optimizations to ease development. + * - Release + - :file:`prj_release.conf` + - ``release`` + - Release version of the application. Disables logging capabilities and disables development features to create a smaller application binary. + * - FOTA DFU + - :file:`prj_fota.conf` + - ``fota`` + - | Builds the debug version of the application with the features needed to perform DFU over Bluetooth LE, and includes bootloaders so that the applications on both the application core and network core can be updated. + | See :ref:`nrf53_audio_app_fota` for more information. + +.. _nrf53_audio_app_configuration_select_build: + +Building the application +======================== + +Complete the following steps to build the application: + +1. Choose the combination of build flags: + + a. Choose the device type by using one of the following :ref:`CMake options for extra Kconfig fragments `: + + * For unicast headset: ``-DEXTRA_CONF_FILE=".\unicast_server\overlay-unicast_server.conf"`` + * For unicast gateway: ``-DEXTRA_CONF_FILE=".\unicast_client\overlay-unicast_client.conf"`` + * For broadcast headset: ``-DEXTRA_CONF_FILE=".\broadcast_sink\overlay-broadcast_sink.conf"`` + * For broadcast gateway: ``-DEXTRA_CONF_FILE=".\broadcast_source\overlay-broadcast_source.conf"`` + + #. Choose the application version (:ref:`nrf53_audio_app_building_config_files`) by using one of the following options: + + * For the debug version: No build flag needed. + * For the release version: ``-DFILE_SUFFIX=release`` + +#. Build the application using the standard :ref:`build steps ` for the command line. + For example, if you want to build the firmware for the application core as a headset using the ``release`` application version, you can run the following command from the :file:`applications/nrf5340_audio/` directory: + + .. code-block:: console + + west build -b nrf5340_audio_dk/nrf5340/cpuapp --pristine -- -DEXTRA_CONF_FILE=".\unicast_server\overlay-unicast_server.conf" -DFILE_SUFFIX=release + + This command creates the build files for headset device directly in the :file:`build` directory. + What this means is that you cannot create build files for all devices you want to program, because the subsequent commands will overwrite the files in the :file:`build` directory. + + To work around this standard west behavior, you can add the ``-d`` parameter to the ``west`` command to specify a custom build folder for each device. + This way, you can build firmware for headset and gateway to separate directories before programming the development kits. + Alternatively, you can use the :ref:`nrf53_audio_app_building_script`, which handles this automatically. + +Building the application for FOTA +--------------------------------- + +The following command example builds the application for :ref:`nrf53_audio_app_fota`: + +.. code-block:: console + + west build -b nrf5340_audio_dk/nrf5340/cpuapp --pristine -- -DEXTRA_CONF_FILE=".\unicast_server\overlay-unicast_server.conf" -DFILE_SUFFIX=fota + +The command uses ``-DFILE_SUFFIX=fota`` to pick :file:`prj_fota.conf` instead of the default :file:`prj.conf`. +It also uses the ``--pristine`` to clean the existing directory before starting the build process. + +Programming the application +=========================== + +After building the files for the development kit you want to program, follow the :ref:`standard procedure for programming applications ` in the |NCS|. + +When using the default CIS configuration, if you want to use two headset devices, you must also populate the UICR with the desired channel for each headset. +Use the following commands, depending on which headset you want to populate: + +* Left headset (``--value 0``): + + .. code-block:: console + + nrfutil device x-write --address 0x00FF80F4 --value 0 + +* Right headset (``--value 1``): + + .. code-block:: console + + nrfutil device x-write --address 0x00FF80F4 --value 1 + +Select the correct board when prompted with the popup. +Alternatively, you can add the ``--serial-number`` parameter followed by the SEGGER serial number of the correct board at the end of the ``nrfutil device`` command. +You can check the serial numbers of the connected devices with the ``nrfutil device list`` command. + +.. note:: + |usb_known_issues| diff --git a/doc/configuration.rst b/doc/configuration.rst new file mode 100644 index 0000000..0444d56 --- /dev/null +++ b/doc/configuration.rst @@ -0,0 +1,87 @@ +.. _nrf53_audio_app_configuration: + +Configuring the nRF5340 Audio applications +########################################## + +.. contents:: + :local: + :depth: 2 + +|config| + +.. _nrf53_audio_app_configuration_select_bidirectional: + +Selecting the CIS bidirectional communication +********************************************* + +To switch to the bidirectional mode, set the ``CONFIG_STREAM_BIDIRECTIONAL`` Kconfig option to ``y`` in the :file:`applications/nrf5340_audio/prj.conf` file (for the debug version) or in the :file:`applications/nrf5340_audio/prj_release.conf` file (for the release version). + +.. _nrf53_audio_app_configuration_enable_walkie_talkie: + +Enabling the walkie-talkie demo +=============================== + +The walkie-talkie demo uses one or two bidirectional streams from the gateway to one or two headsets. +The PDM microphone is used as input on both the gateway and headset device. +To switch to using the walkie-talkie, set the ``CONFIG_WALKIE_TALKIE_DEMO`` Kconfig option to ``y`` in the :file:`applications/nrf5340_audio/prj.conf` file (for the debug version) or in the :file:`applications/nrf5340_audio/prj_release.conf` file (for the release version). + +Enabling the Auracastâ„¢ (broadcast) mode +======================================= + +If you want to work with `Auracastâ„¢`_ (broadcast) sources and sinks, set the :kconfig:option:`CONFIG_TRANSPORT_BIS` Kconfig option to ``y`` in the :file:`applications/nrf5340_audio/prj.conf` file. + +.. _nrf53_audio_app_configuration_select_bis_two_gateways: + +Enabling the BIS mode with two gateways +*************************************** + +In addition to the standard BIS mode with one gateway, you can also add a second gateway device. +The BIS headsets can then switch between the two gateways and receive audio stream from one of the two gateways. + +To configure the second gateway, add both the ``CONFIG_TRANSPORT_BIS`` and the ``CONFIG_BT_AUDIO_USE_BROADCAST_NAME_ALT`` Kconfig options set to ``y`` to the :file:`applications/nrf5340_audio/prj.conf` file for the debug version and to the :file:`applications/nrf5340_audio/prj_release.conf` file for the release version. +You can provide an alternative name to the second gateway using the ``CONFIG_BT_AUDIO_BROADCAST_NAME_ALT`` or use the default alternative name. + +You build each BIS gateway separately using the normal procedures from :ref:`nrf53_audio_app_building`. +After building the first gateway, configure the required Kconfig options for the second gateway and build the second gateway firmware. +Remember to program the two firmware versions to two separate gateway devices. + +.. _nrf53_audio_app_configuration_select_i2s: + +Selecting the analog jack input using I2S +***************************************** + +In the default configuration, the gateway application uses USB as the audio source. +The :ref:`nrf53_audio_app_building` and the testing steps also refer to using the USB serial connection. + +To switch to using the 3.5 mm jack analog input, set the ``CONFIG_AUDIO_SOURCE_I2S`` Kconfig option to ``y`` in the :file:`applications/nrf5340_audio/prj.conf` file for the debug version and in the :file:`applications/nrf5340_audio/prj_release.conf` file for the release version. + +When testing the application, an additional audio jack cable is required to use I2S. +Use this cable to connect the audio source (PC) to the analog **LINE IN** on the development kit. + +.. _nrf53_audio_app_adding_FEM_support: + +Adding FEM support +****************** + +You can add support for the nRF21540 front-end module (FEM) to the following nRF5340 Audio applications: + +* :ref:`Broadcast source ` +* :ref:`Unicast client ` +* :ref:`Unicast server ` + +The :ref:`broadcast sink application ` does not need FEM support as it only receives data. + +Adding FEM support happens when :ref:`nrf53_audio_app_building`. +You can use one of the following options, depending on how you decide to build the application: + +* If you opt for :ref:`nrf53_audio_app_building_script`, add the ``--nrf21540`` to the script's building command. +* If you opt for :ref:`nrf53_audio_app_building_standard`, add the ``-Dnrf5340_audio_SHIELD=nrf21540ek -Dipc_radio_SHIELD=nrf21540ek`` to the ``west build`` command. + For example: + + .. code-block:: console + + west build -b nrf5340_audio_dk/nrf5340/cpuapp --pristine -- -DEXTRA_CONF_FILE=".\unicast_server\overlay-unicast_server.conf" -Dnrf5340_audio_SHIELD=nrf21540ek -Dipc_radio_SHIELD=nrf21540ek + +To set the TX power output, use the ``CONFIG_BT_CTLR_TX_PWR_ANTENNA`` and ``CONFIG_MPSL_FEM_NRF21540_TX_GAIN_DB`` Kconfig options in :file:`applications/nrf5340_audio/sysbuild/ipc_radio/prj.conf`. + +See :ref:`ug_radio_fem` for more information about FEM in the |NCS|. diff --git a/doc/feature_support.rst b/doc/feature_support.rst new file mode 100644 index 0000000..5c4aeef --- /dev/null +++ b/doc/feature_support.rst @@ -0,0 +1,44 @@ +.. _nrf53_audio_app_dk_legal: +.. _nrf53_audio_feature_support: + +nRF5340 Audio feature support and QDIDs +####################################### + +.. contents:: + :local: + :depth: 2 + +The following table lists features of the nRF5340 Audio application and their respective limitations and maturity level. +For an explanation of the maturity levels, see :ref:`Software maturity levels `. + +.. note:: + Features not listed are not supported. + +.. include:: /releases_and_maturity/software_maturity.rst + :start-after: software_maturity_application_nrf5340audio_table: + :end-before: software_maturity_protocol + +.. _nrf5340_audio_dns_and_qdids: + +nRF5340 Audio DNs and QDIDs +*************************** + +The following DNs (Design Numbers) and QDIDs (Qualified Design IDs) are related to the nRF5340 LE Audio applications: + +nRF5340 DK Bluetooth DNs/QDIDs + See `nRF5340 DK Bluetooth DNs and QDIDs Compatibility Matrix`_ for the DNs/QDIDs for nRF5340 LE Audio applications. + + A full Audio product DN will typically require DNs/QDIDs for Controller component, Host component, Profiles and Services component and LC3 codec component. + The exact DN/QDID numbers depend on the project configuration and the features used in the application. + + .. note:: + * The DNs/QDIDs listed in the Compatibility Matrix might not cover all use cases or combinations. + The full details of what is supported by a DN/QDID can be found in the associated ICS (Implementation Conformance Statement). + + * The Audio applications do not demonstrate the full capabilities of the underlying DNs/QDIDs. + At the same time, the Audio applications may demonstrate features not available in the underlying DNs/QDID. + +.. ncs-include:: lc3/README.rst + :docset: nrfxlib + :start-after: lc3_qdid_start + :end-before: lc3_qdid_end diff --git a/doc/firmware_architecture.rst b/doc/firmware_architecture.rst new file mode 100644 index 0000000..7defcd5 --- /dev/null +++ b/doc/firmware_architecture.rst @@ -0,0 +1,290 @@ +.. _nrf53_audio_app_overview: + +nRF5340 Audio overview and firmware architecture +################################################ + +.. contents:: + :local: + :depth: 2 + +Each nRF5340 Audio application corresponds to one specific LE Audio role: unicast client (gateway), unicast server (headset), broadcast source (gateway), or broadcast sink (headset). + +Likewise, each nRF5340 Audio application is configured for one specific LE Audio mode: the *connected isochronous stream* (CIS, unicast) mode or in the *broadcast isochronous stream* (BIS) mode. +See :ref:`nrf53_audio_app_overview_modes` for more information. + +The applications use the same code base, but use different :file:`main.c` files and include different modules and libraries depending on the configuration. + +You might need to configure and program two applications for testing the interoperability, depending on your use case. +See the testing steps for each of the application for more information. + +.. _nrf53_audio_app_overview_gateway_headsets: + +Gateway and headset roles +************************* + +The gateway is a common term for a base device, such as the unicast client or an `Auracastâ„¢`_ (broadcast) source, often used with USB or analog jack input. +Often, but not always, the gateway is the largest or most stationary device, and is commonly the Bluetooth Central (if applicable). + +The headset is a common term for a receiver device that plays back the audio it gets from the gateway. +Headset devices include earbuds, headphones, speakers, hearing aids, or similar. +They act as a unicast server or a broadcast sink. +With reference to the gateway, the headset is often the smallest and most portable device, and is commonly the Bluetooth Peripheral (if applicable). + +You can :ref:`select gateway or headset build ` when :ref:`nrf53_audio_app_configuration`. + +.. _nrf53_audio_app_overview_modes: + +Application modes +***************** + +Each application works either in the *connected isochronous stream* (CIS) mode or in the *broadcast isochronous stream* (BIS) mode. + +.. figure:: /images/nrf5340_audio_application_topologies.png + :alt: CIS and BIS mode overview + + CIS and BIS mode overview + +Connected Isochronous Stream (CIS) + CIS is a bidirectional communication protocol that allows for sending separate connected audio streams from a source device to one or more receivers. + The gateway can send the audio data using both the left and the right ISO channels at the same time, allowing for stereophonic sound reproduction with synchronized playback. + + This is the mode available for the unicast applications (:ref:`unicast client` and :ref:`unicast server`). + In this mode, you can use the nRF5340 Audio development kit in the role of the gateway, the left headset, or the right headset. + + In the current version of the nRF5340 Audio unicast client, the application offers both unidirectional and bidirectional communication. + In the bidirectional communication, the headset device will send audio from the on-board PDM microphone. + See :ref:`nrf53_audio_app_configuration_select_bidirectional` in the application description for more information. + + You can also enable a walkie-talkie demonstration. + In this demonstration, the gateway device will send audio from the on-board PDM microphone instead of using USB or the line-in. + See :ref:`nrf53_audio_app_configuration_enable_walkie_talkie` in the application description for more information. + +Broadcast Isochronous Stream (BIS) + BIS is a unidirectional communication protocol that allows for broadcasting one or more audio streams from a source device to an unlimited number of receivers that are not connected to the source. + + This is the mode available for the broadcast applications (:ref:`broadcast source` for gateway and :ref:`broadcast sink` for headset). + In this mode, you can use the nRF5340 Audio development kit in the role of the gateway or as one of the headsets. + Use multiple nRF5340 Audio development kits to test BIS having multiple receiving headsets. + + .. note:: + In the BIS mode, you can use any number of nRF5340 Audio development kits as receivers. + +The audio quality for both modes does not change, although the processing time for stereo can be longer. + +.. _nrf53_audio_app_overview_architecture: + +Firmware architecture +********************* + +The following figure illustrates the high-level software layout for the nRF5340 Audio application: + +.. figure:: /images/nrf5340_audio_structure_generic.svg + :alt: nRF5340 Audio high-level design (overview) + + nRF5340 Audio high-level design (overview) + +The network core of the nRF5340 SoC runs the SoftDevice Controller, which is responsible for receiving the audio stream data from hardware layers and forwarding the data to the Bluetooth LE host on the application core. +The controller implements the lower layers of the Bluetooth Low Energy software stack. +See :ref:`ug_ble_controller_softdevice` for more information about the controller, and :ref:`SoftDevice Controller for LE Isochronous Channels ` for information on how it implements ISO channels used by the nRF5340 Audio applications. + +The application core runs both the Bluetooth LE Host from Zephyr and the application layer. +The application layer is composed of a series of modules from different sources. +These modules include the following major ones: + +* Peripheral modules from the |NCS|: + + * I2S + * USB + * SPI + * TWI/I2C + * UART (debug) + * Timer + * LC3 encoder/decoder + +* Application-specific Bluetooth modules for handling the Bluetooth connection: + + * Management - This module handles scanning and advertising, in addition to general initialization, controller configuration, and transfer of DFU images. + * Stream - This module handles the setup and transfer of audio in the Bluetooth LE Audio context. + It includes submodules for CIS (unicast) and BIS (broadcast). + * Renderer - This module handles rendering, such as volume up and down. + * Content Control - This module handles content control, such as play and pause. + +* Application-specific custom modules, including the synchronization module (part of `I2S-based firmware for gateway and headsets`_) - See `Synchronization module overview`_ for more information. + +Since the application architecture is the same for all applications and the code before compilation is shared to a significant degree, the set of modules in use depends on the chosen audio inputs and outputs (USB or analog jack). + +.. note:: + In the current versions of the applications, the bootloader is disabled by default. + Device Firmware Update (DFU) can only be enabled when :ref:`nrf53_audio_app_building_script`. + See :ref:`nrf53_audio_app_configuration_configure_fota` for details. + +.. _nrf53_audio_app_overview_files: + +Source file architecture +======================== + +The following figure illustrates the software layout for the nRF5340 Audio application on the file-by-file level, regardless of the application chosen: + +.. figure:: /images/nrf5340audio_all_packages.svg + :alt: nRF5340 Audio application file-level breakdown + + nRF5340 Audio application file-level breakdown + +Communication between modules is primarily done through Zephyr's :ref:`zephyr:zbus` to make sure that there are as few dependencies as possible. Each of the buses used by the applications has their message structures described in :file:`zbus_common.h`. + +.. _nrf53_audio_app_overview_architecture_usb: + +USB-based firmware for gateway +============================== + +The following figures show an overview of the modules currently included in the firmware of applications that use USB. + +In this firmware design, no synchronization module is used after decoding the incoming frames or before encoding the outgoing ones. +The Bluetooth LE RX FIFO is mainly used to make decoding run in a separate thread. + +Broadcast source USB-based firmware +----------------------------------- + +.. figure:: /images/nrf5340_audio_broadcast_source_USB_structure.svg + :alt: nRF5340 Audio modules for the broadcast source using USB + + nRF5340 Audio modules for the broadcast source using USB + +Unicast client USB-based firmware +--------------------------------- + +.. figure:: /images/nrf5340_audio_unicast_client_USB_structure.svg + :alt: nRF5340 Audio modules for the unicast client using USB + + nRF5340 Audio modules for the unicast client using USB + +.. _nrf53_audio_app_overview_architecture_i2s: + +I2S-based firmware for gateway and headsets +=========================================== + +The following figure shows an overview of the modules currently included in the firmware of applications that use I2S. + +The Bluetooth LE RX FIFO is mainly used to make :file:`audio_datapath.c` (synchronization module) run in a separate thread. + +Broadcast source I2S-based firmware +----------------------------------- + +.. figure:: /images/nrf5340_audio_broadcast_source_I2S_structure.svg + :alt: nRF5340 Audio modules for the broadcast source using I2S + + nRF5340 Audio modules for the broadcast source using I2S + +Broadcast sink I2S-based firmware +--------------------------------- + +.. figure:: /images/nrf5340_audio_broadcast_sink_I2S_structure.svg + :alt: nRF5340 Audio modules for the broadcast sink using I2S + + nRF5340 Audio modules for the broadcast sink using I2S + +Unicast client I2S-based firmware +--------------------------------- + +.. figure:: /images/nrf5340_audio_unicast_client_I2S_structure.svg + :alt: nRF5340 Audio modules for the unicast client using I2S + + nRF5340 Audio modules for the unicast client using I2S + +Unicast server I2S-based firmware +--------------------------------- + +.. figure:: /images/nrf5340_audio_unicast_server_I2S_structure.svg + :alt: nRF5340 Audio modules for the unicast server using I2S + + nRF5340 Audio modules for the unicast server using I2S + +.. _nrf53_audio_app_overview_architecture_sync_module: + +Synchronization module overview +=============================== + +The synchronization module (:file:`audio_datapath.c`) handles audio synchronization. +To synchronize the audio, it executes the following types of adjustments: + +* Presentation compensation +* Drift compensation + +The presentation compensation makes all the headsets play audio at the same time, even if the packets containing the audio frames are not received at the same time on the different headsets. +In practice, it moves the audio data blocks in the FIFO forward or backward a few blocks, adding blocks of *silence* when needed. + +The drift compensation adjusts the frequency of the audio clock to adjust the speed at which the audio is played. +This is required in the CIS mode, where the gateway and headsets must keep the audio playback synchronized to provide True Wireless Stereo (TWS) audio playback. +As such, it provides both larger adjustments at the start and then continuous small adjustments to the audio synchronization. +This compensation method counters any drift caused by the differences in the frequencies of the quartz crystal oscillators used in the development kits. +Development kits use quartz crystal oscillators to generate a stable clock frequency. +However, the frequency of these crystals always slightly differs. +The drift compensation makes the inter-IC sound (I2S) interface on the headsets run as fast as the Bluetooth packets reception. +This prevents I2S overruns or underruns, both in the CIS mode and the BIS mode. + +See the following figure for an overview of the synchronization module. + +.. figure:: /images/nrf5340_audio_structure_sync_module.svg + :alt: nRF5340 Audio synchronization module overview + + nRF5340 Audio synchronization module overview + +Both synchronization methods use the SDU reference timestamps (:c:type:`sdu_ref`) as the reference variable. +If the device is a gateway that is :ref:`using I2S as audio source ` and the stream is unidirectional (gateway to headsets), :c:type:`sdu_ref` is continuously being extracted from the LE Audio Controller Subsystem for nRF53 on the gateway. +The extraction happens inside the :file:`unicast_client.c` and :file:`broadcast_source.c` files' send function. +The :c:type:`sdu_ref` values are then sent to the gateway's synchronization module, and used to do drift compensation. + +.. note:: + Inside the synchronization module (:file:`audio_datapath.c`), all time-related variables end with ``_us`` (for microseconds). + This means that :c:type:`sdu_ref` becomes :c:type:`sdu_ref_us` inside the module. + +As the nRF5340 is a dual-core SoC, and both cores need the same concept of time, each core runs a free-running timer in an infinite loop. +These two timers are reset at the same time, and they run from the same clock source. +This means that they should always show the same values for the same points in time. +The network core of the nRF5340 running the LE controller for nRF53 uses its timer to generate the :c:type:`sdu_ref` timestamp for every audio packet received. +The application core running the nRF5340 Audio application uses its timer to generate :c:type:`cur_time` and :c:type:`frame_start_ts`. + +After the decoding takes place, the audio data is divided into smaller blocks and added to a FIFO. +These blocks are then continuously being fed to I2S, block by block. + +See the following figure for the details of the compensation methods of the synchronization module. + +.. figure:: /images/nrf5340_audio_sync_module_states.svg + :alt: nRF5340 Audio's state machine for compensation mechanisms + + nRF5340 Audio's state machine for compensation mechanisms + +The following external factors can affect the presentation compensation: + +* The drift compensation must be synchronized to the locked state (:c:enumerator:`DRIFT_STATE_LOCKED`) before the presentation compensation can start. + This drift compensation adjusts the frequency of the audio clock, indicating that the audio is being played at the right speed. + When the drift compensation is not in the locked state, the presentation compensation does not leave the init state (:c:enumerator:`PRES_STATE_INIT`). + Also, if the drift compensation loses synchronization, moving out of :c:enumerator:`DRIFT_STATE_LOCKED`, the presentation compensation moves back to :c:enumerator:`PRES_STATE_INIT`. +* When audio is being played, it is expected that a new audio frame is received in each ISO connection interval. + If this does not occur, the headset might have lost its connection with the gateway. + When the connection is restored, the application receives a :c:type:`sdu_ref` not consecutive with the previously received :c:type:`sdu_ref`. + Then the presentation compensation is put into :c:enumerator:`PRES_STATE_WAIT` to ensure that the audio is still in sync. + +.. note:: + When both the drift and presentation compensation are in state *locked* (:c:enumerator:`DRIFT_STATE_LOCKED` and :c:enumerator:`PRES_STATE_LOCKED`), **LED2** lights up. + +Synchronization module flow +--------------------------- + +The received audio data in the I2S-based firmware devices follows the following path: + +1. The SoftDevice Controller running on the network core receives the compressed audio data. +#. The controller, running in the :zephyr:code-sample:`bluetooth_hci_ipc` sample on the nRF5340 SoC network core, sends the audio data to the Zephyr Bluetooth LE host running on the nRF5340 SoC application core. +#. The host sends the data to the stream control module. +#. The data is sent to a FIFO buffer. +#. The data is sent from the FIFO buffer to the :file:`audio_datapath.c` synchronization module. + The :file:`audio_datapath.c` module performs the audio synchronization based on the SDU reference timestamps. + Each package sent from the gateway gets a unique SDU reference timestamp. + These timestamps are generated on the headset Bluetooth LE controller (in the network core). + This enables the creation of True Wireless Stereo (TWS) earbuds where the audio is synchronized in the CIS mode. + It does also keep the speed of the inter-IC sound (I2S) interface synchronized with the sending and receiving speed of Bluetooth packets. +#. The :file:`audio_datapath.c` module sends the compressed audio data to the LC3 audio decoder for decoding. + +#. The audio decoder decodes the data and sends the uncompressed audio data (PCM) back to the :file:`audio_datapath.c` module. +#. The :file:`audio_datapath.c` module continuously feeds the uncompressed audio data to the hardware codec. +#. The hardware codec receives the uncompressed audio data over the inter-IC sound (I2S) interface and performs the digital-to-analog (DAC) conversion to an analog audio signal. diff --git a/doc/fota.rst b/doc/fota.rst new file mode 100644 index 0000000..b03e645 --- /dev/null +++ b/doc/fota.rst @@ -0,0 +1,84 @@ +.. _nrf53_audio_app_fota: + +Configuring and testing FOTA upgrades for nRF5340 Audio applications +#################################################################### + +.. contents:: + :local: + :depth: 2 + +The nRF5340 Audio applications all support FOTA upgrades, and the application implementation is based on the procedure described in :ref:`ug_nrf53_developing_ble_fota`. + +Requirements for FOTA +********************* + +If the application is running on the nRF5340 Audio DK, you need an external flash shield to upgrade both the application and network core at the same time. +See `Requirements for external flash memory DFU`_ in the nRF5340 Audio DK Hardware documentation for more information. + +.. _nrf53_audio_app_configuration_configure_fota: + +Configuring FOTA upgrades +************************* + +The nRF5340 Audio applications can be built with a :ref:`FOTA configuration ` that includes the required features and applications to perform firmware upgrades over Bluetooth LE. + +The FOTA configuration requires that an external flash be available and that the required DTS overlay files use the external flash shield specified in the `Requirements for FOTA`_ above. +With the external flash connected, it is possible to upgrade both the application core and the network core at the same time. + +See :ref:`multi-image DFU ` for more information about the FOTA process on the nRF5340 SoC. + +.. caution:: + Using the single-image upgrade strategy carries risk of the device being in a state where the application core firmware and the network core firmware are no longer compatible, which can result in a bricked device. + For devices where FOTA is the only DFU method available, multi-image upgrades are recommended to ensure compatibility between the cores. + Make sure to evaluate the risks for your device when selecting the FOTA method. + +.. caution:: + The application is provided with a memory partition configuration in :file:`pm_static_fota.yml`, which is required to perform FOTA using an external flash. + The partition configuration is an example that can be changed between versions, and can be incompatible with existing devices. + For this reason, always create a partition configuration that suits your application. + See :ref:`partition_manager` for more information on how to create a partition configuration. + +Updating the SoftDevice +======================= + +Both FOTA upgrade methods support updating the SoftDevice on the network core. +However, the current default build options for the SoftDevice create a binary that is too large to run on the network core together with a bootloader. +To reduce the size of the SoftDevice binary, you can disable unused features in the SoftDevice. +See :ref:`softdevice_controller` documentation for more information. + +Entering the DFU mode +===================== + +The |NCS| uses :ref:`SMP server and mcumgr ` as the DFU backend. +The SMP server service is separated from CIS and BIS services, and is only advertised when the application is in the DFU mode. +To enter the DFU mode, press **BTN 4** on the nRF5340 Audio DK during startup. + +To identify the devices before the DFU takes place, the DFU mode advertising names mention the device type directly. +The names follow the pattern in which the device role is inserted between the device name and the ``_DFU`` suffix. +For example: + +* Gateway: ``NRF5340_AUDIO_GW_DFU`` +* Left Headset: ``NRF5340_AUDIO_HL_DFU`` +* Right Headset: ``NRF5340_AUDIO_HR_DFU`` + +The first part of these names is based on :kconfig:option:`CONFIG_BT_DEVICE_NAME`. + +.. note:: + When performing DFU for the nRF5340 Audio applications, there will be one or more error prints related to opening flash area ID 1. + This is due to restrictions in the DFU system, and the error print is expected. + The DFU process should still complete successfully. + +Building the FOTA configuration +******************************* + +Use the :ref:`nrf53_audio_app_building_standard` procedure to build the nRF5340 Audio applications with the FOTA configuration. +Make sure to provide the :ref:`correct configuration file ` :file:`prj_fota.conf` when running the build command. + +The :ref:`script-based method ` does not support building and programming the FOTA upgrades. + +.. _nrf53_audio_unicast_client_app_testing_steps_fota: + +Testing FOTA upgrades +********************* + +To test FOTA for the nRF5340 Audio application, ensure the application is in the DFU mode, and then follow the testing steps in the FOTA over Bluetooth Low Energy section of :ref:`ug_nrf53_developing_ble_fota` (you can skip the configuration steps). diff --git a/doc/requirements.rst b/doc/requirements.rst new file mode 100644 index 0000000..00f79e4 --- /dev/null +++ b/doc/requirements.rst @@ -0,0 +1,67 @@ +.. _nrf53_audio_app_requirements: + +nRF5340 Audio application requirements +###################################### + +.. contents:: + :local: + :depth: 2 + +The nRF5340 Audio applications are designed to be used only with the following hardware: + +.. table-from-rows:: /includes/sample_board_rows.txt + :header: heading + :rows: nrf5340_audio_dk_nrf5340 + +.. note:: + The applications supports PCA10121 revisions 1.0.0 or above. + The applications are also compatible with the following pre-launch revisions: + + * Revisions 0.8.0 and above. + +You need at least two nRF5340 Audio development kits (one with the gateway firmware and one with headset firmware) to test each of the applications. +For CIS with TWS in mind, three kits are required. + +If you want to test with other hardware (for example, a mobile phone or PC), it is highly recommended to test with Audio DKs on both the gateway and headset side first to verify basic functionality before moving on to testing with other vendors. + +.. _nrf53_audio_app_requirements_codec: + +Software codec requirements +*************************** + +The nRF5340 Audio applications only support the :ref:`LC3 software codec `, developed specifically for use with LE Audio. + +The applications can be configured for other alternative codecs, but this integration is beyond the scope of this documentation. + +.. _nrf53_audio_app_dk: +.. _nrf53_audio_app_dk_features: + +nRF5340 Audio development kit +***************************** + +The nRF5340 Audio development kit is a hardware development platform that demonstrates the nRF5340 Audio applications. +Read the `nRF5340 Audio DK Hardware`_ documentation for more information about this development kit. + +You can :ref:`test the DK out of the box ` before you program it. + +.. _nrf53_audio_app_configuration_files: + +nRF5340 Audio configuration files +********************************* + +All applications use the :file:`Kconfig.defaults` located in the :file:`nrf5340_audio` directory. +Additionally, each nRF5340 Audio application uses its own, application-specific :file:`Kconfig.defaults` file from the application directory, which includes configuration specific to the given application. +These files change the configuration defaults automatically, based on the different application versions and device types. + +For each application, only one of the following :file:`.conf` files is included when building: + +* :file:`prj.conf` is the default configuration file and it implements the debug application version. +* :file:`prj_release.conf` is the optional configuration file and it implements the release application version. + No debug features are enabled in the release application version. + When building using the command line, you must explicitly specify if :file:`prj_release.conf` is going to be included instead of :file:`prj.conf`. + See :ref:`nrf53_audio_app_building` for details. +* :file:`prj_fota.conf` is the optional configuration file used for FOTA DFU. + When used, the build system builds the debug version of the application (:file:`prj.conf`), but with the features needed to perform DFU over Bluetooth LE. + It also includes bootloaders so that the applications on both the application core and network core can be updated. + When building using the command line, you must explicitly specify if :file:`prj_fota.conf` is going to be included instead of :file:`prj.conf`. + See :ref:`nrf53_audio_app_fota` for more information. diff --git a/doc/user_interface.rst b/doc/user_interface.rst new file mode 100644 index 0000000..387d049 --- /dev/null +++ b/doc/user_interface.rst @@ -0,0 +1,160 @@ +.. _nrf53_audio_app_ui: + +User interface +############## + +.. contents:: + :local: + :depth: 2 + +All nRF5340 Audio applications implement the same, simple user interface based on the available PCB elements of the nRF5340 Audio development kit. +You can control the application using predefined switches and buttons while the LEDs display information. + +Some user interface options are only valid for some nRF5340 Audio applications. + +.. _nrf53_audio_app_ui_switches: + +Switches +******** + +The application uses the following switches on the supported development kit: + ++-------------------+-------------------------------------------------------------------------------------+---------------------------------------+ +| Switch | Function | Applications | ++===================+=====================================================================================+=======================================+ +| **POWER** | Turns the development kit on or off. | All | ++-------------------+-------------------------------------------------------------------------------------+---------------------------------------+ +| **DEBUG ENABLE** | Turns on or off power for debug features. | All | +| | This switch is used for accurate power and current measurements. | | ++-------------------+-------------------------------------------------------------------------------------+---------------------------------------+ + +.. _nrf53_audio_app_ui_buttons: + +Buttons +******* + +The application uses the following buttons on the supported development kit: + ++---------------+-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ +| Button | Function | Applications | ++===============+===========================================================================================================+=============================================+ +| **VOL-** | Long-pressed during startup: Changes the headset to the left channel one. | * :ref:`nrf53_audio_broadcast_sink_app` | +| | | * :ref:`nrf53_audio_unicast_server_app` | +| +-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ +| | Pressed on the headset or the CIS gateway during playback: Turns the playback volume down. | * :ref:`nrf53_audio_broadcast_sink_app` | +| | | * :ref:`nrf53_audio_unicast_server_app` | +| | | * :ref:`nrf53_audio_unicast_client_app` | ++---------------+-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ +| **VOL+** | Long-pressed during startup: Changes the headset to the right channel one. | * :ref:`nrf53_audio_broadcast_sink_app` | +| | | * :ref:`nrf53_audio_unicast_server_app` | +| +-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ +| | Pressed on the headset or the CIS gateway during playback: Turns the playback volume up. | * :ref:`nrf53_audio_broadcast_sink_app` | +| | | * :ref:`nrf53_audio_unicast_server_app` | +| | | * :ref:`nrf53_audio_unicast_client_app` | ++---------------+-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ +| **PLAY/PAUSE**| Starts or pauses the playback of the stream or listening to the stream. | All | ++---------------+-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ +| **BTN 4** | Long-pressed during startup: Turns on the DFU mode, if | All | +| | the device is :ref:`configured for it`. | | +| +-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ +| | Pressed on the gateway during playback: Toggles between the normal audio stream and different test | * :ref:`nrf53_audio_broadcast_source_app` | +| | tones generated on the device. Use this tone to check the synchronization of headsets. | * :ref:`nrf53_audio_unicast_client_app` | +| +-----------------------------------------------------------------------------------------------------------+ | +| | Pressed on the gateway during playback multiple times: Changes the test tone frequency. | | +| | The available values are 1000 Hz, 2000 Hz, and 4000 Hz. | | +| +-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ +| | Pressed on a BIS headset during playback: Change stream (different BIS), if more than one is available. | :ref:`nrf53_audio_broadcast_sink_app` | ++---------------+-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ +| **BTN 5** | Long-pressed during startup: Clears the previously stored bonding information. | * :ref:`nrf53_audio_unicast_server_app` | +| | | * :ref:`nrf53_audio_unicast_client_app` | +| +-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ +| | Pressed during playback: Mutes the playback volume. | * :ref:`nrf53_audio_unicast_server_app` | +| | | * :ref:`nrf53_audio_unicast_client_app` | +| +-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ +| | Pressed on a BIS headset during playback: Change the gateway, if more than one is available. | :ref:`nrf53_audio_broadcast_sink_app` | ++---------------+-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ +| **RESET** | Resets the device to the originally programmed settings. | All | +| | This reverts any changes made during testing, for example the channel switches with **VOL** buttons. | | ++---------------+-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ + +.. _nrf53_audio_app_ui_leds: + +LEDs +**** + +To indicate the tasks performed, the application uses the LED behavior described in the following table: + ++--------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ +| LED |Indication | Applications | ++==========================+===========================================================================================================+=============================================+ +| **LED1** | Off - No Bluetooth connection. | All | +| +-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ +| | Solid blue on the CIS gateway and headset: Kits have connected. | * :ref:`nrf53_audio_unicast_server_app` | +| | | * :ref:`nrf53_audio_unicast_client_app` | +| +-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ +| | Solid blue on the BIS headset: Kits have found a broadcasting stream. | :ref:`nrf53_audio_broadcast_sink_app` | +| +-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ +| | Blinking blue on headset: Kits have started streaming audio (BIS and CIS modes). | * :ref:`nrf53_audio_broadcast_sink_app` | +| | | * :ref:`nrf53_audio_unicast_server_app` | +| +-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ +| | Blinking blue on the CIS gateway: Kit is streaming to a headset. | :ref:`nrf53_audio_unicast_client_app` | +| +-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ +| | Blinking blue on the BIS gateway: Kit has started broadcasting audio. | :ref:`nrf53_audio_broadcast_source_app` | ++--------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ +| **LED2** | Off - Sync not achieved. | All | +| +-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ +| | Solid green - Sync achieved (both drift and presentation compensation are in the ``LOCKED`` state). | * :ref:`nrf53_audio_broadcast_sink_app` | +| | | * :ref:`nrf53_audio_unicast_server_app` | ++--------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ +| **LED3** | Blinking green - The nRF5340 Audio DK application core is running. | All | ++--------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ +| **CODEC** | Off - No configuration loaded to the onboard hardware codec. | All | +| +-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ +| | Solid green - Hardware codec configuration loaded. | All | ++--------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ +| **RGB** | Solid green - The device is programmed as the gateway. | * :ref:`nrf53_audio_broadcast_source_app` | +| | | * :ref:`nrf53_audio_unicast_client_app` | +| (bottom side LEDs around +-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ +| the center opening) | Solid blue - The device is programmed as the left headset. | * :ref:`nrf53_audio_broadcast_sink_app` | +| | | * :ref:`nrf53_audio_unicast_server_app` | +| +-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ +| | Solid magenta - The device is programmed as the right headset. | * :ref:`nrf53_audio_broadcast_sink_app` | +| | | * :ref:`nrf53_audio_unicast_server_app` | +| +-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ +| | Solid yellow - The device is programmed with factory firmware. | All | +| | It must be re-programmed as gateway or headset. | | +| +-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ +| | Solid red (debug mode) - Fault in the application core has occurred. | All | +| | See UART log for details and use the **RESET** button to reset the device. | | +| | In the release mode, the device resets automatically with no indication on LED or UART. | | ++--------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ +| **ERR** | PMIC error or a charging error (or both). | All | +| | Also turns on when charging the battery exceeds seven hours, since the PMIC has a protection timeout, | | +| | which stops the charging. | | ++--------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ +| **CHG** | Off - Charge completed or no battery connected. | All | +| +-----------------------------------------------------------------------------------------------------------+ | +| | Solid yellow - Charging in progress. | | ++--------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ +| **OB/EXT** | Off - No 3.3 V power available. | All | +| +-----------------------------------------------------------------------------------------------------------+ | +| | Solid green - On-board hardware codec selected. | | +| +-----------------------------------------------------------------------------------------------------------+ | +| | Solid yellow - External hardware codec selected. | | +| | This LED turns solid yellow also when the devices are reset, for the time then pins are floating. | | ++--------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ +| **FTDI SPI** | Off - No data is written to the hardware codec using SPI. | All | +| +-----------------------------------------------------------------------------------------------------------+ | +| | Yellow - The same SPI is used for both the hardware codec and the SD card. | | +| | When this LED is yellow, the shared SPI is used by the FTDI to write data to the hardware codec. | | ++--------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ +| **IFMCU** | Off - No PC connection available. | All | +| (bottom side) +-----------------------------------------------------------------------------------------------------------+ | +| | Solid green - Connected to PC. | | +| +-----------------------------------------------------------------------------------------------------------+ | +| | Rapid green flash - USB enumeration failed. | | ++--------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ +| **HUB** | Off - No PC connection available. | All | +| (bottom side) +-----------------------------------------------------------------------------------------------------------+ | +| | Green - Standard USB hub operation. | | ++--------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------------------------+ diff --git a/include/zbus_common.h b/include/zbus_common.h new file mode 100644 index 0000000..a82eded --- /dev/null +++ b/include/zbus_common.h @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _ZBUS_COMMON_H_ +#define _ZBUS_COMMON_H_ + +#include + +#include "le_audio.h" + +#define ZBUS_READ_TIMEOUT_MS K_MSEC(100) +#define ZBUS_ADD_OBS_TIMEOUT_MS K_MSEC(200) + +/***** Messages for zbus ******/ + +enum button_action { + BUTTON_PRESS = 1, +}; + +struct button_msg { + uint32_t button_pin; + enum button_action button_action; +}; + +enum le_audio_evt_type { + LE_AUDIO_EVT_CONFIG_RECEIVED = 1, + LE_AUDIO_EVT_PRES_DELAY_SET, + LE_AUDIO_EVT_STREAMING, + LE_AUDIO_EVT_NOT_STREAMING, + LE_AUDIO_EVT_STREAM_SENT, + LE_AUDIO_EVT_SYNC_LOST, + LE_AUDIO_EVT_NO_VALID_CFG, + LE_AUDIO_EVT_COORD_SET_DISCOVERED, +}; + +struct le_audio_msg { + enum le_audio_evt_type event; + struct bt_conn *conn; + struct bt_le_per_adv_sync *pa_sync; + enum bt_audio_dir dir; + uint8_t set_size; + uint8_t const *sirk; + struct stream_index idx; +}; + +/** + * tx_sync_ts_us The timestamp from get_tx_sync. + * curr_ts_us The current time. This must be in the controller frame of reference. + */ +struct sdu_ref_msg { + uint32_t tx_sync_ts_us; + uint32_t curr_ts_us; + bool adjust; +}; + +enum bt_mgmt_evt_type { + BT_MGMT_EXT_ADV_WITH_PA_READY = 1, + BT_MGMT_CONNECTED, + BT_MGMT_SECURITY_CHANGED, + BT_MGMT_PA_SYNCED, + BT_MGMT_PA_SYNC_LOST, + BT_MGMT_DISCONNECTED, + BT_MGMT_BROADCAST_SINK_DISABLE, + BT_MGMT_BROADCAST_CODE_RECEIVED, +}; + +struct bt_mgmt_msg { + enum bt_mgmt_evt_type event; + struct bt_conn *conn; + uint8_t index; + struct bt_le_ext_adv *ext_adv; + struct bt_le_per_adv_sync *pa_sync; + uint32_t broadcast_id; + uint8_t pa_sync_term_reason; +}; + +enum volume_evt_type { + VOLUME_UP = 1, + VOLUME_DOWN, + VOLUME_SET, + VOLUME_MUTE, + VOLUME_UNMUTE, +}; + +struct volume_msg { + enum volume_evt_type event; + uint8_t volume; +}; + +enum content_control_evt_type { + MEDIA_START = 1, + MEDIA_STOP, +}; + +struct content_control_msg { + enum content_control_evt_type event; +}; + +#endif /* _ZBUS_COMMON_H_ */ diff --git a/index.rst b/index.rst new file mode 100644 index 0000000..a122100 --- /dev/null +++ b/index.rst @@ -0,0 +1,56 @@ +.. _nrf53_audio_app: + +nRF5340 Audio applications +########################## + +The nRF5340 Audio applications demonstrate audio playback over isochronous channels (ISO) using LC3 codec compression and decompression, as per `Bluetooth® LE Audio specifications`_. + +.. note:: + nRF5340 Audio applications and their DFU/FOTA functionality are marked as :ref:`experimental `. + +The following table summarizes the differences between the available nRF5340 Audio applications. + +.. list-table:: Differences between nRF5340 Audio applications + :header-rows: 1 + + * - :ref:`Application name (LE Audio role) ` + - :ref:`Application mode ` + - Minimum amount of nRF5340 Audio DKs recommended for testing + - :ref:`FEM support ` + * - :ref:`Broadcast sink` + - BIS (headset) + - 2 + - + * - :ref:`Broadcast source` + - BIS (gateway) + - 2 + - ✔ + * - :ref:`Unicast client` + - CIS (gateway) + - 3 + - ✔ + * - :ref:`Unicast server` + - CIS (headset) + - 3 + - ✔ + +See the subpages for detailed documentation of each of the nRF5340 applications and their internal modules: + +.. _nrf53_audio_app_subpages: + +.. toctree:: + :maxdepth: 1 + :caption: Subpages: + + doc/firmware_architecture + doc/feature_support + doc/requirements + doc/user_interface + doc/configuration + doc/building + broadcast_sink/README + broadcast_source/README + unicast_client/README + unicast_server/README + doc/fota + doc/adapting_application diff --git a/pm_static_fota.yml b/pm_static_fota.yml new file mode 100644 index 0000000..7a48d51 --- /dev/null +++ b/pm_static_fota.yml @@ -0,0 +1,55 @@ +app: + address: 0x10200 + region: flash_primary + size: 0xdfe00 +mcuboot: + address: 0x0 + region: flash_primary + size: 0x10000 +mcuboot_pad: + address: 0x10000 + region: flash_primary + size: 0x200 +mcuboot_primary: + address: 0x10000 + orig_span: &id001 + - mcuboot_pad + - app + region: flash_primary + size: 0xe0000 + span: *id001 +mcuboot_primary_app: + address: 0x10200 + orig_span: &id002 + - app + region: flash_primary + size: 0xdfe00 + span: *id002 +settings_storage: + address: 0xf0000 + region: flash_primary + size: 0x10000 +mcuboot_primary_1: + address: 0x0 + size: 0x40000 + device: flash_ctrl + region: ram_flash +mcuboot_secondary: + address: 0x00000 + size: 0xe0000 + device: MX25R64 + region: external_flash +mcuboot_secondary_1: + address: 0xe0000 + size: 0x40000 + device: MX25R64 + region: external_flash +external_flash: + address: 0x120000 + size: 0x6e0000 + device: MX25R64 + region: external_flash +pcd_sram: + address: 0x20000000 + size: 0x2000 + region: sram_primary diff --git a/prj.conf b/prj.conf new file mode 100644 index 0000000..0b3b57f --- /dev/null +++ b/prj.conf @@ -0,0 +1,72 @@ +# +# Copyright (c) 2022 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +# nRF5340 Audio +CONFIG_NRF5340_AUDIO=y +CONFIG_SAMPLE_RATE_CONVERTER=y +CONFIG_SAMPLE_RATE_CONVERTER_FILTER_SIMPLE=y + +# General +CONFIG_DEBUG=y +CONFIG_DEBUG_INFO=y +CONFIG_ASSERT=y +CONFIG_STACK_USAGE=y +CONFIG_THREAD_RUNTIME_STATS=y +CONFIG_STACK_SENTINEL=y +CONFIG_INIT_STACKS=y +CONFIG_BT=y + +# Uart driver +CONFIG_SERIAL=y + +# Logging +CONFIG_LOG=y +CONFIG_NEWLIB_LIBC_FLOAT_PRINTF=y +CONFIG_LOG_TAG_MAX_LEN=2 +CONFIG_LOG_TAG_DEFAULT="--" +CONFIG_LOG_BACKEND_UART=y +CONFIG_LOG_BUFFER_SIZE=4096 + +# Use this for debugging thread usage +#CONFIG_LOG_THREAD_ID_PREFIX=y + +# Console related defines +CONFIG_CONSOLE=y +CONFIG_RTT_CONSOLE=y +CONFIG_UART_CONSOLE=y + +# Shell related defines +CONFIG_SHELL=y +CONFIG_KERNEL_SHELL=y +CONFIG_USE_SEGGER_RTT=y +## Disable logs on RTT +CONFIG_SHELL_RTT_INIT_LOG_LEVEL_NONE=y +CONFIG_SHELL_BACKEND_RTT=y +CONFIG_SHELL_BACKEND_SERIAL=n +CONFIG_SHELL_VT100_COMMANDS=y +CONFIG_SHELL_VT100_COLORS=y +CONFIG_SHELL_STACK_SIZE=4096 +CONFIG_SHELL_CMD_BUFF_SIZE=128 +## Reduce shell memory usage +CONFIG_SHELL_WILDCARD=n +CONFIG_SHELL_HELP_ON_WRONG_ARGUMENT_COUNT=n +CONFIG_SHELL_STATS=n +CONFIG_SHELL_CMDS=n +CONFIG_SHELL_HISTORY=y + +# Turn off default shell commands +CONFIG_I2C_SHELL=n +CONFIG_HWINFO_SHELL=n +CONFIG_CLOCK_CONTROL_NRF_SHELL=n +CONFIG_FLASH_SHELL=n +CONFIG_DEVICE_SHELL=n + +# Suppress LOG_ERR messages from sd_check_card_type. Because SPI_SDHC has no card presence method, +# assume card is in slot. Thus error message is always shown if card is not inserted +CONFIG_SD_LOG_LEVEL_OFF=y + +# Suppress LOG_INF messages from hci_core +CONFIG_BT_HCI_CORE_LOG_LEVEL_WRN=y diff --git a/prj_fota.conf b/prj_fota.conf new file mode 100644 index 0000000..b696d20 --- /dev/null +++ b/prj_fota.conf @@ -0,0 +1,84 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +# nRF5340 Audio +CONFIG_NRF5340_AUDIO=y + +CONFIG_SAMPLE_RATE_CONVERTER=y +CONFIG_SAMPLE_RATE_CONVERTER_FILTER_SIMPLE=y + +# General +CONFIG_DEBUG=y +CONFIG_DEBUG_INFO=y +CONFIG_ASSERT=y +CONFIG_STACK_USAGE=y +CONFIG_THREAD_RUNTIME_STATS=y +CONFIG_STACK_SENTINEL=y +CONFIG_INIT_STACKS=y + +# Uart driver +CONFIG_SERIAL=y + +# Logging +CONFIG_LOG=y +CONFIG_NEWLIB_LIBC_FLOAT_PRINTF=y +CONFIG_LOG_TAG_MAX_LEN=2 +CONFIG_LOG_TAG_DEFAULT="--" +CONFIG_LOG_BACKEND_UART=y +CONFIG_LOG_BUFFER_SIZE=4096 + +# Use this for debugging thread usage +#CONFIG_LOG_THREAD_ID_PREFIX=y + +# Console related defines +CONFIG_CONSOLE=y +CONFIG_RTT_CONSOLE=y +CONFIG_UART_CONSOLE=y + +# Shell related defines +CONFIG_SHELL=y +CONFIG_KERNEL_SHELL=y +CONFIG_USE_SEGGER_RTT=y +## Disable logs on RTT +CONFIG_SHELL_RTT_INIT_LOG_LEVEL_NONE=y +CONFIG_SHELL_BACKEND_RTT=y +CONFIG_SHELL_BACKEND_SERIAL=n +CONFIG_SHELL_VT100_COMMANDS=y +CONFIG_SHELL_VT100_COLORS=y +CONFIG_SHELL_STACK_SIZE=4096 +CONFIG_SHELL_CMD_BUFF_SIZE=128 +## Reduce shell memory usage +CONFIG_SHELL_WILDCARD=n +CONFIG_SHELL_HELP_ON_WRONG_ARGUMENT_COUNT=n +CONFIG_SHELL_STATS=n +CONFIG_SHELL_CMDS=n +CONFIG_SHELL_HISTORY=y + +# Turn off default shell commands +CONFIG_I2C_SHELL=n +CONFIG_HWINFO_SHELL=n +CONFIG_CLOCK_CONTROL_NRF_SHELL=n +CONFIG_FLASH_SHELL=n +CONFIG_DEVICE_SHELL=n + +# Suppress LOG_ERR messages from sd_check_card_type. Because SPI_SDHC has no card presence method, +# assume card is in slot. Thus error message is always shown if card is not inserted +CONFIG_SD_LOG_LEVEL_OFF=y + +# Suppress LOG_INF messages from hci_core +CONFIG_BT_HCI_CORE_LOG_LEVEL_WRN=y + +# DFU +CONFIG_AUDIO_BT_MGMT_DFU=y +CONFIG_MCUMGR_TRANSPORT_BT_PERM_RW=y +CONFIG_BT_L2CAP_TX_MTU=498 +CONFIG_BT_BUF_ACL_TX_SIZE=251 + +# External Flash +CONFIG_FLASH=y +CONFIG_FLASH_MAP=y +CONFIG_SPI_NOR=y +CONFIG_SPI_NOR_SFDP_DEVICETREE=y diff --git a/prj_release.conf b/prj_release.conf new file mode 100644 index 0000000..1adec71 --- /dev/null +++ b/prj_release.conf @@ -0,0 +1,34 @@ +# +# Copyright (c) 2022 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +# nRF5340 Audio +CONFIG_NRF5340_AUDIO=y + +CONFIG_SAMPLE_RATE_CONVERTER=y +CONFIG_SAMPLE_RATE_CONVERTER_FILTER_SIMPLE=y + +# General +CONFIG_DEBUG=n +CONFIG_ASSERT=n +CONFIG_STACK_USAGE=n +CONFIG_THREAD_MONITOR=n +CONFIG_SERIAL=n +CONFIG_CONSOLE=n +CONFIG_PRINTK=n +CONFIG_UART_CONSOLE=n +CONFIG_BOOT_BANNER=n + +# Bluetooth +# Default value from src/bluetooth/Kconfig.defaults. +# BT_PRIVACY is default turned off to ease the development. +# Should be turned on before production. +# CONFIG_BT_PRIVACY=y + +# USB +# Default values from src/modules/Kconfig.defaults. +# USB VID and PID must be changed before production. +# CONFIG_USB_DEVICE_VID=0x1915 +# CONFIG_USB_DEVICE_PID=0x530A diff --git a/sample.yaml b/sample.yaml new file mode 100644 index 0000000..12d55da --- /dev/null +++ b/sample.yaml @@ -0,0 +1,47 @@ +sample: + name: nRF5340 Audio application + description: LE Audio and Auracast implementation example +common: + integration_platforms: + - nrf5340_audio_dk/nrf5340/cpuapp + platform_allow: nrf5340_audio_dk/nrf5340/cpuapp + sysbuild: true + build_only: true + tags: + - ci_build + - sysbuild +tests: + applications.nrf5340_audio.headset_unicast: + extra_args: + - FILE_SUFFIX=release + - CONFIG_AUDIO_DEV=1 + - EXTRA_CONF_FILE=unicast_server/overlay-unicast_server.conf + applications.nrf5340_audio.gateway_unicast: + extra_args: + - FILE_SUFFIX=release + - CONFIG_AUDIO_DEV=2 + - EXTRA_CONF_FILE=unicast_client/overlay-unicast_client.conf + applications.nrf5340_audio.headset_broadcast: + extra_args: + - FILE_SUFFIX=release + - CONFIG_AUDIO_DEV=1 + - CONFIG_TRANSPORT_BIS=y + - EXTRA_CONF_FILE=broadcast_sink/overlay-broadcast_sink.conf + applications.nrf5340_audio.gateway_broadcast: + extra_args: + - FILE_SUFFIX=release + - CONFIG_AUDIO_DEV=2 + - CONFIG_TRANSPORT_BIS=y + - EXTRA_CONF_FILE=broadcast_source/overlay-broadcast_source.conf + applications.nrf5340_audio.headset_unicast_sd_card: + extra_args: + - FILE_SUFFIX=release + - CONFIG_AUDIO_DEV=1 + - CONFIG_SD_CARD_PLAYBACK=y + - EXTRA_CONF_FILE=unicast_server/overlay-unicast_server.conf + applications.nrf5340_audio.headset_dfu: + extra_args: + - FILE_SUFFIX=fota + - ipc_radio_FILE_SUFFIX=release + - CONFIG_AUDIO_DEV=1 + - EXTRA_CONF_FILE=unicast_server/overlay-unicast_server.conf diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt new file mode 100644 index 0000000..306830c --- /dev/null +++ b/src/audio/CMakeLists.txt @@ -0,0 +1,12 @@ +# +# Copyright (c) 2022 Nordic Semiconductor +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +target_sources(app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/audio_system.c + ${CMAKE_CURRENT_SOURCE_DIR}/audio_datapath.c + ${CMAKE_CURRENT_SOURCE_DIR}/sw_codec_select.c + ${CMAKE_CURRENT_SOURCE_DIR}/le_audio_rx.c +) diff --git a/src/audio/Kconfig b/src/audio/Kconfig new file mode 100644 index 0000000..68004f3 --- /dev/null +++ b/src/audio/Kconfig @@ -0,0 +1,388 @@ +# +# Copyright (c) 2022 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +rsource "Kconfig.defaults" + +menu "Audio" + +choice AUDIO_FRAME_DURATION + prompt "Select frame duration - 7.5 ms frame duration is not tested" + default AUDIO_FRAME_DURATION_10_MS + help + LC3 supports frame duration of 7.5 and 10 ms. + If USB is selected as audio source, we should + have a frame duration of 10 ms since USB sends 1ms at a time. + +config AUDIO_FRAME_DURATION_7_5_MS + bool "7.5 ms" + +config AUDIO_FRAME_DURATION_10_MS + bool "10 ms" +endchoice + +config AUDIO_FRAME_DURATION_US + int + default 7500 if AUDIO_FRAME_DURATION_7_5_MS + default 10000 if AUDIO_FRAME_DURATION_10_MS + help + Audio frame duration in µs. + +config AUDIO_MIN_PRES_DLY_US + int "The minimum presentation delay" + default 5000 if STREAM_BIDIRECTIONAL + default 3000 + help + The minimum allowable presentation delay in microseconds. + This needs to allow time for decoding and internal routing. + +config AUDIO_MAX_PRES_DLY_US + int "The maximum presentation delay" + default 60000 + help + The maximum allowable presentation delay in microseconds. + Increasing this will also increase the FIFO buffers to allow buffering. + +choice AUDIO_SYSTEM_SAMPLE_RATE + prompt "System audio sample rate" + default AUDIO_SAMPLE_RATE_16000_HZ if BT_BAP_BROADCAST_16_2_1 + default AUDIO_SAMPLE_RATE_16000_HZ if BT_BAP_BROADCAST_16_2_2 + default AUDIO_SAMPLE_RATE_16000_HZ if BT_BAP_UNICAST_16_2_1 + default AUDIO_SAMPLE_RATE_24000_HZ if BT_BAP_BROADCAST_24_2_1 + default AUDIO_SAMPLE_RATE_24000_HZ if BT_BAP_BROADCAST_24_2_2 + default AUDIO_SAMPLE_RATE_24000_HZ if BT_BAP_UNICAST_24_2_1 + default AUDIO_SAMPLE_RATE_48000_HZ + help + This configuration reflects the system sample rate, but the audio data may be resampled to + another sample rate before encoding, and after decoding. + +config AUDIO_SAMPLE_RATE_16000_HZ + bool "16 kHz" + help + Sample rate of 16kHz is currently only valid for I2S/line-in. + +config AUDIO_SAMPLE_RATE_24000_HZ + bool "24 kHz" + help + Sample rate of 24kHz is currently only valid for I2S/line-in. + +config AUDIO_SAMPLE_RATE_48000_HZ + bool "48 kHz" + help + Sample rate of 48kHz is valid for both I2S/line-in and USB. +endchoice + +config AUDIO_SAMPLE_RATE_HZ + int + default 16000 if AUDIO_SAMPLE_RATE_16000_HZ + default 24000 if AUDIO_SAMPLE_RATE_24000_HZ + default 48000 if AUDIO_SAMPLE_RATE_48000_HZ + help + I2S supports 16, 24, and 48 kHz sample rates for both input and output. + USB supports only 48 kHz for input. + +choice AUDIO_BIT_DEPTH + prompt "Audio bit depth" + default AUDIO_BIT_DEPTH_16 + help + Select the bit depth for audio. + +config AUDIO_BIT_DEPTH_16 + bool "16 bit audio" + +config AUDIO_BIT_DEPTH_32 + bool "32 bit audio" +endchoice + +config AUDIO_BIT_DEPTH_BITS + int + default 16 if AUDIO_BIT_DEPTH_16 + default 32 if AUDIO_BIT_DEPTH_32 + help + Bit depth of one sample in storage. + +config AUDIO_BIT_DEPTH_OCTETS + int + default 2 if AUDIO_BIT_DEPTH_16 + default 4 if AUDIO_BIT_DEPTH_32 + help + Bit depth of one sample in storage given in octets. + +choice AUDIO_SOURCE_GATEWAY + prompt "Audio source for gateway" + default AUDIO_SOURCE_I2S if WALKIE_TALKIE_DEMO + default AUDIO_SOURCE_USB + help + Select audio source for the gateway. + +config AUDIO_SOURCE_USB + bool "Use USB as audio source" + help + Set USB as audio source. Note that this forces the + stream to be unidirectional because of CPU load. + +config AUDIO_SOURCE_I2S + bool "Use I2S as audio source" +endchoice + +choice AUDIO_HEADSET_CHANNEL + prompt "Headset audio channel assignment" + default AUDIO_HEADSET_CHANNEL_RUNTIME + help + Set whether audio channel assignment for the headset + should happen at runtime or compile-time. + +config AUDIO_HEADSET_CHANNEL_RUNTIME + bool "Select at runtime" + help + Make channel selection at runtime. Selected value is stored in persistent memory. + Left channel: Hold volume-down button on headset while resetting headset. + Right channel: Hold volume-up button on headset while resetting headset. + +config AUDIO_HEADSET_CHANNEL_COMPILE_TIME + bool "Set at compile-time" + help + Set channel selection at compile-time. +endchoice + +config AUDIO_TEST_TONE + bool "Test tone instead of doing user defined action" + select TONE + default y + help + Use button 4 to set a test tone + instead of doing a user defined action. + The test tone is generated on the device itself. + +config AUDIO_MUTE + bool "Mute instead of doing user defined action" + default y + help + Use button 5 to mute audio instead of + doing a user defined action. + +if AUDIO_HEADSET_CHANNEL_COMPILE_TIME + +config AUDIO_HEADSET_CHANNEL + int "Audio channel used by headset" + range 0 1 + default 0 + help + Audio channel compile-time selection. + Left = 0. + Right = 1. + +endif # AUDIO_HEADSET_CHANNEL_COMPILE_TIME + +#----------------------------------------------------------------------------# +menu "SW Codec" + +choice SW_CODEC_DEFAULT + prompt "Starting SW codec" + default SW_CODEC_LC3 + help + Select the default codec to use on start up. + +config SW_CODEC_LC3 + bool "LC3" + select SW_CODEC_LC3_T2_SOFTWARE + help + LC3 is the mandatory codec for LE Audio. + +config SW_CODEC_NONE + bool "None" + help + Choose this if no software (SW) codec is needed. + +# Leave room for other codecs +endchoice + +config SW_CODEC_PLC_DISABLED + bool "Skip PLC on a bad frame and fill the output buffer(s) with zeros instead" + default n + select LC3_PLC_DISABLED + +#----------------------------------------------------------------------------# +menu "LC3" +visible if SW_CODEC_LC3 + +config LC3_BITRATE_MAX + int "Max bitrate for LC3" + default 124000 + +config LC3_BITRATE_MIN + int "Min bitrate for LC3" + default 32000 + +config LC3_BITRATE + int + range LC3_BITRATE_MIN LC3_BITRATE_MAX + default 96000 + +osource "../nrfxlib/lc3/Kconfig" + +endmenu # LC3 +endmenu # SW Codec + +#----------------------------------------------------------------------------# +menu "Stream" + +config BUF_BLE_RX_PACKET_NUM + int + default 5 + range 2 5 + help + Value can be adjusted to affect the overall latency. + This adjusts the number packets in the BLE FIFO RX buffer, + which is where the main latency resides. A low value will decrease + latency and reduce stability, and vice-versa. + Two is recommended minimum to reduce the likelyhood of audio + gaps due to BLE retransmits. + +config STREAM_BIDIRECTIONAL + depends on TRANSPORT_CIS + bool "Bidirectional stream" + default n + help + Bidirectional stream enables encoder and decoder on both sides, + and one device can both send and receive audio. + +config WALKIE_TALKIE_DEMO + select STREAM_BIDIRECTIONAL + bool "Walkie talkie demo" + default n + help + The walkie talkie demo will set up a bidirectional stream using PDM + microphones on each side. + +config MONO_TO_ALL_RECEIVERS + bool "Send mono (first/left channel) to all receivers" + default y if BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT = 1 + default y if BT_BAP_BROADCAST_SRC_STREAM_COUNT = 1 + default n + help + With this flag set, the gateway will encode and send the same (first/left) + channel on all ISO channels. + +endmenu # Stream + +#----------------------------------------------------------------------------# +menu "Log levels" + +module = AUDIO_SYSTEM +module-str = audio-system +source "subsys/logging/Kconfig.template.log_config" + +module = SW_CODEC_SELECT +module-str = sw-codec-select +source "subsys/logging/Kconfig.template.log_config" + +module = STREAMCTRL +module-str = streamctrl +source "subsys/logging/Kconfig.template.log_config" + +module = AUDIO_DATAPATH +module-str = audio-datapath +source "subsys/logging/Kconfig.template.log_config" + +module = AUDIO_SYNC_TIMER +module-str = audio-sync-timer +source "subsys/logging/Kconfig.template.log_config" + +module = LE_AUDIO_RX +module-str = le-audio-rx +source "subsys/logging/Kconfig.template.log_config" + +endmenu # Log levels + +#----------------------------------------------------------------------------# +menu "Thread priorities" + +config ENCODER_THREAD_PRIO + int "Priority for encoder thread" + default 3 + help + This is a preemptible thread. + +config AUDIO_DATAPATH_THREAD_PRIO + int "Priority for audio datapath thread" + default 4 + help + This is a preemptible thread. + +config BUTTON_MSG_SUB_THREAD_PRIO + int "Thread priority for button subscriber" + default 5 + help + This is a preemptible thread. + This thread will subscribe to button events from zbus. + +config LE_AUDIO_MSG_SUB_THREAD_PRIO + int "Thread priority for LE Audio subscriber" + default 5 + help + This is a preemptible thread. + This thread will subscribe to LE Audio events from zbus. + +config BT_MGMT_MSG_SUB_THREAD_PRIO + int "Thread priority for bt_mgmt subscriber" + default 5 + help + This is a preemptible thread. + This thread will subscribe to BT management events from zbus. + +config CONTENT_CONTROL_MSG_SUB_THREAD_PRIO + int "Thread priority for content control subscriber" + default 5 + help + This is a preemptible thread. + This thread will subscribe to content control events from zbus. + +endmenu # Thread priorities + +#----------------------------------------------------------------------------# +menu "Stack sizes" + +config ENCODER_STACK_SIZE + int "Stack size for encoder thread" + default 11000 if AUDIO_BIT_DEPTH_16 + default 21400 if AUDIO_BIT_DEPTH_32 + +config AUDIO_DATAPATH_STACK_SIZE + int "Stack size for audio datapath thread" + default 7600 if AUDIO_BIT_DEPTH_16 + default 14700 if AUDIO_BIT_DEPTH_32 + +config BUTTON_MSG_SUB_STACK_SIZE + int "Stack size for button subscriber" + default 2048 + +config LE_AUDIO_MSG_SUB_STACK_SIZE + int "Stack size for LE Audio subscriber" + default 2048 + +config BT_MGMT_MSG_SUB_STACK_SIZE + int "Stack size for bt_mgmt subscriber" + default 2048 + +config CONTENT_CONTROL_MSG_SUB_STACK_SIZE + int "Stack size for content control subscriber" + default 1024 + +endmenu # Stack sizes + +#----------------------------------------------------------------------------# +menu "Zbus" + +config BUTTON_MSG_SUB_QUEUE_SIZE + int "Queue size for button subscriber" + default 4 + +config CONTENT_CONTROL_MSG_SUB_QUEUE_SIZE + int "Queue size for content control subscriber" + default 4 + +endmenu # Zbus +endmenu # Audio diff --git a/src/audio/Kconfig.defaults b/src/audio/Kconfig.defaults new file mode 100644 index 0000000..809aad9 --- /dev/null +++ b/src/audio/Kconfig.defaults @@ -0,0 +1,13 @@ +# +# Copyright (c) 2022 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +# Audio sync timer +config NRFX_TIMER1 + default y + +# Audio sync timer +config NRFX_DPPI + default y diff --git a/src/audio/audio_datapath.c b/src/audio/audio_datapath.c new file mode 100644 index 0000000..349ce52 --- /dev/null +++ b/src/audio/audio_datapath.c @@ -0,0 +1,1218 @@ +/* + * Copyright (c) 2021, PACKETCRAFT, INC. + * + * SPDX-License-Identifier: LicenseRef-PCFT + */ + +#include "audio_datapath.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "zbus_common.h" +#include "macros_common.h" +#include "led.h" +#include "audio_i2s.h" +#include "sw_codec_select.h" +#include "audio_system.h" +#include "streamctrl.h" +#include "sd_card_playback.h" + +#include +LOG_MODULE_REGISTER(audio_datapath, CONFIG_AUDIO_DATAPATH_LOG_LEVEL); + +/* + * Terminology + * - sample: signed integer of audio waveform amplitude + * - sample FIFO: circular array of raw audio samples + * - block: set of raw audio samples exchanged with I2S + * - frame: encoded audio packet exchanged with connectivity + */ + +#define SDU_REF_DELTA_MAX_ERR_US (int)(CONFIG_AUDIO_FRAME_DURATION_US * 0.001) + +#define BLK_PERIOD_US 1000 + +/* Total sample FIFO period in microseconds */ +#define FIFO_SMPL_PERIOD_US (CONFIG_AUDIO_MAX_PRES_DLY_US * 2) +#define FIFO_NUM_BLKS NUM_BLKS(FIFO_SMPL_PERIOD_US) +#define MAX_FIFO_SIZE (FIFO_NUM_BLKS * BLK_SIZE_SAMPLES(CONFIG_AUDIO_SAMPLE_RATE_HZ) * 2) + +/* Number of audio blocks given a duration */ +#define NUM_BLKS(d) ((d) / BLK_PERIOD_US) +/* Single audio block size in number of samples (stereo) */ +/* clang-format off */ +#define BLK_SIZE_SAMPLES(r) (((r)*BLK_PERIOD_US) / 1000000) +/* clang-format on */ +/* Increment sample FIFO index by one block */ +#define NEXT_IDX(i) (((i) < (FIFO_NUM_BLKS - 1)) ? ((i) + 1) : 0) +/* Decrement sample FIFO index by one block */ +#define PREV_IDX(i) (((i) > 0) ? ((i)-1) : (FIFO_NUM_BLKS - 1)) + +#define NUM_BLKS_IN_FRAME NUM_BLKS(CONFIG_AUDIO_FRAME_DURATION_US) +#define BLK_MONO_NUM_SAMPS BLK_SIZE_SAMPLES(CONFIG_AUDIO_SAMPLE_RATE_HZ) +#define BLK_STEREO_NUM_SAMPS (BLK_MONO_NUM_SAMPS * 2) +/* Number of octets in a single audio block */ +#define BLK_MONO_SIZE_OCTETS (BLK_MONO_NUM_SAMPS * CONFIG_AUDIO_BIT_DEPTH_OCTETS) +#define BLK_STEREO_SIZE_OCTETS (BLK_MONO_SIZE_OCTETS * 2) +/* How many function calls before moving on with drift compensation */ +#define DRIFT_COMP_WAITING_CNT (DRIFT_MEAS_PERIOD_US / BLK_PERIOD_US) +/* How much data to be collected before moving on with presentation compensation */ +#define PRES_COMP_NUM_DATA_PTS (DRIFT_MEAS_PERIOD_US / CONFIG_AUDIO_FRAME_DURATION_US) + +/* Audio clock - nRF5340 Analog Phase-Locked Loop (APLL) */ +#define APLL_FREQ_MIN HFCLKAUDIO_12_165_MHZ +#define APLL_FREQ_CENTER HFCLKAUDIO_12_288_MHZ +#define APLL_FREQ_MAX HFCLKAUDIO_12_411_MHZ +/* Use nanoseconds to reduce rounding errors */ +/* clang-format off */ +#define APLL_FREQ_ADJ(t) (-((t)*1000) / 331) +/* clang-format on */ + +#define DRIFT_MEAS_PERIOD_US 100000 +#define DRIFT_ERR_THRESH_LOCK 16 +#define DRIFT_ERR_THRESH_UNLOCK 32 +/* To get smaller corrections */ +#define DRIFT_REGULATOR_DIV_FACTOR 2 + +/* To allow BLE transmission and (host -> HCI -> controller) */ +#define JUST_IN_TIME_TARGET_DLY_US 3000 +#define JUST_IN_TIME_BOUND_US 2500 + +/* How often to print under-run warning */ +#define UNDERRUN_LOG_INTERVAL_BLKS 5000 + +enum drift_comp_state { + DRIFT_STATE_INIT, /* Waiting for data to be received */ + DRIFT_STATE_CALIB, /* Calibrate and zero out local delay */ + DRIFT_STATE_OFFSET, /* Adjust I2S offset relative to SDU Reference */ + DRIFT_STATE_LOCKED /* Drift compensation locked - Minor corrections */ +}; + +static const char *const drift_comp_state_names[] = { + "INIT", + "CALIB", + "OFFSET", + "LOCKED", +}; + +enum pres_comp_state { + PRES_STATE_INIT, /* Initialize presentation compensation */ + PRES_STATE_MEAS, /* Measure presentation delay */ + PRES_STATE_WAIT, /* Wait for some time */ + PRES_STATE_LOCKED /* Presentation compensation locked */ +}; + +static const char *const pres_comp_state_names[] = { + "INIT", + "MEAS", + "WAIT", + "LOCKED", +}; + +static struct { + bool datapath_initialized; + bool stream_started; + void *decoded_data; + + struct { + struct data_fifo *fifo; + } in; + + struct { +#if CONFIG_AUDIO_BIT_DEPTH_16 + int16_t __aligned(sizeof(uint32_t)) fifo[MAX_FIFO_SIZE]; +#elif CONFIG_AUDIO_BIT_DEPTH_32 + int32_t __aligned(sizeof(uint32_t)) fifo[MAX_FIFO_SIZE]; +#endif + uint16_t prod_blk_idx; /* Output producer audio block index */ + uint16_t cons_blk_idx; /* Output consumer audio block index */ + uint32_t prod_blk_ts[FIFO_NUM_BLKS]; + /* Statistics */ + uint32_t total_blk_underruns; + } out; + + uint32_t prev_drift_sdu_ref_us; + uint32_t prev_pres_sdu_ref_us; + uint32_t current_pres_dly_us; + + struct { + enum drift_comp_state state: 8; + uint16_t ctr; /* Count func calls. Used for waiting */ + uint32_t meas_start_time_us; + uint32_t center_freq; + bool enabled; + } drift_comp; + + struct { + enum pres_comp_state state: 8; + uint16_t ctr; /* Count func calls. Used for collecting data points and waiting */ + int32_t sum_err_dly_us; + uint32_t pres_delay_us; + bool enabled; + } pres_comp; +} ctrl_blk; + +/** + * @brief Get the current number of blocks in the output buffer. + */ +static int filled_blocks_get(void) +{ + if (ctrl_blk.out.cons_blk_idx < ctrl_blk.out.prod_blk_idx) { + return ctrl_blk.out.prod_blk_idx - ctrl_blk.out.cons_blk_idx; + } else if (ctrl_blk.out.cons_blk_idx > ctrl_blk.out.prod_blk_idx) { + return (FIFO_NUM_BLKS - ctrl_blk.out.cons_blk_idx) + ctrl_blk.out.prod_blk_idx; + } else { + return 0; + } +} + +static bool tone_active; +/* Buffer which can hold max 1 period test tone at 100 Hz */ +static uint16_t test_tone_buf[CONFIG_AUDIO_SAMPLE_RATE_HZ / 100]; +static size_t test_tone_size; + +/** + * @brief Calculate error between sdu_ref and frame_start_ts_us. + * + * @note Used to adjust audio clock to account for drift. + * + * @param sdu_ref_us Timestamp for SDU. + * @param frame_start_ts_us Timestamp for I2S. + * + * @return Error in microseconds (err_us). + */ +static int32_t err_us_calculate(uint32_t sdu_ref_us, uint32_t frame_start_ts_us) +{ + bool err_neg = false; + + int64_t total_err = ((int64_t)sdu_ref_us - (int64_t)frame_start_ts_us); + + /* Store sign for later use, since remainder operation is undefined for negatives */ + if (total_err < 0) { + err_neg = true; + total_err *= -1; + } + + /* Check diff below 1000 us, diff above 1000 us is fixed later on */ + int32_t err_us = total_err % BLK_PERIOD_US; + + if (err_us > (BLK_PERIOD_US / 2)) { + err_us = err_us - BLK_PERIOD_US; + } + + /* Restore the sign */ + if (err_neg) { + err_us *= -1; + } + + return err_us; +} + +static void hfclkaudio_set(uint16_t freq_value) +{ + uint16_t freq_val = freq_value; + + freq_val = MIN(freq_val, APLL_FREQ_MAX); + freq_val = MAX(freq_val, APLL_FREQ_MIN); + + nrfx_clock_hfclkaudio_config_set(freq_val); +} + +static void drift_comp_state_set(enum drift_comp_state new_state) +{ + if (new_state == ctrl_blk.drift_comp.state) { + LOG_WRN("Trying to change to the same drift compensation state"); + return; + } + + ctrl_blk.drift_comp.state = new_state; + LOG_INF("Drft comp state: %s", drift_comp_state_names[new_state]); +} + +/** + * @brief Adjust frequency of HFCLKAUDIO to get audio in sync. + * + * @note The audio sync is based on sdu_ref_us. + * + * @param frame_start_ts_us I2S frame start timestamp. + */ +static void audio_datapath_drift_compensation(uint32_t frame_start_ts_us) +{ + if (CONFIG_AUDIO_DEV == HEADSET) { + /** For headsets we do not use the timestamp gotten from hci_tx_sync_get to adjust + * for drift + */ + ctrl_blk.prev_drift_sdu_ref_us = ctrl_blk.prev_pres_sdu_ref_us; + } + switch (ctrl_blk.drift_comp.state) { + case DRIFT_STATE_INIT: { + /* Check if audio data has been received */ + if (ctrl_blk.prev_drift_sdu_ref_us) { + ctrl_blk.drift_comp.meas_start_time_us = ctrl_blk.prev_drift_sdu_ref_us; + + drift_comp_state_set(DRIFT_STATE_CALIB); + } + + break; + } + case DRIFT_STATE_CALIB: { + if (++ctrl_blk.drift_comp.ctr < DRIFT_COMP_WAITING_CNT) { + /* Waiting */ + return; + } + + ctrl_blk.drift_comp.ctr = 0; + + int32_t err_us = DRIFT_MEAS_PERIOD_US - (ctrl_blk.prev_drift_sdu_ref_us - + ctrl_blk.drift_comp.meas_start_time_us); + + int32_t freq_adj = APLL_FREQ_ADJ(err_us); + + ctrl_blk.drift_comp.center_freq = APLL_FREQ_CENTER + freq_adj; + + if ((ctrl_blk.drift_comp.center_freq > (APLL_FREQ_MAX)) || + (ctrl_blk.drift_comp.center_freq < (APLL_FREQ_MIN))) { + LOG_DBG("Invalid center frequency, re-calculating"); + drift_comp_state_set(DRIFT_STATE_INIT); + return; + } + + hfclkaudio_set(ctrl_blk.drift_comp.center_freq); + + drift_comp_state_set(DRIFT_STATE_OFFSET); + + break; + } + case DRIFT_STATE_OFFSET: { + if (++ctrl_blk.drift_comp.ctr < DRIFT_COMP_WAITING_CNT) { + /* Waiting */ + return; + } + + ctrl_blk.drift_comp.ctr = 0; + + int32_t err_us = + err_us_calculate(ctrl_blk.prev_drift_sdu_ref_us, frame_start_ts_us); + + err_us /= DRIFT_REGULATOR_DIV_FACTOR; + int32_t freq_adj = APLL_FREQ_ADJ(err_us); + + hfclkaudio_set(ctrl_blk.drift_comp.center_freq + freq_adj); + + if ((err_us < DRIFT_ERR_THRESH_LOCK) && (err_us > -DRIFT_ERR_THRESH_LOCK)) { + drift_comp_state_set(DRIFT_STATE_LOCKED); + } + + break; + } + case DRIFT_STATE_LOCKED: { + if (++ctrl_blk.drift_comp.ctr < DRIFT_COMP_WAITING_CNT) { + /* Waiting */ + return; + } + + ctrl_blk.drift_comp.ctr = 0; + + int32_t err_us = + err_us_calculate(ctrl_blk.prev_drift_sdu_ref_us, frame_start_ts_us); + + err_us /= DRIFT_REGULATOR_DIV_FACTOR; + int32_t freq_adj = APLL_FREQ_ADJ(err_us); + + hfclkaudio_set(ctrl_blk.drift_comp.center_freq + freq_adj); + + if ((err_us > DRIFT_ERR_THRESH_UNLOCK) || (err_us < -DRIFT_ERR_THRESH_UNLOCK)) { + drift_comp_state_set(DRIFT_STATE_INIT); + } + + break; + } + default: { + break; + } + } +} + +static void pres_comp_state_set(enum pres_comp_state new_state) +{ + int ret; + + if (new_state == ctrl_blk.pres_comp.state) { + return; + } + + ctrl_blk.pres_comp.state = new_state; + /* NOTE: The string below is used by the Nordic CI system */ + LOG_INF("Pres comp state: %s", pres_comp_state_names[new_state]); + if (new_state == PRES_STATE_LOCKED) { + ret = led_on(LED_APP_2_GREEN); + } else { + ret = led_off(LED_APP_2_GREEN); + } + ERR_CHK(ret); +} + +/** + * @brief Move audio blocks back and forth in FIFO to get audio in sync. + * + * @note The audio sync is based on sdu_ref_us. + * + * @param recv_frame_ts_us Timestamp of when frame was received. + * @param sdu_ref_us ISO timestamp reference from Bluetooth LE controller. + * @param sdu_ref_not_consecutive True if sdu_ref_us and the previous sdu_ref_us + * originate from non-consecutive frames. + */ +static void audio_datapath_presentation_compensation(uint32_t recv_frame_ts_us, uint32_t sdu_ref_us, + bool sdu_ref_not_consecutive) +{ + if (ctrl_blk.drift_comp.state != DRIFT_STATE_LOCKED) { + /* Unconditionally reset state machine if drift compensation looses lock */ + pres_comp_state_set(PRES_STATE_INIT); + return; + } + + /* Move presentation compensation into PRES_STATE_WAIT if sdu_ref_us and + * the previous sdu_ref_us originate from non-consecutive frames. + */ + if (sdu_ref_not_consecutive) { + ctrl_blk.pres_comp.ctr = 0; + pres_comp_state_set(PRES_STATE_WAIT); + } + + int32_t wanted_pres_dly_us = + ctrl_blk.pres_comp.pres_delay_us - (recv_frame_ts_us - sdu_ref_us); + int32_t pres_adj_us = 0; + + switch (ctrl_blk.pres_comp.state) { + case PRES_STATE_INIT: { + ctrl_blk.pres_comp.ctr = 0; + ctrl_blk.pres_comp.sum_err_dly_us = 0; + pres_comp_state_set(PRES_STATE_MEAS); + + break; + } + case PRES_STATE_MEAS: { + if (ctrl_blk.pres_comp.ctr++ < PRES_COMP_NUM_DATA_PTS) { + ctrl_blk.pres_comp.sum_err_dly_us += + wanted_pres_dly_us - ctrl_blk.current_pres_dly_us; + + /* Same state - Collect more data */ + break; + } + + ctrl_blk.pres_comp.ctr = 0; + + pres_adj_us = ctrl_blk.pres_comp.sum_err_dly_us / PRES_COMP_NUM_DATA_PTS; + if ((pres_adj_us >= (BLK_PERIOD_US / 2)) || (pres_adj_us <= -(BLK_PERIOD_US / 2))) { + pres_comp_state_set(PRES_STATE_WAIT); + } else { + /* Drift compensation will always be in DRIFT_STATE_LOCKED here */ + pres_comp_state_set(PRES_STATE_LOCKED); + } + + break; + } + case PRES_STATE_WAIT: { + if (ctrl_blk.pres_comp.ctr++ > + (FIFO_SMPL_PERIOD_US / CONFIG_AUDIO_FRAME_DURATION_US)) { + pres_comp_state_set(PRES_STATE_INIT); + } + + break; + } + case PRES_STATE_LOCKED: { + /* + * Presentation delay compensation moves into PRES_STATE_WAIT if sdu_ref_us + * and the previous sdu_ref_us originate from non-consecutive frames, or into + * PRES_STATE_INIT if drift compensation unlocks. + */ + + break; + } + default: { + break; + } + } + + if (pres_adj_us == 0) { + return; + } + + /* Operation to obtain nearest whole number in subsequent operations */ + if (pres_adj_us >= 0) { + pres_adj_us += (BLK_PERIOD_US / 2); + } else { + pres_adj_us += -(BLK_PERIOD_US / 2); + } + + /* Number of adjustment blocks is 0 as long as |pres_adj_us| < BLK_PERIOD_US */ + int32_t pres_adj_blks = pres_adj_us / BLK_PERIOD_US; + + if (pres_adj_blks > (FIFO_NUM_BLKS / 2)) { + /* Limit adjustment */ + pres_adj_blks = FIFO_NUM_BLKS / 2; + + LOG_WRN("Requested presentation delay out of range: pres_adj_us=%d", pres_adj_us); + } else if (pres_adj_blks < -(FIFO_NUM_BLKS / 2)) { + /* Limit adjustment */ + pres_adj_blks = -(FIFO_NUM_BLKS / 2); + + LOG_WRN("Requested presentation delay out of range: pres_adj_us=%d", pres_adj_us); + } + + if (pres_adj_blks > 0) { + LOG_DBG("Presentation delay inserted: pres_adj_blks=%d", pres_adj_blks); + + /* Increase presentation delay */ + for (int i = 0; i < pres_adj_blks; i++) { + /* Mute audio block */ + memset(&ctrl_blk.out.fifo[ctrl_blk.out.prod_blk_idx * BLK_STEREO_NUM_SAMPS], + 0, BLK_STEREO_SIZE_OCTETS); + + /* Record producer block start reference */ + ctrl_blk.out.prod_blk_ts[ctrl_blk.out.prod_blk_idx] = + recv_frame_ts_us - ((pres_adj_blks - i) * BLK_PERIOD_US); + + ctrl_blk.out.prod_blk_idx = NEXT_IDX(ctrl_blk.out.prod_blk_idx); + } + } else if (pres_adj_blks < 0) { + LOG_DBG("Presentation delay removed: pres_adj_blks=%d", pres_adj_blks); + + /* Reduce presentation delay */ + for (int i = 0; i > pres_adj_blks; i--) { + ctrl_blk.out.prod_blk_idx = PREV_IDX(ctrl_blk.out.prod_blk_idx); + } + } +} + +static void tone_stop_worker(struct k_work *work) +{ + tone_active = false; + memset(test_tone_buf, 0, sizeof(test_tone_buf)); + LOG_DBG("Tone stopped"); +} + +K_WORK_DEFINE(tone_stop_work, tone_stop_worker); + +static void tone_stop_timer_handler(struct k_timer *dummy) +{ + k_work_submit(&tone_stop_work); +}; + +K_TIMER_DEFINE(tone_stop_timer, tone_stop_timer_handler, NULL); + +int audio_datapath_tone_play(uint16_t freq, uint16_t dur_ms, float amplitude) +{ + int ret; + + if (tone_active) { + return -EBUSY; + } + + if (IS_ENABLED(CONFIG_AUDIO_TEST_TONE)) { + ret = tone_gen(test_tone_buf, &test_tone_size, freq, CONFIG_AUDIO_SAMPLE_RATE_HZ, + amplitude); + if (ret) { + return ret; + } + } else { + LOG_ERR("Test tone is not enabled"); + return -ENXIO; + } + + /* If duration is 0, play forever */ + if (dur_ms != 0) { + k_timer_start(&tone_stop_timer, K_MSEC(dur_ms), K_NO_WAIT); + } + + tone_active = true; + LOG_DBG("Tone started"); + return 0; +} + +void audio_datapath_tone_stop(void) +{ + k_timer_stop(&tone_stop_timer); + k_work_submit(&tone_stop_work); +} + +static void tone_mix(uint8_t *tx_buf) +{ + int ret; + int8_t tone_buf_continuous[BLK_MONO_SIZE_OCTETS]; + static uint32_t finite_pos; + + ret = contin_array_create(tone_buf_continuous, BLK_MONO_SIZE_OCTETS, test_tone_buf, + test_tone_size, &finite_pos); + ERR_CHK(ret); + + ret = pcm_mix(tx_buf, BLK_STEREO_SIZE_OCTETS, tone_buf_continuous, BLK_MONO_SIZE_OCTETS, + B_MONO_INTO_A_STEREO_L); + ERR_CHK(ret); +} + +/* Alternate-buffers used when there is no active audio stream. + * Used interchangeably by I2S. + */ +static struct { + uint8_t __aligned(WB_UP(1)) buf_0[BLK_STEREO_SIZE_OCTETS]; + uint8_t __aligned(WB_UP(1)) buf_1[BLK_STEREO_SIZE_OCTETS]; + bool buf_0_in_use; + bool buf_1_in_use; +} alt; + +/** + * @brief Get first available alternative-buffer. + * + * @param p_buffer Double pointer to populate with buffer. + * + * @retval 0 if success. + * @retval -ENOMEM No available buffers. + */ +static int alt_buffer_get(void **p_buffer) +{ + if (!alt.buf_0_in_use) { + alt.buf_0_in_use = true; + *p_buffer = alt.buf_0; + } else if (!alt.buf_1_in_use) { + alt.buf_1_in_use = true; + *p_buffer = alt.buf_1; + } else { + return -ENOMEM; + } + + return 0; +} + +/** + * @brief Checks if pointer matches that of a buffer + * and frees it in one operation. + * + * @param p_buffer Buffer to free. + */ +static void alt_buffer_free(void const *const p_buffer) +{ + if (p_buffer == alt.buf_0) { + alt.buf_0_in_use = false; + } else if (p_buffer == alt.buf_1) { + alt.buf_1_in_use = false; + } +} + +/** + * @brief Frees both alternative buffers. + */ +static void alt_buffer_free_both(void) +{ + alt.buf_0_in_use = false; + alt.buf_1_in_use = false; +} + +/* + * This handler function is called every time I2S needs new buffers for + * TX and RX data. + * + * The new TX data buffer is the next consumer block in out.fifo. + * + * The new RX data buffer is the first empty slot of in.fifo. + * New I2S RX data is located in rx_buf_released, and is locked into + * the in.fifo message queue. + */ +static void audio_datapath_i2s_blk_complete(uint32_t frame_start_ts_us, uint32_t *rx_buf_released, + uint32_t const *tx_buf_released) +{ + int ret; + static bool underrun_condition; + + alt_buffer_free(tx_buf_released); + + /*** Presentation delay measurement ***/ + ctrl_blk.current_pres_dly_us = + frame_start_ts_us - ctrl_blk.out.prod_blk_ts[ctrl_blk.out.cons_blk_idx]; + + /********** I2S TX **********/ + static uint8_t *tx_buf; + + if (IS_ENABLED(CONFIG_STREAM_BIDIRECTIONAL) || (CONFIG_AUDIO_DEV == HEADSET)) { + if (tx_buf_released != NULL) { + /* Double buffered index */ + uint32_t next_out_blk_idx = NEXT_IDX(ctrl_blk.out.cons_blk_idx); + + if (next_out_blk_idx != ctrl_blk.out.prod_blk_idx) { + /* Only increment if not in under-run condition */ + ctrl_blk.out.cons_blk_idx = next_out_blk_idx; + if (underrun_condition) { + underrun_condition = false; + LOG_WRN("Data received, total under-runs: %d", + ctrl_blk.out.total_blk_underruns); + } + + tx_buf = (uint8_t *)&ctrl_blk.out + .fifo[next_out_blk_idx * BLK_STEREO_NUM_SAMPS]; + + } else { + if (stream_state_get() == STATE_STREAMING) { + underrun_condition = true; + ctrl_blk.out.total_blk_underruns++; + + if ((ctrl_blk.out.total_blk_underruns % + UNDERRUN_LOG_INTERVAL_BLKS) == 0) { + LOG_WRN("In I2S TX under-run condition, total: %d", + ctrl_blk.out.total_blk_underruns); + } + } + + /* + * No data available in out.fifo + * use alternative buffers + */ + ret = alt_buffer_get((void **)&tx_buf); + ERR_CHK(ret); + + memset(tx_buf, 0, BLK_STEREO_SIZE_OCTETS); + } + + if (tone_active) { + tone_mix(tx_buf); + } + } + } + + /********** I2S RX **********/ + static uint32_t *rx_buf; + static int prev_ret; + + if (IS_ENABLED(CONFIG_STREAM_BIDIRECTIONAL) || (CONFIG_AUDIO_DEV == GATEWAY)) { + /* Lock last filled buffer into message queue */ + if (rx_buf_released != NULL) { + ret = data_fifo_block_lock(ctrl_blk.in.fifo, (void **)&rx_buf_released, + BLOCK_SIZE_BYTES); + + ERR_CHK_MSG(ret, "Unable to lock block RX"); + } + + /* Get new empty buffer to send to I2S HW */ + ret = data_fifo_pointer_first_vacant_get(ctrl_blk.in.fifo, (void **)&rx_buf, + K_NO_WAIT); + if (ret == 0 && prev_ret == -ENOMEM) { + LOG_WRN("I2S RX continuing stream"); + prev_ret = ret; + } + + /* If RX FIFO is filled up */ + if (ret == -ENOMEM) { + void *data; + size_t size; + + if (ret != prev_ret) { + LOG_WRN("I2S RX overrun. Single msg"); + prev_ret = ret; + } + + ret = data_fifo_pointer_last_filled_get(ctrl_blk.in.fifo, &data, &size, + K_NO_WAIT); + ERR_CHK(ret); + + data_fifo_block_free(ctrl_blk.in.fifo, data); + + ret = data_fifo_pointer_first_vacant_get(ctrl_blk.in.fifo, (void **)&rx_buf, + K_NO_WAIT); + } + + ERR_CHK_MSG(ret, "RX failed to get block"); + } + + /*** Data exchange ***/ + audio_i2s_set_next_buf(tx_buf, rx_buf); + + /*** Drift compensation ***/ + if (ctrl_blk.drift_comp.enabled) { + audio_datapath_drift_compensation(frame_start_ts_us); + } +} + +static void audio_datapath_i2s_start(void) +{ + int ret; + + /* Double buffer I2S */ + uint8_t *tx_buf_one = NULL; + uint8_t *tx_buf_two = NULL; + uint32_t *rx_buf_one = NULL; + uint32_t *rx_buf_two = NULL; + + /* TX */ + if (IS_ENABLED(CONFIG_STREAM_BIDIRECTIONAL) || (CONFIG_AUDIO_DEV == HEADSET)) { + ctrl_blk.out.cons_blk_idx = PREV_IDX(ctrl_blk.out.cons_blk_idx); + tx_buf_one = (uint8_t *)&ctrl_blk.out + .fifo[ctrl_blk.out.cons_blk_idx * BLK_STEREO_NUM_SAMPS]; + + ctrl_blk.out.cons_blk_idx = PREV_IDX(ctrl_blk.out.cons_blk_idx); + tx_buf_two = (uint8_t *)&ctrl_blk.out + .fifo[ctrl_blk.out.cons_blk_idx * BLK_STEREO_NUM_SAMPS]; + } + + /* RX */ + if (IS_ENABLED(CONFIG_STREAM_BIDIRECTIONAL) || (CONFIG_AUDIO_DEV == GATEWAY)) { + uint32_t alloced_cnt; + uint32_t locked_cnt; + + ret = data_fifo_num_used_get(ctrl_blk.in.fifo, &alloced_cnt, &locked_cnt); + if (alloced_cnt || locked_cnt || ret) { + ERR_CHK_MSG(-ENOMEM, "FIFO is not empty!"); + } + + ret = data_fifo_pointer_first_vacant_get(ctrl_blk.in.fifo, (void **)&rx_buf_one, + K_NO_WAIT); + ERR_CHK_MSG(ret, "RX failed to get block"); + ret = data_fifo_pointer_first_vacant_get(ctrl_blk.in.fifo, (void **)&rx_buf_two, + K_NO_WAIT); + ERR_CHK_MSG(ret, "RX failed to get block"); + } + + /* Start I2S */ + audio_i2s_start(tx_buf_one, rx_buf_one); + audio_i2s_set_next_buf(tx_buf_two, rx_buf_two); +} + +static void audio_datapath_i2s_stop(void) +{ + audio_i2s_stop(); + alt_buffer_free_both(); +} + +/** + * @brief Adjust timing to make sure audio data is sent just in time for Bluetooth LE event. + * + * @note The time from last anchor point is checked and then blocks of 1 ms can be dropped + * to allow the sending of encoded data to be sent just before the connection interval + * opens up. This is done to reduce overall latency. + * + * @param[in] tx_sync_ts_us The timestamp from get_tx_sync. + * @param[in] curr_ts_us The current time. This must be in the controller frame of reference. + */ +static void audio_datapath_just_in_time_check_and_adjust(uint32_t tx_sync_ts_us, + uint32_t curr_ts_us) +{ + int ret; + static int32_t print_count; + int64_t diff; + + diff = (int64_t)tx_sync_ts_us - curr_ts_us; + + /* + * The diff should always be positive. If diff is a large negative number, it is likely + * that wrapping has occurred. A small negative value however, may point to the application + * sending data too late, and we need to drop data to get back in sync with the controller. + */ + if (diff < -((int64_t)UINT32_MAX / 2)) { + LOG_DBG("Timestamp wrap. diff: %lld", diff); + diff += UINT32_MAX; + + } else if (diff < 0) { + LOG_DBG("tx_sync_ts_us: %u is earlier than curr_ts_us %u", tx_sync_ts_us, + curr_ts_us); + } + + if (print_count % 100 == 0) { + LOG_DBG("JIT diff: %lld us. Target: %u +/- %u", diff, JUST_IN_TIME_TARGET_DLY_US, + JUST_IN_TIME_BOUND_US); + } + print_count++; + + if ((diff < (JUST_IN_TIME_TARGET_DLY_US - JUST_IN_TIME_BOUND_US)) || + (diff > (JUST_IN_TIME_TARGET_DLY_US + JUST_IN_TIME_BOUND_US))) { + ret = audio_system_fifo_rx_block_drop(); + if (ret) { + LOG_WRN("Not able to drop FIFO RX block"); + return; + } + LOG_DBG("Dropped block to align with connection interval"); + print_count = 0; + } +} + +/** + * @brief Update sdu_ref_us so that drift compensation can work correctly. + * + * @note This function is only valid for gateway using I2S as audio source + * and unidirectional audio stream (gateway to one or more headsets). + * + * @param sdu_ref_us ISO timestamp reference from Bluetooth LE controller. + * @param adjust Indicate if the sdu_ref should be used to adjust timing. + */ +static void audio_datapath_sdu_ref_update(const struct zbus_channel *chan) +{ + if (IS_ENABLED(CONFIG_AUDIO_SOURCE_I2S)) { + uint32_t tx_sync_ts_us; + uint32_t curr_ts_us; + bool adjust; + const struct sdu_ref_msg *msg; + + msg = zbus_chan_const_msg(chan); + tx_sync_ts_us = msg->tx_sync_ts_us; + curr_ts_us = msg->curr_ts_us; + adjust = msg->adjust; + + if (ctrl_blk.stream_started) { + ctrl_blk.prev_drift_sdu_ref_us = tx_sync_ts_us; + + if (adjust && tx_sync_ts_us != 0) { + audio_datapath_just_in_time_check_and_adjust(tx_sync_ts_us, + curr_ts_us); + } + } else { + LOG_WRN("Stream not started - Can not update tx_sync_ts_us"); + } + } +} + +ZBUS_LISTENER_DEFINE(sdu_ref_msg_listen, audio_datapath_sdu_ref_update); + +int audio_datapath_pres_delay_us_set(uint32_t delay_us) +{ + if (!IN_RANGE(delay_us, CONFIG_AUDIO_MIN_PRES_DLY_US, CONFIG_AUDIO_MAX_PRES_DLY_US)) { + LOG_WRN("Presentation delay not supported: %d us", delay_us); + LOG_WRN("Keeping current value: %d us", ctrl_blk.pres_comp.pres_delay_us); + return -EINVAL; + } + + ctrl_blk.pres_comp.pres_delay_us = delay_us; + + LOG_DBG("Presentation delay set to %d us", delay_us); + + return 0; +} + +void audio_datapath_pres_delay_us_get(uint32_t *delay_us) +{ + *delay_us = ctrl_blk.pres_comp.pres_delay_us; +} + +void audio_datapath_stream_out(const uint8_t *buf, size_t size, uint32_t sdu_ref_us, bool bad_frame, + uint32_t recv_frame_ts_us) +{ + if (!ctrl_blk.stream_started) { + LOG_WRN("Stream not started"); + return; + } + + /*** Check incoming data ***/ + + if (!buf) { + LOG_ERR("Buffer pointer is NULL"); + } + + if (sdu_ref_us == ctrl_blk.prev_pres_sdu_ref_us && sdu_ref_us != 0) { + LOG_WRN("Duplicate sdu_ref_us (%d) - Dropping audio frame", sdu_ref_us); + return; + } + + bool sdu_ref_not_consecutive = false; + + if (ctrl_blk.prev_pres_sdu_ref_us) { + uint32_t sdu_ref_delta_us = sdu_ref_us - ctrl_blk.prev_pres_sdu_ref_us; + + /* Check if the delta is from two consecutive frames */ + if (sdu_ref_delta_us < + (CONFIG_AUDIO_FRAME_DURATION_US + (CONFIG_AUDIO_FRAME_DURATION_US / 2))) { + /* Check for invalid delta */ + if ((sdu_ref_delta_us > + (CONFIG_AUDIO_FRAME_DURATION_US + SDU_REF_DELTA_MAX_ERR_US)) || + (sdu_ref_delta_us < + (CONFIG_AUDIO_FRAME_DURATION_US - SDU_REF_DELTA_MAX_ERR_US))) { + LOG_DBG("Invalid sdu_ref_us delta (%d) - Estimating sdu_ref_us", + sdu_ref_delta_us); + + /* Estimate sdu_ref_us */ + sdu_ref_us = ctrl_blk.prev_pres_sdu_ref_us + + CONFIG_AUDIO_FRAME_DURATION_US; + } + } else { + LOG_INF("sdu_ref_us not from consecutive frames (diff: %d us)", + sdu_ref_delta_us); + sdu_ref_not_consecutive = true; + } + } + + ctrl_blk.prev_pres_sdu_ref_us = sdu_ref_us; + + /*** Presentation compensation ***/ + if (ctrl_blk.pres_comp.enabled) { + audio_datapath_presentation_compensation(recv_frame_ts_us, sdu_ref_us, + sdu_ref_not_consecutive); + } + + /*** Decode ***/ + + int ret; + size_t pcm_size; + + ret = sw_codec_decode(buf, size, bad_frame, &ctrl_blk.decoded_data, &pcm_size); + if (ret) { + LOG_WRN("SW codec decode error: %d", ret); + } + + if (IS_ENABLED(CONFIG_SD_CARD_PLAYBACK)) { + if (sd_card_playback_is_active()) { + sd_card_playback_mix_with_stream(ctrl_blk.decoded_data, pcm_size); + } + } + + if (pcm_size != (BLK_STEREO_SIZE_OCTETS * NUM_BLKS_IN_FRAME)) { + LOG_WRN("Decoded audio has wrong size: %d. Expected: %d", pcm_size, + (BLK_STEREO_SIZE_OCTETS * NUM_BLKS_IN_FRAME)); + /* Discard frame */ + return; + } + + /*** Add audio data to FIFO buffer ***/ + uint32_t num_blks_in_fifo = filled_blocks_get(); + + if ((num_blks_in_fifo + NUM_BLKS_IN_FRAME) > FIFO_NUM_BLKS) { + LOG_WRN("Output audio stream overrun - Discarding audio frame"); + + /* Discard frame to allow consumer to catch up */ + return; + } + + uint32_t out_blk_idx = ctrl_blk.out.prod_blk_idx; + + for (uint32_t i = 0; i < NUM_BLKS_IN_FRAME; i++) { + if (IS_ENABLED(CONFIG_AUDIO_BIT_DEPTH_16)) { + memcpy(&ctrl_blk.out.fifo[out_blk_idx * BLK_STEREO_NUM_SAMPS], + &((int16_t *)ctrl_blk.decoded_data)[i * BLK_STEREO_NUM_SAMPS], + BLK_STEREO_SIZE_OCTETS); + } else if (IS_ENABLED(CONFIG_AUDIO_BIT_DEPTH_32)) { + memcpy(&ctrl_blk.out.fifo[out_blk_idx * BLK_STEREO_NUM_SAMPS], + &((int32_t *)ctrl_blk.decoded_data)[i * BLK_STEREO_NUM_SAMPS], + BLK_STEREO_SIZE_OCTETS); + } + + /* Record producer block start reference */ + ctrl_blk.out.prod_blk_ts[out_blk_idx] = recv_frame_ts_us + (i * BLK_PERIOD_US); + + out_blk_idx = NEXT_IDX(out_blk_idx); + } + + ctrl_blk.out.prod_blk_idx = out_blk_idx; +} + +int audio_datapath_start(struct data_fifo *fifo_rx) +{ + __ASSERT_NO_MSG(fifo_rx != NULL); + + if (!ctrl_blk.datapath_initialized) { + LOG_WRN("Audio datapath not initialized"); + return -ECANCELED; + } + + if (!ctrl_blk.stream_started) { + ctrl_blk.in.fifo = fifo_rx; + + /* Clear counters and mute initial audio */ + memset(&ctrl_blk.out, 0, sizeof(ctrl_blk.out)); + + audio_datapath_i2s_start(); + ctrl_blk.stream_started = true; + + return 0; + } else { + return -EALREADY; + } +} + +int audio_datapath_stop(void) +{ + if (ctrl_blk.stream_started) { + ctrl_blk.stream_started = false; + audio_datapath_i2s_stop(); + ctrl_blk.prev_pres_sdu_ref_us = 0; + ctrl_blk.prev_drift_sdu_ref_us = 0; + + pres_comp_state_set(PRES_STATE_INIT); + + return 0; + } else { + return -EALREADY; + } +} + +int audio_datapath_init(void) +{ + memset(&ctrl_blk, 0, sizeof(ctrl_blk)); + audio_i2s_blk_comp_cb_register(audio_datapath_i2s_blk_complete); + audio_i2s_init(); + ctrl_blk.datapath_initialized = true; + ctrl_blk.drift_comp.enabled = true; + ctrl_blk.pres_comp.enabled = true; + + if (IS_ENABLED(CONFIG_STREAM_BIDIRECTIONAL) && (CONFIG_AUDIO_DEV == GATEWAY)) { + /* Disable presentation compensation feature for microphone return on gateway, + * since there's only one stream output from gateway for now, so no need to + * qhave presentation compensation. + */ + ctrl_blk.pres_comp.enabled = false; + } else { + ctrl_blk.pres_comp.enabled = true; + } + + ctrl_blk.pres_comp.pres_delay_us = CONFIG_BT_AUDIO_PRESENTATION_DELAY_US; + + return 0; +} + +static int cmd_i2s_tone_play(const struct shell *shell, size_t argc, const char **argv) +{ + int ret; + uint16_t freq; + uint16_t dur_ms; + float amplitude; + + if (argc != 4) { + shell_error( + shell, + "3 arguments (freq [Hz], dur [ms], and amplitude [0-1.0] must be provided"); + return -EINVAL; + } + + if (!isdigit((int)argv[1][0])) { + shell_error(shell, "Argument 1 is not numeric"); + return -EINVAL; + } + + if (!isdigit((int)argv[2][0])) { + shell_error(shell, "Argument 2 is not numeric"); + return -EINVAL; + } + + freq = strtoul(argv[1], NULL, 10); + dur_ms = strtoul(argv[2], NULL, 10); + amplitude = strtof(argv[3], NULL); + + if (amplitude <= 0 || amplitude > 1) { + shell_error(shell, "Make sure amplitude is 0 < [float] >= 1"); + return -EINVAL; + } + + shell_print(shell, "Setting tone %d Hz for %d ms", freq, dur_ms); + ret = audio_datapath_tone_play(freq, dur_ms, amplitude); + + if (ret == -EBUSY) { + /* Abort continuous running tone with new tone */ + audio_datapath_tone_stop(); + ret = audio_datapath_tone_play(freq, dur_ms, amplitude); + } + + if (ret) { + shell_print(shell, "Tone failed with code %d", ret); + } + + shell_print(shell, "Tone play: %d Hz for %d ms with amplitude %.02f", freq, dur_ms, + (double)amplitude); + + return ret; +} + +static int cmd_i2s_tone_stop(const struct shell *shell, size_t argc, const char **argv) +{ + ARG_UNUSED(argc); + ARG_UNUSED(argv); + + audio_datapath_tone_stop(); + + shell_print(shell, "Tone stop"); + + return 0; +} + +static int cmd_hfclkaudio_drift_comp_enable(const struct shell *shell, size_t argc, + const char **argv) +{ + ARG_UNUSED(argc); + ARG_UNUSED(argv); + + ctrl_blk.drift_comp.enabled = true; + + shell_print(shell, "Audio PLL drift compensation enabled"); + + return 0; +} + +static int cmd_hfclkaudio_drift_comp_disable(const struct shell *shell, size_t argc, + const char **argv) +{ + ARG_UNUSED(argc); + ARG_UNUSED(argv); + + if (ctrl_blk.pres_comp.enabled) { + shell_print(shell, "Pres comp must be disabled to disable drift comp"); + } else { + ctrl_blk.drift_comp.enabled = false; + ctrl_blk.drift_comp.ctr = 0; + drift_comp_state_set(DRIFT_STATE_INIT); + + shell_print(shell, "Audio PLL drift compensation disabled"); + } + + return 0; +} + +static int cmd_audio_pres_comp_enable(const struct shell *shell, size_t argc, const char **argv) +{ + ARG_UNUSED(argc); + ARG_UNUSED(argv); + + if (ctrl_blk.drift_comp.enabled) { + ctrl_blk.pres_comp.enabled = true; + + shell_print(shell, "Presentation compensation enabled"); + } else { + shell_print(shell, "Drift comp must be enabled to enable pres comp"); + } + + return 0; +} + +static int cmd_audio_pres_comp_disable(const struct shell *shell, size_t argc, const char **argv) +{ + ARG_UNUSED(argc); + ARG_UNUSED(argv); + + ctrl_blk.pres_comp.enabled = false; + pres_comp_state_set(PRES_STATE_INIT); + + shell_print(shell, "Presentation compensation disabled"); + + return 0; +} + +SHELL_STATIC_SUBCMD_SET_CREATE(test_cmd, + SHELL_COND_CMD(CONFIG_SHELL, nrf_tone_start, NULL, + "Start local tone from nRF5340", cmd_i2s_tone_play), + SHELL_COND_CMD(CONFIG_SHELL, nrf_tone_stop, NULL, + "Stop local tone from nRF5340", cmd_i2s_tone_stop), + SHELL_COND_CMD(CONFIG_SHELL, pll_drift_comp_enable, NULL, + "Enable audio PLL auto drift compensation (default)", + cmd_hfclkaudio_drift_comp_enable), + SHELL_COND_CMD(CONFIG_SHELL, pll_drift_comp_disable, NULL, + "Disable audio PLL auto drift compensation", + cmd_hfclkaudio_drift_comp_disable), + SHELL_COND_CMD(CONFIG_SHELL, pll_pres_comp_enable, NULL, + "Enable audio presentation compensation (default)", + cmd_audio_pres_comp_enable), + SHELL_COND_CMD(CONFIG_SHELL, pll_pres_comp_disable, NULL, + "Disable audio presentation compensation", + cmd_audio_pres_comp_disable), + SHELL_SUBCMD_SET_END); + +SHELL_CMD_REGISTER(test, &test_cmd, "Test mode commands", NULL); diff --git a/src/audio/audio_datapath.h b/src/audio/audio_datapath.h new file mode 100644 index 0000000..36ad0f5 --- /dev/null +++ b/src/audio/audio_datapath.h @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2021, PACKETCRAFT, INC. + * + * SPDX-License-Identifier: LicenseRef-PCFT + */ + +#ifndef _AUDIO_DATAPATH_H_ +#define _AUDIO_DATAPATH_H_ + +#include +#include +#include +#include + +#include "sw_codec_select.h" + +/** + * @brief Mixes a tone into the I2S TX stream + * + * @param freq Tone frequency [Hz] + * @param dur_ms Tone duration [ms]. 0 = forever + * @param amplitude Tone amplitude [0, 1] + * + * @return 0 if successful, error otherwise + */ +int audio_datapath_tone_play(uint16_t freq, uint16_t dur_ms, float amplitude); + +/** + * @brief Stops tone playback + */ +void audio_datapath_tone_stop(void); + +/** + * @brief Set the presentation delay + * + * @param delay_us The presentation delay in µs + * + * @return 0 if successful, error otherwise + */ +int audio_datapath_pres_delay_us_set(uint32_t delay_us); + +/** + * @brief Get the current presentation delay + * + * @param delay_us The presentation delay in µs + */ +void audio_datapath_pres_delay_us_get(uint32_t *delay_us); + +/** + * @brief Input an audio data frame which is processed and outputted over I2S + * + * @note A frame of raw encoded audio data is inputted, and this data then is decoded + * and processed before being outputted over I2S. The audio is synchronized + * using sdu_ref_us + * + * @param buf Pointer to audio data frame + * @param size Size of audio data frame in bytes + * @param sdu_ref_us ISO timestamp reference from BLE controller + * @param bad_frame Indicating if the audio frame is bad or not + * @param recv_frame_ts_us Timestamp of when audio frame was received + */ +void audio_datapath_stream_out(const uint8_t *buf, size_t size, uint32_t sdu_ref_us, bool bad_frame, + uint32_t recv_frame_ts_us); + +/** + * @brief Start the audio datapath module + * + * @note The continuously running I2S is started + * + * @param fifo_rx Pointer to FIFO structure where I2S RX data is put + * + * @return 0 if successful, error otherwise + */ +int audio_datapath_start(struct data_fifo *fifo_rx); + +/** + * @brief Stop the audio datapath module + * + * @return 0 if successful, error otherwise + */ +int audio_datapath_stop(void); + +/** + * @brief Initialize the audio datapath module + * + * @return 0 if successful, error otherwise + */ +int audio_datapath_init(void); + +#endif /* _AUDIO_DATAPATH_H_ */ diff --git a/src/audio/audio_system.c b/src/audio/audio_system.c new file mode 100644 index 0000000..664067e --- /dev/null +++ b/src/audio/audio_system.c @@ -0,0 +1,530 @@ +/* + * Copyright (c) 2018 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "audio_system.h" + +#include +#include +#include +#include +#include +#include + +#include "macros_common.h" +#include "sw_codec_select.h" +#include "audio_datapath.h" +#include "audio_i2s.h" +#include "hw_codec.h" +#include "audio_usb.h" +#include "streamctrl.h" + +#include +LOG_MODULE_REGISTER(audio_system, CONFIG_AUDIO_SYSTEM_LOG_LEVEL); + +#define FIFO_TX_BLOCK_COUNT (CONFIG_FIFO_FRAME_SPLIT_NUM * CONFIG_FIFO_TX_FRAME_COUNT) +#define FIFO_RX_BLOCK_COUNT (CONFIG_FIFO_FRAME_SPLIT_NUM * CONFIG_FIFO_RX_FRAME_COUNT) + +#define DEBUG_INTERVAL_NUM 1000 +#define TEST_TONE_BASE_FREQ_HZ 1000 + +K_THREAD_STACK_DEFINE(encoder_thread_stack, CONFIG_ENCODER_STACK_SIZE); + +DATA_FIFO_DEFINE(fifo_tx, FIFO_TX_BLOCK_COUNT, WB_UP(BLOCK_SIZE_BYTES)); +DATA_FIFO_DEFINE(fifo_rx, FIFO_RX_BLOCK_COUNT, WB_UP(BLOCK_SIZE_BYTES)); + +static K_SEM_DEFINE(sem_encoder_start, 0, 1); + +static struct k_thread encoder_thread_data; +static k_tid_t encoder_thread_id; + +static struct k_poll_signal encoder_sig; + +static struct k_poll_event encoder_evt = + K_POLL_EVENT_INITIALIZER(K_POLL_TYPE_SIGNAL, K_POLL_MODE_NOTIFY_ONLY, &encoder_sig); + +static struct sw_codec_config sw_codec_cfg; +/* Buffer which can hold max 1 period test tone at 1000 Hz */ +static int16_t test_tone_buf[CONFIG_AUDIO_SAMPLE_RATE_HZ / 1000]; +static size_t test_tone_size; + +static bool sample_rate_valid(uint32_t sample_rate_hz) +{ + if (sample_rate_hz == 16000 || sample_rate_hz == 24000 || sample_rate_hz == 48000) { + return true; + } + + return false; +} + +static void audio_gateway_configure(void) +{ + if (IS_ENABLED(CONFIG_SW_CODEC_LC3)) { + sw_codec_cfg.sw_codec = SW_CODEC_LC3; + } else { + ERR_CHK_MSG(-EINVAL, "No codec selected"); + } + +#if (CONFIG_STREAM_BIDIRECTIONAL) + sw_codec_cfg.decoder.audio_ch = AUDIO_CHANNEL_DEFAULT; + sw_codec_cfg.decoder.num_ch = 1; + sw_codec_cfg.decoder.channel_mode = SW_CODEC_MONO; +#endif /* (CONFIG_STREAM_BIDIRECTIONAL) */ + + if (IS_ENABLED(CONFIG_MONO_TO_ALL_RECEIVERS)) { + sw_codec_cfg.encoder.num_ch = 1; + } else { + sw_codec_cfg.encoder.num_ch = 2; + } + + sw_codec_cfg.encoder.channel_mode = + (sw_codec_cfg.encoder.num_ch == 1) ? SW_CODEC_MONO : SW_CODEC_STEREO; +} + +static void audio_headset_configure(void) +{ + if (IS_ENABLED(CONFIG_SW_CODEC_LC3)) { + sw_codec_cfg.sw_codec = SW_CODEC_LC3; + } else { + ERR_CHK_MSG(-EINVAL, "No codec selected"); + } + +#if (CONFIG_STREAM_BIDIRECTIONAL) + sw_codec_cfg.decoder.audio_ch = AUDIO_CHANNEL_DEFAULT; + sw_codec_cfg.encoder.num_ch = 1; + sw_codec_cfg.encoder.channel_mode = SW_CODEC_MONO; +#endif /* (CONFIG_STREAM_BIDIRECTIONAL) */ + + channel_assignment_get(&sw_codec_cfg.decoder.audio_ch); + + sw_codec_cfg.decoder.num_ch = 1; + sw_codec_cfg.decoder.channel_mode = SW_CODEC_MONO; + + if (IS_ENABLED(CONFIG_SD_CARD_PLAYBACK)) { + /* Need an extra decoder channel to decode data from SD card */ + sw_codec_cfg.decoder.num_ch++; + } +} + +static void encoder_thread(void *arg1, void *arg2, void *arg3) +{ + int ret; + uint32_t blocks_alloced_num; + uint32_t blocks_locked_num; + + int debug_trans_count = 0; + size_t encoded_data_size = 0; + + void *tmp_pcm_raw_data[CONFIG_FIFO_FRAME_SPLIT_NUM]; + char pcm_raw_data[FRAME_SIZE_BYTES]; + + static uint8_t *encoded_data; + static size_t pcm_block_size; + static uint32_t test_tone_finite_pos; + + while (1) { + /* Don't start encoding until the stream needing it has started */ + ret = k_poll(&encoder_evt, 1, K_FOREVER); + + /* Get PCM data from I2S */ + /* Since one audio frame is divided into a number of + * blocks, we need to fetch the pointers to all of these + * blocks before copying it to a continuous area of memory + * before sending it to the encoder + */ + for (int i = 0; i < CONFIG_FIFO_FRAME_SPLIT_NUM; i++) { + ret = data_fifo_pointer_last_filled_get(&fifo_rx, &tmp_pcm_raw_data[i], + &pcm_block_size, K_FOREVER); + ERR_CHK(ret); + memcpy(pcm_raw_data + (i * BLOCK_SIZE_BYTES), tmp_pcm_raw_data[i], + pcm_block_size); + + data_fifo_block_free(&fifo_rx, tmp_pcm_raw_data[i]); + } + + if (sw_codec_cfg.encoder.enabled) { + if (test_tone_size) { + /* Test tone takes over audio stream */ + uint32_t num_bytes; + char tmp[FRAME_SIZE_BYTES / 2]; + + ret = contin_array_create(tmp, FRAME_SIZE_BYTES / 2, test_tone_buf, + test_tone_size, &test_tone_finite_pos); + ERR_CHK(ret); + + ret = pscm_copy_pad(tmp, FRAME_SIZE_BYTES / 2, + CONFIG_AUDIO_BIT_DEPTH_BITS, pcm_raw_data, + &num_bytes); + ERR_CHK(ret); + } + + ret = sw_codec_encode(pcm_raw_data, FRAME_SIZE_BYTES, &encoded_data, + &encoded_data_size); + + ERR_CHK_MSG(ret, "Encode failed"); + } + + /* Print block usage */ + if (debug_trans_count == DEBUG_INTERVAL_NUM) { + ret = data_fifo_num_used_get(&fifo_rx, &blocks_alloced_num, + &blocks_locked_num); + ERR_CHK(ret); + LOG_DBG(COLOR_CYAN "RX alloced: %d, locked: %d" COLOR_RESET, + blocks_alloced_num, blocks_locked_num); + debug_trans_count = 0; + } else { + debug_trans_count++; + } + + if (sw_codec_cfg.encoder.enabled) { + streamctrl_send(encoded_data, encoded_data_size, + sw_codec_cfg.encoder.num_ch); + } + STACK_USAGE_PRINT("encoder_thread", &encoder_thread_data); + } +} + +void audio_system_encoder_start(void) +{ + LOG_DBG("Encoder started"); + k_poll_signal_raise(&encoder_sig, 0); +} + +void audio_system_encoder_stop(void) +{ + k_poll_signal_reset(&encoder_sig); +} + +int audio_system_encode_test_tone_set(uint32_t freq) +{ + int ret; + + if (freq == 0) { + test_tone_size = 0; + return 0; + } + + if (IS_ENABLED(CONFIG_AUDIO_TEST_TONE)) { + ret = tone_gen(test_tone_buf, &test_tone_size, freq, CONFIG_AUDIO_SAMPLE_RATE_HZ, + 1); + ERR_CHK(ret); + } else { + LOG_ERR("Test tone is not enabled"); + return -ENXIO; + } + + if (test_tone_size > sizeof(test_tone_buf)) { + return -ENOMEM; + } + + return 0; +} + +int audio_system_encode_test_tone_step(void) +{ + int ret; + static uint32_t test_tone_hz; + + if (CONFIG_AUDIO_BIT_DEPTH_BITS != 16) { + LOG_WRN("Tone gen only supports 16 bits"); + return -ECANCELED; + } + + if (test_tone_hz == 0) { + test_tone_hz = TEST_TONE_BASE_FREQ_HZ; + } else if (test_tone_hz >= TEST_TONE_BASE_FREQ_HZ * 4) { + test_tone_hz = 0; + } else { + test_tone_hz = test_tone_hz * 2; + } + + if (test_tone_hz != 0) { + LOG_INF("Test tone set at %d Hz", test_tone_hz); + } else { + LOG_INF("Test tone off"); + } + + ret = audio_system_encode_test_tone_set(test_tone_hz); + if (ret) { + LOG_ERR("Failed to generate test tone"); + return ret; + } + + return 0; +} + +int audio_system_config_set(uint32_t encoder_sample_rate_hz, uint32_t encoder_bitrate, + uint32_t decoder_sample_rate_hz) +{ + if (sample_rate_valid(encoder_sample_rate_hz)) { + sw_codec_cfg.encoder.sample_rate_hz = encoder_sample_rate_hz; + } else if (encoder_sample_rate_hz) { + LOG_ERR("%d is not a valid sample rate", encoder_sample_rate_hz); + return -EINVAL; + } + + if (sample_rate_valid(decoder_sample_rate_hz)) { + sw_codec_cfg.decoder.enabled = true; + sw_codec_cfg.decoder.sample_rate_hz = decoder_sample_rate_hz; + } else if (decoder_sample_rate_hz) { + LOG_ERR("%d is not a valid sample rate", decoder_sample_rate_hz); + return -EINVAL; + } + + if (encoder_bitrate) { + sw_codec_cfg.encoder.enabled = true; + sw_codec_cfg.encoder.bitrate = encoder_bitrate; + } + + return 0; +} + +/* This function is only used on gateway using USB as audio source and bidirectional stream */ +int audio_system_decode(void const *const encoded_data, size_t encoded_data_size, bool bad_frame) +{ + int ret; + uint32_t blocks_alloced_num; + uint32_t blocks_locked_num; + static int debug_trans_count; + static void *tmp_pcm_raw_data[CONFIG_FIFO_FRAME_SPLIT_NUM]; + static void *pcm_raw_data; + size_t pcm_block_size; + + if (!sw_codec_cfg.initialized) { + /* Throw away data */ + /* This can happen when using play/pause since there might be + * some packages left in the buffers + */ + LOG_DBG("Trying to decode while codec is not initialized"); + return -EPERM; + } + + ret = data_fifo_num_used_get(&fifo_tx, &blocks_alloced_num, &blocks_locked_num); + if (ret) { + return ret; + } + + uint8_t free_blocks_num = FIFO_TX_BLOCK_COUNT - blocks_locked_num; + + /* If not enough space for a full frame, remove oldest samples to make room */ + if (free_blocks_num < CONFIG_FIFO_FRAME_SPLIT_NUM) { + void *old_data; + size_t size; + + for (int i = 0; i < (CONFIG_FIFO_FRAME_SPLIT_NUM - free_blocks_num); i++) { + ret = data_fifo_pointer_last_filled_get(&fifo_tx, &old_data, &size, + K_NO_WAIT); + if (ret == -ENOMSG) { + /* If there are no more blocks in FIFO, break */ + break; + } + + data_fifo_block_free(&fifo_tx, old_data); + } + } + + for (int i = 0; i < CONFIG_FIFO_FRAME_SPLIT_NUM; i++) { + ret = data_fifo_pointer_first_vacant_get(&fifo_tx, &tmp_pcm_raw_data[i], K_FOREVER); + if (ret) { + return ret; + } + } + + ret = sw_codec_decode(encoded_data, encoded_data_size, bad_frame, &pcm_raw_data, + &pcm_block_size); + if (ret) { + LOG_ERR("Failed to decode"); + return ret; + } + + /* Split decoded frame into CONFIG_FIFO_FRAME_SPLIT_NUM blocks */ + for (int i = 0; i < CONFIG_FIFO_FRAME_SPLIT_NUM; i++) { + memcpy(tmp_pcm_raw_data[i], (char *)pcm_raw_data + (i * (BLOCK_SIZE_BYTES)), + BLOCK_SIZE_BYTES); + + ret = data_fifo_block_lock(&fifo_tx, &tmp_pcm_raw_data[i], BLOCK_SIZE_BYTES); + if (ret) { + LOG_ERR("Failed to lock block"); + return ret; + } + } + if (debug_trans_count == DEBUG_INTERVAL_NUM) { + ret = data_fifo_num_used_get(&fifo_tx, &blocks_alloced_num, &blocks_locked_num); + if (ret) { + return ret; + } + LOG_DBG(COLOR_MAGENTA "TX alloced: %d, locked: %d" COLOR_RESET, blocks_alloced_num, + blocks_locked_num); + debug_trans_count = 0; + } else { + debug_trans_count++; + } + + return 0; +} + +/**@brief Initializes the FIFOs, the codec, and starts the I2S + */ +void audio_system_start(void) +{ + int ret; + + if (CONFIG_AUDIO_DEV == HEADSET) { + audio_headset_configure(); + } else if (CONFIG_AUDIO_DEV == GATEWAY) { + audio_gateway_configure(); + } else { + LOG_ERR("Invalid CONFIG_AUDIO_DEV: %d", CONFIG_AUDIO_DEV); + ERR_CHK(-EINVAL); + } + + if (!fifo_tx.initialized) { + ret = data_fifo_init(&fifo_tx); + ERR_CHK_MSG(ret, "Failed to set up tx FIFO"); + } + + if (!fifo_rx.initialized) { + ret = data_fifo_init(&fifo_rx); + ERR_CHK_MSG(ret, "Failed to set up rx FIFO"); + } + + ret = sw_codec_init(sw_codec_cfg); + ERR_CHK_MSG(ret, "Failed to set up codec"); + + sw_codec_cfg.initialized = true; + + if (sw_codec_cfg.encoder.enabled && encoder_thread_id == NULL) { + encoder_thread_id = k_thread_create( + &encoder_thread_data, encoder_thread_stack, CONFIG_ENCODER_STACK_SIZE, + (k_thread_entry_t)encoder_thread, NULL, NULL, NULL, + K_PRIO_PREEMPT(CONFIG_ENCODER_THREAD_PRIO), 0, K_NO_WAIT); + ret = k_thread_name_set(encoder_thread_id, "ENCODER"); + ERR_CHK(ret); + } + +#if ((CONFIG_AUDIO_SOURCE_USB) && (CONFIG_AUDIO_DEV == GATEWAY)) + ret = audio_usb_start(&fifo_tx, &fifo_rx); + ERR_CHK(ret); +#else + ret = hw_codec_default_conf_enable(); + ERR_CHK(ret); + + ret = audio_datapath_start(&fifo_rx); + ERR_CHK(ret); +#endif /* ((CONFIG_AUDIO_SOURCE_USB) && (CONFIG_AUDIO_DEV == GATEWAY))) */ +} + +void audio_system_stop(void) +{ + int ret; + + if (!sw_codec_cfg.initialized) { + LOG_WRN("Codec already unitialized"); + return; + } + + LOG_DBG("Stopping codec"); + +#if ((CONFIG_AUDIO_DEV == GATEWAY) && CONFIG_AUDIO_SOURCE_USB) + audio_usb_stop(); +#else + ret = hw_codec_soft_reset(); + ERR_CHK(ret); + + ret = audio_datapath_stop(); + ERR_CHK(ret); +#endif /* ((CONFIG_AUDIO_DEV == GATEWAY) && CONFIG_AUDIO_SOURCE_USB) */ + + ret = sw_codec_uninit(sw_codec_cfg); + ERR_CHK_MSG(ret, "Failed to uninit codec"); + sw_codec_cfg.initialized = false; + + data_fifo_empty(&fifo_rx); + data_fifo_empty(&fifo_tx); +} + +int audio_system_fifo_rx_block_drop(void) +{ + int ret; + void *temp; + size_t temp_size; + + ret = data_fifo_pointer_last_filled_get(&fifo_rx, &temp, &temp_size, K_NO_WAIT); + if (ret) { + LOG_WRN("Failed to get last filled block"); + return -ECANCELED; + } + + data_fifo_block_free(&fifo_rx, temp); + + LOG_DBG("Block dropped"); + return 0; +} + +int audio_system_decoder_num_ch_get(void) +{ + return sw_codec_cfg.decoder.num_ch; +} + +int audio_system_init(void) +{ + int ret; + +#if ((CONFIG_AUDIO_DEV == GATEWAY) && (CONFIG_AUDIO_SOURCE_USB)) + ret = audio_usb_init(); + if (ret) { + LOG_ERR("Failed to initialize USB: %d", ret); + return ret; + } +#else + ret = audio_datapath_init(); + if (ret) { + LOG_ERR("Failed to initialize audio datapath: %d", ret); + return ret; + } + + ret = hw_codec_init(); + if (ret) { + LOG_ERR("Failed to initialize HW codec: %d", ret); + return ret; + } +#endif + k_poll_signal_init(&encoder_sig); + + return 0; +} + +static int cmd_audio_system_start(const struct shell *shell, size_t argc, const char **argv) +{ + ARG_UNUSED(argc); + ARG_UNUSED(argv); + + audio_system_start(); + + shell_print(shell, "Audio system started"); + + return 0; +} + +static int cmd_audio_system_stop(const struct shell *shell, size_t argc, const char **argv) +{ + ARG_UNUSED(argc); + ARG_UNUSED(argv); + + audio_system_stop(); + + shell_print(shell, "Audio system stopped"); + + return 0; +} + +SHELL_STATIC_SUBCMD_SET_CREATE(audio_system_cmd, + SHELL_COND_CMD(CONFIG_SHELL, start, NULL, "Start the audio system", + cmd_audio_system_start), + SHELL_COND_CMD(CONFIG_SHELL, stop, NULL, "Stop the audio system", + cmd_audio_system_stop), + SHELL_SUBCMD_SET_END); + +SHELL_CMD_REGISTER(audio_system, &audio_system_cmd, "Audio system commands", NULL); diff --git a/src/audio/audio_system.h b/src/audio/audio_system.h new file mode 100644 index 0000000..ba6bebf --- /dev/null +++ b/src/audio/audio_system.h @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2018 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _AUDIO_SYSTEM_H_ +#define _AUDIO_SYSTEM_H_ + +#include +#include +#include + +#define VALUE_NOT_SET 0 + +/** + * @brief Start the execution of the encoder thread. + */ +void audio_system_encoder_start(void); + +/** + * @brief Stop the encoder thread from executing. + * + * @note Using this allows the encode thread to always be enabled, + * but disables the execution when not needed, saving power. + */ +void audio_system_encoder_stop(void); + +/** + * @brief Toggle a test tone on and off. + * + * @note A stream must already be running to use this feature. + * + * @param[in] freq Desired frequency of tone. Off if set to 0. + * + * @retval -ENOMEM The frequency is too low (buffer overflow). + * @retval 0 Success. + */ +int audio_system_encode_test_tone_set(uint32_t freq); + +/** + * @brief Step through different test tones. + * + * @note A stream must already be running to use this feature. + * Will step through test tones: 1 kHz, 2 kHz, 4 kHz and off. + * + * @return 0 on success, error otherwise. + */ +int audio_system_encode_test_tone_step(void); + +/** + * @brief Set the sample rates for the encoder and the decoder, and the bit rate for encoder. + * + * @note If any of the values are 0, the corresponding configuration will not be set. + * + * @param[in] encoder_sample_rate_hz Sample rate to be used by the encoder; can be 0. + * @param[in] encoder_bitrate Bit rate to be used by the encoder (bps); can be 0. + * @param[in] decoder_sample_rate_hz Sample rate to be used by the decoder; can be 0. + * + * @retval -EINVAL Invalid sample rate given. + * @retval 0 On success. + */ +int audio_system_config_set(uint32_t encoder_sample_rate_hz, uint32_t encoder_bitrate, + uint32_t decoder_sample_rate_hz); + +/** + * @brief Decode data and then add it to TX FIFO buffer. + * + * @param[in] encoded_data Pointer to encoded data. + * @param[in] encoded_data_size Size of encoded data. + * @param[in] bad_frame Indication on missed or incomplete frame. + * + * @return 0 on success, error otherwise. + */ +int audio_system_decode(void const *const encoded_data, size_t encoded_data_size, bool bad_frame); + +/** + * @brief Initialize and start both HW and SW audio codec. + */ +void audio_system_start(void); + +/** + * @brief Stop all activities related to audio. + */ +void audio_system_stop(void); + +/** + * @brief Drop oldest block from the fifo_rx buffer. + * + * @note This can be used to reduce latency by adjusting the timing of the completed frame + * that was sampled in relation to the connection interval in Bluetooth LE. + * + * @return 0 on success, -ECANCELED otherwise. + */ +int audio_system_fifo_rx_block_drop(void); + +/** + * @brief Get number of decoder channels. + * + * @return Number of decoder channels. + */ +int audio_system_decoder_num_ch_get(void); + +/** + * @brief Initialize the audio_system. + * + * @return 0 on success, error otherwise. + */ +int audio_system_init(void); + +#endif /* _AUDIO_SYSTEM_H_ */ diff --git a/src/audio/le_audio_rx.c b/src/audio/le_audio_rx.c new file mode 100644 index 0000000..8c7c937 --- /dev/null +++ b/src/audio/le_audio_rx.c @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include +#include + +#include "streamctrl.h" +#include "audio_datapath.h" +#include "macros_common.h" +#include "audio_system.h" +#include "audio_sync_timer.h" + +#include +LOG_MODULE_REGISTER(le_audio_rx, CONFIG_LE_AUDIO_RX_LOG_LEVEL); + +struct ble_iso_data { + uint8_t data[CONFIG_BT_ISO_RX_MTU]; + size_t data_size; + bool bad_frame; + uint32_t sdu_ref; + uint32_t recv_frame_ts; +} __packed; + +struct rx_stats { + uint32_t recv_cnt; + uint32_t bad_frame_cnt; + uint32_t data_size_mismatch_cnt; +}; + +static bool initialized; +static struct k_thread audio_datapath_thread_data; +static k_tid_t audio_datapath_thread_id; +K_THREAD_STACK_DEFINE(audio_datapath_thread_stack, CONFIG_AUDIO_DATAPATH_STACK_SIZE); + +DATA_FIFO_DEFINE(ble_fifo_rx, CONFIG_BUF_BLE_RX_PACKET_NUM, WB_UP(sizeof(struct ble_iso_data))); + +/* Callback for handling ISO RX */ +void le_audio_rx_data_handler(uint8_t const *const p_data, size_t data_size, bool bad_frame, + uint32_t sdu_ref, enum audio_channel channel_index, + size_t desired_data_size) +{ + int ret; + uint32_t blocks_alloced_num, blocks_locked_num; + struct ble_iso_data *iso_received = NULL; + static struct rx_stats rx_stats[AUDIO_CH_NUM]; + static uint32_t num_overruns; + static uint32_t num_thrown; + + if (!initialized) { + ERR_CHK_MSG(-EPERM, "Data received but le_audio_rx is not initialized"); + } + + /* Capture timestamp of when audio frame is received */ + uint32_t recv_frame_ts = audio_sync_timer_capture(); + + rx_stats[channel_index].recv_cnt++; + + if (data_size != desired_data_size) { + /* A valid frame should always be equal to desired_data_size, set bad_frame + * if that is not the case + */ + bad_frame = true; + rx_stats[channel_index].data_size_mismatch_cnt++; + } + + if (bad_frame) { + rx_stats[channel_index].bad_frame_cnt++; + } + + if ((rx_stats[channel_index].recv_cnt % 100) == 0 && rx_stats[channel_index].recv_cnt) { + /* NOTE: The string below is used by the Nordic CI system */ + LOG_DBG("ISO RX SDUs: Ch: %d Total: %d Bad: %d Size mismatch %d", channel_index, + rx_stats[channel_index].recv_cnt, rx_stats[channel_index].bad_frame_cnt, + rx_stats[channel_index].data_size_mismatch_cnt); + } + + if (stream_state_get() != STATE_STREAMING) { + /* Throw away data */ + num_thrown++; + if ((num_thrown % 100) == 1) { + LOG_WRN("Not in streaming state (%d), thrown %d packet(s)", + stream_state_get(), num_thrown); + } + return; + } + + if (channel_index != AUDIO_CH_L && (CONFIG_AUDIO_DEV == GATEWAY)) { + /* Only left channel RX data in use on gateway */ + return; + } + + ret = data_fifo_num_used_get(&ble_fifo_rx, &blocks_alloced_num, &blocks_locked_num); + ERR_CHK(ret); + + if (blocks_alloced_num >= CONFIG_BUF_BLE_RX_PACKET_NUM) { + /* FIFO buffer is full, swap out oldest frame for a new one */ + + void *stale_data; + size_t stale_size; + num_overruns++; + + if ((num_overruns % 100) == 1) { + LOG_WRN("BLE ISO RX overrun: Num: %d", num_overruns); + } + + ret = data_fifo_pointer_last_filled_get(&ble_fifo_rx, &stale_data, &stale_size, + K_NO_WAIT); + ERR_CHK(ret); + + data_fifo_block_free(&ble_fifo_rx, stale_data); + } + + ret = data_fifo_pointer_first_vacant_get(&ble_fifo_rx, (void *)&iso_received, K_NO_WAIT); + ERR_CHK_MSG(ret, "Unable to get FIFO pointer"); + + if (data_size > ARRAY_SIZE(iso_received->data)) { + ERR_CHK_MSG(-ENOMEM, "Data size too large for buffer"); + return; + } + + memcpy(iso_received->data, p_data, data_size); + + iso_received->bad_frame = bad_frame; + iso_received->data_size = data_size; + iso_received->sdu_ref = sdu_ref; + iso_received->recv_frame_ts = recv_frame_ts; + + ret = data_fifo_block_lock(&ble_fifo_rx, (void *)&iso_received, + sizeof(struct ble_iso_data)); + ERR_CHK_MSG(ret, "Failed to lock block"); +} + +/** + * @brief Receive data from BLE through a k_fifo and send to USB or audio datapath. + */ +static void audio_datapath_thread(void *dummy1, void *dummy2, void *dummy3) +{ + int ret; + struct ble_iso_data *iso_received = NULL; + size_t iso_received_size; + + while (1) { + ret = data_fifo_pointer_last_filled_get(&ble_fifo_rx, (void *)&iso_received, + &iso_received_size, K_FOREVER); + ERR_CHK(ret); + + if (IS_ENABLED(CONFIG_AUDIO_SOURCE_USB) && (CONFIG_AUDIO_DEV == GATEWAY)) { + ret = audio_system_decode(iso_received->data, iso_received->data_size, + iso_received->bad_frame); + ERR_CHK(ret); + } else { + audio_datapath_stream_out(iso_received->data, iso_received->data_size, + iso_received->sdu_ref, iso_received->bad_frame, + iso_received->recv_frame_ts); + } + data_fifo_block_free(&ble_fifo_rx, (void *)iso_received); + + STACK_USAGE_PRINT("audio_datapath_thread", &audio_datapath_thread_data); + } +} + +static int audio_datapath_thread_create(void) +{ + int ret; + + audio_datapath_thread_id = k_thread_create( + &audio_datapath_thread_data, audio_datapath_thread_stack, + CONFIG_AUDIO_DATAPATH_STACK_SIZE, (k_thread_entry_t)audio_datapath_thread, NULL, + NULL, NULL, K_PRIO_PREEMPT(CONFIG_AUDIO_DATAPATH_THREAD_PRIO), 0, K_NO_WAIT); + ret = k_thread_name_set(audio_datapath_thread_id, "AUDIO_DATAPATH"); + if (ret) { + LOG_ERR("Failed to create audio_datapath thread"); + return ret; + } + + return 0; +} + +int le_audio_rx_init(void) +{ + int ret; + + if (initialized) { + return -EALREADY; + } + + ret = data_fifo_init(&ble_fifo_rx); + if (ret) { + LOG_ERR("Failed to set up ble_rx FIFO"); + return ret; + } + + ret = audio_datapath_thread_create(); + if (ret) { + return ret; + } + + initialized = true; + + return 0; +} diff --git a/src/audio/le_audio_rx.h b/src/audio/le_audio_rx.h new file mode 100644 index 0000000..b50e10c --- /dev/null +++ b/src/audio/le_audio_rx.h @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _LE_AUDIO_RX_H_ +#define _LE_AUDIO_RX_H_ + +/** + * @brief Data handler when ISO data has been received. + * + * @param[in] p_data Pointer to the received data. + * @param[in] data_size Size of the received data. + * @param[in] bad_frame Bad frame flag. (I.e. set for missed ISO data). + * @param[in] sdu_ref SDU reference timestamp. + * @param[in] channel_index Which channel is received. + * @param[in] desired_data_size The expected data size. + */ +void le_audio_rx_data_handler(uint8_t const *const p_data, size_t data_size, bool bad_frame, + uint32_t sdu_ref, enum audio_channel channel_index, + size_t desired_data_size); + +/** + * @brief Initialize the receive audio path. + * + * @return 0 if successful, error otherwise. + */ +int le_audio_rx_init(void); + +#endif /* _LE_AUDIO_RX_H_ */ diff --git a/src/audio/streamctrl.h b/src/audio/streamctrl.h new file mode 100644 index 0000000..d96b7c9 --- /dev/null +++ b/src/audio/streamctrl.h @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2018 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _STREAMCTRL_H_ +#define _STREAMCTRL_H_ + +#include +#include + +/* State machine states for peer or stream. */ +enum stream_state { + STATE_STREAMING, + STATE_PAUSED, +}; + +/** + * @brief Get the current streaming state. + * + * @return strm_state enum value. + */ +uint8_t stream_state_get(void); + +/** + * @brief Send audio data over the stream. + * + * @param data Data to send. + * @param size Size of data. + * @param num_ch Number of audio channels. + */ +void streamctrl_send(void const *const data, size_t size, uint8_t num_ch); + +#endif /* _STREAMCTRL_H_ */ diff --git a/src/audio/sw_codec_select.c b/src/audio/sw_codec_select.c new file mode 100644 index 0000000..5b93871 --- /dev/null +++ b/src/audio/sw_codec_select.c @@ -0,0 +1,464 @@ +/* + * Copyright (c) 2018 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "sw_codec_select.h" + +#include +#include +#include +#include + +#if (CONFIG_SW_CODEC_LC3) +#include "sw_codec_lc3.h" +#endif /* (CONFIG_SW_CODEC_LC3) */ + +#include +LOG_MODULE_REGISTER(sw_codec_select, CONFIG_SW_CODEC_SELECT_LOG_LEVEL); + +static struct sw_codec_config m_config; + +static struct sample_rate_converter_ctx encoder_converters[AUDIO_CH_NUM]; +static struct sample_rate_converter_ctx decoder_converters[AUDIO_CH_NUM]; + +/** + * @brief Converts the sample rate of the uncompressed audio stream if needed. + * + * @details Two buffers must be made available for the function: the input_data buffer that + * contains the samples for the audio stream, and the conversion buffer that will be + * used to store the converted audio stream. data_ptr will point to conversion_buffer + * if a conversion took place; otherwise, it will point to input_data. + * + * @param[in] ctx Sample rate converter context. + * @param[in] input_sample_rate Input sample rate. + * @param[in] output_sample_rate Output sample rate. + * @param[in] input_data Data coming in. Buffer is assumed to be of size + * PCM_NUM_BYTES_MONO. + * @param[in] input_data_size Size of input data. + * @param[in] conversion_buffer Buffer to perform sample rate conversion. Must be of size + * PCM_NUM_BYTES_MONO. + * @param[out] data_ptr Pointer to the data to be used from this point on. + * Will point to either @p input_data or @p conversion_buffer. + * @param[out] output_size Number of bytes out. + * + * @retval -ENOTSUP Sample rates are not equal, and the sample rate conversion has not + *been enabled in the application. + * @retval 0 Success. + */ +static int sw_codec_sample_rate_convert(struct sample_rate_converter_ctx *ctx, + uint32_t input_sample_rate, uint32_t output_sample_rate, + char *input_data, size_t input_data_size, + char *conversion_buffer, char **data_ptr, + size_t *output_size) +{ + int ret; + + if (input_sample_rate == output_sample_rate) { + *data_ptr = input_data; + *output_size = input_data_size; + } else if (IS_ENABLED(CONFIG_SAMPLE_RATE_CONVERTER)) { + ret = sample_rate_converter_process(ctx, SAMPLE_RATE_FILTER_SIMPLE, input_data, + input_data_size, input_sample_rate, + conversion_buffer, PCM_NUM_BYTES_MONO, + output_size, output_sample_rate); + if (ret) { + LOG_ERR("Failed to convert sample rate: %d", ret); + return ret; + } + + *data_ptr = conversion_buffer; + } else { + LOG_ERR("Sample rates are not equal, and sample rate conversion has not been " + "enabled in the application."); + return -ENOTSUP; + } + + return 0; +} + +bool sw_codec_is_initialized(void) +{ + return m_config.initialized; +} + +int sw_codec_encode(void *pcm_data, size_t pcm_size, uint8_t **encoded_data, size_t *encoded_size) +{ + int ret; + + /* Temp storage for split stereo PCM signal */ + char pcm_data_mono_system_sample_rate[AUDIO_CH_NUM][PCM_NUM_BYTES_MONO] = {0}; + /* Make sure we have enough space for two frames (stereo) */ + static uint8_t m_encoded_data[ENC_MAX_FRAME_SIZE * AUDIO_CH_NUM]; + + char pcm_data_mono_converted_buf[AUDIO_CH_NUM][PCM_NUM_BYTES_MONO] = {0}; + + size_t pcm_block_size_mono_system_sample_rate; + size_t pcm_block_size_mono; + + if (!m_config.encoder.enabled) { + LOG_ERR("Encoder has not been initialized"); + return -ENXIO; + } + + switch (m_config.sw_codec) { + case SW_CODEC_LC3: { +#if (CONFIG_SW_CODEC_LC3) + uint16_t encoded_bytes_written; + char *pcm_data_mono_ptrs[m_config.encoder.channel_mode]; + + /* Since LC3 is a single channel codec, we must split the + * stereo PCM stream + */ + ret = pscm_two_channel_split(pcm_data, pcm_size, CONFIG_AUDIO_BIT_DEPTH_BITS, + pcm_data_mono_system_sample_rate[AUDIO_CH_L], + pcm_data_mono_system_sample_rate[AUDIO_CH_R], + &pcm_block_size_mono_system_sample_rate); + if (ret) { + return ret; + } + + for (int i = 0; i < m_config.encoder.channel_mode; ++i) { + ret = sw_codec_sample_rate_convert( + &encoder_converters[i], CONFIG_AUDIO_SAMPLE_RATE_HZ, + m_config.encoder.sample_rate_hz, + pcm_data_mono_system_sample_rate[i], + pcm_block_size_mono_system_sample_rate, + pcm_data_mono_converted_buf[i], &pcm_data_mono_ptrs[i], + &pcm_block_size_mono); + if (ret) { + LOG_ERR("Sample rate conversion failed for channel %d: %d", i, ret); + return ret; + } + } + + switch (m_config.encoder.channel_mode) { + case SW_CODEC_MONO: { + ret = sw_codec_lc3_enc_run(pcm_data_mono_ptrs[AUDIO_CH_L], + pcm_block_size_mono, LC3_USE_BITRATE_FROM_INIT, + 0, sizeof(m_encoded_data), m_encoded_data, + &encoded_bytes_written); + if (ret) { + return ret; + } + break; + } + case SW_CODEC_STEREO: { + ret = sw_codec_lc3_enc_run(pcm_data_mono_ptrs[AUDIO_CH_L], + pcm_block_size_mono, LC3_USE_BITRATE_FROM_INIT, + AUDIO_CH_L, sizeof(m_encoded_data), + m_encoded_data, &encoded_bytes_written); + if (ret) { + return ret; + } + + ret = sw_codec_lc3_enc_run( + pcm_data_mono_ptrs[AUDIO_CH_R], pcm_block_size_mono, + LC3_USE_BITRATE_FROM_INIT, AUDIO_CH_R, + sizeof(m_encoded_data) - encoded_bytes_written, + m_encoded_data + encoded_bytes_written, &encoded_bytes_written); + if (ret) { + return ret; + } + encoded_bytes_written += encoded_bytes_written; + break; + } + default: + LOG_ERR("Unsupported channel mode for encoder: %d", + m_config.encoder.channel_mode); + return -ENODEV; + } + + *encoded_data = m_encoded_data; + *encoded_size = encoded_bytes_written; + +#endif /* (CONFIG_SW_CODEC_LC3) */ + break; + } + default: + LOG_ERR("Unsupported codec: %d", m_config.sw_codec); + return -ENODEV; + } + + return 0; +} + +int sw_codec_decode(uint8_t const *const encoded_data, size_t encoded_size, bool bad_frame, + void **decoded_data, size_t *decoded_size) +{ + if (!m_config.decoder.enabled) { + LOG_ERR("Decoder has not been initialized"); + return -ENXIO; + } + + int ret; + + static char pcm_data_stereo[PCM_NUM_BYTES_STEREO]; + + char decoded_data_mono[AUDIO_CH_NUM][PCM_NUM_BYTES_MONO] = {0}; + char decoded_data_mono_system_sample_rate[AUDIO_CH_NUM][PCM_NUM_BYTES_MONO] = {0}; + + size_t pcm_size_stereo = 0; + size_t pcm_size_mono = 0; + size_t decoded_data_size = 0; + + switch (m_config.sw_codec) { + case SW_CODEC_LC3: { +#if (CONFIG_SW_CODEC_LC3) + char *pcm_in_data_ptrs[m_config.decoder.channel_mode]; + + switch (m_config.decoder.channel_mode) { + case SW_CODEC_MONO: { + if (bad_frame && IS_ENABLED(CONFIG_SW_CODEC_OVERRIDE_PLC)) { + memset(decoded_data_mono[AUDIO_CH_L], 0, PCM_NUM_BYTES_MONO); + decoded_data_size = PCM_NUM_BYTES_MONO; + } else { + ret = sw_codec_lc3_dec_run( + encoded_data, encoded_size, LC3_PCM_NUM_BYTES_MONO, 0, + decoded_data_mono[AUDIO_CH_L], + (uint16_t *)&decoded_data_size, bad_frame); + if (ret) { + return ret; + } + + ret = sw_codec_sample_rate_convert( + &decoder_converters[AUDIO_CH_L], + m_config.decoder.sample_rate_hz, + CONFIG_AUDIO_SAMPLE_RATE_HZ, decoded_data_mono[AUDIO_CH_L], + decoded_data_size, + decoded_data_mono_system_sample_rate[AUDIO_CH_L], + &pcm_in_data_ptrs[AUDIO_CH_L], &pcm_size_mono); + if (ret) { + LOG_ERR("Sample rate conversion failed for mono: %d", ret); + return ret; + } + } + + /* For now, i2s is only stereo, so in order to send + * just one channel, we need to insert 0 for the + * other channel + */ + ret = pscm_zero_pad(pcm_in_data_ptrs[AUDIO_CH_L], pcm_size_mono, + m_config.decoder.audio_ch, CONFIG_AUDIO_BIT_DEPTH_BITS, + pcm_data_stereo, &pcm_size_stereo); + if (ret) { + return ret; + } + break; + } + case SW_CODEC_STEREO: { + if (bad_frame && IS_ENABLED(CONFIG_SW_CODEC_OVERRIDE_PLC)) { + memset(decoded_data_mono[AUDIO_CH_L], 0, PCM_NUM_BYTES_MONO); + memset(decoded_data_mono[AUDIO_CH_R], 0, PCM_NUM_BYTES_MONO); + decoded_data_size = PCM_NUM_BYTES_MONO; + } else { + /* Decode left channel */ + ret = sw_codec_lc3_dec_run( + encoded_data, encoded_size / 2, LC3_PCM_NUM_BYTES_MONO, + AUDIO_CH_L, decoded_data_mono[AUDIO_CH_L], + (uint16_t *)&decoded_data_size, bad_frame); + if (ret) { + return ret; + } + + /* Decode right channel */ + ret = sw_codec_lc3_dec_run( + (encoded_data + (encoded_size / 2)), encoded_size / 2, + LC3_PCM_NUM_BYTES_MONO, AUDIO_CH_R, + decoded_data_mono[AUDIO_CH_R], + (uint16_t *)&decoded_data_size, bad_frame); + if (ret) { + return ret; + } + + for (int i = 0; i < m_config.decoder.channel_mode; ++i) { + ret = sw_codec_sample_rate_convert( + &decoder_converters[i], + m_config.decoder.sample_rate_hz, + CONFIG_AUDIO_SAMPLE_RATE_HZ, decoded_data_mono[i], + decoded_data_size, + decoded_data_mono_system_sample_rate[i], + &pcm_in_data_ptrs[i], &pcm_size_mono); + if (ret) { + LOG_ERR("Sample rate conversion failed for channel " + "%d : %d", + i, ret); + return ret; + } + } + } + + ret = pscm_combine(pcm_in_data_ptrs[AUDIO_CH_L], + pcm_in_data_ptrs[AUDIO_CH_R], pcm_size_mono, + CONFIG_AUDIO_BIT_DEPTH_BITS, pcm_data_stereo, + &pcm_size_stereo); + if (ret) { + return ret; + } + break; + } + default: + LOG_ERR("Unsupported channel mode for decoder: %d", + m_config.decoder.channel_mode); + return -ENODEV; + } + + *decoded_size = pcm_size_stereo; + *decoded_data = pcm_data_stereo; +#endif /* (CONFIG_SW_CODEC_LC3) */ + break; + } + default: + LOG_ERR("Unsupported codec: %d", m_config.sw_codec); + return -ENODEV; + } + return 0; +} + +int sw_codec_uninit(struct sw_codec_config sw_codec_cfg) +{ + int ret; + + if (m_config.sw_codec != sw_codec_cfg.sw_codec) { + LOG_ERR("Trying to uninit a codec that is not first initialized"); + return -ENODEV; + } + switch (m_config.sw_codec) { + case SW_CODEC_LC3: +#if (CONFIG_SW_CODEC_LC3) + if (sw_codec_cfg.encoder.enabled) { + if (!m_config.encoder.enabled) { + LOG_ERR("Trying to uninit encoder, it has not been " + "initialized"); + return -EALREADY; + } + ret = sw_codec_lc3_enc_uninit_all(); + if (ret) { + return ret; + } + m_config.encoder.enabled = false; + } + + if (sw_codec_cfg.decoder.enabled) { + if (!m_config.decoder.enabled) { + LOG_WRN("Trying to uninit decoder, it has not been " + "initialized"); + return -EALREADY; + } + + ret = sw_codec_lc3_dec_uninit_all(); + if (ret) { + return ret; + } + m_config.decoder.enabled = false; + } +#endif /* (CONFIG_SW_CODEC_LC3) */ + break; + default: + LOG_ERR("Unsupported codec: %d", m_config.sw_codec); + return false; + } + + m_config.initialized = false; + + return 0; +} + +int sw_codec_init(struct sw_codec_config sw_codec_cfg) +{ + int ret; + + switch (sw_codec_cfg.sw_codec) { + case SW_CODEC_LC3: { +#if (CONFIG_SW_CODEC_LC3) + if (m_config.sw_codec != SW_CODEC_LC3) { + /* Check if LC3 is already initialized */ + ret = sw_codec_lc3_init(NULL, NULL, CONFIG_AUDIO_FRAME_DURATION_US); + if (ret) { + return ret; + } + } + + if (sw_codec_cfg.encoder.enabled) { + if (m_config.encoder.enabled) { + LOG_WRN("The LC3 encoder is already initialized"); + return -EALREADY; + } + uint16_t pcm_bytes_req_enc; + + LOG_DBG("Encode: %dHz %dbits %dus %dbps %d channel(s)", + sw_codec_cfg.encoder.sample_rate_hz, CONFIG_AUDIO_BIT_DEPTH_BITS, + CONFIG_AUDIO_FRAME_DURATION_US, sw_codec_cfg.encoder.bitrate, + sw_codec_cfg.encoder.num_ch); + + ret = sw_codec_lc3_enc_init( + sw_codec_cfg.encoder.sample_rate_hz, CONFIG_AUDIO_BIT_DEPTH_BITS, + CONFIG_AUDIO_FRAME_DURATION_US, sw_codec_cfg.encoder.bitrate, + sw_codec_cfg.encoder.num_ch, &pcm_bytes_req_enc); + + if (ret) { + return ret; + } + } + + if (sw_codec_cfg.decoder.enabled) { + if (m_config.decoder.enabled) { + LOG_WRN("The LC3 decoder is already initialized"); + return -EALREADY; + } + + LOG_DBG("Decode: %dHz %dbits %dus %d channel(s)", + sw_codec_cfg.decoder.sample_rate_hz, CONFIG_AUDIO_BIT_DEPTH_BITS, + CONFIG_AUDIO_FRAME_DURATION_US, sw_codec_cfg.decoder.num_ch); + + ret = sw_codec_lc3_dec_init( + sw_codec_cfg.decoder.sample_rate_hz, CONFIG_AUDIO_BIT_DEPTH_BITS, + CONFIG_AUDIO_FRAME_DURATION_US, sw_codec_cfg.decoder.num_ch); + + if (ret) { + return ret; + } + } + break; +#else + LOG_ERR("LC3 is not compiled in, please open menuconfig and select " + "LC3"); + return -ENODEV; +#endif /* (CONFIG_SW_CODEC_LC3) */ + } + + default: + LOG_ERR("Unsupported codec: %d", sw_codec_cfg.sw_codec); + return false; + } + + if (sw_codec_cfg.encoder.enabled && IS_ENABLED(SAMPLE_RATE_CONVERTER)) { + for (int i = 0; i < sw_codec_cfg.encoder.channel_mode; i++) { + ret = sample_rate_converter_open(&encoder_converters[i]); + if (ret) { + LOG_ERR("Failed to initialize the sample rate converter for " + "encoding channel %d: %d", + i, ret); + return ret; + } + } + } + + if (sw_codec_cfg.decoder.enabled && IS_ENABLED(SAMPLE_RATE_CONVERTER)) { + for (int i = 0; i < sw_codec_cfg.decoder.channel_mode; i++) { + ret = sample_rate_converter_open(&decoder_converters[i]); + if (ret) { + LOG_ERR("Failed to initialize the sample rate converter for " + "decoding channel %d: %d", + i, ret); + return ret; + } + } + } + + m_config = sw_codec_cfg; + m_config.initialized = true; + + return 0; +} diff --git a/src/audio/sw_codec_select.h b/src/audio/sw_codec_select.h new file mode 100644 index 0000000..3771fa7 --- /dev/null +++ b/src/audio/sw_codec_select.h @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2018 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _SW_CODEC_SELECT_H_ +#define _SW_CODEC_SELECT_H_ + +#include +#include "channel_assignment.h" + +#if (CONFIG_SW_CODEC_LC3) +#define LC3_MAX_FRAME_SIZE_MS 10 +#define LC3_ENC_MONO_FRAME_SIZE (CONFIG_LC3_BITRATE_MAX * LC3_MAX_FRAME_SIZE_MS / (8 * 1000)) + +#define LC3_PCM_NUM_BYTES_MONO \ + (CONFIG_AUDIO_SAMPLE_RATE_HZ * CONFIG_AUDIO_BIT_DEPTH_OCTETS * LC3_MAX_FRAME_SIZE_MS / 1000) +#define LC3_ENC_TIME_US 3000 +#define LC3_DEC_TIME_US 1500 +#else +#define LC3_ENC_MONO_FRAME_SIZE 0 +#define LC3_PCM_NUM_BYTES_MONO 0 +#define LC3_ENC_TIME_US 0 +#define LC3_DEC_TIME_US 0 +#endif /* CONFIG_SW_CODEC_LC3 */ + +/* Max will be used when multiple codecs are supported */ +#define ENC_MAX_FRAME_SIZE MAX(LC3_ENC_MONO_FRAME_SIZE, 0) +#define ENC_TIME_US MAX(LC3_ENC_TIME_US, 0) +#define DEC_TIME_US MAX(LC3_DEC_TIME_US, 0) +#define PCM_NUM_BYTES_MONO MAX(LC3_PCM_NUM_BYTES_MONO, 0) +#define PCM_NUM_BYTES_STEREO (PCM_NUM_BYTES_MONO * 2) + +enum sw_codec_select { + SW_CODEC_NONE, + SW_CODEC_LC3, /* Low Complexity Communication Codec */ +}; + +enum sw_codec_channel_mode { + SW_CODEC_MONO = 1, + SW_CODEC_STEREO, +}; + +struct sw_codec_encoder { + bool enabled; + int bitrate; + enum sw_codec_channel_mode channel_mode; + uint8_t num_ch; + enum audio_channel audio_ch; + uint32_t sample_rate_hz; +}; + +struct sw_codec_decoder { + bool enabled; + enum sw_codec_channel_mode channel_mode; /* Mono or stereo. */ + uint8_t num_ch; /* Number of decoder channels. */ + enum audio_channel audio_ch; /* Used to choose which channel to use. */ + uint32_t sample_rate_hz; +}; + +/** + * @brief Sw_codec configuration structure. + */ +struct sw_codec_config { + enum sw_codec_select sw_codec; /* sw_codec to be used, e.g. LC3, etc. */ + struct sw_codec_decoder decoder; /* Struct containing settings for decoder. */ + struct sw_codec_encoder encoder; /* Struct containing settings for encoder. */ + bool initialized; /* Status of codec. */ +}; + +/** + * @brief Check if the software codec is initialized. + * + * @retval true SW codec is initialized. + * @retval false SW codec is not initialized. + */ +bool sw_codec_is_initialized(void); + +/** + * @brief Encode PCM data and output encoded data. + * + * @note Takes in stereo PCM stream, will encode either one or two + * channels, based on channel_mode set during init. + * + * @param[in] pcm_data Pointer to PCM data. + * @param[in] pcm_size Size of PCM data. + * @param[out] encoded_data Pointer to buffer to store encoded data. + * @param[out] encoded_size Size of encoded data. + * + * @return 0 if success, error codes depends on sw_codec selected. + */ +int sw_codec_encode(void *pcm_data, size_t pcm_size, uint8_t **encoded_data, size_t *encoded_size); + +/** + * @brief Decode encoded data and output PCM data. + * + * @param[in] encoded_data Pointer to encoded data. + * @param[in] encoded_size Size of encoded data. + * @param[in] bad_frame Flag to indicate a missing/bad frame (only LC3). + * @param[out] pcm_data Pointer to buffer to store decoded PCM data. + * @param[out] pcm_size Size of decoded data. + * + * @return 0 if success, error codes depends on sw_codec selected. + */ +int sw_codec_decode(uint8_t const *const encoded_data, size_t encoded_size, bool bad_frame, + void **pcm_data, size_t *pcm_size); + +/** + * @brief Uninitialize the software codec and free the allocated space. + * + * @note Must be called before calling init for another sw_codec. + * + * @param[in] sw_codec_cfg Struct to tear down sw_codec. + * + * @return 0 if success, error codes depends on sw_codec selected. + */ +int sw_codec_uninit(struct sw_codec_config sw_codec_cfg); + +/** + * @brief Initialize the software codec and statically or dynamically + * allocate memory to be used, depending on the selected codec + * and its configuration. + * + * @param[in] sw_codec_cfg Struct to set up sw_codec. + * + * @return 0 if success, error codes depends on sw_codec selected. + */ +int sw_codec_init(struct sw_codec_config sw_codec_cfg); + +#endif /* _SW_CODEC_SELECT_H_ */ diff --git a/src/bluetooth/CMakeLists.txt b/src/bluetooth/CMakeLists.txt new file mode 100644 index 0000000..42a4f65 --- /dev/null +++ b/src/bluetooth/CMakeLists.txt @@ -0,0 +1,17 @@ +# +# Copyright (c) 2022 Nordic Semiconductor +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +add_subdirectory(bt_management) +add_subdirectory(bt_rendering_and_capture) +add_subdirectory(bt_content_control) +add_subdirectory(bt_stream) + +zephyr_library_include_directories( + bt_management + bt_rendering_and_capture + bt_content_control + bt_stream +) diff --git a/src/bluetooth/Kconfig b/src/bluetooth/Kconfig new file mode 100644 index 0000000..2bb2d8d --- /dev/null +++ b/src/bluetooth/Kconfig @@ -0,0 +1,129 @@ +# +# Copyright (c) 2022 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +rsource "Kconfig.defaults" + +menu "Bluetooth" + +rsource "bt_management/Kconfig" + +#----------------------------------------------------------------------------# +menu "Bluetooth audio" + +if TRANSPORT_BIS +rsource "bt_stream/broadcast/Kconfig" +endif # TRANSPORT_BIS + +if TRANSPORT_CIS +rsource "bt_stream/unicast/Kconfig" +endif # TRANSPORT_CIS + +config BT_AUDIO_PACKING_INTERLEAVED + bool "Interleaved packing" + default n + help + ISO channels can either be interleaved or sequentially packed; sequential is the default one. + +config BT_AUDIO_PREF_SAMPLE_RATE_VALUE + hex + default 0x03 if BT_AUDIO_PREF_SAMPLE_RATE_16KHZ + default 0x05 if BT_AUDIO_PREF_SAMPLE_RATE_24KHZ + default 0x08 if BT_AUDIO_PREF_SAMPLE_RATE_48KHZ + +choice BT_AUDIO_PREF_SAMPLE_RATE + prompt "Preferred BT audio sample rate" + default BT_AUDIO_PREF_SAMPLE_RATE_16KHZ if BT_BAP_BROADCAST_16_2_1 + default BT_AUDIO_PREF_SAMPLE_RATE_16KHZ if BT_BAP_BROADCAST_16_2_2 + default BT_AUDIO_PREF_SAMPLE_RATE_16KHZ if BT_BAP_UNICAST_16_2_1 + default BT_AUDIO_PREF_SAMPLE_RATE_24KHZ if STREAM_BIDIRECTIONAL + default BT_AUDIO_PREF_SAMPLE_RATE_24KHZ if BT_BAP_BROADCAST_24_2_1 + default BT_AUDIO_PREF_SAMPLE_RATE_24KHZ if BT_BAP_BROADCAST_24_2_2 + default BT_AUDIO_PREF_SAMPLE_RATE_24KHZ if BT_BAP_UNICAST_24_2_1 + default BT_AUDIO_PREF_SAMPLE_RATE_48KHZ + help + Select the preferred sample rate to stream if there are more than one to choose from. + Only valid when used by unicast_client if CONFIG_SAMPLE_RATE_CONVERTER=y and + CONFIG_AUDIO_SAMPLE_RATE_48000_HZ=y, meaning 16, 24, and 48kHz are supported. + +config BT_AUDIO_PREF_SAMPLE_RATE_48KHZ + bool "48 kHz" + help + Select 48000 Hz as the preferred sample rate. + +config BT_AUDIO_PREF_SAMPLE_RATE_24KHZ + bool "24 kHz" + help + Select 24000 Hz as the preferred sample rate. + +config BT_AUDIO_PREF_SAMPLE_RATE_16KHZ + bool "16 kHz" + help + Select 16000 Hz as the preferred sample rate. +endchoice + +#----------------------------------------------------------------------------# +menu "QoS" + +config BT_AUDIO_PRESENTATION_DELAY_US + int "Presentation delay" + range AUDIO_MIN_PRES_DLY_US AUDIO_MAX_PRES_DLY_US + default AUDIO_MIN_PRES_DLY_US + help + The audio source/client defined presentation delay if within + AUDIO_MIN_PRES_DLY_US and AUDIO_MAX_PRES_DLY_US range. This will + override the audio receivers presentation delay as long as it + is in range of the max and min supported by the audio receivers. + If it is outside this range, then it will revert to the closest + supported value. + +config BT_AUDIO_MAX_TRANSPORT_LATENCY_MS + int "Max transport latency" + range 5 4000 + default 10 + help + Max transport latency for the ISO link. + +config BT_AUDIO_RETRANSMITS + int "Number of re-transmits" + range 0 30 + default 2 + help + Number of re-transmits for the ISO link. 2 re-transmits means a total + of 3 packets sent per stream. + +endmenu # QoS +endmenu # Bluetooth audio + +rsource "bt_rendering_and_capture/Kconfig" +rsource "bt_content_control/Kconfig" + +#----------------------------------------------------------------------------# +menu "Log levels" + +module = BLE +module-str = ble +source "subsys/logging/Kconfig.template.log_config" + +module = BT_LE_AUDIO_TX +module-str = bt_le_audio_tx +source "subsys/logging/Kconfig.template.log_config" + +endmenu # Log levels + +#----------------------------------------------------------------------------# +menu "Testing" + +config TESTING_BLE_ADDRESS_RANDOM + bool "Random address and bonding clear on every restart [EXPERIMENTAL]" + default n + select EXPERIMENTAL + help + If enabled the system will generate a new address on every + restart (i.e. reset, re-flash). Any bonding information will + be cleared. This is only for testing purposes. + +endmenu # Testing +endmenu # Bluetooth diff --git a/src/bluetooth/Kconfig.defaults b/src/bluetooth/Kconfig.defaults new file mode 100644 index 0000000..73a4828 --- /dev/null +++ b/src/bluetooth/Kconfig.defaults @@ -0,0 +1,25 @@ +# +# Copyright (c) 2022 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +config BT_AUDIO + default y + +config BT_DEVICE_NAME + default BT_AUDIO_BROADCAST_NAME if TRANSPORT_BIS + default "NRF5340_AUDIO" + +config BT_DEVICE_NAME_DYNAMIC + default y + +config BT_ECC + default y if BT + +config BT_EXT_ADV + default y + +# Mandatory to support at least 1 for ASCS +config BT_ATT_PREPARE_COUNT + default 1 diff --git a/src/bluetooth/bt_content_control/CMakeLists.txt b/src/bluetooth/bt_content_control/CMakeLists.txt new file mode 100644 index 0000000..2d5d99d --- /dev/null +++ b/src/bluetooth/bt_content_control/CMakeLists.txt @@ -0,0 +1,17 @@ +# +# Copyright (c) 2023 Nordic Semiconductor +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +zephyr_library_include_directories( + media +) + +target_sources(app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/bt_content_ctrl.c) + +if (CONFIG_BT_MCC OR CONFIG_BT_MCS) +target_sources(app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/media/bt_content_ctrl_media.c) +endif() diff --git a/src/bluetooth/bt_content_control/Kconfig b/src/bluetooth/bt_content_control/Kconfig new file mode 100644 index 0000000..2c36036 --- /dev/null +++ b/src/bluetooth/bt_content_control/Kconfig @@ -0,0 +1,19 @@ +# +# Copyright (c) 2023 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +menu "Content control" + +rsource "media/Kconfig" + +#----------------------------------------------------------------------------# +menu "Log level" + +module = BT_CONTENT_CTRL +module-str = bt_content_ctrl +source "subsys/logging/Kconfig.template.log_config" + +endmenu # Log level +endmenu # Content control diff --git a/src/bluetooth/bt_content_control/bt_content_ctrl.c b/src/bluetooth/bt_content_control/bt_content_ctrl.c new file mode 100644 index 0000000..858d0a1 --- /dev/null +++ b/src/bluetooth/bt_content_control/bt_content_ctrl.c @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "bt_content_ctrl.h" + +#include +#include + +#include "bt_content_ctrl_media_internal.h" +#include "zbus_common.h" +#include "macros_common.h" + +#include +LOG_MODULE_REGISTER(bt_content_ctrl, CONFIG_BT_CONTENT_CTRL_LOG_LEVEL); + +ZBUS_CHAN_DEFINE(cont_media_chan, struct content_control_msg, NULL, NULL, ZBUS_OBSERVERS_EMPTY, + ZBUS_MSG_INIT(0)); + +static void media_control_cb(bool play) +{ + int ret; + struct content_control_msg msg; + + if (play) { + msg.event = MEDIA_START; + } else { + msg.event = MEDIA_STOP; + } + + ret = zbus_chan_pub(&cont_media_chan, &msg, K_NO_WAIT); + ERR_CHK_MSG(ret, "zbus publication failed"); +} + +int bt_content_ctrl_start(struct bt_conn *conn) +{ + int ret; + struct content_control_msg msg; + + if (IS_ENABLED(CONFIG_BT_MCC) || IS_ENABLED(CONFIG_BT_MCS)) { + ret = bt_content_ctrl_media_play(conn); + if (ret) { + LOG_WRN("Failed to change the streaming state"); + return ret; + } + + return 0; + } + + msg.event = MEDIA_START; + + ret = zbus_chan_pub(&cont_media_chan, &msg, K_NO_WAIT); + ERR_CHK_MSG(ret, "zbus publication failed"); + + return 0; +} + +int bt_content_ctrl_stop(struct bt_conn *conn) +{ + int ret; + struct content_control_msg msg; + + if (IS_ENABLED(CONFIG_BT_MCC) || IS_ENABLED(CONFIG_BT_MCS)) { + ret = bt_content_ctrl_media_pause(conn); + if (ret) { + LOG_WRN("Failed to change the streaming state"); + return ret; + } + + return 0; + } + + msg.event = MEDIA_STOP; + + ret = zbus_chan_pub(&cont_media_chan, &msg, K_NO_WAIT); + ERR_CHK_MSG(ret, "zbus publication failed"); + + return 0; +} + +int bt_content_ctrl_conn_disconnected(struct bt_conn *conn) +{ + int ret; + + if (IS_ENABLED(CONFIG_BT_MCC)) { + ret = bt_content_ctrl_media_conn_disconnected(conn); + /* Try to reset MCS state. -ESRCH is returned if MCS hasn't been discovered + * yet, and shouldn't cause an error print + */ + if (ret && ret != -ESRCH) { + LOG_ERR("bt_content_ctrl_media_conn_disconnected failed with %d", ret); + } + } + + return 0; +} + +int bt_content_ctrl_discover(struct bt_conn *conn) +{ + int ret; + + if (IS_ENABLED(CONFIG_BT_MCC)) { + ret = bt_content_ctrl_media_discover(conn); + if (ret) { + LOG_ERR("Failed to discover the media control client"); + return ret; + } + } + + return 0; +} + +int bt_content_ctrl_uuid_populate(struct net_buf_simple *uuid_buf) +{ + if (IS_ENABLED(CONFIG_BT_MCC)) { + if (net_buf_simple_tailroom(uuid_buf) >= BT_UUID_SIZE_16) { + net_buf_simple_add_le16(uuid_buf, BT_UUID_MCS_VAL); + } else { + return -ENOMEM; + } + } + + return 0; +} + +int bt_content_ctrl_init(void) +{ + int ret; + + if (IS_ENABLED(CONFIG_BT_MCS)) { + ret = bt_content_ctrl_media_server_init(media_control_cb); + if (ret) { + LOG_ERR("MCS server init failed"); + return ret; + } + } + + if (IS_ENABLED(CONFIG_BT_MCC)) { + ret = bt_content_ctrl_media_client_init(); + if (ret) { + LOG_ERR("MCS client init failed"); + return ret; + } + } + + return 0; +} diff --git a/src/bluetooth/bt_content_control/bt_content_ctrl.h b/src/bluetooth/bt_content_control/bt_content_ctrl.h new file mode 100644 index 0000000..805dedc --- /dev/null +++ b/src/bluetooth/bt_content_control/bt_content_ctrl.h @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _BT_CONTENT_CTRL_H_ +#define _BT_CONTENT_CTRL_H_ + +#include + +/** + * @brief Send the start request for content transmission. + * + * @param[in] conn Pointer to the connection to control; can be NULL. + * + * @return 0 for success, error otherwise. + */ +int bt_content_ctrl_start(struct bt_conn *conn); + +/** + * @brief Send the stop request for content transmission. + * + * @param[in] conn Pointer to the connection to control; can be NULL. + * + * @return 0 for success, error otherwise. + */ +int bt_content_ctrl_stop(struct bt_conn *conn); + +/** + * @brief Handle disconnected connection for the content control services. + * + * @param[in] conn Pointer to the disconnected connection. + * + * @return 0 for success, error otherwise. + */ +int bt_content_ctrl_conn_disconnected(struct bt_conn *conn); + +/** + * @brief Discover the content control services for the given connection pointer. + * + * @param[in] conn Pointer to the connection on which to discover the services. + * + * @return 0 for success, error otherwise. + */ +int bt_content_ctrl_discover(struct bt_conn *conn); + +/** + * @brief Put the UUIDs from this module into the buffer. + * + * @note This partial data is used to build a complete extended advertising packet. + * + * @param[out] uuid_buf Buffer being populated with UUIDs. + * + * @return 0 for success, error otherwise. + */ +int bt_content_ctrl_uuid_populate(struct net_buf_simple *uuid_buf); + +/** + * @brief Check if the media player is playing. + * + * @retval true Media player is in a playing state. + * @retval false Media player is not in a playing state. + */ +bool bt_content_ctlr_media_state_playing(void); + +/** + * @brief Initialize the content control module. + * + * @return 0 for success, error otherwise. + */ +int bt_content_ctrl_init(void); + +#endif /* _BT_CONTENT_CTRL_H_ */ diff --git a/src/bluetooth/bt_content_control/media/Kconfig b/src/bluetooth/bt_content_control/media/Kconfig new file mode 100644 index 0000000..eb8881f --- /dev/null +++ b/src/bluetooth/bt_content_control/media/Kconfig @@ -0,0 +1,17 @@ +# +# Copyright (c) 2023 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +menu "Media" + +#----------------------------------------------------------------------------# +menu "Log level" + +module = BT_CONTENT_CTRL_MEDIA +module-str = bt_content_ctrl_media +source "subsys/logging/Kconfig.template.log_config" + +endmenu # Log level +endmenu # Media diff --git a/src/bluetooth/bt_content_control/media/bt_content_ctrl_media.c b/src/bluetooth/bt_content_control/media/bt_content_ctrl_media.c new file mode 100644 index 0000000..18e6e2c --- /dev/null +++ b/src/bluetooth/bt_content_control/media/bt_content_ctrl_media.c @@ -0,0 +1,527 @@ +/* + * Copyright (c) 2021 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "bt_content_ctrl_media_internal.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "macros_common.h" + +#include +LOG_MODULE_REGISTER(bt_content_ctrl_media, CONFIG_BT_CONTENT_CTRL_MEDIA_LOG_LEVEL); + +static uint8_t media_player_state = BT_MCS_MEDIA_STATE_PLAYING; + +static struct media_player *local_player; +static bt_content_ctrl_media_play_pause_cb play_pause_cb; + +enum mcs_disc_status { + IDLE, + IN_PROGRESS, + FINISHED, +}; + +struct media_ctlr { + enum mcs_disc_status mcp_mcs_disc_status; + struct bt_conn *conn; +}; + +static struct media_ctlr mcc_peer[CONFIG_BT_MAX_CONN]; + +/** + * @brief Get the index of the first available mcc_peer + * + * @return Index if success, -ENOMEM if no available indexes + */ +static int mcc_peer_index_free_get(void) +{ + for (int i = 0; i < ARRAY_SIZE(mcc_peer); i++) { + if (mcc_peer[i].conn == NULL) { + return i; + } + } + + LOG_WRN("No more indexes for MCC peer"); + + return -ENOMEM; +} + +/** + * @brief Get index of a given conn pointer + * + * @param conn Pointer to check against + * + * @return index if found, -ESRCH if not found, -EINVAL if invalid conn pointer + */ +static int mcc_peer_index_get(struct bt_conn *conn) +{ + if (conn == NULL) { + LOG_WRN("Invalid conn pointer"); + return -EINVAL; + } + + for (uint8_t i = 0; i < ARRAY_SIZE(mcc_peer); i++) { + if (mcc_peer[i].conn == conn) { + return i; + } + } + + LOG_DBG("No matching conn pointer for this mcc_peer"); + return -ESRCH; +} + +/** + * @brief Callback handler for MCS discover finished. + * + * @note This callback handler will be triggered when MCS + * discovery is finished. Used by the client. + */ +static void mcc_discover_mcs_cb(struct bt_conn *conn, int err) +{ + int ret; + int idx = mcc_peer_index_get(conn); + + if (idx < 0) { + LOG_WRN("Unable to look up conn pointer: %d", idx); + return; + } + + if (err) { + if (err == BT_ATT_ERR_UNLIKELY) { + /* BT_ATT_ERR_UNLIKELY may occur in normal operating conditions if there is + * a disconnect while discovering, hence it will be treated as a warning. + */ + LOG_WRN("Discovery of MCS failed (%d)", err); + } else { + LOG_ERR("Discovery of MCS failed (%d)", err); + } + + mcc_peer[idx].mcp_mcs_disc_status = IDLE; + return; + } + + if (mcc_peer[idx].mcp_mcs_disc_status != IN_PROGRESS) { + /* Due to the design of MCC, there will be several + * invocations of this callback. We are only interested + * in what we have explicitly requested. + */ + LOG_DBG("Filtered out callback"); + return; + } + + mcc_peer[idx].mcp_mcs_disc_status = FINISHED; + /* NOTE: The string below is used by the Nordic CI system */ + LOG_INF("Discovery of MCS finished"); + + ret = bt_content_ctrl_media_state_update(conn); + if (ret < 0 && ret != -EBUSY) { + LOG_WRN("Failed to update media state: %d", ret); + } +} + +#if defined(CONFIG_BT_MCC_SET_MEDIA_CONTROL_POINT) +/** + * @brief Callback handler for sent MCS commands. + * + * @note This callback will be triggered when MCS commands have been sent. + * Used by the client. + */ +static void mcc_send_command_cb(struct bt_conn *conn, int err, const struct mpl_cmd *cmd) +{ + int ret; + + LOG_DBG("mcc_send_command_cb"); + + if (err) { + struct bt_conn_info info; + + /* Check that we are actually in a connected state before printing an error */ + ret = bt_conn_get_info(conn, &info); + if (!ret && info.state == (BT_CONN_STATE_CONNECTED)) { + LOG_ERR("MCC: cmd send failed (%d) - opcode: %u, param: %d", err, + cmd->opcode, cmd->param); + } + } +} +#endif /* defined(CONFIG_BT_MCC_SET_MEDIA_CONTROL_POINT) */ + +/** + * @brief Callback handler for received notifications. + * + * @note This callback will be triggered when a notification has been received. + * Used by the client. + */ +static void mcc_cmd_notification_cb(struct bt_conn *conn, int err, const struct mpl_cmd_ntf *ntf) +{ + LOG_DBG("mcc_cmd_ntf_cb"); + + if (err) { + LOG_ERR("MCC: cmd ntf error (%d) - opcode: %u, result: %u", err, + ntf->requested_opcode, ntf->result_code); + } +} + +#if defined(CONFIG_BT_MCC_READ_MEDIA_STATE) +/** + * @brief Callback handler for reading media state. + * + * @note This callback will be triggered when the client has asked to read + * the current state of the media player. + */ +static void mcc_read_media_state_cb(struct bt_conn *conn, int err, uint8_t state) +{ + LOG_DBG("mcc_read_media_cb, state: %d", state); + + if (err) { + LOG_ERR("MCC: Media State read failed (%d)", err); + return; + } + + media_player_state = state; +} +#endif /* defined(CONFIG_BT_MCC_READ_MEDIA_STATE) */ + +/** + * @brief Callback handler for received MCS commands. + * + * @note This callback will be triggered when the server has received a + * command from the client or the commander. + */ +static void mcs_command_recv_cb(struct media_player *plr, int err, + const struct mpl_cmd_ntf *cmd_ntf) +{ + if (err) { + LOG_ERR("Command failed (%d)", err); + return; + } + + LOG_DBG("Received opcode: %d", cmd_ntf->requested_opcode); + + if (cmd_ntf->requested_opcode == BT_MCS_OPC_PLAY) { + play_pause_cb(true); + } else if (cmd_ntf->requested_opcode == BT_MCS_OPC_PAUSE) { + play_pause_cb(false); + } else { + LOG_WRN("Unsupported opcode: %d", cmd_ntf->requested_opcode); + } +} + +/** + * @brief Callback handler for getting the current state of the media player. + * + * @note This callback will be triggered when the server has asked for the + * current state of its local media player. + */ +static void mcs_media_state_cb(struct media_player *plr, int err, uint8_t state) +{ + if (err) { + LOG_ERR("Media state failed (%d)", err); + return; + } + + media_player_state = state; +} + +/** + * @brief Callback handler for getting a pointer to the local media player. + * + * @note This callback will be triggered during initialization when the + * local media player is ready. + */ +static void mcs_local_player_instance_cb(struct media_player *player, int err) +{ + int ret; + struct mpl_cmd cmd; + + if (err) { + LOG_ERR("Local player instance failed (%d)", err); + return; + } + + LOG_DBG("Received local player"); + + local_player = player; + + cmd.opcode = BT_MCS_OPC_PLAY; + + /* Since the media player is default paused when initialized, we + * send a play command when the first stream is enabled + */ + ret = media_proxy_ctrl_send_command(local_player, &cmd); + if (ret) { + LOG_WRN("Failed to set media proxy state to play: %d", ret); + } +} + +/** + * @brief Send command to either local media player or peer + * + * @param conn Pointer to the conn to send the command to + * @param cmd Command to send + * + * @return 0 for success, error otherwise. + */ +static int mpl_cmd_send(struct bt_conn *conn, struct mpl_cmd *cmd) +{ + int ret; + int any_failures = 0; + + if (IS_ENABLED(CONFIG_BT_MCS)) { + ret = media_proxy_ctrl_send_command(local_player, cmd); + if (ret) { + LOG_WRN("Failed to send command: %d", ret); + return ret; + } + } + + if (IS_ENABLED(CONFIG_BT_MCC)) { + if (conn != NULL) { + int idx = mcc_peer_index_get(conn); + + if (idx < 0) { + LOG_ERR("Unable to find mcc_peer"); + return idx; + } + + if (mcc_peer[idx].mcp_mcs_disc_status == FINISHED) { + ret = bt_mcc_send_cmd(mcc_peer[idx].conn, cmd); + if (ret) { + LOG_WRN("Failed to send command: %d", ret); + return ret; + } + } else { + LOG_WRN("MCS discovery has not finished: %d", + mcc_peer[idx].mcp_mcs_disc_status); + return -EBUSY; + } + + return 0; + } + + /* Send cmd to all peers connected and has finished discovery */ + for (uint8_t i = 0; i < ARRAY_SIZE(mcc_peer); i++) { + if (mcc_peer[i].conn != NULL) { + if (mcc_peer[i].mcp_mcs_disc_status == FINISHED) { + ret = bt_mcc_send_cmd(mcc_peer[i].conn, cmd); + if (ret) { + LOG_WRN("Failed to send command: %d", ret); + any_failures = ret; + } + } else { + LOG_WRN("MCS discovery has not finished: %d", + mcc_peer[i].mcp_mcs_disc_status); + any_failures = -EBUSY; + } + } + } + } + + if (any_failures) { + return any_failures; + } + + return 0; +} + +int bt_content_ctrl_media_discover(struct bt_conn *conn) +{ + int ret; + + if (!IS_ENABLED(CONFIG_BT_MCC)) { + LOG_ERR("MCC not enabled"); + return -ECANCELED; + } + + if (conn == NULL) { + LOG_ERR("Invalid conn pointer"); + return -EINVAL; + } + + int idx = mcc_peer_index_get(conn); + + if (idx == -ESRCH) { + idx = mcc_peer_index_free_get(); + if (idx < 0) { + LOG_WRN("Error getting free index: %d", idx); + return idx; + } + + mcc_peer[idx].conn = conn; + } + + if (mcc_peer[idx].mcp_mcs_disc_status == FINISHED || + mcc_peer[idx].mcp_mcs_disc_status == IN_PROGRESS) { + return -EALREADY; + } + + mcc_peer[idx].mcp_mcs_disc_status = IN_PROGRESS; + + ret = bt_mcc_discover_mcs(conn, true); + if (ret) { + mcc_peer[idx].mcp_mcs_disc_status = IDLE; + return ret; + } + + return 0; +} + +int bt_content_ctrl_media_state_update(struct bt_conn *conn) +{ + if (!IS_ENABLED(CONFIG_BT_MCC)) { + LOG_ERR("MCC not enabled"); + return -ECANCELED; + } + + int idx = mcc_peer_index_get(conn); + + if (idx < 0) { + LOG_WRN("Unable to look up conn pointer: %d", idx); + return idx; + } + + if (mcc_peer[idx].mcp_mcs_disc_status != FINISHED) { + LOG_WRN("MCS discovery has not finished"); + return -EBUSY; + } + + return bt_mcc_read_media_state(conn); +} + +int bt_content_ctrl_media_play(struct bt_conn *conn) +{ + int ret; + struct mpl_cmd cmd; + + if (media_player_state != BT_MCS_MEDIA_STATE_PLAYING && + media_player_state != BT_MCS_MEDIA_STATE_PAUSED) { + LOG_ERR("Invalid state: %d", media_player_state); + return -ECANCELED; + } + + if (media_player_state == BT_MCS_MEDIA_STATE_PLAYING) { + LOG_WRN("Already in a playing state"); + return -EAGAIN; + } + + cmd.opcode = BT_MCS_OPC_PLAY; + cmd.use_param = false; + + ret = mpl_cmd_send(conn, &cmd); + if (ret) { + return ret; + } + + return 0; +} + +int bt_content_ctrl_media_pause(struct bt_conn *conn) +{ + int ret; + struct mpl_cmd cmd; + + if (media_player_state != BT_MCS_MEDIA_STATE_PLAYING && + media_player_state != BT_MCS_MEDIA_STATE_PAUSED) { + LOG_ERR("Invalid state: %d", media_player_state); + return -ECANCELED; + } + + if (media_player_state == BT_MCS_MEDIA_STATE_PAUSED) { + LOG_WRN("Already in a paused state"); + return -EAGAIN; + } + + cmd.opcode = BT_MCS_OPC_PAUSE; + cmd.use_param = false; + + ret = mpl_cmd_send(conn, &cmd); + if (ret) { + return ret; + } + + return 0; +} + +bool bt_content_ctlr_media_state_playing(void) +{ + if (media_player_state == BT_MCS_MEDIA_STATE_PLAYING) { + return true; + } + + return false; +} + +int bt_content_ctrl_media_conn_disconnected(struct bt_conn *conn) +{ + int idx = mcc_peer_index_get(conn); + + if (idx < 0) { + LOG_WRN("Unable to look up conn pointer: %d", idx); + return idx; + } + + LOG_DBG("MCS discover state reset due to disconnection"); + mcc_peer[idx].mcp_mcs_disc_status = IDLE; + mcc_peer[idx].conn = NULL; + return 0; +} + +int bt_content_ctrl_media_client_init(void) +{ + if (!IS_ENABLED(CONFIG_BT_MCC)) { + LOG_ERR("MCC not enabled"); + return -ECANCELED; + } + + static struct bt_mcc_cb mcc_cb; + + mcc_cb.discover_mcs = mcc_discover_mcs_cb; +#if defined(CONFIG_BT_MCC_SET_MEDIA_CONTROL_POINT) + mcc_cb.send_cmd = mcc_send_command_cb; +#endif /* defined(CONFIG_BT_MCC_SET_MEDIA_CONTROL_POINT) */ + mcc_cb.cmd_ntf = mcc_cmd_notification_cb; +#if defined(CONFIG_BT_MCC_READ_MEDIA_STATE) + mcc_cb.read_media_state = mcc_read_media_state_cb; +#endif /* defined(CONFIG_BT_MCC_READ_MEDIA_STATE) */ + return bt_mcc_init(&mcc_cb); +} + +int bt_content_ctrl_media_server_init(bt_content_ctrl_media_play_pause_cb play_pause) +{ + int ret; + + if (!IS_ENABLED(CONFIG_BT_MCS)) { + LOG_ERR("MCS not enabled"); + return -ECANCELED; + } + + static struct media_proxy_ctrl_cbs mcs_cb; + + play_pause_cb = play_pause; + + ret = media_proxy_pl_init(); + if (ret) { + LOG_ERR("Failed to init media proxy: %d", ret); + return ret; + } + + mcs_cb.command_recv = mcs_command_recv_cb; + mcs_cb.media_state_recv = mcs_media_state_cb; + mcs_cb.local_player_instance = mcs_local_player_instance_cb; + + ret = media_proxy_ctrl_register(&mcs_cb); + if (ret) { + LOG_ERR("Could not init mpl: %d", ret); + return ret; + } + + return 0; +} diff --git a/src/bluetooth/bt_content_control/media/bt_content_ctrl_media_internal.h b/src/bluetooth/bt_content_control/media/bt_content_ctrl_media_internal.h new file mode 100644 index 0000000..fdda262 --- /dev/null +++ b/src/bluetooth/bt_content_control/media/bt_content_ctrl_media_internal.h @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2021 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _BT_CONTENT_CTRL_MEDIA_INTERNAL_H_ +#define _BT_CONTENT_CTRL_MEDIA_INTERNAL_H_ + +#include + +/** + * @brief Callback for changing the stream state. + * + * @param[in] play Differentiate between the play command and the pause command. + */ +typedef void (*bt_content_ctrl_media_play_pause_cb)(bool play); + +/** + * @brief Discover Media Control Service and the included services. + * + * @note Only valid for client. + * + * @param[in] conn Pointer to the active connection. + * + * @return 0 for success, error otherwise. + */ +int bt_content_ctrl_media_discover(struct bt_conn *conn); + +/** + * @brief Get the current state of the media player. + * + * @note Only valid for client. + * + * @param[in] conn Pointer to the active connection. + * + * @return 0 for success, error otherwise. + */ +int bt_content_ctrl_media_state_update(struct bt_conn *conn); + +/** + * @brief Send a play command to the media player, + * depending on the current state. + * + * @param[in] conn Pointer to the connection to control; can be NULL. + * + * @note If @p conn is NULL, play will be sent to all mcc_peers discovered. + * + * @return 0 for success, error otherwise. + */ +int bt_content_ctrl_media_play(struct bt_conn *conn); + +/** + * @brief Send a pause command to the media player, + * depending on the current state. + * + * @param[in] conn Pointer to the connection to control; can be NULL. + * + * @note If @p conn is NULL, pause will be sent to all mcc_peers discovered. + * + * @return 0 for success, error otherwise. + */ +int bt_content_ctrl_media_pause(struct bt_conn *conn); + +/** + * @brief Reset the media control peer's discovered state + * + * @note Only valid for client. + * + * @param[in] conn Pointer to the active connection. + * + * @return 0 for success, error otherwise. + */ +int bt_content_ctrl_media_conn_disconnected(struct bt_conn *conn); + +/** + * @brief Initialize the Media Control Client. + * + * @return 0 for success, error otherwise. + */ +int bt_content_ctrl_media_client_init(void); + +/** + * @brief Initialize the Media Control Server. + * + * @param[in] play_pause_cb Callback for received play/pause commands. + * + * @return 0 for success, error otherwise. + */ +int bt_content_ctrl_media_server_init(bt_content_ctrl_media_play_pause_cb play_pause_cb); + +#endif /* _BT_CONTENT_CTRL_MEDIA_INTERNAL_H_ */ diff --git a/src/bluetooth/bt_management/CMakeLists.txt b/src/bluetooth/bt_management/CMakeLists.txt new file mode 100644 index 0000000..49b8d36 --- /dev/null +++ b/src/bluetooth/bt_management/CMakeLists.txt @@ -0,0 +1,44 @@ +# +# Copyright (c) 2023 Nordic Semiconductor +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +zephyr_library_include_directories( + advertising + controller_config + dfu + scanning + ${ZEPHYR_BASE}/subsys/bluetooth/host/ +) + +target_sources(app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/bt_mgmt.c + ${CMAKE_CURRENT_SOURCE_DIR}/controller_config/bt_mgmt_ctlr_cfg.c +) + +if (CONFIG_BT_CENTRAL) + target_sources(app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/scanning/bt_mgmt_scan_for_conn.c) +endif() + +if (CONFIG_BT_BAP_BROADCAST_SINK) +target_sources(app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/scanning/bt_mgmt_scan_for_broadcast.c) +endif() + +if (CONFIG_BT_OBSERVER) +target_sources(app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/scanning/bt_mgmt_scan.c) +endif() + +if (CONFIG_BT_PERIPHERAL OR CONFIG_BT_BROADCASTER) + target_sources(app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/advertising/bt_mgmt_adv.c) +endif() + +if (CONFIG_AUDIO_BT_MGMT_DFU) + target_sources(app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/dfu/bt_mgmt_dfu.c + ) +endif() diff --git a/src/bluetooth/bt_management/Kconfig b/src/bluetooth/bt_management/Kconfig new file mode 100644 index 0000000..b2313dd --- /dev/null +++ b/src/bluetooth/bt_management/Kconfig @@ -0,0 +1,43 @@ +# +# Copyright (c) 2023 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +menu "BT management" + +rsource "controller_config/Kconfig" + +#----------------------------------------------------------------------------# +config WDT_CTLR + bool "Enable watchdog for controller" + default y + help + When true, the controller will be polled at regular intervals to check that it is alive. + Turn off to reduce overhead, or HCI traffic. + The watchdog will be deactivated automatically for DFU procedures. + +menu "Thread priorities" + +config CTLR_POLL_WORK_Q_PRIO + int "Work queue priority for controller poll" + default 2 + help + This is a preemptible work queue. + This work queue will poll the controller to check it is alive. + +endmenu # Thread priorities + +rsource "dfu/Kconfig" +rsource "advertising/Kconfig" +rsource "scanning/Kconfig" + +#----------------------------------------------------------------------------# +menu "Log level" + +module = BT_MGMT +module-str = bt-mgmt +source "subsys/logging/Kconfig.template.log_config" + +endmenu # Log level +endmenu # BT management diff --git a/src/bluetooth/bt_management/advertising/Kconfig b/src/bluetooth/bt_management/advertising/Kconfig new file mode 100644 index 0000000..8f895f3 --- /dev/null +++ b/src/bluetooth/bt_management/advertising/Kconfig @@ -0,0 +1,78 @@ +# +# Copyright (c) 2023 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +menu "Advertising" + +config BLE_ACL_PER_ADV_INT_MIN + hex "Minimum periodic advertising interval" + range 0x0018 0x03C0 + default 0x0078 + help + Minimum hexadecimal value for the interval of periodic advertisements. + For ms, multiply the provided value by 1.25. + +config BLE_ACL_PER_ADV_INT_MAX + hex "Maximum periodic advertising interval" + range 0x0018 0x03C0 + default 0x00A0 + help + Maximum hexadecimal value for the interval of periodic advertisements. + For ms, multiply the provided value by 1.25. + +config BLE_ACL_EXT_ADV_INT_MIN + hex "Minimum extended advertising interval" + range 0x0030 0x0780 + default 0x30 if TRANSPORT_BIS + default 0x00A0 + help + Minimum hexadecimal value for the interval of extended advertisements. + When the LE Audio Controller Subsystem for nRF53 is used, this interval + should be a multiple of the ISO interval and maximum 4x larger than the + lowest interval if using BIS. + For ms, multiply the provided value by 0.625. + +config BLE_ACL_EXT_ADV_INT_MAX + hex "Maximum extended advertising interval" + range 0x0030 0x0780 + default 0x40 if TRANSPORT_BIS + default 0x00F0 + help + Maximum hexadecimal value for the interval of extended advertisements. + When the LE Audio Controller Subsystem for nRF53 is used, this interval + should be a multiple of the ISO interval and maximum 4x larger than the + lowest interval if using BIS. + For ms, multiply the provided value by 0.625. + +config EXT_ADV_BUF_MAX + int "Maximum number of extended advertising data parameters" + default 20 + +config EXT_ADV_UUID_BUF_MAX + int "Maximum number of UUIDs to add to extended advertisements" + default 40 + +config BT_DEVICE_MANUFACTURER_ID + hex "Manufacturer ID" + default 0xFE58 + help + Bluetooth manufacturer ID. For the list of possible values please + consult the following link: + https://www.bluetooth.com/specifications/assigned-numbers + +config BLE_ACL_ADV_SID + hex "Advertising set ID" + range 0x00 0x0F + default 0x00 + +#----------------------------------------------------------------------------# +menu "Log level" + +module = BT_MGMT_ADV +module-str = bt-mgmt-adv +source "subsys/logging/Kconfig.template.log_config" + +endmenu # Log level +endmenu # Advertising diff --git a/src/bluetooth/bt_management/advertising/Kconfig.default b/src/bluetooth/bt_management/advertising/Kconfig.default new file mode 100644 index 0000000..4a9d889 --- /dev/null +++ b/src/bluetooth/bt_management/advertising/Kconfig.default @@ -0,0 +1,8 @@ +# +# Copyright (c) 2023 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +config BT_EXT_ADV + default y diff --git a/src/bluetooth/bt_management/advertising/bt_mgmt_adv.c b/src/bluetooth/bt_management/advertising/bt_mgmt_adv.c new file mode 100644 index 0000000..a92f462 --- /dev/null +++ b/src/bluetooth/bt_management/advertising/bt_mgmt_adv.c @@ -0,0 +1,476 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "bt_mgmt.h" + +#include +#include +#include + +#include "macros_common.h" +#include "zbus_common.h" + +#include +LOG_MODULE_REGISTER(bt_mgmt_adv, CONFIG_BT_MGMT_ADV_LOG_LEVEL); + +ZBUS_CHAN_DECLARE(bt_mgmt_chan); + +#ifndef CONFIG_BT_MAX_PAIRED +#define BONDS_QUEUE_SIZE 0 +#else +#define BONDS_QUEUE_SIZE CONFIG_BT_MAX_PAIRED +#endif + +static struct k_work adv_work; +static bool dir_adv_timed_out; +static struct bt_le_ext_adv *ext_adv[CONFIG_BT_EXT_ADV_MAX_ADV_SET]; + +static const struct bt_data *adv_local[CONFIG_BT_EXT_ADV_MAX_ADV_SET]; +static size_t adv_local_size[CONFIG_BT_EXT_ADV_MAX_ADV_SET]; +static const struct bt_data *per_adv_local[CONFIG_BT_EXT_ADV_MAX_ADV_SET]; +static size_t per_adv_local_size[CONFIG_BT_EXT_ADV_MAX_ADV_SET]; + +/* Bonded address queue */ +K_MSGQ_DEFINE(bonds_queue, sizeof(bt_addr_le_t), BONDS_QUEUE_SIZE, 4); +K_MSGQ_DEFINE(adv_queue, sizeof(uint8_t), CONFIG_BT_EXT_ADV_MAX_ADV_SET, 4); + +static struct bt_le_adv_param ext_adv_param = { + .id = BT_ID_DEFAULT, + .sid = CONFIG_BLE_ACL_ADV_SID, + .secondary_max_skip = 0, + .options = BT_LE_ADV_OPT_EXT_ADV | BT_LE_ADV_OPT_USE_NAME, + .interval_min = CONFIG_BLE_ACL_EXT_ADV_INT_MIN, + .interval_max = CONFIG_BLE_ACL_EXT_ADV_INT_MAX, + .peer = NULL, +}; + +static void bond_find(const struct bt_bond_info *info, void *user_data) +{ + int ret; + struct bt_conn *conn; + + if (!IS_ENABLED(CONFIG_BT_BONDABLE)) { + return; + } + + /* Filter already connected peers. */ + conn = bt_conn_lookup_addr_le(BT_ID_DEFAULT, &info->addr); + if (conn) { + struct bt_conn_info conn_info; + + ret = bt_conn_get_info(conn, &conn_info); + if (ret) { + LOG_WRN("Could not get conn info"); + bt_conn_unref(conn); + return; + } + + if (conn_info.state == BT_CONN_STATE_CONNECTED) { + LOG_WRN("Already connected"); + bt_conn_unref(conn); + return; + } + + bt_conn_unref(conn); + } + + ret = k_msgq_put(&bonds_queue, (void *)&info->addr, K_NO_WAIT); + if (ret) { + LOG_WRN("No space in the queue for the bond"); + } +} + +static void filter_accept_list_add(const struct bt_bond_info *info, void *user_data) +{ + int ret; + + ret = bt_le_filter_accept_list_add(&info->addr); + if (ret) { + LOG_WRN("Could not add peer to Filter Accept List: %d", ret); + return; + } +} + +/** + * @brief Prints the address of the local device and the remote device. + * + * @note The address of the remote device is only printed if directed advertisement is active. + */ +static int addr_print(bt_addr_le_t const *const local_addr, bt_addr_le_t const *const dir_adv_addr) +{ + char local_addr_str[BT_ADDR_LE_STR_LEN] = {'\0'}; + char directed_to_addr_str[BT_ADDR_LE_STR_LEN] = {'\0'}; + + if (local_addr == NULL) { + return -EINVAL; + } + + (void)bt_addr_le_to_str(local_addr, local_addr_str, BT_ADDR_LE_STR_LEN); + LOG_INF("Local addr: %s", local_addr_str); + + if (dir_adv_addr != NULL) { + (void)bt_addr_le_to_str(dir_adv_addr, directed_to_addr_str, BT_ADDR_LE_STR_LEN); + LOG_INF("Adv directed to: %s.", directed_to_addr_str); + } + + return 0; +} + +#if defined(CONFIG_BT_PRIVACY) +static bool adv_rpa_expired_cb(struct bt_le_ext_adv *adv) +{ + int ret; + struct bt_le_ext_adv_info ext_adv_info; + + LOG_INF("RPA (Resolvable Private Address) expired."); + + ret = bt_le_ext_adv_get_info(adv, &ext_adv_info); + ERR_CHK_MSG(ret, "bt_le_ext_adv_get_info failed"); + + ret = addr_print(ext_adv_info.addr, NULL); + if (ret) { + LOG_ERR("addr_print failed"); + } + + return true; +} +#endif /* CONFIG_BT_PRIVACY */ + +static const struct bt_le_ext_adv_cb adv_cb = { +#if defined(CONFIG_BT_PRIVACY) + .rpa_expired = adv_rpa_expired_cb, +#endif /* CONFIG_BT_PRIVACY */ +}; + +static int direct_adv_create(uint8_t ext_adv_index, bt_addr_le_t addr) +{ + int ret; + struct bt_le_ext_adv_info ext_adv_info; + + ext_adv_param = *BT_LE_ADV_CONN_DIR(&addr); + ext_adv_param.id = BT_ID_DEFAULT; + ext_adv_param.options |= BT_LE_ADV_OPT_DIR_ADDR_RPA; + + /* Clear ADV data set before update to direct advertising */ + ret = bt_le_ext_adv_set_data(ext_adv[ext_adv_index], NULL, 0, NULL, 0); + if (ret) { + LOG_ERR("Failed to clear advertising data for set %d. Err: %d", ext_adv_index, ret); + return ret; + } + + ret = bt_le_ext_adv_update_param(ext_adv[ext_adv_index], &ext_adv_param); + if (ret) { + LOG_ERR("Failed to update ext_adv set %d to directed advertising. Err = %d", + ext_adv_index, ret); + return ret; + } + + ret = bt_le_ext_adv_get_info(ext_adv[ext_adv_index], &ext_adv_info); + if (ret) { + return ret; + } + + ret = addr_print(ext_adv_info.addr, &addr); + if (ret) { + return ret; + } + + return 0; +} + +static int extended_adv_create(uint8_t ext_adv_index) +{ + int ret; + struct bt_le_ext_adv_info ext_adv_info; + + if (adv_local[ext_adv_index] == NULL) { + LOG_ERR("Adv_local not set"); + return -ENXIO; + } + + ret = bt_le_ext_adv_set_data(ext_adv[ext_adv_index], adv_local[ext_adv_index], + adv_local_size[ext_adv_index], NULL, 0); + if (ret) { + LOG_ERR("Failed to set advertising data: %d", ret); + return ret; + } + + if (per_adv_local[ext_adv_index] != NULL && IS_ENABLED(CONFIG_BT_PER_ADV)) { + /* Set periodic advertising parameters */ + ret = bt_le_per_adv_set_param(ext_adv[ext_adv_index], LE_AUDIO_PERIODIC_ADV); + if (ret) { + LOG_ERR("Failed to set periodic advertising parameters: %d", ret); + return ret; + } + + ret = bt_le_per_adv_set_data(ext_adv[ext_adv_index], per_adv_local[ext_adv_index], + per_adv_local_size[ext_adv_index]); + if (ret) { + LOG_ERR("Failed to set periodic advertising data: %d", ret); + return ret; + } + } + + ret = bt_le_ext_adv_get_info(ext_adv[ext_adv_index], &ext_adv_info); + if (ret) { + return ret; + } + + ret = addr_print(ext_adv_info.addr, NULL); + if (ret) { + return ret; + } + + return 0; +} + +static void advertising_process(struct k_work *work) +{ + int ret; + struct bt_mgmt_msg msg; + uint8_t ext_adv_index; + + ret = k_msgq_get(&adv_queue, &ext_adv_index, K_NO_WAIT); + if (ret) { + LOG_ERR("No ext_adv_index found"); + return; + } + + k_msgq_purge(&bonds_queue); + + if (IS_ENABLED(CONFIG_BT_BONDABLE)) { + bt_foreach_bond(BT_ID_DEFAULT, bond_find, NULL); + /* Populate Filter Accept List */ + if (IS_ENABLED(CONFIG_BT_FILTER_ACCEPT_LIST)) { + ret = bt_le_filter_accept_list_clear(); + if (ret) { + LOG_ERR("Failed to clear filter accept list"); + return; + } + + bt_foreach_bond(BT_ID_DEFAULT, filter_accept_list_add, NULL); + } + } + + bt_addr_le_t addr; + + if (!k_msgq_get(&bonds_queue, &addr, K_NO_WAIT) && !dir_adv_timed_out) { + ret = direct_adv_create(ext_adv_index, addr); + if (ret) { + LOG_WRN("Failed to create direct advertisement: %d", ret); + return; + } + + ret = bt_le_ext_adv_start( + ext_adv[ext_adv_index], + BT_LE_EXT_ADV_START_PARAM(BT_GAP_ADV_HIGH_DUTY_CYCLE_MAX_TIMEOUT, 0)); + } else { + ret = extended_adv_create(ext_adv_index); + if (ret) { + LOG_WRN("Failed to create extended advertisement: %d", ret); + return; + } + + dir_adv_timed_out = false; + ret = bt_le_ext_adv_start(ext_adv[ext_adv_index], BT_LE_EXT_ADV_START_DEFAULT); + } + + if (ret) { + LOG_ERR("Failed to start advertising set. Err: %d", ret); + return; + } + + if (per_adv_local[ext_adv_index] != NULL && IS_ENABLED(CONFIG_BT_PER_ADV)) { + /* Enable Periodic Advertising */ + ret = bt_le_per_adv_start(ext_adv[ext_adv_index]); + if (ret) { + LOG_ERR("Failed to enable periodic advertising: %d", ret); + return; + } + + msg.event = BT_MGMT_EXT_ADV_WITH_PA_READY; + msg.ext_adv = ext_adv[ext_adv_index]; + msg.index = ext_adv_index; + + ret = zbus_chan_pub(&bt_mgmt_chan, &msg, K_NO_WAIT); + ERR_CHK(ret); + } + + /* NOTE: The string below is used by the Nordic CI system */ + LOG_INF("Advertising successfully started"); +} + +void bt_mgmt_dir_adv_timed_out(uint8_t ext_adv_index) +{ + int ret; + + dir_adv_timed_out = true; + + LOG_DBG("Clearing ext_adv"); + + ret = bt_le_ext_adv_delete(ext_adv[ext_adv_index]); + if (ret) { + LOG_ERR("Failed to clear ext_adv"); + } + + if (IS_ENABLED(CONFIG_BT_FILTER_ACCEPT_LIST)) { + ret = bt_le_ext_adv_create(LE_AUDIO_EXTENDED_ADV_CONN_NAME_FILTER, &adv_cb, + &ext_adv[ext_adv_index]); + } else { + ret = bt_le_ext_adv_create(LE_AUDIO_EXTENDED_ADV_CONN_NAME, &adv_cb, + &ext_adv[ext_adv_index]); + } + + if (ret) { + LOG_ERR("Unable to create a connectable extended advertising set: %d", ret); + return; + } + + /* Restart normal advertising */ + ret = bt_mgmt_adv_start(ext_adv_index, NULL, 0, NULL, 0, true); + if (ret) { + LOG_ERR("Unable start advertising: %d", ret); + return; + } +} + +int bt_mgmt_manufacturer_uuid_populate(struct net_buf_simple *uuid_buf, uint16_t company_id) +{ + if (net_buf_simple_tailroom(uuid_buf) >= BT_UUID_SIZE_16) { + net_buf_simple_add_le16(uuid_buf, company_id); + } else { + return -ENOMEM; + } + + return 0; +} + +int bt_mgmt_adv_buffer_put(struct bt_data *const adv_buf, uint32_t *index, size_t adv_buf_vacant, + size_t data_len, uint8_t type, void *data) +{ + if (adv_buf == NULL || index == NULL || data_len == 0) { + return -EINVAL; + } + + /* Check that we have space for data */ + if (adv_buf_vacant <= *index) { + return -ENOMEM; + } + + adv_buf[*index].type = type; + adv_buf[*index].data_len = data_len; + adv_buf[*index].data = data; + (*index)++; + + return 0; +} + +int bt_mgmt_per_adv_stop(uint8_t ext_adv_index) +{ + int ret; + + ret = bt_le_per_adv_stop(ext_adv[ext_adv_index]); + if (ret) { + LOG_ERR("Failed to top periodic advertising: %d", ret); + return ret; + } + + return 0; +} + +int bt_mgmt_ext_adv_stop(uint8_t ext_adv_index) +{ + int ret; + + ret = bt_le_ext_adv_stop(ext_adv[ext_adv_index]); + if (ret) { + LOG_ERR("Failed to stop advertising set: %d", ret); + return ret; + } + + ret = bt_le_ext_adv_delete(ext_adv[ext_adv_index]); + if (ret) { + LOG_ERR("Failed to delete advertising set: %d", ret); + return ret; + } + + return 0; +} + +int bt_mgmt_adv_start(uint8_t ext_adv_index, const struct bt_data *adv, size_t adv_size, + const struct bt_data *per_adv, size_t per_adv_size, bool connectable) +{ + int ret; + + /* Special case for restarting advertising */ + if (adv == NULL && adv_size == 0 && per_adv == NULL && per_adv_size == 0) { + if (adv_local[ext_adv_index] == NULL) { + LOG_ERR("No valid advertising data stored"); + return -ENOENT; + } + + ret = k_msgq_put(&adv_queue, &ext_adv_index, K_NO_WAIT); + if (ret) { + LOG_ERR("No space in the queue for adv_index"); + return -ENOMEM; + } + k_work_submit(&adv_work); + + return 0; + } + + if (adv == NULL) { + LOG_ERR("No adv struct received"); + return -EINVAL; + } + + if (adv_size == 0) { + LOG_ERR("Invalid size of adv struct"); + return -EINVAL; + } + + adv_local[ext_adv_index] = adv; + adv_local_size[ext_adv_index] = adv_size; + per_adv_local[ext_adv_index] = per_adv; + per_adv_local_size[ext_adv_index] = per_adv_size; + + /* Only use fixed address if no privacy and it is the first ext adv set */ + if (!IS_ENABLED(CONFIG_BT_PRIVACY) && ext_adv_index == 0) { + ext_adv_param.options |= BT_LE_ADV_OPT_USE_IDENTITY; + } else { + /* If privacy is enabled, use RPA */ + ext_adv_param.options &= ~BT_LE_ADV_OPT_USE_IDENTITY; + } + + if (connectable) { + ret = bt_le_ext_adv_create(LE_AUDIO_EXTENDED_ADV_CONN_NAME, &adv_cb, + &ext_adv[ext_adv_index]); + if (ret) { + LOG_ERR("Unable to create a connectable extended advertising set: %d", ret); + return ret; + } + } else { + ret = bt_le_ext_adv_create(&ext_adv_param, &adv_cb, &ext_adv[ext_adv_index]); + if (ret) { + LOG_ERR("Unable to create extended advertising set: %d", ret); + return ret; + } + } + + ret = k_msgq_put(&adv_queue, &ext_adv_index, K_NO_WAIT); + if (ret) { + LOG_ERR("No space in the queue for adv_index"); + return -ENOMEM; + } + k_work_submit(&adv_work); + + return 0; +} + +void bt_mgmt_adv_init(void) +{ + k_work_init(&adv_work, advertising_process); +} diff --git a/src/bluetooth/bt_management/advertising/bt_mgmt_adv_internal.h b/src/bluetooth/bt_management/advertising/bt_mgmt_adv_internal.h new file mode 100644 index 0000000..ea29561 --- /dev/null +++ b/src/bluetooth/bt_management/advertising/bt_mgmt_adv_internal.h @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _BT_MGMT_ADV_INTERNAL_H_ +#define _BT_MGMT_ADV_INTERNAL_H_ + +#include + +/** + * @brief Initialize the advertising part of the Bluetooth management module. + */ +void bt_mgmt_adv_init(void); + +/** + * @brief Handle timed-out directed advertisement. + * + * This function deletes the old ext_adv and creates a new one. + * It also sets the dir_adv_timed_out flag and restarts advertisement. + * + * @param[in] ext_adv_index Index of the ext_adv to restart. + */ +void bt_mgmt_dir_adv_timed_out(uint8_t ext_adv_index); + +#endif /* _BT_MGMT_ADV_INTERNAL_H_ */ diff --git a/src/bluetooth/bt_management/bt_mgmt.c b/src/bluetooth/bt_management/bt_mgmt.c new file mode 100644 index 0000000..f4ec824 --- /dev/null +++ b/src/bluetooth/bt_management/bt_mgmt.c @@ -0,0 +1,415 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "bt_mgmt.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "macros_common.h" +#include "zbus_common.h" +#include "button_handler.h" +#include "bt_mgmt_ctlr_cfg_internal.h" +#include "bt_mgmt_adv_internal.h" +#include "bt_mgmt_dfu_internal.h" +#if CONFIG_BOARD_NRF5340_AUDIO_DK_NRF5340_CPUAPP +#include "button_assignments.h" +#endif + +#include +LOG_MODULE_REGISTER(bt_mgmt, CONFIG_BT_MGMT_LOG_LEVEL); + +ZBUS_CHAN_DEFINE(bt_mgmt_chan, struct bt_mgmt_msg, NULL, NULL, ZBUS_OBSERVERS_EMPTY, + ZBUS_MSG_INIT(0)); + +/* The bt_enable should take less than 15 ms. + * Buffer added as this will not add to bootup time + */ +#define BT_ENABLE_TIMEOUT_MS 100 + +K_SEM_DEFINE(sem_bt_enabled, 0, 1); + +/** + * @brief Iterative function used to find connected conns + * + * @param[in] conn The connection to check + * @param[out] data Pointer to store number of valid conns + */ +static void conn_state_connected_check(struct bt_conn *conn, void *data) +{ + int ret; + uint8_t *num_conn = (uint8_t *)data; + struct bt_conn_info info; + + ret = bt_conn_get_info(conn, &info); + if (ret) { + LOG_ERR("Failed to get conn info for %p: %d", (void *)conn, ret); + return; + } + + if (info.state != BT_CONN_STATE_CONNECTED) { + return; + } + + (*num_conn)++; +} + +static void connected_cb(struct bt_conn *conn, uint8_t err) +{ + int ret; + char addr[BT_ADDR_LE_STR_LEN] = {0}; + struct bt_mgmt_msg msg; + + if (err == BT_HCI_ERR_ADV_TIMEOUT && IS_ENABLED(CONFIG_BT_PERIPHERAL)) { + LOG_INF("Directed adv timed out with no connection, reverting to normal adv"); + + bt_mgmt_dir_adv_timed_out(0); + return; + } + + (void)bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + + if (err) { + if (err == BT_HCI_ERR_UNKNOWN_CONN_ID) { + LOG_WRN("ACL connection to addr: %s timed out, will try again", addr); + } else { + LOG_ERR("ACL connection to addr: %s, conn: %p, failed, error %d %s", addr, + (void *)conn, err, bt_hci_err_to_str(err)); + } + + bt_conn_unref(conn); + + if (IS_ENABLED(CONFIG_BT_CENTRAL)) { + ret = bt_mgmt_scan_start(0, 0, BT_MGMT_SCAN_TYPE_CONN, NULL, + BRDCAST_ID_NOT_USED); + if (ret && ret != -EALREADY) { + LOG_ERR("Failed to restart scanning: %d", ret); + } + } + + if (IS_ENABLED(CONFIG_BT_PERIPHERAL)) { + ret = bt_mgmt_adv_start(0, NULL, 0, NULL, 0, true); + if (ret) { + LOG_ERR("Failed to restart advertising: %d", ret); + } + } + + return; + } + + /* ACL connection established */ + /* NOTE: The string below is used by the Nordic CI system */ + LOG_INF("Connected: %s", addr); + + msg.event = BT_MGMT_CONNECTED; + msg.conn = conn; + + ret = zbus_chan_pub(&bt_mgmt_chan, &msg, K_NO_WAIT); + ERR_CHK(ret); + + if (IS_ENABLED(CONFIG_BT_CENTRAL)) { + ret = bt_conn_set_security(conn, BT_SECURITY_L2); + if (ret) { + LOG_ERR("Failed to set security to L2: %d", ret); + } + } +} + +K_MUTEX_DEFINE(mtx_duplicate_scan); + +static void disconnected_cb(struct bt_conn *conn, uint8_t reason) +{ + int ret; + char addr[BT_ADDR_LE_STR_LEN]; + struct bt_mgmt_msg msg; + + (void)bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + + /* NOTE: The string below is used by the Nordic CI system */ + LOG_INF("Disconnected: %s, reason 0x%02x %s", addr, reason, bt_hci_err_to_str(reason)); + + if (IS_ENABLED(CONFIG_BT_CENTRAL)) { + bt_conn_unref(conn); + } + + /* Publish disconnected */ + msg.event = BT_MGMT_DISCONNECTED; + msg.conn = conn; + + ret = zbus_chan_pub(&bt_mgmt_chan, &msg, K_NO_WAIT); + ERR_CHK(ret); + + if (IS_ENABLED(CONFIG_BT_PERIPHERAL)) { + ret = bt_mgmt_adv_start(0, NULL, 0, NULL, 0, true); + ERR_CHK(ret); + } + + /* The mutex for preventing the racing condition if two headset disconnected too close, + * cause the disconnected_cb() triggered in short time leads to duplicate scanning + * operation. + */ + k_mutex_lock(&mtx_duplicate_scan, K_FOREVER); + if (IS_ENABLED(CONFIG_BT_CENTRAL)) { + ret = bt_mgmt_scan_start(0, 0, BT_MGMT_SCAN_TYPE_CONN, NULL, BRDCAST_ID_NOT_USED); + if (ret && ret != -EALREADY) { + LOG_ERR("Failed to restart scanning: %d", ret); + } + } + k_mutex_unlock(&mtx_duplicate_scan); +} + +#if defined(CONFIG_BT_SMP) +static void security_changed_cb(struct bt_conn *conn, bt_security_t level, enum bt_security_err err) +{ + int ret; + struct bt_mgmt_msg msg; + + if (err) { + LOG_WRN("Security failed: level %d err %d %s", level, err, + bt_security_err_to_str(err)); + ret = bt_conn_disconnect(conn, BT_HCI_ERR_AUTH_FAIL); + if (ret) { + LOG_WRN("Failed to disconnect %d", ret); + } + } else { + LOG_DBG("Security changed: level %d", level); + /* Publish connected */ + msg.event = BT_MGMT_SECURITY_CHANGED; + msg.conn = conn; + + ret = zbus_chan_pub(&bt_mgmt_chan, &msg, K_NO_WAIT); + ERR_CHK(ret); + } +} +#endif /* defined(CONFIG_BT_SMP) */ + +static struct bt_conn_cb conn_callbacks = { + .connected = connected_cb, + .disconnected = disconnected_cb, +#if defined(CONFIG_BT_SMP) + .security_changed = security_changed_cb, +#endif /* defined(CONFIG_BT_SMP) */ +}; + +static void bt_enabled_cb(int err) +{ + if (err) { + LOG_ERR("Bluetooth init failed (err code: %d)", err); + ERR_CHK(err); + } + + k_sem_give(&sem_bt_enabled); + + LOG_DBG("Bluetooth initialized"); +} + +static int bonding_clear_check(void) +{ +#if CONFIG_BOARD_NRF5340_AUDIO_DK_NRF5340_CPUAPP + int ret; + bool pressed; + + ret = button_pressed(BUTTON_5, &pressed); + if (ret) { + return ret; + } + + if (pressed) { + ret = bt_mgmt_bonding_clear(); + return ret; + } + +#endif + return 0; +} + +/* This function generates a random address for bonding testing */ +static int random_static_addr_set(void) +{ + int ret; + static bt_addr_le_t addr; + + ret = bt_addr_le_create_static(&addr); + if (ret < 0) { + LOG_ERR("Failed to create address %d", ret); + return ret; + } + + ret = bt_id_create(&addr, NULL); + if (ret < 0) { + LOG_ERR("Failed to create ID %d", ret); + return ret; + } + + return 0; +} + +static int local_identity_addr_print(void) +{ + size_t num_ids = 0; + bt_addr_le_t addrs[CONFIG_BT_ID_MAX]; + char addr_str[BT_ADDR_LE_STR_LEN]; + + bt_id_get(NULL, &num_ids); + if (num_ids != CONFIG_BT_ID_MAX) { + LOG_ERR("The default config supports %d ids, but %d was found", CONFIG_BT_ID_MAX, + num_ids); + return -ENOMEM; + } + + bt_id_get(addrs, &num_ids); + + for (int i = 0; i < num_ids; i++) { + (void)bt_addr_le_to_str(&(addrs[i]), addr_str, BT_ADDR_LE_STR_LEN); + LOG_INF("Local identity addr: %s", addr_str); + } + + return 0; +} + +void bt_mgmt_num_conn_get(uint8_t *num_conn) +{ + bt_conn_foreach(BT_CONN_TYPE_LE, conn_state_connected_check, (void *)num_conn); +} + +int bt_mgmt_bonding_clear(void) +{ + int ret; + + if (IS_ENABLED(CONFIG_SETTINGS)) { + LOG_INF("Clearing all bonds"); + + ret = bt_unpair(BT_ID_DEFAULT, NULL); + if (ret) { + LOG_ERR("Failed to clear bonding: %d", ret); + return ret; + } + } + + return 0; +} + +int bt_mgmt_pa_sync_delete(struct bt_le_per_adv_sync *pa_sync) +{ + if (IS_ENABLED(CONFIG_BT_PER_ADV_SYNC)) { + int ret; + + ret = bt_le_per_adv_sync_delete(pa_sync); + if (ret) { + LOG_ERR("Failed to delete PA sync"); + return ret; + } + } else { + LOG_WRN("Periodic advertisement sync not enabled"); + return -ENOTSUP; + } + + return 0; +} + +int bt_mgmt_conn_disconnect(struct bt_conn *conn, uint8_t reason) +{ + if (IS_ENABLED(CONFIG_BT_CONN)) { + int ret; + + ret = bt_conn_disconnect(conn, reason); + if (ret) { + LOG_ERR("Failed to disconnect connection %p (%d)", (void *)conn, ret); + return ret; + } + } else { + LOG_WRN("BT conn not enabled"); + return -ENOTSUP; + } + + return 0; +} + +int bt_mgmt_init(void) +{ + int ret; + + ret = bt_enable(bt_enabled_cb); + if (ret) { + return ret; + } + + ret = k_sem_take(&sem_bt_enabled, K_MSEC(BT_ENABLE_TIMEOUT_MS)); + if (ret) { + LOG_ERR("bt_enable timed out"); + return ret; + } + + if (IS_ENABLED(CONFIG_TESTING_BLE_ADDRESS_RANDOM)) { + ret = random_static_addr_set(); + if (ret) { + return ret; + } + } + + if (IS_ENABLED(CONFIG_SETTINGS)) { + ret = settings_load(); + if (ret) { + return ret; + } + + ret = bonding_clear_check(); + if (ret) { + return ret; + } + + if (IS_ENABLED(CONFIG_TESTING_BLE_ADDRESS_RANDOM)) { + ret = bt_mgmt_bonding_clear(); + if (ret) { + return ret; + } + } + } + +#if defined(CONFIG_AUDIO_BT_MGMT_DFU) + bool pressed; + + ret = button_pressed(BUTTON_4, &pressed); + if (ret) { + return ret; + } + + if (pressed) { + ret = bt_mgmt_ctlr_cfg_init(false); + if (ret) { + return ret; + } + /* This call will not return */ + bt_mgmt_dfu_start(); + } + +#endif /* CONFIG_AUDIO_BT_MGMT_DFU */ + + ret = bt_mgmt_ctlr_cfg_init(IS_ENABLED(CONFIG_WDT_CTLR)); + if (ret) { + return ret; + } + + ret = local_identity_addr_print(); + if (ret) { + return ret; + } + + if (IS_ENABLED(CONFIG_BT_CONN)) { + bt_conn_cb_register(&conn_callbacks); + } + + if (IS_ENABLED(CONFIG_BT_PERIPHERAL) || IS_ENABLED(CONFIG_BT_BROADCASTER)) { + bt_mgmt_adv_init(); + } + + return 0; +} diff --git a/src/bluetooth/bt_management/bt_mgmt.h b/src/bluetooth/bt_management/bt_mgmt.h new file mode 100644 index 0000000..e0263d9 --- /dev/null +++ b/src/bluetooth/bt_management/bt_mgmt.h @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _BT_MGMT_H_ +#define _BT_MGMT_H_ + +#include +#include +#include + +#define LE_AUDIO_EXTENDED_ADV_NAME \ + BT_LE_ADV_PARAM(BT_LE_ADV_OPT_EXT_ADV | BT_LE_ADV_OPT_USE_NAME, \ + CONFIG_BLE_ACL_EXT_ADV_INT_MIN, CONFIG_BLE_ACL_EXT_ADV_INT_MAX, NULL) + +#define LE_AUDIO_EXTENDED_ADV_CONN_NAME \ + BT_LE_ADV_PARAM(BT_LE_ADV_OPT_EXT_ADV | BT_LE_ADV_OPT_CONN | BT_LE_ADV_OPT_USE_NAME, \ + CONFIG_BLE_ACL_EXT_ADV_INT_MIN, CONFIG_BLE_ACL_EXT_ADV_INT_MAX, NULL) + +#define LE_AUDIO_EXTENDED_ADV_CONN_NAME_FILTER \ + BT_LE_ADV_PARAM(BT_LE_ADV_OPT_EXT_ADV | BT_LE_ADV_OPT_CONN | BT_LE_ADV_OPT_USE_NAME | \ + BT_LE_ADV_OPT_FILTER_CONN, \ + CONFIG_BLE_ACL_EXT_ADV_INT_MIN, CONFIG_BLE_ACL_EXT_ADV_INT_MAX, NULL) + +#define LE_AUDIO_PERIODIC_ADV \ + BT_LE_PER_ADV_PARAM(CONFIG_BLE_ACL_PER_ADV_INT_MIN, CONFIG_BLE_ACL_PER_ADV_INT_MAX, \ + BT_LE_PER_ADV_OPT_NONE) + +#define BT_LE_ADV_FAST_CONN \ + BT_LE_ADV_PARAM(BT_LE_ADV_OPT_CONN, BT_GAP_ADV_FAST_INT_MIN_1, BT_GAP_ADV_FAST_INT_MAX_1, \ + NULL) + +/* Broadcast name can be max 32 bytes long, so this will be the limit for both. + * Add one for '\0' at the end. + */ +#define BLE_SEARCH_NAME_MAX_LEN 33 + +#if (CONFIG_SCAN_MODE_ACTIVE) +#define NRF5340_AUDIO_GATEWAY_SCAN_TYPE BT_LE_SCAN_TYPE_ACTIVE +#define NRF5340_AUDIO_GATEWAY_SCAN_PARAMS BT_LE_SCAN_ACTIVE +#elif (CONFIG_SCAN_MODE_PASSIVE) +#define NRF5340_AUDIO_GATEWAY_SCAN_TYPE BT_LE_SCAN_TYPE_PASSIVE +#define NRF5340_AUDIO_GATEWAY_SCAN_PARAMS BT_LE_SCAN_PASSIVE +#else +#error "Select either CONFIG_SCAN_MODE_ACTIVE or CONFIG_SCAN_MODE_PASSIVE" +#endif + +enum bt_mgmt_scan_type { + BT_MGMT_SCAN_TYPE_CONN = 1, + BT_MGMT_SCAN_TYPE_BROADCAST = 2, +}; + +#define BRDCAST_ID_NOT_USED (BT_AUDIO_BROADCAST_ID_MAX + 1) + +/** + * @brief Get the numbers of connected members of a given 'Set Identity Resolving Key' (SIRK). + * The SIRK shall be set through bt_mgmt_scan_sirk_set() before calling this function. + * + * @param[out] num_filled The number of connected set members. + */ +void bt_mgmt_set_size_filled_get(uint8_t *num_filled); + +/** + * @brief Set 'Set Identity Resolving Key' (SIRK). + * Used for searching for other member of the same set. + * + * @param[in] sirk Pointer to the Set Identity Resolving Key to store. + */ +void bt_mgmt_scan_sirk_set(uint8_t const *const sirk); + +/** + * @brief Load advertising data into an advertising buffer. + * + * @param[out] adv_buf Pointer to the advertising buffer to load. + * @param[in,out] index Next free index in the advertising buffer. + * @param[in] adv_buf_vacant Number of free advertising buffers. + * @param[in] data_len Length of the data. + * @param[in] type Type of the data. + * @param[in] data Data to store in the buffer, can be a pointer or value. + * + * @return 0 if success, error otherwise. + */ +int bt_mgmt_adv_buffer_put(struct bt_data *const adv_buf, uint32_t *index, size_t adv_buf_vacant, + size_t data_len, uint8_t type, void *data); + +/** + * @brief Start scanning for advertisements. + * + * @param[in] scan_intvl Scan interval in units of 0.625ms. + * Valid range: 0x4 - 0xFFFF; can be 0. + * @param[in] scan_win Scan window in units of 0.625ms. + * Valid range: 0x4 - 0xFFFF; can be 0. + * @param[in] type Type to scan for: ACL connection or broadcaster. + * @param[in] name Name to search for. Depending on @p type of search, + * device name or broadcast name. Can be max + * BLE_SEARCH_NAME_MAX_LEN long; everything beyond that value + * will be cropped. Can be NULL. Shall be '\0' terminated. + * @param[in] brdcast_id Broadcast ID to search for. Only valid if @p type is + * BT_MGMT_SCAN_TYPE_BROADCAST. If both @p name and @p brdcast_id are + * provided, then brdcast_id will be used. + * Set to BRDCAST_ID_NOT_USED if not in use. + * + * @note To restart scanning, call this function with all 0s and NULL, except for @p type. + * The same scanning parameters as when bt_mgmt_scan_start was last called will then + * be used. + * + * @return 0 if success, error otherwise. + */ +int bt_mgmt_scan_start(uint16_t scan_intvl, uint16_t scan_win, enum bt_mgmt_scan_type type, + char const *const name, uint32_t brdcast_id); + +/** + * @brief Add manufacturer ID UUID to the advertisement packet. + * + * @param[out] uuid_buf Buffer being populated with UUIDs. + * @param[in] company_id 16 bit UUID specific to the company. + * + * @return 0 for success, error otherwise. + */ +int bt_mgmt_manufacturer_uuid_populate(struct net_buf_simple *uuid_buf, uint16_t company_id); + +/** + * @brief Stop periodic advertising. + * + * @note Must be called before bt_mgmt_ext_adv_stop. + * + * @param[in] ext_adv_index Index of the advertising set to stop. + * + * @return 0 if success, error otherwise. + */ +int bt_mgmt_per_adv_stop(uint8_t ext_adv_index); + +/** + * @brief Stop extended advertising. + * + * @return 0 if success, error otherwise. + */ +int bt_mgmt_ext_adv_stop(uint8_t ext_adv_index); + +/** + * @brief Create and start advertising for an ACL connection. + * + * @param[in] ext_adv_index Index of the advertising set to start. + * @param[in] ext_adv The data to be put in the extended advertisement. + * @param[in] ext_adv_size Size of @p ext_adv. + * @param[in] per_adv The data for the periodic advertisement; can be NULL. + * @param[in] per_adv_size Size of @p per_adv. + * @param[in] connectable Specify if advertisement should be connectable or not. + * + * @note To restart advertising, call this function with all 0s and NULL, except for + * connectable. The same advertising parameters as when bt_mgmt_adv_start was last + * called will then be used. + * + * @return 0 if success, error otherwise. + */ +int bt_mgmt_adv_start(uint8_t ext_adv_index, const struct bt_data *ext_adv, size_t ext_adv_size, + const struct bt_data *per_adv, size_t per_adv_size, bool connectable); + +/** + * @brief Get the number of active connections. + * + * @param[out] num_conn The number of active connections. + */ +void bt_mgmt_num_conn_get(uint8_t *num_conn); + +/** + * @brief Clear all bonded devices. + * + * @return 0 if success, error otherwise. + */ +int bt_mgmt_bonding_clear(void); + +/** + * @brief Scan delegator feature initialization. + */ +void bt_mgmt_scan_delegator_init(void); + +/** + * @brief Get the pointer to broadcast code. + * + * @param[out] broadcast_code_ptr Pointer to the broadcast code. + */ +void bt_mgmt_broadcast_code_ptr_get(uint8_t **broadcast_code_ptr); + +/** + * @brief Delete a periodic advertisement sync. + * + * @param[in] pa_sync Pointer to the periodic advertisement sync to delete. + * + * @return 0 if success, error otherwise. + */ +int bt_mgmt_pa_sync_delete(struct bt_le_per_adv_sync *pa_sync); + +/** + * @brief Disconnect from a remote device or cancel the pending connection. + * + * @param[in] conn Connection to disconnect. + * @param[in] reason Reason code for the disconnection, as specified in + * HCI Error Codes, BT Core Spec [Vol 1, Part F]. + * + * @return 0 if success, error otherwise. + */ +int bt_mgmt_conn_disconnect(struct bt_conn *conn, uint8_t reason); + +/** + * @brief Initialize the Bluetooth management module. + * + * @return 0 if success, error otherwise. + */ +int bt_mgmt_init(void); + +#endif /* _BT_MGMT_H_ */ diff --git a/src/bluetooth/bt_management/controller_config/Kconfig b/src/bluetooth/bt_management/controller_config/Kconfig new file mode 100644 index 0000000..24166a1 --- /dev/null +++ b/src/bluetooth/bt_management/controller_config/Kconfig @@ -0,0 +1,83 @@ +# +# Copyright (c) 2023 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +menu "Controller config" + +#----------------------------------------------------------------------------# +menu "nRF21540" + +config NRF_21540_ACTIVE + def_bool $(shields_list_contains,nrf21540ek) + select EXPERIMENTAL + help + The front end module can help boost the TX power as high as 20 dBm. + +choice NRF_21540_MAIN_TX_POWER + prompt "TX power for the secondary channels" + default NRF_21540_MAIN_TX_POWER_10DBM + help + Set the TX power for the secondary Bluetooth LE channels (0-36). + Check your local regulations for max output power. If the + nRF21540 is used with the nRF5340 Audio DK the actual output power + will be about 25% lower due to the VDD being 1.8V instead of the + nominal 3.3V. + +config NRF_21540_MAIN_TX_POWER_0DBM + bool "0dBm" + +config NRF_21540_MAIN_TX_POWER_10DBM + bool "+10dBm" + +config NRF_21540_MAIN_TX_POWER_20DBM + bool "+20dBm" + +endchoice + +config NRF_21540_MAIN_DBM + int + default 0 if NRF_21540_MAIN_TX_POWER_0DBM + default 10 if NRF_21540_MAIN_TX_POWER_10DBM + default 20 if NRF_21540_MAIN_TX_POWER_20DBM + +choice NRF_21540_PRI_ADV_TX_POWER + prompt "TX power for the primary advertising channels" + default NRF_21540_PRI_ADV_TX_POWER_10DBM + help + Set the TX power for the primary Bluetooth LE advertising channels + (37, 38, 39). + Check your local regulations for max output power. If the + nRF21540 is used with the nRF5340 Audio DK the actual output power + will be about 25% lower due to the VDD being 1.8V instead of the + nominal 3.3V. + +config NRF_21540_PRI_ADV_TX_POWER_0DBM + bool "0dBm" + +config NRF_21540_PRI_ADV_TX_POWER_10DBM + bool "+10dBm" + +config NRF_21540_PRI_ADV_TX_POWER_20DBM + bool "+20dBm" + +endchoice + +config NRF_21540_PRI_ADV_DBM + int + default 0 if NRF_21540_PRI_ADV_TX_POWER_0DBM + default 10 if NRF_21540_PRI_ADV_TX_POWER_10DBM + default 20 if NRF_21540_PRI_ADV_TX_POWER_20DBM + +endmenu # nRF21540 + +#----------------------------------------------------------------------------# +menu "Log level" + +module = BT_MGMT_CTLR_CFG +module-str = bt-mgmt-ctlr-cfg +source "subsys/logging/Kconfig.template.log_config" + +endmenu # Log level +endmenu # Controller config diff --git a/src/bluetooth/bt_management/controller_config/bt_mgmt_ctlr_cfg.c b/src/bluetooth/bt_management/controller_config/bt_mgmt_ctlr_cfg.c new file mode 100644 index 0000000..bf38422 --- /dev/null +++ b/src/bluetooth/bt_management/controller_config/bt_mgmt_ctlr_cfg.c @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "bt_mgmt_ctlr_cfg_internal.h" + +#include +#include +#include +#include +#include + +#include "macros_common.h" + +#include +LOG_MODULE_REGISTER(bt_mgmt_ctlr_cfg, CONFIG_BT_MGMT_CTLR_CFG_LOG_LEVEL); + +#define COMPANY_ID_NORDIC 0x0059 + +#define WDT_TIMEOUT_MS 3000 +#define CTLR_POLL_INTERVAL_MS (WDT_TIMEOUT_MS - 1000) + +static struct k_work work_ctlr_poll; + +#define CTLR_POLL_WORK_STACK_SIZE 1024 + +K_THREAD_STACK_DEFINE(ctlr_poll_stack_area, CTLR_POLL_WORK_STACK_SIZE); + +struct k_work_q ctrl_poll_work_q; + +struct k_work_queue_config ctrl_poll_work_q_config = { + .name = "ctlr_poll", + .no_yield = false, +}; + +static void ctlr_poll_timer_handler(struct k_timer *timer_id); +static int wdt_ch_id; + +K_TIMER_DEFINE(ctlr_poll_timer, ctlr_poll_timer_handler, NULL); + +static void work_ctlr_poll_handler(struct k_work *work) +{ + int ret; + uint16_t manufacturer = 0; + + ret = bt_mgmt_ctlr_cfg_manufacturer_get(false, &manufacturer); + ERR_CHK_MSG(ret, "Failed to contact net core"); + + ret = task_wdt_feed(wdt_ch_id); + ERR_CHK_MSG(ret, "Failed to feed watchdog"); +} + +static void ctlr_poll_timer_handler(struct k_timer *timer_id) +{ + int ret; + + ret = k_work_submit_to_queue(&ctrl_poll_work_q, &work_ctlr_poll); + if (ret < 0) { + LOG_ERR("Work q submit failed: %d", ret); + } +} + +static void wdt_timeout_cb(int channel_id, void *user_data) +{ + ERR_CHK_MSG(-ETIMEDOUT, "No response from IPC or controller"); +} + +int bt_mgmt_ctlr_cfg_manufacturer_get(bool print_version, uint16_t *manufacturer) +{ + int ret; + struct net_buf *rsp; + + ret = bt_hci_cmd_send_sync(BT_HCI_OP_READ_LOCAL_VERSION_INFO, NULL, &rsp); + if (ret) { + return ret; + } + + struct bt_hci_rp_read_local_version_info *rp = (void *)rsp->data; + + if (print_version) { + if (rp->manufacturer == COMPANY_ID_NORDIC) { + /* NOTE: The string below is used by the Nordic CI system */ + LOG_INF("Controller: SoftDevice: Version %s (0x%02x), Revision %d", + bt_hci_get_ver_str(rp->hci_version), rp->hci_version, + rp->hci_revision); + } else { + LOG_ERR("Unsupported controller"); + return -EPERM; + } + } + + *manufacturer = sys_le16_to_cpu(rp->manufacturer); + + net_buf_unref(rsp); + + return 0; +} + +int bt_mgmt_ctlr_cfg_init(bool watchdog_enable) +{ + int ret; + uint16_t manufacturer = 0; + + ret = bt_mgmt_ctlr_cfg_manufacturer_get(true, &manufacturer); + if (ret) { + return ret; + } + + if (watchdog_enable) { + ret = task_wdt_init(NULL); + if (ret != 0) { + LOG_ERR("task wdt init failure: %d\n", ret); + return ret; + } + + wdt_ch_id = task_wdt_add(WDT_TIMEOUT_MS, wdt_timeout_cb, NULL); + if (wdt_ch_id < 0) { + return wdt_ch_id; + } + k_work_queue_init(&ctrl_poll_work_q); + + k_work_queue_start(&ctrl_poll_work_q, ctlr_poll_stack_area, + K_THREAD_STACK_SIZEOF(ctlr_poll_stack_area), + K_PRIO_PREEMPT(CONFIG_CTLR_POLL_WORK_Q_PRIO), + &ctrl_poll_work_q_config); + + k_work_init(&work_ctlr_poll, work_ctlr_poll_handler); + k_timer_start(&ctlr_poll_timer, K_MSEC(CTLR_POLL_INTERVAL_MS), + K_MSEC(CTLR_POLL_INTERVAL_MS)); + } + + return 0; +} diff --git a/src/bluetooth/bt_management/controller_config/bt_mgmt_ctlr_cfg_internal.h b/src/bluetooth/bt_management/controller_config/bt_mgmt_ctlr_cfg_internal.h new file mode 100644 index 0000000..a07d1f1 --- /dev/null +++ b/src/bluetooth/bt_management/controller_config/bt_mgmt_ctlr_cfg_internal.h @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _BT_MGMT_CTRL_CFG_INTERNAL_H_ +#define _BT_MGMT_CTRL_CFG_INTERNAL_H_ + +#include +#include + +/** + * @brief Get the Bluetooth controller manufacturer. + * + * @param[in] print_version Print the controller version. + * @param[out] manufacturer The controller manufacturer. + * + * @return 0 if success, error otherwise. + */ +int bt_mgmt_ctlr_cfg_manufacturer_get(bool print_version, uint16_t *manufacturer); + +/** + * @brief Configure the Bluetooth controller. + * + * @param[in] watchdog_enable If true, the function will, at given intervals, poll the controller + * to ensure it is still alive. + * + * @return 0 if success, error otherwise. + */ +int bt_mgmt_ctlr_cfg_init(bool watchdog_enable); + +#endif /* _BT_MGMT_CTRL_CFG_INTERNAL_H_ */ diff --git a/src/bluetooth/bt_management/dfu/Kconfig b/src/bluetooth/bt_management/dfu/Kconfig new file mode 100644 index 0000000..2cb2dd3 --- /dev/null +++ b/src/bluetooth/bt_management/dfu/Kconfig @@ -0,0 +1,40 @@ +# +# Copyright (c) 2023 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +menuconfig AUDIO_BT_MGMT_DFU + bool "Enable BT MGMT DFU module" + default n + select EXPERIMENTAL + select BT_DEVICE_NAME_DYNAMIC + select MCUMGR + select NET_BUF + select ZCBOR + select CRC + select MCUMGR_TRANSPORT_BT + select IMG_ERASE_PROGRESSIVELY + imply MCUMGR_TRANSPORT_BT_CONN_PARAM_CONTROL + imply IMG_MANAGER + imply STREAM_FLASH + imply FLASH_MAP + imply FLASH + imply MCUMGR_GRP_IMG + imply MCUMGR_GRP_OS + imply MCUMGR_GRP_OS_MCUMGR_PARAMS + imply MCUMGR_GRP_OS_BOOTLOADER_INFO + imply MCUMGR_TRANSPORT_BT_REASSEMBLY + depends on BT_PERIPHERAL + depends on BOOTLOADER_MCUBOOT + help + Enable the BT MGMT module. This module adds the DFU mode + that can be entered from the main application. + +if AUDIO_BT_MGMT_DFU + +module = BT_MGMT_DFU +module-str = bt-mgmt-dfu +source "subsys/logging/Kconfig.template.log_config" + +endif # AUDIO_BT_MGMT_DFU diff --git a/src/bluetooth/bt_management/dfu/bt_mgmt_dfu.c b/src/bluetooth/bt_management/dfu/bt_mgmt_dfu.c new file mode 100644 index 0000000..756eaf3 --- /dev/null +++ b/src/bluetooth/bt_management/dfu/bt_mgmt_dfu.c @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2022 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +/* Override compiler definition to use size-bounded string copying and concatenation function */ +#define _BSD_SOURCE +#include "bt_mgmt_dfu_internal.h" + +#include +#include +#include + +#include "string.h" +#include "macros_common.h" +#include "channel_assignment.h" + +#include +LOG_MODULE_REGISTER(bt_mgmt_dfu, CONFIG_BT_MGMT_DFU_LOG_LEVEL); + +/* These defined name only used by DFU */ +#define DEVICE_NAME_DFU CONFIG_BT_DFU_DEVICE_NAME +#define DEVICE_NAME_DFU_LEN (sizeof(DEVICE_NAME_DFU) - 1) + +static const struct bt_data ad_peer[] = { + BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)), + BT_DATA_BYTES(BT_DATA_UUID128_ALL, 0x84, 0xaa, 0x60, 0x74, 0x52, 0x8a, 0x8b, 0x86, 0xd3, + 0x4c, 0xb7, 0x1d, 0x1d, 0xdc, 0x53, 0x8d), +}; + +/* Set aside space for name to be in scan response */ +static struct bt_data sd_peer[1]; + +K_SEM_DEFINE(adv_sem, 1, 1); + +static void smp_adv(void) +{ + int ret; + + ret = bt_le_adv_start(BT_LE_ADV_CONN_FAST_2, ad_peer, ARRAY_SIZE(ad_peer), sd_peer, + ARRAY_SIZE(sd_peer)); + if (ret == -EALREADY) { + return; + } else if (ret) { + LOG_ERR("SMP_SVR Advertising failed to start (ret %d)", ret); + return; + } + + /* NOTE: The string below is used by the Nordic CI system */ + LOG_INF("Regular SMP_SVR advertising started"); +} + +/* These callbacks are to override callback registed in module le_audio_ */ +static void dfu_connected_cb(struct bt_conn *conn, uint8_t err) +{ + LOG_INF("SMP connected\n"); +} + +static void dfu_disconnected_cb(struct bt_conn *conn, uint8_t reason) +{ + LOG_INF("SMP disconnected 0x%02x\n", reason); +} + +static void dfu_recycled_cb(void) +{ + k_sem_give(&adv_sem); +} + +static struct bt_conn_cb dfu_conn_callbacks = { + .connected = dfu_connected_cb, + .disconnected = dfu_disconnected_cb, + .recycled = dfu_recycled_cb, +}; + +static void dfu_set_bt_name(void) +{ + int ret; + static char name[CONFIG_BT_DEVICE_NAME_MAX]; + + strlcpy(name, CONFIG_BT_DEVICE_NAME, CONFIG_BT_DEVICE_NAME_MAX); + ret = strlcat(name, "_", CONFIG_BT_DEVICE_NAME_MAX); + if (ret >= CONFIG_BT_DEVICE_NAME_MAX) { + LOG_ERR("Failed to set full BT name, will truncate"); + } + +#if (CONFIG_AUDIO_DEV == GATEWAY) + ret = strlcat(name, GW_TAG, CONFIG_BT_DEVICE_NAME_MAX); + if (ret >= CONFIG_BT_DEVICE_NAME_MAX) { + LOG_ERR("Failed to set full BT name, will truncate"); + } +#else + enum audio_channel channel; + + channel_assignment_get(&channel); + + if (channel == AUDIO_CH_L) { + ret = strlcat(name, CH_L_TAG, CONFIG_BT_DEVICE_NAME_MAX); + if (ret >= CONFIG_BT_DEVICE_NAME_MAX) { + LOG_ERR("Failed to set full BT name, will truncate"); + } + } else { + ret = strlcat(name, CH_R_TAG, CONFIG_BT_DEVICE_NAME_MAX); + if (ret >= CONFIG_BT_DEVICE_NAME_MAX) { + LOG_ERR("Failed to set full BT name, will truncate"); + } + } + +#endif + ret = strlcat(name, "_DFU", CONFIG_BT_DEVICE_NAME_MAX); + if (ret >= CONFIG_BT_DEVICE_NAME_MAX) { + LOG_ERR("Failed to set full BT name, will truncate"); + } + + sd_peer[0].type = BT_DATA_NAME_COMPLETE; + sd_peer[0].data_len = strlen(name); + sd_peer[0].data = name; +} + +void bt_mgmt_dfu_start(void) +{ + LOG_INF("Entering SMP server mode"); + + bt_conn_cb_register(&dfu_conn_callbacks); + dfu_set_bt_name(); + + while (1) { + /* In DFU mode, the device should always advertise */ + k_sem_take(&adv_sem, K_FOREVER); + smp_adv(); + } +} diff --git a/src/bluetooth/bt_management/dfu/bt_mgmt_dfu_internal.h b/src/bluetooth/bt_management/dfu/bt_mgmt_dfu_internal.h new file mode 100644 index 0000000..3044907 --- /dev/null +++ b/src/bluetooth/bt_management/dfu/bt_mgmt_dfu_internal.h @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2022 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _BT_MGMT_DFU_INTERNAL_H_ +#define _BT_MGMT_DFU_INTERNAL_H_ + +/** + * @brief Enter the DFU mode. Advertise the SMP_SVR service only. + * + * @note This call does not return. + */ +void bt_mgmt_dfu_start(void); + +#endif /* _BT_MGMT_DFU_INTERNAL_H_ */ diff --git a/src/bluetooth/bt_management/scanning/Kconfig b/src/bluetooth/bt_management/scanning/Kconfig new file mode 100644 index 0000000..ec8c230 --- /dev/null +++ b/src/bluetooth/bt_management/scanning/Kconfig @@ -0,0 +1,59 @@ +# +# Copyright (c) 2023 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +rsource "Kconfig.defaults" + +menu "Scanning" + +choice NRF5340_AUDIO_GATEWAY_SCAN_MODE + prompt "Select Scan Mode to find Unicast Headset" + default SCAN_MODE_PASSIVE + +config SCAN_MODE_PASSIVE + bool "Passive Scan" + +config SCAN_MODE_ACTIVE + bool "Active Scan" + +endchoice + +#----------------------------------------------------------------------------# +menu "Connection" + +config BLE_ACL_CONN_INTERVAL + int "Bluetooth LE ACL Connection Interval (x*1.25ms)" + default 8 + help + This interval should be a multiple of the ISO interval used. The recommendation is to + increase the interval to something like BLE_ACL_CONN_INTERVAL_SLOW after the discovery + process is done, to free up time on air. + +config BLE_ACL_CONN_INTERVAL_SLOW + int "Bluetooth LE ACL Connection Interval (x*1.25ms)" + default 72 + help + This interval should be a multiple of the ISO interval used. 72*1.25=90 which will + suit both 7.5ms and 10ms. + +config BLE_ACL_SLAVE_LATENCY + int "Bluetooth LE Slave Latency" + default 0 + +config BLE_ACL_SUP_TIMEOUT + int "Bluetooth LE Supervision Timeout (x*10ms)" + default 100 + +endmenu # Connection + +#----------------------------------------------------------------------------# +menu "Log level" + +module = BT_MGMT_SCAN +module-str = bt-mgmt-scan +source "subsys/logging/Kconfig.template.log_config" + +endmenu # Log level +endmenu # Scanning diff --git a/src/bluetooth/bt_management/scanning/Kconfig.defaults b/src/bluetooth/bt_management/scanning/Kconfig.defaults new file mode 100644 index 0000000..a742379 --- /dev/null +++ b/src/bluetooth/bt_management/scanning/Kconfig.defaults @@ -0,0 +1,18 @@ +# +# Copyright (c) 2023 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +if BT_OBSERVER + +config BT_BACKGROUND_SCAN_INTERVAL + default 32 + +config BT_BACKGROUND_SCAN_WINDOW + default 32 + +endif # BT_OBSERVER + +config BT_SCAN_WITH_IDENTITY + default y diff --git a/src/bluetooth/bt_management/scanning/bt_mgmt_scan.c b/src/bluetooth/bt_management/scanning/bt_mgmt_scan.c new file mode 100644 index 0000000..5466c1b --- /dev/null +++ b/src/bluetooth/bt_management/scanning/bt_mgmt_scan.c @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "bt_mgmt.h" + +#include +#include + +#include "bt_mgmt_scan_for_broadcast_internal.h" +#include "bt_mgmt_scan_for_conn_internal.h" + +#include +LOG_MODULE_REGISTER(bt_mgmt_scan, CONFIG_BT_MGMT_SCAN_LOG_LEVEL); + +static char srch_name[BLE_SEARCH_NAME_MAX_LEN]; + +static void addr_print(void) +{ + char addr_str[BT_ADDR_LE_STR_LEN]; + static struct bt_le_oob _oob = {.addr = 0}; + + /* NOTE: We are using an internal struct here to get the address without forcing the + * RPA to time out, should be changed once k_forever bug (DRGN-21459) has been fixed + */ + bt_addr_le_copy(&_oob.addr, &bt_dev.random_addr); + + (void)bt_addr_le_to_str(&_oob.addr, addr_str, BT_ADDR_LE_STR_LEN); + LOG_INF("Local addr: %s. May time out. Updates not printed", addr_str); +} + +int bt_mgmt_scan_start(uint16_t scan_intvl, uint16_t scan_win, enum bt_mgmt_scan_type type, + char const *const name, uint32_t brdcast_id) +{ + int ret; + + static int scan_interval = CONFIG_BT_BACKGROUND_SCAN_INTERVAL; + static int scan_window = CONFIG_BT_BACKGROUND_SCAN_WINDOW; + + /* Only change search name if a new name has been supplied */ + if (name != NULL) { + size_t name_size = MIN(strlen(name), BLE_SEARCH_NAME_MAX_LEN - 1); + + memcpy(srch_name, name, name_size); + srch_name[name_size] = '\0'; + } + + if (scan_intvl != 0) { + scan_interval = scan_intvl; + } + + if (scan_win != 0) { + scan_window = scan_win; + } + + struct bt_le_scan_param *scan_param = + BT_LE_SCAN_PARAM(NRF5340_AUDIO_GATEWAY_SCAN_TYPE, BT_LE_SCAN_OPT_FILTER_DUPLICATE, + scan_interval, scan_window); + + if (type == BT_MGMT_SCAN_TYPE_CONN && IS_ENABLED(CONFIG_BT_CENTRAL)) { + ret = bt_mgmt_scan_for_conn_start(scan_param, srch_name); + } else if (type == BT_MGMT_SCAN_TYPE_BROADCAST && + IS_ENABLED(CONFIG_BT_BAP_BROADCAST_SINK)) { + ret = bt_mgmt_scan_for_broadcast_start(scan_param, srch_name, brdcast_id); + } else { + LOG_WRN("Invalid scan type: %d, scan not started", type); + return -EINVAL; + } + + if (ret) { + return ret; + } + + addr_print(); + + /* NOTE: The string below is used by the Nordic CI system */ + LOG_INF("Scanning successfully started"); + return 0; +} diff --git a/src/bluetooth/bt_management/scanning/bt_mgmt_scan_for_broadcast.c b/src/bluetooth/bt_management/scanning/bt_mgmt_scan_for_broadcast.c new file mode 100644 index 0000000..cce523a --- /dev/null +++ b/src/bluetooth/bt_management/scanning/bt_mgmt_scan_for_broadcast.c @@ -0,0 +1,477 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "bt_mgmt_scan_for_broadcast_internal.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "bt_mgmt.h" +#include "macros_common.h" +#include "zbus_common.h" + +#include +LOG_MODULE_DECLARE(bt_mgmt_scan); + +/* Any value above 0xFFFFFF is invalid, so one can use 0xFFFFFFFF to denote + * an invalid broadcast ID. + */ +#define INVALID_BROADCAST_ID 0xFFFFFFFF +#define PA_SYNC_SKIP 2 +/* Similar to retries for connections */ +#define PA_SYNC_INTERVAL_TO_TIMEOUT_RATIO 20 +#define BIS_SYNC_STATE_NOT_SYNCED 0 + +ZBUS_CHAN_DECLARE(bt_mgmt_chan); + +struct bt_le_scan_cb scan_callback; +static bool scan_cb_registered; +static bool scan_dlg_cb_registered; +static bool sync_cb_registered; +static char const *srch_name; +static uint32_t srch_brdcast_id = BRDCAST_ID_NOT_USED; +static struct bt_le_per_adv_sync *pa_sync; +static const struct bt_bap_scan_delegator_recv_state *req_recv_state; +static uint8_t bt_mgmt_broadcast_code[BT_ISO_BROADCAST_CODE_SIZE]; + +struct broadcast_source { + char name[BLE_SEARCH_NAME_MAX_LEN]; + uint32_t id; +}; + +static struct broadcast_source brcast_src_info; + +static void scan_restart_worker(struct k_work *work) +{ + int ret; + + ret = bt_le_scan_stop(); + if (ret && ret != -EALREADY) { + LOG_WRN("Stop scan failed: %d", ret); + } + + /* Delete pending PA sync before restarting scan */ + ret = bt_mgmt_pa_sync_delete(pa_sync); + if (ret) { + LOG_WRN("Failed to delete pending PA sync: %d", ret); + } + + ret = bt_mgmt_scan_start(0, 0, BT_MGMT_SCAN_TYPE_BROADCAST, NULL, BRDCAST_ID_NOT_USED); + if (ret) { + LOG_WRN("Failed to restart scanning for broadcast: %d", ret); + } +} + +K_WORK_DEFINE(scan_restart_work, scan_restart_worker); + +static void pa_sync_timeout(struct k_timer *timer) +{ + LOG_WRN("PA sync create timed out, restarting scanning"); + + k_work_submit(&scan_restart_work); +} + +K_TIMER_DEFINE(pa_sync_timer, pa_sync_timeout, NULL); + +static uint16_t interval_to_sync_timeout(uint16_t interval) +{ + uint32_t interval_ms; + uint32_t timeout; + + /* Add retries and convert to unit in 10's of ms */ + interval_ms = BT_GAP_PER_ADV_INTERVAL_TO_MS(interval); + timeout = (interval_ms * PA_SYNC_INTERVAL_TO_TIMEOUT_RATIO) / 10; + + /* Enforce restraints */ + timeout = CLAMP(timeout, BT_GAP_PER_ADV_MIN_TIMEOUT, BT_GAP_PER_ADV_MAX_TIMEOUT); + + return (uint16_t)timeout; +} + +static void periodic_adv_sync(const struct bt_le_scan_recv_info *info, + struct broadcast_source source) +{ + int ret; + struct bt_le_per_adv_sync_param param; + + bt_le_scan_cb_unregister(&scan_callback); + scan_cb_registered = false; + + bt_addr_le_copy(¶m.addr, info->addr); + param.options = 0; + param.sid = info->sid; + param.skip = PA_SYNC_SKIP; + param.timeout = interval_to_sync_timeout(info->interval); + + /* Set timeout to same value as PA sync timeout in ms */ + k_timer_start(&pa_sync_timer, K_MSEC(param.timeout * 10), K_NO_WAIT); + + ret = bt_le_per_adv_sync_create(¶m, &pa_sync); + if (ret) { + LOG_ERR("Could not sync to PA: %d", ret); + ret = bt_mgmt_pa_sync_delete(pa_sync); + if (ret) { + LOG_ERR("Could not delete PA sync: %d", ret); + } + return; + } + brcast_src_info = source; +} + +/** + * @brief Check and parse advertising data for broadcast name and ID. + * + * @param[in] data Advertising data to check and parse. + * @param[out] user_data Will contain pointer to broadcast_source struct to be populated. + * + * @retval true Continue to parse LTVs. + * @retval false Stop parsing LTVs. + */ +static bool scan_check_broadcast_source(struct bt_data *data, void *user_data) +{ + struct broadcast_source *source = (struct broadcast_source *)user_data; + struct bt_uuid_16 adv_uuid; + + if (data->type == BT_DATA_BROADCAST_NAME && data->data_len) { + /* Ensure that broadcast name is at least one character shorter than the value of + * BLE_SEARCH_NAME_MAX_LEN + */ + if (data->data_len < BLE_SEARCH_NAME_MAX_LEN) { + memcpy(source->name, data->data, data->data_len); + source->name[data->data_len] = '\0'; + } + + return true; + } + + if (data->type != BT_DATA_SVC_DATA16) { + return true; + } + + if (data->data_len < BT_UUID_SIZE_16 + BT_AUDIO_BROADCAST_ID_SIZE) { + return true; + } + + if (!bt_uuid_create(&adv_uuid.uuid, data->data, BT_UUID_SIZE_16)) { + return false; + } + + if (bt_uuid_cmp(&adv_uuid.uuid, BT_UUID_BROADCAST_AUDIO)) { + return true; + } + + source->id = sys_get_le24(data->data + BT_UUID_SIZE_16); + + return true; +} + +/** + * @brief Callback handler for scan receive when scanning for broadcasters. + * + * @param[in] info Advertiser packet and scan response information. + * @param[in] ad Received advertising data. + */ +static void scan_recv_cb(const struct bt_le_scan_recv_info *info, struct net_buf_simple *ad) +{ + struct broadcast_source source = {.id = INVALID_BROADCAST_ID}; + static bool id_change_printed; + + /* We are only interested in non-connectable periodic advertisers */ + if ((info->adv_props & BT_GAP_ADV_PROP_CONNECTABLE) || info->interval == 0) { + return; + } + + bt_data_parse(ad, scan_check_broadcast_source, (void *)&source); + + if (source.id == INVALID_BROADCAST_ID) { + return; + } + + if (srch_brdcast_id < BRDCAST_ID_NOT_USED) { + /* Valid srch_brdcast_id supplied */ + if (source.id != srch_brdcast_id) { + /* Broadcaster does not match src_brdcast_id */ + if (!id_change_printed && + strncmp(source.name, srch_name, BLE_SEARCH_NAME_MAX_LEN) == 0) { + LOG_INF("%s found with ID: 0x%06x\r\n" + "Looking for ID: 0x%06x. Broadcaster may have changed ID", + source.name, source.id, srch_brdcast_id); + id_change_printed = true; + } + return; + } + + } else if (strncmp(source.name, srch_name, BLE_SEARCH_NAME_MAX_LEN) != 0) { + /* Broadcaster does not match src_name */ + return; + } + + LOG_DBG("Broadcast source %s found, id: 0x%06x", source.name, source.id); + id_change_printed = false; + periodic_adv_sync(info, source); +} + +static void pa_synced_cb(struct bt_le_per_adv_sync *sync, + struct bt_le_per_adv_sync_synced_info *info) +{ + int ret; + struct bt_mgmt_msg msg; + + char addr_str[BT_ADDR_LE_STR_LEN]; + (void)bt_addr_le_to_str(&sync->addr, addr_str, BT_ADDR_LE_STR_LEN); + LOG_INF("PA synced to name: %s, id: 0x%06x, addr: %s", brcast_src_info.name, + brcast_src_info.id, addr_str); + + k_timer_stop(&pa_sync_timer); + + ret = bt_le_scan_stop(); + if (ret && ret != -EALREADY) { + LOG_WRN("Stop scan failed: %d", ret); + } + + msg.event = BT_MGMT_PA_SYNCED; + msg.pa_sync = sync; + msg.broadcast_id = brcast_src_info.id; + + ret = zbus_chan_pub(&bt_mgmt_chan, &msg, K_NO_WAIT); + ERR_CHK(ret); +} + +static void pa_sync_terminated_cb(struct bt_le_per_adv_sync *sync, + const struct bt_le_per_adv_sync_term_info *info) +{ + int ret; + struct bt_mgmt_msg msg; + + LOG_DBG("Periodic advertising sync lost"); + + msg.event = BT_MGMT_PA_SYNC_LOST; + msg.pa_sync = sync; + msg.pa_sync_term_reason = info->reason; + + ret = zbus_chan_pub(&bt_mgmt_chan, &msg, K_NO_WAIT); + ERR_CHK(ret); +} + +static struct bt_le_per_adv_sync_cb sync_callbacks = { + .synced = pa_synced_cb, + .term = pa_sync_terminated_cb, +}; + +static void pa_timer_handler(struct k_work *work) +{ + int ret; + + if (req_recv_state != NULL) { + enum bt_bap_pa_state pa_state; + + if (req_recv_state->pa_sync_state == BT_BAP_PA_STATE_INFO_REQ) { + pa_state = BT_BAP_PA_STATE_NO_PAST; + } else { + pa_state = BT_BAP_PA_STATE_FAILED; + } + + ret = bt_bap_scan_delegator_set_pa_state(req_recv_state->src_id, pa_state); + if (ret) { + LOG_ERR("set PA state to %d failed, err = %d", pa_state, ret); + } + } +} + +static K_WORK_DELAYABLE_DEFINE(pa_timer, pa_timer_handler); + +/** + * @brief Subscribe to periodic advertising sync transfer (PAST). + * + * @param[in] conn Pointer to the connection object. + * @param[in] pa_interval Periodic advertising interval. + * @return 0 if success, error otherwise. + */ +static int pa_sync_past(struct bt_conn *conn, uint16_t pa_interval) +{ + int ret; + struct bt_le_per_adv_sync_transfer_param param = {0}; + + param.skip = PA_SYNC_SKIP; + param.timeout = interval_to_sync_timeout(pa_interval); + + ret = bt_le_per_adv_sync_transfer_subscribe(conn, ¶m); + if (ret) { + LOG_WRN("Could not do PAST subscribe: %d", ret); + return ret; + } + + LOG_DBG("Syncing with PAST: %d", ret); + + /* param.timeout is scaled in 10ms, so we need to *10 when we put it into K_MSEC() */ + (void)k_work_reschedule(&pa_timer, K_MSEC(param.timeout * 10)); + + return 0; +} + +static int pa_sync_req_cb(struct bt_conn *conn, + const struct bt_bap_scan_delegator_recv_state *recv_state, + bool past_avail, uint16_t pa_interval) +{ + int ret; + + req_recv_state = recv_state; + + if (recv_state->pa_sync_state == BT_BAP_PA_STATE_SYNCED || + recv_state->pa_sync_state == BT_BAP_PA_STATE_INFO_REQ) { + LOG_DBG("Already syncing"); + /* TODO: Terminate existing sync and then sync to new?*/ + return -EALREADY; + } + + LOG_INF("broadcast ID received = %X", recv_state->broadcast_id); + brcast_src_info.id = recv_state->broadcast_id; + + if (IS_ENABLED(CONFIG_BT_PER_ADV_SYNC_TRANSFER_RECEIVER) && past_avail) { + ret = pa_sync_past(conn, pa_interval); + if (ret) { + LOG_ERR("Subscribe to PA sync PAST failed, ret = %d", ret); + return ret; + } + + ret = bt_bap_scan_delegator_set_pa_state(req_recv_state->src_id, + BT_BAP_PA_STATE_INFO_REQ); + if (ret) { + LOG_ERR("Set PA state to INFO_REQ failed, err = %d", ret); + return ret; + } + + } else if (brcast_src_info.id != INVALID_BROADCAST_ID) { + ret = bt_mgmt_scan_start(0, 0, BT_MGMT_SCAN_TYPE_BROADCAST, NULL, + brcast_src_info.id); + return ret; + } + + return 0; +} + +static int pa_sync_term_req_cb(struct bt_conn *conn, + const struct bt_bap_scan_delegator_recv_state *recv_state) +{ + int ret; + struct bt_mgmt_msg msg; + + msg.event = BT_MGMT_BROADCAST_SINK_DISABLE; + ret = zbus_chan_pub(&bt_mgmt_chan, &msg, K_NO_WAIT); + ERR_CHK(ret); + + return 0; +} + +static void broadcast_code_cb(struct bt_conn *conn, + const struct bt_bap_scan_delegator_recv_state *recv_state, + const uint8_t broadcast_code[BT_ISO_BROADCAST_CODE_SIZE]) +{ + int ret; + struct bt_mgmt_msg msg; + + LOG_DBG("Broadcast code received for %p", (void *)recv_state); + memcpy(bt_mgmt_broadcast_code, broadcast_code, BT_ISO_BROADCAST_CODE_SIZE); + + msg.event = BT_MGMT_BROADCAST_CODE_RECEIVED; + ret = zbus_chan_pub(&bt_mgmt_chan, &msg, K_NO_WAIT); + ERR_CHK(ret); +} + +static int bis_sync_req_cb(struct bt_conn *conn, + const struct bt_bap_scan_delegator_recv_state *recv_state, + const uint32_t bis_sync_req[CONFIG_BT_BAP_BASS_MAX_SUBGROUPS]) +{ + int ret; + struct bt_mgmt_msg msg; + + /* Only support one subgroup for now */ + LOG_DBG("BIS sync request received for %p: 0x%08x", (void *)recv_state, bis_sync_req[0]); + if (bis_sync_req[0] == BIS_SYNC_STATE_NOT_SYNCED) { + msg.event = BT_MGMT_BROADCAST_SINK_DISABLE; + ret = zbus_chan_pub(&bt_mgmt_chan, &msg, K_NO_WAIT); + ERR_CHK(ret); + } + + return 0; +} + +static struct bt_bap_scan_delegator_cb scan_delegator_cbs = { + .pa_sync_req = pa_sync_req_cb, + .pa_sync_term_req = pa_sync_term_req_cb, + .broadcast_code = broadcast_code_cb, + .bis_sync_req = bis_sync_req_cb, +}; + +void bt_mgmt_broadcast_code_ptr_get(uint8_t **broadcast_code_ptr) +{ + if (broadcast_code_ptr == NULL) { + LOG_ERR("Null pointer given"); + return; + } + + *broadcast_code_ptr = bt_mgmt_broadcast_code; +} + +void bt_mgmt_scan_delegator_init(void) +{ + if (!scan_dlg_cb_registered) { + bt_bap_scan_delegator_register(&scan_delegator_cbs); + scan_dlg_cb_registered = true; + } + + if (!sync_cb_registered) { + bt_le_per_adv_sync_cb_register(&sync_callbacks); + sync_cb_registered = true; + } +} + +int bt_mgmt_scan_for_broadcast_start(struct bt_le_scan_param *scan_param, char const *const name, + uint32_t brdcast_id) +{ + int ret; + + if (!sync_cb_registered) { + bt_le_per_adv_sync_cb_register(&sync_callbacks); + sync_cb_registered = true; + } + + if (!scan_cb_registered) { + scan_callback.recv = scan_recv_cb; + bt_le_scan_cb_register(&scan_callback); + scan_cb_registered = true; + } else { + if (name == srch_name && brdcast_id == BRDCAST_ID_NOT_USED) { + return -EALREADY; + } + /* Might already be scanning, stop current scan to update param in case it has + * changed. + */ + ret = bt_le_scan_stop(); + if (ret && ret != -EALREADY) { + LOG_ERR("Failed to stop scan: %d", ret); + return ret; + } + } + + srch_name = name; + if (brdcast_id != BRDCAST_ID_NOT_USED) { + srch_brdcast_id = brdcast_id; + } + + ret = bt_le_scan_start(scan_param, NULL); + if (ret) { + return ret; + } + + return 0; +} diff --git a/src/bluetooth/bt_management/scanning/bt_mgmt_scan_for_broadcast_internal.h b/src/bluetooth/bt_management/scanning/bt_mgmt_scan_for_broadcast_internal.h new file mode 100644 index 0000000..f4c89ea --- /dev/null +++ b/src/bluetooth/bt_management/scanning/bt_mgmt_scan_for_broadcast_internal.h @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _BT_MGMT_SCAN_FOR_BROADCAST_INTERNAL_H_ +#define _BT_MGMT_SCAN_FOR_BROADCAST_INTERNAL_H_ + +#include + +/** + * @brief Scan for a broadcaster with the given @p name. + * + * @param[in] scan_param Pointer to the struct containing parameters to use. + * @param[in] name Broadcast name to search for. + * @param[in] brdcast_id Broadcast ID to search for. + * + * @return 0 if success, error otherwise. + */ +int bt_mgmt_scan_for_broadcast_start(struct bt_le_scan_param *scan_param, char const *const name, + uint32_t brdcast_id); + +#endif /* _BT_MGMT_SCAN_FOR_BROADCAST_INTERNAL_H_ */ diff --git a/src/bluetooth/bt_management/scanning/bt_mgmt_scan_for_conn.c b/src/bluetooth/bt_management/scanning/bt_mgmt_scan_for_conn.c new file mode 100644 index 0000000..425e073 --- /dev/null +++ b/src/bluetooth/bt_management/scanning/bt_mgmt_scan_for_conn.c @@ -0,0 +1,363 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "bt_mgmt_scan_for_conn_internal.h" + +#include +#include +#include +#include + +/* Will become public when https://github.com/zephyrproject-rtos/zephyr/pull/73445 is merged */ +#include <../subsys/bluetooth/audio/csip_internal.h> + +#include "bt_mgmt.h" + +#include +LOG_MODULE_DECLARE(bt_mgmt_scan); + +#define CONNECTION_PARAMETERS \ + BT_LE_CONN_PARAM(CONFIG_BLE_ACL_CONN_INTERVAL, CONFIG_BLE_ACL_CONN_INTERVAL, \ + CONFIG_BLE_ACL_SLAVE_LATENCY, CONFIG_BLE_ACL_SUP_TIMEOUT) + +static uint8_t bonded_num; +static struct bt_le_scan_cb scan_callback; +static bool cb_registered; +static char const *srch_name; +static uint8_t const *server_sirk; + +static void bond_check(const struct bt_bond_info *info, void *user_data) +{ + char addr_buf[BT_ADDR_LE_STR_LEN]; + + bt_addr_le_to_str(&info->addr, addr_buf, BT_ADDR_LE_STR_LEN); + + LOG_DBG("Stored bonding found: %s", addr_buf); + bonded_num++; +} + +static void bond_connect(const struct bt_bond_info *bond_info, void *user_data) +{ + int ret; + const bt_addr_le_t *adv_addr = user_data; + struct bt_conn *conn = NULL; + char addr_string[BT_ADDR_LE_STR_LEN]; + + if (!bt_addr_le_cmp(&bond_info->addr, adv_addr)) { + LOG_DBG("Found bonded device"); + + /* Check if the device is still connected due to waiting for ACL timeout */ + struct bt_conn *bonded_conn = + bt_conn_lookup_addr_le(BT_ID_DEFAULT, &bond_info->addr); + struct bt_conn_info conn_info; + + if (bonded_conn != NULL) { + ret = bt_conn_get_info(bonded_conn, &conn_info); + if (ret == 0 && conn_info.state == BT_CONN_STATE_CONNECTED) { + LOG_DBG("Trying to connect to an already connected conn"); + bt_conn_unref(bonded_conn); + return; + } + + /* Unref is needed due to bt_conn_lookup */ + bt_conn_unref(bonded_conn); + } + + bt_le_scan_cb_unregister(&scan_callback); + cb_registered = false; + + ret = bt_le_scan_stop(); + if (ret) { + LOG_WRN("Stop scan failed: %d", ret); + } + + bt_addr_le_to_str(adv_addr, addr_string, BT_ADDR_LE_STR_LEN); + + LOG_INF("Creating connection to bonded device: %s", addr_string); + + ret = bt_conn_le_create(adv_addr, BT_CONN_LE_CREATE_CONN, CONNECTION_PARAMETERS, + &conn); + if (ret) { + LOG_WRN("Create ACL connection failed: %d", ret); + + ret = bt_mgmt_scan_start(0, 0, BT_MGMT_SCAN_TYPE_CONN, NULL, + BRDCAST_ID_NOT_USED); + if (ret) { + LOG_ERR("Failed to restart scanning: %d", ret); + } + } + } +} + +/** + * @brief Check if the address belongs to an already connected device. + * + * @param[in] addr Address to check. + * + * @retval false No device in connected state with that address. + * @retval true Device found. + */ +static bool conn_exist_check(bt_addr_le_t *addr) +{ + int ret; + struct bt_conn_info info; + struct bt_conn *existing_conn = bt_conn_lookup_addr_le(BT_ID_DEFAULT, addr); + + if (existing_conn != NULL) { + ret = bt_conn_get_info(existing_conn, &info); + if (ret == 0 && info.state == BT_CONN_STATE_CONNECTED) { + LOG_DBG("Trying to connect to an already connected conn"); + bt_conn_unref(existing_conn); + return true; + } else if (ret) { + LOG_WRN("Failed to get info from conn: %d", ret); + } + + /* Unref is needed due to bt_conn_lookup */ + bt_conn_unref(existing_conn); + } + + /* No existing connection found with that address */ + return false; +} + +/** + * @brief Check the advertising data for the matching device name. + * + * @param[in] data The advertising data to be checked. + * @param[in] user_data Pointer to the address. + * + * @retval false Stop going through adv data. + * @retval true Continue checking the data. + */ +static bool device_name_check(struct bt_data *data, void *user_data) +{ + int ret; + bt_addr_le_t *addr = user_data; + struct bt_conn *conn = NULL; + char addr_string[BT_ADDR_LE_STR_LEN]; + + /* We only care about LTVs with name */ + if (data->type == BT_DATA_NAME_COMPLETE || data->type == BT_DATA_NAME_SHORTENED) { + size_t srch_name_size = strlen(srch_name); + if ((data->data_len == srch_name_size) && + (memcmp(srch_name, data->data, srch_name_size) == 0)) { + /* Check if the device is still connected due to waiting for ACL timeout */ + if (conn_exist_check(addr)) { + /* Device is already connected, stop parsing the adv data */ + return false; + } + + LOG_DBG("Device found: %s", srch_name); + + bt_le_scan_cb_unregister(&scan_callback); + cb_registered = false; + + ret = bt_le_scan_stop(); + if (ret) { + LOG_ERR("Stop scan failed: %d", ret); + } + + bt_addr_le_to_str(addr, addr_string, BT_ADDR_LE_STR_LEN); + + LOG_INF("Creating connection to device: %s", addr_string); + + ret = bt_conn_le_create(addr, BT_CONN_LE_CREATE_CONN, CONNECTION_PARAMETERS, + &conn); + if (ret) { + LOG_ERR("Could not init connection: %d", ret); + + ret = bt_mgmt_scan_start(0, 0, BT_MGMT_SCAN_TYPE_CONN, NULL, + BRDCAST_ID_NOT_USED); + if (ret) { + LOG_ERR("Failed to restart scanning: %d", ret); + } + } + + return false; + } + } + + return true; +} + +/** + * @brief Check the advertising data for the matching 'Set Identity Resolving Key' (SIRK). + * + * @param[in] data The advertising data to be checked. + * @param[in] user_data Pointer to the address. + * + * @retval false Stop going through adv data. + * @retval true Continue checking the data. + */ +static bool csip_found(struct bt_data *data, void *user_data) +{ + int ret; + bt_addr_le_t *addr = user_data; + struct bt_conn *conn = NULL; + char addr_string[BT_ADDR_LE_STR_LEN]; + + if (!bt_csip_set_coordinator_is_set_member(server_sirk, data)) { + /* This part of the data doesn't contain matching SIRK, continue parsing */ + return true; + } + + /* Check if the device is still connected due to waiting for ACL timeout */ + if (conn_exist_check(addr)) { + /* Device is already connected, stop parsing the adv data */ + return false; + } + + LOG_DBG("Coordinated set device found"); + server_sirk = NULL; + + bt_le_scan_cb_unregister(&scan_callback); + cb_registered = false; + + ret = bt_le_scan_stop(); + if (ret) { + LOG_ERR("Stop scan failed: %d", ret); + } + + bt_addr_le_to_str(addr, addr_string, BT_ADDR_LE_STR_LEN); + + LOG_INF("Creating connection to device in coordinated set: %s", addr_string); + + ret = bt_conn_le_create(addr, BT_CONN_LE_CREATE_CONN, CONNECTION_PARAMETERS, &conn); + if (ret) { + LOG_ERR("Could not init connection: %d", ret); + + ret = bt_mgmt_scan_start(0, 0, BT_MGMT_SCAN_TYPE_CONN, NULL, BRDCAST_ID_NOT_USED); + if (ret) { + LOG_ERR("Failed to restart scanning: %d", ret); + } + } + + /* Set member found and connected, stop parsing */ + return false; +} + +/** + * @brief Callback handler for scan receive when scanning for connections. + * + * @param[in] info Advertiser packet and scan response information. + * @param[in] ad Received advertising data. + */ +static void scan_recv_cb(const struct bt_le_scan_recv_info *info, struct net_buf_simple *ad) +{ + + /* We only care about connectable advertisers */ + if (!(info->adv_props & BT_GAP_ADV_PROP_CONNECTABLE)) { + return; + } + + switch (info->adv_type) { + case BT_GAP_ADV_TYPE_ADV_DIRECT_IND: + /* Direct advertising has no payload, so no need to parse */ + bt_foreach_bond(BT_ID_DEFAULT, bond_connect, (void *)info->addr); + break; + case BT_GAP_ADV_TYPE_ADV_IND: + /* Fall through */ + case BT_GAP_ADV_TYPE_EXT_ADV: + /* Fall through */ + case BT_GAP_ADV_TYPE_SCAN_RSP: + /* Note: May lead to connection creation */ + if (bonded_num < CONFIG_BT_MAX_PAIRED) { + if (server_sirk == NULL) { + bt_data_parse(ad, device_name_check, (void *)info->addr); + } else { + bt_data_parse(ad, csip_found, (void *)info->addr); + } + } else { + /* All bonded slots are taken, so we will only + * accept previously bonded devices + */ + bt_foreach_bond(BT_ID_DEFAULT, bond_connect, (void *)info->addr); + } + break; + default: + break; + } +} + +static void conn_in_coord_set_check(struct bt_conn *conn, void *data) +{ + int ret; + struct bt_conn_info info; + const struct bt_csip_set_coordinator_set_member *member; + + if (data == NULL) { + LOG_ERR("Got NULL pointer"); + return; + } + + uint8_t *num_filled = (uint8_t *)data; + + ret = bt_conn_get_info(conn, &info); + if (ret) { + LOG_ERR("Failed to get conn info for %p: %d", (void *)conn, ret); + return; + } + + /* Don't care about connection not in a connected state */ + if (info.state != BT_CONN_STATE_CONNECTED) { + return; + } + + member = bt_csip_set_coordinator_set_member_by_conn(conn); + + if (memcmp((void *)server_sirk, (void *)member->insts[0].info.sirk, BT_CSIP_SIRK_SIZE) == + 0) { + (*num_filled)++; + } +} + +void bt_mgmt_set_size_filled_get(uint8_t *num_filled) +{ + bt_conn_foreach(BT_CONN_TYPE_LE, conn_in_coord_set_check, (void *)num_filled); +} + +void bt_mgmt_scan_sirk_set(uint8_t const *const sirk) +{ + server_sirk = sirk; +} + +int bt_mgmt_scan_for_conn_start(struct bt_le_scan_param *scan_param, char const *const name) +{ + int ret; + + srch_name = name; + + if (!cb_registered) { + scan_callback.recv = scan_recv_cb; + bt_le_scan_cb_register(&scan_callback); + cb_registered = true; + } else { + /* Already scanning, stop current scan to update param in case it has changed */ + ret = bt_le_scan_stop(); + if (ret && ret != -EALREADY) { + LOG_ERR("Failed to stop scan: %d", ret); + return ret; + } + } + + /* Reset number of bonds found */ + bonded_num = 0; + + bt_foreach_bond(BT_ID_DEFAULT, bond_check, NULL); + + if (bonded_num >= CONFIG_BT_MAX_PAIRED) { + /* NOTE: The string below is used by the Nordic CI system */ + LOG_INF("All bonded slots filled, will not accept new devices"); + } + + ret = bt_le_scan_start(scan_param, NULL); + if (ret && ret != -EALREADY) { + return ret; + } + + return 0; +} diff --git a/src/bluetooth/bt_management/scanning/bt_mgmt_scan_for_conn_internal.h b/src/bluetooth/bt_management/scanning/bt_mgmt_scan_for_conn_internal.h new file mode 100644 index 0000000..f3d03ba --- /dev/null +++ b/src/bluetooth/bt_management/scanning/bt_mgmt_scan_for_conn_internal.h @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _BT_MGMT_SCAN_FOR_CONN_INTERNAL_H_ +#define _BT_MGMT_SCAN_FOR_CONN_INTERNAL_H_ + +#include + +/** + * @brief Scan for a connection with the given device @p name. + * + * @param[in] scan_param Scan parameters to use. + * @param[in] name Device name to search for. + * + * @return 0 if success, error otherwise. + */ +int bt_mgmt_scan_for_conn_start(struct bt_le_scan_param *scan_param, char const *const name); + +#endif /* _BT_MGMT_SCAN_FOR_CONN_INTERNAL_H_ */ diff --git a/src/bluetooth/bt_rendering_and_capture/CMakeLists.txt b/src/bluetooth/bt_rendering_and_capture/CMakeLists.txt new file mode 100644 index 0000000..b7f7ac8 --- /dev/null +++ b/src/bluetooth/bt_rendering_and_capture/CMakeLists.txt @@ -0,0 +1,26 @@ +# +# Copyright (c) 2023 Nordic Semiconductor +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +zephyr_library_include_directories( + volume +) + +target_sources(app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/bt_rendering_and_capture.c) + +if (CONFIG_BT_VCP_VOL_CTLR AND CONFIG_BT_VCP_VOL_REND) + message(FATAL_ERROR "No support for vol controller and renderer on same device") +endif() + +if (CONFIG_BT_VCP_VOL_REND) +target_sources(app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/volume/bt_vol_rend.c) +endif() + +if (CONFIG_BT_VCP_VOL_CTLR) + target_sources(app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/volume/bt_vol_ctlr.c) +endif() diff --git a/src/bluetooth/bt_rendering_and_capture/Kconfig b/src/bluetooth/bt_rendering_and_capture/Kconfig new file mode 100644 index 0000000..e4d526c --- /dev/null +++ b/src/bluetooth/bt_rendering_and_capture/Kconfig @@ -0,0 +1,19 @@ +# +# Copyright (c) 2023 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +menu "BT renderer" + +rsource "volume/Kconfig" + +#----------------------------------------------------------------------------# +menu "Log level" + +module = BT_RENDERING_AND_CAPTURE +module-str = bt-rendering-and-capture +source "subsys/logging/Kconfig.template.log_config" + +endmenu # Log level +endmenu # BT renderer diff --git a/src/bluetooth/bt_rendering_and_capture/bt_rendering_and_capture.c b/src/bluetooth/bt_rendering_and_capture/bt_rendering_and_capture.c new file mode 100644 index 0000000..9f38d35 --- /dev/null +++ b/src/bluetooth/bt_rendering_and_capture/bt_rendering_and_capture.c @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "bt_rendering_and_capture.h" + +#include +#include +#include + +#include "bt_vol_rend_internal.h" +#include "bt_vol_ctlr_internal.h" +#include "zbus_common.h" + +#include +LOG_MODULE_REGISTER(bt_r_c, CONFIG_BT_RENDERING_AND_CAPTURE_LOG_LEVEL); + +ZBUS_CHAN_DEFINE(volume_chan, struct volume_msg, NULL, NULL, ZBUS_OBSERVERS_EMPTY, + ZBUS_MSG_INIT(0)); + +int bt_r_and_c_volume_up(void) +{ + int ret; + struct volume_msg msg; + + if (IS_ENABLED(CONFIG_BT_VCP_VOL_REND)) { + ret = bt_vol_rend_up(); + return ret; + } + + if (IS_ENABLED(CONFIG_BT_VCP_VOL_CTLR)) { + ret = bt_vol_ctlr_up(); + return ret; + } + + msg.event = VOLUME_UP; + + ret = zbus_chan_pub(&volume_chan, &msg, K_NO_WAIT); + return ret; +} + +int bt_r_and_c_volume_down(void) +{ + int ret; + struct volume_msg msg; + + if (IS_ENABLED(CONFIG_BT_VCP_VOL_REND)) { + ret = bt_vol_rend_down(); + return ret; + } + + if (IS_ENABLED(CONFIG_BT_VCP_VOL_CTLR)) { + ret = bt_vol_ctlr_down(); + return ret; + } + + msg.event = VOLUME_DOWN; + + ret = zbus_chan_pub(&volume_chan, &msg, K_NO_WAIT); + return ret; +} + +int bt_r_and_c_volume_set(uint8_t volume, bool from_vcp) +{ + int ret; + struct volume_msg msg; + + if ((IS_ENABLED(CONFIG_BT_VCP_VOL_REND)) && !from_vcp) { + ret = bt_vol_rend_set(volume); + return ret; + } + + if ((IS_ENABLED(CONFIG_BT_VCP_VOL_CTLR)) && !from_vcp) { + ret = bt_vol_ctlr_set(volume); + return ret; + } + + msg.event = VOLUME_SET; + msg.volume = volume; + + ret = zbus_chan_pub(&volume_chan, &msg, K_NO_WAIT); + return ret; +} + +int bt_r_and_c_volume_mute(bool from_vcp) +{ + int ret; + struct volume_msg msg; + + if ((IS_ENABLED(CONFIG_BT_VCP_VOL_REND)) && !from_vcp) { + ret = bt_vol_rend_mute(); + return ret; + } + + if ((IS_ENABLED(CONFIG_BT_VCP_VOL_CTLR)) && !from_vcp) { + ret = bt_vol_ctlr_mute(); + return ret; + } + + msg.event = VOLUME_MUTE; + + ret = zbus_chan_pub(&volume_chan, &msg, K_NO_WAIT); + return ret; +} + +int bt_r_and_c_volume_unmute(void) +{ + int ret; + struct volume_msg msg; + + if (IS_ENABLED(CONFIG_BT_VCP_VOL_REND)) { + ret = bt_vol_rend_unmute(); + return ret; + } + + if (IS_ENABLED(CONFIG_BT_VCP_VOL_CTLR)) { + ret = bt_vol_ctlr_unmute(); + return ret; + } + + msg.event = VOLUME_UNMUTE; + + ret = zbus_chan_pub(&volume_chan, &msg, K_NO_WAIT); + return ret; +} + +int bt_r_and_c_discover(struct bt_conn *conn) +{ + int ret; + + if (IS_ENABLED(CONFIG_BT_VCP_VOL_CTLR)) { + ret = bt_vol_ctlr_discover(conn); + if (ret) { + LOG_WRN("Failed to discover VCS: %d", ret); + return ret; + } + } else { + return -ENOTSUP; + } + + return 0; +} + +int bt_r_and_c_uuid_populate(struct net_buf_simple *uuid_buf) +{ + if (IS_ENABLED(CONFIG_BT_VCP_VOL_REND)) { + if (net_buf_simple_tailroom(uuid_buf) >= BT_UUID_SIZE_16) { + net_buf_simple_add_le16(uuid_buf, BT_UUID_VCS_VAL); + } else { + return -ENOMEM; + } + } else { + return -ENOTSUP; + } + + return 0; +} + +int bt_r_and_c_init(void) +{ + int ret; + + if (IS_ENABLED(CONFIG_BT_VCP_VOL_REND)) { + ret = bt_vol_rend_init(); + + if (ret) { + LOG_WRN("Failed to initialize VCS renderer: %d", ret); + return ret; + } + } + + if (IS_ENABLED(CONFIG_BT_VCP_VOL_CTLR)) { + ret = bt_vol_ctlr_init(); + + if (ret) { + LOG_WRN("Failed to initialize VCS controller: %d", ret); + return ret; + } + } + + return 0; +} diff --git a/src/bluetooth/bt_rendering_and_capture/bt_rendering_and_capture.h b/src/bluetooth/bt_rendering_and_capture/bt_rendering_and_capture.h new file mode 100644 index 0000000..cfefb49 --- /dev/null +++ b/src/bluetooth/bt_rendering_and_capture/bt_rendering_and_capture.h @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _BT_REND_H_ +#define _BT_REND_H_ + +#include + +/** + * @brief Adjust volume up by one step. + * + * @return 0 if success, error otherwise. + */ +int bt_r_and_c_volume_up(void); + +/** + * @brief Adjust volume down by one step. + * + * @return 0 if success, error otherwise. + */ +int bt_r_and_c_volume_down(void); + +/** + * @brief Set the volume to the given @p volume value. + * + * @param[in] volume Value to set the volume to (0-255). + * @param[in] from_vcp Describe if the function was called from a service + * or from somewhere else (buttons, shell, etc). + * + * @return 0 if success, error otherwise. + */ +int bt_r_and_c_volume_set(uint8_t volume, bool from_vcp); + +/** + * @brief Mute the volume. + * + * @param[in] from_vcp Describe if the function was called from a service + * or from somewhere else (buttons, shell, etc). + * + * @return 0 if success, error otherwise. + */ +int bt_r_and_c_volume_mute(bool from_vcp); + +/** + * @brief Unmute the volume. + * + * @return 0 if success, error otherwise. + */ +int bt_r_and_c_volume_unmute(void); + +/** + * @brief Discover the rendering services. + * + * @param[in] conn Pointer to the connection on which to do the discovery. + * + * @return 0 if success, error otherwise. + */ +int bt_r_and_c_discover(struct bt_conn *conn); + +/** + * @brief Put the UUIDs from this module into the buffer. + * + * @note This partial data is used to build a complete extended advertising packet. + * + * @param[out] uuid_buf Buffer being populated with UUIDs. + * + * @return 0 for success, error otherwise. + */ +int bt_r_and_c_uuid_populate(struct net_buf_simple *uuid_buf); + +/** + * @brief Initialize the rendering services or profiles, or both. + * + * @return 0 if success, error otherwise. + */ +int bt_r_and_c_init(void); + +#endif /* _BT_REND_H_ */ diff --git a/src/bluetooth/bt_rendering_and_capture/volume/Kconfig b/src/bluetooth/bt_rendering_and_capture/volume/Kconfig new file mode 100644 index 0000000..35306eb --- /dev/null +++ b/src/bluetooth/bt_rendering_and_capture/volume/Kconfig @@ -0,0 +1,29 @@ +# +# Copyright (c) 2023 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +menu "Volume" + +config BT_AUDIO_VOL_DEFAULT + int "Default volume" + range 0 255 + default 195 + help + The default volume when starting a volume control renderer. + +config BT_AUDIO_VOL_STEP_SIZE + int "Volume adjust step size" + range 6 32 + default 16 + +#----------------------------------------------------------------------------# +menu "Log level" + +module = BT_VOL +module-str = bt-vol +source "subsys/logging/Kconfig.template.log_config" + +endmenu # Log level +endmenu # Volume diff --git a/src/bluetooth/bt_rendering_and_capture/volume/bt_vol_ctlr.c b/src/bluetooth/bt_rendering_and_capture/volume/bt_vol_ctlr.c new file mode 100644 index 0000000..a04db8c --- /dev/null +++ b/src/bluetooth/bt_rendering_and_capture/volume/bt_vol_ctlr.c @@ -0,0 +1,254 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "bt_vol_ctlr_internal.h" + +#include +#include +#include +#include + +#include +LOG_MODULE_REGISTER(bt_vol_ctlr, CONFIG_BT_VOL_LOG_LEVEL); + +static struct bt_vcp_vol_ctlr *vcs_client_peer[CONFIG_BT_MAX_CONN]; + +/** + * @brief Get the index of the first available vcs_client_peer. + * + * @retval Index if success. + * @retval -ENOMEM if no available indexes. + */ +static int vcs_client_peer_index_free_get(void) +{ + for (int i = 0; i < ARRAY_SIZE(vcs_client_peer); i++) { + if (vcs_client_peer[i] == NULL) { + return i; + } + } + + LOG_WRN("No more indexes for VCS peer clients"); + + return -ENOMEM; +} + +/** + * @brief Check if the given @p conn has a vcs_client_peer pointer. + * + * @param[in] conn The connection pointer to be checked. + * + * @retval True if vcs_client_peer exists. + * @retval False otherwise. + */ +static bool vcs_client_peer_exists(struct bt_conn *conn) +{ + int ret; + + struct bt_conn *result_conn = NULL; + + for (int i = 0; i < ARRAY_SIZE(vcs_client_peer); i++) { + ret = bt_vcp_vol_ctlr_conn_get(vcs_client_peer[i], &result_conn); + + if (!ret && conn == result_conn) { + return true; + } + + if (ret == -ENOTCONN) { + /* VCS client no longer connected, free the index */ + vcs_client_peer[i] = NULL; + return false; + } + } + + return false; +} + +/** + * @brief Callback handler for the volume state. + * + * @note This callback handler will be triggered if volume state has changed, + * or the playback was muted or unmuted. + */ +static void vcs_state_ctlr_cb_handler(struct bt_vcp_vol_ctlr *vcs, int err, uint8_t volume, + uint8_t mute) +{ + int ret; + + if (err) { + LOG_ERR("VCS state callback error: %d", err); + return; + } + + for (int i = 0; i < ARRAY_SIZE(vcs_client_peer); i++) { + if (vcs == vcs_client_peer[i]) { + LOG_DBG("VCS state from remote device %d:", i); + continue; + } + + LOG_DBG("Sync with other devices %d", i); + + if (vcs_client_peer[i] == NULL) { + /* Skip */ + continue; + } + + ret = bt_vcp_vol_ctlr_set_vol(vcs_client_peer[i], volume); + if (ret) { + LOG_DBG("Failed to sync volume to remote device %d, err = " + "%d", + i, ret); + } + } +} + +/** + * @brief Callback handler for the VCS controller flags. + * + * @note This callback handler will be triggered if VCS flags changed. + */ +static void vcs_flags_ctlr_cb_handler(struct bt_vcp_vol_ctlr *vcs, int err, uint8_t flags) +{ + if (err) { + LOG_ERR("VCS flag callback error: %d", err); + } else { + LOG_DBG("Volume flags = 0x%01X", flags); + } +} + +/** + * @brief Callback handler for the finished VCS discovery. + * + * @note This callback handler will be triggered when the VCS discovery has finished. + */ +static void vcs_discover_cb_handler(struct bt_vcp_vol_ctlr *vcs, int err, uint8_t vocs_count, + uint8_t aics_count) +{ + if (err) { + LOG_WRN("VCS discover finished callback error: %d", err); + } else { + LOG_INF("VCS discover finished"); + } +} + +int bt_vol_ctlr_set(uint8_t volume) +{ + int ret; + + for (int i = 0; i < ARRAY_SIZE(vcs_client_peer); i++) { + if (vcs_client_peer[i] != NULL) { + ret = bt_vcp_vol_ctlr_set_vol(vcs_client_peer[i], volume); + if (ret) { + LOG_WRN("Failed to set volume for remote channel %d, ret = " + "%d", + i, ret); + } + } + } + + return 0; +} + +int bt_vol_ctlr_up(void) +{ + int ret; + + for (int i = 0; i < ARRAY_SIZE(vcs_client_peer); i++) { + if (vcs_client_peer[i] != NULL) { + ret = bt_vcp_vol_ctlr_unmute_vol_up(vcs_client_peer[i]); + if (ret) { + LOG_WRN("Failed to volume up for remote channel %d, ret = " + "%d", + i, ret); + } + } + } + + return 0; +} + +int bt_vol_ctlr_down(void) +{ + int ret; + + for (int i = 0; i < ARRAY_SIZE(vcs_client_peer); i++) { + if (vcs_client_peer[i] != NULL) { + ret = bt_vcp_vol_ctlr_unmute_vol_down(vcs_client_peer[i]); + if (ret) { + LOG_WRN("Failed to volume down for remote channel %d, ret " + "= %d", + i, ret); + } + } + } + + return 0; +} + +int bt_vol_ctlr_mute(void) +{ + int ret; + + for (int i = 0; i < ARRAY_SIZE(vcs_client_peer); i++) { + if (vcs_client_peer[i] != NULL) { + ret = bt_vcp_vol_ctlr_mute(vcs_client_peer[i]); + if (ret) { + LOG_WRN("Failed to mute for remote channel %d, ret " + "= %d", + i, ret); + } + } + } + + return 0; +} + +int bt_vol_ctlr_unmute(void) +{ + + int ret; + + for (int i = 0; i < ARRAY_SIZE(vcs_client_peer); i++) { + if (vcs_client_peer[i] != NULL) { + ret = bt_vcp_vol_ctlr_unmute(vcs_client_peer[i]); + if (ret) { + LOG_WRN("Failed to unmute for remote channel %d, " + "ret = %d", + i, ret); + } + } + } + + return 0; +} + +int bt_vol_ctlr_discover(struct bt_conn *conn) +{ + + int ret, index; + + if (vcs_client_peer_exists(conn)) { + return -EAGAIN; + } + + index = vcs_client_peer_index_free_get(); + if (index < 0) { + return index; + } + + ret = bt_vcp_vol_ctlr_discover(conn, &vcs_client_peer[index]); + return ret; +} + +int bt_vol_ctlr_init(void) +{ + static struct bt_vcp_vol_ctlr_cb vcs_client_callback; + + vcs_client_callback.discover = vcs_discover_cb_handler; + vcs_client_callback.state = vcs_state_ctlr_cb_handler; + vcs_client_callback.flags = vcs_flags_ctlr_cb_handler; + + return bt_vcp_vol_ctlr_cb_register(&vcs_client_callback); +} diff --git a/src/bluetooth/bt_rendering_and_capture/volume/bt_vol_ctlr_internal.h b/src/bluetooth/bt_rendering_and_capture/volume/bt_vol_ctlr_internal.h new file mode 100644 index 0000000..cab3478 --- /dev/null +++ b/src/bluetooth/bt_rendering_and_capture/volume/bt_vol_ctlr_internal.h @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _BT_VOL_CTLR_INTERNAL_H_ +#define _BT_VOL_CTLR_INTERNAL_H_ + +#include + +/** + * @brief Set volume to a specific value. + * + * @param[in] volume The absolute volume to be set. + * + * @retval 0 Volume change success. + * @retval -ENXIO The feature is disabled. + * @retval other Errors from underlying drivers. + */ +int bt_vol_ctlr_set(uint8_t volume); + +/** + * @brief Turn the volume up by one step. + * + * @retval 0 Volume change success. + * @retval -ENXIO The feature is disabled. + * @retval other Errors from underlying drivers. + */ +int bt_vol_ctlr_up(void); + +/** + * @brief Turn the volume down by one step. + * + * @retval 0 Volume change success. + * @retval -ENXIO The feature is disabled. + * @retval other Errors from underlying drivers. + */ +int bt_vol_ctlr_down(void); + +/** + * @brief Mute the output volume of the device. + * + * @retval 0 Volume change success. + * @retval -ENXIO The feature is disabled. + * @retval other Errors from underlying drivers. + */ +int bt_vol_ctlr_mute(void); + +/** + * @brief Unmute the output volume of the device. + * + * @retval 0 Volume change success. + * @retval -ENXIO The feature is disabled. + * @retval other Errors from underlying drivers. + */ +int bt_vol_ctlr_unmute(void); + +/** + * @brief Discover Volume Control Service and included services. + * + * @param[in] conn Pointer to the connection on which to discover the services. + * + * @note This function starts a GATT discovery and sets up handles and + * subscriptions for the VCS and included services. + * Call it once before any other actions related to the VCS. + * + * @return 0 for success, error otherwise. + */ +int bt_vol_ctlr_discover(struct bt_conn *conn); + +/** + * @brief Initialize the Volume Control Service client. + * + * @return 0 for success, error otherwise. + */ +int bt_vol_ctlr_init(void); + +#endif /* _BT_VOL_CTLR_INTERNAL_H_ */ diff --git a/src/bluetooth/bt_rendering_and_capture/volume/bt_vol_rend.c b/src/bluetooth/bt_rendering_and_capture/volume/bt_vol_rend.c new file mode 100644 index 0000000..32f1f9e --- /dev/null +++ b/src/bluetooth/bt_rendering_and_capture/volume/bt_vol_rend.c @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "bt_vol_rend_internal.h" + +#include +#include +#include +#include + +#include "bt_rendering_and_capture.h" + +#include +LOG_MODULE_REGISTER(bt_vol_rend, CONFIG_BT_VOL_LOG_LEVEL); + +/** + * @brief Callback handler for the volume state. + * + * @note This callback handler will be triggered if volume state has changed, + * or the playback was muted or unmuted from the volume_controller. + */ +static void vcs_state_rend_cb_handler(struct bt_conn *conn, int err, uint8_t volume, uint8_t mute) +{ + int ret; + + if (err) { + LOG_ERR("VCS state callback error: %d", err); + return; + } + LOG_INF("Volume = %d, mute state = %d", volume, mute); + + /* Send to bt_rend */ + ret = bt_r_and_c_volume_set(volume, true); + if (ret) { + LOG_WRN("Failed to set volume"); + } + + if (mute) { + ret = bt_r_and_c_volume_mute(true); + if (ret) { + LOG_WRN("Error muting volume"); + } + } +} + +/** + * @brief Callback handler for the changed VCS renderer flags. + * + * @note This callback handler will be triggered if the VCS flags has changed. + */ +static void vcs_flags_rend_cb_handler(struct bt_conn *conn, int err, uint8_t flags) +{ + if (err) { + LOG_ERR("VCS flag callback error: %d", err); + } else { + LOG_DBG("Volume flags = 0x%01X", flags); + } +} + +int bt_vol_rend_set(uint8_t volume) +{ + + return bt_vcp_vol_rend_set_vol(volume); +} + +int bt_vol_rend_up(void) +{ + return bt_vcp_vol_rend_unmute_vol_up(); +} + +int bt_vol_rend_down(void) +{ + + return bt_vcp_vol_rend_unmute_vol_down(); +} + +int bt_vol_rend_mute(void) +{ + return bt_vcp_vol_rend_mute(); +} + +int bt_vol_rend_unmute(void) +{ + return bt_vcp_vol_rend_unmute(); +} + +int bt_vol_rend_init(void) +{ + int ret; + struct bt_vcp_vol_rend_register_param vcs_param; + static struct bt_vcp_vol_rend_cb vcs_server_callback; + + vcs_server_callback.state = vcs_state_rend_cb_handler; + vcs_server_callback.flags = vcs_flags_rend_cb_handler; + vcs_param.cb = &vcs_server_callback; + vcs_param.mute = BT_VCP_STATE_UNMUTED; + vcs_param.step = CONFIG_BT_AUDIO_VOL_STEP_SIZE; + vcs_param.volume = CONFIG_BT_AUDIO_VOL_DEFAULT; + + ret = bt_vcp_vol_rend_register(&vcs_param); + if (ret) { + return ret; + } + + return 0; +} diff --git a/src/bluetooth/bt_rendering_and_capture/volume/bt_vol_rend_internal.h b/src/bluetooth/bt_rendering_and_capture/volume/bt_vol_rend_internal.h new file mode 100644 index 0000000..cdbd1f8 --- /dev/null +++ b/src/bluetooth/bt_rendering_and_capture/volume/bt_vol_rend_internal.h @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _BT_VOL_REND_INTERNAL_H_ +#define _BT_VOL_REND_INTERNAL_H_ + +#include +/** + * @brief Set volume to a specific value. + * + * @param[in] volume The absolute volume to be set. + * + * @retval 0 Volume change success. + * @retval -ENXIO The feature is disabled. + * @retval other Errors from underlying drivers. + */ +int bt_vol_rend_set(uint8_t volume); + +/** + * @brief Turn the volume up by one step. + * + * @retval 0 Volume change success. + * @retval -ENXIO The feature is disabled. + * @retval other Errors from underlying drivers. + */ +int bt_vol_rend_up(void); + +/** + * @brief Turn the volume down by one step. + * + * @retval 0 Volume change success. + * @retval -ENXIO The feature is disabled. + * @retval other Errors from underlying drivers. + */ +int bt_vol_rend_down(void); + +/** + * @brief Mute the output volume of the device. + * + * @retval 0 Volume change success. + * @retval -ENXIO The feature is disabled. + * @retval other Errors from underlying drivers. + */ +int bt_vol_rend_mute(void); + +/** + * @brief Unmute the output volume of the device. + * + * @retval 0 Volume change success. + * @retval -ENXIO The feature is disabled. + * @retval other Errors from underlying drivers. + */ +int bt_vol_rend_unmute(void); + +/** + * @brief Initialize the Volume renderer. + * + * @return 0 for success, error otherwise. + */ +int bt_vol_rend_init(void); + +#endif /* _BT_VOL_REND_INTERNAL_H_ */ diff --git a/src/bluetooth/bt_stream/CMakeLists.txt b/src/bluetooth/bt_stream/CMakeLists.txt new file mode 100644 index 0000000..389ec2d --- /dev/null +++ b/src/bluetooth/bt_stream/CMakeLists.txt @@ -0,0 +1,36 @@ +# +# Copyright (c) 2023 Nordic Semiconductor +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +zephyr_library_include_directories( + broadcast + unicast + bt_le_audio_tx +) + +add_subdirectory(bt_le_audio_tx) + +target_sources(app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/le_audio.c) + +if (CONFIG_BT_BAP_BROADCAST_SINK) + target_sources(app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/broadcast/broadcast_sink.c) +endif() + +if (CONFIG_BT_BAP_BROADCAST_SOURCE) + target_sources(app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/broadcast/broadcast_source.c) +endif() + +if (CONFIG_BT_BAP_UNICAST_CLIENT) + target_sources(app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/unicast/unicast_client.c) +endif() + +if (CONFIG_BT_BAP_UNICAST_SERVER) + target_sources(app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/unicast/unicast_server.c) +endif() diff --git a/src/bluetooth/bt_stream/broadcast/Kconfig b/src/bluetooth/bt_stream/broadcast/Kconfig new file mode 100644 index 0000000..1fda6b8 --- /dev/null +++ b/src/bluetooth/bt_stream/broadcast/Kconfig @@ -0,0 +1,259 @@ +# +# Copyright (c) 2023 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +rsource "Kconfig.defaults" + +menu "Broadcast" + +choice BT_AUDIO_BROADCAST_BAP_CONFIGURATION + prompt "Broadcast codec configuration" + depends on TRANSPORT_BIS + default BT_AUDIO_BROADCAST_CONFIGURABLE + help + Select the broadcast codec configuration as given + in Table 6.4 of the Bluetooth Audio Profile specification. + USB only supports 48-kHz sampling rate. + +config BT_AUDIO_BROADCAST_CONFIGURABLE + bool "Configurable broadcast settings" + depends on TRANSPORT_BIS + help + Configurable option that doesn't follow any preset. Allows for more flexibility. + +config BT_BAP_BROADCAST_16_2_1 + bool "16_2_1" + depends on TRANSPORT_BIS + help + Broadcast mandatory codec capability 16_2_1. + 16kHz, 32kbps, 2 retransmits, 10ms transport latency, and 40ms presentation delay. + +config BT_BAP_BROADCAST_16_2_2 + bool "16_2_2" + depends on TRANSPORT_BIS + help + Broadcast mandatory codec capability 16_2_2. + 16kHz, 32kbps, 4 retransmits, 60ms transport latency, and 40ms presentation delay. + +config BT_BAP_BROADCAST_24_2_1 + bool "24_2_1" + depends on TRANSPORT_BIS + help + Broadcast codec capability 24_2_1. + 24kHz, 48kbps, 2 retransmits, 10ms transport latency, and 40ms presentation delay. + +config BT_BAP_BROADCAST_24_2_2 + bool "24_2_2" + depends on TRANSPORT_BIS + help + Broadcast codec capability 24_2_2. + 24kHz, 48kbps, 4 retransmits, 60ms transport latency, and 40ms presentation delay. + +config BT_BAP_BROADCAST_48_2_1 + bool "48_2_1" + depends on TRANSPORT_BIS + help + Broadcast codec capability 48_2_1. + 48kHz, 80kbps, 4 retransmits, 20ms transport latency, and 40ms presentation delay. + +config BT_BAP_BROADCAST_48_2_2 + bool "48_2_2" + depends on TRANSPORT_BIS + help + Broadcast codec capability 48_2_2. + 48kHz, 80kbps, 4 retransmits, 65ms transport latency, and 40ms presentation delay. + +config BT_BAP_BROADCAST_48_4_1 + bool "48_4_1" + depends on TRANSPORT_BIS + help + Broadcast codec capability 48_4_1. + 48kHz, 96kbps, 4 retransmits, 20ms transport latency, and 40ms presentation delay. + +config BT_BAP_BROADCAST_48_4_2 + bool "48_4_2" + depends on TRANSPORT_BIS + help + Broadcast codec capability 48_4_2. + 48kHz, 96kbps, 4 retransmits, 65ms transport latency, and 40ms presentation delay. + + +config BT_BAP_BROADCAST_48_6_1 + bool "48_6_1" + depends on TRANSPORT_BIS + help + Broadcast codec capability 48_6_1. + 48kHz, 124kbps, 4 retransmits, 20ms transport latency, and 40ms presentation delay. + +config BT_BAP_BROADCAST_48_6_2 + bool "48_6_2" + depends on TRANSPORT_BIS + help + Broadcast codec capability 48_6_2. + 48kHz, 124kbps, 4 retransmits, 65ms transport latency, and 40ms presentation delay. +endchoice + +config BT_AUDIO_BROADCAST_NAME + string "Broadcast name" + default "NRF5340_BROADCASTER" + # TODO: Add back 'depends on TRANSPORT_BIS' once applications are ready + help + Name of the broadcast; not the same as BT_DEVICE_NAME. + +config BT_AUDIO_BROADCAST_NAME_ALT + string "Alternative broadcast name" + default "NRF5340_BROADCASTER_ALT" + # TODO: Add back 'depends on TRANSPORT_BIS' once applications are ready + help + Alternative name of the broadcast. + +config BT_AUDIO_USE_BROADCAST_NAME_ALT + bool "Use the alternative broadcast name" + default n + # TODO: Add back 'depends on TRANSPORT_BIS' once applications are ready + help + Use the alternative broadcast name. + +config BT_AUDIO_BROADCAST_ENCRYPTED + bool "Encrypted broadcast" + depends on TRANSPORT_BIS + default n + help + Encrypt the broadcast to limit the connection possibilities. + +config BT_AUDIO_BROADCAST_ENCRYPTION_KEY + string "Broadcast encryption key" + depends on TRANSPORT_BIS + default "NRF5340_BIS_DEMO" + help + Key to use for encryption and decryption, with maximum BT_ISO_BROADCAST_CODE_SIZE + characters. Encryption keys larger than BT_ISO_BROADCAST_CODE_SIZE will be truncated to + BT_ISO_BROADCAST_CODE_SIZE. + +config BT_AUDIO_USE_BROADCAST_ID_RANDOM + bool "Use a random broadcast ID" + depends on TRANSPORT_BIS + default y + help + Use a randomly generated broadcast ID. + +config BT_AUDIO_BROADCAST_ID_FIXED + hex "Fixed broadcast ID" + depends on TRANSPORT_BIS + default 0x123456 + help + Fixed broadcast ID; 3 octets. Will only be used if BT_AUDIO_USE_BROADCAST_ID_RANDOM=n. + Only use for debugging. + +config BT_AUDIO_BROADCAST_PBA_METADATA_SIZE + int "Configure PBA meta data buffer size" + depends on TRANSPORT_BIS && AURACAST + default 16 + help + Configure the maximum size of the Public Broadcast Announcement meata data buffer in octets. + This is the number of meta data LVT records, or the number of meta data items multiplied by + the size of the LTV (sizeof(bt_data)). Configurable option that doesn't follow any preset. + Allows for more flexibility. + +config BT_AUDIO_BROADCAST_PARENTAL_RATING + hex "Parental rating" + depends on TRANSPORT_BIS + default 0x00 + range 0x00 0x0F + help + Set the parental rating for the broadcast. + BT_AUDIO_PARENTAL_RATING_NO_RATING = 0x00, + BT_AUDIO_PARENTAL_RATING_AGE_ANY = 0x01, + BT_AUDIO_PARENTAL_RATING_AGE_5_OR_ABOVE = 0x02, + BT_AUDIO_PARENTAL_RATING_AGE_6_OR_ABOVE = 0x03, + BT_AUDIO_PARENTAL_RATING_AGE_7_OR_ABOVE = 0x04, + BT_AUDIO_PARENTAL_RATING_AGE_8_OR_ABOVE = 0x05, + BT_AUDIO_PARENTAL_RATING_AGE_9_OR_ABOVE = 0x06, + BT_AUDIO_PARENTAL_RATING_AGE_10_OR_ABOVE = 0x07, + BT_AUDIO_PARENTAL_RATING_AGE_11_OR_ABOVE = 0x08, + BT_AUDIO_PARENTAL_RATING_AGE_12_OR_ABOVE = 0x09, + BT_AUDIO_PARENTAL_RATING_AGE_13_OR_ABOVE = 0x0A, + BT_AUDIO_PARENTAL_RATING_AGE_14_OR_ABOVE = 0x0B, + BT_AUDIO_PARENTAL_RATING_AGE_15_OR_ABOVE = 0x0C, + BT_AUDIO_PARENTAL_RATING_AGE_16_OR_ABOVE = 0x0D, + BT_AUDIO_PARENTAL_RATING_AGE_17_OR_ABOVE = 0x0E, + BT_AUDIO_PARENTAL_RATING_AGE_18_OR_ABOVE = 0x0F + +config BT_AUDIO_BROADCAST_IMMEDIATE_FLAG + bool "Immediate rendering flag" + depends on TRANSPORT_BIS + default n + help + Set the immediate rendering flag. + +config AURACAST + bool "Enable Auracast" + depends on TRANSPORT_BIS + default y + help + When Auracast is enabled, a Public Broadcast Announcement will be included + when advertising. + +config BT_AUDIO_BITRATE_BROADCAST_SRC + int "ISO stream bitrate" + depends on TRANSPORT_BIS + default 96000 if BT_AUDIO_BROADCAST_CONFIGURABLE + default 32000 if BT_BAP_BROADCAST_16_2_1 || BT_BAP_BROADCAST_16_2_2 + default 48000 if BT_BAP_BROADCAST_24_2_1 || BT_BAP_BROADCAST_24_2_2 + default 80000 if BT_BAP_BROADCAST_48_2_1 || BT_BAP_BROADCAST_48_2_2 + default 96000 if BT_BAP_BROADCAST_48_4_1 || BT_BAP_BROADCAST_48_4_2 + default 124000 if BT_BAP_BROADCAST_48_6_1 || BT_BAP_BROADCAST_48_6_2 + help + Bitrate for the broadcast source ISO stream. + +config BT_AUDIO_SCAN_DELEGATOR + bool "Enable scan delegator" + depends on TRANSPORT_BIS + select BT_CAP_ACCEPTOR + select BT_CSIP_SET_MEMBER + select BT_CAP_ACCEPTOR_SET_MEMBER + select BT_GAP_PERIPHERAL_PREF_PARAMS + select BT_VCP_VOL_REND + select BT_PER_ADV_SYNC_TRANSFER_RECEIVER + help + When scan delegator feature is enabled, the broadcast sink will not + search for a predefined broadcast source. Instead, it will wait for a + broadcast assistant to connect and control. + +config BT_SET_IDENTITY_RESOLVING_KEY_DEFAULT + string + default "NRF5340_BIS_DEMO" + help + Default string to configure the Set Identify Resolving Key (SIRK), must + be changed before production uniquely for each coordinated set. + +config BT_SET_IDENTITY_RESOLVING_KEY + string "String used to configure the SIRK" + default BT_SET_IDENTITY_RESOLVING_KEY_DEFAULT + help + Defines a string to configure the Set Identify Resolving Key (SIRK), must + be changed before production uniquely for each coordinated set. The SIRK + must be 16 characters (16 bytes). + +config BT_AUDIO_BROADCAST_ZBUS_EVT_STREAM_SENT + bool "Enable Zephyr bus event for stream sent" + help + Enable ZBUS event signalling that a stream has been sent, and that the next frame can + be prepared. As this event will trigger once for each frame it will cause significant + overhead, even if the event is not used. + +#----------------------------------------------------------------------------# +menu "Log levels" + +module = BROADCAST_SOURCE +module-str = broadcast_source +source "subsys/logging/Kconfig.template.log_config" + +module = BROADCAST_SINK +module-str = broadcast_sink +source "subsys/logging/Kconfig.template.log_config" + +endmenu # Log levels +endmenu # Broadcast diff --git a/src/bluetooth/bt_stream/broadcast/Kconfig.defaults b/src/bluetooth/bt_stream/broadcast/Kconfig.defaults new file mode 100644 index 0000000..63eccfa --- /dev/null +++ b/src/bluetooth/bt_stream/broadcast/Kconfig.defaults @@ -0,0 +1,11 @@ +# +# Copyright (c) 2023 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +config BT_BUF_ACL_RX_SIZE + default 502 if (AUDIO_DFU > 0) + +config BT_AUDIO_CODEC_CFG_MAX_METADATA_SIZE + default 80 diff --git a/src/bluetooth/bt_stream/broadcast/broadcast_sink.c b/src/bluetooth/bt_stream/broadcast/broadcast_sink.c new file mode 100644 index 0000000..ff1a05f --- /dev/null +++ b/src/bluetooth/bt_stream/broadcast/broadcast_sink.c @@ -0,0 +1,849 @@ +/* + * Copyright (c) 2022 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "broadcast_sink.h" + +#include +#include +#include +#include +#include +#include +#include + +/* TODO: Remove when a get_info function is implemented in host */ +#include <../subsys/bluetooth/audio/bap_endpoint.h> + +#include "bt_mgmt.h" +#include "macros_common.h" +#include "zbus_common.h" +#include "channel_assignment.h" + +#include +LOG_MODULE_REGISTER(broadcast_sink, CONFIG_BROADCAST_SINK_LOG_LEVEL); + +BUILD_ASSERT(CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT <= 2, + "A maximum of two broadcast streams are currently supported"); + +ZBUS_CHAN_DEFINE(le_audio_chan, struct le_audio_msg, NULL, NULL, ZBUS_OBSERVERS_EMPTY, + ZBUS_MSG_INIT(0)); + +static uint8_t bis_encryption_key[BT_ISO_BROADCAST_CODE_SIZE] = {0}; +static bool broadcast_code_received; +struct audio_codec_info { + uint8_t id; + uint16_t cid; + uint16_t vid; + int frequency; + int frame_duration_us; + enum bt_audio_location chan_allocation; + int octets_per_sdu; + int bitrate; + int blocks_per_sdu; +}; +struct active_audio_stream { + struct bt_bap_stream *stream; + struct audio_codec_info *codec; + uint32_t pd; +}; + +static struct bt_bap_broadcast_sink *broadcast_sink; +static struct bt_bap_stream audio_streams[CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT]; +static struct audio_codec_info audio_codec_info[CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT]; +static uint32_t bis_index_bitfields[CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT]; +static struct bt_le_per_adv_sync *pa_sync_stored; +static struct active_audio_stream active_stream; + +/* The values of sync_stream_cnt and active_stream_index must never become larger + * than the sizes of the arrays above (audio_streams etc.) + */ +static uint8_t sync_stream_cnt; +static uint8_t active_stream_index; + +static struct bt_audio_codec_cap codec_cap = BT_AUDIO_CODEC_CAP_LC3( + BT_AUDIO_CODEC_CAPABILIY_FREQ, + (BT_AUDIO_CODEC_CAP_DURATION_10 | BT_AUDIO_CODEC_CAP_DURATION_PREFER_10), + BT_AUDIO_CODEC_CAP_CHAN_COUNT_SUPPORT(1), LE_AUDIO_SDU_SIZE_OCTETS(CONFIG_LC3_BITRATE_MIN), + LE_AUDIO_SDU_SIZE_OCTETS(CONFIG_LC3_BITRATE_MAX), 1u, BT_AUDIO_CONTEXT_TYPE_ANY); + +static struct bt_pacs_cap capabilities = { + .codec_cap = &codec_cap, +}; + +#define AVAILABLE_SINK_CONTEXT (BT_AUDIO_CONTEXT_TYPE_ANY) + +static le_audio_receive_cb receive_cb; + +static bool init_routine_completed; +static bool paused; + +static struct bt_csip_set_member_svc_inst *csip; + +static uint8_t flags_adv_data; +static uint8_t bass_service_uuid[BT_UUID_SIZE_16]; +static uint8_t gap_appear_adv_data[BT_UUID_SIZE_16]; +static uint8_t csip_rsi_adv_data[BT_CSIP_RSI_SIZE]; + +#define CSIP_SET_SIZE 2 +enum csip_set_rank { + CSIP_HL_RANK = 1, + CSIP_HR_RANK = 2 +}; + +/* Callback for locking state change from server side */ +static void csip_lock_changed_cb(struct bt_conn *conn, struct bt_csip_set_member_svc_inst *csip, + bool locked) +{ + LOG_DBG("Client %p %s the lock", (void *)conn, locked ? "locked" : "released"); +} + +/* Callback for SIRK read request from peer side */ +static uint8_t sirk_read_req_cb(struct bt_conn *conn, struct bt_csip_set_member_svc_inst *csip) +{ + /* Accept the request to read the SIRK, but return encrypted SIRK instead of plaintext */ + return BT_CSIP_READ_SIRK_REQ_RSP_ACCEPT_ENC; +} + +static struct bt_csip_set_member_cb csip_callbacks = { + .lock_changed = csip_lock_changed_cb, + .sirk_read_req = sirk_read_req_cb, +}; + +struct bt_csip_set_member_register_param csip_param = { + .set_size = CSIP_SET_SIZE, + .lockable = true, + .cb = &csip_callbacks, +}; + +int broadcast_sink_uuid_populate(struct net_buf_simple *uuid_buf) +{ + if (net_buf_simple_tailroom(uuid_buf) >= (BT_UUID_SIZE_16 * 3)) { + net_buf_simple_add_le16(uuid_buf, BT_UUID_BASS_VAL); + net_buf_simple_add_le16(uuid_buf, BT_UUID_PACS_VAL); + } else { + LOG_ERR("Not enough space for UUIDS"); + return -ENOMEM; + } + + return 0; +} + +int broadcast_sink_adv_populate(struct bt_data *adv_buf, uint8_t adv_buf_vacant) +{ + int ret; + uint32_t adv_buf_cnt = 0; + + if (IS_ENABLED(CONFIG_BT_CSIP_SET_MEMBER)) { + ret = bt_mgmt_adv_buffer_put(adv_buf, &adv_buf_cnt, adv_buf_vacant, + sizeof(csip_rsi_adv_data), BT_DATA_CSIS_RSI, + (void *)csip_rsi_adv_data); + if (ret) { + return ret; + } + } + + /* + * AD format required for broadcast sink with scan delegator. + * Details can be found in Basic Audio Profile Section 3.9.2. + */ + sys_put_le16(BT_UUID_BASS_VAL, &bass_service_uuid[0]); + + ret = bt_mgmt_adv_buffer_put(adv_buf, &adv_buf_cnt, adv_buf_vacant, + sizeof(bass_service_uuid), BT_DATA_SVC_DATA16, + (void *)bass_service_uuid); + if (ret) { + return ret; + } + + sys_put_le16(CONFIG_BT_DEVICE_APPEARANCE, &gap_appear_adv_data[0]); + + ret = bt_mgmt_adv_buffer_put(adv_buf, &adv_buf_cnt, adv_buf_vacant, + sizeof(gap_appear_adv_data), BT_DATA_GAP_APPEARANCE, + (void *)gap_appear_adv_data); + if (ret) { + return ret; + } + + flags_adv_data = BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR; + + ret = bt_mgmt_adv_buffer_put(adv_buf, &adv_buf_cnt, adv_buf_vacant, sizeof(uint8_t), + BT_DATA_FLAGS, (void *)&flags_adv_data); + if (ret) { + return ret; + } + + return adv_buf_cnt; +} + +static int broadcast_sink_cleanup(void) +{ + int ret; + + init_routine_completed = false; + + active_stream.pd = 0; + active_stream.stream = NULL; + active_stream.codec = NULL; + + if (broadcast_sink != NULL) { + ret = bt_bap_broadcast_sink_delete(broadcast_sink); + if (ret && ret != -EALREADY) { + return ret; + } + + broadcast_sink = NULL; + } + + return 0; +} + +static void bis_cleanup_worker(struct k_work *work) +{ + int ret; + + ret = broadcast_sink_cleanup(); + if (ret) { + LOG_WRN("Failed to clean up BISes: %d", ret); + } +} + +K_WORK_DEFINE(bis_cleanup_work, bis_cleanup_worker); + +static void le_audio_event_publish(enum le_audio_evt_type event) +{ + int ret; + struct le_audio_msg msg; + + if (event == LE_AUDIO_EVT_SYNC_LOST) { + msg.pa_sync = pa_sync_stored; + pa_sync_stored = NULL; + } + + msg.event = event; + + ret = zbus_chan_pub(&le_audio_chan, &msg, LE_AUDIO_ZBUS_EVENT_WAIT_TIME); + ERR_CHK(ret); +} + +static void print_codec(const struct audio_codec_info *codec) +{ + LOG_INF("Codec config for LC3:"); + LOG_INF("\tFrequency: %d Hz", codec->frequency); + LOG_INF("\tFrame Duration: %d us", codec->frame_duration_us); + LOG_INF("\tOctets per frame: %d (%d kbps)", codec->octets_per_sdu, codec->bitrate); + LOG_INF("\tFrames per SDU: %d", codec->blocks_per_sdu); + if (codec->chan_allocation >= 0) { + LOG_INF("\tChannel allocation: 0x%x", codec->chan_allocation); + } +} + +static void get_codec_info(const struct bt_audio_codec_cfg *codec, + struct audio_codec_info *codec_info) +{ + int ret; + + ret = le_audio_freq_hz_get(codec, &codec_info->frequency); + if (ret) { + LOG_DBG("Failed retrieving sampling frequency: %d", ret); + } + + ret = le_audio_duration_us_get(codec, &codec_info->frame_duration_us); + if (ret) { + LOG_DBG("Failed retrieving frame duration: %d", ret); + } + + ret = bt_audio_codec_cfg_get_chan_allocation(codec, &codec_info->chan_allocation, false); + if (ret == -ENODATA) { + /* Codec channel allocation not set, defaulting to 0 */ + codec_info->chan_allocation = 0; + } else if (ret) { + LOG_DBG("Failed retrieving channel allocation: %d", ret); + } + + ret = le_audio_octets_per_frame_get(codec, &codec_info->octets_per_sdu); + if (ret) { + LOG_DBG("Failed retrieving octets per frame: %d", ret); + } + + ret = le_audio_bitrate_get(codec, &codec_info->bitrate); + if (ret) { + LOG_DBG("Failed calculating bitrate: %d", ret); + } + + ret = le_audio_frame_blocks_per_sdu_get(codec, &codec_info->blocks_per_sdu); + if (codec_info->octets_per_sdu < 0) { + LOG_DBG("Failed retrieving frame blocks per SDU: %d", codec_info->octets_per_sdu); + } +} + +static void stream_started_cb(struct bt_bap_stream *stream) +{ + le_audio_event_publish(LE_AUDIO_EVT_STREAMING); + + /* NOTE: The string below is used by the Nordic CI system */ + LOG_INF("Stream index %d started", active_stream_index); + print_codec(&audio_codec_info[active_stream_index]); +} + +static void stream_stopped_cb(struct bt_bap_stream *stream, uint8_t reason) +{ + + switch (reason) { + case BT_HCI_ERR_LOCALHOST_TERM_CONN: + LOG_INF("Stream stopped by user"); + le_audio_event_publish(LE_AUDIO_EVT_NOT_STREAMING); + + break; + + case BT_HCI_ERR_CONN_FAIL_TO_ESTAB: + /* Fall-through */ + case BT_HCI_ERR_CONN_TIMEOUT: + LOG_INF("Stream sync lost"); + k_work_submit(&bis_cleanup_work); + + le_audio_event_publish(LE_AUDIO_EVT_SYNC_LOST); + + break; + + case BT_HCI_ERR_REMOTE_USER_TERM_CONN: + LOG_INF("Broadcast source stopped streaming"); + le_audio_event_publish(LE_AUDIO_EVT_NOT_STREAMING); + + break; + + case BT_HCI_ERR_TERM_DUE_TO_MIC_FAIL: + LOG_INF("MIC fail. The encryption key may be wrong"); + break; + + default: + LOG_WRN("Unhandled reason: %d", reason); + + break; + } + + /* NOTE: The string below is used by the Nordic CI system */ + LOG_INF("Stream index %d stopped. Reason: %d", active_stream_index, reason); +} + +static void stream_recv_cb(struct bt_bap_stream *stream, const struct bt_iso_recv_info *info, + struct net_buf *buf) +{ + bool bad_frame = false; + + if (receive_cb == NULL) { + LOG_ERR("The RX callback has not been set"); + return; + } + + if (!(info->flags & BT_ISO_FLAGS_VALID)) { + bad_frame = true; + } + + receive_cb(buf->data, buf->len, bad_frame, info->ts, active_stream_index, + active_stream.codec->octets_per_sdu); +} + +static struct bt_bap_stream_ops stream_ops = { + .started = stream_started_cb, + .stopped = stream_stopped_cb, + .recv = stream_recv_cb, +}; + +static bool base_subgroup_bis_cb(const struct bt_bap_base_subgroup_bis *bis, void *user_data) +{ + int ret; + struct bt_audio_codec_cfg codec_cfg = {0}; + + LOG_DBG("BIS found, index %d", bis->index); + + ret = bt_bap_base_subgroup_bis_codec_to_codec_cfg(bis, &codec_cfg); + if (ret != 0) { + LOG_WRN("Could not find codec configuration for BIS index %d, ret " + "= %d", + bis->index, ret); + return true; + } + + get_codec_info(&codec_cfg, &audio_codec_info[bis->index - 1]); + + LOG_DBG("Channel allocation: 0x%x for BIS index %d", + audio_codec_info[bis->index - 1].chan_allocation, bis->index); + + uint32_t chan_bitfield = audio_codec_info[bis->index - 1].chan_allocation; + bool single_bit = (chan_bitfield & (chan_bitfield - 1)) == 0; + + if (single_bit) { + bis_index_bitfields[bis->index - 1] = BIT(bis->index - 1); + } else { + LOG_WRN("More than one bit set in channel location, we only support 1 channel per " + "BIS"); + } + + return true; +} + +static bool base_subgroup_cb(const struct bt_bap_base_subgroup *subgroup, void *user_data) +{ + int ret; + int bis_num; + struct bt_audio_codec_cfg codec_cfg = {0}; + struct bt_bap_base_codec_id codec_id; + bool *suitable_stream_found = user_data; + + ret = bt_bap_base_subgroup_codec_to_codec_cfg(subgroup, &codec_cfg); + if (ret) { + LOG_WRN("Failed to convert codec to codec_cfg: %d", ret); + return true; + } + + ret = bt_bap_base_get_subgroup_codec_id(subgroup, &codec_id); + if (ret && codec_id.cid != BT_HCI_CODING_FORMAT_LC3) { + LOG_WRN("Failed to get codec ID or codec ID is not supported: %d", ret); + return true; + } + + ret = le_audio_bitrate_check(&codec_cfg); + if (!ret) { + LOG_WRN("Bitrate check failed"); + return true; + } + + ret = le_audio_freq_check(&codec_cfg); + if (!ret) { + LOG_WRN("Sample rate not supported"); + return true; + } + + bis_num = bt_bap_base_get_subgroup_bis_count(subgroup); + LOG_DBG("Subgroup %p has %d BISes", (void *)subgroup, bis_num); + if (bis_num > 0) { + *suitable_stream_found = true; + sync_stream_cnt = bis_num; + for (int i = 0; i < bis_num; i++) { + get_codec_info(&codec_cfg, &audio_codec_info[i]); + } + + ret = bt_bap_base_subgroup_foreach_bis(subgroup, base_subgroup_bis_cb, NULL); + if (ret < 0) { + LOG_WRN("Could not get BIS for subgroup %p: %d", (void *)subgroup, ret); + } + return false; + } + + return true; +} + +static void base_recv_cb(struct bt_bap_broadcast_sink *sink, const struct bt_bap_base *base, + size_t base_size) +{ + int ret; + bool suitable_stream_found = false; + + if (init_routine_completed) { + return; + } + + sync_stream_cnt = 0; + + uint32_t subgroup_count = bt_bap_base_get_subgroup_count(base); + + LOG_DBG("Received BASE with %d subgroup(s) from broadcast sink", subgroup_count); + + ret = bt_bap_base_foreach_subgroup(base, base_subgroup_cb, &suitable_stream_found); + if (ret != 0 && ret != -ECANCELED) { + LOG_WRN("Failed to parse subgroups: %d", ret); + return; + } + + if (suitable_stream_found) { + /* Set the initial active stream based on the defined channel of the device */ + enum audio_channel audio_channel_temp; + + channel_assignment_get(&audio_channel_temp); + if (audio_channel_temp > AUDIO_CH_NUM) { + LOG_ERR("Invalid channel assignment"); + return; + } + + active_stream_index = (uint8_t)audio_channel_temp; + + /** If the stream matching channel is not present, revert back to first BIS, e.g. + * mono stream but channel assignment is RIGHT + */ + if ((active_stream_index + 1) > sync_stream_cnt) { + LOG_WRN("BIS index: %d not found, reverting to first BIS", + (active_stream_index + 1)); + active_stream_index = 0; + } + + active_stream.stream = &audio_streams[active_stream_index]; + active_stream.codec = &audio_codec_info[active_stream_index]; + ret = bt_bap_base_get_pres_delay(base); + if (ret == -EINVAL) { + LOG_WRN("Failed to get pres_delay: %d", ret); + active_stream.pd = 0; + } else { + active_stream.pd = ret; + } + le_audio_event_publish(LE_AUDIO_EVT_CONFIG_RECEIVED); + + LOG_DBG("Channel %s active", + ((active_stream_index == AUDIO_CH_L) ? CH_L_TAG : CH_R_TAG)); + LOG_DBG("Waiting for syncable"); + } else { + LOG_DBG("Found no suitable stream"); + le_audio_event_publish(LE_AUDIO_EVT_NO_VALID_CFG); + } +} + +static void syncable_cb(struct bt_bap_broadcast_sink *sink, const struct bt_iso_biginfo *biginfo) +{ + int ret; + struct bt_bap_stream *audio_streams_p[] = {&audio_streams[active_stream_index]}; + static uint32_t prev_broadcast_id; + + LOG_DBG("Broadcast sink is syncable"); + + if (active_stream.stream != NULL && active_stream.stream->ep != NULL) { + if (active_stream.stream->ep->status.state == BT_BAP_EP_STATE_STREAMING) { + LOG_WRN("Syncable received, but already in a stream"); + return; + } + } + + if (paused) { + LOG_DBG("Syncable received, but in paused state"); + return; + } + + if (bis_index_bitfields[active_stream_index] == 0) { + LOG_ERR("No bits set in bitfield"); + return; + } else if (!IS_POWER_OF_TWO(bis_index_bitfields[active_stream_index])) { + /* Check that only one bit is set */ + LOG_ERR("Application syncs to only one stream"); + return; + } + + /* NOTE: The string below is used by the Nordic CI system */ + LOG_INF("Syncing to broadcast stream index %d", active_stream_index); + + if (IS_ENABLED(CONFIG_BT_AUDIO_BROADCAST_ENCRYPTED)) { + memcpy(bis_encryption_key, CONFIG_BT_AUDIO_BROADCAST_ENCRYPTION_KEY, + MIN(strlen(CONFIG_BT_AUDIO_BROADCAST_ENCRYPTION_KEY), + ARRAY_SIZE(bis_encryption_key))); + } else { + /* If the biginfo shows the stream is encrypted, then wait until broadcast code is + * received then start to sync. If headset is out of sync but still looking for same + * broadcaster, then the same broadcast code can be used. + */ + if (!broadcast_code_received && biginfo->encryption == true && + sink->broadcast_id != prev_broadcast_id) { + LOG_WRN("Stream is encrypted, but haven not received broadcast code"); + return; + } + + broadcast_code_received = false; + } + + ret = bt_bap_broadcast_sink_sync(broadcast_sink, bis_index_bitfields[active_stream_index], + audio_streams_p, bis_encryption_key); + + if (ret) { + LOG_WRN("Unable to sync to broadcast source, ret: %d", ret); + return; + } + + prev_broadcast_id = sink->broadcast_id; + + /* Only a single stream used for now */ + active_stream.stream = &audio_streams[active_stream_index]; + + init_routine_completed = true; +} + +static struct bt_bap_broadcast_sink_cb broadcast_sink_cbs = { + .base_recv = base_recv_cb, + .syncable = syncable_cb, +}; + +int broadcast_sink_change_active_audio_stream(void) +{ + int ret; + + if (broadcast_sink == NULL) { + LOG_WRN("No broadcast sink"); + return -ECANCELED; + } + + if (active_stream.stream != NULL && active_stream.stream->ep != NULL) { + if (active_stream.stream->ep->status.state == BT_BAP_EP_STATE_STREAMING) { + ret = bt_bap_broadcast_sink_stop(broadcast_sink); + if (ret) { + LOG_ERR("Failed to stop sink"); + } + } + } + + /* Wrap streams */ + if (++active_stream_index >= sync_stream_cnt) { + active_stream_index = 0; + } + + active_stream.stream = &audio_streams[active_stream_index]; + active_stream.codec = &audio_codec_info[active_stream_index]; + + LOG_INF("Changed to stream %d", active_stream_index); + + return 0; +} + +int broadcast_sink_config_get(uint32_t *bitrate, uint32_t *sampling_rate, uint32_t *pres_delay) +{ + if (active_stream.codec == NULL) { + LOG_WRN("No active stream to get config from"); + return -ENXIO; + } + + if (bitrate == NULL && sampling_rate == NULL && pres_delay == NULL) { + LOG_ERR("No valid pointers received"); + return -ENXIO; + } + + if (sampling_rate != NULL) { + *sampling_rate = active_stream.codec->frequency; + } + + if (bitrate != NULL) { + *bitrate = active_stream.codec->bitrate; + } + + if (pres_delay != NULL) { + if (active_stream.stream == NULL) { + LOG_WRN("No active stream"); + return -ENXIO; + } + + *pres_delay = active_stream.pd; + } + + return 0; +} + +int broadcast_sink_pa_sync_set(struct bt_le_per_adv_sync *pa_sync, uint32_t broadcast_id) +{ + int ret; + + if (pa_sync == NULL) { + LOG_ERR("Invalid PA sync received"); + return -EINVAL; + } + + LOG_DBG("Trying to set PA sync with ID: %d", broadcast_id); + + if (active_stream.stream != NULL && active_stream.stream->ep != NULL) { + if (active_stream.stream->ep->status.state == BT_BAP_EP_STATE_STREAMING) { + ret = bt_bap_broadcast_sink_stop(broadcast_sink); + if (ret) { + LOG_ERR("Failed to stop broadcast sink: %d", ret); + return ret; + } + + broadcast_sink_cleanup(); + } + } + + /* If broadcast_sink was not in an active stream we still need to clean it up */ + if (broadcast_sink != NULL) { + broadcast_sink_cleanup(); + } + + ret = bt_bap_broadcast_sink_create(pa_sync, broadcast_id, &broadcast_sink); + if (ret) { + LOG_WRN("Failed to create sink: %d", ret); + return ret; + } + + pa_sync_stored = pa_sync; + + return 0; +} + +int broadcast_sink_broadcast_code_set(uint8_t *broadcast_code) +{ + if (broadcast_code == NULL) { + LOG_ERR("Invalid broadcast code received"); + return -EINVAL; + } + + memcpy(bis_encryption_key, broadcast_code, BT_ISO_BROADCAST_CODE_SIZE); + broadcast_code_received = true; + + return 0; +} + +int broadcast_sink_start(void) +{ + if (!paused) { + LOG_WRN("Already playing"); + return -EALREADY; + } + + paused = false; + return 0; +} + +int broadcast_sink_stop(void) +{ + int ret; + + if (paused) { + LOG_WRN("Already paused"); + return -EALREADY; + } + + if (active_stream.stream == NULL || active_stream.stream->ep == NULL) { + LOG_WRN("Stream or endpoint not set"); + return -EPERM; + } + + if (active_stream.stream->ep->status.state == BT_BAP_EP_STATE_STREAMING) { + paused = true; + ret = bt_bap_broadcast_sink_stop(broadcast_sink); + if (ret) { + LOG_ERR("Failed to stop broadcast sink: %d", ret); + return ret; + } + } else { + LOG_WRN("Current stream not in streaming state"); + return -EALREADY; + } + + return 0; +} + +int broadcast_sink_disable(void) +{ + int ret; + + if (active_stream.stream != NULL && active_stream.stream->ep != NULL) { + if (active_stream.stream->ep->status.state == BT_BAP_EP_STATE_STREAMING) { + ret = bt_bap_broadcast_sink_stop(broadcast_sink); + if (ret) { + LOG_ERR("Failed to stop sink"); + } + } + } + + if (pa_sync_stored != NULL) { + ret = bt_le_per_adv_sync_delete(pa_sync_stored); + if (ret) { + LOG_ERR("Failed to delete pa_sync"); + return ret; + } + } + + ret = broadcast_sink_cleanup(); + if (ret) { + LOG_ERR("Error cleaning up"); + return ret; + } + + LOG_DBG("Broadcast sink disabled"); + + return 0; +} + +int broadcast_sink_enable(le_audio_receive_cb recv_cb) +{ + int ret; + static bool initialized; + enum audio_channel channel; + + if (initialized) { + LOG_WRN("Already initialized"); + return -EALREADY; + } + + if (recv_cb == NULL) { + LOG_ERR("Receive callback is NULL"); + return -EINVAL; + } + + receive_cb = recv_cb; + + channel_assignment_get(&channel); + + if (channel == AUDIO_CH_L) { + ret = bt_pacs_set_location(BT_AUDIO_DIR_SINK, BT_AUDIO_LOCATION_FRONT_LEFT); + csip_param.rank = CSIP_HL_RANK; + } else { + ret = bt_pacs_set_location(BT_AUDIO_DIR_SINK, BT_AUDIO_LOCATION_FRONT_RIGHT); + csip_param.rank = CSIP_HR_RANK; + } + + if (ret) { + LOG_ERR("Location set failed"); + return ret; + } + + ret = bt_pacs_set_supported_contexts(BT_AUDIO_DIR_SINK, AVAILABLE_SINK_CONTEXT); + if (ret) { + LOG_ERR("Supported context set failed. Err: %d", ret); + return ret; + } + + ret = bt_pacs_set_available_contexts(BT_AUDIO_DIR_SINK, AVAILABLE_SINK_CONTEXT); + if (ret) { + LOG_ERR("Available context set failed. Err: %d", ret); + return ret; + } + + ret = bt_pacs_cap_register(BT_AUDIO_DIR_SINK, &capabilities); + if (ret) { + LOG_ERR("Capability register failed (ret %d)", ret); + return ret; + } + + if (IS_ENABLED(CONFIG_BT_AUDIO_SCAN_DELEGATOR)) { + if (IS_ENABLED(CONFIG_BT_CSIP_SET_MEMBER_TEST_SAMPLE_DATA)) { + LOG_WRN("CSIP test sample data is used, must be changed " + "before production"); + } else { + if (strcmp(CONFIG_BT_SET_IDENTITY_RESOLVING_KEY_DEFAULT, + CONFIG_BT_SET_IDENTITY_RESOLVING_KEY) == 0) { + LOG_WRN("CSIP using the default SIRK, must be changed " + "before production"); + } + memcpy(csip_param.sirk, CONFIG_BT_SET_IDENTITY_RESOLVING_KEY, + BT_CSIP_SIRK_SIZE); + } + + ret = bt_cap_acceptor_register(&csip_param, &csip); + if (ret) { + LOG_ERR("Failed to register CAP acceptor. Err: %d", ret); + return ret; + } + + ret = bt_csip_set_member_generate_rsi(csip, csip_rsi_adv_data); + if (ret) { + LOG_ERR("Failed to generate RSI. Err: %d", ret); + return ret; + } + } + + bt_bap_broadcast_sink_register_cb(&broadcast_sink_cbs); + + for (int i = 0; i < ARRAY_SIZE(audio_streams); i++) { + audio_streams[i].ops = &stream_ops; + } + + initialized = true; + + LOG_DBG("Broadcast sink enabled"); + + return 0; +} diff --git a/src/bluetooth/bt_stream/broadcast/broadcast_sink.h b/src/bluetooth/bt_stream/broadcast/broadcast_sink.h new file mode 100644 index 0000000..29129df --- /dev/null +++ b/src/bluetooth/bt_stream/broadcast/broadcast_sink.h @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _BROADCAST_SINK_H_ +#define _BROADCAST_SINK_H_ + +#include "bt_le_audio_tx.h" + +/** + * @brief Put the UUIDs from this module into the buffer. + * + * @note This partial data is used to build a complete extended advertising packet. + * + * @param[out] uuid_buf Buffer being populated with UUIDs. + * + * @return 0 for success, error otherwise. + */ +int broadcast_sink_uuid_populate(struct net_buf_simple *uuid_buf); + +/** + * @brief Put the advertising data from this module into the buffer. + * + * @note This partial data is used to build a complete extended advertising packet. + * + * @param[out] adv_buf Buffer being populated with ext adv elements. + * @param[in] adv_buf_vacant Number of vacant elements in @p adv_buf. + * + * @return Negative values for errors or number of elements added to @p adv_buf. + */ +int broadcast_sink_adv_populate(struct bt_data *adv_buf, uint8_t adv_buf_vacant); + +/** + * @brief Change the active audio stream if the broadcast isochronous group (BIG) contains + * more than one broadcast isochronous stream (BIS). + * + * @note Only streams within the same broadcast source are relevant, meaning + * that the broadcast source is not changed. + * The active stream will iterate every time this function is called. + * + * @return 0 for success, error otherwise. + */ +int broadcast_sink_change_active_audio_stream(void); + +/** + * @brief Get configuration for the audio stream. + * + * @param[out] bitrate Pointer to the bitrate used; can be NULL. + * @param[out] sampling_rate Pointer to the sampling rate used; can be NULL. + * @param[out] pres_delay Pointer to the presentation delay used; can be NULL. + * + * @retval 0 Operation successful. + * @retval -ENXIO The feature is disabled. + */ +int broadcast_sink_config_get(uint32_t *bitrate, uint32_t *sampling_rate, uint32_t *pres_delay); + +/** + * @brief Set periodic advertising sync. + * + * @param[in] pa_sync Pointer to the periodic advertising sync. + * @param[in] broadcast_id Broadcast ID of the periodic advertising. + * + * @return 0 for success, error otherwise. + */ +int broadcast_sink_pa_sync_set(struct bt_le_per_adv_sync *pa_sync, uint32_t broadcast_id); + +/** + * @brief Set the broadcast code for the Bluetooth LE Audio broadcast sink. + * The broadcast code length is defined in BT_ISO_BROADCAST_CODE_SIZE, + * which is 16 bytes. + * + * @param[in] broadcast_code Pointer to the broadcast code. + * + * @return 0 for success, error otherwise. + */ +int broadcast_sink_broadcast_code_set(uint8_t *broadcast_code); + +/** + * @brief Start the Bluetooth LE Audio broadcast sink. + * + * @return 0 for success, error otherwise. + */ +int broadcast_sink_start(void); + +/** + * @brief Stop the Bluetooth LE Audio broadcast sink. + * + * @return 0 for success, error otherwise. + */ +int broadcast_sink_stop(void); + +/** + * @brief Disable the LE Audio broadcast (BIS) sink. + * + * @return 0 for success, error otherwise. + */ +int broadcast_sink_disable(void); + +/** + * @brief Enable the LE Audio broadcast (BIS) sink. + * + * @param[in] recv_cb Callback for receiving Bluetooth LE Audio data. + * + * @return 0 for success, error otherwise. + */ +int broadcast_sink_enable(le_audio_receive_cb recv_cb); + +#endif /* _BROADCAST_SINK_H_ */ diff --git a/src/bluetooth/bt_stream/broadcast/broadcast_source.c b/src/bluetooth/bt_stream/broadcast/broadcast_source.c new file mode 100644 index 0000000..4d31340 --- /dev/null +++ b/src/bluetooth/bt_stream/broadcast/broadcast_source.c @@ -0,0 +1,800 @@ +/* + * Copyright (c) 2022 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "broadcast_source.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "bt_mgmt.h" +#include "macros_common.h" +#include "bt_le_audio_tx.h" +#include "le_audio.h" +#include "zbus_common.h" + +#include +LOG_MODULE_REGISTER(broadcast_source, CONFIG_BROADCAST_SOURCE_LOG_LEVEL); + +/* Length-type-value size for channel allocation */ +#define LTV_CHAN_ALLOC_SIZE 6 + +#if (CONFIG_AURACAST) +/* Index values into the PBA advertising data format. + * + * See Table 4.1 of the Public Broadcast Profile, Bluetooth® Profile Specification, v1.0. + */ +#define PBA_UUID_INDEX (0) +#define PBA_FEATURES_INDEX (2) +#define PBA_METADATA_SIZE_INDEX (3) +#define PBA_METADATA_START_INDEX (4) +#endif /* CONFIG_AURACAST */ + +ZBUS_CHAN_DEFINE(le_audio_chan, struct le_audio_msg, NULL, NULL, ZBUS_OBSERVERS_EMPTY, + ZBUS_MSG_INIT(0)); + +static struct bt_cap_broadcast_source *broadcast_sources[CONFIG_BT_ISO_MAX_BIG]; +struct bt_cap_initiator_broadcast_create_param create_param[CONFIG_BT_ISO_MAX_BIG]; +/* Make sure we have statically allocated streams for all potential BISes */ +static struct bt_cap_stream cap_streams[CONFIG_BT_ISO_MAX_BIG] + [CONFIG_BT_BAP_BROADCAST_SRC_SUBGROUP_COUNT] + [CONFIG_BT_BAP_BROADCAST_SRC_STREAM_COUNT]; + +static struct bt_bap_lc3_preset lc3_preset = BT_BAP_LC3_BROADCAST_PRESET_NRF5340_AUDIO; + +static bool initialized; +static bool delete_broadcast_src[CONFIG_BT_ISO_MAX_BIG]; +static uint32_t stored_broadcast_id; + +static int metadata_u8_add(uint8_t buffer[], uint8_t *index, uint8_t type, uint8_t value) +{ + if (buffer == NULL || index == NULL) { + return -EINVAL; + } + + /* Add length of type and value */ + buffer[(*index)++] = (sizeof(type) + sizeof(uint8_t)); + buffer[(*index)++] = type; + buffer[(*index)++] = value; + + return 0; +} + +static void le_audio_event_publish(enum le_audio_evt_type event, const struct stream_index *idx) +{ + int ret; + struct le_audio_msg msg; + + msg.event = event; + msg.idx = *idx; + + ret = zbus_chan_pub(&le_audio_chan, &msg, LE_AUDIO_ZBUS_EVENT_WAIT_TIME); + ERR_CHK(ret); +} + +static int stream_index_get(struct bt_bap_stream *stream, struct stream_index *idx) +{ + for (int i = 0; i < CONFIG_BT_ISO_MAX_BIG; i++) { + for (int j = 0; j < CONFIG_BT_BAP_BROADCAST_SRC_SUBGROUP_COUNT; j++) { + for (int k = 0; k < ARRAY_SIZE(cap_streams[i][j]); k++) { + if (&cap_streams[i][j][k].bap_stream == stream) { + idx->lvl1 = i; + idx->lvl2 = j; + idx->lvl3 = k; + return 0; + } + } + } + } + + LOG_WRN("Stream %p not found", (void *)stream); + + return -EINVAL; +} + +static void stream_sent_cb(struct bt_bap_stream *stream) +{ + int ret; + struct stream_index idx; + + ret = stream_index_get(stream, &idx); + if (ret) { + return; + } + + if (IS_ENABLED(CONFIG_BT_AUDIO_BROADCAST_ZBUS_EVT_STREAM_SENT)) { + le_audio_event_publish(LE_AUDIO_EVT_STREAM_SENT, &idx); + } + + ERR_CHK(bt_le_audio_tx_stream_sent(idx)); +} + +static void stream_started_cb(struct bt_bap_stream *stream) +{ + int ret; + struct stream_index idx; + + ret = stream_index_get(stream, &idx); + if (ret) { + return; + } + + ERR_CHK(bt_le_audio_tx_stream_started(idx)); + + le_audio_event_publish(LE_AUDIO_EVT_STREAMING, &idx); + + /* NOTE: The string below is used by the Nordic CI system */ + LOG_INF("Broadcast source %p started", (void *)stream); + + le_audio_print_codec(stream->codec_cfg, BT_AUDIO_DIR_SOURCE); +} + +/** + * @brief Check if there are any streaming streams in a broadcast source (BIG). + * + * @param big_index BIG index. + * + * @return true if there are streaming streams, false otherwise. + */ +static bool source_has_streaming_streams(uint8_t big_index) +{ + for (int i = 0; i < CONFIG_BT_BAP_BROADCAST_SRC_SUBGROUP_COUNT; i++) { + for (int j = 0; j < CONFIG_BT_BAP_BROADCAST_SRC_STREAM_COUNT; j++) { + if (le_audio_ep_state_check(cap_streams[big_index][i][j].bap_stream.ep, + BT_BAP_EP_STATE_STREAMING)) { + return true; + } + } + } + + return false; +} + +static void stream_stopped_cb(struct bt_bap_stream *stream, uint8_t reason) +{ + int ret; + struct stream_index idx; + + ret = stream_index_get(stream, &idx); + if (ret) { + return; + } + + le_audio_event_publish(LE_AUDIO_EVT_NOT_STREAMING, &idx); + + LOG_INF("Broadcast source %p stopped. Reason: %d", (void *)stream, reason); + + if (delete_broadcast_src[idx.lvl1] && broadcast_sources[idx.lvl1] != NULL && + !source_has_streaming_streams(idx.lvl1)) { + ret = bt_cap_initiator_broadcast_audio_delete(broadcast_sources[idx.lvl1]); + if (ret) { + LOG_ERR("Unable to delete broadcast source %p, ret: %d", (void *)stream, + ret); + delete_broadcast_src[idx.lvl1] = false; + return; + } + + broadcast_sources[idx.lvl1] = NULL; + + LOG_INF("Broadcast source %p deleted", (void *)stream); + + delete_broadcast_src[idx.lvl1] = false; + } +} + +static struct bt_bap_stream_ops stream_ops = { + .sent = stream_sent_cb, + .started = stream_started_cb, + .stopped = stream_stopped_cb, +}; + +#if (CONFIG_AURACAST) +static void public_broadcast_features_set(uint8_t *features, uint8_t big_index) +{ + if (features == NULL) { + LOG_ERR("No pointer to features"); + return; + } + + if (big_index >= ARRAY_SIZE(create_param)) { + LOG_ERR("BIG index %d out of range", big_index); + return; + } + + if (create_param[big_index].encryption) { + *features |= BT_PBP_ANNOUNCEMENT_FEATURE_ENCRYPTION; + } + + for (uint8_t i = 0; i < create_param->subgroup_count; i++) { + int freq = bt_audio_codec_cfg_get_freq( + create_param[big_index].subgroup_params[i].codec_cfg); + + if (freq < 0) { + LOG_ERR("Unable to get frequency"); + continue; + } + + if (freq == BT_AUDIO_CODEC_CFG_FREQ_16KHZ || + freq == BT_AUDIO_CODEC_CFG_FREQ_24KHZ) { + *features |= BT_PBP_ANNOUNCEMENT_FEATURE_STANDARD_QUALITY; + } else if (freq == BT_AUDIO_CODEC_CFG_FREQ_48KHZ) { + *features |= BT_PBP_ANNOUNCEMENT_FEATURE_HIGH_QUALITY; + } else { + LOG_WRN("%dkHz is not compatible with Auracast, choose 16kHz, 24kHz or " + "48kHz", + freq); + } + } +} +#endif /* (CONFIG_AURACAST) */ + +int broadcast_source_ext_adv_populate(uint8_t big_index, bool fixed_id, uint32_t broadcast_id, + struct broadcast_source_ext_adv_data *ext_adv_data, + struct bt_data *ext_adv_buf, size_t ext_adv_buf_vacant) +{ + int ret; + uint32_t ext_adv_buf_cnt = 0; + + if (big_index >= CONFIG_BT_ISO_MAX_BIG) { + LOG_ERR("Trying to populate ext adv for BIG %d out of %d", big_index, + CONFIG_BT_ISO_MAX_BIG); + return -EINVAL; + } + + if (ext_adv_data == NULL || ext_adv_buf == NULL || ext_adv_buf_vacant == 0) { + LOG_ERR("Advertising populate failed."); + return -EINVAL; + } + + size_t brdcast_name_size = strlen(ext_adv_data->brdcst_name_buf); + + ret = bt_mgmt_adv_buffer_put(ext_adv_buf, &ext_adv_buf_cnt, ext_adv_buf_vacant, + brdcast_name_size, BT_DATA_BROADCAST_NAME, + (void *)ext_adv_data->brdcst_name_buf); + if (ret) { + return ret; + } + + if (!fixed_id) { + /* Use a random broadcast ID */ + ret = bt_rand(&broadcast_id, BT_AUDIO_BROADCAST_ID_SIZE); + if (ret) { + LOG_WRN("Unable to generate broadcast ID: %d\n", ret); + return ret; + } + } + + stored_broadcast_id = broadcast_id; + + sys_put_le16(BT_UUID_BROADCAST_AUDIO_VAL, ext_adv_data->brdcst_id_buf); + + sys_put_le24(broadcast_id, &ext_adv_data->brdcst_id_buf[BROADCAST_SOURCE_ADV_ID_START]); + + ret = bt_mgmt_adv_buffer_put(ext_adv_buf, &ext_adv_buf_cnt, ext_adv_buf_vacant, + sizeof(ext_adv_data->brdcst_id_buf), BT_DATA_SVC_DATA16, + (void *)ext_adv_data->brdcst_id_buf); + if (ret) { + return ret; + } + + sys_put_le16(CONFIG_BT_DEVICE_APPEARANCE, ext_adv_data->brdcst_appearance_buf); + + ret = bt_mgmt_adv_buffer_put(ext_adv_buf, &ext_adv_buf_cnt, ext_adv_buf_vacant, + sizeof(ext_adv_data->brdcst_appearance_buf), + BT_DATA_GAP_APPEARANCE, + (void *)ext_adv_data->brdcst_appearance_buf); + if (ret) { + return ret; + } + +#if (CONFIG_AURACAST) + uint8_t meta_data_buf_size = 0; + + sys_put_le16(BT_UUID_PBA_VAL, &ext_adv_data->pba_buf[PBA_UUID_INDEX]); + public_broadcast_features_set(&ext_adv_data->pba_buf[PBA_FEATURES_INDEX], big_index); + + /* Metadata */ + /* Parental rating */ + ret = metadata_u8_add(&ext_adv_data->pba_buf[PBA_METADATA_START_INDEX], &meta_data_buf_size, + BT_AUDIO_METADATA_TYPE_PARENTAL_RATING, + CONFIG_BT_AUDIO_BROADCAST_PARENTAL_RATING); + if (ret) { + return ret; + } + + /* Active flag */ + ret = metadata_u8_add(&ext_adv_data->pba_buf[PBA_METADATA_START_INDEX], &meta_data_buf_size, + BT_AUDIO_METADATA_TYPE_AUDIO_STATE, BT_AUDIO_ACTIVE_STATE_ENABLED); + if (ret) { + return ret; + } + + /* Metadata size */ + ext_adv_data->pba_buf[PBA_METADATA_SIZE_INDEX] = meta_data_buf_size; + + /* Add PBA buffer to extended advertising data */ + ret = bt_mgmt_adv_buffer_put(ext_adv_buf, &ext_adv_buf_cnt, ext_adv_buf_vacant, + BROADCAST_SOURCE_PBA_HEADER_SIZE + + ext_adv_data->pba_buf[PBA_METADATA_SIZE_INDEX], + BT_DATA_SVC_DATA16, (void *)ext_adv_data->pba_buf); + if (ret) { + return ret; + } + +#endif /* (CONFIG_AURACAST) */ + + return ext_adv_buf_cnt; +} + +int broadcast_source_per_adv_populate(uint8_t big_index, + struct broadcast_source_per_adv_data *per_adv_data, + struct bt_data *per_adv_buf, size_t per_adv_buf_vacant) +{ + int ret; + size_t per_adv_buf_cnt = 0; + + if (big_index >= CONFIG_BT_ISO_MAX_BIG) { + LOG_ERR("Trying to populate per adv for BIG %d out of %d", big_index, + CONFIG_BT_ISO_MAX_BIG); + return -EINVAL; + } + + if (per_adv_data == NULL || per_adv_buf == NULL || per_adv_buf_vacant == 0) { + LOG_ERR("Periodic advertising populate failed."); + return -EINVAL; + } + + /* Setup periodic advertising data */ + ret = bt_cap_initiator_broadcast_get_base(broadcast_sources[big_index], + per_adv_data->base_buf); + if (ret) { + LOG_ERR("Failed to get encoded BASE: %d", ret); + return ret; + } + + ret = bt_mgmt_adv_buffer_put(per_adv_buf, &per_adv_buf_cnt, per_adv_buf_vacant, + per_adv_data->base_buf->len, BT_DATA_SVC_DATA16, + (void *)per_adv_data->base_buf->data); + if (ret) { + return ret; + } + + return per_adv_buf_cnt; +} + +/** + * @brief Set the channel allocation to a preset codec configuration. + * + * @param data The preset codec configuration. + * @param data_len Length of @p data + * @param loc Location bitmask setting. + */ +static void bt_audio_codec_allocation_set(uint8_t *data, uint8_t data_len, + enum bt_audio_location loc) +{ + data[0] = data_len - 1; + data[1] = BT_AUDIO_CODEC_CFG_CHAN_ALLOC; + sys_put_le32((const uint32_t)loc, &data[2]); +} + +static int create_param_produce(uint8_t big_index, + struct broadcast_source_big const *const ext_create_param, + struct bt_cap_initiator_broadcast_create_param *create_param) +{ + int ret; + + if (big_index >= CONFIG_BT_ISO_MAX_BIG) { + LOG_ERR("Trying to create param for BIG %d out of %d", big_index, + CONFIG_BT_ISO_MAX_BIG); + return -EINVAL; + } + + if (ext_create_param->num_subgroups > CONFIG_BT_BAP_BROADCAST_SRC_SUBGROUP_COUNT) { + LOG_ERR("Trying to create %d subgroups, but only allocated memory for %d", + ext_create_param->num_subgroups, + CONFIG_BT_BAP_BROADCAST_SRC_SUBGROUP_COUNT); + return -EINVAL; + } + + uint8_t total_num_bis = 0; + + for (size_t i = 0U; i < ext_create_param->num_subgroups; i++) { + for (size_t j = 0; j < ext_create_param->subgroups[i].num_bises; j++) { + total_num_bis++; + } + } + + if (total_num_bis > CONFIG_BT_BAP_BROADCAST_SRC_STREAM_COUNT) { + LOG_ERR("Trying to set up %d BISes in total, but only allocated memory for %d", + total_num_bis, CONFIG_BT_BAP_BROADCAST_SRC_STREAM_COUNT); + return -EINVAL; + } + + static struct bt_cap_initiator_broadcast_stream_param + stream_params[CONFIG_BT_BAP_BROADCAST_SRC_SUBGROUP_COUNT][2]; + static uint8_t bis_codec_data[CONFIG_BT_BAP_BROADCAST_SRC_SUBGROUP_COUNT][2] + [LTV_CHAN_ALLOC_SIZE]; + static struct bt_cap_initiator_broadcast_subgroup_param + subgroup_params[CONFIG_BT_BAP_BROADCAST_SRC_SUBGROUP_COUNT]; + + (void)memset(cap_streams[big_index], 0, sizeof(cap_streams[big_index])); + + for (size_t i = 0U; i < ext_create_param->num_subgroups; i++) { + enum bt_audio_location subgroup_loc = 0; + + for (size_t j = 0; j < ext_create_param->subgroups[i].num_bises; j++) { + stream_params[i][j].stream = &cap_streams[big_index][i][j]; + + stream_params[i][j].data_len = ARRAY_SIZE(bis_codec_data[i][j]); + stream_params[i][j].data = bis_codec_data[i][j]; + + enum bt_audio_location loc = ext_create_param->subgroups[i].location[j]; + + subgroup_loc |= loc; + bt_audio_codec_allocation_set(stream_params[i][j].data, + stream_params[i][j].data_len, loc); + } + + subgroup_params[i].stream_count = ext_create_param->subgroups[i].num_bises; + subgroup_params[i].stream_params = stream_params[i]; + subgroup_params[i].codec_cfg = + &ext_create_param->subgroups[i].group_lc3_preset.codec_cfg; + ret = bt_audio_codec_cfg_set_chan_allocation(subgroup_params[i].codec_cfg, + subgroup_loc); + if (ret < 0) { + LOG_WRN("Failed to set location: %d", ret); + return -EINVAL; + } + + ret = bt_audio_codec_cfg_meta_set_stream_context( + subgroup_params[i].codec_cfg, ext_create_param->subgroups[i].context); + if (ret < 0) { + LOG_WRN("Failed to set context: %d", ret); + return -EINVAL; + } + } + + /* Create broadcast_source */ + create_param->subgroup_count = ext_create_param->num_subgroups; + create_param->subgroup_params = subgroup_params; + /* All QoS within the BIG will be the same, so we get the one from the first subgroup */ + create_param->qos = &ext_create_param->subgroups[0].group_lc3_preset.qos; + + create_param->packing = ext_create_param->packing; + + create_param->encryption = ext_create_param->encryption; + if (ext_create_param->encryption) { + memset(create_param->broadcast_code, 0, sizeof(create_param->broadcast_code)); + memcpy(create_param->broadcast_code, ext_create_param->broadcast_code, + sizeof(ext_create_param->broadcast_code)); + } + + return 0; +} + +bool broadcast_source_is_streaming(uint8_t big_index) +{ + if (big_index >= CONFIG_BT_ISO_MAX_BIG) { + LOG_ERR("Trying to check BIG %d out of %d", big_index, CONFIG_BT_ISO_MAX_BIG); + return false; + } + + if (broadcast_sources[big_index] == NULL) { + return false; + } + + /* All streams in a broadcast source is in the same state, + * so we can just check the first stream + */ + return le_audio_ep_state_check(cap_streams[big_index][0][0].bap_stream.ep, + BT_BAP_EP_STATE_STREAMING); +} + +int broadcast_source_start(uint8_t big_index, struct bt_le_ext_adv *ext_adv) +{ + int ret; + + if (ext_adv == NULL) { + LOG_ERR("No advertising set available"); + return -EINVAL; + } + + if (big_index >= CONFIG_BT_ISO_MAX_BIG) { + LOG_ERR("Trying to start BIG %d out of %d", big_index, CONFIG_BT_ISO_MAX_BIG); + return -EINVAL; + } + + LOG_DBG("Starting broadcast source"); + + /* All streams in a broadcast source is in the same state, + * so we can just check the first stream + */ + if (cap_streams[big_index][0][0].bap_stream.ep == NULL) { + LOG_ERR("stream->ep is NULL"); + return -ECANCELED; + } + + if (le_audio_ep_state_check(cap_streams[big_index][0][0].bap_stream.ep, + BT_BAP_EP_STATE_STREAMING)) { + LOG_WRN("Already streaming"); + return -EALREADY; + } + + ret = bt_cap_initiator_broadcast_audio_start(broadcast_sources[big_index], ext_adv); + if (ret) { + LOG_WRN("Failed to start broadcast, ret: %d", ret); + return ret; + } + + return 0; +} + +int broadcast_source_stop(uint8_t big_index) +{ + int ret; + + if (big_index >= CONFIG_BT_ISO_MAX_BIG) { + LOG_ERR("Trying to stop BIG %d out of %d", big_index, CONFIG_BT_ISO_MAX_BIG); + return -EINVAL; + } + + /* All streams in a broadcast source is in the same state, + * so we can just check the first stream + */ + if (cap_streams[big_index][0][0].bap_stream.ep == NULL) { + LOG_ERR("stream->ep is NULL"); + return -ECANCELED; + } + + if (le_audio_ep_state_check(cap_streams[big_index][0][0].bap_stream.ep, + BT_BAP_EP_STATE_STREAMING)) { + ret = bt_cap_initiator_broadcast_audio_stop(broadcast_sources[big_index]); + if (ret) { + LOG_WRN("Failed to stop broadcast, ret: %d", ret); + return ret; + } + } else { + LOG_WRN("Not in a streaming state"); + return -EINVAL; + } + + return 0; +} + +/* TODO: Use the function below once + * https://github.com/zephyrproject-rtos/zephyr/pull/72908 is merged + */ +#if CONFIG_CUSTOM_BROADCASTER +static uint8_t audio_map_location_get(struct bt_bap_stream *bap_stream) +{ + int ret; + enum bt_audio_location loc; + + ret = bt_audio_codec_cfg_get_chan_allocation(bap_stream->codec_cfg, &loc, false); + if (ret) { + LOG_WRN("Unable to find location, defaulting to left"); + return AUDIO_CH_L; + } + + /* For now, only front_left and front_right are supported, + * left is default for everything else. + */ + + if (loc == BT_AUDIO_LOCATION_FRONT_RIGHT) { + LOG_WRN("Setting right"); + return AUDIO_CH_R; + } + + return AUDIO_CH_L; +} +#endif + +int broadcast_source_id_get(uint8_t big_index, uint32_t *broadcast_id) +{ + if (big_index >= CONFIG_BT_ISO_MAX_BIG) { + LOG_ERR("Failed to get broadcast ID for BIG %d out of %d", big_index, + CONFIG_BT_ISO_MAX_BIG); + return -EINVAL; + } + + if (broadcast_sources[big_index] == NULL) { + LOG_ERR("No broadcast source"); + return -EINVAL; + } + + if (broadcast_id == NULL) { + LOG_ERR("NULL pointer given for broadcast_id"); + return -EINVAL; + } + + *broadcast_id = stored_broadcast_id; + + return 0; +} + +int broadcast_source_send(uint8_t big_index, uint8_t subgroup_index, + struct le_audio_encoded_audio enc_audio) +{ + int ret; + uint8_t num_active_streams = 0; + + if (big_index >= CONFIG_BT_ISO_MAX_BIG) { + LOG_ERR("Trying to send to BIG %d out of %d", big_index, CONFIG_BT_ISO_MAX_BIG); + return -EINVAL; + } + + struct le_audio_tx_info + tx[CONFIG_BT_ISO_MAX_CHAN * CONFIG_BT_BAP_BROADCAST_SRC_SUBGROUP_COUNT]; + + for (int i = 0; i < ARRAY_SIZE(cap_streams[big_index][subgroup_index]); i++) { + if (!le_audio_ep_state_check( + cap_streams[big_index][subgroup_index][i].bap_stream.ep, + BT_BAP_EP_STATE_STREAMING)) { + /* Skip streams not in a streaming state */ + continue; + } + + /* Set cap stream pointer */ + tx[num_active_streams].cap_stream = &cap_streams[big_index][subgroup_index][i]; + + /* Set index */ + tx[num_active_streams].idx.lvl1 = big_index; + tx[num_active_streams].idx.lvl2 = subgroup_index; + tx[num_active_streams].idx.lvl3 = i; + + /* Set channel location */ + /* TODO: Use the function below once + * https://github.com/zephyrproject-rtos/zephyr/pull/72908 is merged + */ + /* tx[num_active_streams].audio_channel = audio_map_location_get(stream);*/ + tx[num_active_streams].audio_channel = i; + + num_active_streams++; + } + + if (num_active_streams == 0) { + LOG_WRN("No active streams"); + return -ECANCELED; + } + + ret = bt_le_audio_tx_send(tx, num_active_streams, enc_audio); + if (ret) { + return ret; + } + + return 0; +} + +int broadcast_source_disable(uint8_t big_index) +{ + int ret; + + if (big_index >= CONFIG_BT_ISO_MAX_BIG) { + LOG_ERR("Trying to disable BIG %d out of %d", big_index, CONFIG_BT_ISO_MAX_BIG); + return -EINVAL; + } + + /* All streams in a broadcast source is in the same state, + * so we can just check the first stream + */ + if (le_audio_ep_state_check(cap_streams[big_index][0][0].bap_stream.ep, + BT_BAP_EP_STATE_STREAMING)) { + /* Deleting broadcast source in stream_stopped_cb() */ + delete_broadcast_src[big_index] = true; + + ret = bt_cap_initiator_broadcast_audio_stop(broadcast_sources[big_index]); + if (ret) { + LOG_WRN("Failed to stop broadcast source"); + return ret; + } + } else if (broadcast_sources[big_index] != NULL) { + ret = bt_cap_initiator_broadcast_audio_delete(broadcast_sources[big_index]); + if (ret) { + LOG_WRN("Failed to delete broadcast source"); + return ret; + } + + broadcast_sources[big_index] = NULL; + } + + initialized = false; + + LOG_DBG("Broadcast source disabled"); + + return 0; +} + +/* Will set up one BIG, one subgroup and two BISes */ +void broadcast_source_default_create(struct broadcast_source_big *broadcast_param) +{ + static enum bt_audio_location location[2] = {BT_AUDIO_LOCATION_FRONT_LEFT, + BT_AUDIO_LOCATION_FRONT_RIGHT}; + static struct subgroup_config subgroups; + + subgroups.group_lc3_preset = lc3_preset; + + subgroups.num_bises = 2; + subgroups.context = BT_AUDIO_CONTEXT_TYPE_MEDIA; + + subgroups.location = location; + + broadcast_param->subgroups = &subgroups; + broadcast_param->num_subgroups = 1; + + if (IS_ENABLED(CONFIG_BT_AUDIO_PACKING_INTERLEAVED)) { + broadcast_param->packing = BT_ISO_PACKING_INTERLEAVED; + } else { + broadcast_param->packing = BT_ISO_PACKING_SEQUENTIAL; + } + + if (IS_ENABLED(CONFIG_BT_AUDIO_BROADCAST_ENCRYPTED)) { + broadcast_param->encryption = true; + memset(broadcast_param->broadcast_code, 0, sizeof(broadcast_param->broadcast_code)); + memcpy(broadcast_param->broadcast_code, CONFIG_BT_AUDIO_BROADCAST_ENCRYPTION_KEY, + MIN(sizeof(CONFIG_BT_AUDIO_BROADCAST_ENCRYPTION_KEY), + sizeof(broadcast_param->broadcast_code))); + } else { + broadcast_param->encryption = false; + } + + if (IS_ENABLED(CONFIG_BT_AUDIO_BROADCAST_IMMEDIATE_FLAG)) { + bt_audio_codec_cfg_meta_set_bcast_audio_immediate_rend_flag( + &subgroups.group_lc3_preset.codec_cfg); + } + + bt_audio_codec_cfg_meta_set_lang(&subgroups.group_lc3_preset.codec_cfg, "eng"); +} + +int broadcast_source_enable(struct broadcast_source_big const *const broadcast_param, + uint8_t big_index) +{ + int ret; + + if (big_index >= CONFIG_BT_ISO_MAX_BIG) { + LOG_ERR("Trying to set up %d BIGS, but only allocated memory for %d", big_index, + CONFIG_BT_ISO_MAX_BIG); + return -EINVAL; + } + + if (!initialized) { + bt_le_audio_tx_init(); + } + + LOG_INF("Enabling broadcast_source %d", big_index); + + ret = create_param_produce(big_index, broadcast_param, &create_param[big_index]); + if (ret) { + LOG_ERR("Failed to create the create_param: %d", ret); + return ret; + } + + /* Register callbacks per stream */ + for (size_t j = 0U; j < create_param[big_index].subgroup_count; j++) { + for (size_t k = 0; k < create_param[big_index].subgroup_params[j].stream_count; + k++) { + bt_cap_stream_ops_register( + create_param[big_index].subgroup_params[j].stream_params[k].stream, + &stream_ops); + } + } + + ret = bt_cap_initiator_broadcast_audio_create(&create_param[big_index], + &broadcast_sources[big_index]); + if (ret) { + LOG_ERR("Failed to create broadcast source, ret: %d", ret); + return ret; + } + + initialized = true; + + LOG_DBG("Broadcast source enabled"); + + return 0; +} diff --git a/src/bluetooth/bt_stream/broadcast/broadcast_source.h b/src/bluetooth/bt_stream/broadcast/broadcast_source.h new file mode 100644 index 0000000..d13a04f --- /dev/null +++ b/src/bluetooth/bt_stream/broadcast/broadcast_source.h @@ -0,0 +1,266 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _BROADCAST_SOURCE_H_ +#define _BROADCAST_SOURCE_H_ + +#include +#include +#include "bt_le_audio_tx.h" + +#if CONFIG_BT_AUDIO_BROADCAST_CONFIGURABLE +#define BT_BAP_LC3_BROADCAST_PRESET_NRF5340_AUDIO \ + BT_BAP_LC3_PRESET_CONFIGURABLE( \ + BT_AUDIO_LOCATION_FRONT_LEFT | BT_AUDIO_LOCATION_FRONT_RIGHT, \ + BT_AUDIO_CONTEXT_TYPE_MEDIA, CONFIG_BT_AUDIO_BITRATE_BROADCAST_SRC) + +#elif CONFIG_BT_BAP_BROADCAST_16_2_1 +#define BT_BAP_LC3_BROADCAST_PRESET_NRF5340_AUDIO \ + BT_BAP_LC3_BROADCAST_PRESET_16_2_1(BT_AUDIO_LOCATION_FRONT_LEFT | \ + BT_AUDIO_LOCATION_FRONT_RIGHT, \ + BT_AUDIO_CONTEXT_TYPE_MEDIA) + +#elif CONFIG_BT_BAP_BROADCAST_16_2_2 +#define BT_BAP_LC3_BROADCAST_PRESET_NRF5340_AUDIO \ + BT_BAP_LC3_BROADCAST_PRESET_16_2_2(BT_AUDIO_LOCATION_FRONT_LEFT | \ + BT_AUDIO_LOCATION_FRONT_RIGHT, \ + BT_AUDIO_CONTEXT_TYPE_MEDIA) + +#elif CONFIG_BT_BAP_BROADCAST_24_2_1 +#define BT_BAP_LC3_BROADCAST_PRESET_NRF5340_AUDIO \ + BT_BAP_LC3_BROADCAST_PRESET_24_2_1(BT_AUDIO_LOCATION_FRONT_LEFT | \ + BT_AUDIO_LOCATION_FRONT_RIGHT, \ + BT_AUDIO_CONTEXT_TYPE_MEDIA) + +#elif CONFIG_BT_BAP_BROADCAST_24_2_2 +#define BT_BAP_LC3_BROADCAST_PRESET_NRF5340_AUDIO \ + BT_BAP_LC3_BROADCAST_PRESET_24_2_2(BT_AUDIO_LOCATION_FRONT_LEFT | \ + BT_AUDIO_LOCATION_FRONT_RIGHT, \ + BT_AUDIO_CONTEXT_TYPE_MEDIA) + +#elif CONFIG_BT_BAP_BROADCAST_48_2_1 +#define BT_BAP_LC3_BROADCAST_PRESET_NRF5340_AUDIO \ + BT_BAP_LC3_BROADCAST_PRESET_48_2_1(BT_AUDIO_LOCATION_FRONT_LEFT | \ + BT_AUDIO_LOCATION_FRONT_RIGHT, \ + BT_AUDIO_CONTEXT_TYPE_MEDIA) +#elif CONFIG_BT_BAP_BROADCAST_48_2_2 +#define BT_BAP_LC3_BROADCAST_PRESET_NRF5340_AUDIO \ + BT_BAP_LC3_BROADCAST_PRESET_48_2_2(BT_AUDIO_LOCATION_FRONT_LEFT | \ + BT_AUDIO_LOCATION_FRONT_RIGHT, \ + BT_AUDIO_CONTEXT_TYPE_MEDIA) + +#elif CONFIG_BT_BAP_BROADCAST_48_4_1 +#define BT_BAP_LC3_BROADCAST_PRESET_NRF5340_AUDIO \ + BT_BAP_LC3_BROADCAST_PRESET_48_4_1(BT_AUDIO_LOCATION_FRONT_LEFT | \ + BT_AUDIO_LOCATION_FRONT_RIGHT, \ + BT_AUDIO_CONTEXT_TYPE_MEDIA) + +#elif CONFIG_BT_BAP_BROADCAST_48_4_2 +#define BT_BAP_LC3_BROADCAST_PRESET_NRF5340_AUDIO \ + BT_BAP_LC3_BROADCAST_PRESET_48_4_2(BT_AUDIO_LOCATION_FRONT_LEFT | \ + BT_AUDIO_LOCATION_FRONT_RIGHT, \ + BT_AUDIO_CONTEXT_TYPE_MEDIA) + +#elif CONFIG_BT_BAP_BROADCAST_48_6_1 +#define BT_BAP_LC3_BROADCAST_PRESET_NRF5340_AUDIO \ + BT_BAP_LC3_BROADCAST_PRESET_48_6_1(BT_AUDIO_LOCATION_FRONT_LEFT | \ + BT_AUDIO_LOCATION_FRONT_RIGHT, \ + BT_AUDIO_CONTEXT_TYPE_MEDIA) + +#elif CONFIG_BT_BAP_BROADCAST_48_6_2 +#define BT_BAP_LC3_BROADCAST_PRESET_NRF5340_AUDIO \ + BT_BAP_LC3_BROADCAST_PRESET_48_6_2(BT_AUDIO_LOCATION_FRONT_LEFT | \ + BT_AUDIO_LOCATION_FRONT_RIGHT, \ + BT_AUDIO_CONTEXT_TYPE_MEDIA) +#else +#error Unsupported LC3 codec preset for broadcast +#endif /* CONFIG_BT_AUDIO_BROADCAST_CONFIGURABLE */ + +/* Size of the Public Broadcast Announcement header, 2-octet Service UUID followed by + * an octet for the features and an octet for the length of the meta data field. + */ +#define BROADCAST_SOURCE_PBA_HEADER_SIZE (BT_UUID_SIZE_16 + (sizeof(uint8_t) * 2)) + +#define BROADCAST_SOURCE_ADV_NAME_MAX (32) +#define BROADCAST_SOURCE_ADV_ID_START (BT_UUID_SIZE_16) + +struct subgroup_config { + enum bt_audio_location *location; + uint8_t num_bises; + enum bt_audio_context context; + struct bt_bap_lc3_preset group_lc3_preset; + char *preset_name; +}; + +struct broadcast_source_big { + struct subgroup_config *subgroups; + uint8_t num_subgroups; + uint8_t packing; + bool encryption; + uint8_t broadcast_code[BT_ISO_BROADCAST_CODE_SIZE]; + char broadcast_name[BROADCAST_SOURCE_ADV_NAME_MAX + 1]; + char adv_name[CONFIG_BT_DEVICE_NAME_MAX + 1]; + bool fixed_id; + uint32_t broadcast_id; +}; + +/** + * @brief Advertising data for broadcast source. + */ +struct broadcast_source_ext_adv_data { + /* Broadcast Audio Streaming UUIDs. */ + struct net_buf_simple *uuid_buf; + + /* Broadcast Audio Streaming Endpoint advertising data. */ + uint8_t brdcst_id_buf[BT_UUID_SIZE_16 + BT_AUDIO_BROADCAST_ID_SIZE]; + + /* Buffer for Appearance. */ + uint8_t brdcst_appearance_buf[(sizeof(uint8_t) * 2)]; + + /* Broadcast name, must be between 4 and 32 UTF-8 encoded characters in length. */ + uint8_t brdcst_name_buf[BROADCAST_SOURCE_ADV_NAME_MAX]; + +#if (CONFIG_AURACAST) + /* Number of free metadata items */ + uint8_t pba_metadata_vacant_cnt; + + /* Public Broadcast Announcement buffer. */ + uint8_t *pba_buf; +#endif /* (CONFIG_AURACAST) */ +}; + +/** + * @brief Periodic advertising data for broadcast source. + */ +struct broadcast_source_per_adv_data { + /* Buffer for periodic advertising data */ + struct net_buf_simple *base_buf; +}; + +/** + * @brief Populate the extended advertising data buffer. + * + * @param[in] big_index Index of the Broadcast Isochronous Group (BIG) to get + * advertising data for. + * @param[in] fixed_id Flag to indicate if the broadcast ID will be random or not. + * @param[in] broadcast_id Broadcast ID to be used in the advertising data if + * @p fixed_id is set to true. The broadcast ID is three octets + * long. + * @param[in] ext_adv_data Pointer to the extended advertising buffers. + * @param[out] ext_adv_buf Pointer to the bt_data used for extended advertising. + * @param[out] ext_adv_buf_vacant Pointer to unused size of @p ext_adv_buf. + * + * @return Negative values for errors or number of elements added to @p ext_adv_buf. + */ +int broadcast_source_ext_adv_populate(uint8_t big_index, bool fixed_id, uint32_t broadcast_id, + struct broadcast_source_ext_adv_data *ext_adv_data, + struct bt_data *ext_adv_buf, size_t ext_adv_buf_vacant); + +/** + * @brief Populate the periodic advertising data buffer. + * + * @param[in] big_index Index of the Broadcast Isochronous Group (BIG) to get + * advertising data for. + * @param[in] per_adv_data Pointer to a structure of periodic advertising buffers. + * @param[out] per_adv_buf Pointer to the bt_data used for periodic advertising. + * @param[out] per_adv_buf_vacant Pointer to unused size of @p per_adv_buf. + * + * @return Negative values for errors or number of elements added to @p per_adv_buf. + */ +int broadcast_source_per_adv_populate(uint8_t big_index, + struct broadcast_source_per_adv_data *per_adv_data, + struct bt_data *per_adv_buf, size_t per_adv_buf_vacant); + +/** + * @brief Check if the broadcast source is streaming. + * + * @param[in] big_index Index of the Broadcast Isochronous Group (BIG) to check. + * + * @retval True The broadcast source is streaming. + * @retval False The broadcast source is not streaming. + */ + +bool broadcast_source_is_streaming(uint8_t big_index); + +/** + * @brief Start the Bluetooth LE Audio broadcast (BIS) source. + * + * @param[in] big_index Index of the Broadcast Isochronous Group (BIG) to start. + * @param[in] ext_adv Pointer to the extended advertising set; can be NULL if a stream + * is restarted. + * + * @return 0 for success, error otherwise. + */ +int broadcast_source_start(uint8_t big_index, struct bt_le_ext_adv *ext_adv); + +/** + * @brief Stop the Bluetooth LE Audio broadcast (BIS) source. + * + * @param[in] big_index Index of the Broadcast Isochronous Group (BIG) to stop. + * + * @return 0 for success, error otherwise. + */ +int broadcast_source_stop(uint8_t big_index); + +/** + * @brief Get the broadcast ID for the given Broadcast Isochronous Group (BIG). + * + * @note The broadcast ID is used to identify the broadcast. Its value is three octets long. + * This function should only be called after the BIG has been created. + * + * @param[in] big_index Index of the Broadcast Isochronous Group (BIG) to get the broadcast + * ID for. + * @param[out] broadcast_id Pointer to the broadcast ID. + * + * @return 0 for success, error otherwise. + */ +int broadcast_source_id_get(uint8_t big_index, uint32_t *broadcast_id); + +/** + * @brief Broadcast the Bluetooth LE Audio data. + * + * @param[in] big_index Index of the Broadcast Isochronous Group (BIG) to broadcast. + * @param[in] subgroup_index Index of the subgroup to broadcast. + * @param[in] enc_audio Encoded audio struct. + * + * @return 0 for success, error otherwise. + */ +int broadcast_source_send(uint8_t big_index, uint8_t subgroup_index, + struct le_audio_encoded_audio enc_audio); + +/** + * @brief Disable the LE Audio broadcast (BIS) source. + * + * @param[in] big_index Index of the Broadcast Isochronous Group (BIG) to disable. + * + * @return 0 for success, error otherwise. + */ +int broadcast_source_disable(uint8_t big_index); + +/** + * @brief Create a set up for a default broadcaster. + * + * @note This will create the parameters for a simple broadcaster with 1 Broadcast + * Isochronous Group (BIG), 1 subgroup, and 2 BISes. + * The BISes will be front_left and front_right and language will be set to 'eng'. + * + * @param[out] broadcast_param Pointer to populate with parameters for setting up the broadcaster. + */ +void broadcast_source_default_create(struct broadcast_source_big *broadcast_param); + +/** + * @brief Enable the LE Audio broadcast (BIS) source. + * + * @param[in] broadcast_param Array of create parameters for creating a Broadcast Isochronous + * Group (BIG). + * @param[in] big_index Index of the BIG to enable. + * + * @return 0 for success, error otherwise. + */ +int broadcast_source_enable(struct broadcast_source_big const *const broadcast_param, + uint8_t big_index); + +#endif /* _BROADCAST_SOURCE_H_ */ diff --git a/src/bluetooth/bt_stream/bt_le_audio_tx/CMakeLists.txt b/src/bluetooth/bt_stream/bt_le_audio_tx/CMakeLists.txt new file mode 100644 index 0000000..e22ad9b --- /dev/null +++ b/src/bluetooth/bt_stream/bt_le_audio_tx/CMakeLists.txt @@ -0,0 +1,10 @@ +# +# Copyright (c) 2024 Nordic Semiconductor +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +if (CONFIG_BT_AUDIO_TX) + target_sources(app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/bt_le_audio_tx.c) +endif() diff --git a/src/bluetooth/bt_stream/bt_le_audio_tx/bt_le_audio_tx.c b/src/bluetooth/bt_stream/bt_le_audio_tx/bt_le_audio_tx.c new file mode 100644 index 0000000..41608e6 --- /dev/null +++ b/src/bluetooth/bt_stream/bt_le_audio_tx/bt_le_audio_tx.c @@ -0,0 +1,368 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "bt_le_audio_tx.h" + +#include +#include +#include +#include <../subsys/bluetooth/audio/bap_stream.h> +#include + +#include "zbus_common.h" +#include "audio_sync_timer.h" + +#include +LOG_MODULE_REGISTER(bt_le_audio_tx, CONFIG_BT_LE_AUDIO_TX_LOG_LEVEL); + +ZBUS_CHAN_DEFINE(sdu_ref_chan, struct sdu_ref_msg, NULL, NULL, ZBUS_OBSERVERS_EMPTY, + ZBUS_MSG_INIT(0)); + +#define HANDLE_INVALID 0xFFFF + +#define HCI_ISO_BUF_PER_CHAN 2 + +#if defined(CONFIG_BT_ISO_MAX_CIG) && defined(CONFIG_BT_ISO_MAX_BIG) +#define GROUP_MAX (CONFIG_BT_ISO_MAX_BIG + CONFIG_BT_ISO_MAX_CIG) +#elif defined(CONFIG_BT_ISO_MAX_CIG) +#define GROUP_MAX CONFIG_BT_ISO_MAX_CIG +#elif defined(CONFIG_BT_ISO_MAX_BIG) +#define GROUP_MAX CONFIG_BT_ISO_MAX_BIG +#else +#error Neither CIG nor BIG defined +#endif + +#if (defined(CONFIG_BT_BAP_BROADCAST_SRC_SUBGROUP_COUNT) && defined(CONFIG_BT_BAP_UNICAST)) +/* 1 since CIGs doesn't have the concept of subgroups */ +#define SUBGROUP_MAX (1 + CONFIG_BT_BAP_BROADCAST_SRC_SUBGROUP_COUNT) + +#elif (defined(CONFIG_BT_BAP_BROADCAST_SRC_SUBGROUP_COUNT) && !defined(CONFIG_BT_BAP_UNICAST)) +#define SUBGROUP_MAX CONFIG_BT_BAP_BROADCAST_SRC_SUBGROUP_COUNT + +#else +/* 1 since CIGs doesn't have the concept of subgroups */ +#define SUBGROUP_MAX 1 +#endif + +/* Since we can't assume that the number of streams are equally distributed on the subgroups ,we + * need to allocate the max number per subgroup + */ +#define STREAMS_MAX (CONFIG_BT_ISO_MAX_CHAN) + +#define NET_BUF_POOL_MAX ((GROUP_MAX) * (SUBGROUP_MAX) * (STREAMS_MAX) * (HCI_ISO_BUF_PER_CHAN)) + +NET_BUF_POOL_FIXED_DEFINE(iso_tx_pool, NET_BUF_POOL_MAX, BT_ISO_SDU_BUF_SIZE(CONFIG_BT_ISO_TX_MTU), + CONFIG_BT_CONN_TX_USER_DATA_SIZE, NULL); + +struct tx_inf { + uint16_t iso_conn_handle; + struct bt_iso_tx_info iso_tx; + struct bt_iso_tx_info iso_tx_readback; + atomic_t iso_tx_pool_alloc; + bool hci_wrn_printed; +}; + +static bool initialized; +static struct tx_inf tx_info_arr[GROUP_MAX][SUBGROUP_MAX][STREAMS_MAX]; + +/** + * @brief Sends audio data over a single BAP stream. + * + * @param data Audio data to send. + * @param size Size of data. + * @param bap_stream Pointer to BAP stream to use. + * @param tx_info Pointer to tx_info struct. + * @param ts_tx Timestamp to send. Note that for some controllers, BT_ISO_TIMESTAMP_NONE + * is used. This timestamp is used to ensure that SDUs are sent in the same + * connection interval. + * @return 0 if successful, error otherwise. + */ +static int iso_stream_send(uint8_t const *const data, size_t size, struct bt_cap_stream *cap_stream, + struct tx_inf *tx_info, uint32_t ts_tx) +{ + int ret; + struct net_buf *buf; + + /* net_buf_alloc allocates buffers for APP->NET transfer over HCI RPMsg, + * but when these buffers are released it is not guaranteed that the + * data has actually been sent. The data might be queued on the NET core, + * and this can cause delays in the audio. + * When the sent callback is called the data has been sent, and we can free the buffer. + * Data will be discarded if allocation becomes too high, to avoid audio delays. + * If the NET and APP core operates in clock sync, discarding should not occur. + */ + if (atomic_get(&tx_info->iso_tx_pool_alloc) >= HCI_ISO_BUF_PER_CHAN) { + if (!tx_info->hci_wrn_printed) { + struct bt_iso_chan *iso_chan; + + iso_chan = bt_bap_stream_iso_chan_get(&cap_stream->bap_stream); + + LOG_WRN("HCI ISO TX overrun on stream %p - Single print", + (void *)&cap_stream->bap_stream); + tx_info->hci_wrn_printed = true; + } + return -ENOMEM; + } + + tx_info->hci_wrn_printed = false; + + buf = net_buf_alloc(&iso_tx_pool, K_NO_WAIT); + if (buf == NULL) { + /* This should never occur because of the iso_tx_pool_alloc check above */ + LOG_WRN("Out of TX buffers"); + return -ENOMEM; + } + + net_buf_reserve(buf, BT_ISO_CHAN_SEND_RESERVE); + net_buf_add_mem(buf, data, size); + + atomic_inc(&tx_info->iso_tx_pool_alloc); + + if (ts_tx == 0) { + ret = bt_cap_stream_send(cap_stream, buf, tx_info->iso_tx.seq_num); + } else { + ret = bt_cap_stream_send_ts(cap_stream, buf, tx_info->iso_tx.seq_num, ts_tx); + } + + if (ret < 0) { + if (ret != -ENOTCONN) { + LOG_WRN("Failed to send audio data: %d stream %p", ret, + (void *)&cap_stream->bap_stream); + } + net_buf_unref(buf); + atomic_dec(&tx_info->iso_tx_pool_alloc); + return ret; + } else { + tx_info->iso_tx.seq_num++; + } + + return 0; +} + +static int get_tx_sync_sdc(uint16_t iso_conn_handle, struct bt_iso_tx_info *info) +{ + int ret; + sdc_hci_cmd_vs_iso_read_tx_timestamp_t cmd_read_tx_timestamp; + sdc_hci_cmd_vs_iso_read_tx_timestamp_return_t rsp_params; + + cmd_read_tx_timestamp.conn_handle = iso_conn_handle; + + ret = hci_vs_sdc_iso_read_tx_timestamp(&cmd_read_tx_timestamp, &rsp_params); + if (ret) { + return ret; + } + + info->ts = rsp_params.tx_time_stamp; + info->seq_num = rsp_params.packet_sequence_number; + info->offset = 0; + + return 0; +} + +static int iso_conn_handle_set(struct bt_bap_stream *bap_stream, uint16_t *iso_conn_handle) +{ + int ret; + + if (*iso_conn_handle == HANDLE_INVALID) { + struct bt_bap_ep_info ep_info; + + ret = bt_bap_ep_get_info(bap_stream->ep, &ep_info); + if (ret) { + LOG_WRN("Unable to get info for ep"); + return -EACCES; + } + + ret = bt_hci_get_conn_handle(ep_info.iso_chan->iso, iso_conn_handle); + if (ret) { + LOG_ERR("Failed obtaining conn_handle (ret:%d)", ret); + return ret; + } + } else { + /* Already set. */ + } + + return 0; +} + +int bt_le_audio_tx_send(struct le_audio_tx_info *tx, uint8_t num_tx, + struct le_audio_encoded_audio enc_audio) +{ + int ret; + size_t data_size_pr_stream = 0; + + if (!initialized) { + return -EACCES; + } + + if (tx == NULL) { + return -EINVAL; + } + + data_size_pr_stream = enc_audio.size / enc_audio.num_ch; + + /* When sending ISO data, we always send ts = 0 to the first active transmitting channel. + * The controller will populate with a ts which is fetched using bt_iso_chan_get_tx_sync. + * This timestamp will be submitted to all the other channels in order to place data on all + * channels in the same ISO interval. + */ + + uint32_t common_tx_sync_ts_us = 0; + uint32_t curr_ts_us = 0; + bool ts_common_acquired = false; + uint32_t common_interval = 0; + + for (int i = 0; i < num_tx; i++) { + struct tx_inf *tx_info = + &tx_info_arr[tx[i].idx.lvl1][tx[i].idx.lvl2][tx[i].idx.lvl3]; + + if (tx_info->iso_tx.seq_num == 0) { + /* Temporary fix until /zephyr/pull/68745/ is available + */ +#if defined(CONFIG_BT_BAP_DEBUG_STREAM_SEQ_NUM) + tx[i].cap_stream->bap_stream._prev_seq_num = 0; +#endif /* CONFIG_BT_BAP_DEBUG_STREAM_SEQ_NUM */ + } + + if (!le_audio_ep_state_check(tx[i].cap_stream->bap_stream.ep, + BT_BAP_EP_STATE_STREAMING)) { + /* This bap_stream is not streaming*/ + continue; + } + + if (tx[i].audio_channel > enc_audio.num_ch) { + LOG_WRN("Unsupported audio_channel: %d", tx[i].audio_channel); + continue; + } + + uint32_t bitrate; + + ret = le_audio_bitrate_get(tx[i].cap_stream->bap_stream.codec_cfg, &bitrate); + if (ret) { + LOG_ERR("Failed to calculate bitrate: %d", ret); + return ret; + } + + if (data_size_pr_stream != LE_AUDIO_SDU_SIZE_OCTETS(bitrate)) { + LOG_ERR("The encoded data size does not match the SDU size"); + return -EINVAL; + } + + if (common_interval != 0 && + (common_interval != tx[i].cap_stream->bap_stream.qos->interval)) { + LOG_ERR("Not all channels have the same ISO interval"); + return -EINVAL; + } + common_interval = tx[i].cap_stream->bap_stream.qos->interval; + + /* Check if same audio is sent to all channels */ + if (enc_audio.num_ch == 1) { + ret = iso_stream_send(enc_audio.data, data_size_pr_stream, tx[i].cap_stream, + tx_info, common_tx_sync_ts_us); + } else { + ret = iso_stream_send( + &enc_audio.data[(data_size_pr_stream * tx[i].audio_channel)], + data_size_pr_stream, tx[i].cap_stream, tx_info, + common_tx_sync_ts_us); + } + + if (ret) { + /* DBG used here as prints are handled within iso_stream_send */ + LOG_DBG("Failed to send to idx: %d stream: %p, ret: %d ", i, + (void *)&tx[i].cap_stream->bap_stream, ret); + continue; + } + + ret = iso_conn_handle_set(&tx[i].cap_stream->bap_stream, &tx_info->iso_conn_handle); + if (ret) { + continue; + } + + /* Strictly, it is only required to call get_tx_sync_sdc on the first streaming + * channel to get the timestamp which is sent to all other channels. + * However, to be able to detect errors, this is called on each TX. + */ + ret = get_tx_sync_sdc(tx_info->iso_conn_handle, &tx_info->iso_tx_readback); + if (ret) { + if (ret != -ENOTCONN) { + LOG_WRN("Unable to get tx sync. ret: %d stream: %p", ret, + (void *)&tx[i].cap_stream->bap_stream); + } + continue; + } + + if (!ts_common_acquired) { + curr_ts_us = audio_sync_timer_capture(); + common_tx_sync_ts_us = tx_info->iso_tx_readback.ts; + ts_common_acquired = true; + } + } + + if (ts_common_acquired) { + struct sdu_ref_msg msg; + + msg.tx_sync_ts_us = common_tx_sync_ts_us; + msg.curr_ts_us = curr_ts_us; + msg.adjust = true; + + ret = zbus_chan_pub(&sdu_ref_chan, &msg, K_NO_WAIT); + if (ret) { + LOG_WRN("Failed to publish timestamp: %d", ret); + } + } + + return 0; +} + +int bt_le_audio_tx_stream_started(struct stream_index idx) +{ + if (!initialized) { + return -EACCES; + } + + atomic_clear(&tx_info_arr[idx.lvl1][idx.lvl2][idx.lvl3].iso_tx_pool_alloc); + + tx_info_arr[idx.lvl1][idx.lvl2][idx.lvl3].hci_wrn_printed = false; + tx_info_arr[idx.lvl1][idx.lvl2][idx.lvl3].iso_conn_handle = HANDLE_INVALID; + tx_info_arr[idx.lvl1][idx.lvl2][idx.lvl3].iso_tx.seq_num = 0; + tx_info_arr[idx.lvl1][idx.lvl2][idx.lvl3].iso_tx_readback.seq_num = 0; + return 0; +} + +int bt_le_audio_tx_stream_sent(struct stream_index stream_idx) +{ + if (!initialized) { + return -EACCES; + } + + atomic_dec( + &tx_info_arr[stream_idx.lvl1][stream_idx.lvl2][stream_idx.lvl3].iso_tx_pool_alloc); + return 0; +} + +void bt_le_audio_tx_init(void) +{ + if (initialized) { + /* If TX is disabled and enabled again this should be called to reset the state */ + LOG_DBG("Already initialized"); + } + + for (int i = 0; i < GROUP_MAX; i++) { + for (int j = 0; j < SUBGROUP_MAX; j++) { + for (int k = 0; k < STREAMS_MAX; k++) { + tx_info_arr[i][j][k].hci_wrn_printed = false; + tx_info_arr[i][j][k].iso_conn_handle = HANDLE_INVALID; + tx_info_arr[i][j][k].iso_tx.ts = 0; + tx_info_arr[i][j][k].iso_tx.offset = 0; + tx_info_arr[i][j][k].iso_tx.seq_num = 0; + tx_info_arr[i][j][k].iso_tx_readback.ts = 0; + tx_info_arr[i][j][k].iso_tx_readback.offset = 0; + tx_info_arr[i][j][k].iso_tx_readback.seq_num = 0; + } + } + } + + initialized = true; +} diff --git a/src/bluetooth/bt_stream/bt_le_audio_tx/bt_le_audio_tx.h b/src/bluetooth/bt_stream/bt_le_audio_tx/bt_le_audio_tx.h new file mode 100644 index 0000000..51de67a --- /dev/null +++ b/src/bluetooth/bt_stream/bt_le_audio_tx/bt_le_audio_tx.h @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _LE_AUDIO_TX_H_ +#define _LE_AUDIO_TX_H_ + +#include +#include +#include + +#include "le_audio.h" + +struct le_audio_tx_info { + struct stream_index idx; + struct bt_cap_stream *cap_stream; + uint8_t audio_channel; +}; + +/** + * @brief Allocates buffers and sends data to the controller. + * + * @note Send all available channels in a single call. + * Do not call this for each channel. + * + * @param[in] tx Pointer to an array of le_audio_tx_info elements. + * @param[in] num_tx Number of elements in @p tx. + * @param[in] enc_audio Encoded audio data. + * + * @return 0 if successful, error otherwise. + */ +int bt_le_audio_tx_send(struct le_audio_tx_info *tx, uint8_t num_tx, + struct le_audio_encoded_audio enc_audio); + +/** + * @brief Initializes a stream. Must be called when a TX stream is started. + * + * @param[in] stream_idx Stream index. + * + * @retval -EACCES The module has not been initialized. + * @retval 0 Success. + */ +int bt_le_audio_tx_stream_started(struct stream_index stream_idx); + +/** + * @brief Frees a TX buffer. Must be called when a TX stream has been sent. + * + * @param[in] stream_idx Stream index. + * + * @retval -EACCES The module has not been initialized. + * @retval 0 Success. + */ +int bt_le_audio_tx_stream_sent(struct stream_index stream_idx); + +/** + * @brief Initializes the TX path for ISO transmission. + */ +void bt_le_audio_tx_init(void); + +#endif /* _LE_AUDIO_TX_H_ */ diff --git a/src/bluetooth/bt_stream/le_audio.c b/src/bluetooth/bt_stream/le_audio.c new file mode 100644 index 0000000..50a8d5e --- /dev/null +++ b/src/bluetooth/bt_stream/le_audio.c @@ -0,0 +1,318 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "le_audio.h" + +#include +#include + +#include +LOG_MODULE_REGISTER(le_audio, CONFIG_BLE_LOG_LEVEL); + +int le_audio_ep_state_get(struct bt_bap_ep *ep, uint8_t *state) +{ + int ret; + struct bt_bap_ep_info ep_info; + + if (ep == NULL) { + /* If an endpoint is NULL it is not in any of the states */ + return -EINVAL; + } + + ret = bt_bap_ep_get_info(ep, &ep_info); + if (ret) { + LOG_WRN("Unable to get info for ep"); + return ret; + } + + *state = ep_info.state; + + return 0; +} + +/*TODO: Create helper function in host to perform this action. */ +bool le_audio_ep_state_check(struct bt_bap_ep *ep, enum bt_bap_ep_state state) +{ + int ret; + uint8_t ep_state; + + ret = le_audio_ep_state_get(ep, &ep_state); + if (ret) { + return false; + } + + if (ep_state == state) { + return true; + } + + return false; +} + +bool le_audio_ep_qos_configured(struct bt_bap_ep const *const ep) +{ + int ret; + struct bt_bap_ep_info ep_info; + + if (ep == NULL) { + LOG_DBG("EP is NULL"); + /* If an endpoint is NULL it is not in any of the states */ + return false; + } + + ret = bt_bap_ep_get_info(ep, &ep_info); + if (ret) { + LOG_WRN("Unable to get info for ep"); + return false; + } + + if (ep_info.state == BT_BAP_EP_STATE_QOS_CONFIGURED || + ep_info.state == BT_BAP_EP_STATE_ENABLING || + ep_info.state == BT_BAP_EP_STATE_STREAMING || + ep_info.state == BT_BAP_EP_STATE_DISABLING) { + return true; + } + + return false; +} + +int le_audio_freq_hz_get(const struct bt_audio_codec_cfg *codec, int *freq_hz) +{ + int ret; + + ret = bt_audio_codec_cfg_get_freq(codec); + if (ret < 0) { + return ret; + } + + ret = bt_audio_codec_cfg_freq_to_freq_hz(ret); + if (ret < 0) { + return ret; + } + + *freq_hz = ret; + + return 0; +} + +int le_audio_duration_us_get(const struct bt_audio_codec_cfg *codec, int *frame_dur_us) +{ + int ret; + + ret = bt_audio_codec_cfg_get_frame_dur(codec); + if (ret < 0) { + return ret; + } + + ret = bt_audio_codec_cfg_frame_dur_to_frame_dur_us(ret); + if (ret < 0) { + return ret; + } + + *frame_dur_us = ret; + + return 0; +} + +int le_audio_octets_per_frame_get(const struct bt_audio_codec_cfg *codec, uint32_t *octets_per_sdu) +{ + int ret; + + ret = bt_audio_codec_cfg_get_octets_per_frame(codec); + if (ret < 0) { + return ret; + } + + *octets_per_sdu = ret; + + return 0; +} + +int le_audio_frame_blocks_per_sdu_get(const struct bt_audio_codec_cfg *codec, + uint32_t *frame_blks_per_sdu) +{ + int ret; + + ret = bt_audio_codec_cfg_get_frame_blocks_per_sdu(codec, true); + if (ret < 0) { + return ret; + } + + *frame_blks_per_sdu = ret; + + return 0; +} + +int le_audio_bitrate_get(const struct bt_audio_codec_cfg *const codec, uint32_t *bitrate) +{ + int ret; + int dur_us; + + ret = le_audio_duration_us_get(codec, &dur_us); + if (ret) { + return ret; + } + + int frames_per_sec = 1000000 / dur_us; + int octets_per_sdu; + + ret = le_audio_octets_per_frame_get(codec, &octets_per_sdu); + if (ret) { + return ret; + } + + *bitrate = frames_per_sec * (octets_per_sdu * 8); + + return 0; +} + +int le_audio_stream_dir_get(struct bt_bap_stream const *const stream) +{ + int ret; + struct bt_bap_ep_info ep_info; + + ret = bt_bap_ep_get_info(stream->ep, &ep_info); + + if (ret) { + LOG_WRN("Failed to get ep_info"); + return ret; + } + + return ep_info.dir; +} + +bool le_audio_bitrate_check(const struct bt_audio_codec_cfg *codec) +{ + int ret; + uint32_t octets_per_sdu; + + ret = le_audio_octets_per_frame_get(codec, &octets_per_sdu); + if (ret) { + LOG_ERR("Error retrieving octets per frame: %d", ret); + return false; + } + + if (octets_per_sdu < LE_AUDIO_SDU_SIZE_OCTETS(CONFIG_LC3_BITRATE_MIN)) { + LOG_WRN("Bitrate too low"); + return false; + } else if (octets_per_sdu > LE_AUDIO_SDU_SIZE_OCTETS(CONFIG_LC3_BITRATE_MAX)) { + LOG_WRN("Bitrate too high"); + return false; + } + + return true; +} + +bool le_audio_freq_check(const struct bt_audio_codec_cfg *codec) +{ + int ret; + uint32_t frequency_hz; + + ret = le_audio_freq_hz_get(codec, &frequency_hz); + if (ret) { + LOG_ERR("Error retrieving sampling rate: %d", ret); + return false; + } + + switch (frequency_hz) { + case 8000U: + return (BT_AUDIO_CODEC_CAP_FREQ_8KHZ & (BT_AUDIO_CODEC_CAPABILIY_FREQ)); + case 11025U: + return (BT_AUDIO_CODEC_CAP_FREQ_11KHZ & (BT_AUDIO_CODEC_CAPABILIY_FREQ)); + case 16000U: + return (BT_AUDIO_CODEC_CAP_FREQ_16KHZ & (BT_AUDIO_CODEC_CAPABILIY_FREQ)); + case 22050U: + return (BT_AUDIO_CODEC_CAP_FREQ_22KHZ & (BT_AUDIO_CODEC_CAPABILIY_FREQ)); + case 24000U: + return (BT_AUDIO_CODEC_CAP_FREQ_24KHZ & (BT_AUDIO_CODEC_CAPABILIY_FREQ)); + case 32000U: + return (BT_AUDIO_CODEC_CAP_FREQ_32KHZ & (BT_AUDIO_CODEC_CAPABILIY_FREQ)); + case 44100U: + return (BT_AUDIO_CODEC_CAP_FREQ_44KHZ & (BT_AUDIO_CODEC_CAPABILIY_FREQ)); + case 48000U: + return (BT_AUDIO_CODEC_CAP_FREQ_48KHZ & (BT_AUDIO_CODEC_CAPABILIY_FREQ)); + case 88200U: + return (BT_AUDIO_CODEC_CAP_FREQ_88KHZ & (BT_AUDIO_CODEC_CAPABILIY_FREQ)); + case 96000U: + return (BT_AUDIO_CODEC_CAP_FREQ_96KHZ & (BT_AUDIO_CODEC_CAPABILIY_FREQ)); + case 176400U: + return (BT_AUDIO_CODEC_CAP_FREQ_176KHZ & (BT_AUDIO_CODEC_CAPABILIY_FREQ)); + case 192000U: + return (BT_AUDIO_CODEC_CAP_FREQ_192KHZ & (BT_AUDIO_CODEC_CAPABILIY_FREQ)); + case 384000U: + return (BT_AUDIO_CODEC_CAP_FREQ_384KHZ & (BT_AUDIO_CODEC_CAPABILIY_FREQ)); + default: + return false; + } +} + +void le_audio_print_codec(const struct bt_audio_codec_cfg *codec, enum bt_audio_dir dir) +{ + if (codec->id == BT_HCI_CODING_FORMAT_LC3) { + /* LC3 uses the generic LTV format - other codecs might do as well */ + int ret; + enum bt_audio_location chan_allocation; + int freq_hz; + int dur_us; + uint32_t octets_per_sdu; + int frame_blks_per_sdu; + uint32_t bitrate; + + ret = le_audio_freq_hz_get(codec, &freq_hz); + if (ret) { + LOG_ERR("Error retrieving sampling frequency: %d", ret); + return; + } + + ret = le_audio_duration_us_get(codec, &dur_us); + if (ret) { + LOG_ERR("Error retrieving frame duration: %d", ret); + return; + } + + ret = le_audio_octets_per_frame_get(codec, &octets_per_sdu); + if (ret) { + LOG_ERR("Error retrieving octets per frame: %d", ret); + return; + } + + ret = le_audio_frame_blocks_per_sdu_get(codec, &frame_blks_per_sdu); + if (ret) { + LOG_ERR("Error retrieving frame blocks per SDU: %d", ret); + return; + } + + ret = bt_audio_codec_cfg_get_chan_allocation(codec, &chan_allocation, false); + if (ret == -ENODATA) { + /* Codec channel allocation not set, defaulting to 0 */ + chan_allocation = 0; + } else if (ret) { + LOG_ERR("Error retrieving channel allocation: %d", ret); + return; + } + + ret = le_audio_bitrate_get(codec, &bitrate); + if (ret) { + LOG_ERR("Unable to calculate bitrate: %d", ret); + return; + } + + if (dir == BT_AUDIO_DIR_SINK) { + LOG_INF("LC3 codec config for sink:"); + } else if (dir == BT_AUDIO_DIR_SOURCE) { + LOG_INF("LC3 codec config for source:"); + } else { + LOG_INF("LC3 codec config for :"); + } + + LOG_INF("\tFrequency: %d Hz", freq_hz); + LOG_INF("\tDuration: %d us", dur_us); + LOG_INF("\tChannel allocation: 0x%x", chan_allocation); + LOG_INF("\tOctets per frame: %d (%d bps)", octets_per_sdu, bitrate); + LOG_INF("\tFrames per SDU: %d", frame_blks_per_sdu); + } else { + LOG_WRN("Codec is not LC3, codec_id: 0x%2x", codec->id); + } +} diff --git a/src/bluetooth/bt_stream/le_audio.h b/src/bluetooth/bt_stream/le_audio.h new file mode 100644 index 0000000..d578d5b --- /dev/null +++ b/src/bluetooth/bt_stream/le_audio.h @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _LE_AUDIO_H_ +#define _LE_AUDIO_H_ + +#include +#include + +#define LE_AUDIO_ZBUS_EVENT_WAIT_TIME K_MSEC(5) +#define LE_AUDIO_SDU_SIZE_OCTETS(bitrate) (bitrate / (1000000 / CONFIG_AUDIO_FRAME_DURATION_US) / 8) + +#if CONFIG_SAMPLE_RATE_CONVERTER && CONFIG_AUDIO_SAMPLE_RATE_48000_HZ +#define BT_AUDIO_CODEC_CAPABILIY_FREQ \ + BT_AUDIO_CODEC_CAP_FREQ_48KHZ | BT_AUDIO_CODEC_CAP_FREQ_24KHZ | \ + BT_AUDIO_CODEC_CAP_FREQ_16KHZ + +#elif CONFIG_AUDIO_SAMPLE_RATE_16000_HZ +#define BT_AUDIO_CODEC_CAPABILIY_FREQ BT_AUDIO_CODEC_CAP_FREQ_16KHZ + +#elif CONFIG_AUDIO_SAMPLE_RATE_24000_HZ +#define BT_AUDIO_CODEC_CAPABILIY_FREQ BT_AUDIO_CODEC_CAP_FREQ_24KHZ + +#elif CONFIG_AUDIO_SAMPLE_RATE_48000_HZ +#define BT_AUDIO_CODEC_CAPABILIY_FREQ BT_AUDIO_CODEC_CAP_FREQ_48KHZ + +#else +#error No sample rate supported +#endif /* CONFIG_SAMPLE_RATE_CONVERTER */ + +#define BT_BAP_LC3_PRESET_CONFIGURABLE(_loc, _stream_context, _bitrate) \ + BT_BAP_LC3_PRESET(BT_AUDIO_CODEC_LC3_CONFIG(CONFIG_BT_AUDIO_PREF_SAMPLE_RATE_VALUE, \ + BT_AUDIO_CODEC_CFG_DURATION_10, _loc, \ + LE_AUDIO_SDU_SIZE_OCTETS(_bitrate), 1, \ + _stream_context), \ + BT_BAP_QOS_CFG_UNFRAMED(10000u, LE_AUDIO_SDU_SIZE_OCTETS(_bitrate), \ + CONFIG_BT_AUDIO_RETRANSMITS, \ + CONFIG_BT_AUDIO_MAX_TRANSPORT_LATENCY_MS, \ + CONFIG_BT_AUDIO_PRESENTATION_DELAY_US)) + +/** + * @brief Callback for receiving Bluetooth LE Audio data. + * + * @param data Pointer to received data. + * @param size Size of received data. + * @param bad_frame Indicating if the frame is a bad frame or not. + * @param sdu_ref ISO timestamp. + * @param channel_index Audio channel index. + * @param desired_size The expected data size. + */ +typedef void (*le_audio_receive_cb)(const uint8_t *const data, size_t size, bool bad_frame, + uint32_t sdu_ref, enum audio_channel channel_index, + size_t desired_size); + +/** + * @brief Encoded audio data and information. + * + * @note Container for SW codec (typically LC3) compressed audio data. + */ +struct le_audio_encoded_audio { + uint8_t const *const data; + size_t size; + uint8_t num_ch; +}; + +struct stream_index { + uint8_t lvl1; /* BIG / CIG */ + uint8_t lvl2; /* Subgroups (only applicable to Broadcast) */ + uint8_t lvl3; /* BIS / CIS */ +}; + +/** + * @brief Get the current state of an endpoint. + * + * @param[in] ep The endpoint to check. + * @param[out] state The state of the endpoint. + * + * @return 0 for success, error otherwise. + */ +int le_audio_ep_state_get(struct bt_bap_ep *ep, uint8_t *state); + +/** + * @brief Check if an endpoint is in the given state. + * If the endpoint is NULL, it is not in the + * given state, and this function returns false. + * + * @param[in] ep The endpoint to check. + * @param[in] state The state to check for. + * + * @retval true The endpoint is in the given state. + * @retval false Otherwise. + */ +bool le_audio_ep_state_check(struct bt_bap_ep *ep, enum bt_bap_ep_state state); + +/** + * @brief Check if an endpoint has had the QoS configured. + * If the endpoint is NULL, it is not in the state, and this function returns false. + * + * @param[in] ep The endpoint to check. + * + * @retval true The endpoint QoS is configured. + * @retval false Otherwise. + */ +bool le_audio_ep_qos_configured(struct bt_bap_ep const *const ep); + +/** + * @brief Decode the audio sampling frequency in the codec configuration. + * + * @param[in] codec Pointer to the audio codec structure. + * @param[out] freq_hz Pointer to the sampling frequency in Hz. + * + * @return 0 for success, error otherwise. + */ +int le_audio_freq_hz_get(const struct bt_audio_codec_cfg *codec, int *freq_hz); + +/** + * @brief Decode the audio frame duration in us in the codec configuration. + * + * @param[in] codec Pointer to the audio codec structure. + * @param[out] frame_dur_us Pointer to the frame duration in us. + * + * @return 0 for success, error otherwise. + */ +int le_audio_duration_us_get(const struct bt_audio_codec_cfg *codec, int *frame_dur_us); + +/** + * @brief Decode the number of octets per frame in the codec configuration. + * + * @param[in] codec Pointer to the audio codec structure. + * @param[out] octets_per_sdu Pointer to the number of octets per SDU. + * + * @return 0 for success, error otherwise. + */ +int le_audio_octets_per_frame_get(const struct bt_audio_codec_cfg *codec, uint32_t *octets_per_sdu); + +/** + * @brief Decode the number of frame blocks per SDU in the codec configuration. + * + * @param[in] codec Pointer to the audio codec structure. + * @param[out] frame_blks_per_sdu Pointer to the number of frame blocks per SDU. + * + * @return 0 for success, error otherwise. + */ +int le_audio_frame_blocks_per_sdu_get(const struct bt_audio_codec_cfg *codec, + uint32_t *frame_blks_per_sdu); + +/** + * @brief Get the bitrate for the codec configuration. + * + * @details Decodes the audio frame duration and the number of octets per fram from the codec + * configuration, and calculates the bitrate. + * + * @param[in] codec Pointer to the audio codec structure. + * @param[out] bitrate Pointer to the bitrate in bps. + */ +int le_audio_bitrate_get(const struct bt_audio_codec_cfg *const codec, uint32_t *bitrate); + +/** + * @brief Get the direction of the @p stream provided. + * + * @param[in] stream Stream to check direction for. + * + * @retval BT_AUDIO_DIR_SINK sink direction. + * @retval BT_AUDIO_DIR_SOURCE source direction. + * @retval Negative value Failed to get ep_info from host. + * + */ +int le_audio_stream_dir_get(struct bt_bap_stream const *const stream); + +/** + * @brief Check that the bitrate is within the supported range. + * + * @param[in] codec The audio codec structure. + * + * retval true The bitrate is in the supported range. + * retval false Otherwise. + */ +bool le_audio_bitrate_check(const struct bt_audio_codec_cfg *codec); + +/** + * @brief Check that the sample rate is supported. + * + * @param[in] codec The audio codec structure. + * + * retval true The sample rate is supported. + * retval false Otherwise. + */ +bool le_audio_freq_check(const struct bt_audio_codec_cfg *codec); + +/** + * @brief Print the codec configuration + * + * @param[in] codec Pointer to the audio codec structure. + * @param[in] dir Direction to print the codec configuration for. + */ +void le_audio_print_codec(const struct bt_audio_codec_cfg *codec, enum bt_audio_dir dir); + +#endif /* _LE_AUDIO_H_ */ diff --git a/src/bluetooth/bt_stream/unicast/Kconfig b/src/bluetooth/bt_stream/unicast/Kconfig new file mode 100644 index 0000000..4500db6 --- /dev/null +++ b/src/bluetooth/bt_stream/unicast/Kconfig @@ -0,0 +1,161 @@ +# +# Copyright (c) 2023 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +rsource "Kconfig.defaults" + +menu "Unicast" + +choice BT_BAP_UNICAST_BAP_CONFIGURATION + prompt "Unicast codec configuration" + depends on TRANSPORT_CIS + default BT_BAP_UNICAST_CONFIGURABLE + help + Select the unicast codec configuration as given in + Table 5.2 of the Bluetooth Audio Profile Specification. + USB only supports 48 kHz samplig rate. + +config BT_BAP_UNICAST_CONFIGURABLE + bool "Configurable unicast settings" + depends on TRANSPORT_CIS + help + Configurable option that doesn't follow any preset. Allows for more flexibility. + +config BT_BAP_UNICAST_16_2_1 + bool "16_2_1" + depends on TRANSPORT_CIS + help + Unicast mandatory codec capability 16_2_1. + 16kHz, 32kbps, 2 retransmits, 10ms transport latency, and 40ms presentation delay. + +config BT_BAP_UNICAST_24_2_1 + bool "24_2_1" + depends on TRANSPORT_CIS + help + Unicast codec capability 24_2_1. + 24kHz, 48kbps, 2 retransmits, 10ms transport latency, and 40ms presentation delay. + +config BT_BAP_UNICAST_48_4_1 + bool "48_4_1" + depends on TRANSPORT_CIS + help + Unicast codec capability 48_4_1. + 48kHz, 96kbps, 5 retransmits, 20ms transport latency, and 40ms presentation delay. +endchoice + +choice BT_AUDIO_PRES_DLY_SRCH + prompt "Default search mode for the presentation delay" + default BT_AUDIO_PRES_DELAY_SRCH_PREF_MIN + help + Set the default search mode for the presentation delay. + +config BT_AUDIO_PRES_DELAY_SRCH_MIN + bool "Largest minimum delay over all audio receivers" + help + Search for the largest minimum delay over all audio receivers. + +config BT_AUDIO_PRES_DELAY_SRCH_MAX + bool "Smallest maximum delay over all audio receivers" + help + Search for the smallest maximum delay over all audio receivers. + +config BT_AUDIO_PRES_DELAY_SRCH_PREF_MIN + bool "Largest minimum preferred delay over all audio receivers" + help + Search for the largest minimum preferred delay over all audio receivers. + +config BT_AUDIO_PRES_DELAY_SRCH_PREF_MAX + bool "Smallest maximum preferred delay over all audio receivers" + help + Search for the smallest maximum preferred delay over all audio receivers. + +config BT_AUDIO_PRES_DELAY_SRCH_SOURCE + bool "Use the presentation delay of audio source or client" + help + Use the presentation delay defined by the broadcast_source or unicast_client if it + is within the range set by AUDIO_MIN_PRES_DLY_US and AUDIO_MAX_PRES_DLY_US. This will + override the audio receiver presentation delay as long as it is in range of + the max and min supported by the audio receivers. If it is outside this range, + then it will revert to the closest supported value. +endchoice + +config BT_AUDIO_EP_PRINT + bool "Print discovered endpoint capabilities" + default n + help + Print the supported capabilities of an endpoint when it is discovered. + + +config CODEC_CAP_COUNT_MAX + int "Max storage of codec capabilities" + default 5 + help + Max number of codec capabilities to store per stream. + +config BT_AUDIO_PREFERRED_MIN_PRES_DLY_US + int "The preferred minimum presentation delay" + range AUDIO_MIN_PRES_DLY_US AUDIO_MAX_PRES_DLY_US + default AUDIO_MIN_PRES_DLY_US + help + The preferred minimum presentation delay in microseconds. This can not + be less than the absolute minimum presentation delay. + +config BT_AUDIO_PREFERRED_MAX_PRES_DLY_US + int "The preferred maximum presentation delay" + range BT_AUDIO_PREFERRED_MIN_PRES_DLY_US AUDIO_MAX_PRES_DLY_US + default 40000 + help + The preferred maximum presentation delay in microseconds. This can not + be less than the absolute maximum presentation delay. + +config BT_AUDIO_BITRATE_UNICAST_SINK + int "ISO stream bitrate" + depends on TRANSPORT_CIS + default 64000 if BT_BAP_UNICAST_CONFIGURABLE && STREAM_BIDIRECTIONAL + default 96000 if BT_BAP_UNICAST_CONFIGURABLE + default 32000 if BT_BAP_UNICAST_16_2_1 + default 48000 if BT_BAP_UNICAST_24_2_1 + help + Bitrate for the unicast sink ISO stream. + +config BT_AUDIO_BITRATE_UNICAST_SRC + int "ISO stream bitrate" + depends on TRANSPORT_CIS + default 32000 if BT_BAP_UNICAST_16_2_1 + default 48000 if BT_BAP_UNICAST_24_2_1 + default 64000 + help + Bitrate for the unicast source ISO stream. + +config BT_SET_IDENTITY_RESOLVING_KEY_DEFAULT + string + default "NRF5340_TWS_DEMO" + help + Default string to configure the Set Identify Resolving Key (SIRK), must + be changed before production uniquely for each coordinated set. + +config BT_SET_IDENTITY_RESOLVING_KEY + string "String used to configure the SIRK" + depends on TRANSPORT_CIS && BT_BAP_UNICAST_SERVER + default BT_SET_IDENTITY_RESOLVING_KEY_DEFAULT + help + Defines a string to configure the Set Identify Resolving Key (SIRK), must + be changed before production uniquely for each coordinated set. The SIRK + must be 16 characters (16 bytes). + + +#----------------------------------------------------------------------------# +menu "Log levels" + +module = UNICAST_SERVER +module-str = unicast_server +source "subsys/logging/Kconfig.template.log_config" + +module = UNICAST_CLIENT +module-str = unicast_client +source "subsys/logging/Kconfig.template.log_config" + +endmenu # Log levels +endmenu # Unicast diff --git a/src/bluetooth/bt_stream/unicast/Kconfig.defaults b/src/bluetooth/bt_stream/unicast/Kconfig.defaults new file mode 100644 index 0000000..fd60726 --- /dev/null +++ b/src/bluetooth/bt_stream/unicast/Kconfig.defaults @@ -0,0 +1,57 @@ +# +# Copyright (c) 2023 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +config BT_GATT_CLIENT + default y + +config BT_BONDABLE + default y + +config BT_PRIVACY + default y + +config BT_FILTER_ACCEPT_LIST + default y + +config BT_SMP + default y + +config BT_GAP_AUTO_UPDATE_CONN_PARAMS + default n + +config BT_AUTO_PHY_UPDATE + default n + +config BT_AUTO_DATA_LEN_UPDATE + default n + +config BT_L2CAP_TX_BUF_COUNT + default 12 + +config BT_BUF_ACL_RX_SIZE + default 502 if (AUDIO_DFU > 0) + default 259 + +config BT_BUF_ACL_TX_COUNT + default 12 + +config SETTINGS + default y + +config BT_SETTINGS + default y + +config FLASH + default y + +config FLASH_MAP + default y + +config NVS + default y + +config NVS_LOG_LEVEL + default 2 diff --git a/src/bluetooth/bt_stream/unicast/unicast_client.c b/src/bluetooth/bt_stream/unicast/unicast_client.c new file mode 100644 index 0000000..e98330a --- /dev/null +++ b/src/bluetooth/bt_stream/unicast/unicast_client.c @@ -0,0 +1,1827 @@ +/* + * Copyright (c) 2022 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "unicast_client.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include <../subsys/bluetooth/audio/bap_iso.h> + +/* TODO: Remove when a qos_pref_get function has been added in host */ +/* https://github.com/zephyrproject-rtos/zephyr/issues/72359 */ +#include <../subsys/bluetooth/audio/bap_endpoint.h> + +#include "macros_common.h" +#include "zbus_common.h" +#include "bt_le_audio_tx.h" +#include "le_audio.h" + +#include +LOG_MODULE_REGISTER(unicast_client, CONFIG_UNICAST_CLIENT_LOG_LEVEL); + +ZBUS_CHAN_DEFINE(le_audio_chan, struct le_audio_msg, NULL, NULL, ZBUS_OBSERVERS_EMPTY, + ZBUS_MSG_INIT(0)); + +#define CAP_PROCED_MUTEX_WAIT_TIME_MS K_MSEC(500) + +struct le_audio_unicast_server { + char *ch_name; + struct bt_conn *device_conn; + enum bt_audio_location location; + bool qos_reconfigure; + uint32_t reconfigure_pd; + /* Sink variables */ + bool waiting_for_sink_disc; + struct bt_audio_codec_cap sink_codec_cap[CONFIG_CODEC_CAP_COUNT_MAX]; + uint8_t num_sink_eps; + struct bt_bap_ep *sink_ep; + struct bt_cap_stream cap_sink_stream; + /* Source variables */ + bool waiting_for_source_disc; + struct bt_audio_codec_cap source_codec_cap[CONFIG_CODEC_CAP_COUNT_MAX]; + uint8_t num_source_eps; + struct bt_bap_ep *source_ep; + struct bt_cap_stream cap_source_stream; + const struct bt_csip_set_coordinator_set_member *member; +}; + +struct discover_dir { + struct bt_conn *conn; + bool sink; + bool source; +}; + +struct temp_cap_storage { + struct bt_conn *conn; + uint8_t num_caps; + /* Must be the same size as sink_codec_cap and source_codec_cap */ + struct bt_audio_codec_cap codec[CONFIG_CODEC_CAP_COUNT_MAX]; +}; + +/* Since there is no subgroups for CIG we will use 1 as a hard coded value */ +static struct le_audio_unicast_server unicast_servers[CONFIG_BT_ISO_MAX_CIG][1][CONFIG_BT_MAX_CONN]; + +K_MSGQ_DEFINE(cap_start_msgq, sizeof(struct stream_index), CONFIG_BT_ISO_MAX_CHAN, + sizeof(uint32_t)); + +static struct temp_cap_storage temp_cap[CONFIG_BT_ISO_MAX_CHAN]; + +/* Make sure that we have at least one unicast_server per CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK */ +BUILD_ASSERT(ARRAY_SIZE(unicast_servers[0][0]) >= CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT, + "We need to have at least one unicast_server per ASE SINK"); + +/* Make sure that we have at least one unicast_server per CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC */ +BUILD_ASSERT(ARRAY_SIZE(unicast_servers[0][0]) >= CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT, + "We need to have at least one unicast_server per ASE SOURCE"); + +BUILD_ASSERT(CONFIG_BT_ISO_MAX_CIG == 1, "Only one CIG is supported"); + +static le_audio_receive_cb receive_cb; + +static struct bt_bap_unicast_group *unicast_group; +static bool unicast_group_created; + +static struct bt_bap_lc3_preset lc3_preset_sink = BT_BAP_LC3_UNICAST_PRESET_NRF5340_AUDIO_SINK; +static struct bt_bap_lc3_preset lc3_preset_sink_48_4_1 = BT_BAP_LC3_UNICAST_PRESET_48_4_1( + BT_AUDIO_LOCATION_ANY, (BT_AUDIO_CONTEXT_TYPE_UNSPECIFIED)); +static struct bt_bap_lc3_preset lc3_preset_sink_24_2_1 = BT_BAP_LC3_UNICAST_PRESET_24_2_1( + BT_AUDIO_LOCATION_ANY, (BT_AUDIO_CONTEXT_TYPE_UNSPECIFIED)); +static struct bt_bap_lc3_preset lc3_preset_sink_16_2_1 = BT_BAP_LC3_UNICAST_PRESET_16_2_1( + BT_AUDIO_LOCATION_ANY, (BT_AUDIO_CONTEXT_TYPE_UNSPECIFIED)); + +static struct bt_bap_lc3_preset lc3_preset_source = BT_BAP_LC3_UNICAST_PRESET_NRF5340_AUDIO_SOURCE; +static struct bt_bap_lc3_preset lc3_preset_source_48_4_1 = + BT_BAP_LC3_UNICAST_PRESET_48_4_1(BT_AUDIO_LOCATION_ANY, BT_AUDIO_CONTEXT_TYPE_UNSPECIFIED); +static struct bt_bap_lc3_preset lc3_preset_source_24_2_1 = + BT_BAP_LC3_UNICAST_PRESET_24_2_1(BT_AUDIO_LOCATION_ANY, BT_AUDIO_CONTEXT_TYPE_UNSPECIFIED); +static struct bt_bap_lc3_preset lc3_preset_source_16_2_1 = + BT_BAP_LC3_UNICAST_PRESET_16_2_1(BT_AUDIO_LOCATION_ANY, BT_AUDIO_CONTEXT_TYPE_UNSPECIFIED); + +static bool playing_state = true; + +static void le_audio_event_publish(enum le_audio_evt_type event, struct bt_conn *conn, + enum bt_audio_dir dir) +{ + int ret; + struct le_audio_msg msg; + + msg.event = event; + msg.conn = conn; + msg.dir = dir; + + ret = zbus_chan_pub(&le_audio_chan, &msg, LE_AUDIO_ZBUS_EVENT_WAIT_TIME); + ERR_CHK(ret); +} + +K_MUTEX_DEFINE(mtx_cap_procedure_proceed); + +static void cap_start_worker(struct k_work *work) +{ + int ret; + struct stream_index idx; + int device_iterator = 0; + int stream_iterator = 0; + + /* Check msgq for a pending start procedure */ + ret = k_msgq_get(&cap_start_msgq, &idx, K_NO_WAIT); + if (ret) { + LOG_ERR("Failed to get device index for pending cap start procedure: %d", ret); + return; + } + + struct le_audio_unicast_server *unicast_server = + &unicast_servers[idx.lvl1][idx.lvl2][idx.lvl3]; + + if (unicast_server->sink_ep == NULL && unicast_server->source_ep == NULL) { + LOG_ERR("No sink or source endpoint found for device"); + return; + } + + if (unicast_group_created == false) { + uint8_t cig_index = idx.lvl1; + struct bt_bap_unicast_group_stream_pair_param + pair_params[ARRAY_SIZE(unicast_servers[cig_index][0])]; + /* 2 streams (one sink and one source stream) for each unicast_server */ + struct bt_bap_unicast_group_stream_param + group_stream_params[(ARRAY_SIZE(unicast_servers[cig_index][0]) * 2)]; + struct bt_bap_unicast_group_param group_param; + + for (int i = 0; i < ARRAY_SIZE(group_stream_params); i++) { + /* Every other stream should be sink or source */ + if ((i % 2) == 0) { + group_stream_params[i].qos = &lc3_preset_sink.qos; + group_stream_params[i].stream = + &unicast_servers[cig_index][0][device_iterator] + .cap_sink_stream.bap_stream; + } else { + group_stream_params[i].qos = &lc3_preset_source.qos; + group_stream_params[i].stream = + &unicast_servers[cig_index][0][device_iterator] + .cap_source_stream.bap_stream; + device_iterator++; + } + } + + for (int i = 0; i < ARRAY_SIZE(pair_params); i++) { + if (unicast_server->sink_ep) { + pair_params[i].tx_param = &group_stream_params[stream_iterator]; + } else { + pair_params[i].tx_param = NULL; + } + stream_iterator++; + + if (unicast_server->source_ep) { + pair_params[i].rx_param = &group_stream_params[stream_iterator]; + } else { + pair_params[i].rx_param = NULL; + } + + stream_iterator++; + } + + group_param.params = pair_params; + group_param.params_count = ARRAY_SIZE(pair_params); + + if (IS_ENABLED(CONFIG_BT_AUDIO_PACKING_INTERLEAVED)) { + group_param.packing = BT_ISO_PACKING_INTERLEAVED; + } else { + group_param.packing = BT_ISO_PACKING_SEQUENTIAL; + } + + ret = bt_bap_unicast_group_create(&group_param, &unicast_group); + if (ret) { + LOG_ERR("Failed to create unicast group: %d", ret); + } else { + unicast_group_created = true; + } + } + + ret = k_mutex_lock(&mtx_cap_procedure_proceed, CAP_PROCED_MUTEX_WAIT_TIME_MS); + if (ret == -EAGAIN) { + LOG_ERR("CAP procedure lock timeout"); + } + + struct bt_cap_unicast_audio_start_stream_param + cap_stream_params[CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT + + CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT]; + + struct bt_cap_unicast_audio_start_param param; + + param.stream_params = cap_stream_params; + param.count = 0; + param.type = BT_CAP_SET_TYPE_AD_HOC; + + if (unicast_server->sink_ep) { + cap_stream_params[param.count].member.member = unicast_server->device_conn; + cap_stream_params[param.count].stream = &unicast_server->cap_sink_stream; + cap_stream_params[param.count].ep = unicast_server->sink_ep; + cap_stream_params[param.count].codec_cfg = &lc3_preset_sink.codec_cfg; + param.count++; + } + + if (unicast_server->source_ep) { + cap_stream_params[param.count].member.member = unicast_server->device_conn; + cap_stream_params[param.count].stream = &unicast_server->cap_source_stream; + cap_stream_params[param.count].ep = unicast_server->source_ep; + cap_stream_params[param.count].codec_cfg = &lc3_preset_source.codec_cfg; + param.count++; + } + + ret = bt_cap_initiator_unicast_audio_start(¶m); + if (ret == -EBUSY) { + /* Try again once the ongoing start procedure is completed */ + ret = k_msgq_put(&cap_start_msgq, &idx, K_NO_WAIT); + if (ret) { + LOG_ERR("Failed to put device_index on the queue: %d", ret); + } + } else if (ret) { + LOG_ERR("Failed to start unicast sink audio: %d", ret); + } +} +K_WORK_DEFINE(cap_start_work, cap_start_worker); + +/** + * @brief Get the common presentation delay for all unicast_servers. + * + * @param[in] index The index of the device to test against. + * @param[out] pres_dly_us Pointer to where the presentation delay will be stored. + * + * @retval 0 Operation successful. + * @retval -EINVAL Any error. + */ +static int device_pres_delay_find(struct stream_index idx, uint32_t *pres_dly_us) +{ + struct le_audio_unicast_server *unicast_server = + &unicast_servers[idx.lvl1][idx.lvl2][idx.lvl3]; + + uint32_t pd_min = unicast_server->sink_ep->qos_pref.pd_min; + uint32_t pd_max = unicast_server->sink_ep->qos_pref.pd_max; + uint32_t pref_pd_min = unicast_server->sink_ep->qos_pref.pref_pd_min; + uint32_t pref_pd_max = unicast_server->sink_ep->qos_pref.pref_pd_max; + + LOG_DBG("Index: %d, Pref min: %d, pref max: %d, pres_min: %d, pres_max: %d", idx.lvl3, + pref_pd_min, pref_pd_max, pd_min, pd_max); + + *pres_dly_us = 0; + + for (int i = 0; i < ARRAY_SIZE(unicast_servers[idx.lvl1][idx.lvl2]); i++) { + if (le_audio_ep_qos_configured(unicast_servers[idx.lvl1][idx.lvl2][i].sink_ep)) { + LOG_DBG("i: %d, Pref min: %d, pref max: %d, pres_min: %d, pres_max: %d", i, + unicast_servers[idx.lvl1][idx.lvl2][i] + .sink_ep->qos_pref.pref_pd_min, + unicast_servers[idx.lvl1][idx.lvl2][i] + .sink_ep->qos_pref.pref_pd_max, + unicast_servers[idx.lvl1][idx.lvl2][i].sink_ep->qos_pref.pd_min, + unicast_servers[idx.lvl1][idx.lvl2][i].sink_ep->qos_pref.pd_max); + + pd_min = MAX( + pd_min, + unicast_servers[idx.lvl1][idx.lvl2][i].sink_ep->qos_pref.pd_min); + pref_pd_min = MAX(pref_pd_min, unicast_servers[idx.lvl1][idx.lvl2][i] + .sink_ep->qos_pref.pref_pd_min); + + if (unicast_servers[idx.lvl1][idx.lvl2][i].sink_ep->qos_pref.pd_max) { + pd_max = MIN(pd_max, unicast_servers[idx.lvl1][idx.lvl2][i] + .sink_ep->qos_pref.pd_max); + } + + if (unicast_servers[idx.lvl1][idx.lvl2][i].sink_ep->qos_pref.pref_pd_max) { + pref_pd_max = + MIN(pref_pd_max, unicast_servers[idx.lvl1][idx.lvl2][i] + .sink_ep->qos_pref.pref_pd_max); + } + } + } + + if (IS_ENABLED(CONFIG_BT_AUDIO_PRES_DELAY_SRCH_MIN)) { + *pres_dly_us = pd_min; + + return 0; + } + + if (IS_ENABLED(CONFIG_BT_AUDIO_PRES_DELAY_SRCH_MAX)) { + *pres_dly_us = pd_max; + + return 0; + } + + if (IS_ENABLED(CONFIG_BT_AUDIO_PRES_DELAY_SRCH_PREF_MIN)) { + if (pref_pd_min == 0) { + *pres_dly_us = pd_min; + } else if (pref_pd_min < pd_min) { + *pres_dly_us = pd_min; + LOG_WRN("pref_pd_min < pd_min (%d < %d), pres delay set to %d", pref_pd_min, + pd_min, *pres_dly_us); + } else if (pref_pd_min <= pd_max) { + *pres_dly_us = pref_pd_min; + } else { + *pres_dly_us = pd_max; + LOG_WRN("pref_pd_min > pd_max (%d > %d), pres delay set to %d", pref_pd_min, + pd_max, *pres_dly_us); + } + + return 0; + } + + if (IS_ENABLED(CONFIG_BT_AUDIO_PRES_DELAY_SRCH_PREF_MAX)) { + if (pref_pd_max == 0) { + *pres_dly_us = pd_max; + } else if (pref_pd_max > pd_max) { + *pres_dly_us = pd_max; + LOG_WRN("pref_pd_max > pd_max (%d > %d), pres delay set to %d", pref_pd_max, + pd_max, *pres_dly_us); + } else if (pref_pd_max >= pd_min) { + *pres_dly_us = pref_pd_max; + } else { + *pres_dly_us = pd_min; + LOG_WRN("pref_pd_max < pd_min (%d < %d), pres delay set to %d", pref_pd_max, + pd_min, *pres_dly_us); + } + + return 0; + } + + LOG_ERR("Trying to use unrecognized search mode"); + + return -EINVAL; +} + +/** + * @brief Get device index based on connection. + * + * @param[in] conn The connection to search for. + * @param[out] index The device index. + * + * @retval 0 Operation successful. + * @retval -EINVAL There is no match. + */ +static int device_index_get(const struct bt_conn *conn, struct stream_index *idx) +{ + if (conn == NULL) { + LOG_ERR("No connection provided"); + return -EINVAL; + } + + for (int i = 0; i < CONFIG_BT_ISO_MAX_CIG; i++) { + for (int j = 0; j < ARRAY_SIZE(unicast_servers[i][0]); j++) { + if (unicast_servers[i][0][j].device_conn == conn) { + idx->lvl1 = i; + idx->lvl2 = 0; + idx->lvl3 = j; + return 0; + } + } + } + + return -EINVAL; +} + +static int device_index_vacant_get(const struct bt_conn *conn, struct stream_index *idx) +{ + + if (device_index_get(conn, idx) == 0) { + LOG_WRN("Device has already been discovered"); + return -EALREADY; + } + + for (int i = 0; i < CONFIG_BT_ISO_MAX_CIG; i++) { + for (int j = 0; j < ARRAY_SIZE(unicast_servers[i][0]); j++) { + if (unicast_servers[i][0][j].device_conn == NULL) { + idx->lvl1 = i; + idx->lvl2 = 0; + idx->lvl3 = j; + return 0; + } + } + } + + LOG_WRN("No more room in device list"); + return -ENOSPC; +} + +static void supported_sample_rates_print(uint16_t supported_sample_rates, enum bt_audio_dir dir) +{ + char supported_str[20] = ""; + + if (supported_sample_rates & BT_AUDIO_CODEC_CAP_FREQ_48KHZ) { + strcat(supported_str, "48, "); + } + + if (supported_sample_rates & BT_AUDIO_CODEC_CAP_FREQ_24KHZ) { + strcat(supported_str, "24, "); + } + + if (supported_sample_rates & BT_AUDIO_CODEC_CAP_FREQ_32KHZ) { + strcat(supported_str, "32, "); + } + + if (supported_sample_rates & BT_AUDIO_CODEC_CAP_FREQ_16KHZ) { + strcat(supported_str, "16, "); + } + + if (dir == BT_AUDIO_DIR_SINK) { + LOG_DBG("Unicast_server supports: %s kHz in sink direction", supported_str); + } else if (dir == BT_AUDIO_DIR_SOURCE) { + LOG_DBG("Unicast_server supports: %s kHz in source direction", supported_str); + } +} + +static bool sink_parse_cb(struct bt_data *data, void *user_data) +{ + if (data->type == BT_AUDIO_CODEC_CAP_TYPE_FREQ) { + uint16_t lc3_freq_bit = sys_get_le16(data->data); + + supported_sample_rates_print(lc3_freq_bit, BT_AUDIO_DIR_SINK); + + /* Try with the preferred sample rate first */ + switch (CONFIG_BT_AUDIO_PREF_SAMPLE_RATE_VALUE) { + case BT_AUDIO_CODEC_CFG_FREQ_48KHZ: + if (lc3_freq_bit & BT_AUDIO_CODEC_CAP_FREQ_48KHZ) { + lc3_preset_sink = lc3_preset_sink_48_4_1; + *(bool *)user_data = true; + /* Found what we were looking for, stop parsing LTV */ + return false; + } + + break; + + case BT_AUDIO_CODEC_CFG_FREQ_24KHZ: + if (lc3_freq_bit & BT_AUDIO_CODEC_CAP_FREQ_24KHZ) { + lc3_preset_sink = lc3_preset_sink_24_2_1; + *(bool *)user_data = true; + /* Found what we were looking for, stop parsing LTV */ + return false; + } + + break; + + case BT_AUDIO_CODEC_CFG_FREQ_16KHZ: + if (lc3_freq_bit & BT_AUDIO_CODEC_CAP_FREQ_16KHZ) { + lc3_preset_sink = lc3_preset_sink_16_2_1; + *(bool *)user_data = true; + /* Found what we were looking for, stop parsing LTV */ + return false; + } + + break; + } + + /* If no match with the preferred, revert to trying highest first */ + if (lc3_freq_bit & BT_AUDIO_CODEC_CAP_FREQ_48KHZ) { + lc3_preset_sink = lc3_preset_sink_48_4_1; + *(bool *)user_data = true; + } else if (lc3_freq_bit & BT_AUDIO_CODEC_CAP_FREQ_24KHZ) { + lc3_preset_sink = lc3_preset_sink_24_2_1; + *(bool *)user_data = true; + } else if (lc3_freq_bit & BT_AUDIO_CODEC_CAP_FREQ_16KHZ) { + lc3_preset_sink = lc3_preset_sink_16_2_1; + *(bool *)user_data = true; + } + + /* Found what we were looking for, stop parsing LTV */ + return false; + } + + /* Did not find what we were looking for, continue parsing LTV */ + return true; +} + +static void set_color_if_supported(char *str, uint16_t bitfield, uint16_t mask) +{ + if (bitfield & mask) { + strcat(str, COLOR_GREEN); + } else { + strcat(str, COLOR_RED); + } +} + +static bool caps_print_cb(struct bt_data *data, void *user_data) +{ + if (data->type == BT_AUDIO_CODEC_CAP_TYPE_FREQ) { + uint16_t freq_bit = sys_get_le16(data->data); + char supported_freq[320] = ""; + + set_color_if_supported(supported_freq, freq_bit, BT_AUDIO_CODEC_CAP_FREQ_8KHZ); + strcat(supported_freq, "8, "); + set_color_if_supported(supported_freq, freq_bit, BT_AUDIO_CODEC_CAP_FREQ_11KHZ); + strcat(supported_freq, "11.025, "); + set_color_if_supported(supported_freq, freq_bit, BT_AUDIO_CODEC_CAP_FREQ_16KHZ); + strcat(supported_freq, "16, "); + set_color_if_supported(supported_freq, freq_bit, BT_AUDIO_CODEC_CAP_FREQ_22KHZ); + strcat(supported_freq, "22.05, "); + set_color_if_supported(supported_freq, freq_bit, BT_AUDIO_CODEC_CAP_FREQ_24KHZ); + strcat(supported_freq, "24, "); + set_color_if_supported(supported_freq, freq_bit, BT_AUDIO_CODEC_CAP_FREQ_32KHZ); + strcat(supported_freq, "32, "); + set_color_if_supported(supported_freq, freq_bit, BT_AUDIO_CODEC_CAP_FREQ_44KHZ); + strcat(supported_freq, "44.1, "); + set_color_if_supported(supported_freq, freq_bit, BT_AUDIO_CODEC_CAP_FREQ_48KHZ); + strcat(supported_freq, "48, "); + set_color_if_supported(supported_freq, freq_bit, BT_AUDIO_CODEC_CAP_FREQ_88KHZ); + strcat(supported_freq, "88.2, "); + set_color_if_supported(supported_freq, freq_bit, BT_AUDIO_CODEC_CAP_FREQ_96KHZ); + strcat(supported_freq, "96, "); + set_color_if_supported(supported_freq, freq_bit, BT_AUDIO_CODEC_CAP_FREQ_176KHZ); + strcat(supported_freq, "176, "); + set_color_if_supported(supported_freq, freq_bit, BT_AUDIO_CODEC_CAP_FREQ_192KHZ); + strcat(supported_freq, "192, "); + set_color_if_supported(supported_freq, freq_bit, BT_AUDIO_CODEC_CAP_FREQ_384KHZ); + strcat(supported_freq, "384"); + + LOG_INF("\tFrequencies kHz: %s", supported_freq); + } + + if (data->type == BT_AUDIO_CODEC_CAP_TYPE_DURATION) { + uint16_t dur_bit = sys_get_le16(data->data); + char supported_dur[30] = ""; + + set_color_if_supported(supported_dur, dur_bit, BT_AUDIO_CODEC_CAP_DURATION_7_5); + strcat(supported_dur, "7.5, "); + set_color_if_supported(supported_dur, dur_bit, BT_AUDIO_CODEC_CAP_DURATION_10); + strcat(supported_dur, "10"); + + LOG_INF("\tFrame duration ms: %s", supported_dur); + } + + if (data->type == BT_AUDIO_CODEC_CAP_TYPE_CHAN_COUNT) { + uint16_t chan_bit = sys_get_le16(data->data); + char supported_chan[120] = ""; + + set_color_if_supported(supported_chan, chan_bit, BT_AUDIO_CODEC_CAP_CHAN_COUNT_1); + strcat(supported_chan, "1, "); + set_color_if_supported(supported_chan, chan_bit, BT_AUDIO_CODEC_CAP_CHAN_COUNT_2); + strcat(supported_chan, "2, "); + set_color_if_supported(supported_chan, chan_bit, BT_AUDIO_CODEC_CAP_CHAN_COUNT_3); + strcat(supported_chan, "3, "); + set_color_if_supported(supported_chan, chan_bit, BT_AUDIO_CODEC_CAP_CHAN_COUNT_4); + strcat(supported_chan, "4, "); + set_color_if_supported(supported_chan, chan_bit, BT_AUDIO_CODEC_CAP_CHAN_COUNT_5); + strcat(supported_chan, "5, "); + set_color_if_supported(supported_chan, chan_bit, BT_AUDIO_CODEC_CAP_CHAN_COUNT_6); + strcat(supported_chan, "6, "); + set_color_if_supported(supported_chan, chan_bit, BT_AUDIO_CODEC_CAP_CHAN_COUNT_7); + strcat(supported_chan, "7, "); + set_color_if_supported(supported_chan, chan_bit, BT_AUDIO_CODEC_CAP_CHAN_COUNT_8); + strcat(supported_chan, "8"); + + LOG_INF("\tChannels supported: %s", supported_chan); + } + + if (data->type == BT_AUDIO_CODEC_CAP_TYPE_FRAME_LEN) { + uint16_t lc3_min_frame_length = sys_get_le16(data->data); + uint16_t lc3_max_frame_length = sys_get_le16(data->data + sizeof(uint16_t)); + + LOG_INF("\tFrame length bytes: %d - %d", lc3_min_frame_length, + lc3_max_frame_length); + } + + if (data->type == BT_AUDIO_CODEC_CAP_TYPE_FRAME_COUNT) { + uint16_t lc3_frame_per_sdu = sys_get_le16(data->data); + + LOG_INF("\tMax frames per SDU: %d", lc3_frame_per_sdu); + } + + return true; +} + +static bool source_parse_cb(struct bt_data *data, void *user_data) +{ + if (data->type == BT_AUDIO_CODEC_CAP_TYPE_FREQ) { + uint16_t lc3_freq_bit = sys_get_le16(data->data); + + supported_sample_rates_print(lc3_freq_bit, BT_AUDIO_DIR_SOURCE); + + /* Try with the preferred sample rate first */ + switch (CONFIG_BT_AUDIO_PREF_SAMPLE_RATE_VALUE) { + case BT_AUDIO_CODEC_CFG_FREQ_48KHZ: + if (lc3_freq_bit & BT_AUDIO_CODEC_CAP_FREQ_48KHZ) { + lc3_preset_source = lc3_preset_source_48_4_1; + *(bool *)user_data = true; + /* Found what we were looking for, stop parsing LTV */ + return false; + } + + break; + + case BT_AUDIO_CODEC_CFG_FREQ_24KHZ: + if (lc3_freq_bit & BT_AUDIO_CODEC_CAP_FREQ_24KHZ) { + lc3_preset_source = lc3_preset_source_24_2_1; + *(bool *)user_data = true; + /* Found what we were looking for, stop parsing LTV */ + return false; + } + + break; + + case BT_AUDIO_CODEC_CFG_FREQ_16KHZ: + if (lc3_freq_bit & BT_AUDIO_CODEC_CAP_FREQ_16KHZ) { + lc3_preset_source = lc3_preset_source_16_2_1; + *(bool *)user_data = true; + /* Found what we were looking for, stop parsing LTV */ + return false; + } + + break; + } + + /* If no match with the preferred, revert to trying highest first */ + if (lc3_freq_bit & BT_AUDIO_CODEC_CAP_FREQ_48KHZ) { + lc3_preset_source = lc3_preset_source_48_4_1; + *(bool *)user_data = true; + } else if (lc3_freq_bit & BT_AUDIO_CODEC_CAP_FREQ_24KHZ) { + lc3_preset_source = lc3_preset_source_24_2_1; + *(bool *)user_data = true; + } else if (lc3_freq_bit & BT_AUDIO_CODEC_CAP_FREQ_16KHZ) { + lc3_preset_source = lc3_preset_source_16_2_1; + *(bool *)user_data = true; + } + + /* Found what we were looking for, stop parsing LTV */ + return false; + } + + /* Did not find what we were looking for, continue parsing LTV */ + return true; +} + +/** + * @brief Check if the gateway can support the codec capabilities. + * + * @note Currently only the sampling frequency is checked. + * + * @param[in] cap_array The array of pointers to codec capabilities. + * @param[in] num_caps The size of cap_array. + * @param[in] dir Direction of the capabilities to check. + * @param[in] index Device index. + * + * @return True if valid codec capability found, false otherwise. + */ +static bool valid_codec_cap_check(struct bt_audio_codec_cap cap_array[], uint8_t num_caps, + enum bt_audio_dir dir, uint8_t index) +{ + bool valid_result = false; + + /* Only the sampling frequency is checked */ + if (dir == BT_AUDIO_DIR_SINK) { + LOG_INF("Discovered %d sink endpoint(s) for device %d", num_caps, index); + for (int i = 0; i < num_caps; i++) { + if (IS_ENABLED(CONFIG_BT_AUDIO_EP_PRINT)) { + LOG_INF(""); + LOG_INF("Dev: %d Sink EP %d", index, i); + (void)bt_audio_data_parse(cap_array[i].data, cap_array[i].data_len, + caps_print_cb, NULL); + LOG_INF("__________________________"); + } + + (void)bt_audio_data_parse(cap_array[i].data, cap_array[i].data_len, + sink_parse_cb, &valid_result); + } + } else if (dir == BT_AUDIO_DIR_SOURCE) { + LOG_INF("Discovered %d source endpoint(s) for device %d", num_caps, index); + for (int i = 0; i < num_caps; i++) { + if (IS_ENABLED(CONFIG_BT_AUDIO_EP_PRINT)) { + LOG_INF(""); + LOG_INF("Dev: %d Source EP %d", index, i); + (void)bt_audio_data_parse(cap_array[i].data, cap_array[i].data_len, + caps_print_cb, NULL); + LOG_INF("__________________________"); + } + + (void)bt_audio_data_parse(cap_array[i].data, cap_array[i].data_len, + source_parse_cb, &valid_result); + } + } + + return valid_result; +} + +/** + * @brief Set the allocation to a preset codec configuration. + * + * @param codec The preset codec configuration. + * @param loc Location bitmask setting. + * + */ +static void bt_audio_codec_allocation_set(struct bt_audio_codec_cfg *codec_cfg, + enum bt_audio_location loc) +{ + for (size_t i = 0U; i < codec_cfg->data_len;) { + const uint8_t len = codec_cfg->data[i++]; + const uint8_t type = codec_cfg->data[i++]; + uint8_t *value = &codec_cfg->data[i]; + const uint8_t value_len = len - sizeof(type); + + if (type == BT_AUDIO_CODEC_CFG_CHAN_ALLOC) { + const uint32_t loc_32 = loc; + + sys_put_le32(loc_32, value); + + return; + } + i += value_len; + } +} + +static int update_cap_sink_stream_qos(struct le_audio_unicast_server *unicast_server, + uint32_t pres_delay_us) +{ + int ret; + + if (unicast_server->cap_sink_stream.bap_stream.ep == NULL) { + return -ESRCH; + } + + if (unicast_server->cap_sink_stream.bap_stream.qos == NULL) { + LOG_WRN("No QoS found for %p", (void *)&unicast_server->cap_sink_stream.bap_stream); + return -ENXIO; + } + + if (unicast_server->cap_sink_stream.bap_stream.qos->pd != pres_delay_us) { + struct bt_cap_unicast_audio_stop_param param; + struct bt_cap_stream *streams[2]; + + LOG_DBG("Current preset PD = %d us, target PD = %d us", + unicast_server->cap_sink_stream.bap_stream.qos->pd, pres_delay_us); + + param.streams = streams; + param.count = 0; + param.type = BT_CAP_SET_TYPE_AD_HOC; + param.release = true; + + if (playing_state && + le_audio_ep_state_check(unicast_server->cap_sink_stream.bap_stream.ep, + BT_BAP_EP_STATE_STREAMING)) { + LOG_DBG("Update streaming %s unicast_server, connection %p, stream %p", + unicast_server->ch_name, (void *)&unicast_server->device_conn, + (void *)&unicast_server->cap_sink_stream.bap_stream); + + unicast_server->qos_reconfigure = true; + unicast_server->reconfigure_pd = pres_delay_us; + + streams[param.count] = &unicast_server->cap_sink_stream; + param.count++; + } else { + LOG_DBG("Reset %s unicast_server, connection %p, stream %p", + unicast_server->ch_name, (void *)&unicast_server->device_conn, + (void *)&unicast_server->cap_sink_stream.bap_stream); + unicast_server->cap_sink_stream.bap_stream.qos->pd = pres_delay_us; + } + + if (playing_state && + le_audio_ep_state_check(unicast_server->cap_source_stream.bap_stream.ep, + BT_BAP_EP_STATE_STREAMING)) { + unicast_server->qos_reconfigure = true; + unicast_server->reconfigure_pd = pres_delay_us; + + streams[param.count] = &unicast_server->cap_source_stream; + param.count++; + } + + if (param.count > 0) { + ret = bt_cap_initiator_unicast_audio_stop(¶m); + if (ret) { + LOG_WRN("Failed to stop streams: %d, use default PD in preset", + ret); + return ret; + } + } + } + + return 0; +} + +static void unicast_client_location_cb(struct bt_conn *conn, enum bt_audio_dir dir, + enum bt_audio_location loc) +{ + int ret; + struct stream_index idx; + + ret = device_index_get(conn, &idx); + if (ret) { + LOG_ERR("Device index not found"); + return; + } + + struct le_audio_unicast_server *unicast_server = + &unicast_servers[idx.lvl1][idx.lvl2][idx.lvl3]; + + if ((loc & BT_AUDIO_LOCATION_FRONT_LEFT) || (loc & BT_AUDIO_LOCATION_SIDE_LEFT) || + (loc == BT_AUDIO_LOCATION_MONO_AUDIO)) { + unicast_server->location = BT_AUDIO_LOCATION_FRONT_LEFT; + unicast_server->ch_name = "LEFT"; + + } else if ((loc & BT_AUDIO_LOCATION_FRONT_RIGHT) || (loc & BT_AUDIO_LOCATION_SIDE_RIGHT)) { + unicast_server->location = BT_AUDIO_LOCATION_FRONT_RIGHT; + unicast_server->ch_name = "RIGHT"; + } else { + LOG_WRN("Channel location not supported: %d", loc); + le_audio_event_publish(LE_AUDIO_EVT_NO_VALID_CFG, conn, dir); + } +} + +static void available_contexts_cb(struct bt_conn *conn, enum bt_audio_context snk_ctx, + enum bt_audio_context src_ctx) +{ + char addr[BT_ADDR_LE_STR_LEN]; + + (void)bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + + LOG_DBG("conn: %s, snk ctx %d src ctx %d", addr, snk_ctx, src_ctx); +} + +static int temp_cap_index_get(struct bt_conn *conn, uint8_t *index) +{ + if (conn == NULL) { + LOG_ERR("No conn provided"); + return -EINVAL; + } + + for (int i = 0; i < ARRAY_SIZE(temp_cap); i++) { + if (temp_cap[i].conn == conn) { + *index = i; + return 0; + } + } + + /* Connection not found in temp_cap, searching for empty slot */ + for (int i = 0; i < ARRAY_SIZE(temp_cap); i++) { + if (temp_cap[i].conn == NULL) { + temp_cap[i].conn = conn; + *index = i; + return 0; + } + } + + LOG_WRN("No more space in temp_cap"); + + return -ECANCELED; +} + +static void pac_record_cb(struct bt_conn *conn, enum bt_audio_dir dir, + const struct bt_audio_codec_cap *codec) +{ + int ret; + uint8_t temp_cap_index; + + ret = temp_cap_index_get(conn, &temp_cap_index); + if (ret) { + LOG_ERR("Could not get temporary CAP storage index"); + return; + } + + if (codec->id != BT_HCI_CODING_FORMAT_LC3) { + LOG_DBG("Only the LC3 codec is supported"); + return; + } + + /* num_caps is an increasing index that starts at 0 */ + if (temp_cap[temp_cap_index].num_caps < ARRAY_SIZE(temp_cap[temp_cap_index].codec)) { + struct bt_audio_codec_cap *codec_loc = + &temp_cap[temp_cap_index].codec[temp_cap[temp_cap_index].num_caps]; + + memcpy(codec_loc, codec, sizeof(struct bt_audio_codec_cap)); + + temp_cap[temp_cap_index].num_caps++; + } else { + LOG_WRN("No more space. Increase CONFIG_CODEC_CAP_COUNT_MAX"); + } +} + +static void endpoint_cb(struct bt_conn *conn, enum bt_audio_dir dir, struct bt_bap_ep *ep) +{ + int ret; + struct stream_index idx; + + ret = device_index_get(conn, &idx); + if (ret) { + LOG_ERR("Unknown connection, should not reach here"); + return; + } + + struct le_audio_unicast_server *unicast_server = + &unicast_servers[idx.lvl1][idx.lvl2][idx.lvl3]; + + if (dir == BT_AUDIO_DIR_SINK) { + if (ep != NULL) { + if (unicast_server->num_sink_eps > 0) { + LOG_WRN("More than one sink endpoint found, idx 0 is used " + "by default"); + return; + } + + unicast_server->sink_ep = ep; + unicast_server->num_sink_eps++; + return; + } + + if (unicast_server->sink_ep == NULL) { + LOG_WRN("No sink endpoints found"); + } + + return; + } else if (dir == BT_AUDIO_DIR_SOURCE) { + if (ep != NULL) { + if (unicast_server->num_source_eps > 0) { + LOG_WRN("More than one source endpoint found, idx 0 is " + "used by default"); + return; + } + + unicast_server->source_ep = ep; + unicast_server->num_source_eps++; + return; + } + + if (unicast_server->source_ep == NULL) { + LOG_WRN("No source endpoints found"); + } + + return; + } else { + LOG_WRN("Endpoint direction not recognized: %d", dir); + } +} + +static void discover_cb(struct bt_conn *conn, int err, enum bt_audio_dir dir) +{ + int ret; + uint8_t temp_cap_index; + + struct stream_index idx; + + ret = device_index_get(conn, &idx); + if (ret) { + LOG_ERR("Unknown connection, should not reach here"); + return; + } + + struct le_audio_unicast_server *unicast_server = + &unicast_servers[idx.lvl1][idx.lvl2][idx.lvl3]; + + if (err == BT_ATT_ERR_ATTRIBUTE_NOT_FOUND) { + if (dir == BT_AUDIO_DIR_SINK) { + LOG_WRN("No sinks found"); + unicast_server->waiting_for_sink_disc = false; + } else if (dir == BT_AUDIO_DIR_SOURCE) { + LOG_WRN("No sources found"); + unicast_server->waiting_for_source_disc = false; + } + } else if (err) { + LOG_ERR("Discovery failed: %d", err); + return; + } + + ret = temp_cap_index_get(conn, &temp_cap_index); + if (ret) { + LOG_ERR("Could not get temporary CAP storage index"); + return; + } + + for (int i = 0; i < CONFIG_CODEC_CAP_COUNT_MAX; i++) { + if (dir == BT_AUDIO_DIR_SINK && !err) { + memcpy(&unicast_server->sink_codec_cap[i], + &temp_cap[temp_cap_index].codec[i], + sizeof(struct bt_audio_codec_cap)); + } else if (dir == BT_AUDIO_DIR_SOURCE && !err) { + memcpy(&unicast_server->source_codec_cap[i], + &temp_cap[temp_cap_index].codec[i], + sizeof(struct bt_audio_codec_cap)); + } + } + + if (dir == BT_AUDIO_DIR_SINK && !err) { + if (valid_codec_cap_check(unicast_server->sink_codec_cap, + temp_cap[temp_cap_index].num_caps, BT_AUDIO_DIR_SINK, + idx.lvl3)) { + bt_audio_codec_allocation_set(&lc3_preset_sink.codec_cfg, + unicast_server->location); + } else { + /* NOTE: The string below is used by the Nordic CI system */ + LOG_WRN("No valid codec capability found for %s sink", + unicast_server->ch_name); + unicast_server->sink_ep = NULL; + } + } else if (dir == BT_AUDIO_DIR_SOURCE && !err) { + if (valid_codec_cap_check(unicast_server->source_codec_cap, + temp_cap[temp_cap_index].num_caps, BT_AUDIO_DIR_SOURCE, + idx.lvl3)) { + bt_audio_codec_allocation_set(&lc3_preset_source.codec_cfg, + unicast_server->location); + } else { + LOG_WRN("No valid codec capability found for %s source", + unicast_server->ch_name); + unicast_server->source_ep = NULL; + } + } + + /* Free up the slot in temp_cap */ + memset(temp_cap[temp_cap_index].codec, 0, sizeof(temp_cap[temp_cap_index].codec)); + temp_cap[temp_cap_index].conn = NULL; + temp_cap[temp_cap_index].num_caps = 0; + + if (dir == BT_AUDIO_DIR_SINK) { + unicast_server->waiting_for_sink_disc = false; + + if (unicast_server->waiting_for_source_disc) { + ret = bt_bap_unicast_client_discover(conn, BT_AUDIO_DIR_SOURCE); + if (ret) { + LOG_WRN("Failed to discover source: %d", ret); + } + + return; + } + } else if (dir == BT_AUDIO_DIR_SOURCE) { + unicast_server->waiting_for_source_disc = false; + } + + if (!playing_state) { + /* Since we are not in a playing state we return before starting the new streams */ + return; + } + + ret = k_msgq_put(&cap_start_msgq, &idx, K_NO_WAIT); + if (ret) { + LOG_ERR("Failed to put device_index on the queue: %d", ret); + return; + } + + k_work_submit(&cap_start_work); +} + +#if (CONFIG_BT_AUDIO_TX) +static void stream_sent_cb(struct bt_bap_stream *stream) +{ + int ret; + struct stream_index idx; + uint8_t state; + + ret = le_audio_ep_state_get(stream->ep, &state); + if (ret) { + return; + } + + if (state == BT_BAP_EP_STATE_STREAMING) { + + ret = device_index_get(stream->conn, &idx); + if (ret) { + LOG_ERR("Device index not found"); + } else { + ERR_CHK(bt_le_audio_tx_stream_sent(idx)); + } + } else { + LOG_WRN("Not in streaming state: %d", state); + } +} +#endif /* CONFIG_BT_AUDIO_TX */ + +static void check_and_update_pd_in_group(struct stream_index idx, uint32_t new_pres_dly_us) +{ + int ret; + + for (int i = 0; i < ARRAY_SIZE(unicast_servers[idx.lvl1][idx.lvl2]); i++) { + if (i != idx.lvl3 && unicast_servers[idx.lvl1][idx.lvl2][i].device_conn != NULL) { + ret = update_cap_sink_stream_qos(&unicast_servers[idx.lvl1][idx.lvl2][i], + new_pres_dly_us); + if (ret && ret != -ESRCH) { + /* TODO: Fix OCT-3111 and then turn the WRN to ERR */ + LOG_WRN("Presentation delay not set for %s " + "device: %d", + unicast_servers[idx.lvl1][idx.lvl2][i].ch_name, ret); + } + } + } + + LOG_DBG("Set %s, connection %p, stream %p", + unicast_servers[idx.lvl1][idx.lvl2][idx.lvl3].ch_name, + (void *)&unicast_servers[idx.lvl1][idx.lvl2][idx.lvl3].device_conn, + (void *)&unicast_servers[idx.lvl1][idx.lvl2][idx.lvl3].cap_sink_stream.bap_stream); + + unicast_servers[idx.lvl1][idx.lvl2][idx.lvl3].cap_sink_stream.bap_stream.qos->pd = + new_pres_dly_us; +} + +static void stream_configured_cb(struct bt_bap_stream *stream, + const struct bt_bap_qos_cfg_pref *pref) +{ + int ret; + uint32_t new_pres_dly_us; + enum bt_audio_dir dir; + struct stream_index idx; + + ret = device_index_get(stream->conn, &idx); + if (ret) { + LOG_ERR("Unknown connection, should not reach here"); + return; + } + + struct le_audio_unicast_server *unicast_server = + &unicast_servers[idx.lvl1][idx.lvl2][idx.lvl3]; + + dir = le_audio_stream_dir_get(stream); + if (dir <= 0) { + LOG_ERR("Failed to get dir of stream %p", (void *)stream); + return; + } + + if (dir == BT_AUDIO_DIR_SINK) { + /* NOTE: The string below is used by the Nordic CI system */ + LOG_INF("%s sink stream configured", unicast_server->ch_name); + le_audio_print_codec(unicast_server->cap_sink_stream.bap_stream.codec_cfg, dir); + } else if (dir == BT_AUDIO_DIR_SOURCE) { + LOG_INF("%s source stream configured", unicast_server->ch_name); + le_audio_print_codec(unicast_server->cap_source_stream.bap_stream.codec_cfg, dir); + } else { + LOG_ERR("Endpoint direction not recognized: %d", dir); + return; + } + LOG_DBG("Configured Stream info: %s, %p, dir %d", unicast_server->ch_name, (void *)stream, + dir); + + ret = device_pres_delay_find(idx, &new_pres_dly_us); + if (ret) { + LOG_ERR("Cannot get a valid presentation delay"); + return; + } + + if (unicast_server->waiting_for_source_disc) { + return; + } + + if (le_audio_ep_state_check(unicast_server->cap_sink_stream.bap_stream.ep, + BT_BAP_EP_STATE_CODEC_CONFIGURED)) { + check_and_update_pd_in_group(idx, new_pres_dly_us); + } + + le_audio_event_publish(LE_AUDIO_EVT_CONFIG_RECEIVED, stream->conn, dir); + + /* Make sure both sink and source ep (if both are discovered) are configured before + * QoS + */ + if ((unicast_server->sink_ep != NULL && + !le_audio_ep_state_check(unicast_server->cap_sink_stream.bap_stream.ep, + BT_BAP_EP_STATE_CODEC_CONFIGURED)) || + (unicast_server->source_ep != NULL && + !le_audio_ep_state_check(unicast_server->cap_source_stream.bap_stream.ep, + BT_BAP_EP_STATE_CODEC_CONFIGURED))) { + return; + } +} + +static void stream_qos_set_cb(struct bt_bap_stream *stream) +{ + int ret; + struct stream_index idx; + + LOG_DBG("QoS set cb"); + + ret = device_index_get(stream->conn, &idx); + if (ret) { + LOG_ERR("Unknown connection, should not reach here"); + return; + } + + struct le_audio_unicast_server *unicast_server = + &unicast_servers[idx.lvl1][idx.lvl2][idx.lvl3]; + + if (unicast_server->qos_reconfigure) { + LOG_DBG("Reconfiguring: %s to PD: %d", unicast_server->ch_name, + unicast_server->reconfigure_pd); + + unicast_server->qos_reconfigure = false; + unicast_server->cap_sink_stream.bap_stream.qos->pd = unicast_server->reconfigure_pd; + } else { + LOG_DBG("Set %s to PD: %d", unicast_server->ch_name, stream->qos->pd); + } +} + +static void stream_enabled_cb(struct bt_bap_stream *stream) +{ + LOG_DBG("Stream enabled: %p", (void *)stream); +} + +static void stream_started_cb(struct bt_bap_stream *stream) +{ + int ret; + enum bt_audio_dir dir; + + dir = le_audio_stream_dir_get(stream); + if (dir <= 0) { + LOG_ERR("Failed to get dir of stream %p", (void *)stream); + return; + } + + if (IS_ENABLED(CONFIG_BT_AUDIO_TX)) { + struct stream_index idx; + + ret = device_index_get(stream->conn, &idx); + if (ret) { + LOG_ERR("Device index not found"); + } else { + ERR_CHK(bt_le_audio_tx_stream_started(idx)); + } + } + + /* NOTE: The string below is used by the Nordic CI system */ + LOG_INF("Stream %p started", (void *)stream); + + le_audio_event_publish(LE_AUDIO_EVT_STREAMING, stream->conn, dir); +} + +static void stream_metadata_updated_cb(struct bt_bap_stream *stream) +{ + LOG_DBG("Audio Stream %p metadata updated", (void *)stream); +} + +static void stream_disabled_cb(struct bt_bap_stream *stream) +{ + LOG_DBG("Audio Stream %p disabled", (void *)stream); +} + +static void stream_stopped_cb(struct bt_bap_stream *stream, uint8_t reason) +{ + /* NOTE: The string below is used by the Nordic CI system */ + LOG_INF("Stream %p stopped. Reason %d", (void *)stream, reason); + + /* Check if the other streams are streaming, send event if not */ + for (int i = 0; i < CONFIG_BT_ISO_MAX_CIG; i++) { + for (int j = 0; j < ARRAY_SIZE(unicast_servers[i][0]); j++) { + if (le_audio_ep_state_check( + unicast_servers[i][0][j].cap_sink_stream.bap_stream.ep, + BT_BAP_EP_STATE_STREAMING) || + le_audio_ep_state_check( + unicast_servers[i][0][j].cap_source_stream.bap_stream.ep, + BT_BAP_EP_STATE_STREAMING)) { + return; + } + } + } + + le_audio_event_publish(LE_AUDIO_EVT_NOT_STREAMING, stream->conn, BT_AUDIO_DIR_SINK); +} + +static void stream_released_cb(struct bt_bap_stream *stream) +{ + LOG_DBG("Audio Stream %p released", (void *)stream); + + /* Check if the other streams are streaming, send event if not */ + for (int i = 0; i < CONFIG_BT_ISO_MAX_CIG; i++) { + for (int j = 0; j < ARRAY_SIZE(unicast_servers[i][0]); j++) { + if (le_audio_ep_state_check( + unicast_servers[i][0][j].cap_sink_stream.bap_stream.ep, + BT_BAP_EP_STATE_STREAMING) || + le_audio_ep_state_check( + unicast_servers[i][0][j].cap_source_stream.bap_stream.ep, + BT_BAP_EP_STATE_STREAMING)) { + return; + } + } + } + + le_audio_event_publish(LE_AUDIO_EVT_NOT_STREAMING, stream->conn, BT_AUDIO_DIR_SINK); +} + +#if (CONFIG_BT_AUDIO_RX) +static void stream_recv_cb(struct bt_bap_stream *stream, const struct bt_iso_recv_info *info, + struct net_buf *buf) +{ + int ret; + bool bad_frame = false; + struct stream_index idx; + + if (receive_cb == NULL) { + LOG_ERR("The RX callback has not been set"); + return; + } + + if (!(info->flags & BT_ISO_FLAGS_VALID)) { + bad_frame = true; + } + + ret = device_index_get(stream->conn, &idx); + if (ret) { + LOG_ERR("Device index not found"); + return; + } + + receive_cb(buf->data, buf->len, bad_frame, info->ts, idx.lvl3, + bt_audio_codec_cfg_get_octets_per_frame(stream->codec_cfg)); +} +#endif /* (CONFIG_BT_AUDIO_RX) */ + +static struct bt_bap_stream_ops stream_ops = { + .configured = stream_configured_cb, + .qos_set = stream_qos_set_cb, + .enabled = stream_enabled_cb, + .started = stream_started_cb, + .metadata_updated = stream_metadata_updated_cb, + .disabled = stream_disabled_cb, + .stopped = stream_stopped_cb, + .released = stream_released_cb, +#if (CONFIG_BT_AUDIO_RX) + .recv = stream_recv_cb, +#endif /* (CONFIG_BT_AUDIO_RX) */ +#if (CONFIG_BT_AUDIO_TX) + .sent = stream_sent_cb, +#endif /* (CONFIG_BT_AUDIO_TX) */ +}; + +static struct bt_bap_unicast_client_cb unicast_client_cbs = { + .location = unicast_client_location_cb, + .available_contexts = available_contexts_cb, + .pac_record = pac_record_cb, + .endpoint = endpoint_cb, + .discover = discover_cb, +}; + +static void disconnected_cleanup(struct stream_index idx) +{ + struct le_audio_unicast_server *unicast_server = + &unicast_servers[idx.lvl1][idx.lvl2][idx.lvl3]; + + unicast_server->device_conn = NULL; + unicast_server->sink_ep = NULL; + memset(unicast_server->sink_codec_cap, 0, sizeof(unicast_server->sink_codec_cap)); + unicast_server->source_ep = NULL; + memset(unicast_server->source_codec_cap, 0, sizeof(unicast_server->source_codec_cap)); + + unicast_server->num_sink_eps = 0; + unicast_server->num_source_eps = 0; +} + +static void unicast_discovery_complete_cb(struct bt_conn *conn, int err, + const struct bt_csip_set_coordinator_set_member *member, + const struct bt_csip_set_coordinator_csis_inst *csis_inst) +{ + int ret; + struct le_audio_msg msg; + struct stream_index idx; + + ret = device_index_get(conn, &idx); + if (ret) { + LOG_ERR("Device not found"); + return; + } + + if (err || csis_inst == NULL) { + LOG_WRN("Got err: %d from conn: %p", err, (void *)conn); + msg.set_size = 0; + msg.sirk = NULL; + } else { + LOG_DBG("\tErr: %d, set_size: %d", err, csis_inst->info.set_size); + LOG_HEXDUMP_DBG(csis_inst->info.sirk, BT_CSIP_SIRK_SIZE, "\tSIRK:"); + + unicast_servers[idx.lvl1][idx.lvl2][idx.lvl3].member = member; + msg.set_size = csis_inst->info.set_size; + msg.sirk = csis_inst->info.sirk; + } + + LOG_DBG("Unicast discovery complete cb"); + + msg.event = LE_AUDIO_EVT_COORD_SET_DISCOVERED; + msg.conn = conn; + + ret = zbus_chan_pub(&le_audio_chan, &msg, LE_AUDIO_ZBUS_EVENT_WAIT_TIME); + ERR_CHK(ret); +} + +static void unicast_start_complete_cb(int err, struct bt_conn *conn) +{ + int ret; + struct stream_index idx; + + k_mutex_unlock(&mtx_cap_procedure_proceed); + + if (err) { + LOG_WRN("Failed start_complete for conn: %p, err: %d", (void *)conn, err); + } + + LOG_DBG("Unicast start complete cb"); + ret = k_msgq_peek(&cap_start_msgq, &idx); + if (ret == 0) { + /* Pending start procedure found, call k_work */ + k_work_submit(&cap_start_work); + } else { + LOG_DBG("No outstanding work"); + } +} + +static void unicast_update_complete_cb(int err, struct bt_conn *conn) +{ + if (err) { + LOG_WRN("Failed update_complete for conn: %p, err: %d", (void *)conn, err); + } + + LOG_DBG("Unicast update complete cb"); +} + +static void unicast_stop_complete_cb(int err, struct bt_conn *conn) +{ + int ret; + + if (err) { + LOG_WRN("Failed stop_complete for conn: %p, err: %d", (void *)conn, err); + } + + LOG_DBG("Unicast stop complete cb"); + + /* Check for reconfigurable sink streams */ + for (int i = 0; i < CONFIG_BT_ISO_MAX_CIG; i++) { + for (int j = 0; j < ARRAY_SIZE(unicast_servers[i][0]); j++) { + if (unicast_servers[i][0][j].qos_reconfigure && playing_state) { + struct stream_index idx = { + .lvl1 = i, + .lvl2 = 0, + .lvl3 = j, + }; + ret = k_msgq_put(&cap_start_msgq, &idx, K_NO_WAIT); + if (ret) { + LOG_ERR("Failed to put device_index %d on the queue: %d", j, + ret); + } + k_work_submit(&cap_start_work); + } + } + } +} + +static struct bt_cap_initiator_cb cap_cbs = { + .unicast_discovery_complete = unicast_discovery_complete_cb, + .unicast_start_complete = unicast_start_complete_cb, + .unicast_update_complete = unicast_update_complete_cb, + .unicast_stop_complete = unicast_stop_complete_cb, +}; + +int unicast_client_config_get(struct bt_conn *conn, enum bt_audio_dir dir, uint32_t *bitrate, + uint32_t *sampling_rate_hz) +{ + int ret; + struct stream_index idx; + + if (conn == NULL) { + LOG_ERR("No valid connection pointer received"); + return -EINVAL; + } + + if (bitrate == NULL && sampling_rate_hz == NULL) { + LOG_ERR("No valid pointers received"); + return -ENXIO; + } + + ret = device_index_get(conn, &idx); + if (ret) { + LOG_WRN("No configured streams found"); + return ret; + } + + struct le_audio_unicast_server *unicast_server = + &unicast_servers[idx.lvl1][idx.lvl2][idx.lvl3]; + + if (dir == BT_AUDIO_DIR_SINK) { + if (unicast_server->cap_sink_stream.bap_stream.codec_cfg == NULL) { + LOG_ERR("No codec found for the stream"); + + return -ENXIO; + } + + if (sampling_rate_hz != NULL) { + ret = le_audio_freq_hz_get( + unicast_server->cap_sink_stream.bap_stream.codec_cfg, + sampling_rate_hz); + if (ret) { + LOG_ERR("Invalid sampling frequency: %d", ret); + return -ENXIO; + } + } + + if (bitrate != NULL) { + ret = le_audio_bitrate_get( + unicast_server->cap_sink_stream.bap_stream.codec_cfg, bitrate); + if (ret) { + LOG_ERR("Unable to calculate bitrate: %d", ret); + return -ENXIO; + } + } + } else if (dir == BT_AUDIO_DIR_SOURCE) { + if (unicast_server->cap_source_stream.bap_stream.codec_cfg == NULL) { + LOG_ERR("No codec found for the stream"); + return -ENXIO; + } + + if (sampling_rate_hz != NULL) { + ret = le_audio_freq_hz_get( + unicast_server->cap_source_stream.bap_stream.codec_cfg, + sampling_rate_hz); + if (ret) { + LOG_ERR("Invalid sampling frequency: %d", ret); + return -ENXIO; + } + } + + if (bitrate != NULL) { + ret = le_audio_bitrate_get( + unicast_server->cap_source_stream.bap_stream.codec_cfg, bitrate); + if (ret) { + LOG_ERR("Unable to calculate bitrate: %d", ret); + return -ENXIO; + } + } + } + + return 0; +} + +void unicast_client_conn_disconnected(struct bt_conn *conn) +{ + int ret; + struct stream_index idx; + + ret = device_index_get(conn, &idx); + if (ret) { + LOG_WRN("Unknown connection disconnected"); + } else { + disconnected_cleanup(idx); + } +} + +int unicast_client_discover(struct bt_conn *conn, enum unicast_discover_dir dir) +{ + int ret; + struct stream_index idx; + + ret = device_index_vacant_get(conn, &idx); + if (ret) { + return ret; + } + + unicast_servers[idx.lvl1][idx.lvl2][idx.lvl3].device_conn = conn; + + ret = bt_cap_initiator_unicast_discover(conn); + if (ret) { + LOG_WRN("Failed to start cap discover: %d", ret); + return ret; + } + + if (dir & BT_AUDIO_DIR_SOURCE) { + unicast_servers[idx.lvl1][idx.lvl2][idx.lvl3].waiting_for_source_disc = true; + } + + if (dir & BT_AUDIO_DIR_SINK) { + unicast_servers[idx.lvl1][idx.lvl2][idx.lvl3].waiting_for_sink_disc = true; + } + + if (dir == UNICAST_SERVER_BIDIR) { + /* If we need to discover both source and sink, do sink first */ + ret = bt_bap_unicast_client_discover(conn, BT_AUDIO_DIR_SINK); + return ret; + } + + ret = bt_bap_unicast_client_discover(conn, dir); + return ret; +} + +int unicast_client_start(uint8_t cig_index) +{ + int ret; + struct bt_cap_unicast_audio_start_stream_param + cap_stream_params[CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT + + CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT]; + static struct bt_cap_unicast_audio_start_param param; + + if (cig_index >= CONFIG_BT_ISO_MAX_CIG) { + LOG_ERR("Trying to start CIG %d out of %d", cig_index, CONFIG_BT_ISO_MAX_CIG); + return -EINVAL; + } + + param.stream_params = cap_stream_params; + param.count = 0; + param.type = BT_CAP_SET_TYPE_AD_HOC; + + for (int i = 0; i < ARRAY_SIZE(unicast_servers[cig_index][0]); i++) { + uint8_t state; + + ret = le_audio_ep_state_get(unicast_servers[cig_index][0][i].sink_ep, &state); + if (ret) { + continue; + } + + if (state == BT_BAP_EP_STATE_IDLE) { + /* Start all streams in the configured state */ + cap_stream_params[param.count].member.member = + unicast_servers[cig_index][0][i].device_conn; + cap_stream_params[param.count].stream = + &unicast_servers[cig_index][0][i].cap_sink_stream; + cap_stream_params[param.count].ep = + unicast_servers[cig_index][0][i].sink_ep; + cap_stream_params[param.count].codec_cfg = &lc3_preset_sink.codec_cfg; + param.count++; + } else { + LOG_WRN("Found unicast_server[%d] with an endpoint not in IDLE state: %d", + i, state); + } + + ret = le_audio_ep_state_get(unicast_servers[cig_index][0][i].source_ep, &state); + if (ret) { + continue; + } + + if (state == BT_BAP_EP_STATE_IDLE) { + /* Start all streams in the configured state */ + cap_stream_params[param.count].member.member = + unicast_servers[cig_index][0][i].device_conn; + cap_stream_params[param.count].stream = + &unicast_servers[cig_index][0][i].cap_source_stream; + cap_stream_params[param.count].ep = + unicast_servers[cig_index][0][i].source_ep; + cap_stream_params[param.count].codec_cfg = &lc3_preset_source.codec_cfg; + param.count++; + } else { + LOG_WRN("Found unicast_server[%d] with an endpoint not in IDLE state: %d", + i, state); + } + } + + if (param.count > 0) { + ret = bt_cap_initiator_unicast_audio_start(¶m); + if (ret) { + LOG_ERR("Failed to start unicast audio: %d", ret); + return ret; + } + + playing_state = true; + } else { + return -EIO; + } + + return 0; +} + +int unicast_client_stop(uint8_t cig_index) +{ + int ret; + struct bt_cap_stream *streams[CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT + + CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT]; + static struct bt_cap_unicast_audio_stop_param param; + + if (cig_index >= CONFIG_BT_ISO_MAX_CIG) { + LOG_ERR("Trying to stop CIG %d out of %d", cig_index, CONFIG_BT_ISO_MAX_CIG); + return -EINVAL; + } + + param.streams = streams; + param.count = 0; + param.type = BT_CAP_SET_TYPE_AD_HOC; + param.release = true; + + le_audio_event_publish(LE_AUDIO_EVT_NOT_STREAMING, NULL, 0); + + for (int i = 0; i < ARRAY_SIZE(unicast_servers[cig_index][0]); i++) { + if (le_audio_ep_state_check(unicast_servers[cig_index][0][i].sink_ep, + BT_BAP_EP_STATE_STREAMING)) { + /* Stop all sink streams currently in a streaming state */ + streams[param.count++] = &unicast_servers[cig_index][0][i].cap_sink_stream; + } + + if (le_audio_ep_state_check(unicast_servers[cig_index][0][i].source_ep, + BT_BAP_EP_STATE_STREAMING)) { + /* Stop all source streams currently in a streaming state */ + streams[param.count++] = + &unicast_servers[cig_index][0][i].cap_source_stream; + } + } + + if (param.count > 0) { + ret = bt_cap_initiator_unicast_audio_stop(¶m); + if (ret) { + LOG_ERR("Failed to stop unicast audio: %d", ret); + return ret; + } + + playing_state = false; + } else { + return -EIO; + } + + return 0; +} + +int unicast_client_send(uint8_t cig_index, struct le_audio_encoded_audio enc_audio) +{ +#if (CONFIG_BT_AUDIO_TX) + int ret; + uint8_t num_active_streams = 0; + + if (cig_index >= CONFIG_BT_ISO_MAX_CIG) { + LOG_ERR("Trying to send to CIG %d out of %d", cig_index, CONFIG_BT_ISO_MAX_CIG); + return -EINVAL; + } + + struct le_audio_tx_info tx[ARRAY_SIZE(unicast_servers[cig_index][0])]; + + for (int i = 0; i < ARRAY_SIZE(unicast_servers[cig_index][0]); i++) { + /* Skip unicast_servers not in a streaming state */ + if (!le_audio_ep_state_check( + unicast_servers[cig_index][0][i].cap_sink_stream.bap_stream.ep, + BT_BAP_EP_STATE_STREAMING)) { + continue; + } + + /* Set cap stream pointer */ + tx[num_active_streams].cap_stream = + &unicast_servers[cig_index][0][i].cap_sink_stream; + + /* Set index */ + tx[num_active_streams].idx.lvl1 = cig_index; + tx[num_active_streams].idx.lvl2 = 0; + tx[num_active_streams].idx.lvl3 = i; + + /* Set channel location */ + /* Both mono and left unicast_servers will receive left channel */ + tx[num_active_streams].audio_channel = + (unicast_servers[cig_index][0][i].location == BT_AUDIO_LOCATION_FRONT_RIGHT) + ? AUDIO_CH_R + : AUDIO_CH_L; + + num_active_streams++; + } + + if (num_active_streams == 0) { + LOG_WRN("No active streams"); + return -ECANCELED; + } + + ret = bt_le_audio_tx_send(tx, num_active_streams, enc_audio); + if (ret) { + return ret; + } +#endif /* (CONFIG_BT_AUDIO_TX) */ + + return 0; +} + +int unicast_client_disable(uint8_t cig_index) +{ + ARG_UNUSED(cig_index); + + return -ENOTSUP; +} + +int unicast_client_enable(uint8_t cig_index, le_audio_receive_cb recv_cb) +{ + int ret; + static bool initialized; + + if (cig_index >= CONFIG_BT_ISO_MAX_CIG) { + LOG_ERR("Trying to enable CIG %d out of %d", cig_index, CONFIG_BT_ISO_MAX_CIG); + return -EINVAL; + } + + if (initialized) { + LOG_WRN("Already initialized"); + return -EALREADY; + } + + if (recv_cb == NULL) { + LOG_ERR("Receive callback is NULL"); + return -EINVAL; + } + + receive_cb = recv_cb; + + for (int i = 0; i < ARRAY_SIZE(unicast_servers[cig_index][0]); i++) { + bt_cap_stream_ops_register(&unicast_servers[cig_index][0][i].cap_sink_stream, + &stream_ops); + bt_cap_stream_ops_register(&unicast_servers[cig_index][0][i].cap_source_stream, + &stream_ops); + } + + ret = bt_bap_unicast_client_register_cb(&unicast_client_cbs); + if (ret) { + LOG_ERR("Failed to register client callbacks: %d", ret); + return ret; + } + + ret = bt_cap_initiator_register_cb(&cap_cbs); + if (ret) { + LOG_ERR("Failed to register cap callbacks: %d", ret); + return ret; + } + + if (IS_ENABLED(CONFIG_BT_AUDIO_TX)) { + bt_le_audio_tx_init(); + } + + initialized = true; + + return 0; +} diff --git a/src/bluetooth/bt_stream/unicast/unicast_client.h b/src/bluetooth/bt_stream/unicast/unicast_client.h new file mode 100644 index 0000000..834d19c --- /dev/null +++ b/src/bluetooth/bt_stream/unicast/unicast_client.h @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _UNICAST_CLIENT_H_ +#define _UNICAST_CLIENT_H_ + +#include "bt_le_audio_tx.h" + +#include +#include + +enum unicast_discover_dir { + UNICAST_SERVER_SINK = BT_AUDIO_DIR_SINK, + UNICAST_SERVER_SOURCE = BT_AUDIO_DIR_SOURCE, + UNICAST_SERVER_BIDIR = (BT_AUDIO_DIR_SINK | BT_AUDIO_DIR_SOURCE) +}; + +#if CONFIG_BT_BAP_UNICAST_CONFIGURABLE +#define BT_BAP_LC3_UNICAST_PRESET_NRF5340_AUDIO_SINK \ + BT_BAP_LC3_PRESET_CONFIGURABLE(BT_AUDIO_LOCATION_FRONT_LEFT, \ + BT_AUDIO_CONTEXT_TYPE_UNSPECIFIED, \ + CONFIG_BT_AUDIO_BITRATE_UNICAST_SINK) + +#define BT_BAP_LC3_UNICAST_PRESET_NRF5340_AUDIO_SOURCE \ + BT_BAP_LC3_PRESET_CONFIGURABLE(BT_AUDIO_LOCATION_FRONT_LEFT, \ + BT_AUDIO_CONTEXT_TYPE_UNSPECIFIED, \ + CONFIG_BT_AUDIO_BITRATE_UNICAST_SRC) + +#elif CONFIG_BT_BAP_UNICAST_16_2_1 +#define BT_BAP_LC3_UNICAST_PRESET_NRF5340_AUDIO_SINK \ + BT_BAP_LC3_UNICAST_PRESET_16_2_1(BT_AUDIO_LOCATION_FRONT_LEFT, \ + BT_AUDIO_CONTEXT_TYPE_UNSPECIFIED) + +#define BT_BAP_LC3_UNICAST_PRESET_NRF5340_AUDIO_SOURCE \ + BT_BAP_LC3_UNICAST_PRESET_16_2_1(BT_AUDIO_LOCATION_FRONT_LEFT, \ + BT_AUDIO_CONTEXT_TYPE_UNSPECIFIED) + +#elif CONFIG_BT_BAP_UNICAST_24_2_1 +#define BT_BAP_LC3_UNICAST_PRESET_NRF5340_AUDIO_SINK \ + BT_BAP_LC3_UNICAST_PRESET_24_2_1(BT_AUDIO_LOCATION_FRONT_LEFT, \ + BT_AUDIO_CONTEXT_TYPE_UNSPECIFIED) + +#define BT_BAP_LC3_UNICAST_PRESET_NRF5340_AUDIO_SOURCE \ + BT_BAP_LC3_UNICAST_PRESET_24_2_1(BT_AUDIO_LOCATION_FRONT_LEFT, \ + BT_AUDIO_CONTEXT_TYPE_UNSPECIFIED) +#elif CONFIG_BT_BAP_UNICAST_48_4_1 +#define BT_BAP_LC3_UNICAST_PRESET_NRF5340_AUDIO_SINK \ + BT_BAP_LC3_UNICAST_PRESET_48_4_1(BT_AUDIO_LOCATION_ANY, BT_AUDIO_CONTEXT_TYPE_UNSPECIFIED) + +#define BT_BAP_LC3_UNICAST_PRESET_NRF5340_AUDIO_SOURCE \ + BT_BAP_LC3_UNICAST_PRESET_48_4_1(BT_AUDIO_LOCATION_ANY, BT_AUDIO_CONTEXT_TYPE_UNSPECIFIED) +#else +#error Unsupported LC3 codec preset for unicast +#endif /* CONFIG_BT_BAP_UNICAST_CONFIGURABLE */ + +/** + * @brief Get configuration for the audio stream. + * + * @param[in] conn Pointer to the connection to get the configuration for. + * @param[in] dir Direction to get the configuration from. + * @param[out] bitrate Pointer to the bit rate used; can be NULL. + * @param[out] sampling_rate_hz Pointer to the sampling rate used; can be NULL. + * + * @return 0 for success, error otherwise. + */ +int unicast_client_config_get(struct bt_conn *conn, enum bt_audio_dir dir, uint32_t *bitrate, + uint32_t *sampling_rate_hz); + +/** + * @brief Start service discovery for a Bluetooth LE Audio unicast (CIS) server. + * + * @param[in] conn Pointer to the connection. + * @param[in] dir Direction of the stream. + * + * @retval -EALREADY Device has already been discovered. + * @retval -ENOSPC No more room in headset list. + * @retval 0 Success. + */ +int unicast_client_discover(struct bt_conn *conn, enum unicast_discover_dir dir); + +/** + * @brief Handle a disconnected Bluetooth LE Audio unicast (CIS) server. + * + * @param[in] conn Pointer to the connection. + */ +void unicast_client_conn_disconnected(struct bt_conn *conn); + +/** + * @brief Start the Bluetooth LE Audio unicast (CIS) client. + * + * @note Will start both sink and source if present. + * + * @param[in] cig_index Index of the Connected Isochronous Group (CIG) to start. + * + * @return 0 for success, error otherwise. + */ +int unicast_client_start(uint8_t cig_index); + +/** + * @brief Stop the Bluetooth LE Audio unicast (CIS) client. + * + * @note Will stop both sink and source if present. + * + @param[in] cig_index Index of the Connected Isochronous Group (CIG) to stop. + * + * @return 0 for success, error otherwise. + */ +int unicast_client_stop(uint8_t cig_index); + +/** + * @brief Send encoded audio using the Bluetooth LE Audio unicast. + * + * @param[in] cig_index Index of the Connected Isochronous Group (CIG) to send to. + * @param[in] enc_audio Encoded audio struct. + * + * @return 0 for success, error otherwise. + */ +int unicast_client_send(uint8_t cig_index, struct le_audio_encoded_audio enc_audio); + +/** + * @brief Disable the Bluetooth LE Audio unicast (CIS) client. + * + * @param[in] cig_index Index of the Connected Isochronous Group (CIG) to disable. + * + * @return 0 for success, error otherwise. + */ +int unicast_client_disable(uint8_t cig_index); + +/** + * @brief Enable the Bluetooth LE Audio unicast (CIS) client. + * + * @param[in] cig_index Index of the Connected Isochronous Group (CIG) to enable. + * @param[in] recv_cb Callback for handling received data. + * + * @return 0 for success, error otherwise. + */ +int unicast_client_enable(uint8_t cig_index, le_audio_receive_cb recv_cb); + +#endif /* _UNICAST_CLIENT_H_ */ diff --git a/src/bluetooth/bt_stream/unicast/unicast_server.c b/src/bluetooth/bt_stream/unicast/unicast_server.c new file mode 100644 index 0000000..2b90e87 --- /dev/null +++ b/src/bluetooth/bt_stream/unicast/unicast_server.c @@ -0,0 +1,766 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "unicast_server.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "macros_common.h" +#include "zbus_common.h" +#include "bt_mgmt.h" +#include "bt_le_audio_tx.h" +#include "le_audio.h" + +#include +LOG_MODULE_REGISTER(unicast_server, CONFIG_UNICAST_SERVER_LOG_LEVEL); + +ZBUS_CHAN_DEFINE(le_audio_chan, struct le_audio_msg, NULL, NULL, ZBUS_OBSERVERS_EMPTY, + ZBUS_MSG_INIT(0)); + +#define BLE_ISO_LATENCY_MS 10 +#define CSIP_SET_SIZE 2 +enum csip_set_rank { + CSIP_HL_RANK = 1, + CSIP_HR_RANK = 2 +}; + +static le_audio_receive_cb receive_cb; +static struct bt_csip_set_member_svc_inst *csip; + +/* Advertising data for peer connection */ +static uint8_t csip_rsi_adv_data[BT_CSIP_RSI_SIZE]; + +static uint8_t flags_adv_data; + +static uint8_t gap_appear_adv_data[BT_UUID_SIZE_16]; + +static const uint8_t cap_adv_data[] = { + BT_UUID_16_ENCODE(BT_UUID_CAS_VAL), + BT_AUDIO_UNICAST_ANNOUNCEMENT_TARGETED, +}; + +#if defined(CONFIG_BT_AUDIO_RX) +#define AVAILABLE_SINK_CONTEXT (BT_AUDIO_CONTEXT_TYPE_ANY) +#else +#define AVAILABLE_SINK_CONTEXT BT_AUDIO_CONTEXT_TYPE_PROHIBITED +#endif /* CONFIG_BT_AUDIO_RX */ + +static struct bt_cap_stream *cap_tx_streams[CONFIG_BT_ASCS_MAX_ASE_SRC_COUNT]; + +#if defined(CONFIG_BT_AUDIO_TX) +#define AVAILABLE_SOURCE_CONTEXT (BT_AUDIO_CONTEXT_TYPE_ANY) +#else +#define AVAILABLE_SOURCE_CONTEXT BT_AUDIO_CONTEXT_TYPE_PROHIBITED +#endif /* CONFIG_BT_AUDIO_TX */ + +static struct bt_bap_unicast_server_register_param unicast_server_params = { + CONFIG_BT_ASCS_MAX_ASE_SNK_COUNT, CONFIG_BT_ASCS_MAX_ASE_SRC_COUNT}; + +static uint8_t unicast_server_adv_data[] = { + BT_UUID_16_ENCODE(BT_UUID_ASCS_VAL), + BT_AUDIO_UNICAST_ANNOUNCEMENT_TARGETED, + BT_BYTES_LIST_LE16(AVAILABLE_SINK_CONTEXT), + BT_BYTES_LIST_LE16(AVAILABLE_SOURCE_CONTEXT), + 0x00, /* Metadata length */ +}; + +static void le_audio_event_publish(enum le_audio_evt_type event, struct bt_conn *conn, + enum bt_audio_dir dir) +{ + int ret; + struct le_audio_msg msg; + + msg.event = event; + msg.conn = conn; + msg.dir = dir; + + ret = zbus_chan_pub(&le_audio_chan, &msg, LE_AUDIO_ZBUS_EVENT_WAIT_TIME); + ERR_CHK(ret); +} + +/* Callback for locking state change from server side */ +static void csip_lock_changed_cb(struct bt_conn *conn, struct bt_csip_set_member_svc_inst *csip, + bool locked) +{ + LOG_DBG("Client %p %s the lock", (void *)conn, locked ? "locked" : "released"); +} + +/* Callback for SIRK read request from peer side */ +static uint8_t sirk_read_req_cb(struct bt_conn *conn, struct bt_csip_set_member_svc_inst *csip) +{ + /* Accept the request to read the SIRK, but return encrypted SIRK instead of plaintext */ + return BT_CSIP_READ_SIRK_REQ_RSP_ACCEPT_ENC; +} + +static struct bt_csip_set_member_cb csip_callbacks = { + .lock_changed = csip_lock_changed_cb, + .sirk_read_req = sirk_read_req_cb, +}; +struct bt_csip_set_member_register_param csip_param = { + .set_size = CSIP_SET_SIZE, + .lockable = true, + .cb = &csip_callbacks, +}; + +#if defined(CONFIG_BT_AUDIO_RX) +static struct bt_audio_codec_cap lc3_codec_sink = BT_AUDIO_CODEC_CAP_LC3( + BT_AUDIO_CODEC_CAPABILIY_FREQ, + (BT_AUDIO_CODEC_CAP_DURATION_10 | BT_AUDIO_CODEC_CAP_DURATION_PREFER_10), + BT_AUDIO_CODEC_CAP_CHAN_COUNT_SUPPORT(1), LE_AUDIO_SDU_SIZE_OCTETS(CONFIG_LC3_BITRATE_MIN), + LE_AUDIO_SDU_SIZE_OCTETS(CONFIG_LC3_BITRATE_MAX), 1u, AVAILABLE_SINK_CONTEXT); +#endif /* (CONFIG_BT_AUDIO_RX) */ + +#if defined(CONFIG_BT_AUDIO_TX) +static struct bt_audio_codec_cap lc3_codec_source = BT_AUDIO_CODEC_CAP_LC3( + BT_AUDIO_CODEC_CAPABILIY_FREQ, + (BT_AUDIO_CODEC_CAP_DURATION_10 | BT_AUDIO_CODEC_CAP_DURATION_PREFER_10), + BT_AUDIO_CODEC_CAP_CHAN_COUNT_SUPPORT(1), LE_AUDIO_SDU_SIZE_OCTETS(CONFIG_LC3_BITRATE_MIN), + LE_AUDIO_SDU_SIZE_OCTETS(CONFIG_LC3_BITRATE_MAX), 1u, AVAILABLE_SOURCE_CONTEXT); +#endif /* (CONFIG_BT_AUDIO_TX) */ + +static enum bt_audio_dir caps_dirs[] = { +#if defined(CONFIG_BT_AUDIO_RX) + BT_AUDIO_DIR_SINK, +#endif /* CONFIG_BT_AUDIO_RX */ +#if defined(CONFIG_BT_AUDIO_TX) + BT_AUDIO_DIR_SOURCE, +#endif /* (CONFIG_BT_AUDIO_TX) */ +}; + +static const struct bt_bap_qos_cfg_pref qos_pref = BT_BAP_QOS_CFG_PREF( + true, BT_GAP_LE_PHY_2M, CONFIG_BT_AUDIO_RETRANSMITS, BLE_ISO_LATENCY_MS, + CONFIG_AUDIO_MIN_PRES_DLY_US, CONFIG_AUDIO_MAX_PRES_DLY_US, + CONFIG_BT_AUDIO_PREFERRED_MIN_PRES_DLY_US, CONFIG_BT_AUDIO_PREFERRED_MAX_PRES_DLY_US); + +/* clang-format off */ +static struct bt_pacs_cap caps[] = { +#if (CONFIG_BT_AUDIO_RX) + { + .codec_cap = &lc3_codec_sink, + }, +#endif +#if (CONFIG_BT_AUDIO_TX) + { + .codec_cap = &lc3_codec_source, + } +#endif /* (CONFIG_BT_AUDIO_TX) */ +}; +/* clang-format on */ + +static struct bt_cap_stream + cap_audio_streams[CONFIG_BT_ASCS_MAX_ASE_SNK_COUNT + CONFIG_BT_ASCS_MAX_ASE_SRC_COUNT]; + +#if (CONFIG_BT_AUDIO_TX) +BUILD_ASSERT(CONFIG_BT_ASCS_MAX_ASE_SRC_COUNT <= 1, + "CIS headset only supports one source stream for now"); +#endif /* (CONFIG_BT_AUDIO_TX) */ + +static int lc3_config_cb(struct bt_conn *conn, const struct bt_bap_ep *ep, enum bt_audio_dir dir, + const struct bt_audio_codec_cfg *codec, struct bt_bap_stream **stream, + struct bt_bap_qos_cfg_pref *const pref, struct bt_bap_ascs_rsp *rsp) +{ + int ret; + LOG_DBG("LC3 config callback"); + + for (int i = 0; i < ARRAY_SIZE(cap_audio_streams); i++) { + struct bt_cap_stream *cap_audio_stream = &cap_audio_streams[i]; + + if (!cap_audio_stream->bap_stream.conn) { + LOG_DBG("ASE Codec Config stream %p", (void *)cap_audio_stream); + + ret = le_audio_bitrate_check(codec); + if (!ret) { + LOG_WRN("Bitrate check failed"); + return -EINVAL; + } + + ret = le_audio_freq_check(codec); + if (!ret) { + LOG_WRN("Sample rate not supported"); + return -EINVAL; + } + + if (dir == BT_AUDIO_DIR_SINK) { + LOG_DBG("BT_AUDIO_DIR_SINK"); + le_audio_print_codec(codec, dir); + le_audio_event_publish(LE_AUDIO_EVT_CONFIG_RECEIVED, conn, dir); + } +#if (CONFIG_BT_AUDIO_TX) + else if (dir == BT_AUDIO_DIR_SOURCE) { + LOG_DBG("BT_AUDIO_DIR_SOURCE"); + le_audio_print_codec(codec, dir); + le_audio_event_publish(LE_AUDIO_EVT_CONFIG_RECEIVED, conn, dir); + + /* CIS headset only supports one source stream for now */ + cap_tx_streams[0] = cap_audio_stream; + } +#endif /* (CONFIG_BT_AUDIO_TX) */ + else { + LOG_ERR("UNKNOWN DIR"); + return -EINVAL; + } + + *stream = &cap_audio_stream->bap_stream; + *pref = qos_pref; + + return 0; + } + } + + LOG_WRN("No audio_stream available"); + return -ENOMEM; +} + +static int lc3_reconfig_cb(struct bt_bap_stream *stream, enum bt_audio_dir dir, + const struct bt_audio_codec_cfg *codec, + struct bt_bap_qos_cfg_pref *const pref, struct bt_bap_ascs_rsp *rsp) +{ + LOG_DBG("ASE Codec Reconfig: stream %p", (void *)stream); + + return 0; +} + +static int lc3_qos_cb(struct bt_bap_stream *stream, const struct bt_bap_qos_cfg *qos, + struct bt_bap_ascs_rsp *rsp) +{ + enum bt_audio_dir dir; + + dir = le_audio_stream_dir_get(stream); + if (dir <= 0) { + LOG_ERR("Failed to get dir of stream %p", stream); + return -EIO; + } + + le_audio_event_publish(LE_AUDIO_EVT_PRES_DELAY_SET, stream->conn, dir); + + LOG_DBG("QoS: stream %p qos %p", (void *)stream, (void *)qos); + + return 0; +} + +static int lc3_enable_cb(struct bt_bap_stream *stream, const uint8_t *meta, size_t meta_len, + struct bt_bap_ascs_rsp *rsp) +{ + LOG_DBG("Enable: stream %p meta_len %d", (void *)stream, meta_len); + + return 0; +} + +static int lc3_start_cb(struct bt_bap_stream *stream, struct bt_bap_ascs_rsp *rsp) +{ + LOG_DBG("Start stream %p", (void *)stream); + return 0; +} + +static int lc3_metadata_cb(struct bt_bap_stream *stream, const uint8_t *meta, size_t meta_len, + struct bt_bap_ascs_rsp *rsp) +{ + LOG_DBG("Metadata: stream %p meta_len %d", (void *)stream, meta_len); + return 0; +} + +static int lc3_disable_cb(struct bt_bap_stream *stream, struct bt_bap_ascs_rsp *rsp) +{ + enum bt_audio_dir dir; + + dir = le_audio_stream_dir_get(stream); + if (dir <= 0) { + LOG_ERR("Failed to get dir of stream %p", stream); + return -EIO; + } + + LOG_DBG("Disable: stream %p", (void *)stream); + + le_audio_event_publish(LE_AUDIO_EVT_NOT_STREAMING, stream->conn, dir); + + return 0; +} + +static int lc3_stop_cb(struct bt_bap_stream *stream, struct bt_bap_ascs_rsp *rsp) +{ + enum bt_audio_dir dir; + + dir = le_audio_stream_dir_get(stream); + if (dir <= 0) { + LOG_ERR("Failed to get dir of stream %p", stream); + return -EIO; + } + + LOG_DBG("Stop: stream %p", (void *)stream); + + le_audio_event_publish(LE_AUDIO_EVT_NOT_STREAMING, stream->conn, dir); + + return 0; +} + +static int lc3_release_cb(struct bt_bap_stream *stream, struct bt_bap_ascs_rsp *rsp) +{ + enum bt_audio_dir dir; + + dir = le_audio_stream_dir_get(stream); + if (dir <= 0) { + LOG_ERR("Failed to get dir of stream %p", stream); + return -EIO; + } + + LOG_DBG("Release: stream %p", (void *)stream); + + le_audio_event_publish(LE_AUDIO_EVT_NOT_STREAMING, stream->conn, dir); + + return 0; +} + +static const struct bt_bap_unicast_server_cb unicast_server_cb = { + .config = lc3_config_cb, + .reconfig = lc3_reconfig_cb, + .qos = lc3_qos_cb, + .enable = lc3_enable_cb, + .start = lc3_start_cb, + .metadata = lc3_metadata_cb, + .disable = lc3_disable_cb, + .stop = lc3_stop_cb, + .release = lc3_release_cb, +}; + +#if (CONFIG_BT_AUDIO_RX) +static void stream_recv_cb(struct bt_bap_stream *stream, const struct bt_iso_recv_info *info, + struct net_buf *buf) +{ + bool bad_frame = false; + + if (receive_cb == NULL) { + LOG_ERR("The RX callback has not been set"); + return; + } + + if (!(info->flags & BT_ISO_FLAGS_VALID)) { + bad_frame = true; + } + + receive_cb(buf->data, buf->len, bad_frame, info->ts, 0, + bt_audio_codec_cfg_get_octets_per_frame(stream->codec_cfg)); +} +#endif /* (CONFIG_BT_AUDIO_RX) */ + +#if (CONFIG_BT_AUDIO_TX) +static void stream_sent_cb(struct bt_bap_stream *stream) +{ + /* Unicast server/CIS headset only supports one source stream for now */ + struct stream_index idx = { + .lvl1 = 0, + .lvl2 = 0, + .lvl3 = 0, + }; + ERR_CHK(bt_le_audio_tx_stream_sent(idx)); +} +#endif /* (CONFIG_BT_AUDIO_TX) */ + +static void stream_enabled_cb(struct bt_bap_stream *stream) +{ + int ret; + enum bt_audio_dir dir; + + dir = le_audio_stream_dir_get(stream); + if (dir <= 0) { + LOG_ERR("Failed to get dir of stream %p", stream); + return; + } + + LOG_DBG("Stream %p enabled", stream); + + if (dir == BT_AUDIO_DIR_SINK) { + /* Automatically do the receiver start ready operation */ + ret = bt_bap_stream_start(stream); + if (ret != 0) { + LOG_ERR("Failed to start stream: %d", ret); + return; + } + } +} + +static void stream_disabled_cb(struct bt_bap_stream *stream) +{ + LOG_INF("Stream %p disabled", stream); +} + +static void stream_started_cb(struct bt_bap_stream *stream) +{ + enum bt_audio_dir dir; + + dir = le_audio_stream_dir_get(stream); + if (dir <= 0) { + LOG_ERR("Failed to get dir of stream %p", stream); + return; + } + + LOG_INF("Stream %p started", stream); + + if (dir == BT_AUDIO_DIR_SOURCE && IS_ENABLED(CONFIG_BT_AUDIO_TX)) { + struct stream_index idx = { + .lvl1 = 0, + .lvl2 = 0, + .lvl3 = 0, + }; + ERR_CHK(bt_le_audio_tx_stream_started(idx)); + } + + le_audio_event_publish(LE_AUDIO_EVT_STREAMING, stream->conn, dir); +} + +static void stream_stopped_cb(struct bt_bap_stream *stream, uint8_t reason) +{ + enum bt_audio_dir dir; + + dir = le_audio_stream_dir_get(stream); + if (dir <= 0) { + LOG_ERR("Failed to get dir of stream %p", stream); + return; + } + + LOG_DBG("Stream %p stopped. Reason: %d", stream, reason); + + le_audio_event_publish(LE_AUDIO_EVT_NOT_STREAMING, stream->conn, dir); +} + +static void stream_released_cb(struct bt_bap_stream *stream) +{ + /* NOTE: The string below is used by the Nordic CI system */ + LOG_INF("Stream %p released", stream); +} + +static struct bt_bap_stream_ops stream_ops = { +#if (CONFIG_BT_AUDIO_RX) + .recv = stream_recv_cb, +#endif /* (CONFIG_BT_AUDIO_RX) */ +#if (CONFIG_BT_AUDIO_TX) + .sent = stream_sent_cb, +#endif /* (CONFIG_BT_AUDIO_TX) */ + .enabled = stream_enabled_cb, + .disabled = stream_disabled_cb, + .started = stream_started_cb, + .stopped = stream_stopped_cb, + .released = stream_released_cb, +}; + +int unicast_server_config_get(struct bt_conn *conn, enum bt_audio_dir dir, uint32_t *bitrate, + uint32_t *sampling_rate_hz, uint32_t *pres_delay_us) +{ + int ret; + + if (bitrate == NULL && sampling_rate_hz == NULL && pres_delay_us == NULL) { + LOG_ERR("No valid pointers received"); + return -ENXIO; + } + + if (dir == BT_AUDIO_DIR_SINK) { + /* If multiple sink streams exists, they should have the same configurations, + * hence we only check the first one. + */ + if (cap_audio_streams[0].bap_stream.codec_cfg == NULL) { + LOG_ERR("No codec found for the stream"); + + return -ENXIO; + } + + if (sampling_rate_hz != NULL) { + ret = le_audio_freq_hz_get(cap_audio_streams[0].bap_stream.codec_cfg, + sampling_rate_hz); + if (ret) { + LOG_ERR("Invalid sampling frequency: %d", ret); + return -ENXIO; + } + } + + if (bitrate != NULL) { + ret = le_audio_bitrate_get(cap_audio_streams[0].bap_stream.codec_cfg, + bitrate); + if (ret) { + LOG_ERR("Unable to calculate bitrate: %d", ret); + return -ENXIO; + } + } + + if (pres_delay_us != NULL) { + if (cap_audio_streams[0].bap_stream.qos == NULL) { + LOG_ERR("No QoS found for the stream"); + return -ENXIO; + } + + *pres_delay_us = cap_audio_streams[0].bap_stream.qos->pd; + } + } else if (dir == BT_AUDIO_DIR_SOURCE && IS_ENABLED(CONFIG_BT_AUDIO_TX)) { + /* If multiple source streams exists, they should have the same configurations, + * hence we only check the first one. + */ + if (cap_tx_streams[0]->bap_stream.codec_cfg == NULL) { + LOG_ERR("No codec found for the stream"); + return -ENXIO; + } + + if (sampling_rate_hz != NULL) { + ret = le_audio_freq_hz_get(cap_tx_streams[0]->bap_stream.codec_cfg, + sampling_rate_hz); + if (ret) { + LOG_ERR("Invalid sampling frequency: %d", ret); + return -ENXIO; + } + } + + if (bitrate != NULL) { + ret = le_audio_bitrate_get(cap_tx_streams[0]->bap_stream.codec_cfg, + bitrate); + if (ret) { + LOG_ERR("Unable to calculate bitrate: %d", ret); + return -ENXIO; + } + } + + if (pres_delay_us != NULL) { + if (cap_tx_streams[0]->bap_stream.qos == NULL) { + LOG_ERR("No QoS found for the stream"); + return -ENXIO; + } + + *pres_delay_us = cap_tx_streams[0]->bap_stream.qos->pd; + LOG_ERR("pres_delay_us: %d", *pres_delay_us); + } + } + + return 0; +} + +int unicast_server_uuid_populate(struct net_buf_simple *uuid_buf) +{ + if (net_buf_simple_tailroom(uuid_buf) >= (BT_UUID_SIZE_16 * 2)) { + net_buf_simple_add_le16(uuid_buf, BT_UUID_ASCS_VAL); + net_buf_simple_add_le16(uuid_buf, BT_UUID_PACS_VAL); + } else { + LOG_ERR("Not enough space for UUIDS"); + return -ENOMEM; + } + + return 0; +} + +int unicast_server_adv_populate(struct bt_data *adv_buf, uint8_t adv_buf_vacant) +{ + int ret; + uint32_t adv_buf_cnt = 0; + + ret = bt_mgmt_adv_buffer_put(adv_buf, &adv_buf_cnt, adv_buf_vacant, + sizeof(unicast_server_adv_data), BT_DATA_SVC_DATA16, + unicast_server_adv_data); + if (ret) { + return ret; + } + + if (IS_ENABLED(CONFIG_BT_CSIP_SET_MEMBER)) { + ret = bt_mgmt_adv_buffer_put(adv_buf, &adv_buf_cnt, adv_buf_vacant, + sizeof(csip_rsi_adv_data), BT_DATA_CSIS_RSI, + (void *)csip_rsi_adv_data); + if (ret) { + return ret; + } + } + + sys_put_le16(CONFIG_BT_DEVICE_APPEARANCE, &gap_appear_adv_data[0]); + + ret = bt_mgmt_adv_buffer_put(adv_buf, &adv_buf_cnt, adv_buf_vacant, + sizeof(gap_appear_adv_data), BT_DATA_GAP_APPEARANCE, + (void *)gap_appear_adv_data); + if (ret) { + return ret; + } + + flags_adv_data = BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR; + + ret = bt_mgmt_adv_buffer_put(adv_buf, &adv_buf_cnt, adv_buf_vacant, sizeof(uint8_t), + BT_DATA_FLAGS, (void *)&flags_adv_data); + if (ret) { + return ret; + } + + ret = bt_mgmt_adv_buffer_put(adv_buf, &adv_buf_cnt, adv_buf_vacant, + ARRAY_SIZE(cap_adv_data), BT_DATA_SVC_DATA16, + (void *)cap_adv_data); + if (ret) { + return ret; + } + + return adv_buf_cnt; +} + +int unicast_server_send(struct le_audio_encoded_audio enc_audio) +{ +#if (CONFIG_BT_AUDIO_TX) + int ret; + uint8_t num_active_streams = 0; + + struct le_audio_tx_info tx[CONFIG_BT_ASCS_MAX_ASE_SRC_COUNT]; + + for (int i = 0; i < ARRAY_SIZE(cap_tx_streams); i++) { + if (!le_audio_ep_state_check(cap_tx_streams[i]->bap_stream.ep, + BT_BAP_EP_STATE_STREAMING)) { + continue; + } + + /* Set cap stream pointer */ + tx[num_active_streams].cap_stream = cap_tx_streams[i]; + + /* Set index */ + tx[num_active_streams].idx.lvl1 = 0; + tx[num_active_streams].idx.lvl2 = 0; + tx[num_active_streams].idx.lvl3 = i; + + /* Set channel location */ + tx[num_active_streams].audio_channel = AUDIO_MIC; + + num_active_streams++; + } + + ret = bt_le_audio_tx_send(tx, num_active_streams, enc_audio); + if (ret) { + return ret; + } + + return 0; +#else + return -ENOTSUP; +#endif /* (CONFIG_BT_AUDIO_TX) */ +} + +int unicast_server_disable(void) +{ + return -ENOTSUP; +} + +int unicast_server_enable(le_audio_receive_cb recv_cb, enum bt_audio_location location) +{ + int ret; + static bool initialized; + + __ASSERT(strlen(CONFIG_BT_SET_IDENTITY_RESOLVING_KEY) == BT_CSIP_SIRK_SIZE, + "SIRK incorrect size, must be 16 bytes"); + + if (initialized) { + LOG_WRN("Already initialized"); + return -EALREADY; + } + + if (recv_cb == NULL && IS_ENABLED(CONFIG_BT_AUDIO_RX)) { + LOG_ERR("Receive callback is NULL"); + return -EINVAL; + } + + receive_cb = recv_cb; + + bt_bap_unicast_server_register(&unicast_server_params); + bt_bap_unicast_server_register_cb(&unicast_server_cb); + + if (IS_ENABLED(CONFIG_BT_CSIP_SET_MEMBER_TEST_SAMPLE_DATA)) { + LOG_WRN("CSIP test sample data is used, must be changed " + "before production"); + } else { + if (strcmp(CONFIG_BT_SET_IDENTITY_RESOLVING_KEY_DEFAULT, + CONFIG_BT_SET_IDENTITY_RESOLVING_KEY) == 0) { + LOG_WRN("CSIP using the default SIRK, must be changed " + "before production"); + } + + memcpy(csip_param.sirk, CONFIG_BT_SET_IDENTITY_RESOLVING_KEY, BT_CSIP_SIRK_SIZE); + } + + for (int i = 0; i < ARRAY_SIZE(caps); i++) { + ret = bt_pacs_cap_register(caps_dirs[i], &caps[i]); + if (ret) { + LOG_ERR("Capability register failed. Err: %d", ret); + return ret; + } + } + + if (IS_ENABLED(CONFIG_BT_AUDIO_RX)) { + if (location == BT_AUDIO_LOCATION_FRONT_LEFT) { + csip_param.rank = CSIP_HL_RANK; + } else if (location == BT_AUDIO_LOCATION_FRONT_RIGHT) { + csip_param.rank = CSIP_HR_RANK; + } else { + LOG_ERR("Channel not supported"); + return -ECANCELED; + } + + ret = bt_pacs_set_location(BT_AUDIO_DIR_SINK, location); + if (ret) { + LOG_ERR("Location set failed. Err: %d", ret); + return ret; + } + } + + if (IS_ENABLED(CONFIG_BT_AUDIO_TX)) { + bt_le_audio_tx_init(); + + ret = bt_pacs_set_location(BT_AUDIO_DIR_SOURCE, location); + if (ret) { + LOG_ERR("Location set failed. Err: %d", ret); + return ret; + } + } + + ret = bt_pacs_set_supported_contexts(BT_AUDIO_DIR_SINK, AVAILABLE_SINK_CONTEXT); + + if (ret) { + LOG_ERR("Supported context set failed. Err: %d", ret); + return ret; + } + + ret = bt_pacs_set_available_contexts(BT_AUDIO_DIR_SINK, AVAILABLE_SINK_CONTEXT); + if (ret) { + LOG_ERR("Available context set failed. Err: %d", ret); + return ret; + } + + ret = bt_pacs_set_supported_contexts(BT_AUDIO_DIR_SOURCE, AVAILABLE_SOURCE_CONTEXT); + + if (ret) { + LOG_ERR("Supported context set failed. Err: %d", ret); + return ret; + } + + ret = bt_pacs_set_available_contexts(BT_AUDIO_DIR_SOURCE, AVAILABLE_SOURCE_CONTEXT); + if (ret) { + LOG_ERR("Available context set failed. Err: %d", ret); + return ret; + } + + for (int i = 0; i < ARRAY_SIZE(cap_audio_streams); i++) { + bt_cap_stream_ops_register(&cap_audio_streams[i], &stream_ops); + } + + if (IS_ENABLED(CONFIG_BT_CSIP_SET_MEMBER)) { + ret = bt_cap_acceptor_register(&csip_param, &csip); + if (ret) { + LOG_ERR("Failed to register CAP acceptor. Err: %d", ret); + return ret; + } + + ret = bt_csip_set_member_generate_rsi(csip, csip_rsi_adv_data); + if (ret) { + LOG_ERR("Failed to generate RSI. Err: %d", ret); + return ret; + } + } + + initialized = true; + + return 0; +} diff --git a/src/bluetooth/bt_stream/unicast/unicast_server.h b/src/bluetooth/bt_stream/unicast/unicast_server.h new file mode 100644 index 0000000..a6f633c --- /dev/null +++ b/src/bluetooth/bt_stream/unicast/unicast_server.h @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _UNICAST_SERVER_H_ +#define _UNICAST_SERVER_H_ + +#include "bt_le_audio_tx.h" +#include "le_audio.h" + +#include + +/** + * @brief Get configuration for audio stream. + * + * @param[in] conn Pointer to the connection to get the configuration for. + * @param[in] dir Direction to get the configuration from. + * @param[out] bitrate Pointer to the bit rate used; can be NULL. + * @param[out] sampling_rate_hz Pointer to the sampling rate used; can be NULL. + * @param[out] pres_delay_us Pointer to the presentation delay used; can be NULL. Only + * valid for the sink direction. + * + * @retval 0 Operation successful. + * @retval -ENXIO The feature is disabled. + */ +int unicast_server_config_get(struct bt_conn *conn, enum bt_audio_dir dir, uint32_t *bitrate, + uint32_t *sampling_rate_hz, uint32_t *pres_delay_us); + +/** + * @brief Put the UUIDs from this module into the buffer. + * + * @note This partial data is used to build a complete extended advertising packet. + * + * @param[out] uuid_buf Buffer being populated with UUIDs. + * + * @return 0 for success, error otherwise. + */ +int unicast_server_uuid_populate(struct net_buf_simple *uuid_buf); + +/** + * @brief Put the advertising data from this module into the buffer. + * + * @note This partial data is used to build a complete extended advertising packet. + * + * @param[out] adv_buf Buffer being populated with ext adv elements. + * @param[in] adv_buf_vacant Number of vacant elements in @p adv_buf. + * + * @return Negative values for errors or number of elements added to @p adv_buf. + */ +int unicast_server_adv_populate(struct bt_data *adv_buf, uint8_t adv_buf_vacant); + +/** + * @brief Send data from the LE Audio unicast (CIS) server, if configured as a source. + * + * @param[in] enc_audio Encoded audio struct. + * + * @return 0 for success, error otherwise. + */ +int unicast_server_send(struct le_audio_encoded_audio enc_audio); + +/** + * @brief Disable the Bluetooth LE Audio unicast (CIS) server. + * + * @return 0 for success, error otherwise. + */ +int unicast_server_disable(void); + +/** + * @brief Enable the Bluetooth LE Audio unicast (CIS) server. + * + * @param[in] rx_cb Callback for handling received data. + * @param[in] location Location of the unicast_server to be enabled. + * + * @return 0 for success, error otherwise. + */ +int unicast_server_enable(le_audio_receive_cb rx_cb, enum bt_audio_location location); + +#endif /* _UNICAST_SERVER_H_ */ diff --git a/src/drivers/CMakeLists.txt b/src/drivers/CMakeLists.txt new file mode 100644 index 0000000..063f3c4 --- /dev/null +++ b/src/drivers/CMakeLists.txt @@ -0,0 +1,9 @@ +# +# Copyright (c) 2022 Nordic Semiconductor +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +target_sources_ifdef(CONFIG_NRF5340_AUDIO_CS47L63_DRIVER + app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/cs47l63_comm.c +) diff --git a/src/drivers/Kconfig b/src/drivers/Kconfig new file mode 100644 index 0000000..5c01ab8 --- /dev/null +++ b/src/drivers/Kconfig @@ -0,0 +1,32 @@ +# +# Copyright (c) 2022 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +menu "Drivers" + +menuconfig NRF5340_AUDIO_CS47L63_DRIVER + bool "CS47L63 HW codec driver" + select HW_CODEC_CIRRUS_LOGIC + help + Include the driver for the Cirrus Logic CS47L63 hardware codec chip + +if NRF5340_AUDIO_CS47L63_DRIVER + +config CS47L63_THREAD_PRIO + int "Priority for CS47L63 thread" + default 5 + help + This is a preemptible thread + +config CS47L63_STACK_SIZE + int "Stack size for CS47L63" + default 700 + +module = CS47L63 +module-str = cs47l63 +source "subsys/logging/Kconfig.template.log_config" + +endif # NRF5340_AUDIO_CS47L63_DRIVER +endmenu # Drivers diff --git a/src/drivers/cs47l63_comm.c b/src/drivers/cs47l63_comm.c new file mode 100644 index 0000000..3f557f8 --- /dev/null +++ b/src/drivers/cs47l63_comm.c @@ -0,0 +1,432 @@ +/* + * Copyright (c) 2018 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#define DT_DRV_COMPAT cirrus_cs47l63 + +#include "cs47l63_comm.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "bsp_driver_if.h" +#include "cs47l63.h" + +#include +LOG_MODULE_REGISTER(CS47L63, CONFIG_CS47L63_LOG_LEVEL); + +#define CS47L63_DEVID_VAL 0x47A63 +#define PAD_LEN 4 /* Four bytes padding after address */ +/* Delay the processing thread to allow interrupts to settle after boot */ +#define CS47L63_PROCESS_THREAD_DELAY_MS 10 + +static const struct gpio_dt_spec hw_codec_gpio = GPIO_DT_SPEC_INST_GET(0, gpio9_gpios); +static const struct gpio_dt_spec hw_codec_irq = GPIO_DT_SPEC_INST_GET(0, irq_gpios); +static const struct gpio_dt_spec hw_codec_reset = GPIO_DT_SPEC_INST_GET(0, reset_gpios); + +const static struct device *gpio_dev = DEVICE_DT_GET(DT_NODELABEL(gpio0)); + +static const struct spi_dt_spec spi = SPI_DT_SPEC_INST_GET( + 0, SPI_OP_MODE_MASTER | SPI_TRANSFER_MSB | SPI_WORD_SET(8) | SPI_LINES_SINGLE, 0); +static bsp_callback_t bsp_callback; +static void *bsp_callback_arg; + +static struct gpio_callback gpio_cb; + +static struct k_thread cs47l63_data; +static K_THREAD_STACK_DEFINE(cs47l63_stack, CONFIG_CS47L63_STACK_SIZE); + +static K_SEM_DEFINE(sem_cs47l63, 0, 1); + +static struct k_mutex cirrus_reg_oper_mutex; + +static void notification_callback(uint32_t event_flags, void *arg) +{ + LOG_DBG("Notification from CS47L63, flags: %d", event_flags); +} + +/* Locks the mutex and holds the CS pin + * for consecutive transactions + */ +static int spi_mutex_lock(void) +{ + int ret; + + ret = k_mutex_lock(&cirrus_reg_oper_mutex, K_FOREVER); + if (ret) { + LOG_ERR("Failed to lock mutex: %d", ret); + return ret; + } + + /* If operation mode set to HOLD or the SPI_LOCK_ON is set when + * taking the mutex something is wrong + */ + if ((spi.config.operation & SPI_HOLD_ON_CS) || (spi.config.operation & SPI_LOCK_ON)) { + LOG_ERR("SPI_HOLD_ON_CS and SPI_LOCK_ON must be freed before releasing mutex"); + return -EPERM; + } + + return 0; +} + +/* Unlocks mutex and CS pin */ +static int spi_mutex_unlock(void) +{ + int ret; + /* If operation mode still set to HOLD or + * the SPI_LOCK_ON is still set when releasing the mutex + * something is wrong + */ + if ((spi.config.operation & SPI_HOLD_ON_CS) || (spi.config.operation & SPI_LOCK_ON)) { + LOG_ERR("SPI_HOLD_ON_CS and SPI_LOCK_ON must be freed before releasing mutex"); + return -EPERM; + } + + ret = k_mutex_unlock(&cirrus_reg_oper_mutex); + if (ret) { + LOG_ERR("Failed to unlock mutex: %d", ret); + return ret; + } + + return 0; +} + +/* Pin interrupt handler for CS47L63 */ +static void cs47l63_comm_pin_int_handler(const struct device *gpio_port, struct gpio_callback *cb, + uint32_t pins) +{ + __ASSERT(bsp_callback != NULL, "No callback registered"); + + if (pins == BIT(hw_codec_irq.pin)) { + bsp_callback(BSP_STATUS_OK, bsp_callback_arg); + k_sem_give(&sem_cs47l63); + } +} + +static uint32_t cs47l63_comm_reg_read(uint32_t bsp_dev_id, uint8_t *addr_buffer, + uint32_t addr_length, uint8_t *data_buffer, + uint32_t data_length, uint32_t pad_len) +{ + if (pad_len != PAD_LEN) { + LOG_ERR("Trying to pad more than 4 bytes: %d", pad_len); + return BSP_STATUS_FAIL; + } + + int ret; + + uint8_t pad_buffer[PAD_LEN] = { 0 }; + + struct spi_buf_set rx; + struct spi_buf rx_buf[] = { { .buf = addr_buffer, .len = addr_length }, + { .buf = pad_buffer, .len = pad_len }, + { .buf = data_buffer, .len = data_length } }; + + rx.buffers = rx_buf; + rx.count = ARRAY_SIZE(rx_buf); + + ret = spi_mutex_lock(); + if (ret) { + return BSP_STATUS_FAIL; + } + + ret = spi_transceive_dt(&spi, &rx, &rx); + if (ret) { + LOG_ERR("Failed transceive operation: %d", ret); + return BSP_STATUS_FAIL; + } + + ret = spi_mutex_unlock(); + if (ret) { + return BSP_STATUS_FAIL; + } + + return BSP_STATUS_OK; +} + +static uint32_t cs47l63_comm_reg_write(uint32_t bsp_dev_id, uint8_t *addr_buffer, + uint32_t addr_length, uint8_t *data_buffer, + uint32_t data_length, uint32_t pad_len) +{ + if (pad_len != PAD_LEN) { + LOG_ERR("Trying to pad more than 4 bytes: %d", pad_len); + return BSP_STATUS_FAIL; + } + + int ret; + + uint8_t pad_buffer[PAD_LEN] = { 0 }; + + struct spi_buf_set tx; + struct spi_buf tx_buf[] = { { .buf = addr_buffer, .len = addr_length }, + { .buf = pad_buffer, .len = pad_len }, + { .buf = data_buffer, .len = data_length } }; + + tx.buffers = tx_buf; + tx.count = ARRAY_SIZE(tx_buf); + + ret = spi_mutex_lock(); + if (ret) { + return BSP_STATUS_FAIL; + } + + ret = spi_write_dt(&spi, &tx); + if (ret) { + LOG_ERR("SPI failed to write: %d", ret); + return BSP_STATUS_FAIL; + } + + ret = spi_mutex_unlock(); + if (ret) { + return BSP_STATUS_FAIL; + } + + return BSP_STATUS_OK; +} + +static uint32_t cs47l63_comm_gpio_set(uint32_t gpio_id, uint8_t gpio_state) +{ + int ret; + + ret = gpio_pin_set_raw(gpio_dev, gpio_id, gpio_state); + + if (ret) { + LOG_ERR("Failed to set gpio state, ret: %d", ret); + return BSP_STATUS_FAIL; + } + + return BSP_STATUS_OK; +} + +/* Register callback for pin interrupt from CS47L63 */ +static uint32_t cs47l63_comm_gpio_cb_register(uint32_t gpio_id, bsp_callback_t cb, void *cb_arg) +{ + int ret; + + bsp_callback = cb; + bsp_callback_arg = cb_arg; + + gpio_init_callback(&gpio_cb, cs47l63_comm_pin_int_handler, BIT(gpio_id)); + + ret = gpio_add_callback(gpio_dev, &gpio_cb); + if (ret) { + return BSP_STATUS_FAIL; + } + + ret = gpio_pin_interrupt_configure(gpio_dev, gpio_id, GPIO_INT_EDGE_TO_INACTIVE); + if (ret) { + return BSP_STATUS_FAIL; + } + + return BSP_STATUS_OK; +} + +static uint32_t cs47l63_comm_timer_set(uint32_t duration_ms, bsp_callback_t cb, void *cb_arg) +{ + if (cb != NULL || cb_arg != NULL) { + LOG_ERR("Timer with callback not supported"); + return BSP_STATUS_FAIL; + } + + k_msleep(duration_ms); + + return BSP_STATUS_OK; +} + +static uint32_t cs47l63_comm_set_supply(uint32_t supply_id, uint8_t supply_state) +{ + LOG_DBG("Tried to set supply, not supported"); + /* OK is returned in order to make reset function work */ + return BSP_STATUS_OK; +} + +static uint32_t cs47l63_comm_i2c_reset(uint32_t bsp_dev_id, bool *was_i2c_busy) +{ + LOG_ERR("Tried to reset I2C, not supported"); + return BSP_STATUS_FAIL; +} + +static uint32_t cs47l63_comm_i2c_read_repeated_start(uint32_t bsp_dev_id, uint8_t *write_buffer, + uint32_t write_length, uint8_t *read_buffer, + uint32_t read_length, bsp_callback_t cb, + void *cb_arg) +{ + LOG_ERR("Tried to read repeated start I2C, not supported"); + return BSP_STATUS_FAIL; +} + +static uint32_t cs47l63_comm_i2c_write(uint32_t bsp_dev_id, uint8_t *write_buffer, + uint32_t write_length, bsp_callback_t cb, void *cb_arg) +{ + LOG_ERR("Tried writing to I2C, not supported"); + return BSP_STATUS_FAIL; +} + +static uint32_t cs47l63_comm_i2c_db_write(uint32_t bsp_dev_id, uint8_t *write_buffer_0, + uint32_t write_length_0, uint8_t *write_buffer_1, + uint32_t write_length_1, bsp_callback_t cb, void *cb_arg) +{ + LOG_ERR("Tried to write double buffered I2C, not supported"); + return BSP_STATUS_FAIL; +} + +static uint32_t cs47l63_comm_enable_irq(void) +{ + LOG_ERR("Tried to enable irq, not supported"); + return BSP_STATUS_FAIL; +} + +static uint32_t cs47l63_comm_disable_irq(void) +{ + LOG_ERR("Tried to disable irq, not supported"); + return BSP_STATUS_FAIL; +} + +static uint32_t cs47l63_comm_spi_throttle_speed(uint32_t speed_hz) +{ + LOG_ERR("Tried to throttle SPI speed, not supported"); + return BSP_STATUS_FAIL; +} + +static uint32_t cs47l63_comm_spi_restore_speed(void) +{ + LOG_ERR("Tried to restore SPI speed, not supported"); + return BSP_STATUS_FAIL; +} + +/* Thread to process events from CS47L63 */ +static void cs47l63_comm_thread(void *cs47l63_driver, void *dummy2, void *dummy3) +{ + int ret; + + while (1) { + k_sem_take(&sem_cs47l63, K_FOREVER); + ret = cs47l63_process((cs47l63_t *)cs47l63_driver); + if (ret) { + LOG_ERR("CS47L63 failed to process event"); + } + } +} + +static cs47l63_bsp_config_t bsp_config = { .bsp_reset_gpio_id = hw_codec_reset.pin, + .bsp_int_gpio_id = hw_codec_irq.pin, + .cp_config.bus_type = CS47L63_BUS_TYPE_SPI, + .cp_config.spi_pad_len = 4, + .notification_cb = ¬ification_callback, + .notification_cb_arg = NULL }; + +int cs47l63_comm_init(cs47l63_t *cs47l63_driver) +{ + int ret; + + cs47l63_config_t cs47l63_config; + + memset(&cs47l63_config, 0, sizeof(cs47l63_config_t)); + + k_mutex_init(&cirrus_reg_oper_mutex); + + if (!spi_is_ready_dt(&spi)) { + LOG_ERR("CS47L63 is not ready!"); + return -ENXIO; + } + + if (!gpio_is_ready_dt(&hw_codec_gpio)) { + LOG_ERR("GPIO is not ready!"); + return -ENXIO; + } + + ret = gpio_pin_configure_dt(&hw_codec_gpio, GPIO_INPUT); + if (ret) { + return ret; + } + + if (!gpio_is_ready_dt(&hw_codec_irq)) { + LOG_ERR("GPIO is not ready!"); + return -ENXIO; + } + + ret = gpio_pin_configure_dt(&hw_codec_irq, GPIO_INPUT); + if (ret) { + return ret; + } + + if (!gpio_is_ready_dt(&hw_codec_reset)) { + LOG_ERR("GPIO is not ready!"); + return -ENXIO; + } + + ret = gpio_pin_configure_dt(&hw_codec_reset, GPIO_OUTPUT); + if (ret) { + return ret; + } + + /* Start thread to handle events from CS47L63 */ + (void)k_thread_create(&cs47l63_data, cs47l63_stack, CONFIG_CS47L63_STACK_SIZE, + (k_thread_entry_t)cs47l63_comm_thread, (void *)cs47l63_driver, NULL, + NULL, K_PRIO_PREEMPT(CONFIG_CS47L63_THREAD_PRIO), 0, + K_MSEC(CS47L63_PROCESS_THREAD_DELAY_MS)); + + ret = k_thread_name_set(&cs47l63_data, "CS47L63"); + if (ret) { + return ret; + } + + /* Initialize CS47L63 drivers */ + ret = cs47l63_initialize(cs47l63_driver); + if (ret != CS47L63_STATUS_OK) { + LOG_ERR("Failed to initialize CS47L63"); + return -ENXIO; + } + + cs47l63_config.bsp_config = bsp_config; + + cs47l63_config.syscfg_regs = cs47l63_syscfg_regs; + cs47l63_config.syscfg_regs_total = CS47L63_SYSCFG_REGS_TOTAL; + + ret = cs47l63_configure(cs47l63_driver, &cs47l63_config); + if (ret != CS47L63_STATUS_OK) { + LOG_ERR("Failed to configure CS47L63"); + return -ENXIO; + } + + /* Will pin reset the device and wait until boot done */ + ret = cs47l63_reset(cs47l63_driver); + if (ret != CS47L63_STATUS_OK) { + LOG_ERR("Failed to reset CS47L63"); + return -ENXIO; + } + + if (cs47l63_driver->devid != CS47L63_DEVID_VAL) { + LOG_ERR("Wrong device id: 0x%02x, should be 0x%02x", cs47l63_driver->devid, + CS47L63_DEVID_VAL); + return -EIO; + } + + return 0; +} + +static bsp_driver_if_t bsp_driver_if_s = { .set_gpio = &cs47l63_comm_gpio_set, + .register_gpio_cb = &cs47l63_comm_gpio_cb_register, + .set_timer = &cs47l63_comm_timer_set, + .spi_read = &cs47l63_comm_reg_read, + .spi_write = &cs47l63_comm_reg_write, + + /* Functions not supported */ + .set_supply = &cs47l63_comm_set_supply, + .i2c_read_repeated_start = + &cs47l63_comm_i2c_read_repeated_start, + .i2c_write = &cs47l63_comm_i2c_write, + .i2c_db_write = &cs47l63_comm_i2c_db_write, + .i2c_reset = &cs47l63_comm_i2c_reset, + .enable_irq = &cs47l63_comm_enable_irq, + .disable_irq = &cs47l63_comm_disable_irq, + .spi_throttle_speed = &cs47l63_comm_spi_throttle_speed, + .spi_restore_speed = &cs47l63_comm_spi_restore_speed }; + +bsp_driver_if_t *bsp_driver_if_g = &bsp_driver_if_s; diff --git a/src/drivers/cs47l63_comm.h b/src/drivers/cs47l63_comm.h new file mode 100644 index 0000000..dda34f5 --- /dev/null +++ b/src/drivers/cs47l63_comm.h @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2018 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _CS47L63_COMM_H_ +#define _CS47L63_COMM_H_ + +#include +#include "cs47l63.h" + +/**@brief Initialize the CS47L63 + * + * @param driver Pointer to CS47L63 driver + * + * @return 0 on success. + */ +int cs47l63_comm_init(cs47l63_t *driver); + +#endif /* _CS47L63_COMM_H_ */ diff --git a/src/drivers/cs47l63_reg_conf.h b/src/drivers/cs47l63_reg_conf.h new file mode 100644 index 0000000..1dc8080 --- /dev/null +++ b/src/drivers/cs47l63_reg_conf.h @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2018 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _CS47L63_REG_CONF_H_ +#define _CS47L63_REG_CONF_H_ + +#include "cs47l63_spec.h" + +/* Magic value to signal a sleep instead of register address. + * This can be used e.g. after resets where time is needed before + * a device is ready. + * Note that this is a busy wait, and should only be used sparingly where fast + * execution is not critical. + * + * 0001 is used as the reg addr. In case of a fault, this reg is read only. + */ +#define SPI_BUSY_WAIT 0x0001 +#define SPI_BUSY_WAIT_US_1000 1000 +#define SPI_BUSY_WAIT_US_3000 3000 + +#define MAX_VOLUME_REG_VAL 0x80 +#define MAX_VOLUME_DB 64 +#define OUT_VOLUME_DEFAULT 0x62 +#define VOLUME_UPDATE_BIT (1 << 9) + +#define CS47L63_SOFT_RESET_VAL 0x5A000000 + +/* clang-format off */ +/* Set up clocks */ +const uint32_t clock_configuration[][2] = { + { CS47L63_SAMPLE_RATE3, 0x0012 }, + { CS47L63_SAMPLE_RATE2, 0x0002 }, + { CS47L63_SAMPLE_RATE1, 0x0003 }, + { CS47L63_SYSTEM_CLOCK1, 0x034C }, + { CS47L63_ASYNC_CLOCK1, 0x034C }, + { CS47L63_FLL1_CONTROL2, 0x88200008 }, + { CS47L63_FLL1_CONTROL3, 0x10000 }, + { CS47L63_FLL1_GPIO_CLOCK, 0x0005 }, + { CS47L63_FLL1_CONTROL1, 0x0001 }, +}; +/* clang-format on */ + +/* Set up GPIOs */ +const uint32_t GPIO_configuration[][2] = { + { CS47L63_GPIO6_CTRL1, 0x61000001 }, + { CS47L63_GPIO7_CTRL1, 0x61000001 }, + { CS47L63_GPIO8_CTRL1, 0x61000001 }, + + /* Enable CODEC LED */ + { CS47L63_GPIO10_CTRL1, 0x41008001 }, +}; + +const uint32_t pdm_mic_enable_configure[][2] = { + /* Set MICBIASes */ + { CS47L63_LDO2_CTRL1, 0x0005 }, + { CS47L63_MICBIAS_CTRL1, 0x00EC }, + { CS47L63_MICBIAS_CTRL5, 0x0272 }, + + /* Enable IN1L */ + { CS47L63_INPUT_CONTROL, 0x000F }, + + /* Enable PDM mic as digital input */ + { CS47L63_INPUT1_CONTROL1, 0x50021 }, + + /* Un-mute and set gain to 0dB */ + { CS47L63_IN1L_CONTROL2, 0x800080 }, + { CS47L63_IN1R_CONTROL2, 0x800080 }, + + /* Volume Update */ + { CS47L63_INPUT_CONTROL3, 0x20000000 }, + + /* Send PDM MIC to I2S Tx */ + { CS47L63_ASP1TX1_INPUT1, 0x800010 }, + { CS47L63_ASP1TX2_INPUT1, 0x800011 }, +}; + +/* Set up input */ +const uint32_t line_in_enable[][2] = { + /* Select LINE-IN as analog input */ + { CS47L63_INPUT2_CONTROL1, 0x50020 }, + + /* Set IN2L and IN2R to single-ended */ + { CS47L63_IN2L_CONTROL1, 0x10000000 }, + { CS47L63_IN2R_CONTROL1, 0x10000000 }, + + /* Un-mute and set gain to 0dB */ + { CS47L63_IN2L_CONTROL2, 0x800080 }, + { CS47L63_IN2R_CONTROL2, 0x800080 }, + + /* Enable IN2L and IN2R */ + { CS47L63_INPUT_CONTROL, 0x000F }, + + /* Volume Update */ + { CS47L63_INPUT_CONTROL3, 0x20000000 }, + + /* Route IN2L and IN2R to I2S */ + { CS47L63_ASP1TX1_INPUT1, 0x800012 }, + { CS47L63_ASP1TX2_INPUT1, 0x800013 }, +}; + +/* Set up output */ +const uint32_t output_enable[][2] = { + { CS47L63_OUTPUT_ENABLE_1, 0x0002 }, + { CS47L63_OUT1L_INPUT1, 0x800020 }, + { CS47L63_OUT1L_INPUT2, 0x800021 }, +}; + +const uint32_t output_disable[][2] = { + { CS47L63_OUTPUT_ENABLE_1, 0x00 }, +}; + +/* Set up ASP1 (I2S) */ +const uint32_t asp1_enable[][2] = { + /* Enable ASP1 GPIOs */ + { CS47L63_GPIO1_CTRL1, 0x61000000 }, + { CS47L63_GPIO2_CTRL1, 0xE1000000 }, + { CS47L63_GPIO3_CTRL1, 0xE1000000 }, + { CS47L63_GPIO4_CTRL1, 0xE1000000 }, + { CS47L63_GPIO5_CTRL1, 0x61000001 }, + +/* Set correct sample rate */ +#if CONFIG_AUDIO_SAMPLE_RATE_16000_HZ + { CS47L63_SAMPLE_RATE1, 0x000000012 }, +#elif CONFIG_AUDIO_SAMPLE_RATE_24000_HZ + { CS47L63_SAMPLE_RATE1, 0x000000002 }, +#elif CONFIG_AUDIO_SAMPLE_RATE_48000_HZ + { CS47L63_SAMPLE_RATE1, 0x000000003 }, +#endif + /* Disable unused sample rates */ + { CS47L63_SAMPLE_RATE2, 0 }, + { CS47L63_SAMPLE_RATE3, 0 }, + { CS47L63_SAMPLE_RATE4, 0 }, + + /* Set ASP1 in slave mode and 16 bit per channel */ + { CS47L63_ASP1_CONTROL2, 0x10100200 }, + { CS47L63_ASP1_CONTROL3, 0x0000 }, + { CS47L63_ASP1_DATA_CONTROL1, 0x0020 }, + { CS47L63_ASP1_DATA_CONTROL5, 0x0020 }, + { CS47L63_ASP1_ENABLES1, 0x30003 }, +}; + +const uint32_t FLL_toggle[][2] = { + { CS47L63_FLL1_CONTROL1, 0x0000 }, + { SPI_BUSY_WAIT, SPI_BUSY_WAIT_US_1000 }, + { CS47L63_FLL1_CONTROL1, 0x0001 }, +}; + +const uint32_t soft_reset[][2] = { + { CS47L63_SFT_RESET, CS47L63_SOFT_RESET_VAL }, + { SPI_BUSY_WAIT, SPI_BUSY_WAIT_US_3000 }, +}; + +#endif /* _CS47L63_REG_CONF_H_ */ diff --git a/src/modules/CMakeLists.txt b/src/modules/CMakeLists.txt new file mode 100644 index 0000000..bb0ce3a --- /dev/null +++ b/src/modules/CMakeLists.txt @@ -0,0 +1,24 @@ +# +# Copyright (c) 2022 Nordic Semiconductor +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +target_sources(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/audio_i2s.c) +target_sources(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/audio_usb.c) +target_sources(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/button_handler.c) +target_sources(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/led.c) +target_sources(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/audio_sync_timer.c) + +target_sources_ifdef(CONFIG_NRF5340_AUDIO_CS47L63_DRIVER app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/hw_codec.c) +target_sources_ifdef(CONFIG_NRF5340_AUDIO_POWER_MEASUREMENT app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/power_meas.c) +target_sources_ifdef(CONFIG_NRF5340_AUDIO_SD_CARD_MODULE app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/sd_card.c) +target_sources_ifdef(CONFIG_NRF5340_AUDIO_SD_CARD_LC3_FILE app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/lc3_file.c) +target_sources_ifdef(CONFIG_SD_CARD_PLAYBACK app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/sd_card_playback.c) +target_sources_ifdef(CONFIG_NRF5340_AUDIO_SD_CARD_LC3_STREAMER app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/lc3_streamer.c) diff --git a/src/modules/Kconfig b/src/modules/Kconfig new file mode 100644 index 0000000..434f13e --- /dev/null +++ b/src/modules/Kconfig @@ -0,0 +1,233 @@ +# +# Copyright (c) 2022 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +rsource "Kconfig.defaults" + +menu "Modules" + +config BUTTON_DEBOUNCE_MS + int "Button debounce time in ms" + default 50 + +config AUDIO_SYNC_TIMER_USES_RTC + bool + default y + select NRFX_RTC0 + +#----------------------------------------------------------------------------# +menuconfig NRF5340_AUDIO_POWER_MEASUREMENT + bool "Power measurement" + help + Include the power measurement driver for the nRF5340 Audio DK + +if NRF5340_AUDIO_POWER_MEASUREMENT + +config POWER_MEAS_INTERVAL_MS + int "Power measurement interval in milliseconds" + default 8500 + help + Power measurement runs continuously, this option just establishes the + results polling period. Note that this value needs to be >= the configured + sampling interval on the current sensor. When below, repeated measurements + will be observed. + +config POWER_MEAS_START_ON_BOOT + bool "Start power measurements for all rails on boot" + help + This option will automatically start and periodically print + the voltage, current consumption, and power usage for the + following rails: VBAT, VDD1_CODEC, VDD2_CODEC, and VDD2_NRF + +endif # NRF5340_AUDIO_POWER_MEASUREMENT + +#----------------------------------------------------------------------------# +menu "I2S" + +config I2S_LRCK_FREQ_HZ + int + default AUDIO_SAMPLE_RATE_HZ + help + The sample rate of I2S. For now this is tied directly to + AUDIO_SAMPLE_RATE_HZ + Note that this setting is only valid in I2S master mode. + +config I2S_CH_NUM + int + default 2 + help + The I2S driver itself supports both mono and stereo. + Parts of the implementation are configured for only stereo. + +endmenu # I2S + +#----------------------------------------------------------------------------# +menu "Log levels" + +module = MODULE_AUDIO_USB +module-str = module-audio-usb +source "subsys/logging/Kconfig.template.log_config" + +module = MODULE_BUTTON_HANDLER +module-str = module-button-handler +source "subsys/logging/Kconfig.template.log_config" + +module = MODULE_HW_CODEC +module-str = module-hw-codec +source "subsys/logging/Kconfig.template.log_config" + +module = MODULE_LED +module-str = module-led +source "subsys/logging/Kconfig.template.log_config" + +module = MODULE_POWER +module-str = module-power +source "subsys/logging/Kconfig.template.log_config" + +module = MODULE_NRF5340_AUDIO_DK +module-str = module-nrf5340-audio_dk +source "subsys/logging/Kconfig.template.log_config" + +endmenu # Log levels + +#----------------------------------------------------------------------------# +menu "Thread priorities" + +config POWER_MEAS_THREAD_PRIO + int "Priority for power measurement thread" + default 6 + help + This is a preemptible thread. + +config BUTTON_PUBLISH_THREAD_PRIO + int "Priority for button publish thread" + default 5 + help + This is a preemptible thread. + This thread will publish button events to zbus. + +config VOLUME_MSG_SUB_THREAD_PRIO + int "Priority for volume message subscribe thread" + default 5 + help + This is a preemptible thread. + This thread will subscribe to volume events from zbus. + +endmenu # Thread priorities + +#----------------------------------------------------------------------------# +menu "Stack sizes" + +config POWER_MEAS_STACK_SIZE + int "Stack size for power measurement thread" + default 1152 + +config BUTTON_PUBLISH_STACK_SIZE + int "Stack size for button publish thread" + default 450 + +config VOLUME_MSG_SUB_STACK_SIZE + int "Stack size for volume message subscribe thread" + default 768 + +endmenu # Stack sizes + +#----------------------------------------------------------------------------# +menu "Zbus" + +config VOLUME_MSG_SUB_QUEUE_SIZE + int "Queue size for volume message subscriber" + default 4 + +endmenu # Zbus + +#----------------------------------------------------------------------------# +config NRF5340_AUDIO_SD_CARD_MODULE + bool "Audio SD Card Module" + help + Include the audio SD card module to support streaming audio files from an SD card. + +if NRF5340_AUDIO_SD_CARD_MODULE + +module = MODULE_SD_CARD +module-str = module-sd-card +source "subsys/logging/Kconfig.template.log_config" + +endif # NRF5340_AUDIO_SD_CARD_MODULE + +config NRF5340_AUDIO_SD_CARD_LC3_FILE + bool "SD card LC3 file support" + depends on NRF5340_AUDIO_SD_CARD_MODULE + default n + help + Include support for reading and parsing LC3 files from an SD card. + +if NRF5340_AUDIO_SD_CARD_LC3_FILE + +module = MODULE_SD_CARD_LC3_FILE +module-str = module-sd-card-lc3-file +source "subsys/logging/Kconfig.template.log_config" + +endif # NRF5340_AUDIO_SD_CARD_LC3_FILE + +menuconfig SD_CARD_PLAYBACK + bool "Enable playback from SD card" + depends on NRF5340_AUDIO_SD_CARD_MODULE + select EXPERIMENTAL + default n + select RING_BUFFER + +if SD_CARD_PLAYBACK + +config SD_CARD_PLAYBACK_STACK_SIZE + int "Stack size for the SD card playback thread" + default 4096 + +config SD_CARD_PLAYBACK_RING_BUF_SIZE + int "Size of the ring buffer for the SD card playback module" + default 960 + +config SD_CARD_PLAYBACK_THREAD_PRIO + int "Priority for the SD card playback thread" + default 7 + + +module = MODULE_SD_CARD_PLAYBACK +module-str = module-sd-card-playback +source "subsys/logging/Kconfig.template.log_config" + +endif # SD_CARD_PLAYBACK + +menuconfig NRF5340_AUDIO_SD_CARD_LC3_STREAMER + bool "Audio SD Card LC3 Streamer" + depends on NRF5340_AUDIO_SD_CARD_LC3_FILE + select EXPERIMENTAL + +if NRF5340_AUDIO_SD_CARD_LC3_STREAMER + +config SD_CARD_LC3_STREAMER_STACK_SIZE + int "Stack size for the SD card LC3 streamer thread" + default 1500 + +config SD_CARD_LC3_STREAMER_THREAD_PRIO + int "Priority for the SD card LC3 streamer thread" + default 4 + +config SD_CARD_LC3_STREAMER_MAX_NUM_STREAMS + int "Maximum number of concurrent LC3 streams" + default 5 + range 0 255 + +config SD_CARD_LC3_STREAMER_MAX_FRAME_SIZE + int "Maximum frame size for LC3 streams" + default 251 + +module = MODULE_SD_CARD_LC3_STREAMER +module-str = module-sd-card-lc3-streamer +source "subsys/logging/Kconfig.template.log_config" + +endif # NRF5340_AUDIO_SD_CARD_LC3_STREAMER + +endmenu # Modules diff --git a/src/modules/Kconfig.defaults b/src/modules/Kconfig.defaults new file mode 100644 index 0000000..18a7a83 --- /dev/null +++ b/src/modules/Kconfig.defaults @@ -0,0 +1,45 @@ +# +# Copyright (c) 2022 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +# GATEWAY +if AUDIO_DEV = 2 + +if AUDIO_SOURCE_USB + +config USB_DEVICE_STACK + default y + +# Net buf options needed for USB stack +config NET_BUF + default y + +config USB_DEVICE_AUDIO + default y + +## TODO: Nordic VID, change accordingly +config USB_DEVICE_VID + default 0x1915 + +## TODO: Change for final product +config USB_DEVICE_PID + default 0x530A + +config USB_DEVICE_PRODUCT + default "nRF5340 USB Audio" + +config USB_DEVICE_MANUFACTURER + default "Nordic Semiconductor AS" + +## Avoid redundant warnings for endpoint setting in USB stack +config USB_DRIVER_LOG_LEVEL + default 1 + +config USB_DEVICE_LOG_LEVEL + default 1 + +endif # AUDIO_SOURCE_USB + +endif # AUDIO_DEV = 2 (GATEWAY) diff --git a/src/modules/audio_i2s.c b/src/modules/audio_i2s.c new file mode 100644 index 0000000..6dfb61a --- /dev/null +++ b/src/modules/audio_i2s.c @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2021, PACKETCRAFT, INC. + * + * SPDX-License-Identifier: LicenseRef-PCFT + */ + +#include "audio_i2s.h" + +#include +#include +#include +#include +#include + +#include "audio_sync_timer.h" + +#define I2S_NL DT_NODELABEL(i2s0) + +enum audio_i2s_state { + AUDIO_I2S_STATE_UNINIT, + AUDIO_I2S_STATE_IDLE, + AUDIO_I2S_STATE_STARTED, +}; + +static enum audio_i2s_state state = AUDIO_I2S_STATE_UNINIT; + +PINCTRL_DT_DEFINE(I2S_NL); + +#if CONFIG_AUDIO_SAMPLE_RATE_16000_HZ +#define CONFIG_AUDIO_RATIO NRF_I2S_RATIO_384X +#elif CONFIG_AUDIO_SAMPLE_RATE_24000_HZ +#define CONFIG_AUDIO_RATIO NRF_I2S_RATIO_256X +#elif CONFIG_AUDIO_SAMPLE_RATE_48000_HZ +#define CONFIG_AUDIO_RATIO NRF_I2S_RATIO_128X +#else +#error "Current AUDIO_SAMPLE_RATE_HZ setting not supported" +#endif + +static nrfx_i2s_t i2s_inst = NRFX_I2S_INSTANCE(0); + +static nrfx_i2s_config_t cfg = { + /* Pins are configured by pinctrl. */ + .skip_gpio_cfg = true, + .skip_psel_cfg = true, + .irq_priority = DT_IRQ(I2S_NL, priority), + .mode = NRF_I2S_MODE_MASTER, + .format = NRF_I2S_FORMAT_I2S, + .alignment = NRF_I2S_ALIGN_LEFT, + .ratio = CONFIG_AUDIO_RATIO, + .mck_setup = 0x66666000, +#if (CONFIG_AUDIO_BIT_DEPTH_16) + .sample_width = NRF_I2S_SWIDTH_16BIT, +#elif (CONFIG_AUDIO_BIT_DEPTH_32) + .sample_width = NRF_I2S_SWIDTH_32BIT, +#else +#error Invalid bit depth selected +#endif /* (CONFIG_AUDIO_BIT_DEPTH_16) */ + .channels = NRF_I2S_CHANNELS_STEREO, + .clksrc = NRF_I2S_CLKSRC_ACLK, + .enable_bypass = false, +}; + +static i2s_blk_comp_callback_t i2s_blk_comp_callback; + +static void i2s_comp_handler(nrfx_i2s_buffers_t const *released_bufs, uint32_t status) +{ + if ((status == NRFX_I2S_STATUS_NEXT_BUFFERS_NEEDED) && released_bufs && + i2s_blk_comp_callback && (released_bufs->p_rx_buffer || released_bufs->p_tx_buffer)) { + i2s_blk_comp_callback(audio_sync_timer_capture_get(), released_bufs->p_rx_buffer, + released_bufs->p_tx_buffer); + } +} + +void audio_i2s_set_next_buf(const uint8_t *tx_buf, uint32_t *rx_buf) +{ + __ASSERT_NO_MSG(state == AUDIO_I2S_STATE_STARTED); + if (IS_ENABLED(CONFIG_STREAM_BIDIRECTIONAL) || (CONFIG_AUDIO_DEV == GATEWAY)) { + __ASSERT_NO_MSG(rx_buf != NULL); + } + + if (IS_ENABLED(CONFIG_STREAM_BIDIRECTIONAL) || (CONFIG_AUDIO_DEV == HEADSET)) { + __ASSERT_NO_MSG(tx_buf != NULL); + } + + const nrfx_i2s_buffers_t i2s_buf = {.p_rx_buffer = rx_buf, + .p_tx_buffer = (uint32_t *)tx_buf, + .buffer_size = I2S_SAMPLES_NUM}; + + nrfx_err_t ret; + + ret = nrfx_i2s_next_buffers_set(&i2s_inst, &i2s_buf); + __ASSERT_NO_MSG(ret == NRFX_SUCCESS); +} + +void audio_i2s_start(const uint8_t *tx_buf, uint32_t *rx_buf) +{ + __ASSERT_NO_MSG(state == AUDIO_I2S_STATE_IDLE); + if (IS_ENABLED(CONFIG_STREAM_BIDIRECTIONAL) || (CONFIG_AUDIO_DEV == GATEWAY)) { + __ASSERT_NO_MSG(rx_buf != NULL); + } + + if (IS_ENABLED(CONFIG_STREAM_BIDIRECTIONAL) || (CONFIG_AUDIO_DEV == HEADSET)) { + __ASSERT_NO_MSG(tx_buf != NULL); + } + + const nrfx_i2s_buffers_t i2s_buf = {.p_rx_buffer = rx_buf, + .p_tx_buffer = (uint32_t *)tx_buf, + .buffer_size = I2S_SAMPLES_NUM}; + + nrfx_err_t ret; + + /* Buffer size in 32-bit words */ + ret = nrfx_i2s_start(&i2s_inst, &i2s_buf, 0); + __ASSERT_NO_MSG(ret == NRFX_SUCCESS); + + state = AUDIO_I2S_STATE_STARTED; +} + +void audio_i2s_stop(void) +{ + __ASSERT_NO_MSG(state == AUDIO_I2S_STATE_STARTED); + + nrfx_i2s_stop(&i2s_inst); + + state = AUDIO_I2S_STATE_IDLE; +} + +void audio_i2s_blk_comp_cb_register(i2s_blk_comp_callback_t blk_comp_callback) +{ + i2s_blk_comp_callback = blk_comp_callback; +} + +void audio_i2s_init(void) +{ + __ASSERT_NO_MSG(state == AUDIO_I2S_STATE_UNINIT); + + nrfx_err_t ret; + + nrfx_clock_hfclkaudio_config_set(HFCLKAUDIO_12_288_MHZ); + + NRF_CLOCK->TASKS_HFCLKAUDIOSTART = 1; + + /* Wait for ACLK to start */ + while (!NRF_CLOCK_EVENT_HFCLKAUDIOSTARTED) { + k_sleep(K_MSEC(1)); + } + + ret = pinctrl_apply_state(PINCTRL_DT_DEV_CONFIG_GET(I2S_NL), PINCTRL_STATE_DEFAULT); + __ASSERT_NO_MSG(ret == 0); + + IRQ_CONNECT(DT_IRQN(I2S_NL), DT_IRQ(I2S_NL, priority), nrfx_isr, nrfx_i2s_0_irq_handler, 0); + irq_enable(DT_IRQN(I2S_NL)); + + ret = nrfx_i2s_init(&i2s_inst, &cfg, i2s_comp_handler); + __ASSERT_NO_MSG(ret == NRFX_SUCCESS); + + state = AUDIO_I2S_STATE_IDLE; +} diff --git a/src/modules/audio_i2s.h b/src/modules/audio_i2s.h new file mode 100644 index 0000000..76339f7 --- /dev/null +++ b/src/modules/audio_i2s.h @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2021, PACKETCRAFT, INC. + * + * SPDX-License-Identifier: LicenseRef-PCFT + */ + +#ifndef _AUDIO_I2S_H_ +#define _AUDIO_I2S_H_ + +#include +#include + +/** + * Calculation: + * FREQ_VALUE = 2^16 * ((12 * f_out / 32M) - 4) + * f_out == 12.288 + * 39845.888 = 2^16 * ((12 * 12.288 / 32M) - 4) + * 39846 = 0x9BA6 + */ +#define HFCLKAUDIO_12_288_MHZ 0x9BA6 +#define HFCLKAUDIO_12_165_MHZ 0x8FD8 +#define HFCLKAUDIO_12_411_MHZ 0xA774 + +/* + * Calculate the number of bytes of one frame, as per now, this frame can either + * be 10 or 7.5 ms. Since we can't have floats in a define we use 15/2 instead + */ +#if ((CONFIG_AUDIO_FRAME_DURATION_US == 7500) && CONFIG_SW_CODEC_LC3) + +#define FRAME_SIZE_BYTES \ + ((CONFIG_I2S_LRCK_FREQ_HZ / 1000 * 15 / 2) * CONFIG_I2S_CH_NUM * \ + CONFIG_AUDIO_BIT_DEPTH_OCTETS) +#else +#define FRAME_SIZE_BYTES \ + ((CONFIG_I2S_LRCK_FREQ_HZ / 1000 * 10) * CONFIG_I2S_CH_NUM * CONFIG_AUDIO_BIT_DEPTH_OCTETS) +#endif /* ((CONFIG_AUDIO_FRAME_DURATION_US == 7500) && CONFIG_SW_CODEC_LC3) */ + +#define BLOCK_SIZE_BYTES (FRAME_SIZE_BYTES / CONFIG_FIFO_FRAME_SPLIT_NUM) + +/* + * Calculate the number of samples in a block, divided by the number of samples + * that will fit within a 32-bit word + */ +#define I2S_SAMPLES_NUM \ + (BLOCK_SIZE_BYTES / (CONFIG_AUDIO_BIT_DEPTH_OCTETS) / (32 / CONFIG_AUDIO_BIT_DEPTH_BITS)) + +/** + * @brief I2S block complete event callback type + * + * @param frame_start_ts I2S frame start timestamp + * @param rx_buf_released Pointer to the released buffer containing received data + * @param tx_buf_released Pointer to the released buffer that was used to sent data + */ +typedef void (*i2s_blk_comp_callback_t)(uint32_t frame_start_ts, uint32_t *rx_buf_released, + uint32_t const *tx_buf_released); + +/** + * @brief Supply the buffers to be used in the next part of the I2S transfer + * + * @param tx_buf Pointer to the buffer with data to be sent + * @param rx_buf Pointer to the buffer for received data + */ +void audio_i2s_set_next_buf(const uint8_t *tx_buf, uint32_t *rx_buf); + +/** + * @brief Start the continuous I2S transfer + * + * @param tx_buf Pointer to the buffer with data to be sent + * @param rx_buf Pointer to the buffer for received data + */ +void audio_i2s_start(const uint8_t *tx_buf, uint32_t *rx_buf); + +/** + * @brief Stop the continuous I2S transfer + */ +void audio_i2s_stop(void); + +/** + * @brief Register callback function for I2S block complete event + * + * @param blk_comp_callback Callback function + */ +void audio_i2s_blk_comp_cb_register(i2s_blk_comp_callback_t blk_comp_callback); + +/** + * @brief Initialize I2S module + */ +void audio_i2s_init(void); + +#endif /* _AUDIO_I2S_H_ */ diff --git a/src/modules/audio_sync_timer.c b/src/modules/audio_sync_timer.c new file mode 100644 index 0000000..85a476f --- /dev/null +++ b/src/modules/audio_sync_timer.c @@ -0,0 +1,289 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "audio_sync_timer.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +LOG_MODULE_REGISTER(audio_sync_timer, CONFIG_AUDIO_SYNC_TIMER_LOG_LEVEL); + +#define AUDIO_SYNC_TIMER_NET_APP_IPC_EVT_CHANNEL 4 +#define AUDIO_SYNC_TIMER_NET_APP_IPC_EVT NRF_IPC_EVENT_RECEIVE_4 + +#define AUDIO_SYNC_HF_TIMER_INSTANCE_NUMBER 1 + +#define AUDIO_SYNC_HF_TIMER_I2S_FRAME_START_EVT_CAPTURE_CHANNEL 0 +#define AUDIO_SYNC_HF_TIMER_I2S_FRAME_START_EVT_CAPTURE NRF_TIMER_TASK_CAPTURE0 +#define AUDIO_SYNC_HF_TIMER_CURR_TIME_CAPTURE_CHANNEL 1 +#define AUDIO_SYNC_HF_TIMER_CURR_TIME_CAPTURE NRF_TIMER_TASK_CAPTURE1 + +static const nrfx_timer_t audio_sync_hf_timer_instance = + NRFX_TIMER_INSTANCE(AUDIO_SYNC_HF_TIMER_INSTANCE_NUMBER); + +static uint8_t dppi_channel_i2s_frame_start; + +#define AUDIO_SYNC_LF_TIMER_INSTANCE_NUMBER 0 + +#define AUDIO_SYNC_LF_TIMER_I2S_FRAME_START_EVT_CAPTURE_CHANNEL 0 +#define AUDIO_SYNC_LF_TIMER_I2S_FRAME_START_EVT_CAPTURE NRF_RTC_TASK_CAPTURE_0 +#define AUDIO_SYNC_LF_TIMER_CURR_TIME_CAPTURE_CHANNEL 1 +#define AUDIO_SYNC_LF_TIMER_CURR_TIME_CAPTURE NRF_RTC_TASK_CAPTURE_1 +#define CC_GET_CALLS_MAX 30 + +static uint8_t dppi_channel_curr_time_capture; + +static const nrfx_rtc_config_t rtc_cfg = NRFX_RTC_DEFAULT_CONFIG; + +static const nrfx_rtc_t audio_sync_lf_timer_instance = + NRFX_RTC_INSTANCE(AUDIO_SYNC_LF_TIMER_INSTANCE_NUMBER); + +static uint8_t dppi_channel_timer_sync_with_rtc; +static uint8_t dppi_channel_rtc_start; +static volatile uint32_t num_rtc_overflows; + +static nrfx_timer_config_t cfg = {.frequency = NRFX_MHZ_TO_HZ(1UL), + .mode = NRF_TIMER_MODE_TIMER, + .bit_width = NRF_TIMER_BIT_WIDTH_32, + .interrupt_priority = NRFX_TIMER_DEFAULT_CONFIG_IRQ_PRIORITY, + .p_context = NULL}; + +static uint32_t timestamp_from_rtc_and_timer_get(uint32_t ticks, uint32_t remainder_us) +{ + const uint64_t rtc_ticks_in_femto_units = 30517578125UL; + const uint32_t rtc_overflow_time_us = 512000000UL; + + return ((ticks * rtc_ticks_in_femto_units) / 1000000000UL) + + (num_rtc_overflows * rtc_overflow_time_us) + remainder_us; +} + +uint32_t audio_sync_timer_capture(void) +{ + /* Ensure that the follow product specification statement is handled: + * + * There is a delay of 6 PCLK16M periods from when the TASKS_CAPTURE[n] is triggered + * until the corresponding CC[n] register is updated. + * + * Lets have a stale value in the CC[n] register and compare that it is different when + * we capture using DPPI. + * + * We ensure it is stale by setting it as the previous tick relative to current + * counter value. + */ + uint32_t tick_stale = nrf_rtc_counter_get(audio_sync_lf_timer_instance.p_reg); + + /* Set a stale value in the CC[n] register */ + tick_stale--; + nrf_rtc_cc_set(audio_sync_lf_timer_instance.p_reg, + AUDIO_SYNC_LF_TIMER_CURR_TIME_CAPTURE_CHANNEL, tick_stale); + + /* Trigger EGU task to capture RTC and TIMER value */ + nrf_egu_task_trigger(NRF_EGU0, NRF_EGU_TASK_TRIGGER0); + + /* Read captured RTC value */ + uint32_t tick = nrf_rtc_cc_get(audio_sync_lf_timer_instance.p_reg, + AUDIO_SYNC_LF_TIMER_CURR_TIME_CAPTURE_CHANNEL); + + /* If required, wait until CC[n] register is updated */ + while (tick == tick_stale) { + tick = nrf_rtc_cc_get(audio_sync_lf_timer_instance.p_reg, + AUDIO_SYNC_LF_TIMER_CURR_TIME_CAPTURE_CHANNEL); + } + + /* Read captured TIMER value */ + uint32_t remainder_us = + nrf_timer_cc_get(NRF_TIMER1, AUDIO_SYNC_HF_TIMER_CURR_TIME_CAPTURE_CHANNEL); + + return timestamp_from_rtc_and_timer_get(tick, remainder_us); +} + +uint32_t audio_sync_timer_capture_get(void) +{ + uint32_t cc_get_calls = 0; + uint32_t tick = 0; + static uint32_t prev_tick; + uint32_t remainder_us = 0; + static uint32_t prev_remainder_us; + + /* This function is called too soon after I2S frame start may + * result in values not yet being updated in the *_cc_get calls. + * Ref: OCT-2585. To ensure new values are fetched, they are + * read in a while-loop with a timeout. + */ + + do { + tick = nrf_rtc_cc_get(audio_sync_lf_timer_instance.p_reg, + AUDIO_SYNC_LF_TIMER_I2S_FRAME_START_EVT_CAPTURE_CHANNEL); + cc_get_calls++; + if (cc_get_calls > CC_GET_CALLS_MAX) { + LOG_WRN("Unable to get new CC value"); + break; + } + } while (tick == prev_tick); + + cc_get_calls = 0; + + do { + remainder_us = nrf_timer_cc_get( + NRF_TIMER1, AUDIO_SYNC_HF_TIMER_I2S_FRAME_START_EVT_CAPTURE_CHANNEL); + cc_get_calls++; + if (cc_get_calls > CC_GET_CALLS_MAX) { + LOG_WRN("Unable to get new CC value"); + break; + } + } while (remainder_us == prev_remainder_us); + + prev_tick = tick; + prev_remainder_us = remainder_us; + + return timestamp_from_rtc_and_timer_get(tick, remainder_us); +} + +static void unused_timer_isr_handler(nrf_timer_event_t event_type, void *ctx) +{ + ARG_UNUSED(event_type); + ARG_UNUSED(ctx); +} + +static void rtc_isr_handler(nrfx_rtc_int_type_t int_type) +{ + if (int_type == NRFX_RTC_INT_OVERFLOW) { + num_rtc_overflows++; + } +} + +/** + * @brief Initialize audio sync timer + * + * @note The audio sync timers is replicating the controller's clock. + * The controller starts or clears the sync timer using a PPI signal + * sent from the controller. This makes the two clocks synchronized. + * + * @return 0 if successful, error otherwise + */ +static int audio_sync_timer_init(void) +{ + nrfx_err_t ret; + nrfx_dppi_t dppi = NRFX_DPPI_INSTANCE(0); + + ret = nrfx_timer_init(&audio_sync_hf_timer_instance, &cfg, unused_timer_isr_handler); + if (ret - NRFX_ERROR_BASE_NUM) { + LOG_ERR("nrfx timer init error: %d", ret); + return -ENODEV; + } + + ret = nrfx_rtc_init(&audio_sync_lf_timer_instance, &rtc_cfg, rtc_isr_handler); + if (ret - NRFX_ERROR_BASE_NUM) { + LOG_ERR("nrfx rtc init error: %d", ret); + return -ENODEV; + } + + IRQ_CONNECT(RTC0_IRQn, IRQ_PRIO_LOWEST, nrfx_isr, nrfx_rtc_0_irq_handler, 0); + nrfx_rtc_overflow_enable(&audio_sync_lf_timer_instance, true); + + /* Initialize capturing of I2S frame start event timestamps */ + ret = nrfx_dppi_channel_alloc(&dppi, &dppi_channel_i2s_frame_start); + if (ret - NRFX_ERROR_BASE_NUM) { + LOG_ERR("nrfx DPPI channel alloc error (I2S frame start): %d", ret); + return -ENOMEM; + } + + nrf_timer_subscribe_set(audio_sync_hf_timer_instance.p_reg, + AUDIO_SYNC_HF_TIMER_I2S_FRAME_START_EVT_CAPTURE, + dppi_channel_i2s_frame_start); + + /* Initialize capturing of I2S frame start event timestamps at the RTC as well. */ + nrf_rtc_subscribe_set(audio_sync_lf_timer_instance.p_reg, + AUDIO_SYNC_LF_TIMER_I2S_FRAME_START_EVT_CAPTURE, + dppi_channel_i2s_frame_start); + + nrf_i2s_publish_set(NRF_I2S0, NRF_I2S_EVENT_FRAMESTART, dppi_channel_i2s_frame_start); + ret = nrfx_dppi_channel_enable(&dppi, dppi_channel_i2s_frame_start); + if (ret - NRFX_ERROR_BASE_NUM) { + LOG_ERR("nrfx DPPI channel enable error (I2S frame start): %d", ret); + return -EIO; + } + + /* Initialize capturing of current timestamps */ + ret = nrfx_dppi_channel_alloc(&dppi, &dppi_channel_curr_time_capture); + if (ret - NRFX_ERROR_BASE_NUM) { + LOG_ERR("nrfx DPPI channel alloc error (I2S frame start) - Return value: %d", ret); + return -ENOMEM; + } + + nrf_rtc_subscribe_set(audio_sync_lf_timer_instance.p_reg, + AUDIO_SYNC_LF_TIMER_CURR_TIME_CAPTURE, + dppi_channel_curr_time_capture); + + nrf_timer_subscribe_set(audio_sync_hf_timer_instance.p_reg, + AUDIO_SYNC_HF_TIMER_CURR_TIME_CAPTURE, + dppi_channel_curr_time_capture); + + nrf_egu_publish_set(NRF_EGU0, NRF_EGU_EVENT_TRIGGERED0, dppi_channel_curr_time_capture); + + ret = nrfx_dppi_channel_enable(&dppi, dppi_channel_curr_time_capture); + if (ret - NRFX_ERROR_BASE_NUM) { + LOG_ERR("nrfx DPPI channel enable error (I2S frame start) - Return value: %d", ret); + return -EIO; + } + + /* Initialize functionality for synchronization between APP and NET core */ + ret = nrfx_dppi_channel_alloc(&dppi, &dppi_channel_rtc_start); + if (ret - NRFX_ERROR_BASE_NUM) { + LOG_ERR("nrfx DPPI channel alloc error (timer clear): %d", ret); + return -ENOMEM; + } + + nrf_rtc_subscribe_set(audio_sync_lf_timer_instance.p_reg, NRF_RTC_TASK_CLEAR, + dppi_channel_rtc_start); + nrf_timer_subscribe_set(audio_sync_hf_timer_instance.p_reg, NRF_TIMER_TASK_START, + dppi_channel_rtc_start); + + nrf_ipc_receive_config_set(NRF_IPC, AUDIO_SYNC_TIMER_NET_APP_IPC_EVT_CHANNEL, + NRF_IPC_CHANNEL_4); + nrf_ipc_publish_set(NRF_IPC, AUDIO_SYNC_TIMER_NET_APP_IPC_EVT, dppi_channel_rtc_start); + + ret = nrfx_dppi_channel_enable(&dppi, dppi_channel_rtc_start); + if (ret - NRFX_ERROR_BASE_NUM) { + LOG_ERR("nrfx DPPI channel enable error (timer clear): %d", ret); + return -EIO; + } + + /* Initialize functionality for synchronization between RTC and TIMER */ + ret = nrfx_dppi_channel_alloc(&dppi, &dppi_channel_timer_sync_with_rtc); + if (ret - NRFX_ERROR_BASE_NUM) { + LOG_ERR("nrfx DPPI channel alloc error (timer clear): %d", ret); + return -ENOMEM; + } + + nrf_rtc_publish_set(audio_sync_lf_timer_instance.p_reg, NRF_RTC_EVENT_TICK, + dppi_channel_timer_sync_with_rtc); + nrf_timer_subscribe_set(audio_sync_hf_timer_instance.p_reg, NRF_TIMER_TASK_CLEAR, + dppi_channel_timer_sync_with_rtc); + + nrfx_rtc_tick_enable(&audio_sync_lf_timer_instance, false); + + ret = nrfx_dppi_channel_enable(&dppi, dppi_channel_timer_sync_with_rtc); + if (ret - NRFX_ERROR_BASE_NUM) { + LOG_ERR("nrfx DPPI channel enable error (timer clear): %d", ret); + return -EIO; + } + + nrfx_rtc_enable(&audio_sync_lf_timer_instance); + + LOG_DBG("Audio sync timer initialized"); + + return 0; +} + +SYS_INIT(audio_sync_timer_init, POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT); diff --git a/src/modules/audio_sync_timer.h b/src/modules/audio_sync_timer.h new file mode 100644 index 0000000..46d98d0 --- /dev/null +++ b/src/modules/audio_sync_timer.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _AUDIO_SYNC_TIMER_H_ +#define _AUDIO_SYNC_TIMER_H_ + +#include +#include + +/** + * @brief Capture a timestamp on the sync timer. + * + * @retval The current timestamp of the audio sync timer. + */ +uint32_t audio_sync_timer_capture(void); + +/** + * @brief Returns the last captured value of the sync timer. + * + * The captured time is corresponding to the I2S frame start. + * NOTE: This function is not reentrant and must only be called + * once in the I2S ISR. There may be a delay in the capturing of + * the clock value. Hence, there is a retry-loop with a timeout. + * Should we not get the new capture value before the timeout, + * a warning will be printed and calculations based on the old + * timer capture values. + * + * See @ref audio_sync_timer_capture(). + * + * @retval The last captured timestamp of the audio sync timer. + */ +uint32_t audio_sync_timer_capture_get(void); + +#endif /* _AUDIO_SYNC_TIMER_H_ */ diff --git a/src/modules/audio_usb.c b/src/modules/audio_usb.c new file mode 100644 index 0000000..2b8b330 --- /dev/null +++ b/src/modules/audio_usb.c @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2018 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "audio_usb.h" + +#include +#include +#include +#include + +#include "macros_common.h" + +#include +LOG_MODULE_REGISTER(audio_usb, CONFIG_MODULE_AUDIO_USB_LOG_LEVEL); + +#define USB_FRAME_SIZE_STEREO \ + (((CONFIG_AUDIO_SAMPLE_RATE_HZ * CONFIG_AUDIO_BIT_DEPTH_OCTETS) / 1000) * 2) + +static struct data_fifo *fifo_tx; +static struct data_fifo *fifo_rx; + +NET_BUF_POOL_FIXED_DEFINE(pool_out, CONFIG_FIFO_FRAME_SPLIT_NUM, USB_FRAME_SIZE_STEREO, 8, + net_buf_destroy); + +static uint32_t rx_num_overruns; +static bool rx_first_data; +static bool tx_first_data; + +#if (CONFIG_STREAM_BIDIRECTIONAL) +static uint32_t tx_num_underruns; + +static void data_write(const struct device *dev) +{ + int ret; + + if (fifo_tx == NULL) { + return; + } + + void *data_out; + size_t data_out_size; + struct net_buf *buf_out; + + buf_out = net_buf_alloc(&pool_out, K_NO_WAIT); + + ret = data_fifo_pointer_last_filled_get(fifo_tx, &data_out, &data_out_size, K_NO_WAIT); + if (ret) { + tx_num_underruns++; + if ((tx_num_underruns % 100) == 1) { + LOG_WRN("USB TX underrun. Num: %d", tx_num_underruns); + } + net_buf_unref(buf_out); + + return; + } + + memcpy(buf_out->data, data_out, data_out_size); + data_fifo_block_free(fifo_tx, data_out); + + if (data_out_size == usb_audio_get_in_frame_size(dev)) { + ret = usb_audio_send(dev, buf_out, data_out_size); + if (ret) { + LOG_WRN("USB TX failed, ret: %d", ret); + net_buf_unref(buf_out); + } + + } else { + LOG_WRN("Wrong size write: %d", data_out_size); + } + + if (!tx_first_data) { + LOG_INF("USB TX first data sent."); + tx_first_data = true; + } +} +#endif /* (CONFIG_STREAM_BIDIRECTIONAL) */ + +static void data_received(const struct device *dev, struct net_buf *buffer, size_t size) +{ + int ret; + void *data_in; + + if (fifo_rx == NULL) { + /* Throwing away data */ + net_buf_unref(buffer); + return; + } + + if (buffer == NULL || size == 0 || buffer->data == NULL) { + /* This should never happen */ + ERR_CHK(-EINVAL); + } + + /* Receive data from USB */ + if (size != USB_FRAME_SIZE_STEREO) { + LOG_WRN("Wrong length: %d", size); + net_buf_unref(buffer); + return; + } + + ret = data_fifo_pointer_first_vacant_get(fifo_rx, &data_in, K_NO_WAIT); + + /* RX FIFO can fill up due to retransmissions or disconnect */ + if (ret == -ENOMEM) { + void *temp; + size_t temp_size; + + rx_num_overruns++; + if ((rx_num_overruns % 100) == 1) { + LOG_WRN("USB RX overrun. Num: %d", rx_num_overruns); + } + + ret = data_fifo_pointer_last_filled_get(fifo_rx, &temp, &temp_size, K_NO_WAIT); + ERR_CHK(ret); + + data_fifo_block_free(fifo_rx, temp); + + ret = data_fifo_pointer_first_vacant_get(fifo_rx, &data_in, K_NO_WAIT); + } + + ERR_CHK_MSG(ret, "RX failed to get block"); + + memcpy(data_in, buffer->data, size); + + ret = data_fifo_block_lock(fifo_rx, &data_in, size); + ERR_CHK_MSG(ret, "Failed to lock block"); + + net_buf_unref(buffer); + + if (!rx_first_data) { + LOG_INF("USB RX first data received."); + rx_first_data = true; + } +} + +static void feature_update(const struct device *dev, const struct usb_audio_fu_evt *evt) +{ + LOG_DBG("Control selector %d for channel %d updated", evt->cs, evt->channel); + switch (evt->cs) { + case USB_AUDIO_FU_MUTE_CONTROL: + /* Fall through */ + default: + break; + } +} + +static const struct usb_audio_ops ops = { + .data_received_cb = data_received, + .feature_update_cb = feature_update, +#if (CONFIG_STREAM_BIDIRECTIONAL) + .data_request_cb = data_write, +#endif /* (CONFIG_STREAM_BIDIRECTIONAL) */ +}; + +int audio_usb_start(struct data_fifo *fifo_tx_in, struct data_fifo *fifo_rx_in) +{ + if (fifo_tx_in == NULL || fifo_rx_in == NULL) { + return -EINVAL; + } + + fifo_tx = fifo_tx_in; + fifo_rx = fifo_rx_in; + + return 0; +} + +void audio_usb_stop(void) +{ + rx_first_data = false; + tx_first_data = false; + fifo_tx = NULL; + fifo_rx = NULL; +} + +int audio_usb_disable(void) +{ + int ret; + + audio_usb_stop(); + + ret = usb_disable(); + if (ret) { + LOG_ERR("Failed to disable USB"); + return ret; + } + + return 0; +} + +int audio_usb_init(void) +{ + int ret; + const struct device *hs_dev = DEVICE_DT_GET(DT_NODELABEL(hs_0)); + + if (!device_is_ready(hs_dev)) { + LOG_ERR("USB Headset Device not ready"); + return -EIO; + } + + usb_audio_register(hs_dev, &ops); + + ret = usb_enable(NULL); + if (ret) { + LOG_ERR("Failed to enable USB"); + return ret; + } + + LOG_INF("Ready for USB host to send/receive."); + + return 0; +} diff --git a/src/modules/audio_usb.h b/src/modules/audio_usb.h new file mode 100644 index 0000000..9fe4cc0 --- /dev/null +++ b/src/modules/audio_usb.h @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2018 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _AUDIO_USB_H_ +#define _AUDIO_USB_H_ + +#include + +#if (CONFIG_AUDIO_SOURCE_USB && !CONFIG_AUDIO_SAMPLE_RATE_48000_HZ) +/* Only 48kHz is supported when using USB */ +#error USB only supports 48kHz +#endif /* (CONFIG_AUDIO_SOURCE_USB && !CONFIG_AUDIO_SAMPLE_RATE_48000_HZ) */ + +/** + * @brief Set fifo buffers to be used by USB module and start sending/receiving data + * + * @param fifo_tx_in Pointer to fifo structure for tx + * @param fifo_rx_in Pointer to fifo structure for rx + * + * @return 0 if successful, error otherwise + */ +int audio_usb_start(struct data_fifo *fifo_tx_in, struct data_fifo *fifo_rx_in); + +/** + * @brief Stop sending/receiving data + * + * @note The USB device will still be running, but all data sent to + * it will be discarded + */ +void audio_usb_stop(void); + +/** + * @brief Stop and disable USB device + * + * @return 0 if successful, error otherwise + */ +int audio_usb_disable(void); + +/** + * @brief Register and enable USB device + * + * @return 0 if successful, error otherwise + */ +int audio_usb_init(void); + +#endif /* _AUDIO_USB_H_ */ diff --git a/src/modules/board.h b/src/modules/board.h new file mode 100644 index 0000000..36f7d16 --- /dev/null +++ b/src/modules/board.h @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2018 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef __BOARD_H__ +#define __BOARD_H__ + +#include + +/* Voltage divider PCA10121 board versions. + * The defines give what value the ADC will read back. + * This is determined by the on-board voltage divider. + */ + +struct board_version { + char name[10]; + uint32_t mask; + uint32_t adc_reg_val; +}; + +#define BOARD_PCA10121_0_0_0_MSK (BIT(0)) +#define BOARD_PCA10121_0_6_0_MSK (BIT(1)) +#define BOARD_PCA10121_0_7_0_MSK (BIT(2)) +#define BOARD_PCA10121_0_7_1_MSK (BIT(3)) +#define BOARD_PCA10121_0_8_0_MSK (BIT(4)) +#define BOARD_PCA10121_0_8_1_MSK (BIT(5)) +#define BOARD_PCA10121_0_8_2_MSK (BIT(6)) +#define BOARD_PCA10121_0_9_0_MSK (BIT(7)) +#define BOARD_PCA10121_0_10_0_MSK (BIT(8)) +#define BOARD_PCA10121_1_0_0_MSK (BIT(9)) +#define BOARD_PCA10121_1_1_0_MSK (BIT(10)) +#define BOARD_PCA10121_1_2_0_MSK (BIT(11)) + +static const struct board_version BOARD_VERSION_ARR[] = { + { "0.0.0", BOARD_PCA10121_0_0_0_MSK, INT_MIN }, + { "0.6.0", BOARD_PCA10121_0_6_0_MSK, 61 }, + { "0.7.0", BOARD_PCA10121_0_7_0_MSK, 102 }, + { "0.7.1", BOARD_PCA10121_0_7_1_MSK, 303 }, + { "0.8.0", BOARD_PCA10121_0_8_0_MSK, 534 }, + { "0.8.1", BOARD_PCA10121_0_8_1_MSK, 780 }, + { "0.8.2", BOARD_PCA10121_0_8_2_MSK, 1018 }, + { "0.9.0", BOARD_PCA10121_0_9_0_MSK, 1260 }, + /* Lower value used on 0.10.0 due to high ohm divider */ + { "0.10.0", BOARD_PCA10121_0_10_0_MSK, 1480 }, + { "1.0.0", BOARD_PCA10121_1_0_0_MSK, 1743 }, + { "1.1.0", BOARD_PCA10121_1_1_0_MSK, 1982 }, + { "1.2.0", BOARD_PCA10121_1_2_0_MSK, 2219 }, +}; + +#define BOARD_VERSION_VALID_MSK \ + (BOARD_PCA10121_0_8_0_MSK | BOARD_PCA10121_0_8_1_MSK | BOARD_PCA10121_0_8_2_MSK | \ + BOARD_PCA10121_0_9_0_MSK | BOARD_PCA10121_0_10_0_MSK | BOARD_PCA10121_1_0_0_MSK | \ + BOARD_PCA10121_1_1_0_MSK | BOARD_PCA10121_1_2_0_MSK) + +#define BOARD_VERSION_VALID_MSK_SD_CARD \ + (BOARD_PCA10121_0_8_0_MSK | BOARD_PCA10121_0_8_1_MSK | BOARD_PCA10121_0_8_2_MSK | \ + BOARD_PCA10121_0_9_0_MSK | BOARD_PCA10121_0_10_0_MSK | BOARD_PCA10121_1_0_0_MSK | \ + BOARD_PCA10121_1_1_0_MSK | BOARD_PCA10121_1_2_0_MSK) + +#endif diff --git a/src/modules/button_assignments.h b/src/modules/button_assignments.h new file mode 100644 index 0000000..5ef75ab --- /dev/null +++ b/src/modules/button_assignments.h @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +/** @file + * @brief Button assignments + * + * Button mappings are listed here. + * + */ + +#ifndef _BUTTON_ASSIGNMENTS_H_ +#define _BUTTON_ASSIGNMENTS_H_ + +#include + +/** @brief List of buttons and associated metadata + */ +enum button_pin_names { + BUTTON_VOLUME_DOWN = DT_GPIO_PIN(DT_ALIAS(sw0), gpios), + BUTTON_VOLUME_UP = DT_GPIO_PIN(DT_ALIAS(sw1), gpios), + BUTTON_PLAY_PAUSE = DT_GPIO_PIN(DT_ALIAS(sw2), gpios), + BUTTON_4 = DT_GPIO_PIN(DT_ALIAS(sw3), gpios), + BUTTON_5 = DT_GPIO_PIN(DT_ALIAS(sw4), gpios), +}; + +#endif /* _BUTTON_ASSIGNMENTS_H_ */ diff --git a/src/modules/button_handler.c b/src/modules/button_handler.c new file mode 100644 index 0000000..fbe746d --- /dev/null +++ b/src/modules/button_handler.c @@ -0,0 +1,305 @@ +/* + * Copyright (c) 2018 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "button_handler.h" +#include "button_assignments.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "macros_common.h" +#include "zbus_common.h" + +#include +LOG_MODULE_REGISTER(button_handler, CONFIG_MODULE_BUTTON_HANDLER_LOG_LEVEL); + +ZBUS_CHAN_DEFINE(button_chan, struct button_msg, NULL, NULL, ZBUS_OBSERVERS_EMPTY, + ZBUS_MSG_INIT(0)); + +/* How many buttons does the module support. Increase at memory cost */ +#define BUTTONS_MAX 5 +#define BASE_10 10 + +/* Only allow one button msg at a time, as a mean of debounce */ +K_MSGQ_DEFINE(button_queue, sizeof(struct button_msg), 1, 4); + +static bool debounce_is_ongoing; +static struct gpio_callback btn_callback[BUTTONS_MAX]; + +/* clang-format off */ +const static struct btn_config btn_cfg[] = { + { + .btn_name = STRINGIFY(BUTTON_VOLUME_DOWN), + .btn_pin = BUTTON_VOLUME_DOWN, + .btn_cfg_mask = DT_GPIO_FLAGS(DT_ALIAS(sw0), gpios), + }, + { + .btn_name = STRINGIFY(BUTTON_VOLUME_UP), + .btn_pin = BUTTON_VOLUME_UP, + .btn_cfg_mask = DT_GPIO_FLAGS(DT_ALIAS(sw1), gpios), + }, + { + .btn_name = STRINGIFY(BUTTON_PLAY_PAUSE), + .btn_pin = BUTTON_PLAY_PAUSE, + .btn_cfg_mask = DT_GPIO_FLAGS(DT_ALIAS(sw2), gpios), + }, + { + .btn_name = STRINGIFY(BUTTON_4), + .btn_pin = BUTTON_4, + .btn_cfg_mask = DT_GPIO_FLAGS(DT_ALIAS(sw3), gpios), + }, + { + .btn_name = STRINGIFY(BUTTON_5), + .btn_pin = BUTTON_5, + .btn_cfg_mask = DT_GPIO_FLAGS(DT_ALIAS(sw4), gpios), + } +}; +/* clang-format on */ + +static const struct device *gpio_53_dev; + +/**@brief Simple debouncer for buttons + * + * @note Needed as low-level driver debouce is not + * implemented in Zephyr for nRF53 yet + */ +static void on_button_debounce_timeout(struct k_timer *timer) +{ + debounce_is_ongoing = false; +} + +K_TIMER_DEFINE(button_debounce_timer, on_button_debounce_timeout, NULL); + +/** @brief Find the index of a button from the pin number + */ +static int pin_to_btn_idx(uint8_t btn_pin, uint32_t *pin_idx) +{ + for (uint8_t i = 0; i < ARRAY_SIZE(btn_cfg); i++) { + if (btn_pin == btn_cfg[i].btn_pin) { + *pin_idx = i; + return 0; + } + } + + LOG_WRN("Button idx not found"); + return -ENODEV; +} + +/** @brief Convert from mask to pin + * + * @note: Will check that a single bit and a single bit only is set in the mask. + */ +static int pin_msk_to_pin(uint32_t pin_msk, uint32_t *pin_out) +{ + if (!pin_msk) { + LOG_ERR("Mask is empty"); + return -EACCES; + } + + if (pin_msk & (pin_msk - 1)) { + LOG_ERR("Two or more buttons set in mask"); + return -EACCES; + } + + *pin_out = 0; + + while (pin_msk) { + pin_msk = pin_msk >> 1; + (*pin_out)++; + } + + /* Deduct 1 for zero indexing */ + (*pin_out)--; + + return 0; +} + +static void button_publish_thread(void) +{ + int ret; + struct button_msg msg; + + while (1) { + k_msgq_get(&button_queue, &msg, K_FOREVER); + + ret = zbus_chan_pub(&button_chan, &msg, K_NO_WAIT); + if (ret) { + LOG_ERR("Failed to publish button msg, ret: %d", ret); + } + } +} + +K_THREAD_DEFINE(button_publish, CONFIG_BUTTON_PUBLISH_STACK_SIZE, button_publish_thread, NULL, NULL, + NULL, K_PRIO_PREEMPT(CONFIG_BUTTON_PUBLISH_THREAD_PRIO), 0, 0); + +/* ISR triggered by GPIO when assigned button(s) are pushed */ +static void button_isr(const struct device *port, struct gpio_callback *cb, uint32_t pin_msk) +{ + int ret; + struct button_msg msg; + + if (debounce_is_ongoing) { + LOG_DBG("Btn debounce in action"); + return; + } + + uint32_t btn_pin = 0; + uint32_t btn_idx = 0; + + ret = pin_msk_to_pin(pin_msk, &btn_pin); + ERR_CHK(ret); + + ret = pin_to_btn_idx(btn_pin, &btn_idx); + ERR_CHK(ret); + + LOG_DBG("Pushed button idx: %d pin: %d name: %s", btn_idx, btn_pin, + btn_cfg[btn_idx].btn_name); + + msg.button_pin = btn_pin; + msg.button_action = BUTTON_PRESS; + + ret = k_msgq_put(&button_queue, (void *)&msg, K_NO_WAIT); + if (ret == -EAGAIN) { + LOG_WRN("Btn msg queue full"); + } + + debounce_is_ongoing = true; + k_timer_start(&button_debounce_timer, K_MSEC(CONFIG_BUTTON_DEBOUNCE_MS), K_NO_WAIT); +} + +int button_pressed(gpio_pin_t button_pin, bool *button_pressed) +{ + int ret; + + if (!device_is_ready(gpio_53_dev)) { + return -ENODEV; + } + + if (button_pressed == NULL) { + return -EINVAL; + } + + ret = gpio_pin_get(gpio_53_dev, button_pin); + switch (ret) { + case 0: + *button_pressed = false; + break; + case 1: + *button_pressed = true; + break; + default: + return ret; + } + + return 0; +} + +int button_handler_init(void) +{ + int ret; + + if (ARRAY_SIZE(btn_cfg) == 0) { + LOG_WRN("No buttons assigned"); + return -EINVAL; + } + + gpio_53_dev = DEVICE_DT_GET(DT_NODELABEL(gpio0)); + + if (!device_is_ready(gpio_53_dev)) { + LOG_ERR("Device driver not ready."); + return -ENODEV; + } + + for (uint8_t i = 0; i < ARRAY_SIZE(btn_cfg); i++) { + ret = gpio_pin_configure(gpio_53_dev, btn_cfg[i].btn_pin, + GPIO_INPUT | btn_cfg[i].btn_cfg_mask); + if (ret) { + return ret; + } + + gpio_init_callback(&btn_callback[i], button_isr, BIT(btn_cfg[i].btn_pin)); + + ret = gpio_add_callback(gpio_53_dev, &btn_callback[i]); + if (ret) { + return ret; + } + + ret = gpio_pin_interrupt_configure(gpio_53_dev, btn_cfg[i].btn_pin, + GPIO_INT_EDGE_TO_INACTIVE); + if (ret) { + return ret; + } + } + + return 0; +} + +/* Shell functions */ +static int cmd_print_all_btns(const struct shell *shell, size_t argc, char **argv) +{ + ARG_UNUSED(argc); + ARG_UNUSED(argv); + + for (uint8_t i = 0; i < ARRAY_SIZE(btn_cfg); i++) { + shell_print(shell, "Id %d: pin: %d %s", i, btn_cfg[i].btn_pin, btn_cfg[i].btn_name); + } + + return 0; +} + +static int cmd_push_btn(const struct shell *shell, size_t argc, char **argv) +{ + int ret; + uint8_t btn_idx; + struct button_msg msg; + + /* First argument is function, second is button idx */ + if (argc != 2) { + shell_error(shell, "Wrong number of arguments provided"); + return -EINVAL; + } + + if (!isdigit((int)argv[1][0])) { + shell_error(shell, "Supplied argument is not numeric"); + return -EINVAL; + } + + btn_idx = strtoul(argv[1], NULL, BASE_10); + + if (btn_idx >= ARRAY_SIZE(btn_cfg)) { + shell_error(shell, "Selected button ID out of range"); + return -EINVAL; + } + + msg.button_pin = btn_cfg[btn_idx].btn_pin; + msg.button_action = BUTTON_PRESS; + + ret = zbus_chan_pub(&button_chan, &msg, K_NO_WAIT); + if (ret) { + LOG_ERR("Failed to publish button msg, ret: %d", ret); + } + + shell_print(shell, "Pushed button idx: %d pin: %d : %s", btn_idx, btn_cfg[btn_idx].btn_pin, + btn_cfg[btn_idx].btn_name); + + return 0; +} + +/* Creating subcommands (level 1 command) array for command "demo". */ +SHELL_STATIC_SUBCMD_SET_CREATE(buttons_cmd, + SHELL_COND_CMD(CONFIG_SHELL, print, NULL, "Print all buttons.", + cmd_print_all_btns), + SHELL_COND_CMD(CONFIG_SHELL, push, NULL, "Push button.", + cmd_push_btn), + SHELL_SUBCMD_SET_END); +/* Creating root (level 0) command "demo" without a handler */ +SHELL_CMD_REGISTER(buttons, &buttons_cmd, "List and push buttons", NULL); diff --git a/src/modules/button_handler.h b/src/modules/button_handler.h new file mode 100644 index 0000000..f600d2d --- /dev/null +++ b/src/modules/button_handler.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _BUTTON_HANDLER_H_ +#define _BUTTON_HANDLER_H_ + +#include +#include + +struct btn_config { + const char *btn_name; + uint8_t btn_pin; + uint32_t btn_cfg_mask; +}; + +/** @brief Initialize button handler, with buttons defined in button_assignments.h. + * + * @note This function may only be called once - there is no reinitialize. + * + * @return 0 if successful. + * @return -ENODEV gpio driver not found + */ +int button_handler_init(void); + +/** @brief Check button state. + * + * @param[in] button_pin Button pin + * @param[out] button_pressed Button state. True if currently pressed, false otherwise + * + * @return 0 if success, an error code otherwise. + */ +int button_pressed(gpio_pin_t button_pin, bool *button_pressed); + +#endif /* _BUTTON_HANDLER_H_ */ diff --git a/src/modules/hw_codec.c b/src/modules/hw_codec.c new file mode 100644 index 0000000..b96f18d --- /dev/null +++ b/src/modules/hw_codec.c @@ -0,0 +1,487 @@ +/* + * Copyright (c) 2018 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "hw_codec.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "macros_common.h" +#include "zbus_common.h" +#include "cs47l63.h" +#include "cs47l63_spec.h" +#include "cs47l63_reg_conf.h" +#include "cs47l63_comm.h" + +#include +LOG_MODULE_REGISTER(hw_codec, CONFIG_MODULE_HW_CODEC_LOG_LEVEL); + +#define VOLUME_ADJUST_STEP_DB 3 +#define BASE_10 10 + +ZBUS_SUBSCRIBER_DEFINE(volume_evt_sub, CONFIG_VOLUME_MSG_SUB_QUEUE_SIZE); + +static uint32_t prev_volume_reg_val = OUT_VOLUME_DEFAULT; + +static cs47l63_t cs47l63_driver; + +static k_tid_t volume_msg_sub_thread_id; +static struct k_thread volume_msg_sub_thread_data; + +K_THREAD_STACK_DEFINE(volume_msg_sub_thread_stack, CONFIG_VOLUME_MSG_SUB_STACK_SIZE); + +/** + * @brief Convert the zbus volume to the actual volume setting for the HW codec. + * + * @note The range for zbus volume is from 0 to 255 and the + * range for HW codec volume is from 0 to 128. + */ +static uint16_t zbus_vol_conversion(uint8_t volume) +{ + return (((uint16_t)volume + 1) / 2); +} + +/** + * @brief Handle volume events from zbus. + */ +static void volume_msg_sub_thread(void) +{ + int ret; + + const struct zbus_channel *chan; + + while (1) { + ret = zbus_sub_wait(&volume_evt_sub, &chan, K_FOREVER); + ERR_CHK(ret); + + struct volume_msg msg; + + ret = zbus_chan_read(chan, &msg, ZBUS_READ_TIMEOUT_MS); + if (ret) { + LOG_ERR("Failed to read from zbus: %d", ret); + } + + uint8_t event = msg.event; + uint8_t volume = msg.volume; + + LOG_DBG("Received event = %d, volume = %d", event, volume); + + switch (event) { + case VOLUME_UP: + LOG_DBG("Volume up received"); + ret = hw_codec_volume_increase(); + if (ret) { + LOG_ERR("Failed to increase volume, ret: %d", ret); + } + break; + case VOLUME_DOWN: + LOG_DBG("Volume down received"); + ret = hw_codec_volume_decrease(); + if (ret) { + LOG_ERR("Failed to decrease volume, ret: %d", ret); + } + break; + case VOLUME_SET: + LOG_DBG("Volume set received"); + ret = hw_codec_volume_set(zbus_vol_conversion(volume)); + if (ret) { + LOG_ERR("Failed to set the volume to %d, ret: %d", volume, ret); + } + break; + case VOLUME_MUTE: + LOG_DBG("Volume mute received"); + ret = hw_codec_volume_mute(); + if (ret) { + LOG_ERR("Failed to mute volume, ret: %d", ret); + } + break; + case VOLUME_UNMUTE: + LOG_DBG("Volume unmute received"); + ret = hw_codec_volume_unmute(); + if (ret) { + LOG_ERR("Failed to unmute volume, ret: %d", ret); + } + break; + default: + LOG_WRN("Unexpected/unhandled volume event: %d", event); + break; + } + + STACK_USAGE_PRINT("volume_msg_thread", &volume_msg_sub_thread_data); + } +} + +/** + * @brief Write to multiple registers in CS47L63. + */ +static int cs47l63_comm_reg_conf_write(const uint32_t config[][2], uint32_t num_of_regs) +{ + int ret; + uint32_t reg; + uint32_t value; + + for (int i = 0; i < num_of_regs; i++) { + reg = config[i][0]; + value = config[i][1]; + + if (reg == SPI_BUSY_WAIT) { + LOG_DBG("Busy waiting instead of writing to CS47L63"); + /* Wait for us defined in value */ + k_busy_wait(value); + } else { + ret = cs47l63_write_reg(&cs47l63_driver, reg, value); + if (ret) { + return ret; + } + } + } + + return 0; +} + +int hw_codec_volume_set(uint8_t set_val) +{ + int ret; + uint32_t volume_reg_val; + + volume_reg_val = set_val; + if (volume_reg_val == 0) { + LOG_WRN("Volume at MIN (-64dB)"); + } else if (volume_reg_val >= MAX_VOLUME_REG_VAL) { + LOG_WRN("Volume at MAX (0dB)"); + volume_reg_val = MAX_VOLUME_REG_VAL; + } + + ret = cs47l63_write_reg(&cs47l63_driver, CS47L63_OUT1L_VOLUME_1, + volume_reg_val | CS47L63_OUT_VU); + if (ret) { + return ret; + } + + prev_volume_reg_val = volume_reg_val; + + /* This is rounded down to nearest integer */ + LOG_DBG("Volume: %" PRId32 " dB", (volume_reg_val / 2) - MAX_VOLUME_DB); + + return 0; +} + +int hw_codec_volume_adjust(int8_t adjustment_db) +{ + int ret; + int32_t new_volume_reg_val; + + LOG_DBG("Adj dB in: %d", adjustment_db); + + if (adjustment_db == 0) { + new_volume_reg_val = prev_volume_reg_val; + } else { + uint32_t volume_reg_val; + + ret = cs47l63_read_reg(&cs47l63_driver, CS47L63_OUT1L_VOLUME_1, &volume_reg_val); + if (ret) { + LOG_ERR("Failed to get volume from CS47L63"); + return ret; + } + + volume_reg_val &= CS47L63_OUT1L_VOL_MASK; + + /* The adjustment is in dB, 1 bit equals 0.5 dB, + * so multiply by 2 to get increments of 1 dB + */ + new_volume_reg_val = volume_reg_val + (adjustment_db * 2); + if (new_volume_reg_val <= 0) { + LOG_WRN("Volume at MIN (-64dB)"); + new_volume_reg_val = 0; + } else if (new_volume_reg_val >= MAX_VOLUME_REG_VAL) { + LOG_WRN("Volume at MAX (0dB)"); + new_volume_reg_val = MAX_VOLUME_REG_VAL; + } + } + + ret = hw_codec_volume_set(new_volume_reg_val); + if (ret) { + return ret; + } + + return 0; +} + +int hw_codec_volume_decrease(void) +{ + int ret; + + ret = hw_codec_volume_adjust(-VOLUME_ADJUST_STEP_DB); + if (ret) { + return ret; + } + + return 0; +} + +int hw_codec_volume_increase(void) +{ + int ret; + + ret = hw_codec_volume_adjust(VOLUME_ADJUST_STEP_DB); + if (ret) { + return ret; + } + + return 0; +} + +int hw_codec_volume_mute(void) +{ + int ret; + uint32_t volume_reg_val; + + ret = cs47l63_read_reg(&cs47l63_driver, CS47L63_OUT1L_VOLUME_1, &volume_reg_val); + if (ret) { + return ret; + } + + BIT_SET(volume_reg_val, CS47L63_OUT1L_MUTE_MASK); + + ret = cs47l63_write_reg(&cs47l63_driver, CS47L63_OUT1L_VOLUME_1, + volume_reg_val | CS47L63_OUT_VU); + if (ret) { + return ret; + } + + return 0; +} + +int hw_codec_volume_unmute(void) +{ + int ret; + uint32_t volume_reg_val; + + ret = cs47l63_read_reg(&cs47l63_driver, CS47L63_OUT1L_VOLUME_1, &volume_reg_val); + if (ret) { + return ret; + } + + BIT_CLEAR(volume_reg_val, CS47L63_OUT1L_MUTE_MASK); + + ret = cs47l63_write_reg(&cs47l63_driver, CS47L63_OUT1L_VOLUME_1, + volume_reg_val | CS47L63_OUT_VU); + if (ret) { + return ret; + } + + return 0; +} + +int hw_codec_default_conf_enable(void) +{ + int ret; + + ret = cs47l63_comm_reg_conf_write(clock_configuration, ARRAY_SIZE(clock_configuration)); + if (ret) { + return ret; + } + + ret = cs47l63_comm_reg_conf_write(GPIO_configuration, ARRAY_SIZE(GPIO_configuration)); + if (ret) { + return ret; + } + + ret = cs47l63_comm_reg_conf_write(asp1_enable, ARRAY_SIZE(asp1_enable)); + if (ret) { + return ret; + } + + ret = cs47l63_comm_reg_conf_write(output_enable, ARRAY_SIZE(output_enable)); + if (ret) { + return ret; + } + + ret = hw_codec_volume_adjust(0); + if (ret) { + return ret; + } + +#if ((CONFIG_AUDIO_DEV == GATEWAY) && (CONFIG_AUDIO_SOURCE_I2S)) + if (IS_ENABLED(CONFIG_WALKIE_TALKIE_DEMO)) { + ret = cs47l63_comm_reg_conf_write(pdm_mic_enable_configure, + ARRAY_SIZE(pdm_mic_enable_configure)); + if (ret) { + return ret; + } + } else { + ret = cs47l63_comm_reg_conf_write(line_in_enable, ARRAY_SIZE(line_in_enable)); + if (ret) { + return ret; + } + } +#endif /* ((CONFIG_AUDIO_DEV == GATEWAY) && (CONFIG_AUDIO_SOURCE_I2S)) */ + +#if ((CONFIG_AUDIO_DEV == HEADSET) && CONFIG_STREAM_BIDIRECTIONAL) + ret = cs47l63_comm_reg_conf_write(pdm_mic_enable_configure, + ARRAY_SIZE(pdm_mic_enable_configure)); + if (ret) { + return ret; + } +#endif /* ((CONFIG_AUDIO_DEV == HEADSET) && CONFIG_STREAM_BIDIRECTIONAL) */ + + /* Toggle FLL to start up CS47L63 */ + ret = cs47l63_comm_reg_conf_write(FLL_toggle, ARRAY_SIZE(FLL_toggle)); + if (ret) { + return ret; + } + + return 0; +} + +int hw_codec_soft_reset(void) +{ + int ret; + + ret = cs47l63_comm_reg_conf_write(output_disable, ARRAY_SIZE(output_disable)); + if (ret) { + return ret; + } + + ret = cs47l63_comm_reg_conf_write(soft_reset, ARRAY_SIZE(soft_reset)); + if (ret) { + return ret; + } + + return 0; +} + +int hw_codec_init(void) +{ + int ret; + + ret = cs47l63_comm_init(&cs47l63_driver); + if (ret) { + return ret; + } + + /* Run a soft reset on start to make sure all registers are default values */ + ret = cs47l63_comm_reg_conf_write(soft_reset, ARRAY_SIZE(soft_reset)); + if (ret) { + return ret; + } + cs47l63_driver.state = CS47L63_STATE_STANDBY; + + volume_msg_sub_thread_id = k_thread_create( + &volume_msg_sub_thread_data, volume_msg_sub_thread_stack, + CONFIG_VOLUME_MSG_SUB_STACK_SIZE, (k_thread_entry_t)volume_msg_sub_thread, NULL, + NULL, NULL, K_PRIO_PREEMPT(CONFIG_VOLUME_MSG_SUB_THREAD_PRIO), 0, K_NO_WAIT); + ret = k_thread_name_set(volume_msg_sub_thread_id, "VOLUME_MSG_SUB"); + ERR_CHK(ret); + + return 0; +} + +static int cmd_input(const struct shell *shell, size_t argc, char **argv) +{ + int ret; + uint8_t idx; + + enum hw_codec_input { + LINE_IN, + PDM_MIC, + NUM_INPUTS, + }; + + if (argc != 2) { + shell_error(shell, "Only one argument required, provided: %d", argc); + return -EINVAL; + } + + if ((CONFIG_AUDIO_DEV == GATEWAY) && IS_ENABLED(CONFIG_AUDIO_SOURCE_USB)) { + shell_error(shell, "Can't select PDM mic if audio source is USB"); + return -EINVAL; + } + + if ((CONFIG_AUDIO_DEV == HEADSET) && !IS_ENABLED(CONFIG_STREAM_BIDIRECTIONAL)) { + shell_error(shell, "Can't select input if headset is not in bidirectional stream"); + return -EINVAL; + } + + if (!isdigit((int)argv[1][0])) { + shell_error(shell, "Supplied argument is not numeric"); + return -EINVAL; + } + + idx = strtoul(argv[1], NULL, BASE_10); + + switch (idx) { + case LINE_IN: { + if (CONFIG_AUDIO_DEV == HEADSET) { + ret = cs47l63_comm_reg_conf_write(line_in_enable, + ARRAY_SIZE(line_in_enable)); + if (ret) { + shell_error(shell, "Failed to enable LINE-IN"); + return ret; + } + } + + ret = cs47l63_write_reg(&cs47l63_driver, CS47L63_ASP1TX1_INPUT1, 0x800012); + if (ret) { + shell_error(shell, "Failed to route LINE-IN to I2S"); + return ret; + } + + ret = cs47l63_write_reg(&cs47l63_driver, CS47L63_ASP1TX2_INPUT1, 0x800013); + if (ret) { + shell_error(shell, "Failed to route LINE-IN to I2S"); + return ret; + } + + shell_print(shell, "Selected LINE-IN as input"); + break; + } + case PDM_MIC: { + if (CONFIG_AUDIO_DEV == GATEWAY) { + ret = cs47l63_comm_reg_conf_write(pdm_mic_enable_configure, + ARRAY_SIZE(pdm_mic_enable_configure)); + if (ret) { + shell_error(shell, "Failed to enable PDM mic"); + return ret; + } + } + + ret = cs47l63_write_reg(&cs47l63_driver, CS47L63_ASP1TX1_INPUT1, 0x800010); + if (ret) { + shell_error(shell, "Failed to route PDM mic to I2S"); + return ret; + } + + ret = cs47l63_write_reg(&cs47l63_driver, CS47L63_ASP1TX2_INPUT1, 0x800011); + if (ret) { + shell_error(shell, "Failed to route PDM mic to I2S"); + return ret; + } + + shell_print(shell, "Selected PDM mic as input"); + break; + } + default: + shell_error(shell, "Invalid input"); + return -EINVAL; + } + + return 0; +} + +SHELL_STATIC_SUBCMD_SET_CREATE(hw_codec_cmd, + SHELL_COND_CMD(CONFIG_SHELL, input, NULL, + " Select input\n\t0: LINE_IN\n\t\t1: PDM_MIC", + cmd_input), + SHELL_SUBCMD_SET_END); + +SHELL_CMD_REGISTER(hw_codec, &hw_codec_cmd, "Change settings on HW codec", NULL); diff --git a/src/modules/hw_codec.h b/src/modules/hw_codec.h new file mode 100644 index 0000000..0c9ac98 --- /dev/null +++ b/src/modules/hw_codec.h @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2018 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _HW_CODEC_H_ +#define _HW_CODEC_H_ + +#include + +/** + * @brief Set volume on HW_CODEC + * + * @details Also unmutes the volume on HW_CODEC + * + * @param set_val Set the volume to a specific value. + * This range of the value is between 0 to 128. + * + * @return 0 if successful, error otherwise + */ +int hw_codec_volume_set(uint8_t set_val); + +/** + * @brief Adjust volume on HW_CODEC + * + * @details Also unmute the volume on HW_CODEC + * + * @param adjustment The adjustment in dB, can be negative or positive. + * If the value 0 is used, the previous known value will be + * written, default value will be used if no previous value + * exists + * + * @return 0 if successful, error otherwise + */ +int hw_codec_volume_adjust(int8_t adjustment); + +/** + * @brief Decrease output volume on HW_CODEC by 3 dB + * + * @details Also unmute the volume on HW_CODEC + * + * @return 0 if successful, error otherwise + */ +int hw_codec_volume_decrease(void); + +/** + * @brief Increase output volume on HW_CODEC by 3 dB + * + * @details Also unmute the volume on HW_CODEC + * + * @return 0 if successful, error otherwise + */ +int hw_codec_volume_increase(void); + +/** + * @brief Mute volume on HW_CODEC + * + * @return 0 if successful, error otherwise + */ +int hw_codec_volume_mute(void); + +/** + * @brief Unmute volume on HW_CODEC + * + * @return 0 if successful, error otherwise + */ +int hw_codec_volume_unmute(void); + +/** + * @brief Enable relevant settings in HW_CODEC to + * send and receive PCM data over I2S + * + * @note FLL1 must be toggled after I2S has started to enable HW_CODEC + * + * @return 0 if successful, error otherwise + */ +int hw_codec_default_conf_enable(void); + +/** + * @brief Reset HW_CODEC + * + * @note This will first disable output, then do a soft reset + * + * @return 0 if successful, error otherwise + */ +int hw_codec_soft_reset(void); + +/** + * @brief Initialize HW_CODEC + * + * @return 0 if successful, error otherwise + */ +int hw_codec_init(void); + +#endif /* _HW_CODEC_H_ */ diff --git a/src/modules/lc3_file.c b/src/modules/lc3_file.c new file mode 100644 index 0000000..ebbce1d --- /dev/null +++ b/src/modules/lc3_file.c @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "lc3_file.h" +#include "sd_card.h" + +#include +LOG_MODULE_REGISTER(sd_card_lc3_file, CONFIG_MODULE_SD_CARD_LC3_FILE_LOG_LEVEL); + +#define LC3_FILE_ID 0xCC1C + +static void lc3_header_print(struct lc3_file_header *header) +{ + if (header == NULL) { + LOG_ERR("Nullptr received"); + return; + } + + LOG_DBG("File ID: 0x%04x", header->file_id); + LOG_DBG("Header size: %d", header->hdr_size); + LOG_DBG("Sample rate: %d Hz", header->sample_rate * 100); + LOG_DBG("Bit rate: %d bps", header->bit_rate * 100); + LOG_DBG("Channels: %d", header->channels); + LOG_DBG("Frame duration: %d us", header->frame_duration * 10); + LOG_DBG("Num samples: %d", header->signal_len_lsb | (header->signal_len_msb << 16)); +} + +int lc3_header_get(struct lc3_file_ctx const *const file, struct lc3_file_header *header) +{ + if ((file == NULL) || (header == NULL)) { + LOG_ERR("Nullptr received"); + return -EINVAL; + } + + *header = file->lc3_header; + + return 0; +} + +int lc3_file_frame_get(struct lc3_file_ctx *file, uint8_t *buffer, size_t buffer_size) +{ + int ret; + + if ((file == NULL) || (buffer == NULL)) { + LOG_ERR("Nullptr received"); + return -EINVAL; + } + + /* Read frame header */ + uint16_t frame_header; + size_t frame_header_size = sizeof(frame_header); + + ret = sd_card_read((char *)&frame_header, &frame_header_size, &file->file_object); + if (ret) { + LOG_ERR("Failed to read frame header: %d", ret); + return ret; + } + + if ((frame_header_size == 0) || (frame_header == 0)) { + LOG_DBG("No more frames to read"); + return -ENODATA; + } + + LOG_DBG("Size of frame is %d", frame_header); + + if (buffer_size < frame_header) { + LOG_ERR("Buffer size too small: %d < %d", buffer_size, frame_header); + return -ENOMEM; + } + + /* Read frame data */ + size_t frame_size = frame_header; + + ret = sd_card_read((char *)buffer, &frame_size, &file->file_object); + if (ret) { + LOG_ERR("Failed to read frame data: %d", ret); + return ret; + } + + if (frame_size != frame_header) { + LOG_ERR("Frame size mismatch: %d != %d", frame_size, frame_header); + return -EIO; + } + + return 0; +} + +int lc3_file_open(struct lc3_file_ctx *file, const char *file_name) +{ + int ret; + size_t size = sizeof(file->lc3_header); + + if ((file == NULL) || (file_name == NULL)) { + LOG_ERR("Nullptr received"); + return -EINVAL; + } + + ret = sd_card_open(file_name, &file->file_object); + if (ret) { + LOG_ERR("Failed to open file: %d", ret); + return ret; + } + + /* Read LC3 header and store in struct */ + ret = sd_card_read((char *)&file->lc3_header, &size, &file->file_object); + if (ret) { + LOG_ERR("Failed to read the LC3 header: %d", ret); + return ret; + } + + /* Debug: Print header */ + lc3_header_print(&file->lc3_header); + + if (file->lc3_header.file_id != LC3_FILE_ID) { + LOG_ERR("Invalid file ID: 0x%04x", file->lc3_header.file_id); + return -EINVAL; + } + + return 0; +} + +int lc3_file_close(struct lc3_file_ctx *file) +{ + int ret; + + if (file == NULL) { + LOG_ERR("Nullptr received"); + return -EINVAL; + } + + ret = sd_card_close(&file->file_object); + if (ret) { + LOG_ERR("Failed to close file: %d", ret); + return ret; + } + + return ret; +} + +int lc3_file_init(void) +{ + int ret; + + ret = sd_card_init(); + if (ret) { + LOG_ERR("Failed to initialize SD card: %d", ret); + return ret; + } + + return 0; +} diff --git a/src/modules/lc3_file.h b/src/modules/lc3_file.h new file mode 100644 index 0000000..d2803bb --- /dev/null +++ b/src/modules/lc3_file.h @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef LC3_FILE_H__ +#define LC3_FILE_H__ + +#include +#include + +#include +#include + +struct lc3_file_header { + uint16_t file_id; /* Constant value, 0xCC1C */ + uint16_t hdr_size; /* Header size, 0x0012 */ + uint16_t sample_rate; /* Sample frequency / 100 */ + uint16_t bit_rate; /* Bit rate / 100 (total for all channels) */ + uint16_t channels; /* Number of channels */ + uint16_t frame_duration; /* Frame duration in ms * 100 */ + uint16_t rfu; /* Reserved for future use */ + uint16_t signal_len_lsb; /* Number of samples in signal, 16 LSB */ + uint16_t signal_len_msb; /* Number of samples in signal, 16 MSB (>> 16) */ +} __packed; + +struct lc3_file_ctx { + struct fs_file_t file_object; + struct lc3_file_header lc3_header; + uint32_t number_of_samples; +}; + +/** + * @brief Get the LC3 header from the file. + * + * @param[in] file Pointer to the file context. + * @param[out] header Pointer to the header structure to store the header. + * + * @retval -EINVAL Invalid file context. + * @retval 0 Success. + */ +int lc3_header_get(struct lc3_file_ctx const *const file, struct lc3_file_header *header); + +/** + * @brief Get the next LC3 frame from the file. + * + * @param[in] file Pointer to the file context. + * @param[out] buffer Pointer to the buffer to store the frame. + * @param[in] buffer_size Size of the buffer. + * + * @retval -ENODATA No more frames to read. + * @retval 0 Success. + */ +int lc3_file_frame_get(struct lc3_file_ctx *file, uint8_t *buffer, size_t buffer_size); + +/** + * @brief Open a LC3 file for reading + * + * @details Opens the file and reads the LC3 header. + * + * @param[in] file Pointer to the file context. + * @param[in] file_name Name of the file to open. + * + * @retval -ENODEV SD card init failed. SD card likely not inserted. + * @retval 0 Success. + */ +int lc3_file_open(struct lc3_file_ctx *file, const char *file_name); + +/** + * @brief Close a LC3 file. + * + * @param[in] file Pointer to the file context. + * + * @retval -EPERM SD card operation is not ongoing. + * @retval 0 Success. + */ +int lc3_file_close(struct lc3_file_ctx *file); + +/** + * @brief Initialize the LC3 file module. + * + * Initializes the SD card and mounts the file system. + * + * @retval -ENODEV SD card init failed. SD card likely not inserted. + * @retval 0 Success. + */ +int lc3_file_init(void); + +#endif /* LC3_FILE_H__ */ diff --git a/src/modules/lc3_streamer.c b/src/modules/lc3_streamer.c new file mode 100644 index 0000000..347f633 --- /dev/null +++ b/src/modules/lc3_streamer.c @@ -0,0 +1,545 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "lc3_streamer.h" +#include "lc3_file.h" + +#include +#include +#include + +#include +LOG_MODULE_REGISTER(lc3_streamer, CONFIG_MODULE_SD_CARD_LC3_STREAMER_LOG_LEVEL); + +K_THREAD_STACK_DEFINE(lc3_streamer_work_q_stack_area, CONFIG_SD_CARD_LC3_STREAMER_STACK_SIZE); + +struct k_work_q lc3_streamer_work_q; + +#define LC3_STREAMER_BUFFER_NUM_FRAMES 2 + +#if CONFIG_SD_CARD_LC3_STREAMER_MAX_NUM_STREAMS > UINT8_MAX +#error "CONFIG_SD_CARD_LC3_STREAMER_MAX_NUM_STREAMS must be less than or equal to UINT8_MAX" +#endif + +enum lc3_stream_states { + /* Stream ready to load file and start streaming */ + STREAM_IDLE = 0, + + /* Stream currently playing */ + STREAM_PLAYING, + + /* The last frame in the file is loaded and accessible for the caller */ + STREAM_PLAYING_LAST_FRAME, + + /* Stream has ended. Resources need to be cleaned for stream to be restarted. */ + STREAM_ENDED, +}; + +struct lc3_stream { + /* State of the stream */ + enum lc3_stream_states state; + + /* Flag set at initialization to restart a stream when it reaches end. */ + bool loop_stream; + + /* Pointer to the data_fifo buffer that holds valid, readable LC3 data */ + char *active_buffer; + + /* Filename of the file being streamed */ + char filename[CONFIG_FS_FATFS_MAX_LFN]; + + /* LC3 file context */ + struct lc3_file_ctx file; + + /* Work queue context */ + struct k_work work; + + /* data_fifo context */ + struct data_fifo fifo; + + /* Buffers used by data_fifo for allocating memory*/ + char msgq_buffer[LC3_STREAMER_BUFFER_NUM_FRAMES * sizeof(struct data_fifo_msgq)]; + char slab_buffer[LC3_STREAMER_BUFFER_NUM_FRAMES * + CONFIG_SD_CARD_LC3_STREAMER_MAX_FRAME_SIZE]; +}; + +static struct lc3_stream streams[CONFIG_SD_CARD_LC3_STREAMER_MAX_NUM_STREAMS]; +#if (CONFIG_SD_CARD_LC3_STREAMER_MAX_NUM_STREAMS > UINT8_MAX) +#error "CONFIG_SD_CARD_LC3_STREAMER_MAX_NUM_STREAMS is larger than UINT8_MAX" +#endif + +static bool initialized; + +/** + * @brief Close the stream and free all resources. + * + * @param[in] stream Pointer to the stream to close. + * + * @retval 0 Success, negative value otherwise. + */ +static int stream_close(struct lc3_stream *stream) +{ + int ret; + + if (stream == NULL) { + LOG_ERR("Nullptr received for stream"); + return -EINVAL; + } + + if (stream->active_buffer != NULL) { + data_fifo_block_free(&stream->fifo, (void *)stream->active_buffer); + stream->active_buffer = NULL; + } + + ret = lc3_file_close(&stream->file); + if (ret) { + LOG_ERR("Failed to close file %d", ret); + } + + if (stream->fifo.initialized) { + ret = data_fifo_uninit(&stream->fifo); + if (ret) { + LOG_ERR("Failed to empty data fifo %d", ret); + } + } + + stream->state = STREAM_IDLE; + memset(stream->filename, 0, sizeof(stream->filename)); + + return 0; +} + +/** + * @brief Get the next frame from the file and put it in the fifo. + * + * @param[in] stream Pointer to the stream to get the frame for. + * + * @retval 0 Success, negative value otherwise. + */ +static int put_next_frame_to_fifo(struct lc3_stream *stream) +{ + int ret; + char *data_ptr; + + ret = data_fifo_pointer_first_vacant_get(&stream->fifo, (void **)&data_ptr, K_NO_WAIT); + if (ret) { + LOG_ERR("Failed to get first vacant block %d", ret); + return ret; + } + + ret = lc3_file_frame_get(&stream->file, data_ptr, + CONFIG_SD_CARD_LC3_STREAMER_MAX_FRAME_SIZE); + if (ret) { + if (ret != -ENODATA) { + LOG_ERR("Failed to get frame from file %d", ret); + } + + data_fifo_block_free(&stream->fifo, (void *)data_ptr); + return ret; + } + + ret = data_fifo_block_lock(&stream->fifo, (void **)&data_ptr, + CONFIG_SD_CARD_LC3_STREAMER_MAX_FRAME_SIZE); + if (ret) { + LOG_ERR("Failed to lock block %d", ret); + return ret; + } + + return 0; +} + +/** + * @brief Loop the stream by closing and re-opening the file, and loading the first frame. + * + * @param[in] stream Pointer to the stream to loop. + * + * @retval 0 Success, negative value otherwise. + */ +static int stream_loop(struct lc3_stream *stream) +{ + int ret; + + ret = lc3_file_close(&stream->file); + if (ret) { + LOG_ERR("Failed to close file %d", ret); + return ret; + } + + ret = lc3_file_open(&stream->file, stream->filename); + if (ret) { + LOG_ERR("Failed to open file %s: %d", stream->filename, ret); + return ret; + } + + ret = put_next_frame_to_fifo(stream); + if (ret) { + LOG_ERR("Failed to put first frame after loop to fifo %d", ret); + + int lc3_file_ret = lc3_file_close(&stream->file); + + if (lc3_file_ret) { + LOG_ERR("Failed to close file %d", lc3_file_ret); + } + + return ret; + } + + return 0; +} + +/** + * @brief Load the next frame from the stream to the fifo. This is the work queue function. + * + * @param[in] work Pointer to the work queue item. + */ +static void next_frame_load(struct k_work *work) +{ + int ret; + struct lc3_stream *stream = CONTAINER_OF(work, struct lc3_stream, work); + + ret = put_next_frame_to_fifo(stream); + if (ret == -ENODATA) { + LOG_DBG("End of stream"); + if (stream->loop_stream) { + ret = stream_loop(stream); + if (ret) { + LOG_ERR("Failed to loop stream %d", ret); + stream->state = STREAM_ENDED; + } + } else { + stream->state = STREAM_PLAYING_LAST_FRAME; + } + } else if (ret) { + LOG_ERR("Failed to put next frame to fifo %d", ret); + stream->state = STREAM_ENDED; + } +} + +int lc3_streamer_next_frame_get(const uint8_t streamer_idx, const uint8_t **const frame_buffer) +{ + int ret; + char *data_ptr; + size_t data_len; + + if (!initialized) { + LOG_ERR("LC3 streamer not initialized"); + return -EFAULT; + } + + if (streamer_idx >= ARRAY_SIZE(streams)) { + LOG_ERR("Invalid streamer index %d", streamer_idx); + return -EINVAL; + } + + struct lc3_stream *stream = &streams[streamer_idx]; + + if ((stream->state != STREAM_PLAYING) && (stream->state != STREAM_PLAYING_LAST_FRAME)) { + LOG_ERR("Stream not playing"); + return -EFAULT; + } + + if (stream->active_buffer != NULL) { + data_fifo_block_free(&stream->fifo, (void *)stream->active_buffer); + stream->active_buffer = NULL; + } + + if (stream->state == STREAM_PLAYING_LAST_FRAME) { + LOG_INF("Stream ended"); + stream->state = STREAM_ENDED; + return -ENODATA; + } + + ret = data_fifo_pointer_last_filled_get(&stream->fifo, (void **)&data_ptr, &data_len, + K_NO_WAIT); + if (ret) { + if (ret == -ENOMSG) { + LOG_DBG("Next block is not ready %d", ret); + } else { + LOG_ERR("Failed to get last filled block %d", ret); + } + return ret; + } + + *frame_buffer = (uint8_t *)data_ptr; + stream->active_buffer = data_ptr; + + ret = k_work_submit_to_queue(&lc3_streamer_work_q, &stream->work); + if (ret < 0) { + LOG_ERR("Failed to submit work item %d", ret); + return ret; + } + + return 0; +} + +bool lc3_streamer_file_compatible_check(const char *const filename, + const struct lc3_stream_cfg *const cfg) +{ + int ret; + bool result = true; + + if (filename == NULL || cfg == NULL) { + LOG_ERR("NULL pointer received"); + return false; + } + + if (strlen(filename) > CONFIG_FS_FATFS_MAX_LFN - 1) { + LOG_ERR("Filename too long"); + return false; + } + + struct lc3_file_ctx file; + + ret = lc3_file_open(&file, filename); + if (ret) { + LOG_ERR("Failed to open file %d", ret); + return false; + } + + struct lc3_file_header header; + + ret = lc3_header_get(&file, &header); + if (ret) { + LOG_WRN("Failed to get header %d", ret); + return false; + } + + if ((header.sample_rate * 100) != cfg->sample_rate_hz) { + LOG_WRN("Sample rate mismatch %d Hz != %d Hz", (header.sample_rate * 100), + cfg->sample_rate_hz); + result = false; + } + + if ((header.bit_rate * 100) != cfg->bit_rate_bps) { + LOG_WRN("Bit rate mismatch %d bps != %d bps", (header.bit_rate * 100), + cfg->bit_rate_bps); + result = false; + } + + if ((header.frame_duration * 10) != cfg->frame_duration_us) { + LOG_WRN("Frame duration mismatch %d us != %d us", (header.frame_duration * 10), + cfg->frame_duration_us); + result = false; + } + + ret = lc3_file_close(&file); + if (ret) { + LOG_ERR("Failed to close file %d", ret); + } + + return result; +} + +int lc3_streamer_stream_register(const char *const filename, uint8_t *const streamer_idx, + const bool loop) +{ + int ret; + + if (!initialized) { + LOG_ERR("LC3 streamer not initialized"); + return -EFAULT; + } + + if ((streamer_idx == NULL) || (filename == NULL)) { + LOG_ERR("Nullptr received for streamer_idx or filename"); + return -EINVAL; + } + + /* Check that there's room for the filename and a NULL terminating char */ + if (strlen(filename) > (ARRAY_SIZE(streams[*streamer_idx].filename) - 1)) { + LOG_ERR("Filename too long"); + return -EINVAL; + } + + bool free_slot_found = false; + + for (uint8_t i = 0; i < ARRAY_SIZE(streams); i++) { + if (streams[i].state == STREAM_IDLE) { + LOG_DBG("Found free stream slot %d", i); + *streamer_idx = i; + free_slot_found = true; + break; + } + } + + if (!free_slot_found) { + LOG_ERR("No stream slot is available"); + return -EAGAIN; + } + + ret = lc3_file_open(&streams[*streamer_idx].file, filename); + if (ret) { + LOG_ERR("Failed to open file %d", ret); + return ret; + } + + strcpy(streams[*streamer_idx].filename, filename); + + ret = data_fifo_init(&streams[*streamer_idx].fifo); + if (ret) { + LOG_ERR("Failed to initialize data fifo %d", ret); + int lc3_file_ret; + + lc3_file_ret = lc3_file_close(&streams[*streamer_idx].file); + if (lc3_file_ret) { + LOG_ERR("Failed to close file %d", lc3_file_ret); + } + + return ret; + } + + k_work_init(&streams[*streamer_idx].work, next_frame_load); + + ret = put_next_frame_to_fifo(&streams[*streamer_idx]); + if (ret) { + LOG_ERR("Failed to put next frame to fifo %d", ret); + streams[*streamer_idx].state = STREAM_ENDED; + return ret; + } + + streams[*streamer_idx].state = STREAM_PLAYING; + streams[*streamer_idx].loop_stream = loop; + + return 0; +} + +uint8_t lc3_streamer_num_active_streams(void) +{ + uint8_t num_active = 0; + + if (!initialized) { + return 0; + } + + for (int i = 0; i < ARRAY_SIZE(streams); i++) { + if ((streams[i].state == STREAM_PLAYING) || + (streams[i].state == STREAM_PLAYING_LAST_FRAME)) { + num_active++; + } + } + + return num_active; +} + +int lc3_streamer_file_path_get(const uint8_t streamer_idx, char *const path, const size_t path_len) +{ + if (streamer_idx >= ARRAY_SIZE(streams)) { + LOG_ERR("Invalid streamer index %d", streamer_idx); + return -EINVAL; + } + + if (path == NULL) { + LOG_ERR("Nullptr received for path"); + return -EINVAL; + } + + if (path_len < strlen(streams[streamer_idx].filename)) { + LOG_WRN("Path buffer too small"); + } + + strncpy(path, streams[streamer_idx].filename, path_len); + + return 0; +} + +bool lc3_streamer_is_looping(const uint8_t streamer_idx) +{ + if (streamer_idx >= ARRAY_SIZE(streams)) { + LOG_ERR("Invalid streamer index %d", streamer_idx); + return false; + } + + return streams[streamer_idx].loop_stream; +} + +int lc3_streamer_stream_close(const uint8_t streamer_idx) +{ + int ret; + + if (!initialized) { + LOG_ERR("LC3 streamer not initialized"); + return -EFAULT; + } + + if (streamer_idx >= ARRAY_SIZE(streams)) { + LOG_ERR("Invalid streamer index %d", streamer_idx); + return -EINVAL; + } + + ret = stream_close(&streams[streamer_idx]); + if (ret) { + LOG_ERR("Failed to close stream %d", ret); + return ret; + } + + return 0; +} + +int lc3_streamer_close_all_streams(void) +{ + int ret; + + if (!initialized) { + LOG_ERR("LC3 streamer not initialized"); + return -EFAULT; + } + + ret = k_work_queue_drain(&lc3_streamer_work_q, false); + if (ret < 0) { + LOG_ERR("Failed to drain work queue %d", ret); + return ret; + } + + int first_error = 0; + + for (int i = 0; i < ARRAY_SIZE(streams); i++) { + ret = stream_close(&streams[i]); + if (ret) { + LOG_ERR("Failed to close stream %d %d", i, ret); + if (!first_error) { + first_error = ret; + } + } + } + + return first_error; +} + +int lc3_streamer_init(void) +{ + int ret; + + if (initialized) { + LOG_WRN("LC3 streamer already initialized"); + return 0; + } + + for (int i = 0; i < ARRAY_SIZE(streams); i++) { + streams[i].fifo.msgq_buffer = streams[i].msgq_buffer; + streams[i].fifo.slab_buffer = streams[i].slab_buffer; + streams[i].fifo.block_size_max = WB_UP(CONFIG_SD_CARD_LC3_STREAMER_MAX_FRAME_SIZE); + streams[i].fifo.elements_max = LC3_STREAMER_BUFFER_NUM_FRAMES; + streams[i].fifo.initialized = false; + streams[i].active_buffer = NULL; + streams[i].state = STREAM_IDLE; + } + + ret = lc3_file_init(); + if (ret) { + LOG_ERR("Failed to initialize LC3 file module %d", ret); + return ret; + } + + k_work_queue_init(&lc3_streamer_work_q); + k_work_queue_start(&lc3_streamer_work_q, lc3_streamer_work_q_stack_area, + K_THREAD_STACK_SIZEOF(lc3_streamer_work_q_stack_area), + CONFIG_SD_CARD_LC3_STREAMER_THREAD_PRIO, NULL); + k_thread_name_set(&lc3_streamer_work_q.thread, "lc3_streamer_work_q"); + + initialized = true; + + return 0; +} diff --git a/src/modules/lc3_streamer.h b/src/modules/lc3_streamer.h new file mode 100644 index 0000000..d841772 --- /dev/null +++ b/src/modules/lc3_streamer.h @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef LC3_STREAMER_H +#define LC3_STREAMER_H + +#include +#include +#include +#include + +struct lc3_stream_cfg { + uint32_t sample_rate_hz; + uint32_t bit_rate_bps; + uint32_t frame_duration_us; +}; + +/** + * @brief Get the next frame for the stream. + * + * @details Populates a pointer to the buffer holding the next frame. This buffer is valid until + * the next call to this function. or stream is closed. + * + * @param[in] streamer_idx Index of the streamer to get the next frame from. + * @param[out] frame_buffer Pointer to the buffer holding the next frame. + * + * @retval 0 Success. + * @retval -EINVAL Invalid streamer index. + * @retval -ENODATA No more frames to read, call lc3_streamer_end_stream to clean context. + * @retval -EFAULT Module has not been initialized, or stream is not in a valid state. If + * stream has been playing an error has occurred preventing from further + * streaming. Call lc3_streamer_end_stream to clean context. + */ +int lc3_streamer_next_frame_get(const uint8_t streamer_idx, const uint8_t **const frame_buffer); + +/** + * @brief Verify that the LC3 header matches the stream configuration. + * + * @details Verifies that the file is valid and can be used by the LC3 streamer. + * Since there is no standard header for LC3 files, the header might be different than + * what is defined in the struct lc3_file_header. + * + * @param[in] filename Name of the file to verify. + * @param[in] cfg Stream configuration to compare against. + * + * @retval true Success. + * @retval false Header is not matching the configuration. + */ +bool lc3_streamer_file_compatible_check(const char *const filename, + const struct lc3_stream_cfg *const cfg); + +/** + * @brief Register a new stream that will be played by the LC3 streamer. + * + * @details Opens the specified file on the SD card, and prepares data so the stream can be + * started. When a frame is read by the application the next frame will be read from the + * file to ensure it's ready for streaming when needed. + * + * @param[in] filename Name of the file to open. + * @param[out] streamer_idx Index of the streamer that was registered. + * @param[in] loop If true, the stream will be looped when it reaches the end. + * + * @retval 0 Success. + * @retval -EINVAL Invalid filename or streamer_idx. + * @retval -EAGAIN No stream slot is available. + * @retval -EFAULT Module has not been initialized. + */ +int lc3_streamer_stream_register(const char *const filename, uint8_t *const streamer_idx, + const bool loop); + +/** + * @brief Get the number of active streams. + * + * @retval The number of streams currently playing. + */ +uint8_t lc3_streamer_num_active_streams(void); + +/** + * @brief Get the file path for a stream. + * + * @details If path buffer is smaller than the length of the actual path, the path will be + * truncated. + * + * @param[in] streamer_idx Index of the streamer. + * @param[out] path Pointer for string to store file path in. + * @param[in] path_len Length of string buffer. + * + * @retval -EINVAL Null pointers or invalid index given. + * @retval 0 Success. + */ +int lc3_streamer_file_path_get(const uint8_t streamer_idx, char *const path, const size_t path_len); + +/** + * @brief Check if a stream is configured to loop. + * + * @param[in] streamer_idx Index of the streamer. + * + * @retval true Streamer is configured to loop. + * @retval false Streamer is not configured to loop, or the streamer index is too high. + */ +bool lc3_streamer_is_looping(const uint8_t streamer_idx); + +/** + * @brief End a stream that's playing. + * + * @details Stops the streamer from playing the stream. Any open contexts will be closed, and any + * active frame buffers will become invalid. + * + * @param[in] streamer_idx Index of the streamer to end. + * + * @retval 0 Success. + * @retval -EINVAL Invalid streamer index. + */ +int lc3_streamer_stream_close(const uint8_t streamer_idx); + +/** + * @brief Close all streams and drain the work queue. + * + * @retval -EFAULT Module has not been initialized. + * @retval 0 Success, other negative values are errors from k_work. + */ +int lc3_streamer_close_all_streams(void); + +/** + * @brief Initializes the LC3 streamer. + * + * @details Initializes the lc3_file module and the SD card driver. Initializes an internal work + * queue that is used to schedule fetching new frames from the SD card. + * + * @retval -EALREADY Streamer is already initialized. + * @retval 0 Success, other negative values are errors from lc3_file module. + */ +int lc3_streamer_init(void); + +#endif /* LC3_STREAMER_H */ diff --git a/src/modules/led.c b/src/modules/led.c new file mode 100644 index 0000000..79a1385 --- /dev/null +++ b/src/modules/led.c @@ -0,0 +1,300 @@ +/* + * Copyright (c) 2018 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "led.h" + +#include +#include +#include +#include +#include +#include + +#include "macros_common.h" + +#include +LOG_MODULE_REGISTER(led, CONFIG_MODULE_LED_LOG_LEVEL); + +#define BLINK_FREQ_MS 1000 +/* Maximum number of LED_UNITS. 1 RGB LED = 1 UNIT of 3 LEDS */ +#define LED_UNIT_MAX 10 +#define NUM_COLORS_RGB 3 +#define BASE_10 10 +#define DT_LABEL_AND_COMMA(node_id) DT_PROP(node_id, label), +#define GPIO_DT_SPEC_GET_AND_COMMA(node_id) GPIO_DT_SPEC_GET(node_id, gpios), + +/* The following arrays are populated compile time from the .dts*/ +static const char *const led_labels[] = {DT_FOREACH_CHILD(DT_PATH(leds), DT_LABEL_AND_COMMA)}; + +static const struct gpio_dt_spec leds[] = { + DT_FOREACH_CHILD(DT_PATH(leds), GPIO_DT_SPEC_GET_AND_COMMA)}; + +enum led_type { + LED_MONOCHROME, + LED_COLOR, +}; + +struct user_config { + bool blink; + enum led_color color; +}; + +struct led_unit_cfg { + uint8_t led_no; + enum led_type unit_type; + union { + const struct gpio_dt_spec *mono; + const struct gpio_dt_spec *color[NUM_COLORS_RGB]; + } type; + struct user_config user_cfg; +}; + +static uint8_t leds_num; +static bool initialized; +static struct led_unit_cfg led_units[LED_UNIT_MAX]; + +/** + * @brief Configures fields for a RGB LED + */ +static int configure_led_color(uint8_t led_unit, uint8_t led_color, uint8_t led) +{ + if (!device_is_ready(leds[led].port)) { + LOG_ERR("LED GPIO controller not ready"); + return -ENODEV; + } + + led_units[led_unit].type.color[led_color] = &leds[led]; + led_units[led_unit].unit_type = LED_COLOR; + + return gpio_pin_configure_dt(led_units[led_unit].type.color[led_color], + GPIO_OUTPUT_INACTIVE); +} + +/** + * @brief Configures fields for a monochrome LED + */ +static int config_led_monochrome(uint8_t led_unit, uint8_t led) +{ + if (!device_is_ready(leds[led].port)) { + LOG_ERR("LED GPIO controller not ready"); + return -ENODEV; + } + + led_units[led_unit].type.mono = &leds[led]; + led_units[led_unit].unit_type = LED_MONOCHROME; + + return gpio_pin_configure_dt(led_units[led_unit].type.mono, GPIO_OUTPUT_INACTIVE); +} + +/** + * @brief Parses the device tree for LED settings. + */ +static int led_device_tree_parse(void) +{ + int ret; + + for (uint8_t i = 0; i < leds_num; i++) { + char *end_ptr = NULL; + uint32_t led_unit = strtoul(led_labels[i], &end_ptr, BASE_10); + + if (led_labels[i] == end_ptr) { + LOG_ERR("No match for led unit. The dts is likely not properly formatted"); + return -ENXIO; + } + + if (strstr(led_labels[i], "LED_RGB_RED")) { + ret = configure_led_color(led_unit, RED, i); + if (ret) { + return ret; + } + + } else if (strstr(led_labels[i], "LED_RGB_GREEN")) { + ret = configure_led_color(led_unit, GRN, i); + if (ret) { + return ret; + } + + } else if (strstr(led_labels[i], "LED_RGB_BLUE")) { + ret = configure_led_color(led_unit, BLU, i); + if (ret) { + return ret; + } + + } else if (strstr(led_labels[i], "LED_MONO")) { + ret = config_led_monochrome(led_unit, i); + if (ret) { + return ret; + } + } else { + LOG_ERR("No color identifier for LED %d LED unit %d", i, led_unit); + return -ENODEV; + } + } + return 0; +} + +/** + * @brief Internal handling to set the status of a led unit + */ +static int led_set_int(uint8_t led_unit, enum led_color color) +{ + int ret; + + if (led_units[led_unit].unit_type == LED_MONOCHROME) { + if (color) { + ret = gpio_pin_set_dt(led_units[led_unit].type.mono, 1); + if (ret) { + return ret; + } + } else { + ret = gpio_pin_set_dt(led_units[led_unit].type.mono, 0); + if (ret) { + return ret; + } + } + } else { + for (uint8_t i = 0; i < NUM_COLORS_RGB; i++) { + if (color & BIT(i)) { + ret = gpio_pin_set_dt(led_units[led_unit].type.color[i], 1); + if (ret) { + return ret; + } + } else { + ret = gpio_pin_set_dt(led_units[led_unit].type.color[i], 0); + if (ret) { + return ret; + } + } + } + } + + return 0; +} + +static void led_blink_work_handler(struct k_work *work); + +K_WORK_DEFINE(led_blink_work, led_blink_work_handler); + +/** + * @brief Submit a k_work on timer expiry. + */ +void led_blink_timer_handler(struct k_timer *dummy) +{ + k_work_submit(&led_blink_work); +} + +K_TIMER_DEFINE(led_blink_timer, led_blink_timer_handler, NULL); + +/** + * @brief Periodically invoked by the timer to blink LEDs. + */ +static void led_blink_work_handler(struct k_work *work) +{ + int ret; + static bool on_phase; + + for (uint8_t i = 0; i < leds_num; i++) { + if (led_units[i].user_cfg.blink) { + if (on_phase) { + ret = led_set_int(i, led_units[i].user_cfg.color); + ERR_CHK(ret); + } else { + ret = led_set_int(i, LED_COLOR_OFF); + ERR_CHK(ret); + } + } + } + + on_phase = !on_phase; +} + +static int led_set(uint8_t led_unit, enum led_color color, bool blink) +{ + int ret; + + if (!initialized) { + return -EPERM; + } + + ret = led_set_int(led_unit, color); + if (ret) { + return ret; + } + + led_units[led_unit].user_cfg.blink = blink; + led_units[led_unit].user_cfg.color = color; + + return 0; +} + +int led_on(uint8_t led_unit, ...) +{ + if (led_units[led_unit].unit_type == LED_MONOCHROME) { + return led_set(led_unit, LED_ON, LED_SOLID); + } + + va_list args; + + va_start(args, led_unit); + int color = va_arg(args, int); + + va_end(args); + + if (color <= 0 || color >= LED_COLOR_NUM) { + LOG_ERR("Invalid color code %d", color); + return -EINVAL; + } + return led_set(led_unit, color, LED_SOLID); +} + +int led_blink(uint8_t led_unit, ...) +{ + if (led_units[led_unit].unit_type == LED_MONOCHROME) { + return led_set(led_unit, LED_ON, LED_BLINK); + } + + va_list args; + + va_start(args, led_unit); + + int color = va_arg(args, int); + + va_end(args); + + if (color <= 0 || color >= LED_COLOR_NUM) { + LOG_ERR("Invalid color code %d", color); + return -EINVAL; + } + + return led_set(led_unit, color, LED_BLINK); +} + +int led_off(uint8_t led_unit) +{ + return led_set(led_unit, LED_COLOR_OFF, LED_SOLID); +} + +int led_init(void) +{ + int ret; + + if (initialized) { + return -EPERM; + } + + __ASSERT(ARRAY_SIZE(leds) != 0, "No LEDs found in dts"); + + leds_num = ARRAY_SIZE(leds); + + ret = led_device_tree_parse(); + if (ret) { + return ret; + } + + k_timer_start(&led_blink_timer, K_MSEC(BLINK_FREQ_MS / 2), K_MSEC(BLINK_FREQ_MS / 2)); + initialized = true; + return 0; +} diff --git a/src/modules/led.h b/src/modules/led.h new file mode 100644 index 0000000..59dff83 --- /dev/null +++ b/src/modules/led.h @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2018 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _LED_H_ +#define _LED_H_ + +#include + +#define LED_APP_RGB 0 +#define LED_NET_RGB 1 +#define LED_APP_1_BLUE 2 +#define LED_APP_2_GREEN 3 +#define LED_APP_3_GREEN 4 + +#define RED 0 +#define GREEN 1 +#define BLUE 2 + +#define GRN GREEN +#define BLU BLUE + +enum led_color { + LED_COLOR_OFF, /* 000 */ + LED_COLOR_RED, /* 001 */ + LED_COLOR_GREEN, /* 010 */ + LED_COLOR_YELLOW, /* 011 */ + LED_COLOR_BLUE, /* 100 */ + LED_COLOR_MAGENTA, /* 101 */ + LED_COLOR_CYAN, /* 110 */ + LED_COLOR_WHITE, /* 111 */ + LED_COLOR_NUM, +}; + +#define LED_ON LED_COLOR_WHITE + +#define LED_BLINK true +#define LED_SOLID false + +/** + * @brief Set the state of a given LED unit to blink. + * + * @note A led unit is defined as an RGB LED or a monochrome LED. + * + * @param led_unit Selected LED unit. Defines are located in board.h. + * @note If the given LED unit is an RGB LED, color must be + * provided as a single vararg. See led_color. + * For monochrome LEDs, the vararg will be ignored. + * Using a LED unit assigned to another core will do nothing and return 0. + * @return 0 on success. + * -EPERM if the module has not been initialized. + * -EINVAL if the color argument is illegal. + * Other errors from underlying drivers. + */ +int led_blink(uint8_t led_unit, ...); + +/** + * @brief Turn the given LED unit on. + * + * @note A led unit is defined as an RGB LED or a monochrome LED. + * + * @param led_unit Selected LED unit. Defines are located in board.h. + * @note If the given LED unit is an RGB LED, color must be + * provided as a single vararg. See led_color. + * For monochrome LEDs, the vararg will be ignored. + * Using a LED unit assigned to another core will do nothing and return 0. + * @return 0 on success. + * -EPERM if the module has not been initialized. + * -EINVAL if the color argument is illegal. + * Other errors from underlying drivers. + */ +int led_on(uint8_t led_unit, ...); + +/** + * @brief Set the state of a given LED unit to off. + * + * @note A led unit is defined as an RGB LED or a monochrome LED. + * Using a LED unit assigned to another core will do nothing and return 0. + * + * @param led_unit Selected LED unit. Defines are located in board.h. + * @return 0 on success. + * -EPERM if the module has not been initialized. + * -EINVAL if the color argument is illegal. + * Other errors from underlying drivers. + */ +int led_off(uint8_t led_unit); + +/** + * @brief Initialise the LED module. + * + * @note This will parse the .dts files and configure all LEDs. + * + * @return 0 on success. + * -EPERM if already initialized. + * -ENXIO if a LED is missing unit number in dts. + * -ENODEV if a LED is missing color identifier. + */ +int led_init(void); + +#endif /* _LED_H_ */ diff --git a/src/modules/power_meas.c b/src/modules/power_meas.c new file mode 100644 index 0000000..836dbfe --- /dev/null +++ b/src/modules/power_meas.c @@ -0,0 +1,231 @@ +/* + * Copyright (c) 2022 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include +#include +#include +#include +#include +#include + +LOG_MODULE_REGISTER(power_meas, CONFIG_MODULE_POWER_LOG_LEVEL); + +#define POWER_RAIL_NUM 4U +static k_tid_t pwr_meas_thread_id; + +struct power_rail_config { + const char *name; + const struct device *sensor; +}; + +static const struct power_rail_config rail_config[POWER_RAIL_NUM] = { + {"VBAT", DEVICE_DT_GET(DT_NODELABEL(vbat_sensor))}, + {"VDD1_CODEC", DEVICE_DT_GET(DT_NODELABEL(vdd1_codec_sensor))}, + {"VDD2_CODEC", DEVICE_DT_GET(DT_NODELABEL(vdd2_codec_sensor))}, + {"VDD2_NRF", DEVICE_DT_GET(DT_NODELABEL(vdd2_nrf_sensor))}, +}; +static bool rail_enabled[POWER_RAIL_NUM]; + +K_THREAD_STACK_DEFINE(meas_stack, CONFIG_POWER_MEAS_STACK_SIZE); +static struct k_thread meas_thread; +K_SEM_DEFINE(meas_sem, 0, 1); +static bool meas_enabled; + +/** + * @brief Read a power rail data (voltage/current/power) and log results. + * + * @param[in] config Power rail configuration. + * + * @retval 0 on success + * @retval -errno on sensor fetch/get error. + */ +static int read_and_log(const struct power_rail_config *config) +{ + int ret; + struct sensor_value voltage, current, power; + + ret = sensor_sample_fetch(config->sensor); + if (ret < 0) { + return ret; + } + + ret = sensor_channel_get(config->sensor, SENSOR_CHAN_VOLTAGE, &voltage); + if (ret < 0) { + return ret; + } + + ret = sensor_channel_get(config->sensor, SENSOR_CHAN_CURRENT, ¤t); + if (ret < 0) { + return ret; + } + + ret = sensor_channel_get(config->sensor, SENSOR_CHAN_POWER, &power); + if (ret < 0) { + return ret; + } + + LOG_INF("%-10s:\t%.3fV, %06.3fmA, %06.3fmW", config->name, sensor_value_to_double(&voltage), + sensor_value_to_double(¤t) * 1000.0, + sensor_value_to_double(&power) * 1000.0); + + return 0; +} + +/** @brief Measurement thread */ +static void meas_thread_fn(void *dummy1, void *dummy2, void *dummy3) +{ + ARG_UNUSED(dummy1); + ARG_UNUSED(dummy2); + ARG_UNUSED(dummy3); + + while (1) { + if (!meas_enabled) { + k_sem_take(&meas_sem, K_FOREVER); + } + + for (size_t i = 0U; i < POWER_RAIL_NUM; i++) { + int ret; + + if (!rail_enabled[i]) { + continue; + } + + ret = read_and_log(&rail_config[i]); + if (ret < 0) { + LOG_ERR("Could not read %s", rail_config->name); + } + } + + k_msleep(CONFIG_POWER_MEAS_INTERVAL_MS); + } +} + +static int power_meas_init(void) +{ + int ret; + + /* check if all sensors are ready */ + for (size_t i = 0U; i < POWER_RAIL_NUM; i++) { + if (!device_is_ready(rail_config[i].sensor)) { + LOG_ERR("INA231 device not ready: %s\n", rail_config->name); + return -ENODEV; + } + } + + /* enable all sensors and measurement if configured */ + if (IS_ENABLED(CONFIG_POWER_MEAS_START_ON_BOOT)) { + meas_enabled = true; + + for (size_t i = 0U; i < POWER_RAIL_NUM; i++) { + rail_enabled[i] = true; + } + } + + /* start measurement thread */ + pwr_meas_thread_id = k_thread_create( + &meas_thread, meas_stack, CONFIG_POWER_MEAS_STACK_SIZE, meas_thread_fn, NULL, NULL, + NULL, K_PRIO_PREEMPT(CONFIG_POWER_MEAS_THREAD_PRIO), 0, K_NO_WAIT); + ret = k_thread_name_set(pwr_meas_thread_id, "pwr_meas"); + if (ret) { + return ret; + } + + return 0; +} + +SYS_INIT(power_meas_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY); + +static int cmd_info(const struct shell *shell, size_t argc, char **argv) +{ + ARG_UNUSED(argc); + ARG_UNUSED(argv); + + for (size_t i = 0U; i < POWER_RAIL_NUM; i++) { + shell_print(shell, "%-10s: %s", rail_config[i].name, + rail_enabled[i] ? "enabled" : "disabled"); + } + + return 0; +} + +static int cmd_config(const struct shell *shell, size_t argc, const char **argv) +{ + bool enable; + + if (argc != 3) { + shell_error(shell, "Usage: power config RAIL|all enable|disable"); + return -EINVAL; + } + + enable = strcmp(argv[2], "enable") == 0; + + if (strcmp(argv[1], "all") == 0) { + for (size_t i = 0U; i < POWER_RAIL_NUM; i++) { + rail_enabled[i] = enable; + } + + shell_print(shell, "All rails %s", enable ? "enabled" : "disabled"); + } else { + size_t i; + + for (i = 0U; i < POWER_RAIL_NUM; i++) { + if (strcmp(argv[1], rail_config[i].name) == 0) { + break; + } + } + + if (i == POWER_RAIL_NUM) { + shell_error(shell, "Invalid power rail"); + return -EINVAL; + } + + rail_enabled[i] = enable; + + shell_print(shell, "Rail %s %s", rail_config[i].name, + enable ? "enabled" : "disabled"); + } + + return 0; +} + +static int cmd_meas_start(const struct shell *shell, size_t argc, const char **argv) +{ + ARG_UNUSED(argc); + ARG_UNUSED(argv); + + if (meas_enabled) { + shell_error(shell, "Measurement already started"); + return -EALREADY; + } + + meas_enabled = true; + k_sem_give(&meas_sem); + + shell_print(shell, "Measurement started"); + + return 0; +} + +static int cmd_meas_stop(const struct shell *shell, size_t argc, const char **argv) +{ + ARG_UNUSED(argc); + ARG_UNUSED(argv); + + meas_enabled = false; + + shell_print(shell, "Measurement stopped"); + + return 0; +} + +SHELL_STATIC_SUBCMD_SET_CREATE( + power_meas_cmd, SHELL_COND_CMD(CONFIG_SHELL, info, NULL, "Show power rails info", cmd_info), + SHELL_COND_CMD(CONFIG_SHELL, config, NULL, "Configure power rails", cmd_config), + SHELL_COND_CMD(CONFIG_SHELL, start, NULL, "Start measurements", cmd_meas_start), + SHELL_COND_CMD(CONFIG_SHELL, stop, NULL, "Stop measurements", cmd_meas_stop), + SHELL_SUBCMD_SET_END); + +SHELL_CMD_REGISTER(power, &power_meas_cmd, "Configure power measurements", NULL); diff --git a/src/modules/sd_card.c b/src/modules/sd_card.c new file mode 100644 index 0000000..74fbcff --- /dev/null +++ b/src/modules/sd_card.c @@ -0,0 +1,490 @@ +/* + * Copyright (c) 2018 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + + +#ifdef _POSIX_C_SOURCE +#undef _POSIX_C_SOURCE +#endif +/* Define _POSIX_C_SOURCE before including in order to use `strtok_r`. */ +#define _POSIX_C_SOURCE 200809L + +#include "sd_card.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +LOG_MODULE_REGISTER(sd_card, CONFIG_MODULE_SD_CARD_LOG_LEVEL); + +#define SD_ROOT_PATH "/SD:/" +/* Round down to closest 4-byte boundary */ +#define PATH_MAX_LEN ROUND_DOWN(CONFIG_FS_FATFS_MAX_LFN, 4) +#define SD_CARD_LEVELS_MAX 8 +#define SD_CARD_BUF_SIZE 700 + +static uint32_t num_files_added; +static struct k_mem_slab slab_A; +static struct k_mem_slab slab_B; + +static const char *sd_root_path = "/SD:"; +static FATFS fat_fs; +static bool sd_init_success; + +static struct fs_mount_t mnt_pt = { + .type = FS_FATFS, + .fs_data = &fat_fs, +}; + +/** + * @brief Replaces first carriage return or line feed with null terminator. + */ +static void cr_lf_remove(char *str) +{ + char *p = str; + + while (*p != '\0') { + if (*p == '\r' || *p == '\n') { + *p = '\0'; + break; + } + p++; + } +} + +/** + * @brief Recursively traverse the SD card tree. + */ +static int traverse_down(char const *const path, uint8_t curr_depth, uint16_t result_file_num_max, + uint16_t result_path_len_max, char result[][result_path_len_max], + char const *const search_pattern) +{ + int ret = 0; + + if (curr_depth > SD_CARD_LEVELS_MAX) { + LOG_WRN("At tree curr_depth %u, greater than %u", curr_depth, SD_CARD_LEVELS_MAX); + return 0; + } + + char *slab_A_ptr; + + ret = k_mem_slab_alloc(&slab_A, (void **)&slab_A_ptr, K_NO_WAIT); + if (ret) { + LOG_ERR("Failed to alloc slab A: %d", ret); + return ret; + } + + char *slab_A_ptr_origin = slab_A_ptr; + char *slab_B_ptr; + + ret = k_mem_slab_alloc(&slab_B, (void **)&slab_B_ptr, K_NO_WAIT); + if (ret) { + k_mem_slab_free(&slab_A, (void *)slab_A_ptr_origin); + LOG_ERR("Failed to alloc slab B: %d", ret); + return ret; + } + + char *slab_B_ptr_origin = slab_B_ptr; + size_t slab_A_ptr_size = SD_CARD_BUF_SIZE; + + /* Search for folders */ + ret = sd_card_list_files(path, slab_A_ptr, &slab_A_ptr_size, false); + if (ret == -ENOENT) { + /* Not able to open, hence likely not a folder */ + ret = 0; + goto cleanup; + } else if (ret) { + goto cleanup; + } + + LOG_DBG("At curr_depth %d tmp_buf is: %s", curr_depth, slab_A_ptr); + + char *token = strtok_r(slab_A_ptr, "\r\n", &slab_A_ptr); + + while (token != NULL) { + if (strstr(token, "System Volume Information") != NULL) { + /* Skipping System Volume Information */ + token = strtok_r(NULL, "\n", &slab_A_ptr); + continue; + } + + cr_lf_remove(token); + memset(slab_B_ptr, '\0', PATH_MAX_LEN); + + if (path != NULL) { + strcat(slab_B_ptr, path); + cr_lf_remove(slab_B_ptr); + strcat(slab_B_ptr, "/"); + } + + strcat(slab_B_ptr, token); + + if (strstr(token, search_pattern) != NULL) { + if (num_files_added >= result_file_num_max) { + LOG_WRN("Max file count reached %u", result_file_num_max); + ret = -ENOMEM; + goto cleanup; + } + strcpy(result[num_files_added], slab_B_ptr); + num_files_added++; + LOG_DBG("Added file num: %d at: %s", num_files_added, slab_B_ptr); + + } else { + ret = traverse_down(slab_B_ptr, curr_depth + 1, result_file_num_max, + result_path_len_max, result, search_pattern); + if (ret) { + LOG_ERR("Failed to traverse down: %d", ret); + } + } + + token = strtok_r(NULL, "\n", &slab_A_ptr); + } + +cleanup: + k_mem_slab_free(&slab_A, (void *)slab_A_ptr_origin); + k_mem_slab_free(&slab_B, (void *)slab_B_ptr_origin); + return ret; +} + +int sd_card_list_files_match(uint16_t result_file_num_max, uint16_t result_path_len_max, + char result[][result_path_len_max], char *path, + char const *const search_pattern) +{ + int ret; + + num_files_added = 0; + + if (result == NULL) { + return -EINVAL; + } + + if (result_file_num_max == 0) { + return -EINVAL; + } + + if (result_path_len_max == 0 || (result_path_len_max > PATH_MAX_LEN)) { + return -EINVAL; + } + + if (search_pattern == NULL) { + return -EINVAL; + } + + char __aligned(4) buf_A[SD_CARD_BUF_SIZE * SD_CARD_LEVELS_MAX] = {'\0'}; + char __aligned(4) buf_B[PATH_MAX_LEN * SD_CARD_LEVELS_MAX] = {'\0'}; + + ret = k_mem_slab_init(&slab_A, buf_A, SD_CARD_BUF_SIZE, SD_CARD_LEVELS_MAX); + if (ret) { + LOG_ERR("Failed to init slab: %d", ret); + return ret; + } + + ret = k_mem_slab_init(&slab_B, buf_B, PATH_MAX_LEN, SD_CARD_LEVELS_MAX); + if (ret) { + LOG_ERR("Failed to init slab: %d", ret); + return ret; + } + + ret = traverse_down(path, 0, result_file_num_max, result_path_len_max, result, + search_pattern); + if (ret) { + return ret; + } + + return num_files_added; +} + +int sd_card_list_files(char const *const path, char *buf, size_t *buf_size, bool extra_info) +{ + int ret; + struct fs_dir_t dirp; + static struct fs_dirent entry; + char abs_path_name[PATH_MAX_LEN + 1] = SD_ROOT_PATH; + size_t used_buf_size = 0; + + if (!sd_init_success) { + return -ENODEV; + } + + fs_dir_t_init(&dirp); + if (path == NULL) { + ret = fs_opendir(&dirp, sd_root_path); + if (ret) { + LOG_ERR("Open SD card root dir failed"); + return ret; + } + } else { + if (strlen(path) > PATH_MAX_LEN) { + LOG_ERR("Path is too long"); + return -FR_INVALID_NAME; + } + + strcat(abs_path_name, path); + + if (strchr(abs_path_name, '.')) { + /* Path contains a dot. Regarded as not a folder*/ + return -ENOENT; + } + + ret = fs_opendir(&dirp, abs_path_name); + if (ret) { + LOG_ERR("Open assigned path failed %d. %s", ret, abs_path_name); + return ret; + } + } + + while (1) { + ret = fs_readdir(&dirp, &entry); + if (ret) { + return ret; + } + + if (entry.name[0] == 0) { + break; + } + + if (buf != NULL) { + size_t remaining_buf_size = *buf_size - used_buf_size; + ssize_t len; + + if (extra_info) { + len = snprintk(&buf[used_buf_size], remaining_buf_size, + "[%s]\t%s\r\n", + entry.type == FS_DIR_ENTRY_DIR ? "DIR " : "FILE", + entry.name); + } else { + len = snprintk(&buf[used_buf_size], remaining_buf_size, "%s\r\n", + entry.name); + } + + if (len >= remaining_buf_size) { + LOG_ERR("Failed to append to buffer, error: %d", len); + return -EINVAL; + } + + used_buf_size += len; + } + + LOG_INF("[%s] %s", entry.type == FS_DIR_ENTRY_DIR ? "DIR " : "FILE", entry.name); + } + + ret = fs_closedir(&dirp); + if (ret) { + LOG_ERR("Close SD card root dir failed"); + return ret; + } + + *buf_size = used_buf_size; + return 0; +} + +int sd_card_open_write_close(char const *const filename, char const *const data, size_t *size) +{ + int ret; + struct fs_file_t f_entry; + char abs_path_name[PATH_MAX_LEN + 1] = SD_ROOT_PATH; + + if (!sd_init_success) { + return -ENODEV; + } + + if (strlen(filename) > PATH_MAX_LEN) { + LOG_ERR("Filename is too long"); + return -ENAMETOOLONG; + } + + strcat(abs_path_name, filename); + fs_file_t_init(&f_entry); + + ret = fs_open(&f_entry, abs_path_name, FS_O_CREATE | FS_O_WRITE | FS_O_APPEND); + if (ret) { + LOG_ERR("Create file failed"); + return ret; + } + + /* If the file exists, moves the file position pointer to the end of the file */ + ret = fs_seek(&f_entry, 0, FS_SEEK_END); + if (ret) { + LOG_ERR("Seek file pointer failed"); + return ret; + } + + ret = fs_write(&f_entry, data, *size); + if (ret < 0) { + LOG_ERR("Write file failed"); + return ret; + } + + *size = ret; + + ret = fs_close(&f_entry); + if (ret) { + LOG_ERR("Close file failed"); + return ret; + } + + return 0; +} + +int sd_card_open_read_close(char const *const filename, char *const buf, size_t *size) +{ + int ret; + struct fs_file_t f_entry; + char abs_path_name[PATH_MAX_LEN + 1] = SD_ROOT_PATH; + + if (!sd_init_success) { + return -ENODEV; + } + + if (strlen(filename) > PATH_MAX_LEN) { + LOG_ERR("Filename is too long"); + return -FR_INVALID_NAME; + } + + strcat(abs_path_name, filename); + fs_file_t_init(&f_entry); + + ret = fs_open(&f_entry, abs_path_name, FS_O_READ); + if (ret) { + LOG_ERR("Open file failed"); + return ret; + } + + ret = fs_read(&f_entry, buf, *size); + if (ret < 0) { + LOG_ERR("Read file failed. Ret: %d", ret); + return ret; + } + + *size = ret; + if (*size == 0) { + LOG_WRN("File is empty"); + } + + ret = fs_close(&f_entry); + if (ret) { + LOG_ERR("Close file failed"); + return ret; + } + + return 0; +} + +int sd_card_open(char const *const filename, struct fs_file_t *f_seg_read_entry) +{ + int ret; + char abs_path_name[PATH_MAX_LEN + 1] = SD_ROOT_PATH; + size_t available_path_space = PATH_MAX_LEN - strlen(SD_ROOT_PATH); + + if (!sd_init_success) { + return -ENODEV; + } + + if (strlen(filename) > CONFIG_FS_FATFS_MAX_LFN) { + LOG_ERR("Filename is too long"); + return -ENAMETOOLONG; + } + + if ((strlen(abs_path_name) + strlen(filename)) > PATH_MAX_LEN) { + LOG_ERR("Filepath is too long"); + return -EINVAL; + } + + strncat(abs_path_name, filename, available_path_space); + + LOG_INF("abs path name:\t%s", abs_path_name); + + fs_file_t_init(f_seg_read_entry); + + ret = fs_open(f_seg_read_entry, abs_path_name, FS_O_READ); + if (ret) { + LOG_ERR("Open file failed: %d", ret); + return ret; + } + + return 0; +} + +int sd_card_read(char *buf, size_t *size, struct fs_file_t *f_seg_read_entry) +{ + int ret; + + ret = fs_read(f_seg_read_entry, buf, *size); + if (ret < 0) { + LOG_ERR("Read file failed. Ret: %d", ret); + return ret; + } + + *size = ret; + + return 0; +} + +int sd_card_close(struct fs_file_t *f_seg_read_entry) +{ + int ret; + + ret = fs_close(f_seg_read_entry); + if (ret) { + LOG_ERR("Close file failed: %d", ret); + return ret; + } + + return 0; +} + +int sd_card_init(void) +{ + int ret; + static const char *sd_dev = "SD"; + uint64_t sd_card_size_bytes; + uint32_t sector_count; + size_t sector_size; + + ret = disk_access_init(sd_dev); + if (ret) { + LOG_DBG("SD card init failed, please check if SD card inserted"); + return -ENODEV; + } + + ret = disk_access_ioctl(sd_dev, DISK_IOCTL_GET_SECTOR_COUNT, §or_count); + if (ret) { + LOG_ERR("Unable to get sector count"); + return ret; + } + + LOG_DBG("Sector count: %d", sector_count); + + ret = disk_access_ioctl(sd_dev, DISK_IOCTL_GET_SECTOR_SIZE, §or_size); + if (ret) { + LOG_ERR("Unable to get sector size"); + return ret; + } + + LOG_DBG("Sector size: %d bytes", sector_size); + + sd_card_size_bytes = (uint64_t)sector_count * sector_size; + + LOG_INF("SD card volume size: %lld B", sd_card_size_bytes); + + mnt_pt.mnt_point = sd_root_path; + + ret = fs_mount(&mnt_pt); + if (ret) { + LOG_ERR("Mnt. disk failed, could be format issue. should be FAT/exFAT"); + return ret; + } + + sd_init_success = true; + + return 0; +} diff --git a/src/modules/sd_card.h b/src/modules/sd_card.h new file mode 100644 index 0000000..989f122 --- /dev/null +++ b/src/modules/sd_card.h @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2018 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _SD_CARD_H_ +#define _SD_CARD_H_ + +#include +#include + +/** + * @brief Finds all files on SD card that match the given pattern. + * + * @note The function uses a recursive approach with internal buffers. Memory intensive. + * + * @param[in] result_file_num_max Maximum number of files to be found. + * @param[in] result_file_len_max Maximum length of each file name including total path length + * @param[out] result Pointer to the result array of dimension result_file_num_max + * * result_file_len_max. + * @param[in] path NULL, search from root, otherwise search from the given path. + * Note not to add an ending "/" + * @param[in] pattern Null terminated pattern to find, e.g. *.lc3 or *.wav + * @retval Number of files found. + * @retval -EINVAL invalid parameters. + * @retval -ENOMEM out of memory. + * @retval -ENODEV SD init failed. SD likely not inserted. + * @retval -EPERM SD card operation is ongoing somewhere else. + * @retval -Other, error from underlying drivers. + */ +int sd_card_list_files_match(uint16_t result_file_num_max, uint16_t result_file_len_max, + char result[][result_file_len_max], char *path, + char const *const pattern); + +/** + * @brief Print out the contents under SD card root path and write the content to buffer. + * + * @param[in] path Path of the folder which is going to be listed. + * If assigned path is NULL, then listing the contents under + * root. If assigned path doesn't exist, an error will be + * returned. + * @param[out] buf Buffer where data is written. If set to NULL, it will be + * ignored. + * @param[in, out] buf_size Buffer size. + * @param[in] extra_info Will append DIR/FILE info to string. + * + * @retval 0 on success. + * @retval -EPERM SD card operation is ongoing somewhere else. + * @retval -ENODEV SD init failed. SD card likely not inserted. + * @retval -EINVAL Failed to append to buffer. + * @retval -FR_INVALID_NAME Path is too long. + * @retval Otherwise, error from underlying drivers. + */ +int sd_card_list_files(char const *const path, char *buf, size_t *buf_size, bool extra_info); + +/** + * @brief Write data from buffer into the file. + * + * @note If the file already exists, data will be appended to the end of the file. + * + * @param[in] filename Name of the target file for writing, the default + * location is the root directoy of SD card, accept + absolute path under root of SD card. + * @param[in] data which is going to be written into the file. + * @param[in, out] size Pointer to the number of bytes which is going to be written. + * The actual written size will be returned. + * + * @retval 0 on success. + * @retval -EPERM SD card operation is ongoing somewhere else. + * @retval -ENODEV SD init failed. SD card likely not inserted. + * @retval Otherwise, error from underlying drivers. + */ +int sd_card_open_write_close(char const *const filename, char const *const data, size_t *size); + +/** + * @brief Read data from file into the buffer. + * + * @param[in] filename Name of the target file for reading, the default location is + * the root directoy of SD card, accept absolute path under + * root of SD card. + * @param[out] buf Pointer to the buffer to write the read data into. + * @param[in, out] size Pointer to the number of bytes which wait to be read from + * the file. The actual read size will be returned. If the + * actual read size is 0, there will be a warning message which + indicates that the file is empty. + * + * @retval 0 on success. + * @retval -EPERM SD card operation is ongoing somewhere else. + * @retval -ENODEV SD init failed. SD card likely not inserted. + * @retval Otherwise, error from underlying drivers. + */ +int sd_card_open_read_close(char const *const filename, char *const buf, size_t *size); + +/** + * @brief Open file on SD card. + * + * @param[in] filename Name of file to open. Default + * location is the root directoy of SD card. + * Absolute path under root of SD card is accepted. + * @param[in, out] f_seg_read_entry Pointer to a file object. + * The pointer gets initialized and ready for use. + * + * + * @retval 0 on success. + * @retval -EPERM SD card operation is ongoing somewhere else. + * @retval -ENODEV SD init failed. SD likely not inserted. + * @retval Otherwise, error from underlying drivers. + */ +int sd_card_open(char const *const filename, struct fs_file_t *f_seg_read_entry); + +/** + * @brief Read segment on the open file on the SD card. + * + * @param[out] buf Pointer to the buffer to write the read data into. + * @param[in, out] size Number of bytes to be read from file. + * The actual read size will be returned. + * If the actual read size is 0, there will be a + * warning message which indicates that the file is + * empty. + * @param[in, out] f_seg_read_entry Pointer to a file object. After call to this + * function, the pointer gets updated and can be used + * as entry in next function call. + * + * @retval 0 on success. + * @retval -EPERM SD card operation is not ongoing. + * @retval -ENODEV SD init failed. SD likely not inserted. + * @retval Otherwise, error from underlying drivers. + */ +int sd_card_read(char *buf, size_t *size, struct fs_file_t *f_seg_read_entry); + +/** + * @brief Close the file opened by the sd_card_segment_read_open function. + * + * @param[in, out] f_seg_read_entry Pointer to a file object. After call to this + * function, the pointer is reset and can be used for + * another file. + * + * + * @retval 0 on success. + * @retval -EPERM SD card operation is not ongoing. + * @retval -EBUSY Segment read operation has not started. + * @retval Otherwise, error from underlying drivers. + */ +int sd_card_close(struct fs_file_t *f_seg_read_entry); + +/** + * @brief Initialize the SD card interface and print out SD card details. + * + * @retval 0 on success. + * @retval -ENODEV SD init failed. SD card likely not inserted. + * @retval Otherwise, error from underlying drivers. + */ +int sd_card_init(void); + +#endif /* _SD_CARD_H_ */ diff --git a/src/modules/sd_card_playback.c b/src/modules/sd_card_playback.c new file mode 100644 index 0000000..406c7cb --- /dev/null +++ b/src/modules/sd_card_playback.c @@ -0,0 +1,586 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "sd_card_playback.h" + +#include +#include +#include +#include +#include + +#include "sd_card.h" +#include "sw_codec_lc3.h" +#include "sw_codec_select.h" +#include "audio_system.h" + +#include +LOG_MODULE_REGISTER(sd_card_playback, CONFIG_MODULE_SD_CARD_PLAYBACK_LOG_LEVEL); + +#define MAX_PATH_LEN (CONFIG_FS_FATFS_MAX_LFN) +#define LIST_FILES_BUF_SIZE 512 +#define FRAME_DURATION_MS (CONFIG_AUDIO_FRAME_DURATION_US / 1000) + +#define WAV_FORMAT_PCM 1 +#define WAV_SAMPLE_RATE_48K 48000 + +/* WAV header */ +struct wav_header { + /* RIFF Header */ + char riff_header[4]; + uint32_t wav_size; /* File size excluding first eight bytes */ + char wav_header[4]; /* Contains "WAVE" */ + + /* Format Header */ + char fmt_header[4]; + uint32_t wav_chunk_size; /* Should be 16 for PCM */ + short audio_format; /* Should be 1 for PCM */ + short num_channels; + uint32_t sample_rate; + uint32_t byte_rate; + short block_alignment; /* num_channels * Bytes Per Sample */ + short bit_depth; + + /* Data */ + char data_header[4]; + uint32_t data_bytes; /* Number of bytes in data */ +} __packed; + +/* LC3 header */ +struct lc3_header { + uint16_t file_id; /* Constant value, 0xCC1C */ + uint16_t hdr_size; /* Header size, 0x0012 */ + uint16_t sample_rate; /* Sample frequency / 100 */ + uint16_t bit_rate; /* Bit rate / 100 (total for all channels) */ + uint16_t channels; /* Number of channels */ + uint16_t frame_duration; /* Frame duration in ms * 100 */ + uint16_t rfu; /* Reserved for future use */ + uint16_t signal_len_lsb; /* Number of samples in signal, 16 LSB */ + uint16_t signal_len_msb; /* Number of samples in signal, 16 MSB (>> 16) */ +} __packed; + +struct lc3_playback_config { + uint16_t lc3_frames_num; + uint16_t lc3_frame_length_bytes; +}; + +enum audio_formats { + SD_CARD_PLAYBACK_WAV, + SD_CARD_PLAYBACK_LC3, +}; + +RING_BUF_DECLARE(m_ringbuf_audio_data_lc3, CONFIG_SD_CARD_PLAYBACK_RING_BUF_SIZE); +K_SEM_DEFINE(m_sem_ringbuf_space_available, 0, 1); +K_MUTEX_DEFINE(mtx_ringbuf); +K_SEM_DEFINE(m_sem_playback, 0, 1); +K_THREAD_STACK_DEFINE(sd_card_playback_thread_stack, CONFIG_SD_CARD_PLAYBACK_STACK_SIZE); + +/* Thread */ +static struct k_thread sd_card_playback_thread_data; +static k_tid_t sd_card_playback_thread_id; + +/* Playback */ +static bool sd_card_playback_active; +static char *playback_file_name; +static enum audio_formats playback_file_format; +static uint16_t pcm_frame_size; +static char playback_file_path[MAX_PATH_LEN] = ""; +static struct lc3_header lc3_file_header; +static struct wav_header wav_file_header; +static struct lc3_playback_config lc3_playback_cfg; + +static struct fs_file_t f_seg_read_entry; + +static int sd_card_playback_ringbuf_read(uint8_t *buf, size_t *size) +{ + int ret; + uint16_t read_size; + + ret = k_mutex_lock(&mtx_ringbuf, K_NO_WAIT); + if (ret) { + LOG_ERR("Unable to take mutex. Ret: %d", ret); + return ret; + } + + read_size = ring_buf_get(&m_ringbuf_audio_data_lc3, buf, *size); + if (read_size != *size) { + LOG_WRN("Read size (%d) not equal requested size (%d)", read_size, *size); + } + + ret = k_mutex_unlock(&mtx_ringbuf); + if (ret) { + LOG_ERR("Mutex unlock err: %d", ret); + return ret; + } + + if (ring_buf_space_get(&m_ringbuf_audio_data_lc3) >= pcm_frame_size) { + k_sem_give(&m_sem_ringbuf_space_available); + } + + *size = read_size; + + return 0; +} + +static int sd_card_playback_ringbuf_write(uint8_t *buffer, size_t numbytes) +{ + int ret; + uint8_t *buf_ptr; + + /* The ringbuffer is read every 10 ms by audio datapath when SD card playback is enabled. + * Timeout value should therefore not be less than 10 ms + */ + ret = k_sem_take(&m_sem_ringbuf_space_available, K_MSEC(20)); + if (ret) { + LOG_ERR("Sem take err: %d. Skipping frame", ret); + return ret; + } + + ret = k_mutex_lock(&mtx_ringbuf, K_NO_WAIT); + if (ret) { + LOG_ERR("Unable to take mutex. Ret: %d", ret); + return ret; + } + + numbytes = ring_buf_put_claim(&m_ringbuf_audio_data_lc3, &buf_ptr, numbytes); + memcpy(buf_ptr, buffer, numbytes); + ret = ring_buf_put_finish(&m_ringbuf_audio_data_lc3, numbytes); + if (ret) { + LOG_ERR("Ring buf put finish err: %d", ret); + return ret; + } + + ret = k_mutex_unlock(&mtx_ringbuf); + if (ret) { + LOG_ERR("Mutex unlock err: %d", ret); + return ret; + } + + return numbytes; +} + +static int sd_card_playback_check_wav_header(struct wav_header wav_file_header) +{ + if (wav_file_header.audio_format != WAV_FORMAT_PCM) { + LOG_ERR("This is not a PCM file"); + return -EPERM; + } + + if (wav_file_header.num_channels != SW_CODEC_MONO) { + LOG_ERR("This is not a MONO file"); + return -EPERM; + } + + if (wav_file_header.sample_rate != WAV_SAMPLE_RATE_48K) { + LOG_ERR("Unsupported sample rate: %d", wav_file_header.sample_rate); + return -EPERM; + } + + if (wav_file_header.bit_depth != CONFIG_AUDIO_BIT_DEPTH_BITS) { + LOG_ERR("Bit depth in WAV file is not 16, but %d", wav_file_header.bit_depth); + return -EPERM; + } + + return 0; +} + +static int sd_card_playback_play_wav(void) +{ + int ret; + int ret_sd_card_close; + size_t wav_read_size; + size_t wav_file_header_size = sizeof(wav_file_header); + int audio_length_bytes; + int n_iter; + + ret = sd_card_open(playback_file_name, &f_seg_read_entry); + if (ret) { + LOG_ERR("Open SD card file err: %d", ret); + return ret; + } + + ret = sd_card_read((char *)&wav_file_header, &wav_file_header_size, &f_seg_read_entry); + if (ret) { + LOG_ERR("Read SD card err: %d", ret); + ret_sd_card_close = sd_card_close(&f_seg_read_entry); + if (ret_sd_card_close) { + LOG_ERR("Close SD card err: %d", ret_sd_card_close); + return ret_sd_card_close; + } + return ret; + } + + /* Verify that there is support for playing the specified file */ + ret = sd_card_playback_check_wav_header(wav_file_header); + if (ret) { + LOG_ERR("WAV header check failed. Ret: %d", ret); + ret_sd_card_close = sd_card_close(&f_seg_read_entry); + if (ret_sd_card_close) { + LOG_ERR("Close SD card err: %d", ret_sd_card_close); + return ret_sd_card_close; + } + return ret; + } + + /* Size corresponding to frame size of audio BT stream */ + pcm_frame_size = wav_file_header.byte_rate * FRAME_DURATION_MS / 1000; + wav_read_size = pcm_frame_size; + uint8_t pcm_mono_frame[wav_read_size]; + + audio_length_bytes = wav_file_header.wav_size + 8 - sizeof(wav_file_header); + n_iter = ceil((float)audio_length_bytes / (float)wav_read_size); + + for (int i = 0; i < n_iter; i++) { + /* Read a chunk of audio data from file */ + ret = sd_card_read(pcm_mono_frame, &wav_read_size, &f_seg_read_entry); + if (ret < 0) { + LOG_ERR("SD card read err: %d", ret); + break; + } + + /* Write audio data to the ringbuffer */ + ret = sd_card_playback_ringbuf_write(pcm_mono_frame, wav_read_size); + if (ret < 0) { + LOG_ERR("Load ringbuf err: %d", ret); + break; + } + + if (i == 0) { + /* Data can now be read from the ringbuffer */ + sd_card_playback_active = true; + } + } + + sd_card_playback_active = false; + + ret_sd_card_close = sd_card_close(&f_seg_read_entry); + /* Check if something inside the for loop failed */ + if (ret < 0) { + LOG_ERR("WAV playback err: %d", ret); + return ret; + } + + if (ret_sd_card_close) { + LOG_ERR("SD card close err: %d", ret); + return ret; + } + + return 0; +} + +static int sd_card_playback_play_lc3(void) +{ + int ret; + int ret_sd_card_close; + uint16_t pcm_mono_write_size; + uint8_t decoder_num_ch = audio_system_decoder_num_ch_get(); + size_t lc3_file_header_size = sizeof(lc3_file_header); + size_t lc3_frame_header_size = sizeof(uint16_t); + + ret = sd_card_open(playback_file_name, &f_seg_read_entry); + if (ret) { + LOG_ERR("Open SD card file err: %d", ret); + return ret; + } + + /* Read the file header */ + ret = sd_card_read((char *)&lc3_file_header, &lc3_file_header_size, &f_seg_read_entry); + if (ret < 0) { + LOG_ERR("Read SD card file err: %d", ret); + ret_sd_card_close = sd_card_close(&f_seg_read_entry); + if (ret_sd_card_close) { + LOG_ERR("Close SD card err: %d", ret_sd_card_close); + return ret_sd_card_close; + } + return ret; + } + + pcm_frame_size = sizeof(uint16_t) * lc3_file_header.sample_rate * + lc3_file_header.frame_duration / 1000; + lc3_playback_cfg.lc3_frames_num = + sizeof(uint16_t) * + ((lc3_file_header.signal_len_msb << 16) + lc3_file_header.signal_len_lsb) / + pcm_frame_size; + + uint8_t pcm_mono_frame[pcm_frame_size]; + + for (int i = 0; i < lc3_playback_cfg.lc3_frames_num; i++) { + /* Read the frame header */ + ret = sd_card_read((char *)&lc3_playback_cfg.lc3_frame_length_bytes, + &lc3_frame_header_size, &f_seg_read_entry); + if (ret < 0) { + LOG_ERR("SD card read err: %d", ret); + break; + } + + char lc3_frame[lc3_playback_cfg.lc3_frame_length_bytes]; + size_t lc3_fr_len = lc3_playback_cfg.lc3_frame_length_bytes; + + /* Read the audio data frame to be decoded */ + ret = sd_card_read(lc3_frame, &lc3_fr_len, &f_seg_read_entry); + if (ret < 0) { + LOG_ERR("SD card read err: %d", ret); + break; + } + + if (lc3_fr_len != lc3_playback_cfg.lc3_frame_length_bytes) { + LOG_ERR("SD card read size (%d) not equal requested size (%d)", lc3_fr_len, + lc3_playback_cfg.lc3_frame_length_bytes); + ret = -EPERM; + break; + } + + /* Decode audio data frame */ + ret = sw_codec_lc3_dec_run(lc3_frame, lc3_playback_cfg.lc3_frame_length_bytes, + pcm_frame_size, decoder_num_ch - 1, pcm_mono_frame, + &pcm_mono_write_size, false); + if (ret) { + LOG_ERR("Decoding err: %d", ret); + break; + } + + ret = sd_card_playback_ringbuf_write((char *)pcm_mono_frame, pcm_mono_write_size); + if (ret < 0) { + LOG_ERR("Load ringbuf err: %d", ret); + break; + } + + if (i == 0) { + sd_card_playback_active = true; + } + } + + sd_card_playback_active = false; + ret_sd_card_close = sd_card_close(&f_seg_read_entry); + if (ret < 0) { + LOG_ERR("LC3 playback err: %d", ret); + sd_card_playback_active = false; + return ret; + } + + if (ret_sd_card_close) { + LOG_ERR("SD card close err: %d", ret); + sd_card_playback_active = false; + return ret; + } + + return 0; +} + +static void sd_card_playback_thread(void *arg1, void *arg2, void *arg3) +{ + int ret; + + while (1) { + k_sem_take(&m_sem_playback, K_FOREVER); + switch (playback_file_format) { + case SD_CARD_PLAYBACK_WAV: + ring_buf_reset(&m_ringbuf_audio_data_lc3); + k_sem_reset(&m_sem_ringbuf_space_available); + k_sem_give(&m_sem_ringbuf_space_available); + ret = sd_card_playback_play_wav(); + if (ret) { + LOG_ERR("Wav playback err: %d", ret); + } + + break; + + case SD_CARD_PLAYBACK_LC3: + ring_buf_reset(&m_ringbuf_audio_data_lc3); + k_sem_reset(&m_sem_ringbuf_space_available); + k_sem_give(&m_sem_ringbuf_space_available); + ret = sd_card_playback_play_lc3(); + if (ret) { + LOG_ERR("LC3 playback err: %d", ret); + } + + break; + } + } +} + +bool sd_card_playback_is_active(void) +{ + return sd_card_playback_active; +} + +int sd_card_playback_wav(char *filename) +{ + if (!sw_codec_is_initialized()) { + LOG_ERR("Sw codec not initialized"); + return -EACCES; + } + + playback_file_format = SD_CARD_PLAYBACK_WAV; + playback_file_name = filename; + k_sem_give(&m_sem_playback); + + return 0; +} + +int sd_card_playback_lc3(char *filename) +{ + if (!sw_codec_is_initialized()) { + LOG_ERR("Sw codec not initialized"); + return -EACCES; + } + + playback_file_format = SD_CARD_PLAYBACK_LC3; + playback_file_name = filename; + k_sem_give(&m_sem_playback); + + return 0; +} + +int sd_card_playback_mix_with_stream(void *const pcm_a, size_t pcm_a_size) +{ + int ret; + uint8_t pcm_b[pcm_frame_size]; + size_t read_size = pcm_frame_size; + + if (!sd_card_playback_active) { + LOG_ERR("SD card playback is not active"); + return -EACCES; + } + + ret = sd_card_playback_ringbuf_read(pcm_b, &read_size); + if (ret) { + LOG_ERR("Loading data into buffer err: %d", ret); + return ret; + } + + if (read_size > 0) { + ret = pcm_mix(pcm_a, pcm_a_size, pcm_b, read_size, B_MONO_INTO_A_STEREO_L); + if (ret) { + LOG_ERR("Pcm mix err: %d", ret); + return ret; + } + } else { + LOG_WRN("Size read from ringbuffer: %d. Skipping", read_size); + } + + return 0; +} + +int sd_card_playback_init(void) +{ + int ret; + + sd_card_playback_thread_id = k_thread_create( + &sd_card_playback_thread_data, sd_card_playback_thread_stack, + CONFIG_SD_CARD_PLAYBACK_STACK_SIZE, (k_thread_entry_t)sd_card_playback_thread, NULL, + NULL, NULL, K_PRIO_PREEMPT(CONFIG_SD_CARD_PLAYBACK_THREAD_PRIO), 0, K_NO_WAIT); + ret = k_thread_name_set(sd_card_playback_thread_id, "sd_card_playback"); + if (ret) { + return ret; + } + + return 0; +} + +/* Shell functions */ +static int cmd_play_wav_file(const struct shell *shell, size_t argc, char **argv) +{ + int ret; + + char file_loc[MAX_PATH_LEN] = ""; + + if (argc != 2) { + shell_error(shell, "Incorrect number of args"); + return -EINVAL; + } + + if ((strlen(playback_file_path) + strlen(argv[1])) >= ARRAY_SIZE(file_loc)) { + return -ENOMEM; + } + + strcat(file_loc, playback_file_path); + strcat(file_loc, argv[1]); + ret = sd_card_playback_wav(file_loc); + if (ret) { + shell_error(shell, "WAV playback err: %d", ret); + return ret; + } + + return 0; +} + +static int cmd_play_lc3_file(const struct shell *shell, size_t argc, char **argv) +{ + int ret; + + if (argc != 2) { + shell_error(shell, "Incorrect number of args"); + return -EINVAL; + } + + char file_loc[MAX_PATH_LEN] = ""; + + if ((strlen(playback_file_path) + strlen(argv[1])) >= ARRAY_SIZE(file_loc)) { + return -ENOMEM; + } + + strcat(file_loc, playback_file_path); + strcat(file_loc, argv[1]); + ret = sd_card_playback_lc3(file_loc); + if (ret) { + shell_error(shell, "LC3 playback err: %d", ret); + return ret; + } + + return 0; +} + +static int cmd_change_dir(const struct shell *shell, size_t argc, char **argv) +{ + if (argc != 2) { + shell_error(shell, "Incorrect number of args"); + return -EINVAL; + } + + if (argv[1][0] == '/') { + playback_file_path[0] = '\0'; + shell_print(shell, "Current directory: root"); + } else { + if ((strlen(playback_file_path) + strlen(argv[1])) >= + ARRAY_SIZE(playback_file_path)) { + return -ENOMEM; + } + + strcat(playback_file_path, argv[1]); + strcat(playback_file_path, "/"); + shell_print(shell, "Current directory: %s", playback_file_path); + } + + return 0; +} + +static int cmd_list_files(const struct shell *shell, size_t argc, char **argv) +{ + int ret; + char buf[LIST_FILES_BUF_SIZE]; + size_t buf_size = LIST_FILES_BUF_SIZE; + + ret = sd_card_list_files(playback_file_path, buf, &buf_size, true); + if (ret) { + shell_error(shell, "List files err: %d", ret); + return ret; + } + + shell_print(shell, "%s", buf); + + return 0; +} + +SHELL_STATIC_SUBCMD_SET_CREATE( + sd_card_playback_cmd, + SHELL_COND_CMD(CONFIG_SHELL, play_lc3, NULL, "Play LC3 file", cmd_play_lc3_file), + SHELL_COND_CMD(CONFIG_SHELL, play_wav, NULL, "Play WAV file", cmd_play_wav_file), + SHELL_COND_CMD(CONFIG_SHELL, cd, NULL, "Change directory", cmd_change_dir), + SHELL_COND_CMD(CONFIG_SHELL, list_files, NULL, "List files", cmd_list_files), + SHELL_SUBCMD_SET_END); + +SHELL_CMD_REGISTER(sd_card_playback, &sd_card_playback_cmd, "Play audio files from SD card", NULL); diff --git a/src/modules/sd_card_playback.h b/src/modules/sd_card_playback.h new file mode 100644 index 0000000..39c2709 --- /dev/null +++ b/src/modules/sd_card_playback.h @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _SD_CARD_PLAYBACK_H_ +#define _SD_CARD_PLAYBACK_H_ + +/** + * @file + * @defgroup sd_card_playback SD card playback. + * @{ + * @brief The SD card playback module for nRF5340 Audio. + */ + +#include + +/** + * @brief Check whether or not the SD card playback module is active. + * + * @retval true Active. + * @retval false Not active. + */ +bool sd_card_playback_is_active(void); + +/** + * @brief Play audio from a WAV file from the SD card. Only support for mono files. + * + * @note Supports only 48k mono files. + * + * @param[in] filename Name of file to be played. Path from the root of the SD card is + * accepted. + * + * @retval 0 Success. + * @retval -EACCES SW codec is not initialized. + */ +int sd_card_playback_wav(char *filename); + +/** + * @brief Play audio from an LC3 file from the SD card. + * + * @note Supports only mono files. + * + * @param[in] filename Name of file to be played. Path from the root of the SD card is + * accepted. + * + * @retval 0 Success. + * @retval -EACCES SW codec is not initialized. + */ +int sd_card_playback_lc3(char *filename); + +/** + * @brief Mix the PCM data from the SD card playback module with the audio stream out. + * + * @param[in, out] pcm_a Buffer into which to mix PCM data from the LC3 module. + * @param[in] pcm_a_size Size of the input buffer. + * + * @retval 0 Success. + * @retval -EACCES SD card playback is not active. + * @retval Otherwise, error from underlying drivers. + */ +int sd_card_playback_mix_with_stream(void *const pcm_a, size_t pcm_a_size); + +/** + * @brief Initialize the SD card playback module. Create the SD card playback thread. + * + * @return 0 on success, otherwise, error from underlying drivers. + */ +int sd_card_playback_init(void); + +/** + * @} + */ + +#endif /* _SD_CARD_PLAYBACK_H_ */ diff --git a/src/utils/CMakeLists.txt b/src/utils/CMakeLists.txt new file mode 100644 index 0000000..32516e1 --- /dev/null +++ b/src/utils/CMakeLists.txt @@ -0,0 +1,15 @@ +# +# Copyright (c) 2022 Nordic Semiconductor +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +target_sources(app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/channel_assignment.c + ${CMAKE_CURRENT_SOURCE_DIR}/error_handler.c + ${CMAKE_CURRENT_SOURCE_DIR}/uicr.c +) + +target_sources_ifdef(CONFIG_BOARD_NRF5340_AUDIO_DK_NRF5340_CPUAPP app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/nrf5340_audio_dk.c + ${CMAKE_CURRENT_SOURCE_DIR}/board_version.c) diff --git a/src/utils/Kconfig b/src/utils/Kconfig new file mode 100644 index 0000000..ed02ced --- /dev/null +++ b/src/utils/Kconfig @@ -0,0 +1,60 @@ +# +# Copyright (c) 2022 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +rsource "Kconfig.defaults" + +menu "Utils" + +menu "FIFO" + +config FIFO_FRAME_SPLIT_NUM + int "Number of blocks to make up one frame of audio data" + default 10 + help + Easy DMA in I2S requires two buffers to be filled before I2S + transmission will begin. In order to reduce latency, an audio + frame can be split into multiple blocks with this parameter. USB + sends data in 1 ms blocks, so we need the split to match that. + Since we set frame size to 10 ms for USB, 10 is selected as + FRAME_SPLIT_NUM + +config FIFO_TX_FRAME_COUNT + int "Max number of audio frames in TX slab" + default 3 + help + FIFO_TX is the buffer that holds decoded audio data before it + is sent to either I2S or USB + +config FIFO_RX_FRAME_COUNT + int "Max number of audio frames in RX slab" + default 1 + help + FIFO_RX is the buffer that holds uncompressed audio data coming + from either I2S or USB + +endmenu # FIFO + +#----------------------------------------------------------------------------# +menu "Log levels" + +module = BOARD_VERSION +module-str = board-version +source "subsys/logging/Kconfig.template.log_config" + +module = CHAN_ASSIGNMENT +module-str = chan-assignment +source "subsys/logging/Kconfig.template.log_config" + +module = ERROR_HANDLER +module-str = error-handler +source "subsys/logging/Kconfig.template.log_config" + +module = FW_INFO +module-str = fw-info +source "subsys/logging/Kconfig.template.log_config" + +endmenu # Log levels +endmenu # Utils diff --git a/src/utils/Kconfig.defaults b/src/utils/Kconfig.defaults new file mode 100644 index 0000000..b4e2c58 --- /dev/null +++ b/src/utils/Kconfig.defaults @@ -0,0 +1,9 @@ +# +# Copyright (c) 2022 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +# Channel assignment writes to UICR +config NRFX_NVMC + default y diff --git a/src/utils/board_version.c b/src/utils/board_version.c new file mode 100644 index 0000000..d768a7a --- /dev/null +++ b/src/utils/board_version.c @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2018 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "board_version.h" + +#include +#include +#include +#include + +#include "board.h" +#include "macros_common.h" + +#include +LOG_MODULE_REGISTER(board_version, CONFIG_BOARD_VERSION_LOG_LEVEL); + +#define BOARD_ID DT_NODELABEL(board_id) + +static const struct adc_dt_spec adc = ADC_DT_SPEC_GET(BOARD_ID); +static const struct gpio_dt_spec power_gpios = GPIO_DT_SPEC_GET(BOARD_ID, power_gpios); + +/* We allow the ADC register value to deviate by N points in either direction */ +#define BOARD_VERSION_TOLERANCE 70 +#define VOLTAGE_STABILIZE_TIME_US 5 + +static int16_t sample_buffer; + +static struct adc_sequence sequence = { + .buffer = &sample_buffer, + .buffer_size = sizeof(sample_buffer), +}; + +/* @brief Enable board version voltage divider and trigger ADC read */ +static int divider_value_get(void) +{ + int ret; + + ret = gpio_pin_set_dt(&power_gpios, 1); + if (ret) { + return ret; + } + + /* Wait for voltage to stabilize */ + k_busy_wait(VOLTAGE_STABILIZE_TIME_US); + + ret = adc_read(adc.dev, &sequence); + if (ret) { + return ret; + } + + ret = gpio_pin_set_dt(&power_gpios, 0); + if (ret) { + return ret; + } + + return 0; +} + +/**@brief Traverse all defined versions and get the one with the + * most similar value. Check tolerances. + */ +static int version_search(int reg_value, uint32_t tolerance, struct board_version *board_rev) +{ + uint32_t diff; + uint32_t smallest_diff = UINT_MAX; + uint8_t smallest_diff_idx = UCHAR_MAX; + + for (uint8_t i = 0; i < (uint8_t)ARRAY_SIZE(BOARD_VERSION_ARR); i++) { + diff = abs(BOARD_VERSION_ARR[i].adc_reg_val - reg_value); + + if (diff < smallest_diff) { + smallest_diff = diff; + smallest_diff_idx = i; + } + } + + if (smallest_diff >= tolerance) { + LOG_ERR("Board ver search failed. ADC reg read: %d", reg_value); + return -ESPIPE; /* No valid board_rev found */ + } + + *board_rev = BOARD_VERSION_ARR[smallest_diff_idx]; + LOG_DBG("Board ver search OK!. ADC reg val: %d", reg_value); + return 0; +} + +/* @brief Internal init routine */ +static int board_version_init(void) +{ + int ret; + static bool initialized; + + if (initialized) { + return 0; + } + + if (!gpio_is_ready_dt(&power_gpios)) { + return -ENXIO; + } + + ret = gpio_pin_configure_dt(&power_gpios, GPIO_OUTPUT_INACTIVE); + if (ret) { + return ret; + } + + if (!device_is_ready(adc.dev)) { + LOG_ERR("ADC not ready"); + return -ENODEV; + } + + ret = adc_channel_setup_dt(&adc); + if (ret) { + return ret; + } + + (void)adc_sequence_init_dt(&adc, &sequence); + + initialized = true; + return 0; +} + +int board_version_get(struct board_version *board_rev) +{ + int ret; + + ret = board_version_init(); + if (ret) { + return ret; + } + + ret = divider_value_get(); + if (ret) { + return ret; + } + + ret = version_search(sample_buffer, BOARD_VERSION_TOLERANCE, board_rev); + if (ret) { + return ret; + } + + return 0; +} + +int board_version_valid_check(void) +{ + int ret; + struct board_version board_rev; + + ret = board_version_get(&board_rev); + if (ret) { + LOG_ERR("Unable to get any board version"); + return ret; + } + + if (BOARD_VERSION_VALID_MSK & (board_rev.mask)) { + LOG_INF(COLOR_GREEN "Compatible board/HW version found: %s" COLOR_RESET, + board_rev.name); + } else { + LOG_ERR("Invalid board found, rev: %s Valid mask: 0x%x valid mask: 0x%lx", + board_rev.name, board_rev.mask, BOARD_VERSION_VALID_MSK); + return -EPERM; + } + + return 0; +} diff --git a/src/utils/board_version.h b/src/utils/board_version.h new file mode 100644 index 0000000..b45564a --- /dev/null +++ b/src/utils/board_version.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2018 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _BOARD_VERSION_H_ +#define _BOARD_VERSION_H_ + +#include "board.h" + +/**@brief Get the board/HW version + * + * @note This function will init the ADC, perform a reading, and + * return the HW version. + * + * @param board_rev Pointer to container for board version + * + * @return 0 on success. + * Error code on fault or -ESPIPE if no valid version found + */ +int board_version_get(struct board_version *board_rev); + +/**@brief Check that the FW is compatible with the HW version + * + * @note This function will init the ADC, perform a reading, and + * check for valid version match. + * + * @note The board file must define a BOARD_VERSION_ARR array of + * possible valid ADC register values (voltages) for the divider. + * A BOARD_VERSION_VALID_MSK with valid version bits must also be defined. + * + * @return 0 on success. Error code on fault or -EPERM if incompatible board version. + */ +int board_version_valid_check(void); + +#endif /* _BOARD_VERSION_H_ */ diff --git a/src/utils/channel_assignment.c b/src/utils/channel_assignment.c new file mode 100644 index 0000000..64c6e08 --- /dev/null +++ b/src/utils/channel_assignment.c @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2022 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "channel_assignment.h" + +#include + +#include "uicr.h" + +#include +LOG_MODULE_REGISTER(channel_assignment, CONFIG_CHAN_ASSIGNMENT_LOG_LEVEL); + +static uint8_t channel_value; + +void channel_assignment_get(enum audio_channel *channel) +{ + *channel = (enum audio_channel)channel_value; +} + +#if CONFIG_AUDIO_HEADSET_CHANNEL_RUNTIME +void channel_assignment_set(enum audio_channel channel) +{ + int ret; + + channel_value = channel; + + /* Try to write the channel value to UICR */ + ret = uicr_channel_set(channel); + if (ret) { + LOG_DBG("Unable to write channel value to UICR"); + } +} +#endif /* CONFIG_AUDIO_HEADSET_CHANNEL_RUNTIME */ + +static int channel_assignment_init(void) +{ +#if CONFIG_AUDIO_HEADSET_CHANNEL_RUNTIME + channel_value = uicr_channel_get(); + + if (channel_value >= AUDIO_CH_NUM) { + /* Invalid value in UICR if UICR is not written */ + channel_value = AUDIO_CHANNEL_DEFAULT; + } +#else + channel_value = CONFIG_AUDIO_HEADSET_CHANNEL; +#endif /* CONFIG_AUDIO_HEADSET_CHANNEL_RUNTIME */ + + return 0; +} + +SYS_INIT(channel_assignment_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY); diff --git a/src/utils/channel_assignment.h b/src/utils/channel_assignment.h new file mode 100644 index 0000000..aadaae7 --- /dev/null +++ b/src/utils/channel_assignment.h @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _CHANNEL_ASSIGNMENT_H_ +#define _CHANNEL_ASSIGNMENT_H_ + +/** @file + * @brief Audio channel assignment + * + * Audio channel can be assigned at runtime or compile-time, depending on configuration. + * + */ + +#include + +#ifndef AUDIO_CHANNEL_DEFAULT +#define AUDIO_CHANNEL_DEFAULT AUDIO_CH_L +#endif /* AUDIO_CHANNEL_DEFAULT */ + +static const char CH_L_TAG[] = "HL"; +static const char CH_R_TAG[] = "HR"; +static const char GW_TAG[] = "GW"; + +/** + * @brief Get assigned audio channel. + * + * @param[out] channel Channel value + */ +void channel_assignment_get(enum audio_channel *channel); + +#if CONFIG_AUDIO_HEADSET_CHANNEL_RUNTIME +/** + * @brief Assign audio channel. + * + * @param[out] channel Channel value + */ +void channel_assignment_set(enum audio_channel channel); +#endif /* AUDIO_HEADSET_CHANNEL_RUNTIME */ + +#endif /* _CHANNEL_ASSIGNMENT_H_ */ diff --git a/src/utils/error_handler.c b/src/utils/error_handler.c new file mode 100644 index 0000000..8ae54bd --- /dev/null +++ b/src/utils/error_handler.c @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2018 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include +#include +#include +#include +#include + +/* Print everything from the error handler */ +#include +LOG_MODULE_REGISTER(error_handler, CONFIG_ERROR_HANDLER_LOG_LEVEL); + +#if (defined(CONFIG_BOARD_NRF5340_AUDIO_DK_NRF5340_CPUAPP) && (CONFIG_DEBUG)) +/* nRF5340 Audio DK center RGB LED */ +static const struct gpio_dt_spec center_led_r = GPIO_DT_SPEC_GET(DT_NODELABEL(rgb1_red), gpios); +static const struct gpio_dt_spec center_led_g = GPIO_DT_SPEC_GET(DT_NODELABEL(rgb1_green), gpios); +static const struct gpio_dt_spec center_led_b = GPIO_DT_SPEC_GET(DT_NODELABEL(rgb1_blue), gpios); +#endif /* (defined(CONFIG_BOARD_NRF5340_AUDIO_DK_NRF5340_CPUAPP) && (CONFIG_DEBUG)) */ + +void error_handler(unsigned int reason, const struct arch_esf *esf) +{ +#if (CONFIG_DEBUG) + LOG_ERR("Caught system error -- reason %d. Entering infinite loop", reason); + LOG_PANIC(); +#if defined(CONFIG_BOARD_NRF5340_AUDIO_DK_NRF5340_CPUAPP) + (void)gpio_pin_configure_dt(¢er_led_r, GPIO_OUTPUT_ACTIVE); + (void)gpio_pin_configure_dt(¢er_led_g, GPIO_OUTPUT_INACTIVE); + (void)gpio_pin_configure_dt(¢er_led_b, GPIO_OUTPUT_INACTIVE); +#endif /* defined(CONFIG_BOARD_NRF5340_AUDIO_DK_NRF5340_CPUAPP) */ + irq_lock(); + while (1) { + __asm__ volatile("nop"); + } +#else + LOG_ERR("Caught system error -- reason %d. Cold rebooting.", reason); +#if (CONFIG_LOG) + LOG_PANIC(); +#endif /* (CONFIG_LOG) */ + sys_reboot(SYS_REBOOT_COLD); +#endif /* (CONFIG_DEBUG) */ + CODE_UNREACHABLE; +} + +void bt_ctlr_assert_handle(char *c, int code) +{ + LOG_ERR("BT controller assert: %s, code: 0x%x", c, code); + error_handler(code, NULL); +} + +void k_sys_fatal_error_handler(unsigned int reason, const struct arch_esf *esf) +{ + error_handler(reason, esf); +} + +void assert_post_action(const char *file, unsigned int line) +{ + LOG_ERR("Assert post action: file: %s, line %d", file, line); + error_handler(0, NULL); +} diff --git a/src/utils/fw_info_app.c.in b/src/utils/fw_info_app.c.in new file mode 100644 index 0000000..3c360c8 --- /dev/null +++ b/src/utils/fw_info_app.c.in @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2018 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "fw_info_app.h" + +#include +#include +#include +#include "channel_assignment.h" +#include "ncs_version.h" + +#include "macros_common.h" + +#include +LOG_MODULE_REGISTER(fw_info, CONFIG_FW_INFO_LOG_LEVEL); + +static const char COMPILE_DATE[] = "@NRF5340_AUDIO_CORE_APP_COMP_DATE@"; +/* NOTE: The string below is used by the Nordic CI system */ +static const char NRF5340_CORE[] = "nRF5340 Audio nRF5340 Audio DK cpuapp"; + +int fw_info_app_print(void) +{ + /* NOTE: The string below is used by the Nordic CI system */ + LOG_INF(COLOR_GREEN "\r\n\t %s \ + \r\n\t NCS base version: %s \ + \r\n\t Cmake run : %s" COLOR_RESET, + NRF5340_CORE, NCS_VERSION_STRING, COMPILE_DATE); + +#if (CONFIG_DEBUG) + int ret; + + LOG_INF("------- DEBUG BUILD -------"); + +#if (CONFIG_AUDIO_DEV == HEADSET) + enum audio_channel channel; + + channel_assignment_get(&channel); + if (channel == AUDIO_CH_L) { + ret = log_set_tag(CH_L_TAG); + if (ret) { + return ret; + } + /* NOTE: The string below is used by the Nordic CI system */ + LOG_INF(COLOR_CYAN "HEADSET left device" COLOR_RESET); + } else if (channel == AUDIO_CH_R) { + ret = log_set_tag(CH_R_TAG); + if (ret) { + return ret; + } + /* NOTE: The string below is used by the Nordic CI system */ + LOG_INF(COLOR_CYAN "HEADSET right device" COLOR_RESET); + } else { + __ASSERT(false, "Unknown channel"); + } + +#elif CONFIG_AUDIO_DEV == GATEWAY + ret = log_set_tag(GW_TAG); + if (ret) { + return ret; + } + LOG_INF(COLOR_CYAN "Compiled for GATEWAY device" COLOR_RESET); +#endif /* (CONFIG_AUDIO_DEV == HEADSET) */ +#endif /* (CONFIG_DEBUG) */ + + return 0; +} \ No newline at end of file diff --git a/src/utils/fw_info_app.h b/src/utils/fw_info_app.h new file mode 100644 index 0000000..8ef0228 --- /dev/null +++ b/src/utils/fw_info_app.h @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2018 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _FW_INFO_APP_H_ +#define _FW_INFO_APP_H_ + +/** + * @brief Prints firmware info, such as Git details, compiled timestamp etc. + * + * @return 0 on success. + * Otherwise, error from underlying drivers + */ +int fw_info_app_print(void); + +#endif /* _FW_INFO_APP_H_ */ diff --git a/src/utils/macros/macros_common.h b/src/utils/macros/macros_common.h new file mode 100644 index 0000000..556e3af --- /dev/null +++ b/src/utils/macros/macros_common.h @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2018 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _MACROS_H_ +#define _MACROS_H_ + +#include + +/* Error check. If != 0, print err code and call _SysFatalErrorHandler in main. + * For debug mode all LEDs are turned on in case of an error. + */ + +#define PRINT_AND_OOPS(code) \ + do { \ + LOG_ERR("ERR_CHK Err_code: [%d] @ line: %d\t", code, __LINE__); \ + k_oops(); \ + } while (0) + +#define ERR_CHK(err_code) \ + do { \ + if (err_code) { \ + PRINT_AND_OOPS(err_code); \ + } \ + } while (0) + +#define ERR_CHK_MSG(err_code, msg) \ + do { \ + if (err_code) { \ + LOG_ERR("%s", msg); \ + PRINT_AND_OOPS(err_code); \ + } \ + } while (0) + +#if (defined(CONFIG_INIT_STACKS) && defined(CONFIG_THREAD_ANALYZER)) + +#define STACK_USAGE_PRINT(thread_name, p_thread) \ + do { \ + static uint64_t thread_ts; \ + size_t unused_space_in_thread_bytes; \ + if (k_uptime_get() - thread_ts > CONFIG_PRINT_STACK_USAGE_MS) { \ + k_thread_stack_space_get(p_thread, &unused_space_in_thread_bytes); \ + thread_ts = k_uptime_get(); \ + LOG_DBG("Unused space in %s thread: %d bytes", thread_name, \ + unused_space_in_thread_bytes); \ + } \ + } while (0) +#else +#define STACK_USAGE_PRINT(thread_name, p_stack) +#endif /* (defined(CONFIG_INIT_STACKS) && defined(CONFIG_THREAD_ANALYZER)) */ + +#ifndef MIN +#define MIN(a, b) (((a) < (b)) ? (a) : (b)) +#endif /* MIN */ + +#define COLOR_BLACK "\x1B[0;30m" +#define COLOR_RED "\x1B[0;31m" +#define COLOR_GREEN "\x1B[0;32m" +#define COLOR_YELLOW "\x1B[0;33m" +#define COLOR_BLUE "\x1B[0;34m" +#define COLOR_MAGENTA "\x1B[0;35m" +#define COLOR_CYAN "\x1B[0;36m" +#define COLOR_WHITE "\x1B[0;37m" + +#define COLOR_RESET "\x1b[0m" + +#define BIT_SET(REG, BIT) ((REG) |= (BIT)) +#define BIT_CLEAR(REG, BIT) ((REG) &= ~(BIT)) + +#endif /* _MACROS_H_ */ diff --git a/src/utils/nrf5340_audio_dk.c b/src/utils/nrf5340_audio_dk.c new file mode 100644 index 0000000..7baaf0e --- /dev/null +++ b/src/utils/nrf5340_audio_dk.c @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include + +#include "led.h" +#include "button_handler.h" +#include "button_assignments.h" +#include "sd_card.h" +#include "board_version.h" +#include "channel_assignment.h" + +#include "sd_card_playback.h" + +#include +LOG_MODULE_REGISTER(nrf5340_audio_dk, CONFIG_MODULE_NRF5340_AUDIO_DK_LOG_LEVEL); + +static struct board_version board_rev; + +static int leds_set(void) +{ + int ret; + + /* Blink LED 3 to indicate that APP core is running */ + ret = led_blink(LED_APP_3_GREEN); + if (ret) { + return ret; + } + +#if (CONFIG_AUDIO_DEV == HEADSET) + enum audio_channel channel; + + channel_assignment_get(&channel); + + if (channel == AUDIO_CH_L) { + ret = led_on(LED_APP_RGB, LED_COLOR_BLUE); + } else { + ret = led_on(LED_APP_RGB, LED_COLOR_MAGENTA); + } +#elif (CONFIG_AUDIO_DEV == GATEWAY) + ret = led_on(LED_APP_RGB, LED_COLOR_GREEN); +#endif /* (CONFIG_AUDIO_DEV == HEADSET) */ + + if (ret) { + return ret; + } + + return 0; +} + +static int channel_assign_check(void) +{ +#if (CONFIG_AUDIO_DEV == HEADSET) && CONFIG_AUDIO_HEADSET_CHANNEL_RUNTIME + int ret; + bool pressed; + + ret = button_pressed(BUTTON_VOLUME_DOWN, &pressed); + if (ret) { + return ret; + } + + if (pressed) { + channel_assignment_set(AUDIO_CH_L); + return 0; + } + + ret = button_pressed(BUTTON_VOLUME_UP, &pressed); + if (ret) { + return ret; + } + + if (pressed) { + channel_assignment_set(AUDIO_CH_R); + return 0; + } +#endif + + return 0; +} + +int nrf5340_audio_dk_init(void) +{ + int ret; + + ret = led_init(); + if (ret) { + LOG_ERR("Failed to initialize LED module"); + return ret; + } + + ret = button_handler_init(); + if (ret) { + LOG_ERR("Failed to initialize button handler"); + return ret; + } + + ret = channel_assign_check(); + if (ret) { + LOG_ERR("Failed get channel assignment"); + return ret; + } + + ret = board_version_valid_check(); + if (ret) { + return ret; + } + + ret = board_version_get(&board_rev); + if (ret) { + return ret; + } + + if (board_rev.mask & BOARD_VERSION_VALID_MSK_SD_CARD) { + ret = sd_card_init(); + if (ret != -ENODEV && ret != 0) { + LOG_ERR("Failed to initialize SD card"); + return ret; + } + } + + ret = leds_set(); + if (ret) { + LOG_ERR("Failed to set LEDs"); + return ret; + } + + if (IS_ENABLED(CONFIG_SD_CARD_PLAYBACK)) { + ret = sd_card_playback_init(); + if (ret) { + LOG_ERR("Failed to initialize SD card playback"); + return ret; + } + } + + /* Use this to turn on 128 MHz clock for cpu_app */ + ret = nrfx_clock_divider_set(NRF_CLOCK_DOMAIN_HFCLK, NRF_CLOCK_HFCLK_DIV_1); + ret -= NRFX_ERROR_BASE_NUM; + if (ret) { + return ret; + } + + return 0; +} diff --git a/src/utils/nrf5340_audio_dk.h b/src/utils/nrf5340_audio_dk.h new file mode 100644 index 0000000..25490f5 --- /dev/null +++ b/src/utils/nrf5340_audio_dk.h @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _NRF5340_AUDIO_DK_H_ +#define _NRF5340_AUDIO_DK_H_ + +#include "led.h" + +/** + * @brief Initialize the hardware related modules on the nRF5340 Audio DK/PCA10121. + * + * @return 0 if successful, error otherwise. + */ +int nrf5340_audio_dk_init(void); + +#endif /* _NRF5340_AUDIO_DK_H_ */ diff --git a/src/utils/uicr.c b/src/utils/uicr.c new file mode 100644 index 0000000..4b86bec --- /dev/null +++ b/src/utils/uicr.c @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2021 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "uicr.h" + +#include +#include +#include + +/* Memory address to store segger number of the board */ +#define MEM_ADDR_UICR_SNR UICR_APP_BASE_ADDR +/* Memory address to store the channel intended used for this board */ +#define MEM_ADDR_UICR_CH (MEM_ADDR_UICR_SNR + sizeof(uint32_t)) + +uint8_t uicr_channel_get(void) +{ + return *(uint8_t *)MEM_ADDR_UICR_CH; +} + +int uicr_channel_set(uint8_t channel) +{ + if (channel == *(uint8_t *)MEM_ADDR_UICR_CH) { + return 0; + } else if (*(uint32_t *)MEM_ADDR_UICR_CH != 0xFFFFFFFF) { + return -EROFS; + } + + nrfx_nvmc_byte_write(MEM_ADDR_UICR_CH, channel); + + if (channel == *(uint8_t *)MEM_ADDR_UICR_CH) { + return 0; + } else { + return -EIO; + } +} + +uint64_t uicr_snr_get(void) +{ + return *(uint64_t *)MEM_ADDR_UICR_SNR; +} diff --git a/src/utils/uicr.h b/src/utils/uicr.h new file mode 100644 index 0000000..5c2b729 --- /dev/null +++ b/src/utils/uicr.h @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2021 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _UICR_H_ +#define _UICR_H_ + +#include + +// TODO: Discuss better alternative for UICR storage. This memory range is not documented +#define UICR_APP_BASE_ADDR (NRF_UICR_S_BASE + 0xF0) + +/** + * @brief Get raw channel value from UICR + */ +uint8_t uicr_channel_get(void); + +/** + * @brief Write raw channel value to UICR + * + * @param channel Channel value + * + * @return 0 if successful + * @return -EROFS if different channel is already written + * @return -EIO if channel failed to be written + */ +int uicr_channel_set(uint8_t channel); + +/** + * @brief Get Segger serial number value from UICR + */ +uint64_t uicr_snr_get(void); + +#endif /* _UICR_H_ */ diff --git a/sysbuild/ipc_radio/prj.conf b/sysbuild/ipc_radio/prj.conf new file mode 100644 index 0000000..57a458d --- /dev/null +++ b/sysbuild/ipc_radio/prj.conf @@ -0,0 +1,50 @@ +# +# Copyright (c) 2023 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +CONFIG_HEAP_MEM_POOL_SIZE=8192 +CONFIG_MAIN_STACK_SIZE=2048 +CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048 + +CONFIG_MBOX=y +CONFIG_IPC_SERVICE=y + +CONFIG_BT=y +CONFIG_BT_HCI_RAW=y +CONFIG_BT_CTLR_ASSERT_HANDLER=y + +CONFIG_BT_ISO_PERIPHERAL=y +CONFIG_BT_ISO_CENTRAL=y +CONFIG_BT_ISO_BROADCASTER=y +CONFIG_BT_ISO_SYNC_RECEIVER=y +CONFIG_BT_EXT_ADV=y +CONFIG_BT_PER_ADV_SYNC_TRANSFER_RECEIVER=y +CONFIG_BT_PER_ADV_SYNC_TRANSFER_SENDER=y + +CONFIG_BT_CTLR_CONN_ISO_GROUPS=1 +CONFIG_BT_CTLR_CONN_ISO_STREAMS=2 +CONFIG_BT_CTLR_SYNC_ISO_STREAM_COUNT=2 +CONFIG_BT_CTLR_ADV_ISO_SET=1 +CONFIG_BT_CTLR_ADV_ISO_STREAM_COUNT=2 + +# Support two links as a central, or one link as a peripheral +CONFIG_BT_MAX_CONN=3 +CONFIG_BT_CTLR_SDC_PERIPHERAL_COUNT=1 + +# Allow using more than default advertising event length +CONFIG_BT_CTLR_ADV_DATA_LEN_MAX=251 + +# To present the audio at the right point in time, we need the controller and +# audio clock to be synchronized +CONFIG_MPSL_TRIGGER_IPC_TASK_ON_RTC_START=y +CONFIG_MPSL_TRIGGER_IPC_TASK_ON_RTC_START_CHANNEL=4 + +# Needed for builds with nrf21540 +# Can also be set to 20, but check local restrictions first +#CONFIG_BT_CTLR_TX_PWR_ANTENNA=10 +#CONFIG_MPSL_FEM_NRF21540_TX_GAIN_DB=10 + +CONFIG_IPC_RADIO_BT=y +CONFIG_IPC_RADIO_BT_HCI_IPC=y diff --git a/sysbuild/ipc_radio/prj_release.conf b/sysbuild/ipc_radio/prj_release.conf new file mode 100644 index 0000000..93f28af --- /dev/null +++ b/sysbuild/ipc_radio/prj_release.conf @@ -0,0 +1,56 @@ +# +# Copyright (c) 2023 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +CONFIG_HEAP_MEM_POOL_SIZE=8192 +CONFIG_MAIN_STACK_SIZE=2048 +CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048 + +CONFIG_MBOX=y +CONFIG_IPC_SERVICE=y + +CONFIG_BT=y +CONFIG_BT_HCI_RAW=y +CONFIG_BT_CTLR_ASSERT_HANDLER=y + +CONFIG_BT_ISO_PERIPHERAL=y +CONFIG_BT_ISO_CENTRAL=y +CONFIG_BT_ISO_BROADCASTER=y +CONFIG_BT_ISO_SYNC_RECEIVER=y +CONFIG_BT_EXT_ADV=y +CONFIG_BT_PER_ADV_SYNC_TRANSFER_RECEIVER=y +CONFIG_BT_PER_ADV_SYNC_TRANSFER_SENDER=y + +CONFIG_BT_CTLR_CONN_ISO_GROUPS=1 +CONFIG_BT_CTLR_CONN_ISO_STREAMS=2 +CONFIG_BT_CTLR_SYNC_ISO_STREAM_COUNT=2 +CONFIG_BT_CTLR_ADV_ISO_SET=1 +CONFIG_BT_CTLR_ADV_ISO_STREAM_COUNT=2 + +# Support two links as a central, or one link as a peripheral +CONFIG_BT_MAX_CONN=3 +CONFIG_BT_CTLR_SDC_PERIPHERAL_COUNT=1 + +# Allow using more than default advertising event length +CONFIG_BT_CTLR_ADV_DATA_LEN_MAX=251 + +# To present the audio at the right point in time, we need the controller and +# audio clock to be synchronized +CONFIG_MPSL_TRIGGER_IPC_TASK_ON_RTC_START=y +CONFIG_MPSL_TRIGGER_IPC_TASK_ON_RTC_START_CHANNEL=4 + +CONFIG_IPC_RADIO_BT=y +CONFIG_IPC_RADIO_BT_HCI_IPC=y + +# General +CONFIG_DEBUG=n +CONFIG_ASSERT=n +CONFIG_STACK_USAGE=n +CONFIG_THREAD_MONITOR=n +CONFIG_SERIAL=n +CONFIG_CONSOLE=n +CONFIG_PRINTK=n +CONFIG_UART_CONSOLE=n +CONFIG_BOOT_BANNER=n diff --git a/sysbuild/mcuboot/app_fota.overlay b/sysbuild/mcuboot/app_fota.overlay new file mode 100644 index 0000000..74d3dfb --- /dev/null +++ b/sysbuild/mcuboot/app_fota.overlay @@ -0,0 +1,5 @@ +/ { + chosen { + zephyr,code-partition = &boot_partition; + }; +}; diff --git a/sysbuild/mcuboot/boards/nrf5340_audio_dk_nrf5340_cpuapp_fota.overlay b/sysbuild/mcuboot/boards/nrf5340_audio_dk_nrf5340_cpuapp_fota.overlay new file mode 100644 index 0000000..6b28ae7 --- /dev/null +++ b/sysbuild/mcuboot/boards/nrf5340_audio_dk_nrf5340_cpuapp_fota.overlay @@ -0,0 +1,7 @@ +#include "../../../boards/nrf5340_audio_dk_nrf5340_cpuapp_fota.overlay" + +/ { + chosen { + nordic,pm-ext-flash = &mx25r64; + }; +}; diff --git a/sysbuild/mcuboot/prj_fota.conf b/sysbuild/mcuboot/prj_fota.conf new file mode 100644 index 0000000..7db115d --- /dev/null +++ b/sysbuild/mcuboot/prj_fota.conf @@ -0,0 +1,62 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +CONFIG_PM=n + +CONFIG_MAIN_STACK_SIZE=10240 +CONFIG_MBEDTLS_CFG_FILE="mcuboot-mbedtls-cfg.h" + +CONFIG_BOOT_SWAP_SAVE_ENCTLV=n +CONFIG_BOOT_ENCRYPT_IMAGE=n + +CONFIG_BOOT_BOOTSTRAP=n + +### mbedTLS has its own heap +# CONFIG_HEAP_MEM_POOL_SIZE is not set + +### We never want Zephyr's copy of tinycrypt. If tinycrypt is needed, +### MCUboot has its own copy in tree. +# CONFIG_TINYCRYPT is not set +# CONFIG_TINYCRYPT_ECC_DSA is not set +# CONFIG_TINYCRYPT_SHA256 is not set + +CONFIG_FLASH=y +CONFIG_FPROTECT=y + +### Various Zephyr boards enable features that we don't want. +# CONFIG_BT is not set +# CONFIG_BT_CTLR is not set +# CONFIG_I2C is not set + +CONFIG_LOG=y +CONFIG_LOG_MODE_MINIMAL=y # former CONFIG_MODE_MINIMAL +### Ensure Zephyr logging changes don't use more resources +CONFIG_LOG_DEFAULT_LEVEL=0 +### Use info log level by default +CONFIG_MCUBOOT_LOG_LEVEL_INF=y +### Decrease footprint by ~4 KB in comparison to CBPRINTF_COMPLETE=y +CONFIG_CBPRINTF_NANO=y +### Use the minimal C library to reduce flash usage +CONFIG_MINIMAL_LIBC=y +CONFIG_NRF_RTC_TIMER_USER_CHAN_COUNT=0 + +CONFIG_BOOT_MAX_IMG_SECTORS=2048 + +# Flash +CONFIG_SPI=y +CONFIG_SPI_NOR=y +CONFIG_SPI_NOR_SFDP_DEVICETREE=y +CONFIG_SPI_NOR_CS_WAIT_DELAY=5 +CONFIG_SPI_NOR_FLASH_LAYOUT_PAGE_SIZE=4096 +CONFIG_MULTITHREADING=y +CONFIG_BOOT_ERASE_PROGRESSIVELY=y +CONFIG_SOC_FLASH_NRF_EMULATE_ONE_BYTE_WRITE_ACCESS=y + +# The network core cannot access external flash directly. The flash simulator must be used to +# provide a memory region that is used to forward the new firmware to the network core. +CONFIG_FLASH_SIMULATOR=y +CONFIG_FLASH_SIMULATOR_DOUBLE_WRITES=y +CONFIG_FLASH_SIMULATOR_STATS=n diff --git a/sysbuild_fota.conf b/sysbuild_fota.conf new file mode 100644 index 0000000..5ac30f8 --- /dev/null +++ b/sysbuild_fota.conf @@ -0,0 +1,15 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +# Enable bootloaders for both cores +SB_CONFIG_BOOTLOADER_MCUBOOT=y +SB_CONFIG_SECURE_BOOT_NETCORE=y +SB_CONFIG_NETCORE_APP_UPDATE=y + +# Settings required for external flash to be used for DFU +SB_CONFIG_PM_EXTERNAL_FLASH_MCUBOOT_SECONDARY=y +SB_CONFIG_PM_OVERRIDE_EXTERNAL_DRIVER_CHECK=y +SB_CONFIG_MCUBOOT_MODE_OVERWRITE_ONLY=y diff --git a/tools/buildprog/buildprog.py b/tools/buildprog/buildprog.py new file mode 100644 index 0000000..c2c9255 --- /dev/null +++ b/tools/buildprog/buildprog.py @@ -0,0 +1,418 @@ +# +# Copyright (c) 2018 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +""" +Script to build and program the nRF5340 Audio project to multiple devices +""" + +import argparse +import sys +import shutil +import os +import json +import subprocess +import re +import getpass +from pathlib import Path +from colorama import Fore, Style +from prettytable import PrettyTable +from nrf5340_audio_dk_devices import ( + BuildType, + Channel, + DeviceConf, + BuildConf, + AudioDevice, + SelectFlags, + Core, + Transport, +) +from program import program_threads_run + + +BUILDPROG_FOLDER = Path(__file__).resolve().parent +NRF5340_AUDIO_FOLDER = (BUILDPROG_FOLDER / "../..").resolve() +NRF_FOLDER = (BUILDPROG_FOLDER / "../../../..").resolve() +if os.getenv("AUDIO_KIT_SERIAL_NUMBERS_JSON") is None: + AUDIO_KIT_SERIAL_NUMBERS_JSON = BUILDPROG_FOLDER / "nrf5340_audio_dk_devices.json" +else: + AUDIO_KIT_SERIAL_NUMBERS_JSON = Path( + os.getenv("AUDIO_KIT_SERIAL_NUMBERS_JSON")) +TARGET_BOARD_NRF5340_AUDIO_DK_APP_NAME = "nrf5340_audio_dk/nrf5340/cpuapp" + +TARGET_AUDIO_FOLDER = NRF5340_AUDIO_FOLDER +TARGET_AUDIO_BUILD_FOLDER = TARGET_AUDIO_FOLDER / "tools/build" + +UNICAST_SERVER_OVERLAY = NRF5340_AUDIO_FOLDER / "unicast_server/overlay-unicast_server.conf" +UNICAST_CLIENT_OVERLAY = NRF5340_AUDIO_FOLDER / "unicast_client/overlay-unicast_client.conf" +BROADCAST_SINK_OVERLAY = NRF5340_AUDIO_FOLDER / "broadcast_sink/overlay-broadcast_sink.conf" +BROADCAST_SOURCE_OVERLAY = NRF5340_AUDIO_FOLDER / "broadcast_source/overlay-broadcast_source.conf" + +TARGET_RELEASE_FOLDER = "build_release" +TARGET_DEBUG_FOLDER = "build_debug" + +MAX_USER_NAME_LEN = 248 - len('\0') + + +def __print_add_color(status): + if status == SelectFlags.FAIL: + return Fore.RED + status.value + Style.RESET_ALL + elif status == SelectFlags.DONE: + return Fore.GREEN + status.value + Style.RESET_ALL + return status.value + + +def __print_dev_conf(device_list): + """Print settings in a formatted manner""" + table = PrettyTable() + table.field_names = [ + "snr", + "snr conn", + "device", + "only reboot", + "core app programmed", + "core net programmed", + ] + for device in device_list: + row = [] + row.append(device.nrf5340_audio_dk_snr) + color = Fore.GREEN if device.snr_connected else Fore.YELLOW + row.append(color + str(device.snr_connected) + Style.RESET_ALL) + row.append(device.nrf5340_audio_dk_dev.value) + row.append(__print_add_color(device.only_reboot)) + row.append(__print_add_color(device.core_app_programmed)) + row.append(__print_add_color(device.core_net_programmed)) + + table.add_row(row) + print(table) + + +def __build_cmd_get(core: Core, device: AudioDevice, build: BuildType, + pristine, options): + + build_cmd = (f"west build {TARGET_AUDIO_FOLDER} " + f"-b {TARGET_BOARD_NRF5340_AUDIO_DK_APP_NAME} " + f"--sysbuild") + + if core == Core.app: + build_cmd += " --domain nrf5340_audio" + elif core == Core.net: + build_cmd += " --domain ipc_radio" + else: + raise Exception("Invalid core!") + + if build == BuildType.debug: + release_flag = "" + elif build == BuildType.release: + release_flag = " -DFILE_SUFFIX=release" + else: + raise Exception("Invalid build type!") + + device_flag = "" + + if options.nrf21540: + device_flag += " -Dnrf5340_audio_SHIELD=nrf21540ek" + device_flag += " -Dipc_radio_SHIELD=nrf21540ek" + if options.custom_bt_name is not None and options.user_bt_name: + raise Exception( + "User BT name option is invalid when custom BT name is set") + if options.custom_bt_name is not None: + custom_bt_name = "_".join(options.custom_bt_name)[ + :MAX_USER_NAME_LEN].upper() + device_flag += " -DCONFIG_BT_DEVICE_NAME=\\\"" + custom_bt_name + "\\\"" + if options.user_bt_name: + user_specific_bt_name = ( + "AUDIO_DEV_" + getpass.getuser())[:MAX_USER_NAME_LEN].upper() + device_flag += " -DCONFIG_BT_DEVICE_NAME=\\\"" + user_specific_bt_name + "\\\"" + if options.transport == Transport.broadcast.name: + if device == AudioDevice.headset: + overlay_flag = f" -DEXTRA_CONF_FILE={BROADCAST_SINK_OVERLAY}" + else: + overlay_flag = f" -DEXTRA_CONF_FILE={BROADCAST_SOURCE_OVERLAY}" + else: + if device == AudioDevice.headset: + overlay_flag = f" -DEXTRA_CONF_FILE={UNICAST_SERVER_OVERLAY}" + else: + overlay_flag = f" -DEXTRA_CONF_FILE={UNICAST_CLIENT_OVERLAY}" + + if os.name == 'nt': + release_flag = release_flag.replace('\\', '/') + if pristine: + build_cmd += " --pristine" + + dest_folder = TARGET_AUDIO_BUILD_FOLDER / options.transport / device / core / build + + return build_cmd, dest_folder, device_flag, release_flag, overlay_flag + + +def __build_module(build_config, options): + build_cmd, dest_folder, device_flag, release_flag, overlay_flag = __build_cmd_get( + build_config.core, + build_config.device, + build_config.build, + build_config.pristine, + options, + ) + west_str = f"{build_cmd} -d {dest_folder} " + + if build_config.pristine and dest_folder.exists(): + shutil.rmtree(dest_folder) + + # Only add compiler flags if folder doesn't exist already + if not dest_folder.exists(): + west_str = west_str + device_flag + release_flag + overlay_flag + + print("Run: " + west_str) + + ret_val = os.system(west_str) + + if ret_val: + raise Exception("cmake error: " + str(ret_val)) + + +def __find_snr(): + """Rebooting or programming requires connected programmer/debugger""" + + # Use nrfjprog executable for WSL compatibility + stdout = subprocess.check_output( + "nrfjprog --ids", shell=True).decode("utf-8") + snrs = re.findall(r"([\d]+)", stdout) + + if not snrs: + print("No programmer/debugger connected to PC") + + return list(map(int, snrs)) + + +def __populate_hex_paths(dev, options): + """Poplulate hex paths where relevant""" + + _, temp_dest_folder, _, _, _ = __build_cmd_get(Core.app, dev.nrf5340_audio_dk_dev, options.build, options.pristine, options) + dev.hex_path_app = temp_dest_folder / "nrf5340_audio/zephyr/zephyr.hex" + + _, temp_dest_folder, _, _, _ = __build_cmd_get(Core.net, dev.nrf5340_audio_dk_dev, options.build, options.pristine, options) + dev.hex_path_net = temp_dest_folder / "ipc_radio/zephyr/zephyr.hex" + + +def __finish(device_list): + """Finish script. Print report""" + print("build_prog.py finished. Report:") + __print_dev_conf(device_list) + exit(0) + + +def __main(): + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description=( + "This script builds and programs the nRF5340 " + "Audio project on Windows and Linux" + ), + epilog=("If there exists an environmental variable called \"AUDIO_KIT_SERIAL_NUMBERS_JSON\"" + "which contains the location of a json file," + "the program will use this file as a substitute for nrf5340_audio_dk_devices.json"), + allow_abbrev=False + ) + parser.add_argument( + "-r", + "--only_reboot", + default=False, + action="store_true", + help="Only reboot, no building or programming", + ) + parser.add_argument( + "-p", + "--program", + default=False, + action="store_true", + help="Will program and reboot nRF5340 Audio DK", + ) + parser.add_argument( + "-c", + "--core", + type=str, + choices=[i.name for i in Core], + help="Select which cores to include in build", + ) + parser.add_argument( + "--pristine", + default=False, + action="store_true", + help="Will build cleanly" + ) + parser.add_argument( + "-b", + "--build", + required="-p" in sys.argv or "--program" in sys.argv, + choices=[i.name for i in BuildType], + help="Select the build type", + ) + parser.add_argument( + "-d", + "--device", + required=("-r" in sys.argv or "--only_reboot" in sys.argv) + or ( + ("-b" in sys.argv or "--build" in sys.argv) + and ("both" in sys.argv or "app" in sys.argv) + ), + choices=[i.name for i in AudioDevice], + help=( + "nRF5340 Audio on the application core can be " + "built for either ordinary headset " + "(earbuds/headphone..) use or gateway (USB dongle)" + ), + ) + parser.add_argument( + "-s", + "--sequential", + action="store_true", + dest="sequential_prog", + default=False, + help="Run nrfjprog sequentially instead of in parallel", + ) + parser.add_argument( + "-f", + "--recover_on_fail", + action="store_true", + dest="recover_on_fail", + default=False, + help="Recover device if programming fails", + ) + parser.add_argument( + "--nrf21540", + action="store_true", + dest="nrf21540", + default=False, + help="Set when using nRF21540 for extra TX power", + ) + parser.add_argument( + "-cn", + "--custom_bt_name", + nargs='*', + dest="custom_bt_name", + default=None, + help="Use custom Bluetooth device name", + ) + parser.add_argument( + "-u", + "--user_bt_name", + action="store_true", + dest="user_bt_name", + default=False, + help="Set to generate a user specific Bluetooth device name.\ + Note that this will put the computer user name on air in clear text", + ) + parser.add_argument( + "-t", + "--transport", + required=True, + choices=[i.name for i in Transport], + default=Transport.unicast.name, + help="Select the transport type", + ) + + options = parser.parse_args(args=sys.argv[1:]) + + # Post processing for Enums + if options.core is None: + cores = [] + elif options.core == "both": + cores = [Core.app, Core.net] + else: + cores = [Core[options.core]] + + if options.device is None: + devices = [] + elif options.device == "both": + devices = [AudioDevice.gateway, AudioDevice.headset] + else: + devices = [AudioDevice[options.device]] + + options.build = BuildType[options.build] if options.build else None + + options.only_reboot = SelectFlags.TBD if options.only_reboot else SelectFlags.NOT + + boards_snr_connected = __find_snr() + if not boards_snr_connected: + print("No snrs connected") + + # Update device list + # This JSON file should be altered by the developer. + # Then run git update-index --skip-worktree FILENAME to avoid changes + # being pushed + with AUDIO_KIT_SERIAL_NUMBERS_JSON.open() as f: + dev_arr = json.load(f) + device_list = [ + DeviceConf( + nrf5340_audio_dk_snr=dev["nrf5340_audio_dk_snr"], + channel=Channel[dev["channel"]], + snr_connected=(dev["nrf5340_audio_dk_snr"] + in boards_snr_connected), + recover_on_fail=options.recover_on_fail, + nrf5340_audio_dk_dev=AudioDevice[dev["nrf5340_audio_dk_dev"]], + cores=cores, + devices=devices, + _only_reboot=options.only_reboot, + ) + for dev in dev_arr + ] + + __print_dev_conf(device_list) + + # Initialization step finsihed + # Reboot step start + + if options.only_reboot == SelectFlags.TBD: + program_threads_run(device_list, sequential=options.sequential_prog) + __finish(device_list) + + # Reboot step finished + # Build step start + + if options.build is not None: + print("Invoking build step") + build_configs = [] + + if AudioDevice.headset in devices: + for c in cores: + build_configs.append( + BuildConf( + core=c, + device=AudioDevice.headset, + pristine=options.pristine, + build=options.build, + ) + ) + if AudioDevice.gateway in devices: + for c in cores: + build_configs.append( + BuildConf( + core=c, + device=AudioDevice.gateway, + pristine=options.pristine, + build=options.build, + ) + ) + + for build_cfg in build_configs: + __build_module(build_cfg, options) + + # Build step finished + # Program step start + + if options.program: + for dev in device_list: + if dev.snr_connected: + __populate_hex_paths(dev, options) + + program_threads_run(device_list, sequential=options.sequential_prog) + + # Program step finished + + __finish(device_list) + + +if __name__ == "__main__": + __main() diff --git a/tools/buildprog/fw_info_data.py b/tools/buildprog/fw_info_data.py new file mode 100644 index 0000000..d86a2cb --- /dev/null +++ b/tools/buildprog/fw_info_data.py @@ -0,0 +1,96 @@ +# +# Copyright (c) 2022 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + +""" +Generate fw_info for B0N container from .config +""" + +from intelhex import IntelHex + +import argparse +import struct + + +def get_fw_info(input_hex, offset, magic_value, fw_version, fw_valid_val): + # 0x0c start of fw_info_total_size + # 0x10 start of flash_used + # 0x14 start of fw_version + # 0x18 the address of the start of the image + # 0x1c the address of the boot point (vector table) of the firmware + # 0x20 the address Value that can be modified to invalidate the firmware + + fw_info_bytes = magic_value + for i in range(0xc, 0x14): + fw_info_bytes += input_hex[offset + i].to_bytes(1, byteorder='little') + fw_info_bytes += struct.pack(' int: + if dev.core_net_programmed == SelectFlags.TBD: + if not path.isfile(dev.hex_path_net): + print(f"NET core hex not found. Built only for APP core. {dev.hex_path_net}") + else: + print(f"Programming net core on: {dev}") + cmd = (f"nrfjprog --program {dev.hex_path_net} -f NRF53 -q " + f"--snr {dev.nrf5340_audio_dk_snr} --sectorerase --coprocessor CP_NETWORK") + ret_val = system(cmd) + if ret_val != 0: + if not dev.recover_on_fail: + dev.core_net_programmed = SelectFlags.FAIL + return ret_val + else: + dev.core_net_programmed = SelectFlags.DONE + + if dev.core_app_programmed == SelectFlags.TBD: + if not path.isfile(dev.hex_path_app): + print(f"APP core hex not found. Built only for NET core. {dev.hex_path_app}") + return 1 + else: + print(f"Programming app core on: {dev}") + cmd = (f"nrfjprog --program {dev.hex_path_app} -f NRF53 -q " + f"--snr {dev.nrf5340_audio_dk_snr} --chiperase --coprocessor CP_APPLICATION") + ret_val = system(cmd) + if ret_val != 0: + if not dev.recover_on_fail: + dev.core_app_programmed = SelectFlags.FAIL + return ret_val + else: + dev.core_app_programmed = SelectFlags.DONE + + # Populate UICR data matching the JSON file + if not __populate_uicr(dev): + dev.core_app_programmed = SelectFlags.FAIL + return 1 + + if dev.core_net_programmed != SelectFlags.NOT or dev.core_app_programmed != SelectFlags.NOT: + print(f"Resetting {dev}") + cmd = f"nrfjprog -r --snr {dev.nrf5340_audio_dk_snr}" + ret_val = system(cmd) + if ret_val != 0: + return ret_val + return 0 + + +def _recover(dev: DeviceConf): + print(f"Recovering device: {dev}") + ret_val = system( + f"nrfjprog --recover --coprocessor CP_NETWORK --snr {dev.nrf5340_audio_dk_snr}" + ) + if ret_val != 0: + dev.core_net_programmed = SelectFlags.FAIL + + ret_val = system( + f"nrfjprog --recover --coprocessor CP_APPLICATION --snr {dev.nrf5340_audio_dk_snr}" + ) + if ret_val != 0: + dev.core_app_programmed = SelectFlags.FAIL + + +def __program_thread(dev: DeviceConf): + if dev.only_reboot == SelectFlags.TBD: + print(f"Resetting {dev}") + cmd = f"nrfjprog -r --snr {dev.nrf5340_audio_dk_snr}" + ret_val = system(cmd) + dev.only_reboot = SelectFlags.FAIL if ret_val else SelectFlags.DONE + return + + return_code = _program_cores(dev) + if return_code != 0 and dev.recover_on_fail: + _recover(dev) + _program_cores(dev) + + +def program_threads_run(devices_list: List[DeviceConf], sequential: bool = False): + """Program devices in parallel""" + threads = [] + # First program net cores if applicable + for dev in devices_list: + if not dev.snr_connected: + dev.only_reboot = SelectFlags.NOT + dev.core_app_programmed = SelectFlags.NOT + dev.core_net_programmed = SelectFlags.NOT + continue + thread = Thread(target=__program_thread, args=(dev,)) + threads.append(thread) + thread.start() + if sequential: + thread.join() + + for thread in threads: + thread.join() + + threads.clear() diff --git a/tools/uart_terminal/scripts/get_serial_ports.py b/tools/uart_terminal/scripts/get_serial_ports.py new file mode 100644 index 0000000..b371720 --- /dev/null +++ b/tools/uart_terminal/scripts/get_serial_ports.py @@ -0,0 +1,28 @@ +# +# Copyright (c) 2022 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +import sys +import subprocess + +def get_serial_ports(): + nrfjprog_com = subprocess.Popen(["nrfjprog", "--com"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + nrfjprog_com.wait() + + if nrfjprog_com.returncode != 0: + sys.exit("'nrfjprog --com' failed") + + output = nrfjprog_com.communicate() + output_decoded = output[0].decode() + output_decoded_lines = output_decoded.splitlines() + + ports = list() + + for line in output_decoded_lines: + if "VCOM0" in line: + info = line.split(" ") + ports.append(info[1]) + + return ports diff --git a/tools/uart_terminal/scripts/linux_terminator_config b/tools/uart_terminal/scripts/linux_terminator_config new file mode 100644 index 0000000..25a5224 --- /dev/null +++ b/tools/uart_terminal/scripts/linux_terminator_config @@ -0,0 +1,62 @@ +[global_config] + suppress_multiple_term_dialog = True +[keybindings] +[profiles] + [[default]] + cursor_color = "#aaaaaa" + scrollback_infinite = True +[layouts] + [[default]] + [[[child0]]] + type = Window + parent = "" + order = 0 + position = 72:35 + maximised = False + fullscreen = False + size = 2000, 1000 + title = nRF5340 Audio DK + last_active_window = True + [[[child1]]] + type = VPaned + parent = child0 + order = 0 + position = 540 + ratio = 0.5 + [[[child2]]] + type = HPaned + parent = child1 + order = 0 + position = 600 + ratio = 0.5 + [[[terminal3]]] + type = Terminal + parent = child2 + order = 0 + profile = default + command = python3 scripts/open_terminator.py 0; sh + [[[terminal4]]] + type = Terminal + parent = child2 + order = 1 + profile = default + command = python3 scripts/open_terminator.py 1; sh + [[[child5]]] + type = HPaned + parent = child1 + order = 1 + position = 600 + ratio = 0.5 + [[[terminal6]]] + type = Terminal + parent = child5 + order = 0 + profile = default + command = python3 scripts/open_terminator.py 2; sh + [[[terminal7]]] + type = Terminal + parent = child5 + order = 1 + profile = default + command = echo Available ttyACM devices: && cd /dev/ && ls -l | grep "ttyACM"; bash +[plugins] diff --git a/tools/uart_terminal/scripts/open_putty.py b/tools/uart_terminal/scripts/open_putty.py new file mode 100644 index 0000000..8485c05 --- /dev/null +++ b/tools/uart_terminal/scripts/open_putty.py @@ -0,0 +1,15 @@ +# +# Copyright (c) 2022 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +import subprocess +from get_serial_ports import get_serial_ports + + +def open_putty(): + ports = get_serial_ports() + + for port in ports: + subprocess.Popen("putty -serial " + port + " -sercfg 115200,8,n,1,N") diff --git a/tools/uart_terminal/scripts/open_terminator.py b/tools/uart_terminal/scripts/open_terminator.py new file mode 100644 index 0000000..4862466 --- /dev/null +++ b/tools/uart_terminal/scripts/open_terminator.py @@ -0,0 +1,16 @@ +# +# Copyright (c) 2022 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +import sys +import subprocess +from get_serial_ports import get_serial_ports + +ports = get_serial_ports() + +if int(sys.argv[1]) < len(ports): + subprocess.Popen(["minicom", "--color=on", "-b 115200", "-8", "-D " + ports[int(sys.argv[1])]]) +else: + print("Not enough boards connected") diff --git a/tools/uart_terminal/uart_terminal.py b/tools/uart_terminal/uart_terminal.py new file mode 100644 index 0000000..fcdf236 --- /dev/null +++ b/tools/uart_terminal/uart_terminal.py @@ -0,0 +1,19 @@ +# +# Copyright (c) 2022 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +import sys +import subprocess + +sys.path.append("./scripts") + +from open_putty import open_putty + +if sys.platform == "linux": + terminator = subprocess.Popen(["terminator", "--config=scripts/linux_terminator_config"], stderr=subprocess.PIPE) +elif sys.platform == "win32": + open_putty() +else: + print("OS not supported") diff --git a/unicast_client/CMakeLists.txt b/unicast_client/CMakeLists.txt new file mode 100644 index 0000000..5c48ca6 --- /dev/null +++ b/unicast_client/CMakeLists.txt @@ -0,0 +1,8 @@ +# +# Copyright (c) 2023 Nordic Semiconductor +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +target_sources(app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/main.c) diff --git a/unicast_client/README.rst b/unicast_client/README.rst new file mode 100644 index 0000000..286c52f --- /dev/null +++ b/unicast_client/README.rst @@ -0,0 +1,144 @@ +.. _nrf53_audio_unicast_client_app: + +nRF5340 Audio: Unicast client +############################# + +.. contents:: + :local: + :depth: 2 + +The nRF5340 Audio unicast client application implements the :ref:`CIS gateway mode `. + +In this mode, one Connected Isochronous Group (CIG) can be used with two Connected Isochronous Streams (CIS). +Transmitting unidirectional or transceiving bidirectional audio happens using CIG and CIS. + +The following limitations apply to this application: + +* One CIG with two CIS. +* Audio input: USB or I2S (Line in or using Pulse Density Modulation). +* Audio output: USB or I2S/Analog headset output. +* Configuration: 16 bit, several bit rates ranging from 32 kbps to 124 kbps. + +.. _nrf53_audio_unicast_client_app_requirements: + +Requirements +************ + +The application shares the :ref:`requirements common to all nRF5340 Audio application `. + +.. _nrf53_audio_unicast_client_app_ui: + +User interface +************** + +Most of the user interface mappings are common across all nRF5340 Audio applications. +See the :ref:`nrf53_audio_app_ui` page for detailed overview. + +This application uses specific mapping for the following user interface elements: + +* Long-pressed on the unicast client device during startup: + + * **BTN5** - Clears the previously stored bonding information. + +* Pressed on the unicast client device during playback: + + * **PLAY/PAUSE** - Starts or pauses the playback of the stream. + * **VOL-** - Turns the playback volume down (and unmutes). + * **VOL+** - Turns the playback volume up (and unmutes). + * **BTN 4** - Sends a test tone generated on the device. Use this tone to check the synchronization of headsets. + * **BTN5** - Mutes the playback volume (and unmutes). + +* **LED1** - Blinking blue - Kit is streaming audio to a headset. +* **RGB** - Solid green - The device is programmed as the gateway. + +.. _nrf53_audio_unicast_client_app_configuration: + +Configuration +************* + +By default, if you have not made any changes to :file:`.conf` files at :file:`applications/nrf5340_audio/`, the nRF5340 build script tries to build the CIS applications in the CIS unidirectional mode. +To switch to the bidirectional mode, see :ref:`nrf53_audio_app_configuration_select_bidirectional`. + +For other configuration options, see :ref:`nrf53_audio_app_configuration` and :ref:`nrf53_audio_app_fota`. + +For information about how to configure applications in the |NCS|, see :ref:`configure_application`. + +.. _nrf53_audio_unicast_client_app_building: + +Building and running +******************** + +This application can be found under :file:`applications/nrf5340_audio/unicast_client` in the nRF Connect SDK folder structure, but it uses :file:`.conf` files at :file:`applications/nrf5340_audio/`. + +The nRF5340 Audio DK comes preprogrammed with basic firmware that indicates if the kit is functional. +See :ref:`nrf53_audio_app_dk_testing_out_of_the_box` for more information. + +To build the application, see :ref:`nrf53_audio_app_building`. + +.. _nrf53_audio_unicast_client_app_testing: + +Testing +******* + +After building and programming the application, you can test the default CIS gateway mode using one unicast client device and at least one CIS headset device. +The recommended approach is to use another nRF5340 Audio DK programmed with the :ref:`unicast server application `, but you can also use an external CIS headset device. + +.. note:: + |nrf5340_audio_external_devices_note| + +The following testing scenario assumes you are using USB as the audio source on the gateway. +This is the default setting. + +Complete the following steps to test the unidirectional CIS mode for one gateway and at least one headset device: + +1. Make sure that the development kits are still plugged into the USB ports and are turned on. + + .. note:: + |usb_known_issues| + + **LED3** starts blinking green on every device to indicate the ongoing CPU activity on the application core. +#. Wait for the **LED1** on the gateway to start blinking blue. + This happens shortly after programming the development kit and indicates that the gateway device is connected to at least one headset and ready to send data. +#. Search the list of audio devices listed in the sound settings of your operating system for *nRF5340 USB Audio* (gateway) and select it as the output device. +#. Connect headphones to the **HEADPHONE** audio jack on the headset device. +#. Start audio playback on your PC from any source. +#. Wait for **LED1** to blink blue on the headset. + When they do, the audio stream has started on the headset. + + .. note:: + The audio outputs only to the left channel of the audio jack, even if the given headset is configured as the right headset. + This is because of the mono hardware codec chip used on the development kits. + If you want to play stereo sound using one development kit, you must connect an external hardware codec chip that supports stereo. + +#. Wait for **LED2** to light up solid green on the headsets to indicate that the audio synchronization is achieved. +#. Press the **VOL-** button on the gateway. + The playback volume decreases for the headset. +#. Press the **PLAY/PAUSE** button on any one of the devices. + The playback stops for the headset and the streaming state for all devices is set to paused. +#. Press the **BTN 4** button on the gateway multiple times. + For each button press, the audio stream playback is stopped and the gateway sends a test tone to the headset. + These tones can be used as audio cues to check the synchronization between two headsets. + +For other testing options, refer to :ref:`nrf53_audio_unicast_client_app_ui`. + +After the kits have paired for the first time, they are now bonded. +This means the Long-Term Key (LTK) is stored on each side, and that the kits will only connect to each other unless the bonding information is cleared. +To clear the bonding information, press and hold **BTN 5** during boot or reprogram all the development kits. + +When you finish testing, power off the nRF5340 Audio development kits by switching the power switch from On to Off. + +.. _nrf53_audio_unicast_client_app_testing_steps_cis_walkie_talkie: + +Testing the walkie-talkie demo +============================== + +Testing the walkie-talkie demo is identical to the default testing procedure, except for the following differences: + +* You must enable the Kconfig option mentioned in :ref:`nrf53_audio_app_configuration_enable_walkie_talkie` before building the application. +* Instead of controlling the playback, you can speak through the PDM microphones. + The line is open all the time, no need to press any buttons to talk, but the volume control works as in the default testing procedure. + +Dependencies +************ + +For the list of dependencies, check the application's source files. diff --git a/unicast_client/main.c b/unicast_client/main.c new file mode 100644 index 0000000..f0e9e2c --- /dev/null +++ b/unicast_client/main.c @@ -0,0 +1,583 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "streamctrl.h" + +#include + +#include "unicast_client.h" +#include "zbus_common.h" +#include "nrf5340_audio_dk.h" +#include "led.h" +#include "button_assignments.h" +#include "macros_common.h" +#include "audio_system.h" +#include "button_handler.h" +#include "bt_le_audio_tx.h" +#include "bt_mgmt.h" +#include "bt_rendering_and_capture.h" +#include "bt_content_ctrl.h" +#include "le_audio_rx.h" +#include "fw_info_app.h" + +#include +LOG_MODULE_REGISTER(main, CONFIG_MAIN_LOG_LEVEL); + +static enum stream_state strm_state = STATE_PAUSED; + +ZBUS_SUBSCRIBER_DEFINE(button_evt_sub, CONFIG_BUTTON_MSG_SUB_QUEUE_SIZE); +ZBUS_SUBSCRIBER_DEFINE(content_control_evt_sub, CONFIG_CONTENT_CONTROL_MSG_SUB_QUEUE_SIZE); + +ZBUS_MSG_SUBSCRIBER_DEFINE(le_audio_evt_sub); + +ZBUS_CHAN_DECLARE(button_chan); +ZBUS_CHAN_DECLARE(le_audio_chan); +ZBUS_CHAN_DECLARE(bt_mgmt_chan); +ZBUS_CHAN_DECLARE(cont_media_chan); +ZBUS_CHAN_DECLARE(sdu_ref_chan); + +ZBUS_OBS_DECLARE(sdu_ref_msg_listen); + +static struct k_thread button_msg_sub_thread_data; +static struct k_thread le_audio_msg_sub_thread_data; +static struct k_thread content_control_msg_sub_thread_data; + +static k_tid_t button_msg_sub_thread_id; +static k_tid_t le_audio_msg_sub_thread_id; +static k_tid_t content_control_thread_id; + +K_THREAD_STACK_DEFINE(button_msg_sub_thread_stack, CONFIG_BUTTON_MSG_SUB_STACK_SIZE); +K_THREAD_STACK_DEFINE(le_audio_msg_sub_thread_stack, CONFIG_LE_AUDIO_MSG_SUB_STACK_SIZE); +K_THREAD_STACK_DEFINE(content_control_msg_sub_thread_stack, + CONFIG_CONTENT_CONTROL_MSG_SUB_STACK_SIZE); + +/* Function for handling all stream state changes */ +static void stream_state_set(enum stream_state stream_state_new) +{ + strm_state = stream_state_new; +} + +static void content_control_msg_sub_thread(void) +{ + int ret; + const struct zbus_channel *chan; + + while (1) { + ret = zbus_sub_wait(&content_control_evt_sub, &chan, K_FOREVER); + ERR_CHK(ret); + + struct content_control_msg msg; + + ret = zbus_chan_read(chan, &msg, ZBUS_READ_TIMEOUT_MS); + ERR_CHK(ret); + + switch (msg.event) { + case MEDIA_START: + unicast_client_start(0); + break; + + case MEDIA_STOP: + unicast_client_stop(0); + break; + + default: + LOG_WRN("Unhandled event from content ctrl: %d", msg.event); + break; + } + + STACK_USAGE_PRINT("content_ctrl_msg_thread", &content_control_msg_sub_thread); + } +} + +/** + * @brief Handle button activity. + */ +static void button_msg_sub_thread(void) +{ + int ret; + const struct zbus_channel *chan; + + while (1) { + ret = zbus_sub_wait(&button_evt_sub, &chan, K_FOREVER); + ERR_CHK(ret); + + struct button_msg msg; + + ret = zbus_chan_read(chan, &msg, ZBUS_READ_TIMEOUT_MS); + ERR_CHK(ret); + + LOG_DBG("Got btn evt from queue - id = %d, action = %d", msg.button_pin, + msg.button_action); + + if (msg.button_action != BUTTON_PRESS) { + LOG_WRN("Unhandled button action"); + return; + } + + switch (msg.button_pin) { + case BUTTON_PLAY_PAUSE: + if (IS_ENABLED(CONFIG_WALKIE_TALKIE_DEMO)) { + LOG_WRN("Play/pause not supported in walkie-talkie mode"); + break; + } + + if (strm_state == STATE_STREAMING) { + ret = bt_content_ctrl_stop(NULL); + if (ret) { + LOG_WRN("Could not stop: %d", ret); + } + + } else if (strm_state == STATE_PAUSED) { + ret = bt_content_ctrl_start(NULL); + if (ret) { + LOG_WRN("Could not start: %d", ret); + } + + } else { + LOG_WRN("In invalid state: %d", strm_state); + } + + break; + + case BUTTON_VOLUME_UP: + ret = bt_r_and_c_volume_up(); + if (ret) { + LOG_WRN("Failed to increase volume: %d", ret); + } + + break; + + case BUTTON_VOLUME_DOWN: + ret = bt_r_and_c_volume_down(); + if (ret) { + LOG_WRN("Failed to decrease volume: %d", ret); + } + + break; + + case BUTTON_4: + if (IS_ENABLED(CONFIG_AUDIO_TEST_TONE)) { + if (IS_ENABLED(CONFIG_WALKIE_TALKIE_DEMO)) { + LOG_DBG("Test tone is disabled in walkie-talkie mode"); + break; + } + + if (strm_state != STATE_STREAMING) { + LOG_WRN("Not in streaming state"); + break; + } + + ret = audio_system_encode_test_tone_step(); + if (ret) { + LOG_WRN("Failed to play test tone, ret: %d", ret); + } + + break; + } + + break; + + case BUTTON_5: + if (IS_ENABLED(CONFIG_AUDIO_MUTE)) { + ret = bt_r_and_c_volume_mute(false); + if (ret) { + LOG_WRN("Failed to mute, ret: %d", ret); + } + + break; + } + + break; + + default: + LOG_WRN("Unexpected/unhandled button id: %d", msg.button_pin); + } + + STACK_USAGE_PRINT("button_msg_thread", &button_msg_sub_thread_data); + } +} + +/** + * @brief Handle Bluetooth LE audio events. + */ +static void le_audio_msg_sub_thread(void) +{ + int ret; + uint32_t bitrate_bps; + uint32_t sampling_rate_hz; + const struct zbus_channel *chan; + + while (1) { + struct le_audio_msg msg; + + ret = zbus_sub_wait_msg(&le_audio_evt_sub, &chan, &msg, K_FOREVER); + ERR_CHK(ret); + + LOG_DBG("Received event = %d, current state = %d", msg.event, strm_state); + + switch (msg.event) { + case LE_AUDIO_EVT_STREAMING: + LOG_DBG("LE audio evt streaming"); + + if (strm_state == STATE_STREAMING) { + LOG_DBG("Got streaming event in streaming state"); + break; + } + + if (msg.dir == BT_AUDIO_DIR_SINK) { + audio_system_encoder_start(); + } + + audio_system_start(); + stream_state_set(STATE_STREAMING); + + ret = led_blink(LED_APP_1_BLUE); + ERR_CHK(ret); + break; + + case LE_AUDIO_EVT_NOT_STREAMING: + LOG_DBG("LE audio evt not_streaming"); + + if (strm_state == STATE_PAUSED) { + LOG_DBG("Got not_streaming event in paused state"); + break; + } + + if (msg.dir == BT_AUDIO_DIR_SINK) { + audio_system_encoder_stop(); + } + + stream_state_set(STATE_PAUSED); + audio_system_stop(); + + ret = led_on(LED_APP_1_BLUE); + ERR_CHK(ret); + break; + + case LE_AUDIO_EVT_NO_VALID_CFG: + LOG_WRN("No valid configurations found or CIS establishment failed, will " + "disconnect"); + + ret = bt_mgmt_conn_disconnect(msg.conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN); + if (ret) { + LOG_ERR("Failed to disconnect: %d", ret); + } + + break; + + case LE_AUDIO_EVT_CONFIG_RECEIVED: + struct bt_conn_info conn_info; + uint16_t interval = 0; + + ret = bt_conn_get_info(msg.conn, &conn_info); + if (ret) { + LOG_ERR("Failed to get conn info"); + } else { + interval = conn_info.le.interval; + } + + /* Only update conn param once */ + if (((IS_ENABLED(CONFIG_BT_AUDIO_TX) && msg.dir == BT_AUDIO_DIR_SINK) || + (!IS_ENABLED(CONFIG_BT_AUDIO_TX) && msg.dir == BT_AUDIO_DIR_SOURCE)) && + interval != CONFIG_BLE_ACL_CONN_INTERVAL_SLOW) { + struct bt_le_conn_param param; + + /* Set the ACL interval up to allow more time for ISO packets */ + param.interval_min = CONFIG_BLE_ACL_CONN_INTERVAL_SLOW; + param.interval_max = CONFIG_BLE_ACL_CONN_INTERVAL_SLOW; + param.latency = CONFIG_BLE_ACL_SLAVE_LATENCY; + param.timeout = CONFIG_BLE_ACL_SUP_TIMEOUT; + + ret = bt_conn_le_param_update(msg.conn, ¶m); + if (ret) { + LOG_WRN("Failed to update conn parameters: %d", ret); + } + } + + LOG_DBG("LE audio config received"); + + ret = unicast_client_config_get(msg.conn, msg.dir, &bitrate_bps, + &sampling_rate_hz); + if (ret) { + LOG_WRN("Failed to get config: %d", ret); + break; + } + + LOG_DBG("\tSampling rate: %d Hz", sampling_rate_hz); + LOG_DBG("\tBitrate (compressed): %d bps", bitrate_bps); + + if (msg.dir == BT_AUDIO_DIR_SINK) { + ret = audio_system_config_set(sampling_rate_hz, bitrate_bps, + VALUE_NOT_SET); + ERR_CHK(ret); + } else if (msg.dir == BT_AUDIO_DIR_SOURCE) { + ret = audio_system_config_set(VALUE_NOT_SET, VALUE_NOT_SET, + sampling_rate_hz); + ERR_CHK(ret); + } + + break; + + case LE_AUDIO_EVT_COORD_SET_DISCOVERED: + uint8_t num_conn = 0; + uint8_t num_filled = 0; + + bt_mgmt_num_conn_get(&num_conn); + + if (msg.set_size > 0) { + /* Check how many active connections we have for the given set */ + LOG_DBG("Setting SIRK"); + bt_mgmt_scan_sirk_set(msg.sirk); + bt_mgmt_set_size_filled_get(&num_filled); + + LOG_INF("Set members found: %d of %d", num_filled, msg.set_size); + + if (num_filled == msg.set_size) { + /* All devices in set found, clear SIRK before scanning */ + bt_mgmt_scan_sirk_set(NULL); + } + } + + if (num_conn < CONFIG_BT_MAX_CONN) { + /* Room for more connections, start scanning again */ + ret = bt_mgmt_scan_start(0, 0, BT_MGMT_SCAN_TYPE_CONN, NULL, + BRDCAST_ID_NOT_USED); + if (ret) { + LOG_ERR("Failed to resume scanning: %d", ret); + } + } + + break; + + case LE_AUDIO_EVT_STREAM_SENT: + /* Nothing to do. */ + break; + + default: + LOG_WRN("Unexpected/unhandled le_audio event: %d", msg.event); + break; + } + + STACK_USAGE_PRINT("le_audio_msg_thread", &le_audio_msg_sub_thread_data); + } +} + +/** + * @brief Zbus listener to receive events from bt_mgmt. + * + * @param[in] chan Zbus channel. + * + * @note Will in most cases be called from BT_RX context, + * so there should not be too much processing done here. + */ +static void bt_mgmt_evt_handler(const struct zbus_channel *chan) +{ + int ret; + const struct bt_mgmt_msg *msg; + uint8_t num_conn = 0; + + msg = zbus_chan_const_msg(chan); + bt_mgmt_num_conn_get(&num_conn); + + switch (msg->event) { + case BT_MGMT_CONNECTED: + /* NOTE: The string below is used by the Nordic CI system */ + LOG_INF("Connection event. Num connections: %u", num_conn); + + break; + + case BT_MGMT_SECURITY_CHANGED: + LOG_INF("Security changed"); + + ret = bt_r_and_c_discover(msg->conn); + if (ret) { + LOG_WRN("Failed to discover rendering services"); + } + + if (IS_ENABLED(CONFIG_STREAM_BIDIRECTIONAL)) { + ret = unicast_client_discover(msg->conn, UNICAST_SERVER_BIDIR); + } else { + ret = unicast_client_discover(msg->conn, UNICAST_SERVER_SINK); + } + + if (ret) { + LOG_ERR("Failed to handle unicast client discover: %d", ret); + } + + break; + + case BT_MGMT_DISCONNECTED: + /* NOTE: The string below is used by the Nordic CI system */ + LOG_INF("Disconnection event. Num connections: %u", num_conn); + + unicast_client_conn_disconnected(msg->conn); + break; + + default: + LOG_WRN("Unexpected/unhandled bt_mgmt event: %d", msg->event); + break; + } +} + +ZBUS_LISTENER_DEFINE(bt_mgmt_evt_listen, bt_mgmt_evt_handler); + +/** + * @brief Create zbus subscriber threads. + * + * @return 0 for success, error otherwise. + */ +static int zbus_subscribers_create(void) +{ + int ret; + + button_msg_sub_thread_id = k_thread_create( + &button_msg_sub_thread_data, button_msg_sub_thread_stack, + CONFIG_BUTTON_MSG_SUB_STACK_SIZE, (k_thread_entry_t)button_msg_sub_thread, NULL, + NULL, NULL, K_PRIO_PREEMPT(CONFIG_BUTTON_MSG_SUB_THREAD_PRIO), 0, K_NO_WAIT); + ret = k_thread_name_set(button_msg_sub_thread_id, "BUTTON_MSG_SUB"); + if (ret) { + LOG_ERR("Failed to create button_msg thread"); + return ret; + } + + le_audio_msg_sub_thread_id = k_thread_create( + &le_audio_msg_sub_thread_data, le_audio_msg_sub_thread_stack, + CONFIG_LE_AUDIO_MSG_SUB_STACK_SIZE, (k_thread_entry_t)le_audio_msg_sub_thread, NULL, + NULL, NULL, K_PRIO_PREEMPT(CONFIG_LE_AUDIO_MSG_SUB_THREAD_PRIO), 0, K_NO_WAIT); + ret = k_thread_name_set(le_audio_msg_sub_thread_id, "LE_AUDIO_MSG_SUB"); + if (ret) { + LOG_ERR("Failed to create le_audio_msg thread"); + return ret; + } + + content_control_thread_id = k_thread_create( + &content_control_msg_sub_thread_data, content_control_msg_sub_thread_stack, + CONFIG_CONTENT_CONTROL_MSG_SUB_STACK_SIZE, + (k_thread_entry_t)content_control_msg_sub_thread, NULL, NULL, NULL, + K_PRIO_PREEMPT(CONFIG_CONTENT_CONTROL_MSG_SUB_THREAD_PRIO), 0, K_NO_WAIT); + ret = k_thread_name_set(content_control_thread_id, "CONTENT_CONTROL_MSG_SUB"); + if (ret) { + return ret; + } + + ret = zbus_chan_add_obs(&sdu_ref_chan, &sdu_ref_msg_listen, ZBUS_ADD_OBS_TIMEOUT_MS); + if (ret) { + LOG_ERR("Failed to add timestamp listener"); + return ret; + } + + return 0; +} + +/** + * @brief Link zbus producers and observers. + * + * @return 0 for success, error otherwise. + */ +static int zbus_link_producers_observers(void) +{ + int ret; + + if (!IS_ENABLED(CONFIG_ZBUS)) { + return -ENOTSUP; + } + + ret = zbus_chan_add_obs(&button_chan, &button_evt_sub, ZBUS_ADD_OBS_TIMEOUT_MS); + if (ret) { + LOG_ERR("Failed to add button sub"); + return ret; + } + + ret = zbus_chan_add_obs(&le_audio_chan, &le_audio_evt_sub, ZBUS_ADD_OBS_TIMEOUT_MS); + if (ret) { + LOG_ERR("Failed to add le_audio sub"); + return ret; + } + + ret = zbus_chan_add_obs(&bt_mgmt_chan, &bt_mgmt_evt_listen, ZBUS_ADD_OBS_TIMEOUT_MS); + if (ret) { + LOG_ERR("Failed to add bt_mgmt listener"); + return ret; + } + + ret = zbus_chan_add_obs(&cont_media_chan, &content_control_evt_sub, + ZBUS_ADD_OBS_TIMEOUT_MS); + + return 0; +} + +uint8_t stream_state_get(void) +{ + return strm_state; +} + +void streamctrl_send(void const *const data, size_t size, uint8_t num_ch) +{ + int ret; + static int prev_ret; + + struct le_audio_encoded_audio enc_audio = {.data = data, .size = size, .num_ch = num_ch}; + + if (strm_state == STATE_STREAMING) { + ret = unicast_client_send(0, enc_audio); + + if (ret != 0 && ret != prev_ret) { + if (ret == -ECANCELED) { + LOG_WRN("Sending operation cancelled"); + } else { + LOG_WRN("Problem with sending LE audio data, ret: %d", ret); + } + } + + prev_ret = ret; + } +} + +int main(void) +{ + int ret; + + LOG_DBG("Main started"); + + ret = nrf5340_audio_dk_init(); + ERR_CHK(ret); + + ret = fw_info_app_print(); + ERR_CHK(ret); + + ret = bt_mgmt_init(); + ERR_CHK(ret); + + ret = audio_system_init(); + ERR_CHK(ret); + + ret = zbus_subscribers_create(); + ERR_CHK_MSG(ret, "Failed to create zbus subscriber threads"); + + ret = zbus_link_producers_observers(); + ERR_CHK_MSG(ret, "Failed to link zbus producers and observers"); + + ret = le_audio_rx_init(); + ERR_CHK(ret); + + ret = bt_r_and_c_init(); + ERR_CHK(ret); + + ret = bt_content_ctrl_init(); + ERR_CHK(ret); + + ret = unicast_client_enable(0, le_audio_rx_data_handler); + ERR_CHK(ret); + + ret = bt_mgmt_scan_start(0, 0, BT_MGMT_SCAN_TYPE_CONN, CONFIG_BT_DEVICE_NAME, + BRDCAST_ID_NOT_USED); + if (ret) { + LOG_ERR("Failed to start scanning"); + return ret; + } + + return 0; +} diff --git a/unicast_client/overlay-unicast_client.conf b/unicast_client/overlay-unicast_client.conf new file mode 100644 index 0000000..69c279d --- /dev/null +++ b/unicast_client/overlay-unicast_client.conf @@ -0,0 +1,43 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +CONFIG_BT_CENTRAL=y +CONFIG_AUDIO_DEV=2 + +CONFIG_BT_GATT_DYNAMIC_DB=y +CONFIG_BT_GATT_CLIENT=y +CONFIG_BT_GATT_AUTO_DISCOVER_CCC=y +CONFIG_BT_GATT_AUTO_UPDATE_MTU=y +CONFIG_BT_GATT_CACHING=n +CONFIG_BT_MAX_CONN=2 +CONFIG_BT_MAX_PAIRED=2 +CONFIG_BT_EXT_ADV=y + +CONFIG_BT_AUDIO=y +CONFIG_BT_ISO_CENTRAL=y +CONFIG_BT_BAP_UNICAST_CLIENT=y +CONFIG_BT_CAP_INITIATOR=y +CONFIG_BT_CSIP_SET_COORDINATOR=y +CONFIG_BT_DEVICE_APPEARANCE=2176 + +CONFIG_BT_ISO_TX_BUF_COUNT=4 +# Support an ISO channel per ASE +CONFIG_BT_ISO_MAX_CHAN=4 +CONFIG_BT_VCP_VOL_CTLR=y +CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT=4 +CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT=2 +CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT=2 +CONFIG_BT_MCS=y +CONFIG_BT_MPL=y +CONFIG_UTF8=y +CONFIG_MCTL=y +CONFIG_MCTL_LOCAL_PLAYER_CONTROL=y +CONFIG_MCTL_LOCAL_PLAYER_REMOTE_CONTROL=y + +CONFIG_LC3_ENC_CHAN_MAX=2 +CONFIG_LC3_DEC_CHAN_MAX=1 +CONFIG_MBEDTLS_ENABLE_HEAP=y +CONFIG_MBEDTLS_HEAP_SIZE=2048 diff --git a/unicast_server/CMakeLists.txt b/unicast_server/CMakeLists.txt new file mode 100644 index 0000000..5c48ca6 --- /dev/null +++ b/unicast_server/CMakeLists.txt @@ -0,0 +1,8 @@ +# +# Copyright (c) 2023 Nordic Semiconductor +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +target_sources(app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/main.c) diff --git a/unicast_server/README.rst b/unicast_server/README.rst new file mode 100644 index 0000000..4b253de --- /dev/null +++ b/unicast_server/README.rst @@ -0,0 +1,150 @@ +.. _nrf53_audio_unicast_server_app: + +nRF5340 Audio: Unicast server +############################# + +.. contents:: + :local: + :depth: 2 + +The nRF5340 Audio unicast server application implements the :ref:`CIS headset mode `. + +In this mode, one Connected Isochronous Group (CIG) can be used with two Connected Isochronous Streams (CIS). +Receiving unidirectional or transceiving bidirectional audio happens using CIG and CIS. +In addition, Coordinated Set Identification Service (CSIS) is implemented on the server side. + +The following limitations apply to this application: + +* One CIG, one of the two CIS streams (selectable). +* Audio output: I2S/Analog headset output. +* Audio input: PDM microphone over I2S. +* Configuration: 16 bit, several bit rates ranging from 32 kbps to 124 kbps. + +.. _nrf53_audio_unicast_server_app_requirements: + +Requirements +************ + +The application shares the :ref:`requirements common to all nRF5340 Audio application `. + +.. _nrf53_audio_unicast_server_app_ui: + +User interface +************** + +Most of the user interface mappings are common across all nRF5340 Audio applications. +See the :ref:`nrf53_audio_app_ui` page for detailed overview. + +This application uses specific mapping for the following user interface elements: + +* Long-pressed on the unicast server device during startup: + + * **VOL-** - Changes the headset to the left channel one. + * **VOL+** - Changes the headset to the right channel one. + * **BTN5** - Clears the previously stored bonding information. + +* Pressed on the unicast server device during playback: + + * **PLAY/PAUSE** - Starts or pauses the playback of the stream. + * **VOL-** - Turns the playback volume down. + * **VOL+** - Turns the playback volume up. + * **BTN5** - Mutes the playback volume (and unmutes). + +* **LED1** - Blinking blue - Kits have started streaming audio. +* **LED2** - Solid green - Sync achieved (both drift and presentation compensation are in the ``LOCKED`` state). +* **RGB**: + + * Solid blue - The device is programmed as the left headset. + * Solid magenta - The device is programmed as the right headset. + +.. _nrf53_audio_unicast_server_app_configuration: + +Configuration +************* + +By default, if you have not made any changes to :file:`.conf` files at :file:`applications/nrf5340_audio/`, the nRF5340 build script tries to build the CIS applications in the CIS unidirectional mode. +To switch to the bidirectional mode, see :ref:`nrf53_audio_app_configuration_select_bidirectional`. + +For other configuration options, see :ref:`nrf53_audio_app_configuration` and :ref:`nrf53_audio_app_fota`. + +For information about how to configure applications in the |NCS|, see :ref:`configure_application`. + +.. _nrf53_audio_unicast_server_app_building: + +Building and running +******************** + +This application can be found under :file:`applications/nrf5340_audio/unicast_server` in the nRF Connect SDK folder structure, but it uses :file:`.conf` files at :file:`applications/nrf5340_audio/`. + +The nRF5340 Audio DK comes preprogrammed with basic firmware that indicates if the kit is functional. +See :ref:`nrf53_audio_app_dk_testing_out_of_the_box` for more information. + +To build the application, see :ref:`nrf53_audio_app_building`. + +.. _nrf53_audio_unicast_server_app_testing: + +Testing +******* + +After building and programming the application, you can test the default CIS headset mode using one :ref:`unicast client application ` and one or two unicast server devices (this application). +The recommended approach is to use two other nRF5340 Audio DKs programmed with the :ref:`unicast client application ` for the CIS gateway and the unicast server application (this application) for the CIS headset, respectively, but you can also use an external device that supports the role of unicast server. + +.. note:: + |nrf5340_audio_external_devices_note| + +The following testing scenario assumes you are using USB as the audio source on the gateway. +This is the default setting. + +Complete the following steps to test the unidirectional CIS mode for one gateway and two headset devices: + +1. Make sure that the development kits are still plugged into the USB ports and are turned on. + + .. note:: + |usb_known_issues| + + **LED3** starts blinking green on every device to indicate the ongoing CPU activity on the application core. +#. Wait for the **LED1** on the gateway to start blinking blue. + This happens shortly after programming the development kit and indicates that the gateway device is connected to at least one headset and ready to send data. +#. Search the list of audio devices listed in the sound settings of your operating system for *nRF5340 USB Audio* (gateway) and select it as the output device. +#. Connect headphones to the **HEADPHONE** audio jack on both headset devices. +#. Start audio playback on your PC from any source. +#. Wait for **LED1** to blink blue on the headset. + When they do, the audio stream has started on the headset. + + .. note:: + The audio outputs only to the left channel of the audio jack, even if the given headset is configured as the right headset. + This is because of the mono hardware codec chip used on the development kits. + If you want to play stereo sound using one development kit, you must connect an external hardware codec chip that supports stereo. + +#. Wait for **LED2** to light up solid green on the headsets to indicate that the audio synchronization is achieved. +#. Press the **VOL+** button on one of the headsets. + The playback volume increases for the headset. +#. If you use more than one headset, hold down the **VOL+** button and press the **RESET** button on a headset. + After startup, this headset will be configured as the right channel headset. +#. If you use more than one headset, hold down the **VOL-** button and press the **RESET** button on a headset. + After startup, this headset will be configured as the left channel headset. + You can also just press the **RESET** button to restore the original programmed settings. + +For other testing options, refer to :ref:`nrf53_audio_unicast_server_app_ui`. + +After the kits have paired for the first time, they are now bonded. +This means the Long-Term Key (LTK) is stored on each side, and that the kits will only connect to each other unless the bonding information is cleared. +To clear the bonding information, press and hold **BTN 5** during boot or reprogram all the development kits. + +When you finish testing, power off the nRF5340 Audio development kits by switching the power switch from On to Off. + +.. _nrf53_audio_unicast_server_app_testing_steps_cis_walkie_talkie: + +Testing the walkie-talkie demo +============================== + +Testing the walkie-talkie demo is identical to the default testing procedure, except for the following differences: + +* You must enable the Kconfig option mentioned in :ref:`nrf53_audio_app_configuration_enable_walkie_talkie` before building the application. +* Instead of controlling the playback, you can speak through the PDM microphones. + The line is open all the time, no need to press any buttons to talk, but the volume control works as in the default testing procedure. + +Dependencies +************ + +For the list of dependencies, check the application's source files. diff --git a/unicast_server/main.c b/unicast_server/main.c new file mode 100644 index 0000000..90f9c86 --- /dev/null +++ b/unicast_server/main.c @@ -0,0 +1,570 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include "streamctrl.h" + +#include + +#include "unicast_server.h" +#include "zbus_common.h" +#include "nrf5340_audio_dk.h" +#include "led.h" +#include "button_assignments.h" +#include "macros_common.h" +#include "audio_system.h" +#include "button_handler.h" +#include "bt_le_audio_tx.h" +#include "bt_mgmt.h" +#include "bt_rendering_and_capture.h" +#include "audio_datapath.h" +#include "bt_content_ctrl.h" +#include "le_audio.h" +#include "le_audio_rx.h" +#include "fw_info_app.h" + +#include +LOG_MODULE_REGISTER(main, CONFIG_MAIN_LOG_LEVEL); + +ZBUS_SUBSCRIBER_DEFINE(button_evt_sub, CONFIG_BUTTON_MSG_SUB_QUEUE_SIZE); + +ZBUS_MSG_SUBSCRIBER_DEFINE(le_audio_evt_sub); + +ZBUS_CHAN_DECLARE(button_chan); +ZBUS_CHAN_DECLARE(le_audio_chan); +ZBUS_CHAN_DECLARE(bt_mgmt_chan); +ZBUS_CHAN_DECLARE(volume_chan); + +ZBUS_OBS_DECLARE(volume_evt_sub); + +static struct k_thread button_msg_sub_thread_data; +static struct k_thread le_audio_msg_sub_thread_data; + +static k_tid_t button_msg_sub_thread_id; +static k_tid_t le_audio_msg_sub_thread_id; + +K_THREAD_STACK_DEFINE(button_msg_sub_thread_stack, CONFIG_BUTTON_MSG_SUB_STACK_SIZE); +K_THREAD_STACK_DEFINE(le_audio_msg_sub_thread_stack, CONFIG_LE_AUDIO_MSG_SUB_STACK_SIZE); + +static enum stream_state strm_state = STATE_PAUSED; + +/* Function for handling all stream state changes */ +static void stream_state_set(enum stream_state stream_state_new) +{ + strm_state = stream_state_new; +} + +/** + * @brief Handle button activity. + */ +static void button_msg_sub_thread(void) +{ + int ret; + const struct zbus_channel *chan; + + while (1) { + ret = zbus_sub_wait(&button_evt_sub, &chan, K_FOREVER); + ERR_CHK(ret); + + struct button_msg msg; + + ret = zbus_chan_read(chan, &msg, ZBUS_READ_TIMEOUT_MS); + ERR_CHK(ret); + + LOG_DBG("Got btn evt from queue - id = %d, action = %d", msg.button_pin, + msg.button_action); + + if (msg.button_action != BUTTON_PRESS) { + LOG_WRN("Unhandled button action"); + return; + } + + switch (msg.button_pin) { + case BUTTON_PLAY_PAUSE: + if (IS_ENABLED(CONFIG_WALKIE_TALKIE_DEMO)) { + LOG_WRN("Play/pause not supported in walkie-talkie mode"); + break; + } + + if (bt_content_ctlr_media_state_playing()) { + ret = bt_content_ctrl_stop(NULL); + if (ret) { + LOG_WRN("Could not stop: %d", ret); + } + + } else if (!bt_content_ctlr_media_state_playing()) { + ret = bt_content_ctrl_start(NULL); + if (ret) { + LOG_WRN("Could not start: %d", ret); + } + + } else { + LOG_WRN("In invalid state: %d", strm_state); + } + + break; + + case BUTTON_VOLUME_UP: + ret = bt_r_and_c_volume_up(); + if (ret) { + LOG_WRN("Failed to increase volume: %d", ret); + } + + break; + + case BUTTON_VOLUME_DOWN: + ret = bt_r_and_c_volume_down(); + if (ret) { + LOG_WRN("Failed to decrease volume: %d", ret); + } + + break; + + case BUTTON_4: + if (IS_ENABLED(CONFIG_AUDIO_TEST_TONE)) { + if (IS_ENABLED(CONFIG_WALKIE_TALKIE_DEMO)) { + LOG_DBG("Test tone is disabled in walkie-talkie mode"); + break; + } + + if (strm_state != STATE_STREAMING) { + LOG_WRN("Not in streaming state"); + break; + } + + ret = audio_system_encode_test_tone_step(); + if (ret) { + LOG_WRN("Failed to play test tone, ret: %d", ret); + } + + break; + } + + break; + + case BUTTON_5: + if (IS_ENABLED(CONFIG_AUDIO_MUTE)) { + ret = bt_r_and_c_volume_mute(false); + if (ret) { + LOG_WRN("Failed to mute, ret: %d", ret); + } + + break; + } + + break; + + default: + LOG_WRN("Unexpected/unhandled button id: %d", msg.button_pin); + } + + STACK_USAGE_PRINT("button_msg_thread", &button_msg_sub_thread_data); + } +} + +/** + * @brief Handle Bluetooth LE audio events. + */ +static void le_audio_msg_sub_thread(void) +{ + int ret; + uint32_t pres_delay_us; + uint32_t bitrate_bps; + uint32_t sampling_rate_hz; + const struct zbus_channel *chan; + + while (1) { + struct le_audio_msg msg; + + ret = zbus_sub_wait_msg(&le_audio_evt_sub, &chan, &msg, K_FOREVER); + ERR_CHK(ret); + + LOG_DBG("Received event = %d, current state = %d", msg.event, strm_state); + + switch (msg.event) { + case LE_AUDIO_EVT_STREAMING: + LOG_DBG("LE audio evt streaming"); + + if (msg.dir == BT_AUDIO_DIR_SOURCE) { + audio_system_encoder_start(); + } + + if (strm_state == STATE_STREAMING) { + LOG_DBG("Got streaming event in streaming state"); + break; + } + + audio_system_start(); + stream_state_set(STATE_STREAMING); + ret = led_blink(LED_APP_1_BLUE); + ERR_CHK(ret); + + break; + + case LE_AUDIO_EVT_NOT_STREAMING: + LOG_DBG("LE audio evt not streaming"); + + if (strm_state == STATE_PAUSED) { + LOG_DBG("Got not_streaming event in paused state"); + break; + } + + if (msg.dir == BT_AUDIO_DIR_SOURCE) { + audio_system_encoder_stop(); + } + + stream_state_set(STATE_PAUSED); + audio_system_stop(); + ret = led_on(LED_APP_1_BLUE); + ERR_CHK(ret); + + break; + + case LE_AUDIO_EVT_CONFIG_RECEIVED: + LOG_DBG("LE audio config received"); + + ret = unicast_server_config_get(msg.conn, msg.dir, &bitrate_bps, + &sampling_rate_hz, NULL); + if (ret) { + LOG_WRN("Failed to get config: %d", ret); + break; + } + + LOG_DBG("\tSampling rate: %d Hz", sampling_rate_hz); + LOG_DBG("\tBitrate (compressed): %d bps", bitrate_bps); + + if (msg.dir == BT_AUDIO_DIR_SINK) { + ret = audio_system_config_set(VALUE_NOT_SET, VALUE_NOT_SET, + sampling_rate_hz); + ERR_CHK(ret); + } else if (msg.dir == BT_AUDIO_DIR_SOURCE) { + ret = audio_system_config_set(sampling_rate_hz, bitrate_bps, + VALUE_NOT_SET); + ERR_CHK(ret); + } + + break; + + case LE_AUDIO_EVT_PRES_DELAY_SET: + LOG_DBG("Set presentation delay"); + + ret = unicast_server_config_get(msg.conn, BT_AUDIO_DIR_SINK, NULL, NULL, + &pres_delay_us); + if (ret) { + LOG_ERR("Failed to get config: %d", ret); + break; + } + + ret = audio_datapath_pres_delay_us_set(pres_delay_us); + if (ret) { + LOG_ERR("Failed to set presentation delay to %d", pres_delay_us); + break; + } + + LOG_INF("Presentation delay %d us is set by initiator", pres_delay_us); + + break; + + case LE_AUDIO_EVT_NO_VALID_CFG: + LOG_WRN("No valid configurations found, will disconnect"); + + ret = bt_mgmt_conn_disconnect(msg.conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN); + if (ret) { + LOG_ERR("Failed to disconnect: %d", ret); + } + + break; + + case LE_AUDIO_EVT_STREAM_SENT: + /* Nothing to do. */ + break; + + default: + LOG_WRN("Unexpected/unhandled le_audio event: %d", msg.event); + + break; + } + + STACK_USAGE_PRINT("le_audio_msg_thread", &le_audio_msg_sub_thread_data); + } +} + +/** + * @brief Create zbus subscriber threads. + * + * @return 0 for success, error otherwise. + */ +static int zbus_subscribers_create(void) +{ + int ret; + + button_msg_sub_thread_id = k_thread_create( + &button_msg_sub_thread_data, button_msg_sub_thread_stack, + CONFIG_BUTTON_MSG_SUB_STACK_SIZE, (k_thread_entry_t)button_msg_sub_thread, NULL, + NULL, NULL, K_PRIO_PREEMPT(CONFIG_BUTTON_MSG_SUB_THREAD_PRIO), 0, K_NO_WAIT); + ret = k_thread_name_set(button_msg_sub_thread_id, "BUTTON_MSG_SUB"); + if (ret) { + LOG_ERR("Failed to create button_msg thread"); + return ret; + } + + le_audio_msg_sub_thread_id = k_thread_create( + &le_audio_msg_sub_thread_data, le_audio_msg_sub_thread_stack, + CONFIG_LE_AUDIO_MSG_SUB_STACK_SIZE, (k_thread_entry_t)le_audio_msg_sub_thread, NULL, + NULL, NULL, K_PRIO_PREEMPT(CONFIG_LE_AUDIO_MSG_SUB_THREAD_PRIO), 0, K_NO_WAIT); + ret = k_thread_name_set(le_audio_msg_sub_thread_id, "LE_AUDIO_MSG_SUB"); + if (ret) { + LOG_ERR("Failed to create le_audio_msg thread"); + return ret; + } + + return 0; +} + +/** + * @brief Zbus listener to receive events from bt_mgmt. + * + * @param[in] chan Zbus channel. + * + * @note Will in most cases be called from BT_RX context, + * so there should not be too much processing done here. + */ +static void bt_mgmt_evt_handler(const struct zbus_channel *chan) +{ + int ret; + const struct bt_mgmt_msg *msg; + uint8_t num_conn = 0; + + msg = zbus_chan_const_msg(chan); + bt_mgmt_num_conn_get(&num_conn); + + switch (msg->event) { + case BT_MGMT_CONNECTED: + /* NOTE: The string below is used by the Nordic CI system */ + LOG_INF("Connection event. Num connections: %u", num_conn); + + break; + + case BT_MGMT_DISCONNECTED: + /* NOTE: The string below is used by the Nordic CI system */ + LOG_INF("Disconnection event. Num connections: %u", num_conn); + + ret = bt_content_ctrl_conn_disconnected(msg->conn); + if (ret) { + LOG_ERR("Failed to handle disconnection in content control: %d", ret); + } + + break; + + case BT_MGMT_SECURITY_CHANGED: + LOG_INF("Security changed"); + + ret = bt_content_ctrl_discover(msg->conn); + if (ret == -EALREADY) { + LOG_DBG("Discovery in progress or already done"); + } else if (ret) { + LOG_ERR("Failed to start discovery of content control: %d", ret); + } + + break; + + default: + LOG_WRN("Unexpected/unhandled bt_mgmt event: %d", msg->event); + + break; + } +} + +ZBUS_LISTENER_DEFINE(bt_mgmt_evt_listen, bt_mgmt_evt_handler); + +/** + * @brief Link zbus producers and observers. + * + * @return 0 for success, error otherwise. + */ +static int zbus_link_producers_observers(void) +{ + int ret; + + if (!IS_ENABLED(CONFIG_ZBUS)) { + return -ENOTSUP; + } + + ret = zbus_chan_add_obs(&button_chan, &button_evt_sub, ZBUS_ADD_OBS_TIMEOUT_MS); + if (ret) { + LOG_ERR("Failed to add button sub"); + return ret; + } + + ret = zbus_chan_add_obs(&le_audio_chan, &le_audio_evt_sub, ZBUS_ADD_OBS_TIMEOUT_MS); + if (ret) { + LOG_ERR("Failed to add le_audio sub"); + return ret; + } + + ret = zbus_chan_add_obs(&bt_mgmt_chan, &bt_mgmt_evt_listen, ZBUS_ADD_OBS_TIMEOUT_MS); + if (ret) { + LOG_ERR("Failed to add bt_mgmt sub"); + return ret; + } + + ret = zbus_chan_add_obs(&volume_chan, &volume_evt_sub, ZBUS_ADD_OBS_TIMEOUT_MS); + if (ret) { + LOG_ERR("Failed to add volume sub"); + return ret; + } + + return 0; +} + +/* + * @brief The following configures the data for the extended advertising. This includes the + * Audio Stream Control Service requirements [BAP 3.7.2.1.1] in the AUX_ADV_IND Extended + * Announcements. + * + * @param ext_adv_buf Pointer to the bt_data used for extended advertising. + * @param ext_adv_buf_size Size of @p ext_adv_buf. + * @param ext_adv_count Pointer to the number of elements added to @p adv_buf. + * + * @return 0 for success, error otherwise. + */ + +static int ext_adv_populate(struct bt_data *ext_adv_buf, size_t ext_adv_buf_size, + size_t *ext_adv_count) +{ + int ret; + size_t ext_adv_buf_cnt = 0; + + NET_BUF_SIMPLE_DEFINE_STATIC(uuid_buf, CONFIG_EXT_ADV_UUID_BUF_MAX); + + ext_adv_buf[ext_adv_buf_cnt].type = BT_DATA_UUID16_SOME; + ext_adv_buf[ext_adv_buf_cnt].data = uuid_buf.data; + ext_adv_buf_cnt++; + + ret = bt_r_and_c_uuid_populate(&uuid_buf); + + if (ret) { + LOG_ERR("Failed to add adv data from renderer: %d", ret); + return ret; + } + + ret = bt_content_ctrl_uuid_populate(&uuid_buf); + + if (ret) { + LOG_ERR("Failed to add adv data from content ctrl: %d", ret); + return ret; + } + + ret = bt_mgmt_manufacturer_uuid_populate(&uuid_buf, CONFIG_BT_DEVICE_MANUFACTURER_ID); + if (ret) { + LOG_ERR("Failed to add adv data with manufacturer ID: %d", ret); + return ret; + } + + ret = unicast_server_adv_populate(&ext_adv_buf[ext_adv_buf_cnt], + ext_adv_buf_size - ext_adv_buf_cnt); + + if (ret < 0) { + LOG_ERR("Failed to add adv data from unicast server: %d", ret); + return ret; + } + + ext_adv_buf_cnt += ret; + + /* Add the number of UUIDs */ + ext_adv_buf[0].data_len = uuid_buf.len; + + LOG_DBG("Size of adv data: %d, num_elements: %d", sizeof(struct bt_data) * ext_adv_buf_cnt, + ext_adv_buf_cnt); + + *ext_adv_count = ext_adv_buf_cnt; + + return 0; +} + +uint8_t stream_state_get(void) +{ + return strm_state; +} + +void streamctrl_send(void const *const data, size_t size, uint8_t num_ch) +{ + int ret; + static int prev_ret; + + struct le_audio_encoded_audio enc_audio = {.data = data, .size = size, .num_ch = num_ch}; + + if (strm_state == STATE_STREAMING) { + ret = unicast_server_send(enc_audio); + + if (ret != 0 && ret != prev_ret) { + if (ret == -ECANCELED) { + LOG_WRN("Sending operation cancelled"); + } else { + LOG_WRN("Problem with sending LE audio data, ret: %d", ret); + } + } + + prev_ret = ret; + } +} + +int main(void) +{ + int ret; + enum bt_audio_location location; + enum audio_channel channel; + static struct bt_data ext_adv_buf[CONFIG_EXT_ADV_BUF_MAX]; + + LOG_DBG("Main started"); + + size_t ext_adv_buf_cnt = 0; + + ret = nrf5340_audio_dk_init(); + ERR_CHK(ret); + + ret = fw_info_app_print(); + ERR_CHK(ret); + + ret = bt_mgmt_init(); + ERR_CHK(ret); + + ret = audio_system_init(); + ERR_CHK(ret); + + ret = zbus_subscribers_create(); + ERR_CHK_MSG(ret, "Failed to create zbus subscriber threads"); + + ret = zbus_link_producers_observers(); + ERR_CHK_MSG(ret, "Failed to link zbus producers and observers"); + + ret = le_audio_rx_init(); + ERR_CHK_MSG(ret, "Failed to initialize rx path"); + + channel_assignment_get(&channel); + + if (channel == AUDIO_CH_L) { + location = BT_AUDIO_LOCATION_FRONT_LEFT; + } else { + location = BT_AUDIO_LOCATION_FRONT_RIGHT; + } + + ret = unicast_server_enable(le_audio_rx_data_handler, location); + ERR_CHK_MSG(ret, "Failed to enable LE Audio"); + + ret = bt_r_and_c_init(); + ERR_CHK(ret); + + ret = bt_content_ctrl_init(); + ERR_CHK(ret); + + ret = ext_adv_populate(ext_adv_buf, ARRAY_SIZE(ext_adv_buf), &ext_adv_buf_cnt); + ERR_CHK(ret); + + ret = bt_mgmt_adv_start(0, ext_adv_buf, ext_adv_buf_cnt, NULL, 0, true); + ERR_CHK(ret); + + return 0; +} diff --git a/unicast_server/overlay-unicast_server.conf b/unicast_server/overlay-unicast_server.conf new file mode 100644 index 0000000..dac80b1 --- /dev/null +++ b/unicast_server/overlay-unicast_server.conf @@ -0,0 +1,49 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +CONFIG_AUDIO_DEV=1 + +CONFIG_BT_GATT_AUTO_DISCOVER_CCC=y +CONFIG_BT_GATT_AUTO_RESUBSCRIBE=n +CONFIG_BT_GATT_AUTO_SEC_REQ=n +CONFIG_BT_GATT_AUTO_UPDATE_MTU=y +CONFIG_BT_GATT_CACHING=n +CONFIG_BT_GATT_CLIENT=y +CONFIG_BT_GATT_DYNAMIC_DB=y + +CONFIG_BT_MAX_CONN=4 +CONFIG_BT_MAX_PAIRED=4 +CONFIG_BT_EXT_ADV=y + +CONFIG_BT_AUDIO=y + +CONFIG_BT_PERIPHERAL=y +CONFIG_BT_ISO_PERIPHERAL=y +CONFIG_BT_BAP_UNICAST_SERVER=y +CONFIG_BT_CAP_ACCEPTOR=y +CONFIG_BT_CAP_ACCEPTOR_SET_MEMBER=y +CONFIG_BT_PAC_SNK=y +CONFIG_BT_PAC_SNK_NOTIFIABLE=y +CONFIG_BT_PAC_SRC=y +CONFIG_BT_PAC_SRC_NOTIFIABLE=y +CONFIG_BT_CSIP_SET_MEMBER=y +CONFIG_BT_DEVICE_APPEARANCE=2369 + +CONFIG_BT_ISO_MAX_CHAN=2 +CONFIG_BT_ASCS=y +CONFIG_BT_ASCS_MAX_ASE_SNK_COUNT=1 +CONFIG_BT_ASCS_MAX_ASE_SRC_COUNT=1 +CONFIG_BT_VCP_VOL_REND=y +CONFIG_BT_MCC=y +CONFIG_BT_MCC_READ_MEDIA_STATE=y +CONFIG_BT_MCC_SET_MEDIA_CONTROL_POINT=y +CONFIG_BT_GAP_PERIPHERAL_PREF_PARAMS=n +CONFIG_BT_AUDIO_CODEC_CFG_MAX_METADATA_SIZE=25 + +CONFIG_LC3_ENC_CHAN_MAX=1 +CONFIG_LC3_DEC_CHAN_MAX=1 +CONFIG_MBEDTLS_ENABLE_HEAP=y +CONFIG_MBEDTLS_HEAP_SIZE=2048