diff --git a/prj.conf b/prj.conf index c5ad358..41ea7a1 100644 --- a/prj.conf +++ b/prj.conf @@ -30,7 +30,13 @@ CONFIG_ASSERT=y # USB HID next stack CONFIG_USB_DEVICE_STACK_NEXT=y +CONFIG_SERIAL=y +CONFIG_UART_INTERRUPT_DRIVEN=y +CONFIG_UART_LINE_CTRL=y +CONFIG_UART_USE_RUNTIME_CONFIGURE=y CONFIG_USBD_HID_SUPPORT=y +CONFIG_USBD_CDC_ACM_CLASS=y +CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT=n # BLE CONFIG_BT=y @@ -100,6 +106,7 @@ CONFIG_BT_ADV_PROV_DEVICE_NAME_SD=y CONFIG_LVGL=y CONFIG_LV_Z_AUTO_INIT=n CONFIG_LV_Z_RUN_LVGL_ON_WORKQUEUE=y +CONFIG_LV_Z_LVGL_WORKQUEUE_STACK_SIZE=16384 CONFIG_LV_Z_LVGL_MUTEX=y CONFIG_LV_COLOR_DEPTH_16=y CONFIG_LV_COLOR_16_SWAP=y @@ -109,4 +116,5 @@ CONFIG_LV_Z_DOUBLE_VDB=y CONFIG_LV_Z_MEM_POOL_SIZE=16384 CONFIG_LV_USE_LABEL=y CONFIG_LV_FONT_MONTSERRAT_14=y +CONFIG_LV_FONT_MONTSERRAT_32=y CONFIG_MAIN_STACK_SIZE=4096 diff --git a/src/display_module.c b/src/display_module.c index 7502b5f..e7b6985 100644 --- a/src/display_module.c +++ b/src/display_module.c @@ -13,6 +13,9 @@ #include #include +#include "bat_state_event.h" +#include "hid_led_event.h" +#include "mode_switch_event.h" #include "ui/ui_main.h" LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF); @@ -26,6 +29,11 @@ static const struct device *const display_dev = static const struct device *const backlight_dev = DEVICE_DT_GET(DT_PARENT(DT_ALIAS(backlight))); static const uint32_t backlight_idx = DT_NODE_CHILD_IDX(DT_ALIAS(backlight)); +static struct ui_main_model ui_model = { + .theme_color = LV_COLOR_MAKE(0x4C, 0x9E, 0xF5), + .inactive_border_color = LV_COLOR_MAKE(0x3A, 0x44, 0x52), + .mode = MODE_SWITCH_BLE, +}; static bool initialized; static bool running; static bool lvgl_initialized; @@ -82,7 +90,7 @@ static int module_start(void) lvgl_initialized = true; lvgl_lock(); - ui_main_init(); + ui_main_init(&ui_model, "WH Mini", "Hello World"); lvgl_unlock(); } @@ -117,8 +125,45 @@ static void module_pause(void) LOG_INF("LVGL display paused"); } +static void refresh_ui(void) +{ + if (!lvgl_initialized) { + return; + } + + lvgl_lock(); + ui_main_refresh_all(&ui_model, "WH Mini", "Hello World"); + lvgl_unlock(); +} + static bool app_event_handler(const struct app_event_header *aeh) { + if (is_bat_state_event(aeh)) { + const struct bat_state_event *event = cast_bat_state_event(aeh); + + ui_model.battery_level = event->soc; + ui_model.charging = event->charging; + ui_model.full = event->full; + refresh_ui(); + return false; + } + + if (is_mode_switch_event(aeh)) { + const struct mode_switch_event *event = cast_mode_switch_event(aeh); + + ui_model.mode = event->mode; + refresh_ui(); + return false; + } + + if (is_hid_led_event(aeh)) { + const struct hid_led_event *event = cast_hid_led_event(aeh); + + ui_model.led_mask = event->led_bm; + refresh_ui(); + return false; + } + if (is_module_state_event(aeh)) { const struct module_state_event *event = cast_module_state_event(aeh); int err; @@ -172,6 +217,9 @@ static bool app_event_handler(const struct app_event_header *aeh) } APP_EVENT_LISTENER(MODULE, app_event_handler); +APP_EVENT_SUBSCRIBE(MODULE, bat_state_event); +APP_EVENT_SUBSCRIBE(MODULE, hid_led_event); APP_EVENT_SUBSCRIBE(MODULE, module_state_event); +APP_EVENT_SUBSCRIBE(MODULE, mode_switch_event); APP_EVENT_SUBSCRIBE_EARLY(MODULE, power_down_event); APP_EVENT_SUBSCRIBE(MODULE, wake_up_event); diff --git a/src/ui/ui_main.c b/src/ui/ui_main.c index db9fa0a..0c88628 100644 --- a/src/ui/ui_main.c +++ b/src/ui/ui_main.c @@ -1,34 +1,291 @@ -#include +#include #include +#include #include "ui_main.h" +enum ui_status_id { + UI_STATUS_USB = 0, + UI_STATUS_BLE, + UI_STATUS_NUMLOCK, + UI_STATUS_CAPSLOCK, + UI_STATUS_COUNT, +}; + +enum { + UI_LED_MASK_NUM_LOCK = BIT(0), + UI_LED_MASK_CAPS_LOCK = BIT(1), +}; + +struct ui_main_ctx { + lv_obj_t *status_badges[UI_STATUS_COUNT]; + lv_obj_t *status_labels[UI_STATUS_COUNT]; + lv_obj_t *battery_icon; + lv_obj_t *battery_label; + lv_obj_t *battery_state_label; + lv_obj_t *date_label; + lv_obj_t *time_label; +}; + +static struct ui_main_ctx g_ui; static bool ui_initialized; -void ui_main_init(void) +static const char *const status_texts[UI_STATUS_COUNT] = { + LV_SYMBOL_USB, + LV_SYMBOL_BLUETOOTH, + "1", + "A", +}; + +static lv_color_t ui_main_get_battery_color(uint8_t battery_level) { - lv_obj_t *screen; - lv_obj_t *title; - lv_obj_t *subtitle; + if (battery_level > 70U) { + return lv_color_hex(0x8BD450); + } + + if (battery_level >= 20U) { + return lv_color_hex(0xF4D35E); + } + + return lv_color_hex(0xE63946); +} + +static const char *ui_main_get_battery_symbol(uint8_t battery_level) +{ + if (battery_level > 85U) { + return LV_SYMBOL_BATTERY_FULL; + } + + if (battery_level > 60U) { + return LV_SYMBOL_BATTERY_3; + } + + if (battery_level > 35U) { + return LV_SYMBOL_BATTERY_2; + } + + if (battery_level >= 20U) { + return LV_SYMBOL_BATTERY_1; + } + + return LV_SYMBOL_BATTERY_EMPTY; +} + +static bool ui_main_status_is_active(enum ui_status_id id, + const struct ui_main_model *model) +{ + switch (id) { + case UI_STATUS_USB: + return model->mode == MODE_SWITCH_USB; + + case UI_STATUS_BLE: + return model->mode == MODE_SWITCH_BLE; + + case UI_STATUS_NUMLOCK: + return (model->led_mask & UI_LED_MASK_NUM_LOCK) != 0U; + + case UI_STATUS_CAPSLOCK: + return (model->led_mask & UI_LED_MASK_CAPS_LOCK) != 0U; + + default: + return false; + } +} + +static void ui_main_create_status_chip(lv_obj_t *parent, enum ui_status_id id) +{ + lv_obj_t *badge = lv_obj_create(parent); + lv_obj_t *label = lv_label_create(badge); + + lv_obj_remove_style_all(badge); + lv_obj_set_size(badge, 50, 32); + lv_obj_set_style_radius(badge, 10, 0); + lv_obj_set_style_bg_opa(badge, LV_OPA_COVER, 0); + lv_obj_set_style_pad_all(badge, 0, 0); + + lv_label_set_text(label, status_texts[id]); + lv_obj_set_width(label, LV_PCT(100)); + lv_obj_set_style_text_font(label, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_center(label); + + g_ui.status_badges[id] = badge; + g_ui.status_labels[id] = label; +} + +void ui_main_refresh_status_bar(const struct ui_main_model *model) +{ + for (uint32_t i = 0; i < UI_STATUS_COUNT; i++) { + lv_obj_t *badge = g_ui.status_badges[i]; + lv_obj_t *label = g_ui.status_labels[i]; + bool active = ui_main_status_is_active((enum ui_status_id)i, model); + + if ((badge == NULL) || (label == NULL)) { + continue; + } + + lv_obj_set_style_border_width(badge, 3, 0); + lv_obj_set_style_border_color( + badge, + active ? model->theme_color : model->inactive_border_color, 0); + lv_obj_set_style_bg_color( + badge, + active ? lv_color_hex(0x1D2735) : lv_color_hex(0x161A20), 0); + lv_obj_set_style_text_color( + label, + active ? lv_color_white() : lv_color_hex(0x7C8798), 0); + } +} + +void ui_main_refresh_battery(const struct ui_main_model *model) +{ + char battery_text[8]; + const char *state_symbol = ""; + lv_color_t battery_color; + lv_color_t state_color = lv_color_white(); + + if ((g_ui.battery_icon == NULL) || (g_ui.battery_label == NULL) || + (g_ui.battery_state_label == NULL)) { + return; + } + + battery_color = ui_main_get_battery_color(model->battery_level); + snprintk(battery_text, sizeof(battery_text), "%u%%", model->battery_level); + + if (model->full) { + state_symbol = LV_SYMBOL_USB; + state_color = lv_color_hex(0x4C9EF5); + } else if (model->charging) { + state_symbol = LV_SYMBOL_CHARGE; + state_color = lv_color_hex(0xF4D35E); + } + + lv_label_set_text(g_ui.battery_icon, + ui_main_get_battery_symbol(model->battery_level)); + lv_obj_set_style_text_color(g_ui.battery_icon, battery_color, 0); + lv_label_set_text(g_ui.battery_label, battery_text); + lv_label_set_text(g_ui.battery_state_label, state_symbol); + lv_obj_set_style_text_color(g_ui.battery_state_label, state_color, 0); +} + +void ui_main_refresh_datetime(const char *date_text, const char *time_text) +{ + if ((g_ui.date_label == NULL) || (g_ui.time_label == NULL)) { + return; + } + + lv_label_set_text(g_ui.date_label, date_text); + lv_label_set_text(g_ui.time_label, time_text); +} + +void ui_main_refresh_all(const struct ui_main_model *model, + const char *date_text, + const char *time_text) +{ + ui_main_refresh_status_bar(model); + ui_main_refresh_battery(model); + ui_main_refresh_datetime(date_text, time_text); +} + +void ui_main_init(const struct ui_main_model *model, + const char *date_text, + const char *time_text) +{ + lv_obj_t *screen = lv_screen_active(); + lv_obj_t *content; + lv_obj_t *top_row; + lv_obj_t *battery_wrap; + lv_obj_t *middle_row; + lv_obj_t *bottom_row; if (ui_initialized) { return; } - screen = lv_screen_active(); - lv_obj_set_style_bg_color(screen, lv_color_hex(0x101418), 0); - lv_obj_set_style_text_color(screen, lv_color_hex(0xF5F7FA), 0); + memset(&g_ui, 0, sizeof(g_ui)); - title = lv_label_create(screen); - lv_label_set_text(title, "Hello World"); - lv_obj_set_style_text_font(title, &lv_font_montserrat_14, 0); - lv_obj_align(title, LV_ALIGN_CENTER, 0, -10); + lv_obj_clean(screen); + lv_obj_set_style_bg_color(screen, lv_color_hex(0x0F1115), 0); + lv_obj_set_style_bg_grad_color(screen, lv_color_hex(0x1A1F29), 0); + lv_obj_set_style_bg_grad_dir(screen, LV_GRAD_DIR_VER, 0); + lv_obj_set_style_bg_opa(screen, LV_OPA_COVER, 0); + lv_obj_set_style_text_color(screen, lv_color_white(), 0); + lv_obj_set_style_pad_all(screen, 0, 0); + lv_obj_set_scrollbar_mode(screen, LV_SCROLLBAR_MODE_OFF); - subtitle = lv_label_create(screen); - lv_label_set_text(subtitle, "WH Mini Keyboard"); - lv_obj_set_style_text_opa(subtitle, LV_OPA_70, 0); - lv_obj_align(subtitle, LV_ALIGN_CENTER, 0, 14); + content = lv_obj_create(screen); + lv_obj_remove_style_all(content); + lv_obj_set_size(content, LV_PCT(100), LV_PCT(100)); + lv_obj_set_style_bg_opa(content, LV_OPA_TRANSP, 0); + lv_obj_set_style_pad_left(content, 14, 0); + lv_obj_set_style_pad_right(content, 14, 0); + lv_obj_set_style_pad_top(content, 8, 0); + lv_obj_set_style_pad_bottom(content, 8, 0); + lv_obj_set_layout(content, LV_LAYOUT_FLEX); + lv_obj_set_flex_flow(content, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(content, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, + LV_FLEX_ALIGN_CENTER); + top_row = lv_obj_create(content); + lv_obj_remove_style_all(top_row); + lv_obj_set_width(top_row, LV_PCT(100)); + lv_obj_set_flex_grow(top_row, 1); + lv_obj_set_style_bg_opa(top_row, LV_OPA_TRANSP, 0); + lv_obj_set_layout(top_row, LV_LAYOUT_FLEX); + lv_obj_set_flex_flow(top_row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(top_row, LV_FLEX_ALIGN_SPACE_BETWEEN, + LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + + g_ui.date_label = lv_label_create(top_row); + lv_obj_set_style_text_font(g_ui.date_label, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(g_ui.date_label, lv_color_hex(0xD8DEE9), 0); + + battery_wrap = lv_obj_create(top_row); + lv_obj_remove_style_all(battery_wrap); + lv_obj_set_width(battery_wrap, LV_SIZE_CONTENT); + lv_obj_set_layout(battery_wrap, LV_LAYOUT_FLEX); + lv_obj_set_flex_flow(battery_wrap, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(battery_wrap, LV_FLEX_ALIGN_CENTER, + LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_column(battery_wrap, 4, 0); + + g_ui.battery_icon = lv_label_create(battery_wrap); + lv_obj_set_style_text_font(g_ui.battery_icon, &lv_font_montserrat_14, 0); + + g_ui.battery_label = lv_label_create(battery_wrap); + lv_obj_set_style_text_font(g_ui.battery_label, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(g_ui.battery_label, lv_color_hex(0xD8DEE9), 0); + + g_ui.battery_state_label = lv_label_create(battery_wrap); + lv_obj_set_style_text_font(g_ui.battery_state_label, &lv_font_montserrat_14, 0); + + middle_row = lv_obj_create(content); + lv_obj_remove_style_all(middle_row); + lv_obj_set_width(middle_row, LV_PCT(100)); + lv_obj_set_flex_grow(middle_row, 2); + lv_obj_set_style_bg_opa(middle_row, LV_OPA_TRANSP, 0); + + g_ui.time_label = lv_label_create(middle_row); + lv_obj_set_style_text_font(g_ui.time_label, &lv_font_montserrat_32, 0); + lv_obj_set_style_text_color(g_ui.time_label, lv_color_white(), 0); + lv_obj_center(g_ui.time_label); + + bottom_row = lv_obj_create(content); + lv_obj_remove_style_all(bottom_row); + lv_obj_set_width(bottom_row, LV_PCT(100)); + lv_obj_set_flex_grow(bottom_row, 1); + lv_obj_set_style_bg_opa(bottom_row, LV_OPA_TRANSP, 0); + lv_obj_set_layout(bottom_row, LV_LAYOUT_FLEX); + lv_obj_set_flex_flow(bottom_row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(bottom_row, LV_FLEX_ALIGN_CENTER, + LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_column(bottom_row, 6, 0); + + for (uint32_t i = 0; i < UI_STATUS_COUNT; i++) { + ui_main_create_status_chip(bottom_row, (enum ui_status_id)i); + } + + ui_main_refresh_all(model, date_text, time_text); ui_initialized = true; } diff --git a/src/ui/ui_main.h b/src/ui/ui_main.h index 816baee..921dffe 100644 --- a/src/ui/ui_main.h +++ b/src/ui/ui_main.h @@ -1,11 +1,36 @@ #ifndef BLINKY_UI_MAIN_H_ #define BLINKY_UI_MAIN_H_ +#include +#include + +#include + +#include "mode_switch_event.h" + #ifdef __cplusplus extern "C" { #endif -void ui_main_init(void); +struct ui_main_model { + lv_color_t theme_color; + lv_color_t inactive_border_color; + uint8_t battery_level; + enum mode_switch_mode mode; + uint8_t led_mask; + bool charging; + bool full; +}; + +void ui_main_init(const struct ui_main_model *model, + const char *date_text, + const char *time_text); +void ui_main_refresh_all(const struct ui_main_model *model, + const char *date_text, + const char *time_text); +void ui_main_refresh_status_bar(const struct ui_main_model *model); +void ui_main_refresh_battery(const struct ui_main_model *model); +void ui_main_refresh_datetime(const char *date_text, const char *time_text); #ifdef __cplusplus }