reorder menu and add sound settings
All checks were successful
ESP32 Build & Release / build (push) Successful in 1m18s

This commit is contained in:
2026-09-13 22:54:28 +02:00
parent 7afcc47152
commit 57cf8225bc
8 changed files with 496 additions and 67 deletions

View File

@@ -17,6 +17,7 @@ extern "C" {
typedef enum { typedef enum {
APP_STATE_BOOT = 0, APP_STATE_BOOT = 0,
APP_STATE_MENU, APP_STATE_MENU,
APP_STATE_SETTINGS_MENU,
APP_STATE_SCANNER, APP_STATE_SCANNER,
APP_STATE_CARD_VIEW, APP_STATE_CARD_VIEW,
APP_STATE_LIFE_COUNTER, APP_STATE_LIFE_COUNTER,

View File

@@ -7,6 +7,8 @@
#include "esp_log.h" #include "esp_log.h"
#include "driver/ledc.h" #include "driver/ledc.h"
#include "hw_pins.h" #include "hw_pins.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "buzzer.h" #include "buzzer.h"
#include "freertos/queue.h" #include "freertos/queue.h"
@@ -19,6 +21,7 @@ static const char *TAG = "buzzer";
static QueueHandle_t s_sound_queue = NULL; static QueueHandle_t s_sound_queue = NULL;
static volatile bool s_melody_playing = false; static volatile bool s_melody_playing = false;
static audio_profile_t s_profile = AUDIO_PROFILE_CHIPTUNE;
static const buzzer_note_t MELODY_VICTORY[] = { static const buzzer_note_t MELODY_VICTORY[] = {
{NOTE_C5, 90}, {NOTE_C5, 90}, {NOTE_C5, 90}, {NOTE_C5, 220}, {NOTE_C5, 90}, {NOTE_C5, 90}, {NOTE_C5, 90}, {NOTE_C5, 220},
@@ -79,6 +82,16 @@ static void buzzer_task(void *arg)
esp_err_t buzzer_init(void) esp_err_t buzzer_init(void)
{ {
/* Load saved profile from NVS */
nvs_handle_t h;
if (nvs_open("mtg_cfg", NVS_READONLY, &h) == ESP_OK) {
uint8_t prof = 0;
if (nvs_get_u8(h, "audio_prof", &prof) == ESP_OK && prof <= AUDIO_PROFILE_SILENT) {
s_profile = (audio_profile_t)prof;
}
nvs_close(h);
}
ledc_timer_config_t timer_cfg = { ledc_timer_config_t timer_cfg = {
.speed_mode = BUZZER_SPEED_MODE, .speed_mode = BUZZER_SPEED_MODE,
.timer_num = BUZZER_TIMER_NUM, .timer_num = BUZZER_TIMER_NUM,
@@ -109,12 +122,15 @@ esp_err_t buzzer_init(void)
xTaskCreate(buzzer_task, "buzzer_task", 2048, NULL, 3, NULL); xTaskCreate(buzzer_task, "buzzer_task", 2048, NULL, 3, NULL);
} }
ESP_LOGI(TAG, "buzzer ready on GPIO %d (chiptune synth active)", HW_BUZZER_GPIO); ESP_LOGI(TAG, "buzzer ready on GPIO %d (profile: %s)", HW_BUZZER_GPIO, buzzer_get_profile_name(s_profile));
return ESP_OK; return ESP_OK;
} }
esp_err_t buzzer_start(uint32_t freq_hz, uint32_t duty_percent) esp_err_t buzzer_start(uint32_t freq_hz, uint32_t duty_percent)
{ {
if (s_profile == AUDIO_PROFILE_SILENT) {
return ESP_OK;
}
if (freq_hz == 0) { if (freq_hz == 0) {
return buzzer_stop(); return buzzer_stop();
} }
@@ -142,10 +158,17 @@ esp_err_t buzzer_stop(void)
esp_err_t buzzer_chirp(uint32_t freq_hz, uint32_t duration_ms) esp_err_t buzzer_chirp(uint32_t freq_hz, uint32_t duration_ms)
{ {
if (s_profile == AUDIO_PROFILE_SILENT) {
return ESP_OK;
}
if (s_melody_playing) { if (s_melody_playing) {
return ESP_OK; /* Don't disrupt ongoing background chiptune melody */ return ESP_OK; /* Don't disrupt ongoing background chiptune melody */
} }
esp_err_t ret = buzzer_start(freq_hz, 30); uint32_t duty = (s_profile == AUDIO_PROFILE_TACTILE) ? 10 : 30;
if (s_profile == AUDIO_PROFILE_TACTILE && duration_ms > 20) {
duration_ms = 20;
}
esp_err_t ret = buzzer_start(freq_hz, duty);
if (ret != ESP_OK) { if (ret != ESP_OK) {
return ret; return ret;
} }
@@ -155,8 +178,95 @@ esp_err_t buzzer_chirp(uint32_t freq_hz, uint32_t duration_ms)
esp_err_t buzzer_play_sound(buzzer_sound_t sound) esp_err_t buzzer_play_sound(buzzer_sound_t sound)
{ {
if (s_profile != AUDIO_PROFILE_CHIPTUNE) {
return ESP_OK;
}
if (s_sound_queue != NULL) { if (s_sound_queue != NULL) {
xQueueSend(s_sound_queue, &sound, 0); xQueueSend(s_sound_queue, &sound, 0);
} }
return ESP_OK; return ESP_OK;
} }
void buzzer_set_profile(audio_profile_t profile)
{
s_profile = profile;
nvs_handle_t h;
if (nvs_open("mtg_cfg", NVS_READWRITE, &h) == ESP_OK) {
nvs_set_u8(h, "audio_prof", (uint8_t)profile);
nvs_commit(h);
nvs_close(h);
}
ESP_LOGI(TAG, "Audio profile set to: %s", buzzer_get_profile_name(profile));
}
audio_profile_t buzzer_get_profile(void)
{
return s_profile;
}
const char *buzzer_get_profile_name(audio_profile_t profile)
{
switch (profile) {
case AUDIO_PROFILE_CHIPTUNE: return "CHIPTUNE";
case AUDIO_PROFILE_TACTILE: return "TACTILE";
case AUDIO_PROFILE_SILENT: return "SILENT";
default: return "UNKNOWN";
}
}
void buzzer_sound_life_gain(void)
{
if (s_profile == AUDIO_PROFILE_SILENT) return;
if (s_profile == AUDIO_PROFILE_TACTILE) {
buzzer_chirp(NOTE_C6, 10);
return;
}
/* Chiptune: ascending two-tone */
buzzer_start(NOTE_G5, 25);
vTaskDelay(pdMS_TO_TICKS(22));
buzzer_start(NOTE_C6, 25);
vTaskDelay(pdMS_TO_TICKS(35));
buzzer_stop();
}
void buzzer_sound_life_loss(void)
{
if (s_profile == AUDIO_PROFILE_SILENT) return;
if (s_profile == AUDIO_PROFILE_TACTILE) {
buzzer_chirp(NOTE_C4, 12);
return;
}
/* Chiptune: descending two-tone */
buzzer_start(NOTE_E4, 25);
vTaskDelay(pdMS_TO_TICKS(25));
buzzer_start(NOTE_C4, 25);
vTaskDelay(pdMS_TO_TICKS(40));
buzzer_stop();
}
void buzzer_sound_rfid_tap(void)
{
if (s_profile == AUDIO_PROFILE_SILENT) return;
if (s_profile == AUDIO_PROFILE_TACTILE) {
buzzer_chirp(NOTE_A5, 20);
return;
}
/* Chiptune: bright recognition chime */
buzzer_start(NOTE_A5, 25);
vTaskDelay(pdMS_TO_TICKS(28));
buzzer_start(NOTE_E6, 25);
vTaskDelay(pdMS_TO_TICKS(45));
buzzer_stop();
}
void buzzer_sound_tick(void)
{
if (s_profile == AUDIO_PROFILE_SILENT) return;
if (s_profile == AUDIO_PROFILE_TACTILE) {
buzzer_start(NOTE_D6, 8);
vTaskDelay(pdMS_TO_TICKS(6));
buzzer_stop();
return;
}
buzzer_chirp(NOTE_C5, 12);
}

View File

@@ -90,7 +90,46 @@ esp_err_t buzzer_play_sound(buzzer_sound_t sound);
/** /**
* @brief Play a custom sequence of notes synchronously. * @brief Play a custom sequence of notes synchronously.
*/ */
esp_err_t buzzer_play_notes(const buzzer_note_t *notes, size_t count); typedef enum {
AUDIO_PROFILE_CHIPTUNE = 0, /* Full 8-bit melodies & arcade fanfares */
AUDIO_PROFILE_TACTILE = 1, /* Soft micro-clicks & subtle clean chimes */
AUDIO_PROFILE_SILENT = 2, /* Mute all sound */
} audio_profile_t;
/**
* @brief Set the active audio profile and persist to NVS.
*/
void buzzer_set_profile(audio_profile_t profile);
/**
* @brief Get the active audio profile.
*/
audio_profile_t buzzer_get_profile(void);
/**
* @brief Get human-readable string for audio profile.
*/
const char *buzzer_get_profile_name(audio_profile_t profile);
/**
* @brief Pitch-up feedback when gaining life.
*/
void buzzer_sound_life_gain(void);
/**
* @brief Pitch-down feedback when losing life.
*/
void buzzer_sound_life_loss(void);
/**
* @brief Clean notification chime when RFID card is tapped.
*/
void buzzer_sound_rfid_tap(void);
/**
* @brief Subtle encoder rotary detent click.
*/
void buzzer_sound_tick(void);
#ifdef __cplusplus #ifdef __cplusplus
} }

View File

@@ -115,3 +115,23 @@ lv_disp_t *display_get_disp(void)
{ {
return s_disp; return s_disp;
} }
static bool s_sleeping = false;
esp_err_t display_set_sleep(bool sleep)
{
if (s_panel == NULL) {
return ESP_ERR_INVALID_STATE;
}
if (s_sleeping == sleep) {
return ESP_OK;
}
s_sleeping = sleep;
ESP_LOGI(TAG, "Display %s", sleep ? "sleep (off)" : "wake (on)");
return esp_lcd_panel_disp_on_off(s_panel, !sleep);
}
bool display_is_sleeping(void)
{
return s_sleeping;
}

View File

@@ -16,6 +16,8 @@ extern "C" {
esp_err_t display_manager_init(void); esp_err_t display_manager_init(void);
lv_disp_t *display_get_disp(void); lv_disp_t *display_get_disp(void);
esp_err_t display_set_sleep(bool sleep);
bool display_is_sleeping(void);
#ifdef __cplusplus #ifdef __cplusplus
} }

View File

@@ -39,18 +39,30 @@ static bool s_waiting_fetch = false;
static lv_img_dsc_t s_art_img_dsc; static lv_img_dsc_t s_art_img_dsc;
static uint16_t *s_art_pixels = NULL; static uint16_t *s_art_pixels = NULL;
/* Inactivity sleep timer (3 minutes) */
#define UI_SLEEP_TIMEOUT_MS (180000)
static uint32_t s_last_activity_ms = 0;
/* Menu items */ /* Menu items */
#define MENU_ITEM_COUNT 8 #define MAIN_MENU_ITEM_COUNT 6
static int s_menu_idx = 0; static int s_menu_idx = 0;
static const char *s_menu_labels[MENU_ITEM_COUNT] = { static const char *s_menu_labels[MAIN_MENU_ITEM_COUNT] = {
"1. CARD SCANNER", "1. CARD SCANNER",
"2. LIFE & COUNTERS", "2. LIFE & COUNTERS",
"3. DICE & COIN FLIP", "3. DICE & COIN FLIP",
"4. COMMANDER TAX", "4. COMMANDER TAX",
"5. TOKEN & COUNTERS", "5. TOKEN & COUNTERS",
"6. DECK STORAGE", "6. SETTINGS & TOOLS"
"7. DEVICE & WIFI INFO", };
"8. UPDATE FIRMWARE"
#define SETTINGS_MENU_ITEM_COUNT 5
static int s_settings_idx = 0;
static const char *s_settings_labels[SETTINGS_MENU_ITEM_COUNT] = {
"< BACK TO MAIN MENU",
"1. AUDIO: CHIPTUNE",
"2. DECK STORAGE",
"3. DEVICE & WIFI INFO",
"4. UPDATE FIRMWARE"
}; };
/* Life & Counters */ /* Life & Counters */
@@ -124,6 +136,7 @@ static lv_obj_t *s_card_scroll_cont = NULL;
/* Forward declarations */ /* Forward declarations */
static void view_menu(void); static void view_menu(void);
static void view_settings_menu(void);
static void view_scanner(void); static void view_scanner(void);
static void view_card(void); static void view_card(void);
static void view_life(void); static void view_life(void);
@@ -311,13 +324,15 @@ static void view_menu(void)
ui_clean_screen(); ui_clean_screen();
ui_draw_header("MTG COMPANION",""); ui_draw_header("MTG COMPANION","");
/* 8 Menu Items */ /* 6 Menu Items */
int start_y = 44; int start_y = 44;
for (int i = 0; i < MENU_ITEM_COUNT; i++) { int row_height = 34;
int row_gap = 7;
for (int i = 0; i < MAIN_MENU_ITEM_COUNT; i++) {
bool selected = (i == s_menu_idx); bool selected = (i == s_menu_idx);
lv_obj_t *item_box = lv_obj_create(lv_scr_act()); lv_obj_t *item_box = lv_obj_create(lv_scr_act());
lv_obj_set_size(item_box, DISP_W - 24, 28); lv_obj_set_size(item_box, DISP_W - 24, row_height);
lv_obj_align(item_box, LV_ALIGN_TOP_MID, 0, start_y + i * 31); lv_obj_align(item_box, LV_ALIGN_TOP_MID, 0, start_y + i * (row_height + row_gap));
lv_obj_set_style_radius(item_box, 6, 0); lv_obj_set_style_radius(item_box, 6, 0);
lv_obj_set_style_pad_all(item_box, 2, 0); lv_obj_set_style_pad_all(item_box, 2, 0);
lv_obj_set_scrollbar_mode(item_box, LV_SCROLLBAR_MODE_OFF); lv_obj_set_scrollbar_mode(item_box, LV_SCROLLBAR_MODE_OFF);
@@ -332,12 +347,13 @@ static void view_menu(void)
lv_obj_set_style_border_width(item_box, 1, 0); lv_obj_set_style_border_width(item_box, 1, 0);
} }
const char *item_txt = s_menu_labels[i];
lv_obj_t *lbl = lv_label_create(item_box); lv_obj_t *lbl = lv_label_create(item_box);
if (selected) { if (selected) {
lv_label_set_text_fmt(lbl, "> %s <", s_menu_labels[i]); lv_label_set_text_fmt(lbl, "> %s <", item_txt);
lv_obj_set_style_text_color(lbl, lv_color_black(), 0); lv_obj_set_style_text_color(lbl, lv_color_black(), 0);
} else { } else {
lv_label_set_text_fmt(lbl, " %s", s_menu_labels[i]); lv_label_set_text_fmt(lbl, " %s", item_txt);
lv_obj_set_style_text_color(lbl, lv_color_white(), 0); lv_obj_set_style_text_color(lbl, lv_color_white(), 0);
} }
lv_obj_set_style_text_font(lbl, &lv_font_montserrat_14, 0); lv_obj_set_style_text_font(lbl, &lv_font_montserrat_14, 0);
@@ -349,7 +365,75 @@ static void view_menu(void)
lv_label_set_text(hint, "ROTATE: NAV • CLICK: SELECT"); lv_label_set_text(hint, "ROTATE: NAV • CLICK: SELECT");
lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0); lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_AMBER), 0); lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_AMBER), 0);
lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -6); lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -8);
lvgl_port_unlock();
}
/* Settings & Tools Submenu */
static void view_settings_menu(void)
{
encoder_set_steps_per_detent(4);
lvgl_port_lock(0);
ui_clean_screen();
ui_draw_header("SETTINGS & TOOLS", "");
char audio_lbl_buf[36];
snprintf(audio_lbl_buf, sizeof(audio_lbl_buf), "1. AUDIO: %s", buzzer_get_profile_name(buzzer_get_profile()));
int start_y = 50;
int row_height = 34;
int row_gap = 8;
for (int i = 0; i < SETTINGS_MENU_ITEM_COUNT; i++) {
bool selected = (i == s_settings_idx);
lv_obj_t *item_box = lv_obj_create(lv_scr_act());
lv_obj_set_size(item_box, DISP_W - 24, row_height);
lv_obj_align(item_box, LV_ALIGN_TOP_MID, 0, start_y + i * (row_height + row_gap));
lv_obj_set_style_radius(item_box, 6, 0);
lv_obj_set_style_pad_all(item_box, 2, 0);
lv_obj_set_scrollbar_mode(item_box, LV_SCROLLBAR_MODE_OFF);
if (selected) {
lv_obj_set_style_bg_color(item_box, lv_color_hex(0xFFCC00), 0);
lv_obj_set_style_border_color(item_box, lv_color_white(), 0);
lv_obj_set_style_border_width(item_box, 2, 0);
} else {
lv_obj_set_style_bg_color(item_box, lv_color_hex(0x1C1C1C), 0);
lv_obj_set_style_border_color(item_box, lv_color_hex(0x333333), 0);
lv_obj_set_style_border_width(item_box, 1, 0);
}
const char *item_txt = (i == 1) ? audio_lbl_buf : s_settings_labels[i];
lv_obj_t *lbl = lv_label_create(item_box);
if (i == 0) {
/* Back button item */
if (selected) {
lv_label_set_text(lbl, "> < BACK TO MAIN MENU <");
lv_obj_set_style_text_color(lbl, lv_color_black(), 0);
} else {
lv_label_set_text(lbl, "< BACK TO MAIN MENU");
lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_AMBER), 0);
}
} else {
if (selected) {
lv_label_set_text_fmt(lbl, "> %s <", item_txt);
lv_obj_set_style_text_color(lbl, lv_color_black(), 0);
} else {
lv_label_set_text_fmt(lbl, " %s", item_txt);
lv_obj_set_style_text_color(lbl, lv_color_white(), 0);
}
}
lv_obj_set_style_text_font(lbl, &lv_font_montserrat_14, 0);
lv_obj_align(lbl, LV_ALIGN_CENTER, 0, 0);
}
/* Bottom navigation hint */
lv_obj_t *hint = lv_label_create(lv_scr_act());
lv_label_set_text(hint, "ROTATE: NAV • CLICK: SELECT");
lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_AMBER), 0);
lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -8);
lvgl_port_unlock(); lvgl_port_unlock();
} }
@@ -625,11 +709,13 @@ static void roll_current_dice(void)
if (s_dice_result == 20) { if (s_dice_result == 20) {
/* Natural 20 - Victory Fanfare! */ /* Natural 20 - Victory Fanfare! */
buzzer_play_sound(BUZZER_SOUND_VICTORY); buzzer_play_sound(BUZZER_SOUND_VICTORY);
buzzer_chirp(1046, 20);
} else if (s_dice_result == 1) { } else if (s_dice_result == 1) {
/* Critical Fail */ /* Critical Fail - Sad slide fanfare */
buzzer_chirp(220, 150); buzzer_play_sound(BUZZER_SOUND_LOSS);
buzzer_chirp(220, 30);
} else { } else {
buzzer_chirp(880, 50); buzzer_chirp(880, 25);
} }
} else if (s_dice_mode == DICE_MODE_D6) { } else if (s_dice_mode == DICE_MODE_D6) {
s_dice_result = (int)(esp_random() % 6) + 1; s_dice_result = (int)(esp_random() % 6) + 1;
@@ -993,7 +1079,7 @@ static void view_storage(void)
lv_obj_set_style_border_width(btn1, 1, 0); lv_obj_set_style_border_width(btn1, 1, 0);
} }
lv_obj_t *b1_lbl = lv_label_create(btn1); lv_obj_t *b1_lbl = lv_label_create(btn1);
lv_label_set_text(b1_lbl, s_storage_option_idx == 1 ? "> BACK TO MENU <" : "BACK TO MENU"); lv_label_set_text(b1_lbl, s_storage_option_idx == 1 ? "> BACK <" : "BACK");
lv_obj_set_style_text_font(b1_lbl, &lv_font_montserrat_14, 0); lv_obj_set_style_text_font(b1_lbl, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(b1_lbl, s_storage_option_idx == 1 ? lv_color_black() : lv_color_white(), 0); lv_obj_set_style_text_color(b1_lbl, s_storage_option_idx == 1 ? lv_color_black() : lv_color_white(), 0);
lv_obj_align(b1_lbl, LV_ALIGN_CENTER, 0, 0); lv_obj_align(b1_lbl, LV_ALIGN_CENTER, 0, 0);
@@ -1094,7 +1180,7 @@ static void view_device_info(void)
} }
lv_obj_t *hint = lv_label_create(lv_scr_act()); lv_obj_t *hint = lv_label_create(lv_scr_act());
lv_label_set_text(hint, "CLICK: MENU"); lv_label_set_text(hint, "CLICK: BACK • HOLD: MENU");
lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0); lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_AMBER), 0); lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_AMBER), 0);
lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -8); lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -8);
@@ -1252,7 +1338,7 @@ static void view_ota(void)
lv_obj_align(v, LV_ALIGN_CENTER, 0, 15); lv_obj_align(v, LV_ALIGN_CENTER, 0, 15);
lv_obj_t *hint = lv_label_create(lv_scr_act()); lv_obj_t *hint = lv_label_create(lv_scr_act());
lv_label_set_text(hint, "CLICK: MAIN MENU"); lv_label_set_text(hint, "CLICK: BACK • HOLD: MENU");
lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0); lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_AMBER), 0); lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_AMBER), 0);
lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -8); lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -8);
@@ -1355,7 +1441,7 @@ static void view_ota(void)
lv_obj_align(msg, LV_ALIGN_CENTER, 0, 20); lv_obj_align(msg, LV_ALIGN_CENTER, 0, 20);
lv_obj_t *hint = lv_label_create(lv_scr_act()); lv_obj_t *hint = lv_label_create(lv_scr_act());
lv_label_set_text(hint, "CLICK: MAIN MENU"); lv_label_set_text(hint, "CLICK: BACK • HOLD: MENU");
lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0); lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_AMBER), 0); lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_AMBER), 0);
lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -8); lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -8);
@@ -1366,6 +1452,102 @@ static void view_ota(void)
lvgl_port_unlock(); lvgl_port_unlock();
} }
static void get_mana_badge_colors(const char *sym, uint32_t *bg, uint32_t *text, uint32_t *border)
{
if (strcasecmp(sym, "W") == 0) {
*bg = 0xF9F6DE; *text = 0x222222; *border = 0xD8D19F; /* MTG White / Ivory */
} else if (strcasecmp(sym, "U") == 0) {
*bg = 0x0E68AB; *text = 0xFFFFFF; *border = 0x074270; /* MTG Blue */
} else if (strcasecmp(sym, "B") == 0) {
*bg = 0x242022; *text = 0xEEEEEE; *border = 0x5C5458; /* MTG Black / Charcoal */
} else if (strcasecmp(sym, "R") == 0) {
*bg = 0xD3202A; *text = 0xFFFFFF; *border = 0x8C151B; /* MTG Red */
} else if (strcasecmp(sym, "G") == 0) {
*bg = 0x00733E; *text = 0xFFFFFF; *border = 0x004A28; /* MTG Green */
} else if (strcasecmp(sym, "C") == 0) {
*bg = 0x9E9E9E; *text = 0x111111; *border = 0x616161; /* Colorless Diamond */
} else if (strchr(sym, 'P') != NULL || strchr(sym, 'p') != NULL) {
*bg = 0x8B0000; *text = 0xFFFFFF; *border = 0x550000; /* Phyrexian Crimson */
} else if (strchr(sym, '/') != NULL) {
*bg = 0xD4AF37; *text = 0x111111; *border = 0xAA8820; /* Hybrid Gold */
} else {
*bg = 0xB0BEC5; *text = 0x111111; *border = 0x78909C; /* Mana Silver (0-9, X) */
}
}
static int render_mana_symbols(lv_obj_t *parent, const char *mana)
{
if (mana == NULL || mana[0] == '\0') {
return 0;
}
int sym_count = 0;
const char *p = mana;
while (*p) {
if (*p == '{') sym_count++;
p++;
}
if (sym_count == 0) {
return 0;
}
int badge_sz = (sym_count >= 6) ? 15 : 18;
int gap = (sym_count >= 6) ? 2 : 3;
lv_obj_t *cont = lv_obj_create(parent);
lv_obj_remove_style_all(cont);
lv_obj_set_size(cont, LV_SIZE_CONTENT, badge_sz);
lv_obj_set_layout(cont, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(cont, LV_FLEX_ALIGN_END, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_column(cont, gap, 0);
lv_obj_align(cont, LV_ALIGN_TOP_RIGHT, 0, 0);
lv_obj_clear_flag(cont, LV_OBJ_FLAG_SCROLLABLE);
p = mana;
while (*p) {
if (*p == '{') {
p++;
const char *end = strchr(p, '}');
if (!end) break;
size_t token_len = end - p;
if (token_len > 0 && token_len < 8) {
char sym[8];
memcpy(sym, p, token_len);
sym[token_len] = '\0';
uint32_t bg, txt, border;
get_mana_badge_colors(sym, &bg, &txt, &border);
lv_obj_t *badge = lv_obj_create(cont);
lv_obj_set_size(badge, badge_sz, badge_sz);
lv_obj_set_style_radius(badge, LV_RADIUS_CIRCLE, 0);
lv_obj_set_style_bg_color(badge, lv_color_hex(bg), 0);
lv_obj_set_style_bg_opa(badge, LV_OPA_COVER, 0);
lv_obj_set_style_border_color(badge, lv_color_hex(border), 0);
lv_obj_set_style_border_width(badge, 1, 0);
lv_obj_set_style_pad_all(badge, 0, 0);
lv_obj_clear_flag(badge, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t *lbl = lv_label_create(badge);
lv_label_set_text(lbl, sym);
lv_obj_set_style_text_color(lbl, lv_color_hex(txt), 0);
#if LV_FONT_MONTSERRAT_12
lv_obj_set_style_text_font(lbl, &lv_font_montserrat_12, 0);
#else
lv_obj_set_style_text_font(lbl, &lv_font_montserrat_14, 0);
#endif
lv_obj_align(lbl, LV_ALIGN_CENTER, 0, 0);
}
p = end + 1;
} else {
p++;
}
}
return sym_count * badge_sz + (sym_count - 1) * gap;
}
/* 8. Card View Screen */ /* 8. Card View Screen */
static void view_card(void) static void view_card(void)
{ {
@@ -1449,25 +1631,23 @@ static void view_card(void)
lv_obj_clear_flag(card_box, LV_OBJ_FLAG_SCROLLABLE); lv_obj_clear_flag(card_box, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_scrollbar_mode(card_box, LV_SCROLLBAR_MODE_OFF); lv_obj_set_scrollbar_mode(card_box, LV_SCROLLBAR_MODE_OFF);
/* Pinned Header: Mana Badges */
int mana_w = 0;
if (mana[0]) {
mana_w = render_mana_symbols(card_box, mana);
}
/* Pinned Header: Name */ /* Pinned Header: Name */
lv_obj_t *name_lbl = lv_label_create(card_box); lv_obj_t *name_lbl = lv_label_create(card_box);
lv_label_set_text(name_lbl, name); lv_label_set_text(name_lbl, name);
lv_obj_set_style_text_font(name_lbl, &lv_font_montserrat_14, 0); lv_obj_set_style_text_font(name_lbl, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(name_lbl, lv_color_hex(0xFFCC00), 0); lv_obj_set_style_text_color(name_lbl, lv_color_hex(0xFFCC00), 0);
int name_max_w = (mana[0] != '\0') ? (DISP_W - 36 - 60) : (DISP_W - 36); int name_max_w = (mana_w > 0) ? (DISP_W - 36 - mana_w - 6) : (DISP_W - 36);
if (name_max_w < 60) name_max_w = 60;
lv_obj_set_width(name_lbl, name_max_w); lv_obj_set_width(name_lbl, name_max_w);
lv_label_set_long_mode(name_lbl, LV_LABEL_LONG_DOT); lv_label_set_long_mode(name_lbl, LV_LABEL_LONG_DOT);
lv_obj_align(name_lbl, LV_ALIGN_TOP_LEFT, 0, 0); lv_obj_align(name_lbl, LV_ALIGN_TOP_LEFT, 0, 0);
/* Pinned Header: Mana */
if (mana[0]) {
lv_obj_t *mana_lbl = lv_label_create(card_box);
lv_label_set_text(mana_lbl, mana);
lv_obj_set_style_text_font(mana_lbl, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(mana_lbl, lv_color_hex(0x00E676), 0);
lv_obj_align(mana_lbl, LV_ALIGN_TOP_RIGHT, 0, 0);
}
/* Pinned Footer: P/T or Loyalty */ /* Pinned Footer: P/T or Loyalty */
if (power[0] || tough[0]) { if (power[0] || tough[0]) {
lv_obj_t *pt_lbl = lv_label_create(card_box); lv_obj_t *pt_lbl = lv_label_create(card_box);
@@ -1656,7 +1836,7 @@ static void handle_new_card(const char *uid)
snprintf(s_card_name, sizeof(s_card_name), "%s", name); snprintf(s_card_name, sizeof(s_card_name), "%s", name);
app_set_view(APP_STATE_CARD_VIEW); app_set_view(APP_STATE_CARD_VIEW);
buzzer_chirp(880, 60); buzzer_sound_rfid_tap();
char json_path[72]; char json_path[72];
snprintf(json_path, sizeof(json_path), STORAGE_CARDS_DIR "/%s.json", uid); snprintf(json_path, sizeof(json_path), STORAGE_CARDS_DIR "/%s.json", uid);
@@ -1773,16 +1953,44 @@ static void handle_encoder_click(void)
view_token_spawner(); view_token_spawner();
break; break;
case 5: case 5:
app_set_view(APP_STATE_SETTINGS_MENU);
s_settings_idx = 0;
view_settings_menu();
break;
default:
break;
}
break;
case APP_STATE_SETTINGS_MENU:
buzzer_chirp(988, 40);
switch (s_settings_idx) {
case 0: /* < BACK TO MAIN MENU */
app_set_view(APP_STATE_MENU);
view_menu();
break;
case 1: { /* Audio profile inline toggle */
audio_profile_t next_prof = (buzzer_get_profile() + 1) % 3;
buzzer_set_profile(next_prof);
if (next_prof == AUDIO_PROFILE_CHIPTUNE) {
buzzer_chirp(1046, 50);
} else if (next_prof == AUDIO_PROFILE_TACTILE) {
buzzer_chirp(880, 15);
}
view_settings_menu();
break;
}
case 2: /* DECK STORAGE */
app_set_view(APP_STATE_STORAGE_MANAGER); app_set_view(APP_STATE_STORAGE_MANAGER);
s_storage_confirm_dialog = false; s_storage_confirm_dialog = false;
s_storage_option_idx = 0; s_storage_option_idx = 0;
view_storage(); view_storage();
break; break;
case 6: case 3: /* DEVICE & WIFI INFO */
app_set_view(APP_STATE_DEVICE_INFO); app_set_view(APP_STATE_DEVICE_INFO);
view_device_info(); view_device_info();
break; break;
case 7: case 4: /* UPDATE FIRMWARE */
app_set_view(APP_STATE_OTA_UPDATE); app_set_view(APP_STATE_OTA_UPDATE);
buzzer_chirp(988, 40); buzzer_chirp(988, 40);
ota_manager_check_update_async(); ota_manager_check_update_async();
@@ -1801,8 +2009,8 @@ static void handle_encoder_click(void)
view_ota(); view_ota();
} else if (st == OTA_STATUS_UP_TO_DATE || st == OTA_STATUS_FAILED) { } else if (st == OTA_STATUS_UP_TO_DATE || st == OTA_STATUS_FAILED) {
ota_manager_reset(); ota_manager_reset();
app_set_view(APP_STATE_MENU); app_set_view(APP_STATE_SETTINGS_MENU);
view_menu(); view_settings_menu();
} }
break; break;
} }
@@ -1836,8 +2044,8 @@ static void handle_encoder_click(void)
buzzer_chirp(587, 60); buzzer_chirp(587, 60);
view_storage(); view_storage();
} else { } else {
app_set_view(APP_STATE_MENU); app_set_view(APP_STATE_SETTINGS_MENU);
view_menu(); view_settings_menu();
} }
} else { } else {
if (s_storage_option_idx == 0) { if (s_storage_option_idx == 0) {
@@ -1859,8 +2067,8 @@ static void handle_encoder_click(void)
case APP_STATE_DEVICE_INFO: case APP_STATE_DEVICE_INFO:
encoder_set_steps_per_detent(4); encoder_set_steps_per_detent(4);
buzzer_chirp(880, 40); buzzer_chirp(880, 40);
app_set_view(APP_STATE_MENU); app_set_view(APP_STATE_SETTINGS_MENU);
view_menu(); view_settings_menu();
break; break;
case APP_STATE_SCANNER: case APP_STATE_SCANNER:
@@ -1900,7 +2108,7 @@ static void handle_encoder_rotation(int32_t delta)
lv_obj_scroll_by_bounded(s_card_scroll_cont, 0, -delta * 35, LV_ANIM_ON); lv_obj_scroll_by_bounded(s_card_scroll_cont, 0, -delta * 35, LV_ANIM_ON);
lvgl_port_unlock(); lvgl_port_unlock();
} }
buzzer_chirp(600, 10); buzzer_sound_tick();
return; return;
} }
@@ -1912,9 +2120,17 @@ static void handle_encoder_rotation(int32_t delta)
switch (cur) { switch (cur) {
case APP_STATE_MENU: case APP_STATE_MENU:
if (dir > 0) { if (dir > 0) {
s_menu_idx = (s_menu_idx + 1) % MENU_ITEM_COUNT; s_menu_idx = (s_menu_idx + 1) % MAIN_MENU_ITEM_COUNT;
} else { } else {
s_menu_idx = (s_menu_idx - 1 + MENU_ITEM_COUNT) % MENU_ITEM_COUNT; s_menu_idx = (s_menu_idx - 1 + MAIN_MENU_ITEM_COUNT) % MAIN_MENU_ITEM_COUNT;
}
break;
case APP_STATE_SETTINGS_MENU:
if (dir > 0) {
s_settings_idx = (s_settings_idx + 1) % SETTINGS_MENU_ITEM_COUNT;
} else {
s_settings_idx = (s_settings_idx - 1 + SETTINGS_MENU_ITEM_COUNT) % SETTINGS_MENU_ITEM_COUNT;
} }
break; break;
@@ -1976,32 +2192,41 @@ static void handle_encoder_rotation(int32_t delta)
/* Redraw view once per rotation event */ /* Redraw view once per rotation event */
switch (cur) { switch (cur) {
case APP_STATE_MENU: case APP_STATE_MENU:
buzzer_chirp(523, 15); buzzer_sound_tick();
view_menu(); view_menu();
break; break;
case APP_STATE_SETTINGS_MENU:
buzzer_sound_tick();
view_settings_menu();
break;
case APP_STATE_LIFE_COUNTER: case APP_STATE_LIFE_COUNTER:
buzzer_chirp(700, 15); if (dir > 0) {
buzzer_sound_life_gain();
} else {
buzzer_sound_life_loss();
}
view_life(); view_life();
break; break;
case APP_STATE_DICE_ROLLER: case APP_STATE_DICE_ROLLER:
buzzer_chirp(600, 15); buzzer_sound_tick();
view_dice(); view_dice();
break; break;
case APP_STATE_COMMANDER_TRACKER: case APP_STATE_COMMANDER_TRACKER:
buzzer_chirp(650, 15); buzzer_sound_tick();
view_commander_tax(); view_commander_tax();
break; break;
case APP_STATE_TOKEN_SPAWNER: case APP_STATE_TOKEN_SPAWNER:
buzzer_chirp(s_token_edit_count ? 750 : 550, 15); buzzer_sound_tick();
view_token_spawner(); view_token_spawner();
break; break;
case APP_STATE_STORAGE_MANAGER: case APP_STATE_STORAGE_MANAGER:
buzzer_chirp(523, 15); buzzer_sound_tick();
view_storage(); view_storage();
break; break;
@@ -2009,8 +2234,8 @@ static void handle_encoder_rotation(int32_t delta)
ota_status_t st = ota_manager_get_status(); ota_status_t st = ota_manager_get_status();
if (st == OTA_STATUS_UPDATE_AVAILABLE) { if (st == OTA_STATUS_UPDATE_AVAILABLE) {
ota_manager_reset(); ota_manager_reset();
app_set_view(APP_STATE_MENU); app_set_view(APP_STATE_SETTINGS_MENU);
view_menu(); view_settings_menu();
} }
break; break;
} }
@@ -2033,6 +2258,7 @@ static void ui_task(void *arg)
buzzer_init(); buzzer_init();
/* Start on Main Menu at boot */ /* Start on Main Menu at boot */
s_last_activity_ms = pdTICKS_TO_MS(xTaskGetTickCount());
app_set_view(APP_STATE_MENU); app_set_view(APP_STATE_MENU);
view_menu(); view_menu();
@@ -2042,19 +2268,32 @@ static void ui_task(void *arg)
#endif #endif
while (1) { while (1) {
/* 1. New RFID card - scans immediately anywhere */ /* 1. New RFID card - scans immediately anywhere (wakes if sleeping) */
if (rfid_manager_has_pending_card()) { if (rfid_manager_has_pending_card()) {
if (display_is_sleeping()) {
display_set_sleep(false);
}
s_last_activity_ms = pdTICKS_TO_MS(xTaskGetTickCount());
handle_new_card(rfid_manager_last_uid()); handle_new_card(rfid_manager_last_uid());
} }
/* 2. Async Scryfall cache result */ /* 2. Async Scryfall cache result */
if (s_waiting_fetch && scryfall_has_pending_result()) { if (s_waiting_fetch && scryfall_has_pending_result()) {
s_last_activity_ms = pdTICKS_TO_MS(xTaskGetTickCount());
handle_fetch_ready(); handle_fetch_ready();
} }
/* 3. Button press & hold detection */ /* 3. Button press & hold detection */
int sw_level = gpio_get_level(HW_ENC_SW_GPIO); int sw_level = gpio_get_level(HW_ENC_SW_GPIO);
if (sw_level == 0) { if (sw_level == 0) {
if (display_is_sleeping()) {
display_set_sleep(false);
s_last_activity_ms = pdTICKS_TO_MS(xTaskGetTickCount());
buzzer_sound_tick();
s_btn_held_ms = 0;
s_btn_long_fired = true; /* Suppress click on release when waking */
} else {
s_last_activity_ms = pdTICKS_TO_MS(xTaskGetTickCount());
s_btn_held_ms += 25; s_btn_held_ms += 25;
if (s_btn_held_ms >= 750 && !s_btn_long_fired) { if (s_btn_held_ms >= 750 && !s_btn_long_fired) {
s_btn_long_fired = true; s_btn_long_fired = true;
@@ -2068,8 +2307,10 @@ static void ui_task(void *arg)
app_set_view(APP_STATE_MENU); app_set_view(APP_STATE_MENU);
view_menu(); view_menu();
} }
}
} else { } else {
if (s_btn_held_ms >= 35 && !s_btn_long_fired) { if (s_btn_held_ms >= 35 && !s_btn_long_fired) {
s_last_activity_ms = pdTICKS_TO_MS(xTaskGetTickCount());
handle_encoder_click(); handle_encoder_click();
} }
s_btn_held_ms = 0; s_btn_held_ms = 0;
@@ -2079,8 +2320,23 @@ static void ui_task(void *arg)
/* 4. Rotary encoder rotation */ /* 4. Rotary encoder rotation */
if (encoder_has_delta()) { if (encoder_has_delta()) {
int32_t delta = encoder_get_delta_and_reset(); int32_t delta = encoder_get_delta_and_reset();
if (display_is_sleeping()) {
display_set_sleep(false);
s_last_activity_ms = pdTICKS_TO_MS(xTaskGetTickCount());
buzzer_sound_tick();
} else {
s_last_activity_ms = pdTICKS_TO_MS(xTaskGetTickCount());
handle_encoder_rotation(delta); handle_encoder_rotation(delta);
} }
}
/* 5. Inactivity display sleep check (3 minutes) */
uint32_t now_ms = pdTICKS_TO_MS(xTaskGetTickCount());
if (!display_is_sleeping() && (now_ms - s_last_activity_ms >= UI_SLEEP_TIMEOUT_MS)) {
if (app_get_view() != APP_STATE_OTA_UPDATE || ota_manager_get_status() != OTA_STATUS_DOWNLOADING) {
display_set_sleep(true);
}
}
/* 5. OTA screen live refresh / status change */ /* 5. OTA screen live refresh / status change */
if (app_get_view() == APP_STATE_OTA_UPDATE) { if (app_get_view() == APP_STATE_OTA_UPDATE) {
@@ -2107,7 +2363,7 @@ static void ui_task(void *arg)
} }
} }
if (s_boot_ota_checked && ota_manager_get_status() == OTA_STATUS_UPDATE_AVAILABLE && if (s_boot_ota_checked && ota_manager_get_status() == OTA_STATUS_UPDATE_AVAILABLE &&
app_get_view() == APP_STATE_MENU) { (app_get_view() == APP_STATE_MENU || app_get_view() == APP_STATE_SETTINGS_MENU)) {
app_set_view(APP_STATE_OTA_UPDATE); app_set_view(APP_STATE_OTA_UPDATE);
buzzer_chirp(1046, 120); buzzer_chirp(1046, 120);
view_ota(); view_ota();

View File

@@ -2232,7 +2232,7 @@ CONFIG_LV_ATTRIBUTE_MEM_ALIGN_SIZE=1
# #
# CONFIG_LV_FONT_MONTSERRAT_8 is not set # CONFIG_LV_FONT_MONTSERRAT_8 is not set
# CONFIG_LV_FONT_MONTSERRAT_10 is not set # CONFIG_LV_FONT_MONTSERRAT_10 is not set
# CONFIG_LV_FONT_MONTSERRAT_12 is not set CONFIG_LV_FONT_MONTSERRAT_12=y
CONFIG_LV_FONT_MONTSERRAT_14=y CONFIG_LV_FONT_MONTSERRAT_14=y
# CONFIG_LV_FONT_MONTSERRAT_16 is not set # CONFIG_LV_FONT_MONTSERRAT_16 is not set
# CONFIG_LV_FONT_MONTSERRAT_18 is not set # CONFIG_LV_FONT_MONTSERRAT_18 is not set

View File

@@ -17,6 +17,7 @@ CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y
# LVGL & Font Configuration # LVGL & Font Configuration
CONFIG_LV_COLOR_16_SWAP=y CONFIG_LV_COLOR_16_SWAP=y
CONFIG_LV_FONT_MONTSERRAT_12=y
CONFIG_LV_FONT_MONTSERRAT_14=y CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_FONT_MONTSERRAT_20=y CONFIG_LV_FONT_MONTSERRAT_20=y
CONFIG_LV_FONT_MONTSERRAT_48=y CONFIG_LV_FONT_MONTSERRAT_48=y