#include #include #include #include #define MODULE settings_module #include #include #include #include #include #include "encoder_event.h" #include "module_lifecycle.h" #include "settings_mode_event.h" #include "settings_ui.h" #include "settings_view_event.h" #include "theme_color.h" #include "theme_rgb_update_event.h" LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF); #define SETTINGS_MUTE_KEY_ID 0x180U enum root_menu_item { ROOT_MENU_ITEM_BLUETOOTH = 0, ROOT_MENU_ITEM_THEME, }; enum ble_menu_item { BLE_MENU_ITEM_SLOT_1 = 0, BLE_MENU_ITEM_SLOT_2, BLE_MENU_ITEM_SLOT_3, BLE_MENU_ITEM_ERASE_BOND, }; struct named_theme { const char *name; struct theme_rgb color; }; struct ble_slot_state { char label[SETTINGS_UI_VALUE_MAX]; }; struct settings_module_ctx { struct module_lifecycle_ctx lc; bool active; enum settings_ui_page page; uint8_t root_selected; uint8_t ble_selected; uint8_t theme_selected; uint8_t active_ble_slot; struct ble_slot_state ble_slots[SETTINGS_UI_BLE_SLOT_COUNT]; struct theme_rgb current_theme; uint8_t current_theme_index; bool theme_matches_palette; }; static int do_init(void); static int do_start(void); static int do_stop(void); static const struct module_lifecycle_cfg lifecycle_cfg = { .mode = ML_MODE_POWER, .stopped_state = MODULE_STATE_STANDBY, }; static const struct module_lifecycle_ops lifecycle_ops = { .do_init = do_init, .do_start = do_start, .do_stop = do_stop, }; static const struct named_theme theme_palette[SETTINGS_UI_THEME_OPTION_COUNT] = { { .name = "Red", .color = { .r = 0xFF, .g = 0x00, .b = 0x00 }, }, { .name = "Amber", .color = { .r = 0xFF, .g = 0x95, .b = 0x00 }, }, { .name = "Default", .color = { .r = BLINKY_THEME_DEFAULT_R, .g = BLINKY_THEME_DEFAULT_G, .b = BLINKY_THEME_DEFAULT_B, }, }, { .name = "Green", .color = { .r = 0x34, .g = 0xC7, .b = 0x59 }, }, { .name = "Purple", .color = { .r = 0xBF, .g = 0x5A, .b = 0xF2 }, }, { .name = "White", .color = { .r = 0xF2, .g = 0xF2, .b = 0xF7 }, }, }; static struct settings_module_ctx ctx = { .lc = { .state = LC_UNINIT, .cfg = &lifecycle_cfg, .ops = &lifecycle_ops, }, }; static bool theme_equal(const struct theme_rgb *lhs, const struct theme_rgb *rhs) { return (lhs->r == rhs->r) && (lhs->g == rhs->g) && (lhs->b == rhs->b); } static uint8_t wrap_index(uint8_t current, uint8_t count, int8_t delta) { int32_t next = current; if (count == 0U) { return 0U; } next += delta; while (next < 0) { next += count; } return (uint8_t)(next % count); } static void set_text(char *dst, size_t dst_size, const char *src) { if ((dst == NULL) || (dst_size == 0U) || (src == NULL)) { return; } strncpy(dst, src, dst_size); dst[dst_size - 1U] = '\0'; } static const char *current_theme_name_get(void) { if (!ctx.theme_matches_palette) { return "Custom"; } return theme_palette[ctx.current_theme_index].name; } static void update_theme_match_state(void) { ctx.theme_matches_palette = false; ctx.current_theme_index = 0U; for (uint8_t i = 0; i < SETTINGS_UI_THEME_OPTION_COUNT; i++) { if (!theme_equal(&ctx.current_theme, &theme_palette[i].color)) { continue; } ctx.current_theme_index = i; ctx.theme_matches_palette = true; return; } } static void publish_view_state(void) { struct settings_ui_state state = { .active = ctx.active, .page = ctx.page, .root_selected = ctx.root_selected, .ble_selected = ctx.ble_selected, .theme_selected = ctx.theme_selected, .active_ble_slot = ctx.active_ble_slot, .accent = ctx.current_theme, .theme_option_count = SETTINGS_UI_THEME_OPTION_COUNT, }; set_text(state.root_items[ROOT_MENU_ITEM_BLUETOOTH].title, sizeof(state.root_items[ROOT_MENU_ITEM_BLUETOOTH].title), "Bluetooth"); set_text(state.root_items[ROOT_MENU_ITEM_BLUETOOTH].value, sizeof(state.root_items[ROOT_MENU_ITEM_BLUETOOTH].value), (ctx.active_ble_slot == 0U) ? "Slot 1" : (ctx.active_ble_slot == 1U) ? "Slot 2" : "Slot 3"); set_text(state.root_items[ROOT_MENU_ITEM_THEME].title, sizeof(state.root_items[ROOT_MENU_ITEM_THEME].title), "Theme"); set_text(state.root_items[ROOT_MENU_ITEM_THEME].value, sizeof(state.root_items[ROOT_MENU_ITEM_THEME].value), current_theme_name_get()); for (uint8_t i = 0; i < SETTINGS_UI_BLE_SLOT_COUNT; i++) { char slot_name[SETTINGS_UI_TITLE_MAX]; snprintk(slot_name, sizeof(slot_name), "Slot %u", i + 1U); set_text(state.ble_items[i].title, sizeof(state.ble_items[i].title), slot_name); set_text(state.ble_items[i].value, sizeof(state.ble_items[i].value), ctx.ble_slots[i].label); } set_text(state.ble_items[BLE_MENU_ITEM_ERASE_BOND].title, sizeof(state.ble_items[BLE_MENU_ITEM_ERASE_BOND].title), "Erase Bond"); set_text(state.ble_items[BLE_MENU_ITEM_ERASE_BOND].value, sizeof(state.ble_items[BLE_MENU_ITEM_ERASE_BOND].value), (ctx.active_ble_slot == 0U) ? "Slot 1" : (ctx.active_ble_slot == 1U) ? "Slot 2" : "Slot 3"); for (uint8_t i = 0; i < SETTINGS_UI_THEME_OPTION_COUNT; i++) { set_text(state.theme_options[i].name, sizeof(state.theme_options[i].name), theme_palette[i].name); state.theme_options[i].color = theme_palette[i].color; } submit_settings_view_event(&state); } static void settings_exit(void) { if (!ctx.active) { return; } ctx.active = false; ctx.page = SETTINGS_UI_PAGE_ROOT; submit_settings_mode_event(false); publish_view_state(); } static void settings_enter(void) { if (ctx.active) { return; } ctx.active = true; ctx.page = SETTINGS_UI_PAGE_ROOT; ctx.root_selected = ROOT_MENU_ITEM_BLUETOOTH; ctx.ble_selected = ctx.active_ble_slot; ctx.theme_selected = ctx.current_theme_index; submit_settings_mode_event(true); publish_view_state(); } static void apply_theme_selection(uint8_t index) { if (index >= SETTINGS_UI_THEME_OPTION_COUNT) { return; } ctx.theme_selected = index; ctx.current_theme_index = index; ctx.current_theme = theme_palette[index].color; ctx.theme_matches_palette = true; submit_theme_rgb_update_event(ctx.current_theme); } static void handle_confirm(void) { switch (ctx.page) { case SETTINGS_UI_PAGE_ROOT: ctx.page = (ctx.root_selected == ROOT_MENU_ITEM_BLUETOOTH) ? SETTINGS_UI_PAGE_BLE : SETTINGS_UI_PAGE_THEME; ctx.ble_selected = ctx.active_ble_slot; ctx.theme_selected = ctx.current_theme_index; publish_view_state(); break; case SETTINGS_UI_PAGE_BLE: if (ctx.ble_selected < SETTINGS_UI_BLE_SLOT_COUNT) { ctx.active_ble_slot = ctx.ble_selected; } else { set_text(ctx.ble_slots[ctx.active_ble_slot].label, sizeof(ctx.ble_slots[ctx.active_ble_slot].label), "Empty"); } ctx.page = SETTINGS_UI_PAGE_ROOT; ctx.root_selected = ROOT_MENU_ITEM_BLUETOOTH; publish_view_state(); break; case SETTINGS_UI_PAGE_THEME: apply_theme_selection(ctx.theme_selected); ctx.page = SETTINGS_UI_PAGE_ROOT; ctx.root_selected = ROOT_MENU_ITEM_THEME; publish_view_state(); break; default: break; } } static void handle_back(void) { switch (ctx.page) { case SETTINGS_UI_PAGE_ROOT: settings_exit(); break; case SETTINGS_UI_PAGE_BLE: ctx.page = SETTINGS_UI_PAGE_ROOT; ctx.root_selected = ROOT_MENU_ITEM_BLUETOOTH; publish_view_state(); break; case SETTINGS_UI_PAGE_THEME: ctx.page = SETTINGS_UI_PAGE_ROOT; ctx.root_selected = ROOT_MENU_ITEM_THEME; publish_view_state(); break; default: break; } } static void handle_navigation(int8_t detents) { if (detents == 0) { return; } switch (ctx.page) { case SETTINGS_UI_PAGE_ROOT: ctx.root_selected = wrap_index(ctx.root_selected, SETTINGS_UI_ROOT_ITEM_COUNT, detents); publish_view_state(); break; case SETTINGS_UI_PAGE_BLE: ctx.ble_selected = wrap_index(ctx.ble_selected, SETTINGS_UI_BLE_ITEM_COUNT, detents); publish_view_state(); break; case SETTINGS_UI_PAGE_THEME: ctx.theme_selected = wrap_index(ctx.theme_selected, SETTINGS_UI_THEME_OPTION_COUNT, detents); publish_view_state(); break; default: break; } } static int do_init(void) { ctx.active = false; ctx.page = SETTINGS_UI_PAGE_ROOT; ctx.root_selected = ROOT_MENU_ITEM_BLUETOOTH; ctx.ble_selected = BLE_MENU_ITEM_SLOT_1; ctx.theme_selected = 0U; ctx.active_ble_slot = 0U; ctx.current_theme = (struct theme_rgb) { .r = BLINKY_THEME_DEFAULT_R, .g = BLINKY_THEME_DEFAULT_G, .b = BLINKY_THEME_DEFAULT_B, }; update_theme_match_state(); set_text(ctx.ble_slots[0].label, sizeof(ctx.ble_slots[0].label), "Host A"); set_text(ctx.ble_slots[1].label, sizeof(ctx.ble_slots[1].label), "Host B"); set_text(ctx.ble_slots[2].label, sizeof(ctx.ble_slots[2].label), "Empty"); return 0; } static int do_start(void) { return 0; } static int do_stop(void) { settings_exit(); return 0; } static bool handle_click_event(const struct click_event *event) { if (!module_lifecycle_is_running(&ctx.lc) || (event->key_id != SETTINGS_MUTE_KEY_ID)) { return false; } if (!ctx.active) { if (event->click == CLICK_LONG) { settings_enter(); } return false; } switch (event->click) { case CLICK_SHORT: handle_confirm(); break; case CLICK_LONG: handle_back(); break; default: break; } return false; } static bool handle_encoder_event(const struct encoder_event *event) { if (!module_lifecycle_is_running(&ctx.lc) || !ctx.active) { return false; } handle_navigation(event->detents); return false; } static bool handle_theme_rgb_update_event( const struct theme_rgb_update_event *event) { ctx.current_theme = event->theme; update_theme_match_state(); if (ctx.page == SETTINGS_UI_PAGE_THEME) { ctx.theme_selected = ctx.current_theme_index; } if (ctx.active) { publish_view_state(); } return false; } static bool app_event_handler(const struct app_event_header *aeh) { if (is_click_event(aeh)) { return handle_click_event(cast_click_event(aeh)); } if (is_encoder_event(aeh)) { return handle_encoder_event(cast_encoder_event(aeh)); } if (is_theme_rgb_update_event(aeh)) { return handle_theme_rgb_update_event( cast_theme_rgb_update_event(aeh)); } if (is_module_state_event(aeh)) { const struct module_state_event *event = cast_module_state_event(aeh); if (check_state(event, MODULE_ID(main), MODULE_STATE_READY)) { (void)module_set_lifecycle(&ctx.lc, LC_RUNNING); } return false; } if (is_power_down_event(aeh)) { if (module_lifecycle_is_initialized(&ctx.lc)) { (void)module_set_lifecycle(&ctx.lc, LC_STOPPED); } return false; } if (is_wake_up_event(aeh)) { if (module_lifecycle_is_initialized(&ctx.lc)) { (void)module_set_lifecycle(&ctx.lc, LC_RUNNING); } return false; } return false; } APP_EVENT_LISTENER(MODULE, app_event_handler); APP_EVENT_SUBSCRIBE(MODULE, click_event); APP_EVENT_SUBSCRIBE(MODULE, encoder_event); APP_EVENT_SUBSCRIBE(MODULE, module_state_event); APP_EVENT_SUBSCRIBE(MODULE, theme_rgb_update_event); APP_EVENT_SUBSCRIBE_EARLY(MODULE, power_down_event); APP_EVENT_SUBSCRIBE(MODULE, wake_up_event);