503 lines
11 KiB
C
503 lines
11 KiB
C
|
|
#include <stdbool.h>
|
||
|
|
#include <stdint.h>
|
||
|
|
#include <string.h>
|
||
|
|
|
||
|
|
#include <app_event_manager.h>
|
||
|
|
|
||
|
|
#define MODULE settings_module
|
||
|
|
#include <caf/events/click_event.h>
|
||
|
|
#include <caf/events/module_state_event.h>
|
||
|
|
#include <caf/events/power_event.h>
|
||
|
|
|
||
|
|
#include <zephyr/logging/log.h>
|
||
|
|
#include <zephyr/sys/printk.h>
|
||
|
|
|
||
|
|
#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);
|