diff --git a/applications/nrf5340_audio/Kconfig.defaults b/applications/nrf5340_audio/Kconfig.defaults index 0d0964e9b8a..265345adba1 100644 --- a/applications/nrf5340_audio/Kconfig.defaults +++ b/applications/nrf5340_audio/Kconfig.defaults @@ -51,7 +51,7 @@ config ZBUS default y config ZBUS_RUNTIME_OBSERVERS_POOL_SIZE - default 6 + default 7 config SENSOR default y diff --git a/applications/nrf5340_audio/sample.yaml b/applications/nrf5340_audio/sample.yaml index 1feb91ad6f3..9d97bbbc914 100644 --- a/applications/nrf5340_audio/sample.yaml +++ b/applications/nrf5340_audio/sample.yaml @@ -41,3 +41,21 @@ tests: platform_exclude: nrf5340_audio_dk_nrf5340_cpuapp_ns tags: ci_build extra_args: CONF_FILE="prj_release.conf" CONFIG_AUDIO_DEV=2 CONFIG_AUDIO_DFU=2 + applications.nrf5340_audio.hs_display: + build_only: true + platform_allow: nrf5340_audio_dk_nrf5340_cpuapp + platform_exclude: nrf5340_audio_dk_nrf5340_cpuapp_ns + tags: ci_build + extra_args: CONF_FILE="prj_release.conf" + CONFIG_AUDIO_DEV=1 + SHIELD="nrf5340_audio_display" + CONFIG_NRF5340_AUDIO_DK_DISPLAY=y + applications.nrf5340_audio.gw_display: + build_only: true + platform_allow: nrf5340_audio_dk_nrf5340_cpuapp + platform_exclude: nrf5340_audio_dk_nrf5340_cpuapp_ns + tags: ci_build + extra_args: CONF_FILE="prj_release.conf" + CONFIG_AUDIO_DEV=2 + SHIELD="nrf5340_audio_display" + CONFIG_NRF5340_AUDIO_DK_DISPLAY=y diff --git a/applications/nrf5340_audio/src/audio/audio_datapath.c b/applications/nrf5340_audio/src/audio/audio_datapath.c index 566076e6469..51d04d76784 100644 --- a/applications/nrf5340_audio/src/audio/audio_datapath.c +++ b/applications/nrf5340_audio/src/audio/audio_datapath.c @@ -114,6 +114,7 @@ static struct { bool datapath_initialized; bool stream_started; void *decoded_data; + size_t decoded_size; struct { struct data_fifo *fifo; @@ -861,6 +862,7 @@ void audio_datapath_stream_out(const uint8_t *buf, size_t size, uint32_t sdu_ref /* Discard frame */ return; } + ctrl_blk.decoded_size = pcm_size; /*** Add audio data to FIFO buffer ***/ @@ -947,6 +949,12 @@ int audio_datapath_init(void) return 0; } +void audio_datapath_buffer_ptr_get(void **p_buf, size_t **p_size) +{ + *p_buf = ctrl_blk.decoded_data; + *p_size = &ctrl_blk.decoded_size; +} + static int cmd_i2s_tone_play(const struct shell *shell, size_t argc, const char **argv) { int ret; diff --git a/applications/nrf5340_audio/src/audio/audio_datapath.h b/applications/nrf5340_audio/src/audio/audio_datapath.h index cde32933aea..61a8937ac25 100644 --- a/applications/nrf5340_audio/src/audio/audio_datapath.h +++ b/applications/nrf5340_audio/src/audio/audio_datapath.h @@ -98,4 +98,17 @@ int audio_datapath_stop(void); */ int audio_datapath_init(void); +/** + * @brief Get pointers to the decoded audio data frame + * + * @note The decoded audio data frame lives statically in this module, and can be accessed at + * any time after the module has been initialized. The buffer can be updated by other the + * decoder thread at any time, and hence it is never guaranteed that the buffer data is not + * updated while the buffer is being accessed. + * + * @param p_buf Pointer to variable where the audio data buffer pointer will be stored + * @param p_size Pointer to variable where the size of the audio data buffer will be stored + */ +void audio_datapath_buffer_ptr_get(void **p_buf, size_t **p_size); + #endif /* _AUDIO_DATAPATH_H_ */ diff --git a/applications/nrf5340_audio/src/modules/CMakeLists.txt b/applications/nrf5340_audio/src/modules/CMakeLists.txt index 5568096d772..3d92b69472a 100644 --- a/applications/nrf5340_audio/src/modules/CMakeLists.txt +++ b/applications/nrf5340_audio/src/modules/CMakeLists.txt @@ -14,3 +14,5 @@ target_sources(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/power_meas.c ${CMAKE_CURRENT_SOURCE_DIR}/sd_card.c ) + +add_subdirectory(display) diff --git a/applications/nrf5340_audio/src/modules/Kconfig b/applications/nrf5340_audio/src/modules/Kconfig index fe7522f4811..92225a6816c 100644 --- a/applications/nrf5340_audio/src/modules/Kconfig +++ b/applications/nrf5340_audio/src/modules/Kconfig @@ -5,6 +5,7 @@ # rsource "Kconfig.defaults" +rsource "display/Kconfig" menu "Modules" diff --git a/applications/nrf5340_audio/src/modules/display/CMakeLists.txt b/applications/nrf5340_audio/src/modules/display/CMakeLists.txt new file mode 100644 index 00000000000..c03c70d75e9 --- /dev/null +++ b/applications/nrf5340_audio/src/modules/display/CMakeLists.txt @@ -0,0 +1,15 @@ +# +# Copyright (c) 2023 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +if (CONFIG_NRF5340_AUDIO_DK_DISPLAY) + target_sources(app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/nrf5340_audio_dk_display.c + ${CMAKE_CURRENT_SOURCE_DIR}/tab_menu.c + ) + if (CONFIG_NRF5340_AUDIO_DK_DISPLAY_TAB_LOG) + target_sources(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tab_log.c) + endif() +endif() diff --git a/applications/nrf5340_audio/src/modules/display/Kconfig b/applications/nrf5340_audio/src/modules/display/Kconfig new file mode 100644 index 00000000000..cc1b30171b7 --- /dev/null +++ b/applications/nrf5340_audio/src/modules/display/Kconfig @@ -0,0 +1,45 @@ +# +# Copyright (c) 2023 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +menuconfig NRF5340_AUDIO_DK_DISPLAY + bool "Display for nRF5340 Audio DK [EXPERIMENTAL]" + select EXPERIMENTAL + default n + +if NRF5340_AUDIO_DK_DISPLAY + +rsource "Kconfig.defaults" + +module = NRF5340_AUDIO_DK_DISPLAY +module-str = nrf5340_audio_dk_display +source "subsys/logging/Kconfig.template.log_config" + +config NRF5340_AUDIO_DK_DISPLAY_TAB_LOG + bool "Enable the log tab on the display" + depends on LOG + default y + +if NRF5340_AUDIO_DK_DISPLAY_TAB_LOG + +config NRF5340_AUDIO_DK_DISPLAY_TAB_LOG_LEVEL + int "Log level for message displayed on screen" + default 2 + +config NRF5340_AUDIO_DK_DISPLAY_TAB_LOG_TIMESTAMP + bool "Display timestamp in us in front of log message" + default n + +config NRF5340_AUDIO_DK_DISPLAY_TAB_LOG_MAX_LEN + int "Number of messages to be stored" + default 10 + +endif # NRF5340_AUDIO_DK_DISPLAY_TAB_LOG + +config NRF5340_AUDIO_DK_DISPLAY_TAB_MENU_UPDATE_INTERVAL_MS + int "Update interval for the menu tab, in milliseconds" + default 500 + +endif # NRF5340_AUDIO_DK_DISPLAY diff --git a/applications/nrf5340_audio/src/modules/display/Kconfig.defaults b/applications/nrf5340_audio/src/modules/display/Kconfig.defaults new file mode 100644 index 00000000000..a8b4e25b995 --- /dev/null +++ b/applications/nrf5340_audio/src/modules/display/Kconfig.defaults @@ -0,0 +1,28 @@ +# +# Copyright (c) 2023 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +config DISPLAY + default y + +config LVGL + default y + +config LV_Z_MEM_POOL_NUMBER_BLOCKS + int "Number of max size blocks in memory pool" + default 8 + +config LV_FONT_MONTSERRAT_14 + default y + +config LV_FONT_MONTSERRAT_48 + default y + +config LV_FONT_MONTSERRAT_20 + default y + +config LV_Z_VDB_SIZE + int "Virtual display buffer size" + default 32 diff --git a/applications/nrf5340_audio/src/modules/display/nrf5340_audio_dk_display.c b/applications/nrf5340_audio/src/modules/display/nrf5340_audio_dk_display.c new file mode 100644 index 00000000000..ed4ac1fcb5a --- /dev/null +++ b/applications/nrf5340_audio/src/modules/display/nrf5340_audio_dk_display.c @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "tab_menu.h" +#include "tab_log.h" +#include "macros_common.h" + +#define DISPLAY_THREAD_PRIORITY 5 +/* Must have lower priority than LVGL init */ +#define DISPLAY_INIT_PRIORITY 91 +#define DISPLAY_THREAD_STACK_SIZE 1800 + +LOG_MODULE_REGISTER(display_log, CONFIG_DISPLAY_LOG_LEVEL); + +static struct k_thread display_data; +static k_tid_t display_thread; + +ZBUS_CHAN_DECLARE(button_chan); +ZBUS_CHAN_DECLARE(le_audio_chan); + +ZBUS_OBS_DECLARE(le_audio_evt_sub_display); + +K_THREAD_STACK_DEFINE(display_thread_STACK, DISPLAY_THREAD_STACK_SIZE); + +static void nrf5340_audio_dk_display_update_thread(void *arg1, void *arg2, void *arg3) +{ + uint64_t prev_tab_update = k_uptime_get(); + + while (1) { + if ((k_uptime_get() - prev_tab_update) > + CONFIG_NRF5340_AUDIO_DK_DISPLAY_TAB_MENU_UPDATE_INTERVAL_MS) { + tab_menu_update(); + prev_tab_update = k_uptime_get(); + } + + uint32_t time_till_next = lv_timer_handler(); + + k_sleep(K_MSEC(time_till_next)); + } +} + +static int nrf5340_audio_dk_display_init(void) +{ + int ret; + const struct device *display_dev; + lv_obj_t *tabview; + lv_obj_t *tab_menu; +#ifdef CONFIG_NRF5340_AUDIO_DK_DISPLAY_TAB_LOG + lv_obj_t *tab_log; +#endif /* CONFIG_NRF5340_AUDIO_DK_DISPLAY_TAB_LOG */ + + display_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_display)); + if (!device_is_ready(display_dev)) { + LOG_ERR("Display not ready"); + return -ENODEV; + } + + ret = zbus_chan_add_obs(&le_audio_chan, &le_audio_evt_sub_display, K_MSEC(200)); + if (ret) { + LOG_ERR("Failed to add ZBus observer (%d)", ret); + return ret; + } + + ret = display_blanking_off(display_dev); + if (ret) { + LOG_ERR("Failed to disable display blanking (%d)", ret); + return ret; + } + + tabview = lv_tabview_create(lv_scr_act(), LV_DIR_TOP, 50); + + tab_menu = lv_tabview_add_tab(tabview, "Menu"); + tab_menu_create(tab_menu); + +#ifdef CONFIG_NRF5340_AUDIO_DK_DISPLAY_TAB_LOG + tab_log = lv_tabview_add_tab(tabview, "Logging"); + tab_log_create(tab_log); +#endif /* CONFIG_NRF5340_AUDIO_DK_DISPLAY_TAB_LOG */ + + lv_obj_clear_flag(lv_tabview_get_content(tabview), LV_OBJ_FLAG_SCROLLABLE); + + display_thread = k_thread_create(&display_data, display_thread_STACK, + K_THREAD_STACK_SIZEOF(display_thread_STACK), + nrf5340_audio_dk_display_update_thread, NULL, NULL, NULL, + K_PRIO_PREEMPT(DISPLAY_THREAD_PRIORITY), 0, K_NO_WAIT); + k_thread_name_set(display_thread, "Nrf5340_audio__dk display thread"); + + return 0; +} + +SYS_INIT(nrf5340_audio_dk_display_init, APPLICATION, DISPLAY_INIT_PRIORITY); diff --git a/applications/nrf5340_audio/src/modules/display/tab_log.c b/applications/nrf5340_audio/src/modules/display/tab_log.c new file mode 100644 index 00000000000..0a6127cf6d0 --- /dev/null +++ b/applications/nrf5340_audio/src/modules/display/tab_log.c @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include "tab_log.h" + +#define LOG_MSG_MAX_LENGTH 200 +#define LOG_MSG_N_TIMESTAMP_MAX_LENGTH 220 + +LOG_MODULE_DECLARE(display_log, CONFIG_DISPLAY_LOG_LEVEL); + +/* This is used so there are no conflicts between scheduler and LVGL */ +K_MUTEX_DEFINE(nrf5340_display_mtx); + +struct backend_context { + uint32_t cnt; + const char *exp_str[10]; + uint32_t delay_ms; + bool active; + struct k_timer timer; +}; +static struct backend_context display_log_context; + +static lv_obj_t *tab_log; +static int log_list_length; +static bool stop_logging; + +/** + * @brief putc implementation for logging backend process + * + * @details See documentation for cbprint_cb function pointer in zephyr\sys\cbprintf.h + */ +static int cbprintf_callback(int c, void *ctx) +{ + char **p = ctx; + + **p = c; + (*p)++; + return c; +} + +static void log_panic_handler(const struct log_backend *const backend) +{ + ARG_UNUSED(backend); +} + +static void expire_cb(struct k_timer *timer) +{ + void *ctx = k_timer_user_data_get(timer); + struct backend_context *context = (struct backend_context *)ctx; + + context->active = true; +} + +static void backend_init(const struct log_backend *const backend) +{ + struct backend_context *context = (struct backend_context *)backend->cb->ctx; + + k_timer_init(&context->timer, expire_cb, NULL); + k_timer_user_data_set(&context->timer, (void *)context); + k_timer_start(&context->timer, K_MSEC(context->delay_ms), K_NO_WAIT); +} + +static int backend_is_ready(const struct log_backend *const backend) +{ + struct backend_context *context = (struct backend_context *)backend->cb->ctx; + + return context->active ? 0 : -EBUSY; +} + +static void add_log_msg_to_log_list(lv_obj_t *tab_log, char *log_msg, lv_color_t log_msg_color) +{ + lv_obj_t *label = lv_label_create(tab_log); + + lv_label_set_long_mode(label, LV_LABEL_LONG_WRAP); + lv_obj_set_width(label, 290); + lv_label_set_text(label, log_msg); + lv_obj_set_style_text_color(label, log_msg_color, 0); + lv_obj_scroll_to_view_recursive(label, LV_ANIM_OFF); +} + +static void backend_process(const struct log_backend *const backend, union log_msg_generic *msg) +{ + log_timestamp_t msg_timestamp; + int msg_timestamp_in_us; + char str[LOG_MSG_MAX_LENGTH]; + char timestamp_n_str[LOG_MSG_N_TIMESTAMP_MAX_LENGTH]; + char *pstr = str; + + size_t len; + + uint8_t *p = log_msg_get_package(&msg->log, &len); + + int slen = cbpprintf(cbprintf_callback, &pstr, p); + + str[slen] = '\0'; + + if (IS_ENABLED(CONFIG_NRF5340_AUDIO_DK_DISPLAY_TAB_LOG_TIMESTAMP)) { + msg_timestamp = log_msg_get_timestamp(&msg->log); + msg_timestamp_in_us = log_output_timestamp_to_us(msg_timestamp); + snprintf(timestamp_n_str, sizeof(timestamp_n_str), "[%d] %s", msg_timestamp_in_us, + str); + strcpy(str, timestamp_n_str); + } + + k_mutex_lock(&nrf5340_display_mtx, K_FOREVER); + + switch (log_msg_get_level(&msg->log)) { + case LOG_LEVEL_ERR: + if (CONFIG_NRF5340_AUDIO_DK_DISPLAY_TAB_LOG_LEVEL >= LOG_LEVEL_ERR) { + log_list_length++; + add_log_msg_to_log_list(tab_log, str, + lv_palette_lighten(LV_PALETTE_RED, 1)); + } + + break; + + case LOG_LEVEL_WRN: + if (CONFIG_NRF5340_AUDIO_DK_DISPLAY_TAB_LOG_LEVEL >= LOG_LEVEL_WRN) { + log_list_length++; + add_log_msg_to_log_list(tab_log, str, + lv_palette_lighten(LV_PALETTE_YELLOW, 1)); + } + + break; + + case LOG_LEVEL_INF: + if (CONFIG_NRF5340_AUDIO_DK_DISPLAY_TAB_LOG_LEVEL >= LOG_LEVEL_INF) { + log_list_length++; + add_log_msg_to_log_list(tab_log, str, + lv_palette_lighten(LV_PALETTE_GREY, 5)); + } + + break; + + case LOG_LEVEL_DBG: + if (CONFIG_NRF5340_AUDIO_DK_DISPLAY_TAB_LOG_LEVEL >= LOG_LEVEL_DBG) { + log_list_length++; + add_log_msg_to_log_list(tab_log, str, + lv_palette_lighten(LV_PALETTE_GREY, 5)); + } + + break; + + default: + break; + } + + if (log_list_length >= CONFIG_NRF5340_AUDIO_DK_DISPLAY_TAB_LOG_MAX_LEN) { + lv_obj_t *oldest_label = lv_obj_get_child(tab_log, 0); + + lv_group_remove_obj(oldest_label); + lv_obj_del(oldest_label); + log_list_length--; + } + + k_mutex_unlock(&nrf5340_display_mtx); +} + +static const struct log_backend_api backend_api = {.process = backend_process, + .init = backend_init, + .is_ready = backend_is_ready, + .panic = log_panic_handler}; + +LOG_BACKEND_DEFINE(display_log_backend, backend_api, false, &display_log_context); + +static void tab_log_long_press_cb(lv_event_t *e) +{ + if (stop_logging) { + log_backend_activate(&display_log_backend, &display_log_context); + add_log_msg_to_log_list(tab_log, "Logging backend activated", + lv_palette_lighten(LV_PALETTE_YELLOW, 5)); + stop_logging = false; + } else if (!stop_logging) { + add_log_msg_to_log_list(tab_log, "Logging backend deactivated", + lv_palette_lighten(LV_PALETTE_YELLOW, 5)); + log_backend_deactivate(&display_log_backend); + stop_logging = true; + } +} + +void tab_log_create(lv_obj_t *screen) +{ + stop_logging = false; + tab_log = screen; + lv_obj_set_flex_flow(screen, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_bg_color(screen, lv_palette_darken(LV_PALETTE_GREY, 3), 0); + lv_obj_set_style_bg_opa(screen, LV_OPA_COVER, 0); + + lv_obj_add_event_cb(screen, tab_log_long_press_cb, LV_EVENT_LONG_PRESSED, NULL); + display_log_context.delay_ms = 0; + + log_backend_activate(&display_log_backend, &display_log_context); +} diff --git a/applications/nrf5340_audio/src/modules/display/tab_log.h b/applications/nrf5340_audio/src/modules/display/tab_log.h new file mode 100644 index 00000000000..c362fcc7c86 --- /dev/null +++ b/applications/nrf5340_audio/src/modules/display/tab_log.h @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _TAB_LOG_H_ +#define _TAG_LOG_H_ + +/** + * @brief Create all objects on the log tab + * + * @param screen Parent object of the log tab + */ +void tab_log_create(lv_obj_t *screen); + +#endif /* _TAB_LOG_H_ */ diff --git a/applications/nrf5340_audio/src/modules/display/tab_menu.c b/applications/nrf5340_audio/src/modules/display/tab_menu.c new file mode 100644 index 00000000000..4d2776d635a --- /dev/null +++ b/applications/nrf5340_audio/src/modules/display/tab_menu.c @@ -0,0 +1,409 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include "tab_menu.h" +#include +#include +#include +#include "macros_common.h" +#include +#include +#include +#include +#include "sw_codec_select.h" +#include "audio_datapath.h" + +static lv_obj_t *device_info_label; +static lv_obj_t *device_mode_label; +static lv_obj_t *volume_level_label; +static lv_obj_t *play_pause_label; +static lv_obj_t *vu_bar; + +static lv_style_t style_btn_gray; +static lv_style_t style_btn_red; + +static enum audio_channel channel; + +LOG_MODULE_DECLARE(display_log, CONFIG_DISPLAY_LOG_LEVEL); + +ZBUS_CHAN_DECLARE(button_chan); +ZBUS_SUBSCRIBER_DEFINE(le_audio_evt_sub_display, CONFIG_LE_AUDIO_MSG_SUB_QUEUE_SIZE); + +static void *decoded_data; +static size_t *decoded_size; + +static void style_init(lv_color_t style_color, lv_style_t *style_btn) +{ + lv_style_init(style_btn); + lv_style_set_bg_color(style_btn, style_color); + lv_style_set_bg_grad_color(style_btn, style_color); +} + +static lv_obj_t *button_create(lv_obj_t *screen, char *label_text, lv_align_t button_position, + void (*button_callback_function)(lv_event_t *e), + const lv_font_t *font_size) +{ + lv_obj_t *button = lv_btn_create(screen); + + lv_obj_set_size(button, 47, 47); + lv_obj_set_style_radius(button, LV_RADIUS_CIRCLE, 0); + lv_obj_align(button, button_position, 0, 0); + + lv_obj_t *label = lv_label_create(button); + + lv_label_set_text(label, label_text); + lv_obj_set_style_text_font(label, font_size, LV_STATE_DEFAULT); + lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); + lv_obj_add_event_cb(button, button_callback_function, LV_EVENT_CLICKED, NULL); + + lv_obj_add_style(button, &style_btn_red, LV_STATE_PRESSED); + + return label; +} + +static void play_pause_button_event_cb(lv_event_t *e) +{ + int ret; + struct button_msg msg; + + msg.button_action = BUTTON_PRESS; + msg.button_pin = BUTTON_PLAY_PAUSE; + + ret = zbus_chan_pub(&button_chan, &msg, K_NO_WAIT); + if (ret) { + LOG_ERR("Failed to publish button msg, ret: %d", ret); + } +} + +static void volume_up_button_event_cb(lv_event_t *e) +{ + int ret; + struct button_msg msg; + + msg.button_action = BUTTON_PRESS; + msg.button_pin = BUTTON_VOLUME_UP; + + ret = zbus_chan_pub(&button_chan, &msg, K_NO_WAIT); + if (ret) { + LOG_ERR("Failed to publish button msg, ret: %d", ret); + } +} + +static void volume_down_button_event_cb(lv_event_t *e) + +{ + int ret; + struct button_msg msg; + + msg.button_action = BUTTON_PRESS; + msg.button_pin = BUTTON_VOLUME_DOWN; + + ret = zbus_chan_pub(&button_chan, &msg, K_NO_WAIT); + if (ret) { + LOG_ERR("Failed to publish button msg, ret: %d", ret); + } +} + +static void btn4_button_event_cb(lv_event_t *e) +{ + int ret; + struct button_msg msg; + + msg.button_action = BUTTON_PRESS; + msg.button_pin = BUTTON_4; + + ret = zbus_chan_pub(&button_chan, &msg, K_NO_WAIT); + if (ret) { + LOG_ERR("Failed to publish button msg, ret: %d", ret); + } +} + +static void btn5_button_event_cb(lv_event_t *e) +{ + int ret; + struct button_msg msg; + + msg.button_action = BUTTON_PRESS; + msg.button_pin = BUTTON_5; + + ret = zbus_chan_pub(&button_chan, &msg, K_NO_WAIT); + if (ret) { + LOG_ERR("Failed to publish button msg, ret: %d", ret); + } +} + +static void vu_bar_update(int vu_val) +{ + lv_bar_set_value(vu_bar, vu_val, LV_ANIM_ON); +} + +static int streaming_state_update(uint8_t *streaming_state) +{ + int ret; + const struct zbus_channel *chan; + struct le_audio_msg msg; + static uint8_t audio_streaming_event; + + ret = zbus_sub_wait(&le_audio_evt_sub_display, &chan, K_NO_WAIT); + if (ret == -ENOMSG) { + *streaming_state = audio_streaming_event; + return 0; + } else if (ret != 0) { + LOG_ERR("zbus_sub_wait: %d", ret); + return ret; + } + + ret = zbus_chan_read(chan, &msg, K_NO_WAIT); + if (ret) { + LOG_ERR("zbus_chan_read: %d", ret); + return ret; + } + + audio_streaming_event = msg.event; + *streaming_state = audio_streaming_event; + + return 0; +} + +static void volume_label_update(void) +{ + uint32_t volume_level_int; + int volume_level; + /* The label will always be two digits, followed by an % and newline, giving length 4*/ + const size_t volume_label_length = 4; + char volume_level_str[volume_label_length]; + + volume_level_int = hw_codec_volume_get(); + volume_level = floor((float)volume_level_int * 100.0f / 128.0f); + snprintf(volume_level_str, volume_label_length, "%2d%%", volume_level); + lv_label_set_text(volume_level_label, volume_level_str); +} + +/** + * @brief Calculate average frame value for decoded audio buffer, in percent value + * + * @details Calculates the average of every frame in the decoded audio buffer kept in + * audio_datapath.c. The data buffer is statically kept in audio_datapath, so it is + * always valid when the device is streaming. NOTE! As the audio system is run in a thread + * with higher priority than the display thread there is a risk that the buffer is updated + * while calculation is ongoing. + * + * @retval int Frame value in percentage of possible max value + */ +static int calculate_vu_percentage(void) +{ + int frame_value_total = 0; + int8_t bytes_per_sample = CONFIG_AUDIO_BIT_DEPTH_BITS / 8; + + char *pointer_input = (char *)decoded_data; + + /* Iterate through each sample */ + for (int i = 0; i < (*decoded_size / (bytes_per_sample)); i += 2) { + if (channel == AUDIO_CH_L) { + if (bytes_per_sample == 2) { + frame_value_total += abs(*((int16_t *)pointer_input)); + } else if (bytes_per_sample == 4) { + frame_value_total += abs(*((int32_t *)pointer_input)); + } + + pointer_input += 2 * bytes_per_sample; + } else if (channel == AUDIO_CH_R) { + /* Progress pointer first to read the right channel */ + pointer_input += 2 * bytes_per_sample; + + if (bytes_per_sample == 2) { + frame_value_total += abs(*((int16_t *)pointer_input)); + } else if (bytes_per_sample == 4) { + frame_value_total += abs(*((int32_t *)pointer_input)); + } + } else { + __ASSERT(false, "Unknown audio channel (%d)", channel); + } + } + + /* Calculate the max numeric value for a signed integer of bytes_per_sample number of + * bytes + */ + int frame_max_value = (((1 << ((8 * bytes_per_sample) - 2)) - 1) * 2) + 1; + + int frame_avg_val = frame_value_total / (int)(*decoded_size / 4); + + return round((double)frame_avg_val / (double)frame_max_value * 100); +} + +static void headset_device_info_label_update(void) +{ + char *device_info; + + if (channel == AUDIO_CH_L) { + device_info = "Headset Left"; + } else if (channel == AUDIO_CH_R) { + device_info = "Headset Right"; + } else { + device_info = ""; + __ASSERT(false, "Unknown audio channel (%d)", channel); + } + + lv_label_set_text(device_info_label, device_info); + lv_obj_align_to(device_info_label, device_mode_label, LV_ALIGN_OUT_TOP_MID, 0, 0); +} + +void tab_menu_update(void) +{ + int ret; + uint8_t streaming_state; + + ret = streaming_state_update(&streaming_state); + if (ret) { + LOG_ERR("Failed to update streaming state"); + return; + } + + switch (streaming_state) { + case LE_AUDIO_EVT_STREAMING: + lv_label_set_text(play_pause_label, LV_SYMBOL_PAUSE); + break; + + case LE_AUDIO_EVT_NOT_STREAMING: + lv_label_set_text(play_pause_label, LV_SYMBOL_PLAY); + break; + default: + break; + } + + if (CONFIG_AUDIO_DEV == HEADSET) { + enum audio_channel temp_channel; + + channel_assignment_get(&temp_channel); + if (temp_channel != channel) { + channel = temp_channel; + headset_device_info_label_update(); + } + + volume_label_update(); + + if (streaming_state == LE_AUDIO_EVT_STREAMING) { + audio_datapath_buffer_ptr_get(&decoded_data, &decoded_size); + vu_bar_update(calculate_vu_percentage()); + } else { + vu_bar_update(0); + } + } +} + +static void volume_label_create(lv_obj_t *screen) +{ + + volume_level_label = lv_label_create(screen); + + lv_label_set_text(volume_level_label, ""); + lv_obj_align(volume_level_label, LV_ALIGN_RIGHT_MID, 0, 0); +} + +static void device_info_label_create(lv_obj_t *screen) +{ + char *mode; + + device_mode_label = lv_label_create(screen); + device_info_label = lv_label_create(screen); + + if (IS_ENABLED(CONFIG_TRANSPORT_CIS)) { + mode = "CIS"; + } else { + mode = "BIS"; + } + + lv_label_set_text(device_mode_label, mode); + lv_obj_align(device_mode_label, LV_ALIGN_BOTTOM_MID, 0, 0); + + if (CONFIG_AUDIO_DEV == GATEWAY) { + lv_label_set_text(device_info_label, "Gateway"); + lv_obj_align_to(device_info_label, device_mode_label, LV_ALIGN_OUT_TOP_MID, 0, 0); + } else { + headset_device_info_label_update(); + } +} + +static void vu_bar_create(lv_obj_t *screen) +{ + static lv_style_t style_indic; + + lv_style_init(&style_indic); + lv_style_set_bg_opa(&style_indic, LV_OPA_COVER); + lv_style_set_bg_color(&style_indic, lv_palette_main(LV_PALETTE_BLUE)); + lv_style_set_bg_grad_color(&style_indic, lv_palette_main(LV_PALETTE_RED)); + lv_style_set_bg_grad_dir(&style_indic, LV_GRAD_DIR_HOR); + + vu_bar = lv_bar_create(screen); + lv_obj_add_style(vu_bar, &style_indic, LV_PART_INDICATOR); + lv_obj_set_size(vu_bar, 150, 20); + lv_obj_center(vu_bar); +} + +static void vu_bar_label_create(lv_obj_t *screen) +{ + lv_obj_t *vu_bar_label = lv_label_create(screen); + + lv_label_set_text(vu_bar_label, "VU-bar"); + lv_obj_align(vu_bar_label, LV_ALIGN_TOP_MID, 0, 20); +} + +void tab_menu_create(lv_obj_t *screen) +{ + style_init(lv_palette_main(LV_PALETTE_RED), &style_btn_red); + style_init(lv_palette_main(LV_PALETTE_GREY), &style_btn_gray); + + if (CONFIG_AUDIO_DEV == HEADSET) { + volume_label_create(screen); + vu_bar_create(screen); + vu_bar_label_create(screen); + + lv_obj_t *change_stream_label = + button_create(screen, LV_SYMBOL_BLUETOOTH, LV_ALIGN_TOP_LEFT, + btn4_button_event_cb, &lv_font_montserrat_20); + + if (IS_ENABLED(CONFIG_TRANSPORT_CIS) == 1) { + lv_obj_remove_event_cb(lv_obj_get_parent(change_stream_label), + btn4_button_event_cb); + lv_obj_add_style(lv_obj_get_parent(change_stream_label), &style_btn_gray, + LV_STATE_DEFAULT); + lv_obj_add_style(lv_obj_get_parent(change_stream_label), &style_btn_gray, + LV_STATE_PRESSED); + } + } + + if (IS_ENABLED(CONFIG_AUDIO_TEST_TONE) && (CONFIG_AUDIO_DEV == GATEWAY)) { + button_create(screen, "~", LV_ALIGN_TOP_LEFT, btn4_button_event_cb, + &lv_font_montserrat_48); + } + if (IS_ENABLED(CONFIG_AUDIO_MUTE)) { + button_create(screen, LV_SYMBOL_MUTE, LV_ALIGN_BOTTOM_LEFT, btn5_button_event_cb, + &lv_font_montserrat_20); + } + + play_pause_label = button_create(screen, LV_SYMBOL_PLAY, LV_ALIGN_LEFT_MID, + play_pause_button_event_cb, &lv_font_montserrat_14); + button_create(screen, LV_SYMBOL_VOLUME_MAX, LV_ALIGN_TOP_RIGHT, volume_up_button_event_cb, + &lv_font_montserrat_20); + button_create(screen, LV_SYMBOL_VOLUME_MID, LV_ALIGN_BOTTOM_RIGHT, + volume_down_button_event_cb, &lv_font_montserrat_20); + device_info_label_create(screen); +} diff --git a/applications/nrf5340_audio/src/modules/display/tab_menu.h b/applications/nrf5340_audio/src/modules/display/tab_menu.h new file mode 100644 index 00000000000..46e0062d377 --- /dev/null +++ b/applications/nrf5340_audio/src/modules/display/tab_menu.h @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _TAB_MENU_H_ +#define _TAB_MENU_H_ + +/** + * @brief Create all objects on the button tab + * + * @param current_screen Parent object of the button tab + */ +void tab_menu_create(lv_obj_t *screen); + +/** + * @brief Update all objects on the button tab + */ +void tab_menu_update(void); + +#endif /* _TAB_MENU_H_ */ diff --git a/applications/nrf5340_audio/src/modules/hw_codec.c b/applications/nrf5340_audio/src/modules/hw_codec.c index 2d4781fb9b7..12c60235320 100644 --- a/applications/nrf5340_audio/src/modules/hw_codec.c +++ b/applications/nrf5340_audio/src/modules/hw_codec.c @@ -123,6 +123,7 @@ static void volume_msg_sub_thread(void) /** * @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; @@ -175,6 +176,11 @@ int hw_codec_volume_set(uint8_t set_val) return 0; } +uint32_t hw_codec_volume_get(void) +{ + return prev_volume_reg_val; +} + int hw_codec_volume_adjust(int8_t adjustment_db) { int ret; diff --git a/applications/nrf5340_audio/src/modules/hw_codec.h b/applications/nrf5340_audio/src/modules/hw_codec.h index 0c9ac985e9b..f99f16ef541 100644 --- a/applications/nrf5340_audio/src/modules/hw_codec.h +++ b/applications/nrf5340_audio/src/modules/hw_codec.h @@ -21,6 +21,15 @@ */ int hw_codec_volume_set(uint8_t set_val); +/** + * @brief Get volume on HW_CODEC + * + * @details Range 0-128 + * + * @return Previous volume level registered + */ +uint32_t hw_codec_volume_get(void); + /** * @brief Adjust volume on HW_CODEC * diff --git a/applications/nrf5340_audio/tools/buildprog/buildprog.py b/applications/nrf5340_audio/tools/buildprog/buildprog.py index bc1512f212b..6a1103bc9e3 100644 --- a/applications/nrf5340_audio/tools/buildprog/buildprog.py +++ b/applications/nrf5340_audio/tools/buildprog/buildprog.py @@ -109,6 +109,10 @@ def __build_cmd_get(core: Core, device: AudioDevice, build: BuildType, pristine, if options.nrf21540: device_flag += " -DSHIELD=nrf21540ek_fwd" + if options.display: + device_flag += " -DSHIELD=nrf5340_audio_display" + device_flag += " -DCONFIG_NRF5340_AUDIO_DK_DISPLAY=y" + if os.name == 'nt': release_flag = release_flag.replace('\\', '/') @@ -292,14 +296,22 @@ def __main(): choices=["external", "internal"], default='', help="MCUBOOT with external, internal flash", - ) + ) parser.add_argument( "--nrf21540", action="store_true", dest="nrf21540", default=False, help="Set when using nRF21540 for extra TX power", - ) + ) + parser.add_argument( + "--display", + action="store_true", + dest="display", + default=False, + help="Set when using adafruit display for the audio application", + ) + options = parser.parse_args(args=sys.argv[1:]) # Post processing for Enums @@ -352,7 +364,8 @@ def __main(): # Reboot step start if options.only_reboot == SelectFlags.TBD: - program_threads_run(device_list, options.mcuboot, sequential=options.sequential_prog) + program_threads_run(device_list, options.mcuboot, + sequential=options.sequential_prog) __finish(device_list) # Reboot step finished @@ -393,7 +406,8 @@ def __main(): for dev in device_list: if dev.snr_connected: __populate_hex_paths(dev, options) - program_threads_run(device_list, options.mcuboot, sequential=options.sequential_prog) + program_threads_run(device_list, options.mcuboot, + sequential=options.sequential_prog) # Program step finished diff --git a/boards/shields/nrf5340_audio_display/Kconfig.defconfig b/boards/shields/nrf5340_audio_display/Kconfig.defconfig new file mode 100644 index 00000000000..f3160a07b30 --- /dev/null +++ b/boards/shields/nrf5340_audio_display/Kconfig.defconfig @@ -0,0 +1,60 @@ +# +# Copyright (c) 2023 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +if SHIELD_NRF5340_AUDIO_DISPLAY + +if DISPLAY + +if INPUT + +# NOTE: Enable if IRQ line is available (requires to solder jumper) +config INPUT_FT5336_INTERRUPT + default n + +endif # INPUT + +if LVGL + +config LV_Z_VDB_SIZE + default 64 + +config LV_Z_BITS_PER_PIXEL + default 24 + +choice LV_COLOR_DEPTH + default LV_COLOR_DEPTH_32 +endchoice + +config KSCAN + default y + +config INPUT + default y + +config LV_Z_POINTER_KSCAN + default y + +config LV_Z_POINTER_KSCAN_SWAP_XY + default y + +config LV_Z_POINTER_KSCAN_INVERT_X + default y + +config LV_Z_POINTER_KSCAN_INVERT_Y + default y + +endif # LVGL + +endif # DISPLAY + +if DISK_DRIVERS + +config DISK_DRIVER_SDMMC + default y + +endif # DISK_DRIVERS + +endif # SHIELD_NRF5340_AUDIO_DISPLAY diff --git a/boards/shields/nrf5340_audio_display/Kconfig.shield b/boards/shields/nrf5340_audio_display/Kconfig.shield new file mode 100644 index 00000000000..c48679d120d --- /dev/null +++ b/boards/shields/nrf5340_audio_display/Kconfig.shield @@ -0,0 +1,8 @@ +# +# Copyright (c) 2023 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +config SHIELD_NRF5340_AUDIO_DISPLAY + def_bool $(shields_list_contains,nrf5340_audio_display) diff --git a/boards/shields/nrf5340_audio_display/nrf5340_audio_display.overlay b/boards/shields/nrf5340_audio_display/nrf5340_audio_display.overlay new file mode 100644 index 00000000000..77fad11e756 --- /dev/null +++ b/boards/shields/nrf5340_audio_display/nrf5340_audio_display.overlay @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include + +/ { + chosen { + zephyr,display = &ili9340; + zephyr,keyboard-scan = &kscan_input; + }; +}; + +&spi4 { + status = "okay"; + cs-gpios = <&gpio0 11 GPIO_ACTIVE_LOW>, + <&gpio0 17 GPIO_ACTIVE_LOW>, + <&arduino_header 16 GPIO_ACTIVE_LOW>, + <&arduino_header 10 GPIO_ACTIVE_LOW>; + + + ili9340: ili9340@2 { + compatible = "ilitek,ili9340"; + spi-max-frequency = <15000000>; + reg = <2>; + cmd-data-gpios = <&arduino_header 15 GPIO_ACTIVE_LOW>; /* D9 */ + width = <320>; + height = <240>; + pixel-format = ; + rotation = <90>; + frmctr1 = [ 00 18 ]; + pwctrl1 = [ 23 00 ]; + vmctrl1 = [ 3e 28 ]; + vmctrl2 = [ 86 ]; + pgamctrl = [ 0f 31 2b 0c 0e 08 4e f1 37 07 10 03 0e 09 00 ]; + ngamctrl = [ 00 0e 14 03 11 07 31 c1 48 08 0f 0c 31 36 0f ]; + }; + +}; + +&i2c1 { + ft5336@38 { + compatible = "focaltech,ft5336"; + reg = <0x38>; + /* Uncomment if IRQ line is available (requires to solder jumper) */ + /* int-gpios = <&arduino_header 13 GPIO_ACTIVE_LOW>; */ /* D7 */ + + kscan_input: kscan-input { + compatible = "zephyr,kscan-input"; + }; + }; +};