commit fb4753c1f7514c5c033d66b607e4cb9bd5b64dce Author: drjones Date: Tue Jun 23 06:03:19 2026 -0700 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f3fae2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +build/ +managed_components/ +sdkconfig +sdkconfig.old +dependencies.lock diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..b6d96e6 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,4 @@ +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(esp32p4_tarot VERSION 0.1.0) diff --git a/README.md b/README.md new file mode 100644 index 0000000..fa7b19b --- /dev/null +++ b/README.md @@ -0,0 +1,195 @@ +# ESP32-P4 Tarot Touchscreen App + +ESP-IDF + LVGL tarot reading app for the Waveshare `ESP32-P4-86-Panel-ETH-2RO`. The first version is intentionally simple and stable: it draws unique tarot cards, renders card art on-device, persists settings in NVS, and does not require a microSD card. + +## What Is Included + +- ESP-IDF project scaffold +- LVGL home, reading, and settings screens +- Full 78-card tarot deck data structure +- 3-card and 8-card spread support +- `esp_random()` card drawing with duplicate prevention +- Upright and reversed cards +- On-device generated card art +- NVS-backed settings +- Waveshare BSP display, touch, and backlight integration + +## Project Layout + +```text +main/ + app_main.c + card_assets.c + card_assets.h + tarot_deck.c + tarot_deck.h + tarot_logic.c + tarot_logic.h + ui_main.c + ui_main.h + ui_settings.c + ui_settings.h + settings.c + settings.h +``` + +## Target Hardware + +This project targets the Waveshare `ESP32-P4-86-Panel-ETH-2RO`, part of the ESP32-P4 4-inch 86-panel family. + +Hardware assumptions: + +- 4-inch 720 x 720 IPS display +- MIPI-DSI display interface +- ST7703 display driver through Waveshare BSP +- GT911 capacitive touch +- microSD/TF slot over SDMMC/SDIO, currently not required by this app +- Onboard 32 MB flash and 32 MB PSRAM +- Ethernet, RS485, and 2 relay outputs present but not used by this app yet + +The board support is handled through the Waveshare ESP-IDF BSP component: + +```yaml +waveshare/esp32_p4_wifi6_touch_lcd_4b +``` + +The BSP initializes the MIPI display, GT911 touch, LVGL port, and backlight PWM. + +## Build + +Install and export ESP-IDF first: + +```sh +. $IDF_PATH/export.sh +idf.py set-target esp32p4 +idf.py menuconfig +idf.py build +``` + +This project declares LVGL through `main/idf_component.yml`, so ESP-IDF's component manager will fetch it during build. + +The Waveshare BSP is also declared there. It provides the display, touch, and backlight hardware integration for this panel. + +## Flash And Monitor + +```sh +idf.py -p /dev/tty.usbmodemXXXX flash monitor +``` + +Replace the serial port with your board's port. + +## On-Device Card Art + +No SD card is required. `main/card_assets.c` renders a compact tarot card visual directly with LVGL widgets using deck metadata compiled into firmware. + +This is deliberate for the first device build: + +- No filesystem dependency +- No JPEG decoder dependency +- No missing-image crash path +- Fast startup on the panel + +The deck still keeps a filename-style asset key for each card. When you want full illustrated embedded artwork, replace `card_assets_create_card_art()` with a lookup table from card id to embedded LVGL image descriptors. + +## Embedded Asset Naming Convention + +Major arcana: + +```text +00_fool.jpg +01_magician.jpg +02_high_priestess.jpg +03_empress.jpg +04_emperor.jpg +05_hierophant.jpg +06_lovers.jpg +07_chariot.jpg +08_strength.jpg +09_hermit.jpg +10_wheel_of_fortune.jpg +11_justice.jpg +12_hanged_man.jpg +13_death.jpg +14_temperance.jpg +15_devil.jpg +16_tower.jpg +17_star.jpg +18_moon.jpg +19_sun.jpg +20_judgement.jpg +21_world.jpg +``` + +Minor arcana: + +```text +wands_ace.jpg +wands_02.jpg +wands_03.jpg +wands_04.jpg +wands_05.jpg +wands_06.jpg +wands_07.jpg +wands_08.jpg +wands_09.jpg +wands_10.jpg +wands_page.jpg +wands_knight.jpg +wands_queen.jpg +wands_king.jpg + +cups_ace.jpg +cups_02.jpg +cups_03.jpg +cups_04.jpg +cups_05.jpg +cups_06.jpg +cups_07.jpg +cups_08.jpg +cups_09.jpg +cups_10.jpg +cups_page.jpg +cups_knight.jpg +cups_queen.jpg +cups_king.jpg + +swords_ace.jpg +swords_02.jpg +swords_03.jpg +swords_04.jpg +swords_05.jpg +swords_06.jpg +swords_07.jpg +swords_08.jpg +swords_09.jpg +swords_10.jpg +swords_page.jpg +swords_knight.jpg +swords_queen.jpg +swords_king.jpg + +pentacles_ace.jpg +pentacles_02.jpg +pentacles_03.jpg +pentacles_04.jpg +pentacles_05.jpg +pentacles_06.jpg +pentacles_07.jpg +pentacles_08.jpg +pentacles_09.jpg +pentacles_10.jpg +pentacles_page.jpg +pentacles_knight.jpg +pentacles_queen.jpg +pentacles_king.jpg +``` + +## Settings Saved In NVS + +- `reversed_cards_enabled` +- `save_readings_enabled` +- `brightness` +- `theme` +- `selected_spread` + +The `save_readings_enabled` flag is persisted and exposed in UI. Actual reading history storage is intentionally left for the next version so this first build stays stable. diff --git a/components/esp32_p4_wifi6_touch_lcd_4b/CMakeLists.txt b/components/esp32_p4_wifi6_touch_lcd_4b/CMakeLists.txt new file mode 100644 index 0000000..28e6310 --- /dev/null +++ b/components/esp32_p4_wifi6_touch_lcd_4b/CMakeLists.txt @@ -0,0 +1,8 @@ + +idf_component_register( + SRCS "esp32_p4_wifi6_touch_lcd_4b.c" + INCLUDE_DIRS "include" + PRIV_INCLUDE_DIRS "priv_include" + REQUIRES driver + PRIV_REQUIRES esp_lcd usb spiffs fatfs +) diff --git a/components/esp32_p4_wifi6_touch_lcd_4b/Kconfig b/components/esp32_p4_wifi6_touch_lcd_4b/Kconfig new file mode 100644 index 0000000..6c5be4e --- /dev/null +++ b/components/esp32_p4_wifi6_touch_lcd_4b/Kconfig @@ -0,0 +1,140 @@ +menu "Board Support Package(ESP32-P4)" + + config BSP_ERROR_CHECK + bool "Enable error check in BSP" + default y + help + Error check assert the application before returning the error code. + + menu "I2C" + config BSP_I2C_NUM + int "I2C peripheral index" + default 1 + range 0 1 + help + ESP32P4 has two I2C peripherals, pick the one you want to use. + + config BSP_I2C_FAST_MODE + bool "Enable I2C fast mode" + default y + help + I2C has two speed modes: normal (100kHz) and fast (400kHz). + + config BSP_I2C_CLK_SPEED_HZ + int + default 400000 if BSP_I2C_FAST_MODE + default 100000 + endmenu + + menu "I2S" + config BSP_I2S_NUM + int "I2S peripheral index" + default 1 + range 0 2 + help + ESP32P4 has three I2S peripherals, pick the one you want to use. + endmenu + + menu "uSD card - Virtual File System" + config BSP_SD_FORMAT_ON_MOUNT_FAIL + bool "Format uSD card if mounting fails" + default n + help + The SDMMC host will format (FAT) the uSD card if it fails to mount the filesystem. + + config BSP_SD_MOUNT_POINT + string "uSD card mount point" + default "/sdcard" + help + Mount point of the uSD card in the Virtual File System + + endmenu + + menu "SPIFFS - Virtual File System" + config BSP_SPIFFS_FORMAT_ON_MOUNT_FAIL + bool "Format SPIFFS if mounting fails" + default n + help + Format SPIFFS if it fails to mount the filesystem. + + config BSP_SPIFFS_MOUNT_POINT + string "SPIFFS mount point" + default "/spiffs" + help + Mount point of SPIFFS in the Virtual File System. + + config BSP_SPIFFS_PARTITION_LABEL + string "Partition label of SPIFFS" + default "storage" + help + Partition label which stores SPIFFS. + + config BSP_SPIFFS_MAX_FILES + int "Max files supported for SPIFFS VFS" + default 5 + help + Supported max files for SPIFFS in the Virtual File System. + endmenu + + menu "Display" + config BSP_LCD_DPI_BUFFER_NUMS + int "Set number of frame buffers" + default 1 + range 1 3 + help + Let DPI LCD driver create a specified number of frame-size buffers. Only when it is set to multiple can the avoiding tearing be turned on. + + config BSP_DISPLAY_LVGL_AVOID_TEAR + bool "Avoid tearing effect" + depends on BSP_LCD_DPI_BUFFER_NUMS > 1 + default "n" + help + Avoid tearing effect through LVGL buffer mode and double frame buffers of RGB LCD. This feature is only available for RGB LCD. + + choice BSP_DISPLAY_LVGL_MODE + depends on BSP_DISPLAY_LVGL_AVOID_TEAR + prompt "Select LVGL buffer mode" + default BSP_DISPLAY_LVGL_FULL_REFRESH + config BSP_DISPLAY_LVGL_FULL_REFRESH + bool "Full refresh" + config BSP_DISPLAY_LVGL_DIRECT_MODE + bool "Direct mode" + endchoice + + config BSP_DISPLAY_BRIGHTNESS_LEDC_CH + int "LEDC channel index" + default 1 + range 0 7 + help + LEDC channel is used to generate PWM signal that controls display brightness. + Set LEDC index that should be used. + + choice BSP_LCD_COLOR_FORMAT + prompt "Select LCD color format" + default BSP_LCD_COLOR_FORMAT_RGB565 + help + Select the LCD color format RGB565/RGB888. + + config BSP_LCD_COLOR_FORMAT_RGB565 + bool "RGB565" + config BSP_LCD_COLOR_FORMAT_RGB888 + bool "RGB888" + endchoice + + choice BSP_LCD_TYPE + prompt "Select LCD type" + default BSP_LCD_TYPE_800_800_3_4_INCH + help + Select the LCD. + + config BSP_LCD_TYPE_800_800_3_4_INCH + bool "Waveshare board with 800*800 3.4-inch Display" + config BSP_LCD_TYPE_720_720_4_INCH + bool "Waveshare board with 720*720 4-inch Display" + + endchoice + + + endmenu + +endmenu diff --git a/components/esp32_p4_wifi6_touch_lcd_4b/LICENSE b/components/esp32_p4_wifi6_touch_lcd_4b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/components/esp32_p4_wifi6_touch_lcd_4b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/components/esp32_p4_wifi6_touch_lcd_4b/README.md b/components/esp32_p4_wifi6_touch_lcd_4b/README.md new file mode 100644 index 0000000..9309c5f --- /dev/null +++ b/components/esp32_p4_wifi6_touch_lcd_4b/README.md @@ -0,0 +1,35 @@ +# BSP: Waveshare ESP32-P4-WIFI6-Touch-LCD-4B && ESP32-P4-86-Panel-ETH-2RO + +[![Component Registry](https://components.espressif.com/components/waveshare/esp32_p4_wifi6_touch_lcd_4b/badge.svg)](https://components.espressif.com/components/waveshare/esp32_p4_wifi6_touch_lcd_4b) + +This series of products is based on ESP32-P4 chip high-performance development board. It uses dual-core plus single-core RISC-V architecture design, and a 4-inch 720 × 720 resolution IPS touch display is mounted on the board +| HW version | BSP Version | +| :--------: | :---------: | +| [V1.0](http://www.waveshare.com/wiki/ESP32-P4-WIFI6-Touch-LCD-4B) | ^1 | + + +## BackLight +```c +bsp_display_brightness_init(); + +bsp_display_backlight_on(); + +bsp_display_backlight_off(); + +bsp_display_brightness_set(100); +``` + + +### Capabilities and dependencies +| Capability | Available | Component | Version | +|-------------|------------------|------------------------------------------------------------------------------------------------------------|---------| +| DISPLAY |:heavy_check_mark:| [waveshare/esp_lcd_st7703](https://components.espressif.com/components/waveshare/esp_lcd_st7703) | 1.0.1 | +| LVGL_PORT |:heavy_check_mark:| [espressif/esp_lvgl_port](https://components.espressif.com/components/espressif/esp_lvgl_port) | ^2 | +| TOUCH |:heavy_check_mark:| [espressif/esp_lcd_touch_gt911](https://components.espressif.com/components/espressif/esp_lcd_touch_gt911) | ^1 | +| BUTTONS | :x: | | | +| AUDIO |:heavy_check_mark:| [espressif/esp_codec_dev](https://components.espressif.com/components/espressif/esp_codec_dev) | 1.2.* | +|AUDIO_SPEAKER|:heavy_check_mark:| | | +| AUDIO_MIC |:heavy_check_mark:| | | +| SDCARD |:heavy_check_mark:| idf | >=5.3 | +| IMU | :x: | | | + diff --git a/components/esp32_p4_wifi6_touch_lcd_4b/esp32_p4_wifi6_touch_lcd_4b.c b/components/esp32_p4_wifi6_touch_lcd_4b/esp32_p4_wifi6_touch_lcd_4b.c new file mode 100644 index 0000000..ca5cfbe --- /dev/null +++ b/components/esp32_p4_wifi6_touch_lcd_4b/esp32_p4_wifi6_touch_lcd_4b.c @@ -0,0 +1,678 @@ +#include "sdkconfig.h" +#include "driver/gpio.h" +#include "driver/ledc.h" +#include "esp_err.h" +#include "esp_log.h" +#include "esp_check.h" +#include "esp_spiffs.h" +#include "esp_lcd_panel_ops.h" +#include "esp_lcd_mipi_dsi.h" +#include "esp_ldo_regulator.h" +#include "esp_vfs_fat.h" +#include "usb/usb_host.h" +#include "sd_pwr_ctrl_by_on_chip_ldo.h" +#include "esp_lcd_st7703.h" +#include "bsp/esp32_p4_wifi6_touch_lcd_4b.h" +#include "bsp/display.h" +#include "bsp/touch.h" +#include "esp_lcd_touch_gt911.h" +#include "bsp_err_check.h" +#include "esp_codec_dev_defaults.h" + +static const char *TAG = "ESP32_P4_4B"; + +#if (BSP_CONFIG_NO_GRAPHIC_LIB == 0) +static lv_indev_t *disp_indev = NULL; +#endif // (BSP_CONFIG_NO_GRAPHIC_LIB == 0) + +sdmmc_card_t *bsp_sdcard = NULL; // Global uSD card handler +static bool i2c_initialized = false; +static TaskHandle_t usb_host_task; // USB Host Library task +#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)) +static i2c_master_bus_handle_t i2c_handle = NULL; // I2C Handle +#endif +static i2s_chan_handle_t i2s_tx_chan = NULL; +static i2s_chan_handle_t i2s_rx_chan = NULL; +static const audio_codec_data_if_t *i2s_data_if = NULL; /* Codec data interface */ +#define BSP_ES7210_CODEC_ADDR ES7210_CODEC_DEFAULT_ADDR + +/* Can be used for `i2s_std_gpio_config_t` and/or `i2s_std_config_t` initialization */ +#define BSP_I2S_GPIO_CFG \ + { \ + .mclk = BSP_I2S_MCLK, \ + .bclk = BSP_I2S_SCLK, \ + .ws = BSP_I2S_LCLK, \ + .dout = BSP_I2S_DOUT, \ + .din = BSP_I2S_DSIN, \ + .invert_flags = { \ + .mclk_inv = false, \ + .bclk_inv = false, \ + .ws_inv = false, \ + }, \ + } + +/* This configuration is used by default in `bsp_extra_audio_init()` */ +#define BSP_I2S_DUPLEX_MONO_CFG(_sample_rate) \ + { \ + .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(_sample_rate), \ + .slot_cfg = I2S_STD_PHILIP_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO), \ + .gpio_cfg = BSP_I2S_GPIO_CFG, \ + } + +esp_err_t bsp_i2c_init(void) +{ + /* I2C was initialized before */ + if (i2c_initialized) { + return ESP_OK; + } + + i2c_master_bus_config_t i2c_bus_conf = { + .clk_source = I2C_CLK_SRC_DEFAULT, + .sda_io_num = BSP_I2C_SDA, + .scl_io_num = BSP_I2C_SCL, + .i2c_port = BSP_I2C_NUM, + }; + BSP_ERROR_CHECK_RETURN_ERR(i2c_new_master_bus(&i2c_bus_conf, &i2c_handle)); + + i2c_initialized = true; + + return ESP_OK; +} + +esp_err_t bsp_i2c_deinit(void) +{ + BSP_ERROR_CHECK_RETURN_ERR(i2c_del_master_bus(i2c_handle)); + i2c_initialized = false; + return ESP_OK; +} + +i2c_master_bus_handle_t bsp_i2c_get_handle(void) +{ + return i2c_handle; +} + +static esp_err_t bsp_enable_ldo_vo4(void) +{ + static esp_ldo_channel_handle_t vo4_chan = NULL; + esp_ldo_channel_config_t ldo_cfg = { + .chan_id = 4, + .voltage_mv = 3300, + }; + + ESP_RETURN_ON_ERROR(esp_ldo_acquire_channel(&ldo_cfg, &vo4_chan), TAG, "Acquire LDO VO4 channel failed"); + ESP_LOGI(TAG, "LDO VO4 set to 3300mV"); + + return ESP_OK; +} + +esp_err_t bsp_sdcard_mount(void) +{ + const esp_vfs_fat_sdmmc_mount_config_t mount_config = { +#ifdef CONFIG_BSP_SD_FORMAT_ON_MOUNT_FAIL + .format_if_mount_failed = true, +#else + .format_if_mount_failed = false, +#endif + .max_files = 5, + .allocation_unit_size = 64 * 1024 + }; + + sdmmc_host_t host = SDMMC_HOST_DEFAULT(); + host.slot = SDMMC_HOST_SLOT_0; + host.max_freq_khz = SDMMC_FREQ_HIGHSPEED; + + + ESP_RETURN_ON_ERROR(bsp_enable_ldo_vo4(), TAG, "DSI PHY power failed"); + + const sdmmc_slot_config_t slot_config = { + /* SD card is connected to Slot 0 pins. Slot 0 uses IO MUX, so not specifying the pins here */ + .cd = SDMMC_SLOT_NO_CD, + .wp = SDMMC_SLOT_NO_WP, + .width = 4, + .flags = 0, + }; + + + + return esp_vfs_fat_sdmmc_mount(BSP_SD_MOUNT_POINT, &host, &slot_config, &mount_config, &bsp_sdcard); +} + +esp_err_t bsp_sdcard_unmount(void) +{ + return esp_vfs_fat_sdcard_unmount(BSP_SD_MOUNT_POINT, bsp_sdcard); +} + +esp_err_t bsp_spiffs_mount(void) +{ + esp_vfs_spiffs_conf_t conf = { + .base_path = CONFIG_BSP_SPIFFS_MOUNT_POINT, + .partition_label = CONFIG_BSP_SPIFFS_PARTITION_LABEL, + .max_files = CONFIG_BSP_SPIFFS_MAX_FILES, +#ifdef CONFIG_BSP_SPIFFS_FORMAT_ON_MOUNT_FAIL + .format_if_mount_failed = true, +#else + .format_if_mount_failed = false, +#endif + }; + + esp_err_t ret_val = esp_vfs_spiffs_register(&conf); + + BSP_ERROR_CHECK_RETURN_ERR(ret_val); + + size_t total = 0, used = 0; + ret_val = esp_spiffs_info(conf.partition_label, &total, &used); + if (ret_val != ESP_OK) { + ESP_LOGE(TAG, "Failed to get SPIFFS partition information (%s)", esp_err_to_name(ret_val)); + } else { + ESP_LOGI(TAG, "Partition size: total: %d, used: %d", total, used); + } + + return ret_val; +} + +esp_err_t bsp_spiffs_unmount(void) +{ + return esp_vfs_spiffs_unregister(CONFIG_BSP_SPIFFS_PARTITION_LABEL); +} + +/************************************************************************************************** + * + * I2S Audio Function + * + **************************************************************************************************/ +esp_err_t bsp_audio_init(const i2s_std_config_t *i2s_config) +{ + esp_err_t ret = ESP_FAIL; + if (i2s_tx_chan && i2s_rx_chan) { + /* Audio was initialized before */ + return ESP_OK; + } + + /* Setup I2S peripheral */ + i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(CONFIG_BSP_I2S_NUM, I2S_ROLE_MASTER); + chan_cfg.auto_clear = true; // Auto clear the legacy data in the DMA buffer + BSP_ERROR_CHECK_RETURN_ERR(i2s_new_channel(&chan_cfg, &i2s_tx_chan, &i2s_rx_chan)); + + /* Setup I2S channels */ + const i2s_std_config_t std_cfg_default = BSP_I2S_DUPLEX_MONO_CFG(22050); + const i2s_std_config_t *p_i2s_cfg = &std_cfg_default; + if (i2s_config != NULL) { + p_i2s_cfg = i2s_config; + } + + if (i2s_tx_chan != NULL) { + ESP_GOTO_ON_ERROR(i2s_channel_init_std_mode(i2s_tx_chan, p_i2s_cfg), err, TAG, "I2S channel initialization failed"); + ESP_GOTO_ON_ERROR(i2s_channel_enable(i2s_tx_chan), err, TAG, "I2S enabling failed"); + } + if (i2s_rx_chan != NULL) { + ESP_GOTO_ON_ERROR(i2s_channel_init_std_mode(i2s_rx_chan, p_i2s_cfg), err, TAG, "I2S channel initialization failed"); + ESP_GOTO_ON_ERROR(i2s_channel_enable(i2s_rx_chan), err, TAG, "I2S enabling failed"); + } + + audio_codec_i2s_cfg_t i2s_cfg = { + .port = CONFIG_BSP_I2S_NUM, + .rx_handle = i2s_rx_chan, + .tx_handle = i2s_tx_chan, + }; + i2s_data_if = audio_codec_new_i2s_data(&i2s_cfg); + BSP_NULL_CHECK_GOTO(i2s_data_if, err); + + return ESP_OK; + +err: + if (i2s_tx_chan) { + i2s_del_channel(i2s_tx_chan); + } + if (i2s_rx_chan) { + i2s_del_channel(i2s_rx_chan); + } + + return ret; +} + +esp_codec_dev_handle_t bsp_audio_codec_speaker_init(void) +{ + if (i2s_data_if == NULL) { + /* Initilize I2C */ + BSP_ERROR_CHECK_RETURN_NULL(bsp_i2c_init()); + /* Configure I2S peripheral and Power Amplifier */ + BSP_ERROR_CHECK_RETURN_NULL(bsp_audio_init(NULL)); + } + assert(i2s_data_if); + + const audio_codec_gpio_if_t *gpio_if = audio_codec_new_gpio(); + + audio_codec_i2c_cfg_t i2c_cfg = { + .port = BSP_I2C_NUM, + .addr = ES8311_CODEC_DEFAULT_ADDR, + .bus_handle = i2c_handle, + }; + const audio_codec_ctrl_if_t *i2c_ctrl_if = audio_codec_new_i2c_ctrl(&i2c_cfg); + BSP_NULL_CHECK(i2c_ctrl_if, NULL); + + esp_codec_dev_hw_gain_t gain = { + .pa_voltage = 5.0, + .codec_dac_voltage = 3.3, + }; + + es8311_codec_cfg_t es8311_cfg = { + .ctrl_if = i2c_ctrl_if, + .gpio_if = gpio_if, + .codec_mode = ESP_CODEC_DEV_WORK_MODE_DAC, + .pa_pin = BSP_POWER_AMP_IO, + .pa_reverted = false, + .master_mode = false, + .use_mclk = true, + .digital_mic = false, + .invert_mclk = false, + .invert_sclk = false, + .hw_gain = gain, + }; + const audio_codec_if_t *es8311_dev = es8311_codec_new(&es8311_cfg); + BSP_NULL_CHECK(es8311_dev, NULL); + + esp_codec_dev_cfg_t codec_dev_cfg = { + .dev_type = ESP_CODEC_DEV_TYPE_OUT, + .codec_if = es8311_dev, + .data_if = i2s_data_if, + }; + return esp_codec_dev_new(&codec_dev_cfg); +} + +esp_codec_dev_handle_t bsp_audio_codec_microphone_init(void) +{ + if (i2s_data_if == NULL) { + /* Initilize I2C */ + BSP_ERROR_CHECK_RETURN_NULL(bsp_i2c_init()); + /* Configure I2S peripheral and Power Amplifier */ + BSP_ERROR_CHECK_RETURN_NULL(bsp_audio_init(NULL)); + } + assert(i2s_data_if); + + audio_codec_i2c_cfg_t i2c_cfg = { + .port = BSP_I2C_NUM, + .addr = BSP_ES7210_CODEC_ADDR, + .bus_handle = i2c_handle, + }; + const audio_codec_ctrl_if_t *i2c_ctrl_if = audio_codec_new_i2c_ctrl(&i2c_cfg); + BSP_NULL_CHECK(i2c_ctrl_if, NULL); + + es7210_codec_cfg_t es7210_cfg = { + .ctrl_if = i2c_ctrl_if, + }; + const audio_codec_if_t *es7210_dev = es7210_codec_new(&es7210_cfg); + BSP_NULL_CHECK(es7210_dev, NULL); + + esp_codec_dev_cfg_t codec_es7210_dev_cfg = { + .dev_type = ESP_CODEC_DEV_TYPE_IN, + .codec_if = es7210_dev, + .data_if = i2s_data_if, + }; + return esp_codec_dev_new(&codec_es7210_dev_cfg); +} + +// Bit number used to represent command and parameter +#define LCD_LEDC_CH CONFIG_BSP_DISPLAY_BRIGHTNESS_LEDC_CH + +esp_err_t bsp_display_brightness_init(void) +{ + return bsp_i2c_init(); +} + +esp_err_t bsp_display_brightness_set(int brightness_percent) +{ + if (brightness_percent > 100) { + brightness_percent = 100; + } else if (brightness_percent < 0) { + brightness_percent = 0; + } + + ESP_LOGI(TAG, "Setting LCD backlight: %d%%", brightness_percent); + + uint8_t data = (uint8_t)(255 * brightness_percent / 100); + uint8_t data_to_send[2] = {0x96, data}; + + i2c_device_config_t i2c_dev_conf = { + .scl_speed_hz = 100 * 1000, + .device_address = 0x45, + }; + + i2c_master_dev_handle_t dev_handle = NULL; + BSP_ERROR_CHECK_RETURN_ERR(i2c_master_bus_add_device(i2c_handle, &i2c_dev_conf, &dev_handle)); + esp_err_t ret = i2c_master_transmit(dev_handle, data_to_send, sizeof(data_to_send), 50); + i2c_master_bus_rm_device(dev_handle); + BSP_ERROR_CHECK_RETURN_ERR(ret); + + return ESP_OK; +} + +esp_err_t bsp_display_backlight_off(void) +{ + return bsp_display_brightness_set(0); +} + +esp_err_t bsp_display_backlight_on(void) +{ + return bsp_display_brightness_set(100); +} + +static esp_err_t bsp_enable_dsi_phy_power(void) +{ +#if BSP_MIPI_DSI_PHY_PWR_LDO_CHAN > 0 + // Turn on the power for MIPI DSI PHY, so it can go from "No Power" state to "Shutdown" state + static esp_ldo_channel_handle_t phy_pwr_chan = NULL; + esp_ldo_channel_config_t ldo_cfg = { + .chan_id = BSP_MIPI_DSI_PHY_PWR_LDO_CHAN, + .voltage_mv = BSP_MIPI_DSI_PHY_PWR_LDO_VOLTAGE_MV, + }; + ESP_RETURN_ON_ERROR(esp_ldo_acquire_channel(&ldo_cfg, &phy_pwr_chan), TAG, "Acquire LDO channel for DPHY failed"); + ESP_LOGI(TAG, "MIPI DSI PHY Powered on"); +#endif // BSP_MIPI_DSI_PHY_PWR_LDO_CHAN > 0 + + return ESP_OK; +} + +esp_err_t bsp_display_new(const bsp_display_config_t *config, esp_lcd_panel_handle_t *ret_panel, esp_lcd_panel_io_handle_t *ret_io) +{ + esp_err_t ret = ESP_OK; + bsp_lcd_handles_t handles; + ret = bsp_display_new_with_handles(config, &handles); + + *ret_panel = handles.panel; + *ret_io = handles.io; + + return ret; +} + +esp_err_t bsp_display_new_with_handles(const bsp_display_config_t *config, bsp_lcd_handles_t *ret_handles) +{ + esp_err_t ret = ESP_OK; + + ESP_RETURN_ON_ERROR(bsp_display_brightness_init(), TAG, "Brightness init failed"); + ESP_RETURN_ON_ERROR(bsp_enable_dsi_phy_power(), TAG, "DSI PHY power failed"); + ESP_RETURN_ON_ERROR(bsp_enable_ldo_vo4(), TAG, "DSI PHY power failed"); + + /* create MIPI DSI bus first, it will initialize the DSI PHY as well */ + esp_lcd_dsi_bus_handle_t mipi_dsi_bus; + esp_lcd_dsi_bus_config_t bus_config = { + .bus_id = 0, + .num_data_lanes = BSP_LCD_MIPI_DSI_LANE_NUM, + .phy_clk_src = 0, + .lane_bit_rate_mbps = BSP_LCD_MIPI_DSI_LANE_BITRATE_MBPS, + }; + ESP_RETURN_ON_ERROR(esp_lcd_new_dsi_bus(&bus_config, &mipi_dsi_bus), TAG, "New DSI bus init failed"); + + ESP_LOGI(TAG, "Install MIPI DSI LCD control panel"); + // we use DBI interface to send LCD commands and parameters + esp_lcd_panel_io_handle_t io; + esp_lcd_dbi_io_config_t dbi_config = { + .virtual_channel = 0, + .lcd_cmd_bits = 8, // according to the LCD ILI9881C spec + .lcd_param_bits = 8, // according to the LCD ILI9881C spec + }; + ESP_GOTO_ON_ERROR(esp_lcd_new_panel_io_dbi(mipi_dsi_bus, &dbi_config, &io), err, TAG, "New panel IO failed"); + + esp_lcd_panel_handle_t disp_panel = NULL; + ESP_LOGI(TAG, "Install Waveshare ESP32-P4 4-inch ST7703 LCD control panel"); +#if CONFIG_BSP_LCD_COLOR_FORMAT_RGB888 + esp_lcd_dpi_panel_config_t dpi_config = ST7703_720_720_PANEL_60HZ_DPI_CONFIG(LCD_COLOR_PIXEL_FORMAT_RGB888); +#else + esp_lcd_dpi_panel_config_t dpi_config = ST7703_720_720_PANEL_60HZ_DPI_CONFIG(LCD_COLOR_PIXEL_FORMAT_RGB565); +#endif + dpi_config.num_fbs = CONFIG_BSP_LCD_DPI_BUFFER_NUMS; + + st7703_vendor_config_t vendor_config = { + .mipi_config = { + .dsi_bus = mipi_dsi_bus, + .dpi_config = &dpi_config, + }, + }; + esp_lcd_panel_dev_config_t lcd_dev_config = { +#if CONFIG_BSP_LCD_COLOR_FORMAT_RGB888 + .bits_per_pixel = 24, +#else + .bits_per_pixel = 16, +#endif + .rgb_ele_order = BSP_LCD_COLOR_SPACE, + .reset_gpio_num = BSP_LCD_RST, + .vendor_config = &vendor_config, + }; + ESP_GOTO_ON_ERROR(esp_lcd_new_panel_st7703(io, &lcd_dev_config, &disp_panel), err, TAG, "New LCD panel Waveshare failed"); + ESP_GOTO_ON_ERROR(esp_lcd_panel_reset(disp_panel), err, TAG, "LCD panel reset failed"); + ESP_GOTO_ON_ERROR(esp_lcd_panel_init(disp_panel), err, TAG, "LCD panel init failed"); + ESP_GOTO_ON_ERROR(esp_lcd_panel_disp_on_off(disp_panel, true), err, TAG, "LCD panel display on failed"); + + /* Return all handles */ + ret_handles->io = io; + ret_handles->mipi_dsi_bus = mipi_dsi_bus; + ret_handles->panel = disp_panel; + ret_handles->control = NULL; + + ESP_LOGI(TAG, "Display initialized"); + + return ret; + +err: + if (disp_panel) { + esp_lcd_panel_del(disp_panel); + } + if (io) { + esp_lcd_panel_io_del(io); + } + if (mipi_dsi_bus) { + esp_lcd_del_dsi_bus(mipi_dsi_bus); + } + return ret; +} + +esp_err_t bsp_touch_new(const bsp_touch_config_t *config, esp_lcd_touch_handle_t *ret_touch) +{ + /* Initilize I2C */ + BSP_ERROR_CHECK_RETURN_ERR(bsp_i2c_init()); + + /* Initialize touch */ + const esp_lcd_touch_config_t tp_cfg = { +#if CONFIG_BSP_LCD_TYPE_480_640_2_8_INCH || CONFIG_BSP_LCD_TYPE_480_800_4_INCH + .x_max = BSP_LCD_V_RES, + .y_max = BSP_LCD_H_RES, +#else + .x_max = BSP_LCD_H_RES, + .y_max = BSP_LCD_V_RES, +#endif + .rst_gpio_num = BSP_LCD_TOUCH_RST, // Shared with LCD reset + .int_gpio_num = BSP_LCD_TOUCH_INT, + .levels = { + .reset = 0, + .interrupt = 0, + }, + .flags = { + .swap_xy = 0, + .mirror_x = 0, + .mirror_y = 0, + }, + }; + esp_lcd_panel_io_handle_t tp_io_handle = NULL; + esp_lcd_panel_io_i2c_config_t tp_io_config = ESP_LCD_TOUCH_IO_I2C_GT911_CONFIG(); + tp_io_config.scl_speed_hz = CONFIG_BSP_I2C_CLK_SPEED_HZ; + ESP_RETURN_ON_ERROR(esp_lcd_new_panel_io_i2c(i2c_handle, &tp_io_config, &tp_io_handle), TAG, ""); + return esp_lcd_touch_new_i2c_gt911(tp_io_handle, &tp_cfg, ret_touch); +} + +#if (BSP_CONFIG_NO_GRAPHIC_LIB == 0) +static lv_display_t *bsp_display_lcd_init(const bsp_display_cfg_t *cfg) +{ + assert(cfg != NULL); + bsp_lcd_handles_t lcd_panels; + BSP_ERROR_CHECK_RETURN_NULL(bsp_display_new_with_handles(NULL, &lcd_panels)); + + /* Add LCD screen */ + ESP_LOGD(TAG, "Add LCD screen"); + const lvgl_port_display_cfg_t disp_cfg = { + .io_handle = lcd_panels.io, + .panel_handle = lcd_panels.panel, + .control_handle = lcd_panels.control, + .buffer_size = cfg->buffer_size, + .double_buffer = cfg->double_buffer, + .hres = BSP_LCD_H_RES, + .vres = BSP_LCD_V_RES, + .monochrome = false, + /* Rotation values must be same as used in esp_lcd for initial settings of the screen */ + .rotation = { + .swap_xy = false, + .mirror_x = false, + .mirror_y = false, + }, +#if LVGL_VERSION_MAJOR >= 9 +#if CONFIG_BSP_LCD_COLOR_FORMAT_RGB888 + .color_format = LV_COLOR_FORMAT_RGB888, +#else + .color_format = LV_COLOR_FORMAT_RGB565, +#endif +#endif + .flags = { + .buff_dma = cfg->flags.buff_dma, + .buff_spiram = cfg->flags.buff_spiram, +#if LVGL_VERSION_MAJOR >= 9 + .swap_bytes = (BSP_LCD_BIGENDIAN ? true : false), +#endif +#if CONFIG_BSP_DISPLAY_LVGL_AVOID_TEAR + .sw_rotate = false, /* Avoid tearing is not supported for SW rotation */ +#else + .sw_rotate = cfg->flags.sw_rotate, /* Only SW rotation is supported for 90° and 270° */ +#endif +#if CONFIG_BSP_DISPLAY_LVGL_FULL_REFRESH + .full_refresh = true, +#elif CONFIG_BSP_DISPLAY_LVGL_DIRECT_MODE + .direct_mode = true, +#endif + } + }; + + const lvgl_port_display_dsi_cfg_t dpi_cfg = { + .flags = { +#if CONFIG_BSP_DISPLAY_LVGL_AVOID_TEAR + .avoid_tearing = true, +#else + .avoid_tearing = false, +#endif + } + }; + + return lvgl_port_add_disp_dsi(&disp_cfg, &dpi_cfg); +} + +static lv_indev_t *bsp_display_indev_init(lv_display_t *disp) +{ + esp_lcd_touch_handle_t tp; + BSP_ERROR_CHECK_RETURN_NULL(bsp_touch_new(NULL, &tp)); + assert(tp); + + /* Add touch input (for selected screen) */ + const lvgl_port_touch_cfg_t touch_cfg = { + .disp = disp, + .handle = tp, + }; + + return lvgl_port_add_touch(&touch_cfg); +} + +lv_display_t *bsp_display_start(void) +{ + bsp_display_cfg_t cfg = { + .lvgl_port_cfg = ESP_LVGL_PORT_INIT_CONFIG(), + .buffer_size = BSP_LCD_DRAW_BUFF_SIZE, + .double_buffer = BSP_LCD_DRAW_BUFF_DOUBLE, + .flags = { +#if CONFIG_BSP_LCD_COLOR_FORMAT_RGB888 + .buff_dma = false, +#else + .buff_dma = true, +#endif + .buff_spiram = false, + .sw_rotate = true, + } + }; + return bsp_display_start_with_config(&cfg); +} + +lv_display_t *bsp_display_start_with_config(const bsp_display_cfg_t *cfg) +{ + lv_display_t *disp; + + assert(cfg != NULL); + BSP_ERROR_CHECK_RETURN_NULL(lvgl_port_init(&cfg->lvgl_port_cfg)); + + BSP_ERROR_CHECK_RETURN_NULL(bsp_display_brightness_init()); + + BSP_NULL_CHECK(disp = bsp_display_lcd_init(cfg), NULL); + + BSP_NULL_CHECK(disp_indev = bsp_display_indev_init(disp), NULL); + + return disp; +} + +lv_indev_t *bsp_display_get_input_dev(void) +{ + return disp_indev; +} + +void bsp_display_rotate(lv_display_t *disp, lv_disp_rotation_t rotation) +{ + lv_disp_set_rotation(disp, rotation); +} + +bool bsp_display_lock(uint32_t timeout_ms) +{ + return lvgl_port_lock(timeout_ms); +} + +void bsp_display_unlock(void) +{ + lvgl_port_unlock(); +} + +#endif // (BSP_CONFIG_NO_GRAPHIC_LIB == 0) + +static void usb_lib_task(void *arg) +{ + while (1) { + // Start handling system events + uint32_t event_flags; + usb_host_lib_handle_events(portMAX_DELAY, &event_flags); + if (event_flags & USB_HOST_LIB_EVENT_FLAGS_NO_CLIENTS) { + ESP_ERROR_CHECK(usb_host_device_free_all()); + } + if (event_flags & USB_HOST_LIB_EVENT_FLAGS_ALL_FREE) { + ESP_LOGI(TAG, "USB: All devices freed"); + } + } +} + +esp_err_t bsp_usb_host_start(bsp_usb_host_power_mode_t mode, bool limit_500mA) +{ + //Install USB Host driver. Should only be called once in entire application + ESP_LOGI(TAG, "Installing USB Host"); + const usb_host_config_t host_config = { + .skip_phy_setup = false, + .intr_flags = ESP_INTR_FLAG_LEVEL1, + }; + BSP_ERROR_CHECK_RETURN_ERR(usb_host_install(&host_config)); + + // Create a task that will handle USB library events + if (xTaskCreate(usb_lib_task, "usb_lib", 4096, NULL, 10, &usb_host_task) != pdTRUE) { + ESP_LOGE(TAG, "Creating USB host lib task failed"); + abort(); + } + + return ESP_OK; +} + +esp_err_t bsp_usb_host_stop(void) +{ + usb_host_uninstall(); + if (usb_host_task) { + vTaskSuspend(usb_host_task); + vTaskDelete(usb_host_task); + } + return ESP_OK; +} diff --git a/components/esp32_p4_wifi6_touch_lcd_4b/idf_component.yml b/components/esp32_p4_wifi6_touch_lcd_4b/idf_component.yml new file mode 100644 index 0000000..bfc3edc --- /dev/null +++ b/components/esp32_p4_wifi6_touch_lcd_4b/idf_component.yml @@ -0,0 +1,18 @@ +dependencies: + esp_codec_dev: + public: true + version: "~1.5" + esp_lcd_touch_gt911: ^1 + espressif/esp_lvgl_port: + public: true + version: ^2 + lvgl/lvgl: '>=8,<10' + waveshare/esp_lcd_st7703: "1.0.1" +description: Based on ESP32-P4 chip, Waveshare ESP32-P4-86-Panel-ETH-2RO/4-inch 720x720 panel support +repository: git://github.com/WaveshareCloud/Waveshare-ESP32-components.git +tags: +- bsp +targets: +- esp32p4 +url: https://www.waveshare.com/esp32-p4-wifi6-touch-lcd-3.4c.htm +version: 1.0.3 diff --git a/components/esp32_p4_wifi6_touch_lcd_4b/include/bsp/config.h b/components/esp32_p4_wifi6_touch_lcd_4b/include/bsp/config.h new file mode 100644 index 0000000..927f814 --- /dev/null +++ b/components/esp32_p4_wifi6_touch_lcd_4b/include/bsp/config.h @@ -0,0 +1,10 @@ +#pragma once + +/************************************************************************************************** + * BSP configuration + **************************************************************************************************/ +// By default, this BSP is shipped with LVGL graphical library. Enabling this option will exclude it. +// If you want to use BSP without LVGL, select BSP version with 'noglib' suffix. +#if !defined(BSP_CONFIG_NO_GRAPHIC_LIB) // Check if the symbol is not coming from compiler definitions (-D...) +#define BSP_CONFIG_NO_GRAPHIC_LIB (0) +#endif diff --git a/components/esp32_p4_wifi6_touch_lcd_4b/include/bsp/display.h b/components/esp32_p4_wifi6_touch_lcd_4b/include/bsp/display.h new file mode 100644 index 0000000..061f311 --- /dev/null +++ b/components/esp32_p4_wifi6_touch_lcd_4b/include/bsp/display.h @@ -0,0 +1,154 @@ +#pragma once +#include "esp_lcd_types.h" +#include "esp_lcd_mipi_dsi.h" +#include "sdkconfig.h" + +/* LCD color formats */ +#define ESP_LCD_COLOR_FORMAT_RGB565 (1) +#define ESP_LCD_COLOR_FORMAT_RGB888 (2) + +/* LCD display color format */ +#if CONFIG_BSP_LCD_COLOR_FORMAT_RGB888 +#define BSP_LCD_COLOR_FORMAT (ESP_LCD_COLOR_FORMAT_RGB888) +#else +#define BSP_LCD_COLOR_FORMAT (ESP_LCD_COLOR_FORMAT_RGB565) +#endif +/* LCD display color bytes endianess */ +#define BSP_LCD_BIGENDIAN (0) +/* LCD display color bits */ +#define BSP_LCD_BITS_PER_PIXEL (16) +/* LCD display color space */ +#define BSP_LCD_COLOR_SPACE (LCD_RGB_ELEMENT_ORDER_RGB) + +#define BSP_LCD_H_RES (720) +#define BSP_LCD_V_RES (720) +#define BSP_LCD_MIPI_DSI_LANE_BITRATE_MBPS (1200) + +#define BSP_LCD_MIPI_DSI_LANE_NUM (2) // 2 data lanes +#define BSP_MIPI_DSI_PHY_PWR_LDO_CHAN (3) // LDO_VO3 is connected to VDD_MIPI_DPHY +#define BSP_MIPI_DSI_PHY_PWR_LDO_VOLTAGE_MV (2500) + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief BSP display configuration structure + * + */ +typedef struct { + int dummy; +} bsp_display_config_t; + +/** + * @brief BSP display return handles + * + */ +typedef struct { + esp_lcd_dsi_bus_handle_t mipi_dsi_bus; /*!< MIPI DSI bus handle */ + esp_lcd_panel_io_handle_t io; /*!< ESP LCD IO handle */ + esp_lcd_panel_handle_t panel; /*!< ESP LCD panel (color) handle */ + esp_lcd_panel_handle_t control; /*!< ESP LCD panel (control) handle */ +} bsp_lcd_handles_t; + +/** + * @brief Create new display panel + * + * For maximum flexibility, this function performs only reset and initialization of the display. + * You must turn on the display explicitly by calling esp_lcd_panel_disp_on_off(). + * The display's backlight is not turned on either. You can use bsp_display_backlight_on/off(), + * bsp_display_brightness_set() (on supported boards) or implement your own backlight control. + * + * If you want to free resources allocated by this function, you can use esp_lcd API, ie.: + * + * \code{.c} + * esp_lcd_panel_del(panel); + * esp_lcd_panel_io_del(io); + * esp_lcd_del_dsi_bus(mipi_dsi_bus); + * \endcode + * + * @param[in] config display configuration + * @param[out] ret_panel esp_lcd panel handle + * @param[out] ret_io esp_lcd IO handle + * @return + * - ESP_OK On success + * - Else esp_lcd failure + */ +esp_err_t bsp_display_new(const bsp_display_config_t *config, esp_lcd_panel_handle_t *ret_panel, esp_lcd_panel_io_handle_t *ret_io); + +/** + * @brief Create new display panel + * + * For maximum flexibility, this function performs only reset and initialization of the display. + * You must turn on the display explicitly by calling esp_lcd_panel_disp_on_off(). + * The display's backlight is not turned on either. You can use bsp_display_backlight_on/off(), + * bsp_display_brightness_set() (on supported boards) or implement your own backlight control. + * + * If you want to free resources allocated by this function, you can use esp_lcd API, ie.: + * + * \code{.c} + * esp_lcd_panel_del(panel); + * esp_lcd_panel_del(control); + * esp_lcd_panel_io_del(io); + * esp_lcd_del_dsi_bus(mipi_dsi_bus); + * \endcode + * + * @param[in] config display configuration + * @param[out] ret_handles all esp_lcd handles in one structure + * @return + * - ESP_OK On success + * - Else esp_lcd failure + */ +esp_err_t bsp_display_new_with_handles(const bsp_display_config_t *config, bsp_lcd_handles_t *ret_handles); + +/** + * @brief Initialize display's brightness + * + * Brightness is controlled with PWM signal to a pin controlling backlight. + * + * @return + * - ESP_OK On success + * - ESP_ERR_INVALID_ARG Parameter error + */ +esp_err_t bsp_display_brightness_init(void); + +/** + * @brief Set display's brightness + * + * Brightness is controlled with PWM signal to a pin controlling backlight. + * Brightness must be already initialized by calling bsp_display_brightness_init() or bsp_display_new() + * + * @param[in] brightness_percent Brightness in [%] + * @return + * - ESP_OK On success + * - ESP_ERR_INVALID_ARG Parameter error + */ +esp_err_t bsp_display_brightness_set(int brightness_percent); + +/** + * @brief Turn on display backlight + * + * Brightness is controlled with PWM signal to a pin controlling backlight. + * Brightness must be already initialized by calling bsp_display_brightness_init() or bsp_display_new() + * + * @return + * - ESP_OK On success + * - ESP_ERR_INVALID_ARG Parameter error + */ +esp_err_t bsp_display_backlight_on(void); + +/** + * @brief Turn off display backlight + * + * Brightness is controlled with PWM signal to a pin controlling backlight. + * Brightness must be already initialized by calling bsp_display_brightness_init() or bsp_display_new() + * + * @return + * - ESP_OK On success + * - ESP_ERR_INVALID_ARG Parameter error + */ +esp_err_t bsp_display_backlight_off(void); + +#ifdef __cplusplus +} +#endif diff --git a/components/esp32_p4_wifi6_touch_lcd_4b/include/bsp/esp-bsp.h b/components/esp32_p4_wifi6_touch_lcd_4b/include/bsp/esp-bsp.h new file mode 100644 index 0000000..0ff0b31 --- /dev/null +++ b/components/esp32_p4_wifi6_touch_lcd_4b/include/bsp/esp-bsp.h @@ -0,0 +1,2 @@ +#pragma once +#include "bsp/esp32_p4_wifi6_touch_lcd_4b.h" diff --git a/components/esp32_p4_wifi6_touch_lcd_4b/include/bsp/esp32_p4_wifi6_touch_lcd_4b.h b/components/esp32_p4_wifi6_touch_lcd_4b/include/bsp/esp32_p4_wifi6_touch_lcd_4b.h new file mode 100644 index 0000000..39ca37c --- /dev/null +++ b/components/esp32_p4_wifi6_touch_lcd_4b/include/bsp/esp32_p4_wifi6_touch_lcd_4b.h @@ -0,0 +1,363 @@ +#pragma once + +#include "sdkconfig.h" +#include "driver/gpio.h" +#include "driver/i2c_master.h" +#include "driver/sdmmc_host.h" +#include "driver/i2s_std.h" +#include "bsp/config.h" +#include "bsp/display.h" +#include "esp_codec_dev.h" +#include "sdkconfig.h" + +#if (BSP_CONFIG_NO_GRAPHIC_LIB == 0) +#include "lvgl.h" +#include "esp_lvgl_port.h" +#endif // BSP_CONFIG_NO_GRAPHIC_LIB == 0 + +/************************************************************************************************** + * BSP Capabilities + **************************************************************************************************/ + +#define BSP_CAPS_DISPLAY 1 +#define BSP_CAPS_TOUCH 1 +#define BSP_CAPS_BUTTONS 0 +#define BSP_CAPS_AUDIO 1 +#define BSP_CAPS_AUDIO_SPEAKER 1 +#define BSP_CAPS_AUDIO_MIC 1 +#define BSP_CAPS_SDCARD 1 +#define BSP_CAPS_IMU 0 + +/************************************************************************************************** + * ESP-BOX pinout + **************************************************************************************************/ +/* I2C */ +#define BSP_I2C_SCL (GPIO_NUM_8) +#define BSP_I2C_SDA (GPIO_NUM_7) + +/* Audio */ +#define BSP_I2S_SCLK (GPIO_NUM_12) +#define BSP_I2S_MCLK (GPIO_NUM_13) +#define BSP_I2S_LCLK (GPIO_NUM_10) +#define BSP_I2S_DOUT (GPIO_NUM_9) +#define BSP_I2S_DSIN (GPIO_NUM_11) +#define BSP_POWER_AMP_IO (GPIO_NUM_53) + +#define BSP_LCD_BACKLIGHT (GPIO_NUM_NC) +#define BSP_LCD_RST (GPIO_NUM_NC) +#define BSP_LCD_TOUCH_RST (GPIO_NUM_NC) +#define BSP_LCD_TOUCH_INT (GPIO_NUM_NC) + +/* uSD card */ +#define BSP_SD_D0 (GPIO_NUM_39) +#define BSP_SD_D1 (GPIO_NUM_40) +#define BSP_SD_D2 (GPIO_NUM_41) +#define BSP_SD_D3 (GPIO_NUM_42) +#define BSP_SD_CMD (GPIO_NUM_44) +#define BSP_SD_CLK (GPIO_NUM_43) + +#ifdef __cplusplus +extern "C" { +#endif + +/************************************************************************************************** + * + * I2C interface + * + * There are multiple devices connected to I2C peripheral: + * - Codec ES8311 (configuration only) + * - LCD Touch controller + **************************************************************************************************/ +#define BSP_I2C_NUM CONFIG_BSP_I2C_NUM + +/** + * @brief Init I2C driver + * + * @return + * - ESP_OK On success + * - ESP_ERR_INVALID_ARG I2C parameter error + * - ESP_FAIL I2C driver installation error + * + */ +esp_err_t bsp_i2c_init(void); + +/** + * @brief Deinit I2C driver and free its resources + * + * @return + * - ESP_OK On success + * - ESP_ERR_INVALID_ARG I2C parameter error + * + */ +esp_err_t bsp_i2c_deinit(void); + +/** + * @brief Get I2C driver handle + * + * @return + * - I2C handle + * + */ +i2c_master_bus_handle_t bsp_i2c_get_handle(void); + +/************************************************************************************************** + * + * I2S audio interface + * + * There are two devices connected to the I2S peripheral: + * - Codec ES8311 for output(playback) and input(recording) path + * + * For speaker initialization use bsp_audio_codec_speaker_init() which is inside initialize I2S with bsp_audio_init(). + * For microphone initialization use bsp_audio_codec_microphone_init() which is inside initialize I2S with bsp_audio_init(). + * After speaker or microphone initialization, use functions from esp_codec_dev for play/record audio. + * Example audio play: + * \code{.c} + * esp_codec_dev_set_out_vol(spk_codec_dev, DEFAULT_VOLUME); + * esp_codec_dev_open(spk_codec_dev, &fs); + * esp_codec_dev_write(spk_codec_dev, wav_bytes, bytes_read_from_spiffs); + * esp_codec_dev_close(spk_codec_dev); + * \endcode + **************************************************************************************************/ + +/** + * @brief Init audio + * + * @note There is no deinit audio function. Users can free audio resources by calling i2s_del_channel() + * @warning The type of i2s_config param is depending on IDF version. + * @param[in] i2s_config I2S configuration. Pass NULL to use default values (Mono, duplex, 16bit, 22050 Hz) + * @return + * - ESP_OK On success + * - ESP_ERR_NOT_SUPPORTED The communication mode is not supported on the current chip + * - ESP_ERR_INVALID_ARG NULL pointer or invalid configuration + * - ESP_ERR_NOT_FOUND No available I2S channel found + * - ESP_ERR_NO_MEM No memory for storing the channel information + * - ESP_ERR_INVALID_STATE This channel has not initialized or already started + */ +esp_err_t bsp_audio_init(const i2s_std_config_t *i2s_config); + +/** + * @brief Initialize speaker codec device + * + * @return Pointer to codec device handle or NULL when error occurred + */ +esp_codec_dev_handle_t bsp_audio_codec_speaker_init(void); + +/** + * @brief Initialize microphone codec device + * + * @return Pointer to codec device handle or NULL when error occurred + */ +esp_codec_dev_handle_t bsp_audio_codec_microphone_init(void); + +/************************************************************************************************** + * + * SPIFFS + * + * After mounting the SPIFFS, it can be accessed with stdio functions ie.: + * \code{.c} + * FILE* f = fopen(BSP_SPIFFS_MOUNT_POINT"/hello.txt", "w"); + * fprintf(f, "Hello World!\n"); + * fclose(f); + * \endcode + **************************************************************************************************/ +#define BSP_SPIFFS_MOUNT_POINT CONFIG_BSP_SPIFFS_MOUNT_POINT + +/** + * @brief Mount SPIFFS to virtual file system + * + * @return + * - ESP_OK on success + * - ESP_ERR_INVALID_STATE if esp_vfs_spiffs_register was already called + * - ESP_ERR_NO_MEM if memory can not be allocated + * - ESP_FAIL if partition can not be mounted + * - other error codes + */ +esp_err_t bsp_spiffs_mount(void); + +/** + * @brief Unmount SPIFFS from virtual file system + * + * @return + * - ESP_OK on success + * - ESP_ERR_NOT_FOUND if the partition table does not contain SPIFFS partition with given label + * - ESP_ERR_INVALID_STATE if esp_vfs_spiffs_unregister was already called + * - ESP_ERR_NO_MEM if memory can not be allocated + * - ESP_FAIL if partition can not be mounted + * - other error codes + */ +esp_err_t bsp_spiffs_unmount(void); + +/************************************************************************************************** + * + * uSD card + * + * After mounting the uSD card, it can be accessed with stdio functions ie.: + * \code{.c} + * FILE* f = fopen(BSP_MOUNT_POINT"/hello.txt", "w"); + * fprintf(f, "Hello %s!\n", bsp_sdcard->cid.name); + * fclose(f); + * \endcode + **************************************************************************************************/ +#define BSP_SD_MOUNT_POINT CONFIG_BSP_SD_MOUNT_POINT +extern sdmmc_card_t *bsp_sdcard; + +/** + * @brief Mount microSD card to virtual file system + * + * @return + * - ESP_OK on success + * - ESP_ERR_INVALID_STATE if esp_vfs_fat_sdmmc_mount was already called + * - ESP_ERR_NO_MEM if memory cannot be allocated + * - ESP_FAIL if partition cannot be mounted + * - other error codes from SDMMC or SPI drivers, SDMMC protocol, or FATFS drivers + */ +esp_err_t bsp_sdcard_mount(void); + +/** + * @brief Unmount microSD card from virtual file system + * + * @return + * - ESP_OK on success + * - ESP_ERR_NOT_FOUND if the partition table does not contain FATFS partition with given label + * - ESP_ERR_INVALID_STATE if esp_vfs_fat_spiflash_mount was already called + * - ESP_ERR_NO_MEM if memory can not be allocated + * - ESP_FAIL if partition can not be mounted + * - other error codes from wear levelling library, SPI flash driver, or FATFS drivers + */ +esp_err_t bsp_sdcard_unmount(void); + +/************************************************************************************************** + * + * LCD interface + * + * ESP-BOX is shipped with 2.4inch ST7789 display controller. + * It features 16-bit colors, 320x240 resolution and capacitive touch controller. + * + * LVGL is used as graphics library. LVGL is NOT thread safe, therefore the user must take LVGL mutex + * by calling bsp_display_lock() before calling and LVGL API (lv_...) and then give the mutex with + * bsp_display_unlock(). + * + * Display's backlight must be enabled explicitly by calling bsp_display_backlight_on() + **************************************************************************************************/ +#define BSP_LCD_PIXEL_CLOCK_MHZ (80) + +#if (BSP_CONFIG_NO_GRAPHIC_LIB == 0) + +#define BSP_LCD_DRAW_BUFF_SIZE (BSP_LCD_H_RES * 50) // Frame buffer size in pixels +#define BSP_LCD_DRAW_BUFF_DOUBLE (0) + +/** + * @brief BSP display configuration structure + * + */ +typedef struct { + lvgl_port_cfg_t lvgl_port_cfg; /*!< LVGL port configuration */ + uint32_t buffer_size; /*!< Size of the buffer for the screen in pixels */ + bool double_buffer; /*!< True, if should be allocated two buffers */ + struct { + unsigned int buff_dma: 1; /*!< Allocated LVGL buffer will be DMA capable */ + unsigned int buff_spiram: 1; /*!< Allocated LVGL buffer will be in PSRAM */ + unsigned int sw_rotate: 1; /*!< Use software rotation (slower), The feature is unavailable under avoid-tear mode */ + } flags; +} bsp_display_cfg_t; + +/** + * @brief Initialize display + * + * This function initializes SPI, display controller and starts LVGL handling task. + * LCD backlight must be enabled separately by calling bsp_display_brightness_set() + * + * @return Pointer to LVGL display or NULL when error occured + */ +lv_display_t *bsp_display_start(void); + +/** + * @brief Initialize display + * + * This function initializes SPI, display controller and starts LVGL handling task. + * LCD backlight must be enabled separately by calling bsp_display_brightness_set() + * + * @param cfg display configuration + * + * @return Pointer to LVGL display or NULL when error occured + */ +lv_display_t *bsp_display_start_with_config(const bsp_display_cfg_t *cfg); + +/** + * @brief Get pointer to input device (touch, buttons, ...) + * + * @note The LVGL input device is initialized in bsp_display_start() function. + * + * @return Pointer to LVGL input device or NULL when not initialized + */ +lv_indev_t *bsp_display_get_input_dev(void); + +/** + * @brief Take LVGL mutex + * + * @param timeout_ms Timeout in [ms]. 0 will block indefinitely. + * @return true Mutex was taken + * @return false Mutex was NOT taken + */ +bool bsp_display_lock(uint32_t timeout_ms); + +/** + * @brief Give LVGL mutex + * + */ +void bsp_display_unlock(void); + +/** + * @brief Rotate screen + * + * Display must be already initialized by calling bsp_display_start() + * + * @param[in] disp Pointer to LVGL display + * @param[in] rotation Angle of the display rotation + */ +void bsp_display_rotate(lv_display_t *disp, lv_disp_rotation_t rotation); +#endif // BSP_CONFIG_NO_GRAPHIC_LIB == 0 + +/************************************************************************************************** + * + * USB + * + **************************************************************************************************/ + + +/** + * @brief Power modes of USB Host connector + */ +typedef enum bsp_usb_host_power_mode_t { + BSP_USB_HOST_POWER_MODE_USB_DEV, //!< Power from USB DEV port +} bsp_usb_host_power_mode_t; + +/** + * @brief Start USB host + * + * This is a one-stop-shop function that will configure the board for USB Host mode + * and start USB Host library + * + * @param[in] mode USB Host connector power mode (Not used on this board) + * @param[in] limit_500mA Limit output current to 500mA (Not used on this board) + * @return + * - ESP_OK On success + * - ESP_ERR_INVALID_ARG Parameter error + * - ESP_ERR_NO_MEM Memory cannot be allocated + */ +esp_err_t bsp_usb_host_start(bsp_usb_host_power_mode_t mode, bool limit_500mA); + +/** + * @brief Stop USB host + * + * USB Host lib will be uninstalled and power from connector removed. + * + * @return + * - ESP_OK On success + * - ESP_ERR_INVALID_ARG Parameter error + */ +esp_err_t bsp_usb_host_stop(void); + +#ifdef __cplusplus +} +#endif diff --git a/components/esp32_p4_wifi6_touch_lcd_4b/include/bsp/touch.h b/components/esp32_p4_wifi6_touch_lcd_4b/include/bsp/touch.h new file mode 100644 index 0000000..0f7eb44 --- /dev/null +++ b/components/esp32_p4_wifi6_touch_lcd_4b/include/bsp/touch.h @@ -0,0 +1,35 @@ +#pragma once +#include "esp_lcd_touch.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief BSP touch configuration structure + * + */ +typedef struct { + void *dummy; /*!< Prepared for future use. */ +} bsp_touch_config_t; + +/** + * @brief Create new touchscreen + * + * If you want to free resources allocated by this function, you can use esp_lcd_touch API, ie.: + * + * \code{.c} + * esp_lcd_touch_del(tp); + * \endcode + * + * @param[in] config touch configuration + * @param[out] ret_touch esp_lcd_touch touchscreen handle + * @return + * - ESP_OK On success + * - Else esp_lcd_touch failure + */ +esp_err_t bsp_touch_new(const bsp_touch_config_t *config, esp_lcd_touch_handle_t *ret_touch); + +#ifdef __cplusplus +} +#endif diff --git a/components/esp32_p4_wifi6_touch_lcd_4b/priv_include/bsp_err_check.h b/components/esp32_p4_wifi6_touch_lcd_4b/priv_include/bsp_err_check.h new file mode 100644 index 0000000..44d93dd --- /dev/null +++ b/components/esp32_p4_wifi6_touch_lcd_4b/priv_include/bsp_err_check.h @@ -0,0 +1,52 @@ +#pragma once + +#include "esp_check.h" +#include "sdkconfig.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* Assert on error, if selected in menuconfig. Otherwise return error code. */ +#if CONFIG_BSP_ERROR_CHECK +#define BSP_ERROR_CHECK_RETURN_ERR(x) ESP_ERROR_CHECK(x) +#define BSP_ERROR_CHECK_RETURN_NULL(x) ESP_ERROR_CHECK(x) +#define BSP_ERROR_CHECK(x, ret) ESP_ERROR_CHECK(x) +#define BSP_NULL_CHECK(x, ret) assert(x) +#define BSP_NULL_CHECK_GOTO(x, goto_tag) assert(x) +#else +#define BSP_ERROR_CHECK_RETURN_ERR(x) do { \ + esp_err_t err_rc_ = (x); \ + if (unlikely(err_rc_ != ESP_OK)) { \ + return err_rc_; \ + } \ + } while(0) + +#define BSP_ERROR_CHECK_RETURN_NULL(x) do { \ + if (unlikely((x) != ESP_OK)) { \ + return NULL; \ + } \ + } while(0) + +#define BSP_NULL_CHECK(x, ret) do { \ + if ((x) == NULL) { \ + return ret; \ + } \ + } while(0) + +#define BSP_ERROR_CHECK(x, ret) do { \ + if (unlikely((x) != ESP_OK)) { \ + return ret; \ + } \ + } while(0) + +#define BSP_NULL_CHECK_GOTO(x, goto_tag) do { \ + if ((x) == NULL) { \ + goto goto_tag; \ + } \ + } while(0) +#endif + +#ifdef __cplusplus +} +#endif diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt new file mode 100644 index 0000000..7f2e913 --- /dev/null +++ b/main/CMakeLists.txt @@ -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 "." +) diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild new file mode 100644 index 0000000..df75063 --- /dev/null +++ b/main/Kconfig.projbuild @@ -0,0 +1,7 @@ +menu "Tarot App" + +config TAROT_START_BRIGHTNESS_FROM_NVS + bool "Apply saved brightness during startup" + default y + +endmenu diff --git a/main/app_main.c b/main/app_main.c new file mode 100644 index 0000000..9e0d958 --- /dev/null +++ b/main/app_main.c @@ -0,0 +1,47 @@ +#include +#include + +#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"); + } +} diff --git a/main/card_assets.c b/main/card_assets.c new file mode 100644 index 0000000..f4458ac --- /dev/null +++ b/main/card_assets.c @@ -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; +} diff --git a/main/card_assets.h b/main/card_assets.h new file mode 100644 index 0000000..fdb2224 --- /dev/null +++ b/main/card_assets.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +#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 diff --git a/main/idf_component.yml b/main/idf_component.yml new file mode 100644 index 0000000..b42bc6b --- /dev/null +++ b/main/idf_component.yml @@ -0,0 +1,2 @@ +dependencies: + lvgl/lvgl: ">=9,<10" diff --git a/main/settings.c b/main/settings.c new file mode 100644 index 0000000..54c3de4 --- /dev/null +++ b/main/settings.c @@ -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"; + } +} diff --git a/main/settings.h b/main/settings.h new file mode 100644 index 0000000..1078af6 --- /dev/null +++ b/main/settings.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +#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 diff --git a/main/tarot_deck.c b/main/tarot_deck.c new file mode 100644 index 0000000..27fdccc --- /dev/null +++ b/main/tarot_deck.c @@ -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 ""; + } +} diff --git a/main/tarot_deck.h b/main/tarot_deck.h new file mode 100644 index 0000000..c7c8fa7 --- /dev/null +++ b/main/tarot_deck.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include + +#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 diff --git a/main/tarot_logic.c b/main/tarot_logic.c new file mode 100644 index 0000000..5fd831c --- /dev/null +++ b/main/tarot_logic.c @@ -0,0 +1,71 @@ +#include "tarot_logic.h" + +#include + +#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); + } +} diff --git a/main/tarot_logic.h b/main/tarot_logic.h new file mode 100644 index 0000000..1623394 --- /dev/null +++ b/main/tarot_logic.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include + +#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 diff --git a/main/ui_main.c b/main/ui_main.c new file mode 100644 index 0000000..57a9d3d --- /dev/null +++ b/main/ui_main.c @@ -0,0 +1,215 @@ +#include "ui_main.h" + +#include + +#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(); +} diff --git a/main/ui_main.h b/main/ui_main.h new file mode 100644 index 0000000..aa9e248 --- /dev/null +++ b/main/ui_main.h @@ -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 diff --git a/main/ui_settings.c b/main/ui_settings.c new file mode 100644 index 0000000..1a0dcdc --- /dev/null +++ b/main/ui_settings.c @@ -0,0 +1,151 @@ +#include "ui_settings.h" + +#include + +#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(); +} diff --git a/main/ui_settings.h b/main/ui_settings.h new file mode 100644 index 0000000..b826237 --- /dev/null +++ b/main/ui_settings.h @@ -0,0 +1,13 @@ +#pragma once + +#include "lvgl.h" + +#ifdef __cplusplus +extern "C" { +#endif + +void ui_show_settings(void); + +#ifdef __cplusplus +} +#endif