Initial commit

This commit is contained in:
drjones
2026-06-23 06:03:19 -07:00
commit fb4753c1f7
31 changed files with 2908 additions and 0 deletions

11
main/CMakeLists.txt Normal file
View File

@@ -0,0 +1,11 @@
idf_component_register(
SRCS
"app_main.c"
"card_assets.c"
"tarot_deck.c"
"tarot_logic.c"
"ui_main.c"
"ui_settings.c"
"settings.c"
INCLUDE_DIRS "."
)

7
main/Kconfig.projbuild Normal file
View File

@@ -0,0 +1,7 @@
menu "Tarot App"
config TAROT_START_BRIGHTNESS_FROM_NVS
bool "Apply saved brightness during startup"
default y
endmenu

47
main/app_main.c Normal file
View File

@@ -0,0 +1,47 @@
#include <stdbool.h>
#include <stdint.h>
#include "bsp/esp-bsp.h"
#include "esp_err.h"
#include "esp_log.h"
#include "lvgl.h"
#include "nvs_flash.h"
#include "sdkconfig.h"
#include "settings.h"
#include "ui_main.h"
static const char *TAG = "tarot_app";
void app_main(void)
{
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
err = nvs_flash_init();
}
ESP_ERROR_CHECK(err);
ESP_ERROR_CHECK(settings_init());
lv_display_t *display = bsp_display_start();
if (display == NULL) {
ESP_LOGE(TAG, "Waveshare BSP failed to initialize display/touch");
return;
}
#if CONFIG_TAROT_START_BRIGHTNESS_FROM_NVS
tarot_settings_t settings = settings_get();
ESP_ERROR_CHECK_WITHOUT_ABORT(bsp_display_brightness_set(settings.brightness));
#else
ESP_ERROR_CHECK_WITHOUT_ABORT(bsp_display_backlight_on());
#endif
if (bsp_display_lock(1000)) {
ui_main_init();
ui_show_home();
bsp_display_unlock();
} else {
ESP_LOGE(TAG, "Timed out taking LVGL lock during UI startup");
}
}

89
main/card_assets.c Normal file
View File

