reorder menu and add sound settings
All checks were successful
ESP32 Build & Release / build (push) Successful in 1m18s
All checks were successful
ESP32 Build & Release / build (push) Successful in 1m18s
This commit is contained in:
@@ -17,6 +17,7 @@ extern "C" {
|
||||
typedef enum {
|
||||
APP_STATE_BOOT = 0,
|
||||
APP_STATE_MENU,
|
||||
APP_STATE_SETTINGS_MENU,
|
||||
APP_STATE_SCANNER,
|
||||
APP_STATE_CARD_VIEW,
|
||||
APP_STATE_LIFE_COUNTER,
|
||||
|
||||
114
main/buzzer.c
114
main/buzzer.c
@@ -7,6 +7,8 @@
|
||||
#include "esp_log.h"
|
||||
#include "driver/ledc.h"
|
||||
#include "hw_pins.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "nvs.h"
|
||||
#include "buzzer.h"
|
||||
|
||||
#include "freertos/queue.h"
|
||||
@@ -19,6 +21,7 @@ static const char *TAG = "buzzer";
|
||||
|
||||
static QueueHandle_t s_sound_queue = NULL;
|
||||
static volatile bool s_melody_playing = false;
|
||||
static audio_profile_t s_profile = AUDIO_PROFILE_CHIPTUNE;
|
||||
|
||||
static const buzzer_note_t MELODY_VICTORY[] = {
|
||||
{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)
|
||||
{
|
||||
/* 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 = {
|
||||
.speed_mode = BUZZER_SPEED_MODE,
|
||||
.timer_num = BUZZER_TIMER_NUM,
|
||||
@@ -109,12 +122,15 @@ esp_err_t buzzer_init(void)
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
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)
|
||||
{
|
||||
if (s_profile == AUDIO_PROFILE_SILENT) {
|
||||
return ESP_OK;
|
||||
}
|
||||
if (s_melody_playing) {
|
||||
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) {
|
||||
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)
|
||||
{
|
||||
if (s_profile != AUDIO_PROFILE_CHIPTUNE) {
|
||||
return ESP_OK;
|
||||
}
|
||||
if (s_sound_queue != NULL) {
|
||||
xQueueSend(s_sound_queue, &sound, 0);
|
||||
}
|
||||
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);
|
||||
}
|
||||
@@ -90,7 +90,46 @@ esp_err_t buzzer_play_sound(buzzer_sound_t sound);
|
||||
/**
|
||||
* @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
|
||||
}
|
||||
|
||||
@@ -115,3 +115,23 @@ lv_disp_t *display_get_disp(void)
|
||||
{
|
||||
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;
|
||||
}
|
||||
@@ -16,6 +16,8 @@ extern "C" {
|
||||
|
||||
esp_err_t display_manager_init(void);
|
||||
lv_disp_t *display_get_disp(void);
|
||||
esp_err_t display_set_sleep(bool sleep);
|
||||
bool display_is_sleeping(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
382
main/ui_task.c
382
main/ui_task.c
@@ -39,18 +39,30 @@ static bool s_waiting_fetch = false;
|
||||
static lv_img_dsc_t s_art_img_dsc;
|
||||
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 */
|
||||
#define MENU_ITEM_COUNT 8
|
||||
#define MAIN_MENU_ITEM_COUNT 6
|
||||
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",
|
||||
"2. LIFE & COUNTERS",
|
||||
"3. DICE & COIN FLIP",
|
||||
"4. COMMANDER TAX",
|
||||
"5. TOKEN & COUNTERS",
|
||||
"6. DECK STORAGE",
|
||||
"7. DEVICE & WIFI INFO",
|
||||
"8. UPDATE FIRMWARE"
|
||||
"6. SETTINGS & TOOLS"
|
||||
};
|
||||
|
||||
#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 */
|
||||
@@ -124,6 +136,7 @@ static lv_obj_t *s_card_scroll_cont = NULL;
|
||||
|
||||
/* Forward declarations */
|
||||
static void view_menu(void);
|
||||
static void view_settings_menu(void);
|
||||
static void view_scanner(void);
|
||||
static void view_card(void);
|
||||
static void view_life(void);
|
||||
@@ -311,13 +324,15 @@ static void view_menu(void)
|
||||
ui_clean_screen();
|
||||
ui_draw_header("MTG COMPANION","");
|
||||
|
||||
/* 8 Menu Items */
|
||||
/* 6 Menu Items */
|
||||
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);
|
||||
lv_obj_t *item_box = lv_obj_create(lv_scr_act());
|
||||
lv_obj_set_size(item_box, DISP_W - 24, 28);
|
||||
lv_obj_align(item_box, LV_ALIGN_TOP_MID, 0, start_y + i * 31);
|
||||
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);
|
||||
@@ -332,12 +347,13 @@ static void view_menu(void)
|
||||
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);
|
||||
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);
|
||||
} 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_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_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, -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();
|
||||
}
|
||||
@@ -625,11 +709,13 @@ static void roll_current_dice(void)
|
||||
if (s_dice_result == 20) {
|
||||
/* Natural 20 - Victory Fanfare! */
|
||||
buzzer_play_sound(BUZZER_SOUND_VICTORY);
|
||||
buzzer_chirp(1046, 20);
|
||||
} else if (s_dice_result == 1) {
|
||||
/* Critical Fail */
|
||||
buzzer_chirp(220, 150);
|
||||
/* Critical Fail - Sad slide fanfare */
|
||||
buzzer_play_sound(BUZZER_SOUND_LOSS);
|
||||
buzzer_chirp(220, 30);
|
||||
} else {
|
||||
buzzer_chirp(880, 50);
|
||||
buzzer_chirp(880, 25);
|
||||
}
|
||||
} else if (s_dice_mode == DICE_MODE_D6) {
|
||||
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_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_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);
|
||||
@@ -1094,7 +1180,7 @@ static void view_device_info(void)
|
||||
}
|
||||
|
||||
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_color(hint, lv_color_hex(COLOR_AMBER), 0);
|
||||
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_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_color(hint, lv_color_hex(COLOR_AMBER), 0);
|
||||
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_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_color(hint, lv_color_hex(COLOR_AMBER), 0);
|
||||
lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -8);
|
||||
@@ -1366,6 +1452,102 @@ static void view_ota(void)
|
||||
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 */
|
||||
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_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 */
|
||||
lv_obj_t *name_lbl = lv_label_create(card_box);
|
||||
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_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_label_set_long_mode(name_lbl, LV_LABEL_LONG_DOT);
|
||||
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 */
|
||||
if (power[0] || tough[0]) {
|
||||
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);
|
||||
app_set_view(APP_STATE_CARD_VIEW);
|
||||
|
||||
buzzer_chirp(880, 60);
|
||||
buzzer_sound_rfid_tap();
|
||||
|
||||
char json_path[72];
|
||||
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();
|
||||
break;
|
||||
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);
|
||||
s_storage_confirm_dialog = false;
|
||||
s_storage_option_idx = 0;
|
||||
view_storage();
|
||||
break;
|
||||
case 6:
|
||||
case 3: /* DEVICE & WIFI INFO */
|
||||
app_set_view(APP_STATE_DEVICE_INFO);
|
||||
view_device_info();
|
||||
break;
|
||||
case 7:
|
||||
case 4: /* UPDATE FIRMWARE */
|
||||
app_set_view(APP_STATE_OTA_UPDATE);
|
||||
buzzer_chirp(988, 40);
|
||||
ota_manager_check_update_async();
|
||||
@@ -1801,8 +2009,8 @@ static void handle_encoder_click(void)
|
||||
view_ota();
|
||||
} else if (st == OTA_STATUS_UP_TO_DATE || st == OTA_STATUS_FAILED) {
|
||||
ota_manager_reset();
|
||||
app_set_view(APP_STATE_MENU);
|
||||
view_menu();
|
||||
app_set_view(APP_STATE_SETTINGS_MENU);
|
||||
view_settings_menu();
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -1836,8 +2044,8 @@ static void handle_encoder_click(void)
|
||||
buzzer_chirp(587, 60);
|
||||
view_storage();
|
||||
} else {
|
||||
app_set_view(APP_STATE_MENU);
|
||||
view_menu();
|
||||
app_set_view(APP_STATE_SETTINGS_MENU);
|
||||
view_settings_menu();
|
||||
}
|
||||
} else {
|
||||
if (s_storage_option_idx == 0) {
|
||||
@@ -1859,8 +2067,8 @@ static void handle_encoder_click(void)
|
||||
case APP_STATE_DEVICE_INFO:
|
||||
encoder_set_steps_per_detent(4);
|
||||
buzzer_chirp(880, 40);
|
||||
app_set_view(APP_STATE_MENU);
|
||||
view_menu();
|
||||
app_set_view(APP_STATE_SETTINGS_MENU);
|
||||
view_settings_menu();
|
||||
break;
|
||||
|
||||
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);
|
||||
lvgl_port_unlock();
|
||||
}
|
||||
buzzer_chirp(600, 10);
|
||||
buzzer_sound_tick();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1912,9 +2120,17 @@ static void handle_encoder_rotation(int32_t delta)
|
||||
switch (cur) {
|
||||
case APP_STATE_MENU:
|
||||
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 {
|
||||
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;
|
||||
|
||||
@@ -1976,32 +2192,41 @@ static void handle_encoder_rotation(int32_t delta)
|
||||
/* Redraw view once per rotation event */
|
||||
switch (cur) {
|
||||
case APP_STATE_MENU:
|
||||
buzzer_chirp(523, 15);
|
||||
buzzer_sound_tick();
|
||||
view_menu();
|
||||
break;
|
||||
|
||||
case APP_STATE_SETTINGS_MENU:
|
||||
buzzer_sound_tick();
|
||||
view_settings_menu();
|
||||
break;
|
||||
|
||||
case APP_STATE_LIFE_COUNTER:
|
||||
buzzer_chirp(700, 15);
|
||||
if (dir > 0) {
|
||||
buzzer_sound_life_gain();
|
||||
} else {
|
||||
buzzer_sound_life_loss();
|
||||
}
|
||||
view_life();
|
||||
break;
|
||||
|
||||
case APP_STATE_DICE_ROLLER:
|
||||
buzzer_chirp(600, 15);
|
||||
buzzer_sound_tick();
|
||||
view_dice();
|
||||
break;
|
||||
|
||||
case APP_STATE_COMMANDER_TRACKER:
|
||||
buzzer_chirp(650, 15);
|
||||
buzzer_sound_tick();
|
||||
view_commander_tax();
|
||||
break;
|
||||
|
||||
case APP_STATE_TOKEN_SPAWNER:
|
||||
buzzer_chirp(s_token_edit_count ? 750 : 550, 15);
|
||||
buzzer_sound_tick();
|
||||
view_token_spawner();
|
||||
break;
|
||||
|
||||
case APP_STATE_STORAGE_MANAGER:
|
||||
buzzer_chirp(523, 15);
|
||||
buzzer_sound_tick();
|
||||
view_storage();
|
||||
break;
|
||||
|
||||
@@ -2009,8 +2234,8 @@ static void handle_encoder_rotation(int32_t delta)
|
||||
ota_status_t st = ota_manager_get_status();
|
||||
if (st == OTA_STATUS_UPDATE_AVAILABLE) {
|
||||
ota_manager_reset();
|
||||
app_set_view(APP_STATE_MENU);
|
||||
view_menu();
|
||||
app_set_view(APP_STATE_SETTINGS_MENU);
|
||||
view_settings_menu();
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -2033,6 +2258,7 @@ static void ui_task(void *arg)
|
||||
buzzer_init();
|
||||
|
||||
/* Start on Main Menu at boot */
|
||||
s_last_activity_ms = pdTICKS_TO_MS(xTaskGetTickCount());
|
||||
app_set_view(APP_STATE_MENU);
|
||||
view_menu();
|
||||
|
||||
@@ -2042,34 +2268,49 @@ static void ui_task(void *arg)
|
||||
#endif
|
||||
|
||||
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 (display_is_sleeping()) {
|
||||
display_set_sleep(false);
|
||||
}
|
||||
s_last_activity_ms = pdTICKS_TO_MS(xTaskGetTickCount());
|
||||
handle_new_card(rfid_manager_last_uid());
|
||||
}
|
||||
|
||||
/* 2. Async Scryfall cache result */
|
||||
if (s_waiting_fetch && scryfall_has_pending_result()) {
|
||||
s_last_activity_ms = pdTICKS_TO_MS(xTaskGetTickCount());
|
||||
handle_fetch_ready();
|
||||
}
|
||||
|
||||
/* 3. Button press & hold detection */
|
||||
int sw_level = gpio_get_level(HW_ENC_SW_GPIO);
|
||||
if (sw_level == 0) {
|
||||
s_btn_held_ms += 25;
|
||||
if (s_btn_held_ms >= 750 && !s_btn_long_fired) {
|
||||
s_btn_long_fired = true;
|
||||
/* Long press: RETURN TO MAIN MENU from any screen */
|
||||
buzzer_chirp(1046, 50);
|
||||
vTaskDelay(pdMS_TO_TICKS(40));
|
||||
buzzer_chirp(1318, 70);
|
||||
if (app_get_view() == APP_STATE_REGISTRATION) {
|
||||
wifi_manager_set_power_save(true);
|
||||
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;
|
||||
if (s_btn_held_ms >= 750 && !s_btn_long_fired) {
|
||||
s_btn_long_fired = true;
|
||||
/* Long press: RETURN TO MAIN MENU from any screen */
|
||||
buzzer_chirp(1046, 50);
|
||||
vTaskDelay(pdMS_TO_TICKS(40));
|
||||
buzzer_chirp(1318, 70);
|
||||
if (app_get_view() == APP_STATE_REGISTRATION) {
|
||||
wifi_manager_set_power_save(true);
|
||||
}
|
||||
app_set_view(APP_STATE_MENU);
|
||||
view_menu();
|
||||
}
|
||||
app_set_view(APP_STATE_MENU);
|
||||
view_menu();
|
||||
}
|
||||
} else {
|
||||
if (s_btn_held_ms >= 35 && !s_btn_long_fired) {
|
||||
s_last_activity_ms = pdTICKS_TO_MS(xTaskGetTickCount());
|
||||
handle_encoder_click();
|
||||
}
|
||||
s_btn_held_ms = 0;
|
||||
@@ -2079,7 +2320,22 @@ static void ui_task(void *arg)
|
||||
/* 4. Rotary encoder rotation */
|
||||
if (encoder_has_delta()) {
|
||||
int32_t delta = encoder_get_delta_and_reset();
|
||||
handle_encoder_rotation(delta);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/* 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 */
|
||||
@@ -2107,7 +2363,7 @@ static void ui_task(void *arg)
|
||||
}
|
||||
}
|
||||
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);
|
||||
buzzer_chirp(1046, 120);
|
||||
view_ota();
|
||||
|
||||
@@ -2232,7 +2232,7 @@ CONFIG_LV_ATTRIBUTE_MEM_ALIGN_SIZE=1
|
||||
#
|
||||
# CONFIG_LV_FONT_MONTSERRAT_8 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_16 is not set
|
||||
# CONFIG_LV_FONT_MONTSERRAT_18 is not set
|
||||
|
||||
@@ -17,6 +17,7 @@ CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y
|
||||
|
||||
# LVGL & Font Configuration
|
||||
CONFIG_LV_COLOR_16_SWAP=y
|
||||
CONFIG_LV_FONT_MONTSERRAT_12=y
|
||||
CONFIG_LV_FONT_MONTSERRAT_14=y
|
||||
CONFIG_LV_FONT_MONTSERRAT_20=y
|
||||
CONFIG_LV_FONT_MONTSERRAT_48=y
|
||||
|
||||
Reference in New Issue
Block a user