@@ -0,0 +1,89 @@
#include "card_assets.h"
static lv_color_t suit_color(tarot_suit_t suit)
{
switch (suit) {
case TAROT_SUIT_WANDS:
return lv_color_hex(0xd8743f);
case TAROT_SUIT_CUPS:
return lv_color_hex(0x3b83c4);
case TAROT_SUIT_SWORDS:
return lv_color_hex(0x8a92a6);
case TAROT_SUIT_PENTACLES:
return lv_color_hex(0x5fa66a);
case TAROT_SUIT_NONE:
default:
return lv_color_hex(0xb88cff);
}
}
static const char *suit_glyph(tarot_suit_t suit)
{
switch (suit) {
case TAROT_SUIT_WANDS:
return "W";
case TAROT_SUIT_CUPS:
return "C";
case TAROT_SUIT_SWORDS:
return "S";
case TAROT_SUIT_PENTACLES:
return "P";
case TAROT_SUIT_NONE:
default:
return "M";
}
}
lv_obj_t *card_assets_create_card_art(lv_obj_t *parent, const tarot_card_t *card, bool reversed)
{
lv_obj_t *art = lv_obj_create(parent);
lv_obj_set_size(art, 132, 184);
lv_obj_clear_flag(art, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_style_radius(art, 8, 0);
lv_obj_set_style_border_width(art, 2, 0);
if (card == NULL) {
lv_obj_set_style_border_color(art, lv_color_hex(0xb88cff), 0);
lv_obj_set_style_bg_color(art, lv_color_hex(0xeee7d9), 0);
lv_obj_t *missing = lv_label_create(art);
lv_label_set_text(missing, "Card");
lv_obj_center(missing);
return art;
}
lv_obj_set_style_border_color(art, suit_color(card->suit), 0);
lv_obj_set_style_bg_color(art, lv_color_hex(0xeee7d9), 0);
lv_obj_set_style_pad_all(art, 8, 0);
lv_obj_t *top = lv_label_create(art);
lv_label_set_text_fmt(top, "%02u", card->id);
lv_obj_set_style_text_color(top, lv_color_hex(0x30261f), 0);
lv_obj_align(top, LV_ALIGN_TOP_LEFT, 0, 0);
lv_obj_t *arcana = lv_label_create(art);
lv_label_set_text(arcana, card->arcana == TAROT_ARCANA_MAJOR ? "MAJOR" : suit_glyph(card->suit));
lv_obj_set_style_text_color(arcana, suit_color(card->suit), 0);
lv_obj_align(arcana, LV_ALIGN_TOP_RIGHT, 0, 0);
lv_obj_t *symbol = lv_label_create(art);
lv_label_set_text(symbol, suit_glyph(card->suit));
lv_obj_set_style_text_color(symbol, suit_color(card->suit), 0);
lv_obj_set_style_text_font(symbol, LV_FONT_DEFAULT, 0);
lv_obj_align(symbol, LV_ALIGN_CENTER, 0, -16);
lv_obj_t *name = lv_label_create(art);
lv_label_set_text(name, card->name);
lv_label_set_long_mode(name, LV_LABEL_LONG_WRAP);
lv_obj_set_width(name, 108);
lv_obj_set_style_text_align(name, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_style_text_color(name, lv_color_hex(0x30261f), 0);
lv_obj_align(name, LV_ALIGN_CENTER, 0, 42);
if (reversed) {
lv_obj_t *rev = lv_label_create(art);
lv_label_set_text(rev, "REV");
lv_obj_set_style_text_color(rev, lv_color_hex(0x8b1e2d), 0);
lv_obj_align(rev, LV_ALIGN_BOTTOM_MID, 0, 0);
}
return art;
}

23
main/card_assets.h Normal file
View File

@@ -0,0 +1,23 @@
#pragma once
#include <stdbool.h>
#include "lvgl.h"
#include "tarot_deck.h"
#ifdef __cplusplus
extern "C" {
#endif
/*
* First-pass on-device art renderer.
*
* The app no longer depends on SD card media. Card visuals are generated from
* LVGL widgets using deck metadata already compiled into firmware. Later, real
* embedded LVGL image descriptors can be mapped by card id inside this module.
*/
lv_obj_t *card_assets_create_card_art(lv_obj_t *parent, const tarot_card_t *card, bool reversed);
#ifdef __cplusplus
}
#endif

2
main/idf_component.yml Normal file
View File

@@ -0,0 +1,2 @@
dependencies:
lvgl/lvgl: ">=9,<10"

129
main/settings.c Normal file
View File

@@ -0,0 +1,129 @@
#include "settings.h"
#include "esp_log.h"
#include "nvs.h"
#include "nvs_flash.h"
static const char *TAG = "settings";
static const char *NVS_NAMESPACE = "tarot";
static tarot_settings_t s_settings;
void settings_set_defaults(tarot_settings_t *settings)
{
if (settings == NULL) {
return;
}
settings->reversed_cards_enabled = true;
settings->save_readings_enabled = false;
settings->brightness = 80;
settings->theme = TAROT_THEME_DARK;
settings->selected_spread = TAROT_SPREAD_8_CARD;
}
esp_err_t settings_init(void)
{
nvs_handle_t handle;
esp_err_t err;
uint8_t value = 0;
settings_set_defaults(&s_settings);
err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &handle);
if (err == ESP_ERR_NVS_NOT_FOUND) {
ESP_LOGI(TAG, "No saved settings; using defaults");
return settings_save(&s_settings);
}
if (err != ESP_OK) {
ESP_LOGE(TAG, "nvs_open failed: %s", esp_err_to_name(err));
return err;
}
if (nvs_get_u8(handle, "reversed", &value) == ESP_OK) {
s_settings.reversed_cards_enabled = value != 0;
}
if (nvs_get_u8(handle, "save_read", &value) == ESP_OK) {
s_settings.save_readings_enabled = value != 0;
}
if (nvs_get_u8(handle, "bright", &value) == ESP_OK) {
s_settings.brightness = value <= 100 ? value : 100;
}
if (nvs_get_u8(handle, "theme", &value) == ESP_OK && value <= TAROT_THEME_MYSTIC) {
s_settings.theme = (tarot_theme_t)value;
}
if (nvs_get_u8(handle, "spread", &value) == ESP_OK) {
s_settings.selected_spread = value == TAROT_SPREAD_3_CARD ? TAROT_SPREAD_3_CARD : TAROT_SPREAD_8_CARD;
}
nvs_close(handle);
return ESP_OK;
}
tarot_settings_t settings_get(void)
{
return s_settings;
}
esp_err_t settings_save(const tarot_settings_t *settings)
{
nvs_handle_t handle;
esp_err_t err;
if (settings == NULL) {
return ESP_ERR_INVALID_ARG;
}
err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "nvs_open write failed: %s", esp_err_to_name(err));
return err;
}
err = nvs_set_u8(handle, "reversed", settings->reversed_cards_enabled ? 1 : 0);
if (err != ESP_OK) {
goto fail;
}
err = nvs_set_u8(handle, "save_read", settings->save_readings_enabled ? 1 : 0);
if (err != ESP_OK) {
goto fail;
}
err = nvs_set_u8(handle, "bright", settings->brightness);
if (err != ESP_OK) {
goto fail;
}
err = nvs_set_u8(handle, "theme", (uint8_t)settings->theme);
if (err != ESP_OK) {
goto fail;
}
err = nvs_set_u8(handle, "spread", (uint8_t)settings->selected_spread);
if (err != ESP_OK) {
goto fail;
}
err = nvs_commit(handle);
if (err != ESP_OK) {
goto fail;
}
s_settings = *settings;
nvs_close(handle);
return ESP_OK;
fail:
ESP_LOGE(TAG, "settings_save failed: %s", esp_err_to_name(err));
nvs_close(handle);
return err;
}
const char *settings_theme_name(tarot_theme_t theme)
{
switch (theme) {
case TAROT_THEME_LIGHT:
return "Light";
case TAROT_THEME_MYSTIC:
return "Mystic";
case TAROT_THEME_DARK:
default:
return "Dark";
}
}

35
main/settings.h Normal file
View File

@@ -0,0 +1,35 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include "esp_err.h"
#include "tarot_logic.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
TAROT_THEME_DARK = 0,
TAROT_THEME_LIGHT,
TAROT_THEME_MYSTIC,
} tarot_theme_t;
typedef struct {
bool reversed_cards_enabled;
bool save_readings_enabled;
uint8_t brightness;
tarot_theme_t theme;
tarot_spread_t selected_spread;
} tarot_settings_t;
esp_err_t settings_init(void);
tarot_settings_t settings_get(void);
esp_err_t settings_save(const tarot_settings_t *settings);
void settings_set_defaults(tarot_settings_t *settings);
const char *settings_theme_name(tarot_theme_t theme);
#ifdef __cplusplus
}
#endif

116
main/tarot_deck.c Normal file
View File

@@ -0,0 +1,116 @@
#include "tarot_deck.h"
const tarot_card_t tarot_deck[TAROT_DECK_SIZE] = {
{0, "The Fool", "00_fool.jpg", TAROT_ARCANA_MAJOR, TAROT_SUIT_NONE, "Beginnings, trust, openness", "Recklessness, delay, poor judgment"},
{1, "The Magician", "01_magician.jpg", TAROT_ARCANA_MAJOR, TAROT_SUIT_NONE, "Willpower, skill, focused action", "Manipulation, scattered energy, unused talent"},
{2, "The High Priestess", "02_high_priestess.jpg", TAROT_ARCANA_MAJOR, TAROT_SUIT_NONE, "Intuition, mystery, inner knowing", "Secrets, disconnection, blocked intuition"},
{3, "The Empress", "03_empress.jpg", TAROT_ARCANA_MAJOR, TAROT_SUIT_NONE, "Nurture, abundance, creation", "Dependence, neglect, creative block"},
{4, "The Emperor", "04_emperor.jpg", TAROT_ARCANA_MAJOR, TAROT_SUIT_NONE, "Structure, authority, protection", "Rigidity, control, instability"},
{5, "The Hierophant", "05_hierophant.jpg", TAROT_ARCANA_MAJOR, TAROT_SUIT_NONE, "Tradition, learning, guidance", "Restriction, conformity, stale beliefs"},
{6, "The Lovers", "06_lovers.jpg", TAROT_ARCANA_MAJOR, TAROT_SUIT_NONE, "Choice, union, values alignment", "Disharmony, avoidance, misalignment"},
{7, "The Chariot", "07_chariot.jpg", TAROT_ARCANA_MAJOR, TAROT_SUIT_NONE, "Drive, control, victory", "Aggression, drift, lack of direction"},
{8, "Strength", "08_strength.jpg", TAROT_ARCANA_MAJOR, TAROT_SUIT_NONE, "Courage, patience, inner power", "Self-doubt, force, low confidence"},
{9, "The Hermit", "09_hermit.jpg", TAROT_ARCANA_MAJOR, TAROT_SUIT_NONE, "Reflection, solitude, wisdom", "Isolation, withdrawal, lost guidance"},
{10, "Wheel of Fortune", "10_wheel_of_fortune.jpg", TAROT_ARCANA_MAJOR, TAROT_SUIT_NONE, "Cycles, fate, turning point", "Resistance, setbacks, repeated patterns"},
{11, "Justice", "11_justice.jpg", TAROT_ARCANA_MAJOR, TAROT_SUIT_NONE, "Truth, balance, accountability", "Bias, dishonesty, unfair outcome"},
{12, "The Hanged Man", "12_hanged_man.jpg", TAROT_ARCANA_MAJOR, TAROT_SUIT_NONE, "Pause, surrender, new perspective", "Stalling, resistance, needless sacrifice"},
{13, "Death", "13_death.jpg", TAROT_ARCANA_MAJOR, TAROT_SUIT_NONE, "Ending, release, transformation", "Clinging, stagnation, fear of change"},
{14, "Temperance", "14_temperance.jpg", TAROT_ARCANA_MAJOR, TAROT_SUIT_NONE, "Moderation, healing, integration", "Excess, imbalance, impatience"},
{15, "The Devil", "15_devil.jpg", TAROT_ARCANA_MAJOR, TAROT_SUIT_NONE, "Attachment, temptation, shadow", "Release, awareness, breaking chains"},
{16, "The Tower", "16_tower.jpg", TAROT_ARCANA_MAJOR, TAROT_SUIT_NONE, "Disruption, truth, sudden change", "Avoided collapse, fear, delayed truth"},
{17, "The Star", "17_star.jpg", TAROT_ARCANA_MAJOR, TAROT_SUIT_NONE, "Hope, renewal, spiritual clarity", "Discouragement, depletion, lost faith"},
{18, "The Moon", "18_moon.jpg", TAROT_ARCANA_MAJOR, TAROT_SUIT_NONE, "Dreams, illusion, subconscious", "Confusion lifting, denial, anxiety"},
{19, "The Sun", "19_sun.jpg", TAROT_ARCANA_MAJOR, TAROT_SUIT_NONE, "Joy, success, vitality", "Doubt, muted joy, temporary delay"},
{20, "Judgement", "20_judgement.jpg", TAROT_ARCANA_MAJOR, TAROT_SUIT_NONE, "Awakening, reckoning, renewal", "Self-judgment, avoidance, unfinished lesson"},
{21, "The World", "21_world.jpg", TAROT_ARCANA_MAJOR, TAROT_SUIT_NONE, "Completion, mastery, integration", "Incomplete cycle, delay, loose ends"},
{22, "Ace of Wands", "wands_ace.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_WANDS, "Inspiration, spark, potential", "Delay, burnout, missed start"},
{23, "Two of Wands", "wands_02.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_WANDS, "Planning, vision, decision", "Fear, small thinking, indecision"},
{24, "Three of Wands", "wands_03.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_WANDS, "Expansion, foresight, progress", "Obstacles, limited reach, impatience"},
{25, "Four of Wands", "wands_04.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_WANDS, "Celebration, home, stability", "Tension at home, private milestone, instability"},
{26, "Five of Wands", "wands_05.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_WANDS, "Conflict, competition, friction", "Resolution, avoidance, inner conflict"},
{27, "Six of Wands", "wands_06.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_WANDS, "Recognition, victory, confidence", "Ego, self-doubt, unseen effort"},
{28, "Seven of Wands", "wands_07.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_WANDS, "Defense, courage, standing ground", "Overwhelm, giving up, weak boundaries"},
{29, "Eight of Wands", "wands_08.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_WANDS, "Momentum, messages, swift change", "Delays, chaos, miscommunication"},
{30, "Nine of Wands", "wands_09.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_WANDS, "Resilience, caution, endurance", "Exhaustion, paranoia, defensiveness"},
{31, "Ten of Wands", "wands_10.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_WANDS, "Burden, responsibility, hard work", "Release, collapse, poor delegation"},
{32, "Page of Wands", "wands_page.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_WANDS, "Curiosity, discovery, enthusiasm", "Restlessness, immaturity, false starts"},
{33, "Knight of Wands", "wands_knight.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_WANDS, "Adventure, speed, bold action", "Impulsiveness, anger, scattered force"},
{34, "Queen of Wands", "wands_queen.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_WANDS, "Confidence, warmth, charisma", "Jealousy, insecurity, demanding energy"},
{35, "King of Wands", "wands_king.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_WANDS, "Leadership, vision, command", "Domination, haste, poor follow-through"},
{36, "Ace of Cups", "cups_ace.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_CUPS, "Love, openness, emotional renewal", "Blocked emotion, emptiness, guarded heart"},
{37, "Two of Cups", "cups_02.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_CUPS, "Partnership, attraction, mutual care", "Separation, imbalance, strained bond"},
{38, "Three of Cups", "cups_03.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_CUPS, "Friendship, support, celebration", "Gossip, overindulgence, isolation"},
{39, "Four of Cups", "cups_04.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_CUPS, "Apathy, contemplation, reevaluation", "New interest, acceptance, emotional movement"},
{40, "Five of Cups", "cups_05.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_CUPS, "Loss, regret, disappointment", "Recovery, forgiveness, perspective"},
{41, "Six of Cups", "cups_06.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_CUPS, "Nostalgia, innocence, kindness", "Stuck in past, naivety, moving on"},
{42, "Seven of Cups", "cups_07.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_CUPS, "Options, fantasy, imagination", "Clarity, choice, illusion exposed"},
{43, "Eight of Cups", "cups_08.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_CUPS, "Departure, search, emotional growth", "Avoidance, fear of leaving, drifting"},
{44, "Nine of Cups", "cups_09.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_CUPS, "Contentment, wish fulfilled, pleasure", "Dissatisfaction, excess, hollow success"},
{45, "Ten of Cups", "cups_10.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_CUPS, "Harmony, family, lasting joy", "Disconnection, unrealistic ideal, tension"},
{46, "Page of Cups", "cups_page.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_CUPS, "Sensitivity, message, creative feeling", "Emotional immaturity, insecurity, blocked art"},
{47, "Knight of Cups", "cups_knight.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_CUPS, "Romance, invitation, idealism", "Moodiness, fantasy, empty promise"},
{48, "Queen of Cups", "cups_queen.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_CUPS, "Compassion, intuition, emotional depth", "Overwhelm, codependence, poor boundaries"},
{49, "King of Cups", "cups_king.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_CUPS, "Calm, wisdom, emotional maturity", "Suppression, manipulation, volatility"},
{50, "Ace of Swords", "swords_ace.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_SWORDS, "Truth, clarity, breakthrough", "Confusion, harsh words, clouded judgment"},
{51, "Two of Swords", "swords_02.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_SWORDS, "Stalemate, decision, guarded peace", "Indecision, information overload, avoidance"},
{52, "Three of Swords", "swords_03.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_SWORDS, "Heartbreak, grief, painful truth", "Healing, release, lingering pain"},
{53, "Four of Swords", "swords_04.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_SWORDS, "Rest, recovery, stillness", "Burnout, unrest, forced pause"},
{54, "Five of Swords", "swords_05.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_SWORDS, "Conflict, defeat, hollow victory", "Reconciliation, regret, repeated conflict"},
{55, "Six of Swords", "swords_06.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_SWORDS, "Transition, passage, recovery", "Resistance, baggage, delayed move"},
{56, "Seven of Swords", "swords_07.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_SWORDS, "Strategy, secrecy, self-preservation", "Exposure, confession, poor tactics"},
{57, "Eight of Swords", "swords_08.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_SWORDS, "Restriction, fear, mental trap", "Freedom, new perspective, self-belief"},
{58, "Nine of Swords", "swords_09.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_SWORDS, "Anxiety, worry, sleeplessness", "Support, release, facing fear"},
{59, "Ten of Swords", "swords_10.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_SWORDS, "Ending, betrayal, rock bottom", "Recovery, survival, inevitable closure"},
{60, "Page of Swords", "swords_page.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_SWORDS, "Curiosity, vigilance, new ideas", "Gossip, haste, defensive thinking"},
{61, "Knight of Swords", "swords_knight.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_SWORDS, "Ambition, directness, fast thought", "Recklessness, aggression, tunnel vision"},
{62, "Queen of Swords", "swords_queen.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_SWORDS, "Honesty, discernment, independence", "Coldness, bitterness, severe judgment"},
{63, "King of Swords", "swords_king.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_SWORDS, "Logic, authority, clear standards", "Cruelty, misuse of power, rigidity"},
{64, "Ace of Pentacles", "pentacles_ace.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_PENTACLES, "Opportunity, prosperity, seed", "Missed chance, scarcity, weak foundation"},
{65, "Two of Pentacles", "pentacles_02.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_PENTACLES, "Balance, priorities, adaptation", "Overload, disorganization, imbalance"},
{66, "Three of Pentacles", "pentacles_03.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_PENTACLES, "Teamwork, craft, learning", "Poor collaboration, low standards, misalignment"},
{67, "Four of Pentacles", "pentacles_04.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_PENTACLES, "Security, control, saving", "Greed, fear, loosening grip"},
{68, "Five of Pentacles", "pentacles_05.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_PENTACLES, "Hardship, exclusion, need", "Help, recovery, improved resources"},
{69, "Six of Pentacles", "pentacles_06.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_PENTACLES, "Generosity, exchange, support", "Debt, strings attached, imbalance"},
{70, "Seven of Pentacles", "pentacles_07.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_PENTACLES, "Patience, investment, assessment", "Impatience, wasted effort, poor results"},
{71, "Eight of Pentacles", "pentacles_08.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_PENTACLES, "Skill, discipline, apprenticeship", "Perfectionism, shortcuts, dull routine"},
{72, "Nine of Pentacles", "pentacles_09.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_PENTACLES, "Independence, luxury, self-sufficiency", "Dependence, overspending, insecurity"},
{73, "Ten of Pentacles", "pentacles_10.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_PENTACLES, "Legacy, wealth, family structure", "Instability, family conflict, short-term thinking"},
{74, "Page of Pentacles", "pentacles_page.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_PENTACLES, "Study, ambition, practical start", "Laziness, distraction, poor planning"},
{75, "Knight of Pentacles", "pentacles_knight.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_PENTACLES, "Reliability, patience, steady work", "Stagnation, stubbornness, dullness"},
{76, "Queen of Pentacles", "pentacles_queen.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_PENTACLES, "Care, practicality, resourcefulness", "Neglect, smothering, material anxiety"},
{77, "King of Pentacles", "pentacles_king.jpg", TAROT_ARCANA_MINOR, TAROT_SUIT_PENTACLES, "Stability, success, wise stewardship", "Greed, rigidity, status obsession"},
};
const tarot_card_t *tarot_deck_get_by_id(uint8_t id)
{
if (id >= TAROT_DECK_SIZE) {
return NULL;
}
return &tarot_deck[id];
}
const char *tarot_arcana_name(tarot_arcana_t arcana)
{
return arcana == TAROT_ARCANA_MAJOR ? "Major Arcana" : "Minor Arcana";
}
const char *tarot_suit_name(tarot_suit_t suit)
{
switch (suit) {
case TAROT_SUIT_WANDS:
return "Wands";
case TAROT_SUIT_CUPS:
return "Cups";
case TAROT_SUIT_SWORDS:
return "Swords";
case TAROT_SUIT_PENTACLES:
return "Pentacles";
case TAROT_SUIT_NONE:
default:
return "";
}
}

44
main/tarot_deck.h Normal file
View File

@@ -0,0 +1,44 @@
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define TAROT_DECK_SIZE 78
typedef enum {
TAROT_ARCANA_MAJOR = 0,
TAROT_ARCANA_MINOR,
} tarot_arcana_t;
typedef enum {
TAROT_SUIT_NONE = 0,
TAROT_SUIT_WANDS,
TAROT_SUIT_CUPS,
TAROT_SUIT_SWORDS,
TAROT_SUIT_PENTACLES,
} tarot_suit_t;
typedef struct {
uint8_t id;
const char *name;
const char *image_filename;
tarot_arcana_t arcana;
tarot_suit_t suit;
const char *upright_meaning;
const char *reversed_meaning;
} tarot_card_t;
extern const tarot_card_t tarot_deck[TAROT_DECK_SIZE];
const tarot_card_t *tarot_deck_get_by_id(uint8_t id);
const char *tarot_arcana_name(tarot_arcana_t arcana);
const char *tarot_suit_name(tarot_suit_t suit);
#ifdef __cplusplus
}
#endif

71
main/tarot_logic.c Normal file
View File

@@ -0,0 +1,71 @@
#include "tarot_logic.h"
#include <string.h>
#include "esp_random.h"
static const char *const s_spread_3[] = {
"Past",
"Present",
"Future",
};
static const char *const s_spread_8[] = {
"Current energy",
"Challenge",
"Hidden influence",
"Past root",
"Near future",
"Advice",
"External energy",
"Outcome",
};
const char *tarot_spread_name(tarot_spread_t spread)
{
switch (spread) {
case TAROT_SPREAD_3_CARD:
return "3-card";
case TAROT_SPREAD_8_CARD:
return "8-card";
default:
return "Unknown";
}
}
const char *tarot_spread_position_meaning(tarot_spread_t spread, size_t index)
{
if (spread == TAROT_SPREAD_3_CARD && index < 3) {
return s_spread_3[index];
}
if (spread == TAROT_SPREAD_8_CARD && index < 8) {
return s_spread_8[index];
}
return "";
}
void tarot_logic_draw_reading(tarot_reading_t *reading, tarot_spread_t spread, bool reversed_enabled)
{
bool used[TAROT_DECK_SIZE] = { false };
size_t count = spread == TAROT_SPREAD_3_CARD ? 3 : 8;
if (reading == NULL) {
return;
}
memset(reading, 0, sizeof(*reading));
reading->spread = spread;
reading->count = count;
for (size_t i = 0; i < count; i++) {
uint8_t card_id = 0;
do {
card_id = esp_random() % TAROT_DECK_SIZE;
} while (used[card_id]);
used[card_id] = true;
reading->cards[i].card = &tarot_deck[card_id];
reading->cards[i].reversed = reversed_enabled ? ((esp_random() & 1U) == 1U) : false;
reading->cards[i].position_meaning = tarot_spread_position_meaning(spread, i);
}
}

38
main/tarot_logic.h Normal file
View File

@@ -0,0 +1,38 @@
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "tarot_deck.h"
#ifdef __cplusplus
extern "C" {
#endif
#define TAROT_MAX_SPREAD_CARDS 8
typedef enum {
TAROT_SPREAD_3_CARD = 3,
TAROT_SPREAD_8_CARD = 8,
} tarot_spread_t;
typedef struct {
const tarot_card_t *card;
bool reversed;
const char *position_meaning;
} tarot_drawn_card_t;
typedef struct {
tarot_spread_t spread;
size_t count;
tarot_drawn_card_t cards[TAROT_MAX_SPREAD_CARDS];
} tarot_reading_t;
const char *tarot_spread_name(tarot_spread_t spread);
const char *tarot_spread_position_meaning(tarot_spread_t spread, size_t index);
void tarot_logic_draw_reading(tarot_reading_t *reading, tarot_spread_t spread, bool reversed_enabled);
#ifdef __cplusplus
}
#endif

215
main/ui_main.c Normal file
View File

@@ -0,0 +1,215 @@
#include "ui_main.h"
#include <stdio.h>
#include "esp_log.h"
#include "lvgl.h"
#include "card_assets.h"
#include "settings.h"
#include "tarot_logic.h"
#include "ui_settings.h"
static const char *TAG = "ui_main";
static lv_style_t s_style_screen;
static lv_style_t s_style_title;
static lv_style_t s_style_button;
static lv_style_t s_style_card;
static lv_style_t s_style_muted;
static bool s_styles_ready;
static tarot_reading_t s_current_reading;
static void show_reading_cb(lv_event_t *event);
static void show_home_cb(lv_event_t *event);
static void show_settings_cb(lv_event_t *event);
static lv_obj_t *create_button(lv_obj_t *parent, const char *text, lv_event_cb_t cb)
{
lv_obj_t *button = lv_btn_create(parent);
lv_obj_add_style(button, &s_style_button, 0);
lv_obj_set_size(button, 210, 54);
lv_obj_add_event_cb(button, cb, LV_EVENT_CLICKED, NULL);
lv_obj_t *label = lv_label_create(button);
lv_label_set_text(label, text);
lv_obj_center(label);
return button;
}
void ui_apply_current_theme(void)
{
tarot_settings_t settings = settings_get();
lv_color_t bg = lv_color_hex(0x111018);
lv_color_t fg = lv_color_hex(0xf5efe6);
lv_color_t card = lv_color_hex(0x201827);
lv_color_t accent = lv_color_hex(0x8d5cf6);
if (settings.theme == TAROT_THEME_LIGHT) {
bg = lv_color_hex(0xf7f0e6);
fg = lv_color_hex(0x221b18);
card = lv_color_hex(0xffffff);
accent = lv_color_hex(0x805b2f);
} else if (settings.theme == TAROT_THEME_MYSTIC) {
bg = lv_color_hex(0x080b19);
fg = lv_color_hex(0xf1e8ff);
card = lv_color_hex(0x151b36);
accent = lv_color_hex(0xc39bff);
}
if (!s_styles_ready) {
lv_style_init(&s_style_screen);
lv_style_init(&s_style_title);
lv_style_init(&s_style_button);
lv_style_init(&s_style_card);
lv_style_init(&s_style_muted);
s_styles_ready = true;
}
lv_style_reset(&s_style_screen);
lv_style_set_bg_color(&s_style_screen, bg);
lv_style_set_text_color(&s_style_screen, fg);
lv_style_set_pad_all(&s_style_screen, 18);
lv_style_reset(&s_style_title);
lv_style_set_text_color(&s_style_title, fg);
lv_style_set_text_font(&s_style_title, LV_FONT_DEFAULT);
lv_style_reset(&s_style_button);
lv_style_set_bg_color(&s_style_button, accent);
lv_style_set_text_color(&s_style_button, lv_color_white());
lv_style_set_radius(&s_style_button, 8);
lv_style_reset(&s_style_card);
lv_style_set_bg_color(&s_style_card, card);
lv_style_set_text_color(&s_style_card, fg);
lv_style_set_radius(&s_style_card, 8);
lv_style_set_pad_all(&s_style_card, 10);
lv_style_set_border_width(&s_style_card, 1);
lv_style_set_border_color(&s_style_card, accent);
lv_style_reset(&s_style_muted);
lv_style_set_text_color(&s_style_muted, lv_color_hex(0xaaa2b8));
}
void ui_main_init(void)
{
ui_apply_current_theme();
}
void ui_show_home(void)
{
ui_apply_current_theme();
lv_obj_t *screen = lv_obj_create(NULL);
lv_obj_add_style(screen, &s_style_screen, 0);
lv_obj_set_flex_flow(screen, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(screen, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_t *title = lv_label_create(screen);
lv_obj_add_style(title, &s_style_title, 0);
lv_label_set_text(title, "ESP32-P4 Tarot");
lv_obj_set_style_text_font(title, LV_FONT_DEFAULT, 0);
lv_obj_t *subtitle = lv_label_create(screen);
lv_obj_add_style(subtitle, &s_style_muted, 0);
lv_label_set_text(subtitle, "On-device card art ready");
create_button(screen, "Draw Cards", show_reading_cb);
create_button(screen, "Settings", show_settings_cb);
lv_scr_load(screen);
}
static void add_card_tile(lv_obj_t *grid, const tarot_drawn_card_t *drawn, size_t index)
{
const tarot_card_t *card = drawn->card;
lv_obj_t *tile = lv_obj_create(grid);
lv_obj_add_style(tile, &s_style_card, 0);
lv_obj_set_size(tile, 230, 340);
lv_obj_set_flex_flow(tile, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(tile, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_t *position = lv_label_create(tile);
lv_label_set_text_fmt(position, "%u. %s", (unsigned)(index + 1), drawn->position_meaning);
lv_label_set_long_mode(position, LV_LABEL_LONG_WRAP);
lv_obj_set_width(position, 205);
card_assets_create_card_art(tile, card, drawn->reversed);
lv_obj_t *name = lv_label_create(tile);
lv_label_set_text(name, card->name);
lv_label_set_long_mode(name, LV_LABEL_LONG_WRAP);
lv_obj_set_width(name, 205);
lv_obj_t *orientation = lv_label_create(tile);
lv_obj_add_style(orientation, &s_style_muted, 0);
lv_label_set_text(orientation, drawn->reversed ? "Reversed" : "Upright");
lv_obj_t *meaning = lv_label_create(tile);
lv_label_set_text(meaning, drawn->reversed ? card->reversed_meaning : card->upright_meaning);
lv_label_set_long_mode(meaning, LV_LABEL_LONG_WRAP);
lv_obj_set_width(meaning, 205);
}
void ui_show_reading(void)
{
tarot_settings_t settings = settings_get();
tarot_logic_draw_reading(&s_current_reading, settings.selected_spread, settings.reversed_cards_enabled);
ESP_LOGI(TAG, "Drew %u unique tarot cards", (unsigned)s_current_reading.count);
lv_obj_t *screen = lv_obj_create(NULL);
lv_obj_add_style(screen, &s_style_screen, 0);
lv_obj_set_flex_flow(screen, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(screen, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_t *header = lv_obj_create(screen);
lv_obj_set_width(header, LV_PCT(100));
lv_obj_set_height(header, LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(header, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(header, 0, 0);
lv_obj_set_flex_flow(header, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(header, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_t *title = lv_label_create(header);
lv_label_set_text_fmt(title, "%s Reading", tarot_spread_name(s_current_reading.spread));
lv_obj_t *actions = lv_obj_create(header);
lv_obj_set_style_bg_opa(actions, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(actions, 0, 0);
lv_obj_set_size(actions, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_flex_flow(actions, LV_FLEX_FLOW_ROW);
create_button(actions, "New Reading", show_reading_cb);
create_button(actions, "Back Home", show_home_cb);
lv_obj_t *grid = lv_obj_create(screen);
lv_obj_set_width(grid, LV_PCT(100));
lv_obj_set_flex_grow(grid, 1);
lv_obj_set_style_bg_opa(grid, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(grid, 0, 0);
lv_obj_set_flex_flow(grid, LV_FLEX_FLOW_ROW_WRAP);
lv_obj_set_flex_align(grid, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
for (size_t i = 0; i < s_current_reading.count; i++) {
add_card_tile(grid, &s_current_reading.cards[i], i);
}
lv_scr_load(screen);
}
static void show_reading_cb(lv_event_t *event)
{
(void)event;
ui_show_reading();
}
static void show_home_cb(lv_event_t *event)
{
(void)event;
ui_show_home();
}
static void show_settings_cb(lv_event_t *event)
{
(void)event;
ui_show_settings();
}

16
main/ui_main.h Normal file
View File

@@ -0,0 +1,16 @@
#pragma once
#include "lvgl.h"
#ifdef __cplusplus
extern "C" {
#endif
void ui_main_init(void);
void ui_show_home(void);
void ui_show_reading(void);
void ui_apply_current_theme(void);
#ifdef __cplusplus
}
#endif

151
main/ui_settings.c Normal file
View File

@@ -0,0 +1,151 @@
#include "ui_settings.h"
#include <stdint.h>
#include "bsp/esp-bsp.h"
#include "esp_log.h"
#include "lvgl.h"
#include "settings.h"
#include "ui_main.h"
static const char *TAG = "ui_settings";
static tarot_settings_t s_edit_settings;
static void back_cb(lv_event_t *event);
static void reversed_cb(lv_event_t *event);
static void save_readings_cb(lv_event_t *event);
static void brightness_cb(lv_event_t *event);
static void theme_cb(lv_event_t *event);
static void spread_cb(lv_event_t *event);
static void board_set_brightness(uint8_t brightness)
{
esp_err_t err = bsp_display_brightness_set(brightness);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to set brightness: %s", esp_err_to_name(err));
}
}
static lv_obj_t *add_row(lv_obj_t *parent, const char *label_text)
{
lv_obj_t *row = lv_obj_create(parent);
lv_obj_set_width(row, LV_PCT(100));
lv_obj_set_height(row, LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(row, 0, 0);
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_t *label = lv_label_create(row);
lv_label_set_text(label, label_text);
return row;
}
static void save_current_settings(void)
{
esp_err_t err = settings_save(&s_edit_settings);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to save settings: %s", esp_err_to_name(err));
}
}
void ui_show_settings(void)
{
s_edit_settings = settings_get();
ui_apply_current_theme();
lv_obj_t *screen = lv_obj_create(NULL);
lv_obj_set_style_pad_all(screen, 18, 0);
lv_obj_set_flex_flow(screen, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(screen, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_t *title = lv_label_create(screen);
lv_label_set_text(title, "Settings");
lv_obj_t *row = add_row(screen, "Reversed cards");
lv_obj_t *sw = lv_switch_create(row);
if (s_edit_settings.reversed_cards_enabled) {
lv_obj_add_state(sw, LV_STATE_CHECKED);
}
lv_obj_add_event_cb(sw, reversed_cb, LV_EVENT_VALUE_CHANGED, NULL);
row = add_row(screen, "Save readings");
sw = lv_switch_create(row);
if (s_edit_settings.save_readings_enabled) {
lv_obj_add_state(sw, LV_STATE_CHECKED);
}
lv_obj_add_event_cb(sw, save_readings_cb, LV_EVENT_VALUE_CHANGED, NULL);
row = add_row(screen, "Brightness");
lv_obj_t *slider = lv_slider_create(row);
lv_obj_set_width(slider, 190);
lv_slider_set_range(slider, 10, 100);
lv_slider_set_value(slider, s_edit_settings.brightness, LV_ANIM_OFF);
lv_obj_add_event_cb(slider, brightness_cb, LV_EVENT_VALUE_CHANGED, NULL);
row = add_row(screen, "Theme");
lv_obj_t *theme = lv_dropdown_create(row);
lv_dropdown_set_options(theme, "Dark\nLight\nMystic");
lv_dropdown_set_selected(theme, s_edit_settings.theme);
lv_obj_set_width(theme, 170);
lv_obj_add_event_cb(theme, theme_cb, LV_EVENT_VALUE_CHANGED, NULL);
row = add_row(screen, "Spread");
lv_obj_t *spread = lv_dropdown_create(row);
lv_dropdown_set_options(spread, "3-card\n8-card");
lv_dropdown_set_selected(spread, s_edit_settings.selected_spread == TAROT_SPREAD_3_CARD ? 0 : 1);
lv_obj_set_width(spread, 170);
lv_obj_add_event_cb(spread, spread_cb, LV_EVENT_VALUE_CHANGED, NULL);
lv_obj_t *back = lv_btn_create(screen);
lv_obj_set_size(back, 190, 52);
lv_obj_add_event_cb(back, back_cb, LV_EVENT_CLICKED, NULL);
lv_obj_t *back_label = lv_label_create(back);
lv_label_set_text(back_label, "Back");
lv_obj_center(back_label);
lv_scr_load(screen);
}
static void reversed_cb(lv_event_t *event)
{
lv_obj_t *target = lv_event_get_target(event);
s_edit_settings.reversed_cards_enabled = lv_obj_has_state(target, LV_STATE_CHECKED);
save_current_settings();
}
static void save_readings_cb(lv_event_t *event)
{
lv_obj_t *target = lv_event_get_target(event);
s_edit_settings.save_readings_enabled = lv_obj_has_state(target, LV_STATE_CHECKED);
save_current_settings();
}
static void brightness_cb(lv_event_t *event)
{
lv_obj_t *target = lv_event_get_target(event);
s_edit_settings.brightness = (uint8_t)lv_slider_get_value(target);
board_set_brightness(s_edit_settings.brightness);
save_current_settings();
}
static void theme_cb(lv_event_t *event)
{
lv_obj_t *target = lv_event_get_target(event);
s_edit_settings.theme = (tarot_theme_t)lv_dropdown_get_selected(target);
save_current_settings();
}
static void spread_cb(lv_event_t *event)
{
lv_obj_t *target = lv_event_get_target(event);
s_edit_settings.selected_spread = lv_dropdown_get_selected(target) == 0 ? TAROT_SPREAD_3_CARD : TAROT_SPREAD_8_CARD;
save_current_settings();
}
static void back_cb(lv_event_t *event)
{
(void)event;
ui_show_home();
}

13
main/ui_settings.h Normal file
View File

@@ -0,0 +1,13 @@
#pragma once
#include "lvgl.h"
#ifdef __cplusplus
extern "C" {
#endif
void ui_show_settings(void);
#ifdef __cplusplus
}
#endif