commit 99618a46b0c93186e2a53658ef6f7e612e10bb4d Author: DESKTOP-PUHFO60\dsx2016 Date: Mon Oct 19 18:07:56 2026 +0800 Add CYD photo album with jpg/png/gif viewer and sample assets. Ship TF-card album UI, half-res decode canvas, prebuilt firmware, and documented flash/upload workflow. Co-authored-by: Cursor diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a47f425 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +.pio/ +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch +*.pyc +__pycache__/ +.DS_Store +Thumbs.db +*.elf +# 构建产物不入库;firmware/ 预编译包要入库 +# assets/photos 样张入库,其它临时资源可忽略 +assets/* +!assets/photos/ +!assets/photos/** diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5517c56 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +## 2026-10-19 — Photo album + +- CYD LVGL 壳层来自 tftest(触摸映射冻结,TF + SoftSPI)。 +- 扫描 `/photos` 与 `/` 下 jpg/png/gif;点左/右或 `<`/`>` 切换。 +- JPEGDEC / PNGdec / AnimatedGIF 解码到 160×120 RGB565 画布,界面 2× 显示。 +- 提供 `assets/photos` 样张与 `tools/make_test_photos.py`;串口 `#PUT` 上传。 +- 预编译包:`firmware/cyd/`。 diff --git a/README.md b/README.md new file mode 100644 index 0000000..6e217c8 --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# CYD Photo Album + +ESP32-2432S028R 电子相册:从 TF 卡读取 **JPG / PNG / GIF**,触控切换相片。 + +硬件与触摸标定复用 `tftest`(SoftSPI + BL/TR 对角修正,**勿改** `src/touch.cpp`)。 + +## 功能 + +- 扫描 TF 卡 `/photos` 与根目录的 `.jpg` / `.jpeg` / `.png` / `.gif` +- 半分辨率画布 160×120,界面 2× 放大到 320×240(适配无 PSRAM 的 DRAM) +- JPG 过大自动 1/2、1/4、1/8 缩放;PNG / GIF 等比缩入画布 +- **点左半屏 = 上一张,点右半屏 = 下一张**;底栏 `<` / `>` 同样切换 +- GIF 自动播帧;右上角显示 TF 状态 +- 串口 `#PUT` 传文件到卡(`tools/sd_put.py`) + +## 测试图 + +样张在 `assets/photos/`(已入库): + +| 文件 | 格式 | +|------|------| +| `01_blue.jpg` | JPG | +| `02_red.jpg` | JPG | +| `03_green.png` | PNG | +| `04_bounce.gif` | GIF | + +```bash +python tools/make_test_photos.py +python tools/sd_put.py assets/photos/01_blue.jpg --port COM3 --remote /01_blue.jpg +python tools/sd_put.py assets/photos/02_red.jpg --port COM3 --remote /02_red.jpg +python tools/sd_put.py assets/photos/03_green.png --port COM3 --remote /03_green.png +python tools/sd_put.py assets/photos/04_bounce.gif --port COM3 --remote /04_bounce.gif +``` + +## 构建 / 烧录 + +```bash +pio run -e cyd -t upload --upload-port COM3 +``` + +或先导出再刷预编译包: + +```powershell +.\firmware\export_from_build.ps1 +cd firmware\cyd +.\flash.ps1 -Port COM3 +``` + +## 目录要点 + +| 路径 | 说明 | +|------|------| +| `src/touch.cpp` | 触摸标定,勿改 | +| `src/photo_list.cpp` | 扫描相册文件 | +| `src/photo_view.cpp` | JPG/PNG/GIF 解码 | +| `src/ui.cpp` | 相册界面与切换 | +| `firmware/cyd/` | 预编译固件 + `flash.ps1` | diff --git a/assets/photos/01_blue.jpg b/assets/photos/01_blue.jpg new file mode 100644 index 0000000..d15d962 Binary files /dev/null and b/assets/photos/01_blue.jpg differ diff --git a/assets/photos/02_red.jpg b/assets/photos/02_red.jpg new file mode 100644 index 0000000..1d68779 Binary files /dev/null and b/assets/photos/02_red.jpg differ diff --git a/assets/photos/03_green.png b/assets/photos/03_green.png new file mode 100644 index 0000000..12e1b34 Binary files /dev/null and b/assets/photos/03_green.png differ diff --git a/assets/photos/04_bounce.gif b/assets/photos/04_bounce.gif new file mode 100644 index 0000000..72d2246 Binary files /dev/null and b/assets/photos/04_bounce.gif differ diff --git a/copy_lv_conf.py b/copy_lv_conf.py new file mode 100644 index 0000000..ca4f24c --- /dev/null +++ b/copy_lv_conf.py @@ -0,0 +1,13 @@ +Import("env") # noqa: F821 +from pathlib import Path + +src = Path(env["PROJECT_DIR"]) / "include" / "lv_conf.h" # noqa: F821 +dst_dir = Path(env["PROJECT_LIBDEPS_DIR"]) / env["PIOENV"] # noqa: F821 +dst_dir.mkdir(parents=True, exist_ok=True) +dst = dst_dir / "lv_conf.h" +if src.exists(): + dst.write_text(src.read_text(encoding="utf-8"), encoding="utf-8") + lvgl = dst_dir / "lvgl" / "lv_conf.h" + if (dst_dir / "lvgl").exists(): + lvgl.write_text(src.read_text(encoding="utf-8"), encoding="utf-8") + print(f"copy_lv_conf: synced -> {dst}") diff --git a/firmware/README.md b/firmware/README.md new file mode 100644 index 0000000..b440dca --- /dev/null +++ b/firmware/README.md @@ -0,0 +1,47 @@ +# 预编译固件(可随时刷写) + +当前归档目录:[`cyd/`](cyd/) + +对应板子:**CYD ESP32-2432S028R**(ILI9341) +功能:电子相册(JPG / PNG / GIF)+ SoftSPI 触控 + TF 卡 +版本信息见 [`cyd/VERSION.txt`](cyd/VERSION.txt)。 + +## 直接刷写(无需重新编译) + +1. USB 连接板子 +2. 执行: + +```powershell +cd firmware\cyd +.\flash.ps1 +# 或指定端口 +.\flash.ps1 -Port COM3 +``` + +也可运行 `flash.bat`。 + +依赖其一即可: + +- 已安装 PlatformIO(`pio`,自带 esptool) +- 或 `pip install esptool` + +## 手动 esptool + +```bash +esptool.py --chip esp32 --port COM3 --baud 921600 write_flash -z ^ + --flash_mode dio --flash_freq 40m --flash_size 4MB ^ + 0x1000 bootloader.bin ^ + 0x8000 partitions.bin ^ + 0xe000 boot_app0.bin ^ + 0x10000 firmware.bin +``` + +## 从源码重新打包进本目录 + +在仓库根目录: + +```powershell +.\firmware\export_from_build.ps1 +# 仅复制、不编译: +.\firmware\export_from_build.ps1 -SkipBuild +``` diff --git a/firmware/cyd/VERSION.txt b/firmware/cyd/VERSION.txt new file mode 100644 index 0000000..ec9a464 --- /dev/null +++ b/firmware/cyd/VERSION.txt @@ -0,0 +1,16 @@ +name: CYD Photo album (cyd) +git: unknown +date: 2026-10-19 +board: ESP32-2432S028R (ILI9341) +features: jpg/png/gif album, SoftSPI touch, TF +flash_size: 4MB +offsets: + 0x1000 bootloader.bin + 0x8000 partitions.bin + 0xe000 boot_app0.bin + 0x10000 firmware.bin +sha256: + bootloader.bin 3D234A7471F67B013686DABD4DEE7C1FA915C9928463616A94BC9297ACF1ABF8 + boot_app0.bin F94C5D786A7A8FAB06AC5D10E33BF37711A6697636DC037559EA19CC410A17F0 + firmware.bin CB21DB25D8AAC041B39C10AA2E63E00D2401D1A7EA4F956F9B4985D1EA87C8EB + partitions.bin 148B959CBFF1C38AA8E1D5C0BA9D612C54997B945E56A63F41223EEF650653A1 diff --git a/firmware/cyd/boot_app0.bin b/firmware/cyd/boot_app0.bin new file mode 100644 index 0000000..13562ca Binary files /dev/null and b/firmware/cyd/boot_app0.bin differ diff --git a/firmware/cyd/bootloader.bin b/firmware/cyd/bootloader.bin new file mode 100644 index 0000000..d592359 Binary files /dev/null and b/firmware/cyd/bootloader.bin differ diff --git a/firmware/cyd/firmware.bin b/firmware/cyd/firmware.bin new file mode 100644 index 0000000..4ed631d Binary files /dev/null and b/firmware/cyd/firmware.bin differ diff --git a/firmware/cyd/flash.bat b/firmware/cyd/flash.bat new file mode 100644 index 0000000..bf983b3 --- /dev/null +++ b/firmware/cyd/flash.bat @@ -0,0 +1,5 @@ +@echo off +setlocal +cd /d "%~dp0" +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0flash.ps1" %* +exit /b %ERRORLEVEL% diff --git a/firmware/cyd/flash.ps1 b/firmware/cyd/flash.ps1 new file mode 100644 index 0000000..c80479b --- /dev/null +++ b/firmware/cyd/flash.ps1 @@ -0,0 +1,98 @@ +# Flash archived CYD TF+Speaker firmware (no rebuild needed) +# Usage: +# .\flash.ps1 +# .\flash.ps1 -Port COM5 +# .\flash.ps1 -Port COM5 -Baud 460800 + +param( + [string]$Port = "", + [int]$Baud = 921600 +) + +$ErrorActionPreference = "Stop" +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +Set-Location $here + +function Find-Esptool { + $pio = Get-Command pio -ErrorAction SilentlyContinue + if (-not $pio) { + $pioPath = Join-Path $env:USERPROFILE "AppData\Local\Programs\Python\Python312\Scripts\pio.exe" + if (Test-Path $pioPath) { + $env:Path = "$(Split-Path $pioPath);$env:Path" + $pio = Get-Command pio -ErrorAction SilentlyContinue + } + } + if ($pio) { + $pkg = Join-Path $env:USERPROFILE ".platformio\packages\tool-esptoolpy\esptool.py" + if (Test-Path $pkg) { return @{ Kind = "py"; Path = $pkg } } + } + $py = Get-Command python -ErrorAction SilentlyContinue + if ($py) { + try { + & python -c "import esptool" 2>$null + if ($LASTEXITCODE -eq 0) { return @{ Kind = "mod"; Path = "esptool" } } + } catch {} + } + $esp = Get-Command esptool.py -ErrorAction SilentlyContinue + if ($esp) { return @{ Kind = "py"; Path = $esp.Source } } + return $null +} + +if (-not $Port) { + $ports = @() + try { + Add-Type -AssemblyName System.IO.Ports -ErrorAction SilentlyContinue | Out-Null + $ports = [System.IO.Ports.SerialPort]::GetPortNames() | Sort-Object + } catch {} + if (-not $ports -or $ports.Count -eq 0) { + $ports = Get-CimInstance Win32_SerialPort -ErrorAction SilentlyContinue | + Select-Object -ExpandProperty DeviceID | Sort-Object + } + if (-not $ports -or $ports.Count -eq 0) { + Write-Error "No serial ports found. Plug in the CYD USB cable, then retry with -Port COMx" + } + if ($ports.Count -eq 1) { + $Port = $ports[0] + } else { + Write-Host "Available ports: $($ports -join ', ')" + $Port = $ports[-1] + Write-Host "Using last port: $Port (override with -Port)" + } +} + +$tool = Find-Esptool +if (-not $tool) { + Write-Error "esptool not found. Install PlatformIO (`pio`) or: pip install esptool" +} + +$needed = @("bootloader.bin", "partitions.bin", "boot_app0.bin", "firmware.bin") +foreach ($f in $needed) { + if (-not (Test-Path (Join-Path $here $f))) { + Write-Error "Missing $f — run ..\export_from_build.ps1 from repo root first" + } +} + +$argsCommon = @( + "--chip", "esp32", + "--port", $Port, + "--baud", "$Baud", + "--before", "default_reset", + "--after", "hard_reset", + "write_flash", "-z", + "--flash_mode", "dio", + "--flash_freq", "40m", + "--flash_size", "4MB", + "0x1000", "bootloader.bin", + "0x8000", "partitions.bin", + "0xe000", "boot_app0.bin", + "0x10000", "firmware.bin" +) + +Write-Host "Flashing $here -> $Port @ $Baud" +if ($tool.Kind -eq "mod") { + & python -m esptool @argsCommon +} else { + & python $tool.Path @argsCommon +} +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } +Write-Host "Done. Open serial monitor at 115200 if needed." diff --git a/firmware/cyd/partitions.bin b/firmware/cyd/partitions.bin new file mode 100644 index 0000000..2108af9 Binary files /dev/null and b/firmware/cyd/partitions.bin differ diff --git a/firmware/export_from_build.ps1 b/firmware/export_from_build.ps1 new file mode 100644 index 0000000..6a233a8 --- /dev/null +++ b/firmware/export_from_build.ps1 @@ -0,0 +1,58 @@ +# Rebuild (optional) and refresh firmware/cyd from PlatformIO build output. +# Usage: +# .\firmware\export_from_build.ps1 +# .\firmware\export_from_build.ps1 -SkipBuild + +param([switch]$SkipBuild) + +$ErrorActionPreference = "Stop" +$root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path) +Set-Location $root + +$env:Path = "C:\Users\dsx2016\AppData\Local\Programs\Python\Python312\Scripts;" + $env:Path + +if (-not $SkipBuild) { + pio run -e cyd + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } +} + +$src = Join-Path $root ".pio\build\cyd" +$dst = Join-Path $root "firmware\cyd" +New-Item -ItemType Directory -Force -Path $dst | Out-Null + +foreach ($f in @("bootloader.bin", "partitions.bin", "firmware.bin")) { + $p = Join-Path $src $f + if (-not (Test-Path $p)) { Write-Error "Missing build artifact: $p" } + Copy-Item $p $dst -Force +} + +$bootApp = Join-Path $env:USERPROFILE ".platformio\packages\framework-arduinoespressif32\tools\partitions\boot_app0.bin" +if (-not (Test-Path $bootApp)) { + Write-Error "boot_app0.bin not found under PlatformIO packages" +} +Copy-Item $bootApp (Join-Path $dst "boot_app0.bin") -Force + +# Keep flash scripts if already present +$git = "" +try { $git = (git -C $root rev-parse --short HEAD).Trim() } catch { $git = "unknown" } +$date = Get-Date -Format "yyyy-MM-dd" +$hashes = Get-ChildItem (Join-Path $dst "*.bin") | Get-FileHash -Algorithm SHA256 +$lines = @( + "name: CYD Photo album (cyd)", + "git: $git", + "date: $date", + "board: ESP32-2432S028R (ILI9341)", + "features: jpg/png/gif album, SoftSPI touch, TF card", + "flash_size: 4MB", + "offsets:", + " 0x1000 bootloader.bin", + " 0x8000 partitions.bin", + " 0xe000 boot_app0.bin", + " 0x10000 firmware.bin", + "sha256:" +) +foreach ($h in $hashes) { + $lines += (" {0,-15} {1}" -f (Split-Path $h.Path -Leaf), $h.Hash) +} +$lines | Set-Content -Encoding utf8 (Join-Path $dst "VERSION.txt") +Write-Host "Exported -> $dst (git $git)" diff --git a/include/lv_conf.h b/include/lv_conf.h new file mode 100644 index 0000000..94de0de --- /dev/null +++ b/include/lv_conf.h @@ -0,0 +1,1127 @@ +/** + * @file lv_conf.h + * Configuration file for v9.2.2 + */ + +/* + * Copy this file as `lv_conf.h` + * 1. simply next to the `lvgl` folder + * 2. or any other places and + * - define `LV_CONF_INCLUDE_SIMPLE` + * - add the path as include path + */ + +/* clang-format off */ +#if 1 /*Set it to "1" to enable content*/ + +#ifndef LV_CONF_H +#define LV_CONF_H + +/*If you need to include anything here, do it inside the `__ASSEMBLY__` guard */ +#if 0 && defined(__ASSEMBLY__) +#include "my_include.h" +#endif + +/*==================== + COLOR SETTINGS + *====================*/ + +/*Color depth: 1 (I1), 8 (L8), 16 (RGB565), 24 (RGB888), 32 (XRGB8888)*/ +#define LV_COLOR_DEPTH 16 + +/*========================= + STDLIB WRAPPER SETTINGS + *=========================*/ + +/* Possible values + * - LV_STDLIB_BUILTIN: LVGL's built in implementation + * - LV_STDLIB_CLIB: Standard C functions, like malloc, strlen, etc + * - LV_STDLIB_MICROPYTHON: MicroPython implementation + * - LV_STDLIB_RTTHREAD: RT-Thread implementation + * - LV_STDLIB_CUSTOM: Implement the functions externally + */ +#define LV_USE_STDLIB_MALLOC LV_STDLIB_BUILTIN +#define LV_USE_STDLIB_STRING LV_STDLIB_BUILTIN +#define LV_USE_STDLIB_SPRINTF LV_STDLIB_BUILTIN + +#define LV_STDINT_INCLUDE +#define LV_STDDEF_INCLUDE +#define LV_STDBOOL_INCLUDE +#define LV_INTTYPES_INCLUDE +#define LV_LIMITS_INCLUDE +#define LV_STDARG_INCLUDE + +#if LV_USE_STDLIB_MALLOC == LV_STDLIB_BUILTIN + /*Size of the memory available for `lv_malloc()` in bytes (>= 2kB)*/ + #define LV_MEM_SIZE (64 * 1024U) /*[bytes]*/ + + /*Size of the memory expand for `lv_malloc()` in bytes*/ + #define LV_MEM_POOL_EXPAND_SIZE 0 + + /*Set an address for the memory pool instead of allocating it as a normal array. Can be in external SRAM too.*/ + #define LV_MEM_ADR 0 /*0: unused*/ + /*Instead of an address give a memory allocator that will be called to get a memory pool for LVGL. E.g. my_malloc*/ + #if LV_MEM_ADR == 0 + #undef LV_MEM_POOL_INCLUDE + #undef LV_MEM_POOL_ALLOC + #endif +#endif /*LV_USE_STDLIB_MALLOC == LV_STDLIB_BUILTIN*/ + +/*==================== + HAL SETTINGS + *====================*/ + +/*Default display refresh, input device read and animation step period.*/ +#define LV_DEF_REFR_PERIOD 33 /*[ms]*/ + +/*Default Dot Per Inch. Used to initialize default sizes such as widgets sized, style paddings. + *(Not so important, you can adjust it to modify default sizes and spaces)*/ +#define LV_DPI_DEF 130 /*[px/inch]*/ + +/*================= + * OPERATING SYSTEM + *=================*/ +/*Select an operating system to use. Possible options: + * - LV_OS_NONE + * - LV_OS_PTHREAD + * - LV_OS_FREERTOS + * - LV_OS_CMSIS_RTOS2 + * - LV_OS_RTTHREAD + * - LV_OS_WINDOWS + * - LV_OS_MQX + * - LV_OS_CUSTOM */ +#define LV_USE_OS LV_OS_NONE + +#if LV_USE_OS == LV_OS_CUSTOM + #define LV_OS_CUSTOM_INCLUDE +#endif +#if LV_USE_OS == LV_OS_FREERTOS + /* + * Unblocking an RTOS task with a direct notification is 45% faster and uses less RAM + * than unblocking a task using an intermediary object such as a binary semaphore. + * RTOS task notifications can only be used when there is only one task that can be the recipient of the event. + */ + #define LV_USE_FREERTOS_TASK_NOTIFY 1 +#endif + +/*======================== + * RENDERING CONFIGURATION + *========================*/ + +/*Align the stride of all layers and images to this bytes*/ +#define LV_DRAW_BUF_STRIDE_ALIGN 1 + +/*Align the start address of draw_buf addresses to this bytes*/ +#define LV_DRAW_BUF_ALIGN 4 + +/*Using matrix for transformations. + *Requirements: + `LV_USE_MATRIX = 1`. + The rendering engine needs to support 3x3 matrix transformations.*/ +#define LV_DRAW_TRANSFORM_USE_MATRIX 0 + +/* If a widget has `style_opa < 255` (not `bg_opa`, `text_opa` etc) or not NORMAL blend mode + * it is buffered into a "simple" layer before rendering. The widget can be buffered in smaller chunks. + * "Transformed layers" (if `transform_angle/zoom` are set) use larger buffers + * and can't be drawn in chunks. */ + +/*The target buffer size for simple layer chunks.*/ +#define LV_DRAW_LAYER_SIMPLE_BUF_SIZE (24 * 1024) /*[bytes]*/ + +/* The stack size of the drawing thread. + * NOTE: If FreeType or ThorVG is enabled, it is recommended to set it to 32KB or more. + */ +#define LV_DRAW_THREAD_STACK_SIZE (8 * 1024) /*[bytes]*/ + +#define LV_USE_DRAW_SW 1 +#if LV_USE_DRAW_SW == 1 + + /* + * Selectively disable color format support in order to reduce code size. + * NOTE: some features use certain color formats internally, e.g. + * - gradients use RGB888 + * - bitmaps with transparency may use ARGB8888 + */ + + #define LV_DRAW_SW_SUPPORT_RGB565 1 + #define LV_DRAW_SW_SUPPORT_RGB565A8 1 + #define LV_DRAW_SW_SUPPORT_RGB888 1 + #define LV_DRAW_SW_SUPPORT_XRGB8888 1 + #define LV_DRAW_SW_SUPPORT_ARGB8888 1 + #define LV_DRAW_SW_SUPPORT_L8 1 + #define LV_DRAW_SW_SUPPORT_AL88 1 + #define LV_DRAW_SW_SUPPORT_A8 1 + #define LV_DRAW_SW_SUPPORT_I1 1 + + /* Set the number of draw unit. + * > 1 requires an operating system enabled in `LV_USE_OS` + * > 1 means multiple threads will render the screen in parallel */ + #define LV_DRAW_SW_DRAW_UNIT_CNT 1 + + /* Use Arm-2D to accelerate the sw render */ + #define LV_USE_DRAW_ARM2D_SYNC 0 + + /* Enable native helium assembly to be compiled */ + #define LV_USE_NATIVE_HELIUM_ASM 0 + + /* 0: use a simple renderer capable of drawing only simple rectangles with gradient, images, texts, and straight lines only + * 1: use a complex renderer capable of drawing rounded corners, shadow, skew lines, and arcs too */ + #define LV_DRAW_SW_COMPLEX 1 + + #if LV_DRAW_SW_COMPLEX == 1 + /*Allow buffering some shadow calculation. + *LV_DRAW_SW_SHADOW_CACHE_SIZE is the max. shadow size to buffer, where shadow size is `shadow_width + radius` + *Caching has LV_DRAW_SW_SHADOW_CACHE_SIZE^2 RAM cost*/ + #define LV_DRAW_SW_SHADOW_CACHE_SIZE 0 + + /* Set number of maximally cached circle data. + * The circumference of 1/4 circle are saved for anti-aliasing + * radius * 4 bytes are used per circle (the most often used radiuses are saved) + * 0: to disable caching */ + #define LV_DRAW_SW_CIRCLE_CACHE_SIZE 4 + #endif + + #define LV_USE_DRAW_SW_ASM LV_DRAW_SW_ASM_NONE + + #if LV_USE_DRAW_SW_ASM == LV_DRAW_SW_ASM_CUSTOM + #define LV_DRAW_SW_ASM_CUSTOM_INCLUDE "" + #endif + + /* Enable drawing complex gradients in software: linear at an angle, radial or conical */ + #define LV_USE_DRAW_SW_COMPLEX_GRADIENTS 0 +#endif + +/* Use NXP's VG-Lite GPU on iMX RTxxx platforms. */ +#define LV_USE_DRAW_VGLITE 0 + +#if LV_USE_DRAW_VGLITE + /* Enable blit quality degradation workaround recommended for screen's dimension > 352 pixels. */ + #define LV_USE_VGLITE_BLIT_SPLIT 0 + + #if LV_USE_OS + /* Use additional draw thread for VG-Lite processing.*/ + #define LV_USE_VGLITE_DRAW_THREAD 1 + + #if LV_USE_VGLITE_DRAW_THREAD + /* Enable VGLite draw async. Queue multiple tasks and flash them once to the GPU. */ + #define LV_USE_VGLITE_DRAW_ASYNC 1 + #endif + #endif + + /* Enable VGLite asserts. */ + #define LV_USE_VGLITE_ASSERT 0 +#endif + +/* Use NXP's PXP on iMX RTxxx platforms. */ +#define LV_USE_PXP 0 + +#if LV_USE_PXP + /* Use PXP for drawing.*/ + #define LV_USE_DRAW_PXP 1 + + /* Use PXP to rotate display.*/ + #define LV_USE_ROTATE_PXP 0 + + #if LV_USE_DRAW_PXP && LV_USE_OS + /* Use additional draw thread for PXP processing.*/ + #define LV_USE_PXP_DRAW_THREAD 1 + #endif + + /* Enable PXP asserts. */ + #define LV_USE_PXP_ASSERT 0 +#endif + +/* Use Renesas Dave2D on RA platforms. */ +#define LV_USE_DRAW_DAVE2D 0 + +/* Draw using cached SDL textures*/ +#define LV_USE_DRAW_SDL 0 + +/* Use VG-Lite GPU. */ +#define LV_USE_DRAW_VG_LITE 0 + +#if LV_USE_DRAW_VG_LITE + /* Enable VG-Lite custom external 'gpu_init()' function */ + #define LV_VG_LITE_USE_GPU_INIT 0 + + /* Enable VG-Lite assert. */ + #define LV_VG_LITE_USE_ASSERT 0 + + /* VG-Lite flush commit trigger threshold. GPU will try to batch these many draw tasks. */ + #define LV_VG_LITE_FLUSH_MAX_COUNT 8 + + /* Enable border to simulate shadow + * NOTE: which usually improves performance, + * but does not guarantee the same rendering quality as the software. */ + #define LV_VG_LITE_USE_BOX_SHADOW 0 + + /* VG-Lite gradient maximum cache number. + * NOTE: The memory usage of a single gradient image is 4K bytes. + */ + #define LV_VG_LITE_GRAD_CACHE_CNT 32 + + /* VG-Lite stroke maximum cache number. + */ + #define LV_VG_LITE_STROKE_CACHE_CNT 32 + +#endif + +/*======================= + * FEATURE CONFIGURATION + *=======================*/ + +/*------------- + * Logging + *-----------*/ + +/*Enable the log module*/ +#define LV_USE_LOG 0 +#if LV_USE_LOG + + /*How important log should be added: + *LV_LOG_LEVEL_TRACE A lot of logs to give detailed information + *LV_LOG_LEVEL_INFO Log important events + *LV_LOG_LEVEL_WARN Log if something unwanted happened but didn't cause a problem + *LV_LOG_LEVEL_ERROR Only critical issue, when the system may fail + *LV_LOG_LEVEL_USER Only logs added by the user + *LV_LOG_LEVEL_NONE Do not log anything*/ + #define LV_LOG_LEVEL LV_LOG_LEVEL_WARN + + /*1: Print the log with 'printf'; + *0: User need to register a callback with `lv_log_register_print_cb()`*/ + #define LV_LOG_PRINTF 0 + + /*Set callback to print the logs. + *E.g `my_print`. The prototype should be `void my_print(lv_log_level_t level, const char * buf)` + *Can be overwritten by `lv_log_register_print_cb`*/ + //#define LV_LOG_PRINT_CB + + /*1: Enable print timestamp; + *0: Disable print timestamp*/ + #define LV_LOG_USE_TIMESTAMP 1 + + /*1: Print file and line number of the log; + *0: Do not print file and line number of the log*/ + #define LV_LOG_USE_FILE_LINE 1 + + + /*Enable/disable LV_LOG_TRACE in modules that produces a huge number of logs*/ + #define LV_LOG_TRACE_MEM 1 + #define LV_LOG_TRACE_TIMER 1 + #define LV_LOG_TRACE_INDEV 1 + #define LV_LOG_TRACE_DISP_REFR 1 + #define LV_LOG_TRACE_EVENT 1 + #define LV_LOG_TRACE_OBJ_CREATE 1 + #define LV_LOG_TRACE_LAYOUT 1 + #define LV_LOG_TRACE_ANIM 1 + #define LV_LOG_TRACE_CACHE 1 + +#endif /*LV_USE_LOG*/ + +/*------------- + * Asserts + *-----------*/ + +/*Enable asserts if an operation is failed or an invalid data is found. + *If LV_USE_LOG is enabled an error message will be printed on failure*/ +#define LV_USE_ASSERT_NULL 1 /*Check if the parameter is NULL. (Very fast, recommended)*/ +#define LV_USE_ASSERT_MALLOC 1 /*Checks is the memory is successfully allocated or no. (Very fast, recommended)*/ +#define LV_USE_ASSERT_STYLE 0 /*Check if the styles are properly initialized. (Very fast, recommended)*/ +#define LV_USE_ASSERT_MEM_INTEGRITY 0 /*Check the integrity of `lv_mem` after critical operations. (Slow)*/ +#define LV_USE_ASSERT_OBJ 0 /*Check the object's type and existence (e.g. not deleted). (Slow)*/ + +/*Add a custom handler when assert happens e.g. to restart the MCU*/ +#define LV_ASSERT_HANDLER_INCLUDE +#define LV_ASSERT_HANDLER while(1); /*Halt by default*/ + +/*------------- + * Debug + *-----------*/ + +/*1: Draw random colored rectangles over the redrawn areas*/ +#define LV_USE_REFR_DEBUG 0 + +/*1: Draw a red overlay for ARGB layers and a green overlay for RGB layers*/ +#define LV_USE_LAYER_DEBUG 0 + +/*1: Draw overlays with different colors for each draw_unit's tasks. + *Also add the index number of the draw unit on white background. + *For layers add the index number of the draw unit on black background.*/ +#define LV_USE_PARALLEL_DRAW_DEBUG 0 + +/*------------- + * Others + *-----------*/ + +#define LV_ENABLE_GLOBAL_CUSTOM 0 +#if LV_ENABLE_GLOBAL_CUSTOM + /*Header to include for the custom 'lv_global' function"*/ + #define LV_GLOBAL_CUSTOM_INCLUDE +#endif + +/*Default cache size in bytes. + *Used by image decoders such as `lv_lodepng` to keep the decoded image in the memory. + *If size is not set to 0, the decoder will fail to decode when the cache is full. + *If size is 0, the cache function is not enabled and the decoded mem will be released immediately after use.*/ +#define LV_CACHE_DEF_SIZE 0 + +/*Default number of image header cache entries. The cache is used to store the headers of images + *The main logic is like `LV_CACHE_DEF_SIZE` but for image headers.*/ +#define LV_IMAGE_HEADER_CACHE_DEF_CNT 0 + +/*Number of stops allowed per gradient. Increase this to allow more stops. + *This adds (sizeof(lv_color_t) + 1) bytes per additional stop*/ +#define LV_GRADIENT_MAX_STOPS 2 + +/* Adjust color mix functions rounding. GPUs might calculate color mix (blending) differently. + * 0: round down, 64: round up from x.75, 128: round up from half, 192: round up from x.25, 254: round up */ +#define LV_COLOR_MIX_ROUND_OFS 0 + +/* Add 2 x 32 bit variables to each lv_obj_t to speed up getting style properties */ +#define LV_OBJ_STYLE_CACHE 0 + +/* Add `id` field to `lv_obj_t` */ +#define LV_USE_OBJ_ID 0 + +/* Automatically assign an ID when obj is created */ +#define LV_OBJ_ID_AUTO_ASSIGN LV_USE_OBJ_ID + +/*Use the builtin obj ID handler functions: +* - lv_obj_assign_id: Called when a widget is created. Use a separate counter for each widget class as an ID. +* - lv_obj_id_compare: Compare the ID to decide if it matches with a requested value. +* - lv_obj_stringify_id: Return e.g. "button3" +* - lv_obj_free_id: Does nothing, as there is no memory allocation for the ID. +* When disabled these functions needs to be implemented by the user.*/ +#define LV_USE_OBJ_ID_BUILTIN 1 + +/*Use obj property set/get API*/ +#define LV_USE_OBJ_PROPERTY 0 + +/*Enable property name support*/ +#define LV_USE_OBJ_PROPERTY_NAME 1 + +/* VG-Lite Simulator */ +/*Requires: LV_USE_THORVG_INTERNAL or LV_USE_THORVG_EXTERNAL */ +#define LV_USE_VG_LITE_THORVG 0 + +#if LV_USE_VG_LITE_THORVG + + /*Enable LVGL's blend mode support*/ + #define LV_VG_LITE_THORVG_LVGL_BLEND_SUPPORT 0 + + /*Enable YUV color format support*/ + #define LV_VG_LITE_THORVG_YUV_SUPPORT 0 + + /*Enable Linear gradient extension support*/ + #define LV_VG_LITE_THORVG_LINEAR_GRADIENT_EXT_SUPPORT 0 + + /*Enable 16 pixels alignment*/ + #define LV_VG_LITE_THORVG_16PIXELS_ALIGN 1 + + /*Buffer address alignment*/ + #define LV_VG_LITE_THORVG_BUF_ADDR_ALIGN 64 + + /*Enable multi-thread render*/ + #define LV_VG_LITE_THORVG_THREAD_RENDER 0 + +#endif + +/*===================== + * COMPILER SETTINGS + *====================*/ + +/*For big endian systems set to 1*/ +#define LV_BIG_ENDIAN_SYSTEM 0 + +/*Define a custom attribute to `lv_tick_inc` function*/ +#define LV_ATTRIBUTE_TICK_INC + +/*Define a custom attribute to `lv_timer_handler` function*/ +#define LV_ATTRIBUTE_TIMER_HANDLER + +/*Define a custom attribute to `lv_display_flush_ready` function*/ +#define LV_ATTRIBUTE_FLUSH_READY + +/*Required alignment size for buffers*/ +#define LV_ATTRIBUTE_MEM_ALIGN_SIZE 1 + +/*Will be added where memories needs to be aligned (with -Os data might not be aligned to boundary by default). + * E.g. __attribute__((aligned(4)))*/ +#define LV_ATTRIBUTE_MEM_ALIGN + +/*Attribute to mark large constant arrays for example font's bitmaps*/ +#define LV_ATTRIBUTE_LARGE_CONST + +/*Compiler prefix for a big array declaration in RAM*/ +#define LV_ATTRIBUTE_LARGE_RAM_ARRAY + +/*Place performance critical functions into a faster memory (e.g RAM)*/ +#define LV_ATTRIBUTE_FAST_MEM + +/*Export integer constant to binding. This macro is used with constants in the form of LV_ that + *should also appear on LVGL binding API such as MicroPython.*/ +#define LV_EXPORT_CONST_INT(int_value) struct _silence_gcc_warning /*The default value just prevents GCC warning*/ + +/*Prefix all global extern data with this*/ +#define LV_ATTRIBUTE_EXTERN_DATA + +/* Use `float` as `lv_value_precise_t` */ +#define LV_USE_FLOAT 0 + +/*Enable matrix support + *Requires `LV_USE_FLOAT = 1`*/ +#define LV_USE_MATRIX 0 + +/*Include `lvgl_private.h` in `lvgl.h` to access internal data and functions by default*/ +#define LV_USE_PRIVATE_API 0 + +/*================== + * FONT USAGE + *===================*/ + +/*Montserrat fonts with ASCII range and some symbols using bpp = 4 + *https://fonts.google.com/specimen/Montserrat*/ +#define LV_FONT_MONTSERRAT_8 0 +#define LV_FONT_MONTSERRAT_10 0 +#define LV_FONT_MONTSERRAT_12 0 +#define LV_FONT_MONTSERRAT_14 1 +#define LV_FONT_MONTSERRAT_16 1 +#define LV_FONT_MONTSERRAT_18 1 +#define LV_FONT_MONTSERRAT_20 1 +#define LV_FONT_MONTSERRAT_22 0 +#define LV_FONT_MONTSERRAT_24 0 +#define LV_FONT_MONTSERRAT_26 0 +#define LV_FONT_MONTSERRAT_28 0 +#define LV_FONT_MONTSERRAT_30 0 +#define LV_FONT_MONTSERRAT_32 0 +#define LV_FONT_MONTSERRAT_34 0 +#define LV_FONT_MONTSERRAT_36 0 +#define LV_FONT_MONTSERRAT_38 0 +#define LV_FONT_MONTSERRAT_40 0 +#define LV_FONT_MONTSERRAT_42 0 +#define LV_FONT_MONTSERRAT_44 0 +#define LV_FONT_MONTSERRAT_46 0 +#define LV_FONT_MONTSERRAT_48 0 + +/*Demonstrate special features*/ +#define LV_FONT_MONTSERRAT_28_COMPRESSED 0 /*bpp = 3*/ +#define LV_FONT_DEJAVU_16_PERSIAN_HEBREW 0 /*Hebrew, Arabic, Persian letters and all their forms*/ +#define LV_FONT_SIMSUN_14_CJK 0 /*1000 most common CJK radicals*/ +#define LV_FONT_SIMSUN_16_CJK 0 /*1000 most common CJK radicals*/ + +/*Pixel perfect monospace fonts*/ +#define LV_FONT_UNSCII_8 0 +#define LV_FONT_UNSCII_16 0 + +/*Optionally declare custom fonts here. + *You can use these fonts as default font too and they will be available globally. + *E.g. #define LV_FONT_CUSTOM_DECLARE LV_FONT_DECLARE(my_font_1) LV_FONT_DECLARE(my_font_2)*/ +#define LV_FONT_CUSTOM_DECLARE + +/*Always set a default font*/ +#define LV_FONT_DEFAULT &lv_font_montserrat_14 + +/*Enable handling large font and/or fonts with a lot of characters. + *The limit depends on the font size, font face and bpp. + *Compiler error will be triggered if a font needs it.*/ +#define LV_FONT_FMT_TXT_LARGE 0 + +/*Enables/disables support for compressed fonts.*/ +#define LV_USE_FONT_COMPRESSED 0 + +/*Enable drawing placeholders when glyph dsc is not found*/ +#define LV_USE_FONT_PLACEHOLDER 1 + +/*================= + * TEXT SETTINGS + *=================*/ + +/** + * Select a character encoding for strings. + * Your IDE or editor should have the same character encoding + * - LV_TXT_ENC_UTF8 + * - LV_TXT_ENC_ASCII + */ +#define LV_TXT_ENC LV_TXT_ENC_UTF8 + +/*Can break (wrap) texts on these chars*/ +#define LV_TXT_BREAK_CHARS " ,.;:-_)]}" + +/*If a word is at least this long, will break wherever "prettiest" + *To disable, set to a value <= 0*/ +#define LV_TXT_LINE_BREAK_LONG_LEN 0 + +/*Minimum number of characters in a long word to put on a line before a break. + *Depends on LV_TXT_LINE_BREAK_LONG_LEN.*/ +#define LV_TXT_LINE_BREAK_LONG_PRE_MIN_LEN 3 + +/*Minimum number of characters in a long word to put on a line after a break. + *Depends on LV_TXT_LINE_BREAK_LONG_LEN.*/ +#define LV_TXT_LINE_BREAK_LONG_POST_MIN_LEN 3 + +/*Support bidirectional texts. Allows mixing Left-to-Right and Right-to-Left texts. + *The direction will be processed according to the Unicode Bidirectional Algorithm: + *https://www.w3.org/International/articles/inline-bidi-markup/uba-basics*/ +#define LV_USE_BIDI 0 +#if LV_USE_BIDI + /*Set the default direction. Supported values: + *`LV_BASE_DIR_LTR` Left-to-Right + *`LV_BASE_DIR_RTL` Right-to-Left + *`LV_BASE_DIR_AUTO` detect texts base direction*/ + #define LV_BIDI_BASE_DIR_DEF LV_BASE_DIR_AUTO +#endif + +/*Enable Arabic/Persian processing + *In these languages characters should be replaced with another form based on their position in the text*/ +#define LV_USE_ARABIC_PERSIAN_CHARS 0 + +/*================== + * WIDGETS + *================*/ + +/*Documentation of the widgets: https://docs.lvgl.io/latest/en/html/widgets/index.html*/ + +#define LV_WIDGETS_HAS_DEFAULT_VALUE 1 + +#define LV_USE_ANIMIMG 1 + +#define LV_USE_ARC 1 + +#define LV_USE_BAR 1 + +#define LV_USE_BUTTON 1 + +#define LV_USE_BUTTONMATRIX 1 + +#define LV_USE_CALENDAR 1 +#if LV_USE_CALENDAR + #define LV_CALENDAR_WEEK_STARTS_MONDAY 0 + #if LV_CALENDAR_WEEK_STARTS_MONDAY + #define LV_CALENDAR_DEFAULT_DAY_NAMES {"Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"} + #else + #define LV_CALENDAR_DEFAULT_DAY_NAMES {"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"} + #endif + + #define LV_CALENDAR_DEFAULT_MONTH_NAMES {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"} + #define LV_USE_CALENDAR_HEADER_ARROW 1 + #define LV_USE_CALENDAR_HEADER_DROPDOWN 1 + #define LV_USE_CALENDAR_CHINESE 0 +#endif /*LV_USE_CALENDAR*/ + +#define LV_USE_CANVAS 1 + +#define LV_USE_CHART 1 + +#define LV_USE_CHECKBOX 1 + +#define LV_USE_DROPDOWN 1 /*Requires: lv_label*/ + +#define LV_USE_IMAGE 1 /*Requires: lv_label*/ + +#define LV_USE_IMAGEBUTTON 1 + +#define LV_USE_KEYBOARD 1 + +#define LV_USE_LABEL 1 +#if LV_USE_LABEL + #define LV_LABEL_TEXT_SELECTION 1 /*Enable selecting text of the label*/ + #define LV_LABEL_LONG_TXT_HINT 1 /*Store some extra info in labels to speed up drawing of very long texts*/ + #define LV_LABEL_WAIT_CHAR_COUNT 3 /*The count of wait chart*/ +#endif + +#define LV_USE_LED 1 + +#define LV_USE_LINE 1 + +#define LV_USE_LIST 1 + +#define LV_USE_LOTTIE 0 /*Requires: lv_canvas, thorvg */ + +#define LV_USE_MENU 1 + +#define LV_USE_MSGBOX 1 + +#define LV_USE_ROLLER 1 /*Requires: lv_label*/ + +#define LV_USE_SCALE 1 + +#define LV_USE_SLIDER 1 /*Requires: lv_bar*/ + +#define LV_USE_SPAN 1 +#if LV_USE_SPAN + /*A line text can contain maximum num of span descriptor */ + #define LV_SPAN_SNIPPET_STACK_SIZE 64 +#endif + +#define LV_USE_SPINBOX 1 + +#define LV_USE_SPINNER 1 + +#define LV_USE_SWITCH 1 + +#define LV_USE_TEXTAREA 1 /*Requires: lv_label*/ +#if LV_USE_TEXTAREA != 0 + #define LV_TEXTAREA_DEF_PWD_SHOW_TIME 1500 /*ms*/ +#endif + +#define LV_USE_TABLE 1 + +#define LV_USE_TABVIEW 1 + +#define LV_USE_TILEVIEW 1 + +#define LV_USE_WIN 1 + +/*================== + * THEMES + *==================*/ + +/*A simple, impressive and very complete theme*/ +#define LV_USE_THEME_DEFAULT 1 +#if LV_USE_THEME_DEFAULT + + /*0: Light mode; 1: Dark mode*/ + #define LV_THEME_DEFAULT_DARK 0 + + /*1: Enable grow on press*/ + #define LV_THEME_DEFAULT_GROW 1 + + /*Default transition time in [ms]*/ + #define LV_THEME_DEFAULT_TRANSITION_TIME 80 +#endif /*LV_USE_THEME_DEFAULT*/ + +/*A very simple theme that is a good starting point for a custom theme*/ +#define LV_USE_THEME_SIMPLE 1 + +/*A theme designed for monochrome displays*/ +#define LV_USE_THEME_MONO 1 + +/*================== + * LAYOUTS + *==================*/ + +/*A layout similar to Flexbox in CSS.*/ +#define LV_USE_FLEX 1 + +/*A layout similar to Grid in CSS.*/ +#define LV_USE_GRID 1 + +/*==================== + * 3RD PARTS LIBRARIES + *====================*/ + +/*File system interfaces for common APIs */ + +/*Setting a default driver letter allows skipping the driver prefix in filepaths*/ +#define LV_FS_DEFAULT_DRIVE_LETTER '\0' + +/*API for fopen, fread, etc*/ +#define LV_USE_FS_STDIO 0 +#if LV_USE_FS_STDIO + #define LV_FS_STDIO_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/ + #define LV_FS_STDIO_PATH "" /*Set the working directory. File/directory paths will be appended to it.*/ + #define LV_FS_STDIO_CACHE_SIZE 0 /*>0 to cache this number of bytes in lv_fs_read()*/ +#endif + +/*API for open, read, etc*/ +#define LV_USE_FS_POSIX 0 +#if LV_USE_FS_POSIX + #define LV_FS_POSIX_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/ + #define LV_FS_POSIX_PATH "" /*Set the working directory. File/directory paths will be appended to it.*/ + #define LV_FS_POSIX_CACHE_SIZE 0 /*>0 to cache this number of bytes in lv_fs_read()*/ +#endif + +/*API for CreateFile, ReadFile, etc*/ +#define LV_USE_FS_WIN32 0 +#if LV_USE_FS_WIN32 + #define LV_FS_WIN32_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/ + #define LV_FS_WIN32_PATH "" /*Set the working directory. File/directory paths will be appended to it.*/ + #define LV_FS_WIN32_CACHE_SIZE 0 /*>0 to cache this number of bytes in lv_fs_read()*/ +#endif + +/*API for FATFS (needs to be added separately). Uses f_open, f_read, etc*/ +#define LV_USE_FS_FATFS 0 +#if LV_USE_FS_FATFS + #define LV_FS_FATFS_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/ + #define LV_FS_FATFS_CACHE_SIZE 0 /*>0 to cache this number of bytes in lv_fs_read()*/ +#endif + +/*API for memory-mapped file access. */ +#define LV_USE_FS_MEMFS 0 +#if LV_USE_FS_MEMFS + #define LV_FS_MEMFS_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/ +#endif + +/*API for LittleFs. */ +#define LV_USE_FS_LITTLEFS 0 +#if LV_USE_FS_LITTLEFS + #define LV_FS_LITTLEFS_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/ +#endif + +/*API for Arduino LittleFs. */ +#define LV_USE_FS_ARDUINO_ESP_LITTLEFS 0 +#if LV_USE_FS_ARDUINO_ESP_LITTLEFS + #define LV_FS_ARDUINO_ESP_LITTLEFS_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/ +#endif + +/*API for Arduino Sd. */ +#define LV_USE_FS_ARDUINO_SD 0 +#if LV_USE_FS_ARDUINO_SD + #define LV_FS_ARDUINO_SD_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/ +#endif + +/*LODEPNG decoder library*/ +#define LV_USE_LODEPNG 0 + +/*PNG decoder(libpng) library*/ +#define LV_USE_LIBPNG 0 + +/*BMP decoder library*/ +#define LV_USE_BMP 0 + +/* JPG + split JPG decoder library. + * Split JPG is a custom format optimized for embedded systems. */ +#define LV_USE_TJPGD 0 + +/* libjpeg-turbo decoder library. + * Supports complete JPEG specifications and high-performance JPEG decoding. */ +#define LV_USE_LIBJPEG_TURBO 0 + +/*GIF decoder library*/ +#define LV_USE_GIF 0 +#if LV_USE_GIF + /*GIF decoder accelerate*/ + #define LV_GIF_CACHE_DECODE_DATA 0 +#endif + + +/*Decode bin images to RAM*/ +#define LV_BIN_DECODER_RAM_LOAD 0 + +/*RLE decompress library*/ +#define LV_USE_RLE 0 + +/*QR code library*/ +#define LV_USE_QRCODE 0 + +/*Barcode code library*/ +#define LV_USE_BARCODE 0 + +/*FreeType library*/ +#define LV_USE_FREETYPE 0 +#if LV_USE_FREETYPE + /*Let FreeType to use LVGL memory and file porting*/ + #define LV_FREETYPE_USE_LVGL_PORT 0 + + /*Cache count of the glyphs in FreeType. It means the number of glyphs that can be cached. + *The higher the value, the more memory will be used.*/ + #define LV_FREETYPE_CACHE_FT_GLYPH_CNT 256 +#endif + +/* Built-in TTF decoder */ +#define LV_USE_TINY_TTF 0 +#if LV_USE_TINY_TTF + /* Enable loading TTF data from files */ + #define LV_TINY_TTF_FILE_SUPPORT 0 + #define LV_TINY_TTF_CACHE_GLYPH_CNT 256 +#endif + +/*Rlottie library*/ +#define LV_USE_RLOTTIE 0 + +/*Enable Vector Graphic APIs + *Requires `LV_USE_MATRIX = 1`*/ +#define LV_USE_VECTOR_GRAPHIC 0 + +/* Enable ThorVG (vector graphics library) from the src/libs folder */ +#define LV_USE_THORVG_INTERNAL 0 + +/* Enable ThorVG by assuming that its installed and linked to the project */ +#define LV_USE_THORVG_EXTERNAL 0 + +/*Use lvgl built-in LZ4 lib*/ +#define LV_USE_LZ4_INTERNAL 0 + +/*Use external LZ4 library*/ +#define LV_USE_LZ4_EXTERNAL 0 + +/*FFmpeg library for image decoding and playing videos + *Supports all major image formats so do not enable other image decoder with it*/ +#define LV_USE_FFMPEG 0 +#if LV_USE_FFMPEG + /*Dump input information to stderr*/ + #define LV_FFMPEG_DUMP_FORMAT 0 +#endif + +/*================== + * OTHERS + *==================*/ + +/*1: Enable API to take snapshot for object*/ +#define LV_USE_SNAPSHOT 0 + +/*1: Enable system monitor component*/ +#define LV_USE_SYSMON 0 +#if LV_USE_SYSMON + /*Get the idle percentage. E.g. uint32_t my_get_idle(void);*/ + #define LV_SYSMON_GET_IDLE lv_timer_get_idle + + /*1: Show CPU usage and FPS count + * Requires `LV_USE_SYSMON = 1`*/ + #define LV_USE_PERF_MONITOR 0 + #if LV_USE_PERF_MONITOR + #define LV_USE_PERF_MONITOR_POS LV_ALIGN_BOTTOM_RIGHT + + /*0: Displays performance data on the screen, 1: Prints performance data using log.*/ + #define LV_USE_PERF_MONITOR_LOG_MODE 0 + #endif + + /*1: Show the used memory and the memory fragmentation + * Requires `LV_USE_STDLIB_MALLOC = LV_STDLIB_BUILTIN` + * Requires `LV_USE_SYSMON = 1`*/ + #define LV_USE_MEM_MONITOR 0 + #if LV_USE_MEM_MONITOR + #define LV_USE_MEM_MONITOR_POS LV_ALIGN_BOTTOM_LEFT + #endif + +#endif /*LV_USE_SYSMON*/ + +/*1: Enable the runtime performance profiler*/ +#define LV_USE_PROFILER 0 +#if LV_USE_PROFILER + /*1: Enable the built-in profiler*/ + #define LV_USE_PROFILER_BUILTIN 1 + #if LV_USE_PROFILER_BUILTIN + /*Default profiler trace buffer size*/ + #define LV_PROFILER_BUILTIN_BUF_SIZE (16 * 1024) /*[bytes]*/ + #endif + + /*Header to include for the profiler*/ + #define LV_PROFILER_INCLUDE "lvgl/src/misc/lv_profiler_builtin.h" + + /*Profiler start point function*/ + #define LV_PROFILER_BEGIN LV_PROFILER_BUILTIN_BEGIN + + /*Profiler end point function*/ + #define LV_PROFILER_END LV_PROFILER_BUILTIN_END + + /*Profiler start point function with custom tag*/ + #define LV_PROFILER_BEGIN_TAG LV_PROFILER_BUILTIN_BEGIN_TAG + + /*Profiler end point function with custom tag*/ + #define LV_PROFILER_END_TAG LV_PROFILER_BUILTIN_END_TAG +#endif + +/*1: Enable Monkey test*/ +#define LV_USE_MONKEY 0 + +/*1: Enable grid navigation*/ +#define LV_USE_GRIDNAV 0 + +/*1: Enable lv_obj fragment*/ +#define LV_USE_FRAGMENT 0 + +/*1: Support using images as font in label or span widgets */ +#define LV_USE_IMGFONT 0 + +/*1: Enable an observer pattern implementation*/ +#define LV_USE_OBSERVER 1 + +/*1: Enable Pinyin input method*/ +/*Requires: lv_keyboard*/ +#define LV_USE_IME_PINYIN 0 +#if LV_USE_IME_PINYIN + /*1: Use default thesaurus*/ + /*If you do not use the default thesaurus, be sure to use `lv_ime_pinyin` after setting the thesaurus*/ + #define LV_IME_PINYIN_USE_DEFAULT_DICT 1 + /*Set the maximum number of candidate panels that can be displayed*/ + /*This needs to be adjusted according to the size of the screen*/ + #define LV_IME_PINYIN_CAND_TEXT_NUM 6 + + /*Use 9 key input(k9)*/ + #define LV_IME_PINYIN_USE_K9_MODE 1 + #if LV_IME_PINYIN_USE_K9_MODE == 1 + #define LV_IME_PINYIN_K9_CAND_TEXT_NUM 3 + #endif /*LV_IME_PINYIN_USE_K9_MODE*/ +#endif + +/*1: Enable file explorer*/ +/*Requires: lv_table*/ +#define LV_USE_FILE_EXPLORER 0 +#if LV_USE_FILE_EXPLORER + /*Maximum length of path*/ + #define LV_FILE_EXPLORER_PATH_MAX_LEN (128) + /*Quick access bar, 1:use, 0:not use*/ + /*Requires: lv_list*/ + #define LV_FILE_EXPLORER_QUICK_ACCESS 1 +#endif + +/*================== + * DEVICES + *==================*/ + +/*Use SDL to open window on PC and handle mouse and keyboard*/ +#define LV_USE_SDL 0 +#if LV_USE_SDL + #define LV_SDL_INCLUDE_PATH + #define LV_SDL_RENDER_MODE LV_DISPLAY_RENDER_MODE_DIRECT /*LV_DISPLAY_RENDER_MODE_DIRECT is recommended for best performance*/ + #define LV_SDL_BUF_COUNT 1 /*1 or 2*/ + #define LV_SDL_ACCELERATED 1 /*1: Use hardware acceleration*/ + #define LV_SDL_FULLSCREEN 0 /*1: Make the window full screen by default*/ + #define LV_SDL_DIRECT_EXIT 1 /*1: Exit the application when all SDL windows are closed*/ + #define LV_SDL_MOUSEWHEEL_MODE LV_SDL_MOUSEWHEEL_MODE_ENCODER /*LV_SDL_MOUSEWHEEL_MODE_ENCODER/CROWN*/ +#endif + +/*Use X11 to open window on Linux desktop and handle mouse and keyboard*/ +#define LV_USE_X11 0 +#if LV_USE_X11 + #define LV_X11_DIRECT_EXIT 1 /*Exit the application when all X11 windows have been closed*/ + #define LV_X11_DOUBLE_BUFFER 1 /*Use double buffers for rendering*/ + /*select only 1 of the following render modes (LV_X11_RENDER_MODE_PARTIAL preferred!)*/ + #define LV_X11_RENDER_MODE_PARTIAL 1 /*Partial render mode (preferred)*/ + #define LV_X11_RENDER_MODE_DIRECT 0 /*direct render mode*/ + #define LV_X11_RENDER_MODE_FULL 0 /*Full render mode*/ +#endif + +/*Use Wayland to open a window and handle input on Linux or BSD desktops */ +#define LV_USE_WAYLAND 0 +#if LV_USE_WAYLAND + #define LV_WAYLAND_WINDOW_DECORATIONS 0 /*Draw client side window decorations only necessary on Mutter/GNOME*/ + #define LV_WAYLAND_WL_SHELL 0 /*Use the legacy wl_shell protocol instead of the default XDG shell*/ +#endif + +/*Driver for /dev/fb*/ +#define LV_USE_LINUX_FBDEV 0 +#if LV_USE_LINUX_FBDEV + #define LV_LINUX_FBDEV_BSD 0 + #define LV_LINUX_FBDEV_RENDER_MODE LV_DISPLAY_RENDER_MODE_PARTIAL + #define LV_LINUX_FBDEV_BUFFER_COUNT 0 + #define LV_LINUX_FBDEV_BUFFER_SIZE 60 +#endif + +/*Use Nuttx to open window and handle touchscreen*/ +#define LV_USE_NUTTX 0 + +#if LV_USE_NUTTX + #define LV_USE_NUTTX_LIBUV 0 + + /*Use Nuttx custom init API to open window and handle touchscreen*/ + #define LV_USE_NUTTX_CUSTOM_INIT 0 + + /*Driver for /dev/lcd*/ + #define LV_USE_NUTTX_LCD 0 + #if LV_USE_NUTTX_LCD + #define LV_NUTTX_LCD_BUFFER_COUNT 0 + #define LV_NUTTX_LCD_BUFFER_SIZE 60 + #endif + + /*Driver for /dev/input*/ + #define LV_USE_NUTTX_TOUCHSCREEN 0 + +#endif + +/*Driver for /dev/dri/card*/ +#define LV_USE_LINUX_DRM 0 + +/*Interface for TFT_eSPI*/ +#define LV_USE_TFT_ESPI 1 + +/*Driver for evdev input devices*/ +#define LV_USE_EVDEV 0 + +/*Driver for libinput input devices*/ +#define LV_USE_LIBINPUT 0 + +#if LV_USE_LIBINPUT + #define LV_LIBINPUT_BSD 0 + + /*Full keyboard support*/ + #define LV_LIBINPUT_XKB 0 + #if LV_LIBINPUT_XKB + /*"setxkbmap -query" can help find the right values for your keyboard*/ + #define LV_LIBINPUT_XKB_KEY_MAP { .rules = NULL, .model = "pc101", .layout = "us", .variant = NULL, .options = NULL } + #endif +#endif + +/*Drivers for LCD devices connected via SPI/parallel port*/ +#define LV_USE_ST7735 0 +#define LV_USE_ST7789 0 +#define LV_USE_ST7796 0 +#define LV_USE_ILI9341 0 + +#define LV_USE_GENERIC_MIPI (LV_USE_ST7735 | LV_USE_ST7789 | LV_USE_ST7796 | LV_USE_ILI9341) + +/*Driver for Renesas GLCD*/ +#define LV_USE_RENESAS_GLCDC 0 + +/* LVGL Windows backend */ +#define LV_USE_WINDOWS 0 + +/* Use OpenGL to open window on PC and handle mouse and keyboard */ +#define LV_USE_OPENGLES 0 +#if LV_USE_OPENGLES + #define LV_USE_OPENGLES_DEBUG 1 /* Enable or disable debug for opengles */ +#endif + +/* QNX Screen display and input drivers */ +#define LV_USE_QNX 0 +#if LV_USE_QNX + #define LV_QNX_BUF_COUNT 1 /*1 or 2*/ +#endif + +/*================== +* EXAMPLES +*==================*/ + +/*Enable the examples to be built with the library*/ +#define LV_BUILD_EXAMPLES 1 + +/*=================== + * DEMO USAGE + ====================*/ + +/*Show some widget. It might be required to increase `LV_MEM_SIZE` */ +#define LV_USE_DEMO_WIDGETS 0 + +/*Demonstrate the usage of encoder and keyboard*/ +#define LV_USE_DEMO_KEYPAD_AND_ENCODER 0 + +/*Benchmark your system*/ +#define LV_USE_DEMO_BENCHMARK 0 + +/*Render test for each primitives. Requires at least 480x272 display*/ +#define LV_USE_DEMO_RENDER 0 + +/*Stress test for LVGL*/ +#define LV_USE_DEMO_STRESS 0 + +/*Music player demo*/ +#define LV_USE_DEMO_MUSIC 0 +#if LV_USE_DEMO_MUSIC + #define LV_DEMO_MUSIC_SQUARE 0 + #define LV_DEMO_MUSIC_LANDSCAPE 0 + #define LV_DEMO_MUSIC_ROUND 0 + #define LV_DEMO_MUSIC_LARGE 0 + #define LV_DEMO_MUSIC_AUTO_PLAY 0 +#endif + +/*Flex layout demo*/ +#define LV_USE_DEMO_FLEX_LAYOUT 0 + +/*Smart-phone like multi-language demo*/ +#define LV_USE_DEMO_MULTILANG 0 + +/*Widget transformation demo*/ +#define LV_USE_DEMO_TRANSFORM 0 + +/*Demonstrate scroll settings*/ +#define LV_USE_DEMO_SCROLL 0 + +/*Vector graphic demo*/ +#define LV_USE_DEMO_VECTOR_GRAPHIC 0 + +/*--END OF LV_CONF_H--*/ + +#endif /*LV_CONF_H*/ + +#endif /*End of "Content enable"*/ diff --git a/include/photo_list.h b/include/photo_list.h new file mode 100644 index 0000000..b6cd027 --- /dev/null +++ b/include/photo_list.h @@ -0,0 +1,21 @@ +#pragma once + +#include + +static constexpr int PHOTO_MAX = 64; +static constexpr int PHOTO_PATH_MAX = 96; + +enum PhotoKind : uint8_t { + PHOTO_JPG = 0, + PHOTO_PNG = 1, + PHOTO_GIF = 2, +}; + +struct PhotoEntry { + char path[PHOTO_PATH_MAX]; + PhotoKind kind; +}; + +uint16_t photo_list_count(); +const PhotoEntry *photo_list_get(uint16_t index); +uint16_t photo_list_scan(); // returns count diff --git a/include/photo_view.h b/include/photo_view.h new file mode 100644 index 0000000..583ba42 --- /dev/null +++ b/include/photo_view.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +#include "photo_list.h" + +bool photo_view_init(); +void photo_view_deinit(); + +// Decode entry into half-res RGB565 canvas (160x120, shown 2x). +bool photo_view_show(const PhotoEntry *entry); + +const lv_image_dsc_t *photo_view_dsc(); +uint16_t photo_view_canvas_w(); +uint16_t photo_view_canvas_h(); +uint16_t photo_view_img_w(); +uint16_t photo_view_img_h(); + +bool photo_view_is_gif(); +// Advance GIF frame if due; returns true when pixels changed. +bool photo_view_gif_tick(); diff --git a/include/pins.h b/include/pins.h new file mode 100644 index 0000000..44adc4c --- /dev/null +++ b/include/pins.h @@ -0,0 +1,24 @@ +#pragma once + +// CYD ESP32-2432S028R + +#define TFT_BL_PIN 21 + +// TF / microSD (VSPI;触摸用 SoftSPI,不抢总线) +#define SD_MISO 19 +#define SD_MOSI 23 +#define SD_SCK 18 +#define SD_CS 5 + +#define SPEAKER_PIN 26 + +#define CYD_TOUCH_IRQ 36 +#define CYD_TOUCH_MOSI 32 +#define CYD_TOUCH_MISO 39 +#define CYD_TOUCH_CLK 25 +#define CYD_TOUCH_CS 33 + +#define SCREEN_WIDTH 240 +#define SCREEN_HEIGHT 320 +#define UI_HOR_RES 320 +#define UI_VER_RES 240 diff --git a/include/sd_info.h b/include/sd_info.h new file mode 100644 index 0000000..6b6e755 --- /dev/null +++ b/include/sd_info.h @@ -0,0 +1,21 @@ +#pragma once + +#include + +struct SdCardInfo { + bool mounted; + bool present; + const char *type; + const char *filesystem; + uint64_t card_bytes; + uint64_t total_bytes; + uint64_t used_bytes; + uint64_t free_bytes; + uint32_t root_files; + String error; +}; + +bool sd_init(); +void sd_deinit(); +SdCardInfo sd_read_info(); +String format_bytes(uint64_t bytes); diff --git a/include/sd_put.h b/include/sd_put.h new file mode 100644 index 0000000..30deee2 --- /dev/null +++ b/include/sd_put.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +// 串口协议: "#PUT \n" -> READY -> raw -> DONE/FAIL +// 在 loop 里调用;返回 true 表示刚处理完一次传输 +bool sd_put_poll(); diff --git a/include/touch.h b/include/touch.h new file mode 100644 index 0000000..86e00fe --- /dev/null +++ b/include/touch.h @@ -0,0 +1,12 @@ +#pragma once + +#include +#include + +void touch_init(); +void touch_read_cb(lv_indev_t *indev, lv_indev_data_t *data); + +// 调试:最近一次触点(物理 / LVGL 旋转后逻辑) +extern int16_t g_dbg_phys_x, g_dbg_phys_y; +extern int16_t g_dbg_logic_x, g_dbg_logic_y; +extern bool g_dbg_touch_down; diff --git a/include/ui.h b/include/ui.h new file mode 100644 index 0000000..0cca269 --- /dev/null +++ b/include/ui.h @@ -0,0 +1,9 @@ +#pragma once + +#include "sd_info.h" +#include + +void ui_create(); +void ui_update(const SdCardInfo &info); +void ui_tick(); +void ui_reload_album(); // rescan TF and show current / first photo diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..819730e --- /dev/null +++ b/platformio.ini @@ -0,0 +1,59 @@ +; CYD ESP32-2432S028 — 电子相册(TF + 触控 + LVGL) +[env:cyd] +platform = espressif32 +board = esp32dev +framework = arduino +monitor_speed = 460800 +upload_speed = 460800 +board_build.partitions = default.csv + +lib_deps = + bodmer/TFT_eSPI@^2.5.43 + lvgl/lvgl@9.2.2 + bitbank2/JPEGDEC@^1.5.0 + bitbank2/PNGdec@^1.0.3 + bitbank2/AnimatedGIF@^2.1.1 + +; JPEGDEC 声明依赖 bb_spi_lcd,本项目不需要 +lib_ignore = + bb_spi_lcd + +build_flags = + -DCORE_DEBUG_LEVEL=0 + -DUSER_SETUP_LOADED=1 + -DILI9341_2_DRIVER=1 + -DTFT_WIDTH=240 + -DTFT_HEIGHT=320 + -DTFT_MISO=12 + -DTFT_MOSI=13 + -DTFT_SCLK=14 + -DTFT_CS=15 + -DTFT_DC=2 + -DTFT_RST=-1 + -DTFT_BL=21 + -DTFT_BACKLIGHT_ON=HIGH + -DTOUCH_CS=-1 + -DLOAD_GLCD=1 + -DLOAD_FONT2=1 + -DLOAD_FONT4=1 + -DLOAD_FONT6=1 + -DLOAD_FONT7=1 + -DLOAD_FONT8=1 + -DLOAD_GFXFF=1 + -DSMOOTH_FONT=1 + -DSPI_FREQUENCY=40000000 + -DSPI_READ_FREQUENCY=20000000 + -DLV_CONF_SKIP + -DLV_COLOR_DEPTH=16 + -DLV_MEM_SIZE=24576 + -DLV_FONT_MONTSERRAT_14=1 + -DLV_FONT_MONTSERRAT_16=1 + -DLV_FONT_MONTSERRAT_20=1 + -DLV_USE_TFT_ESPI=1 + -DLV_USE_FLEX=1 + -DLV_USE_BUTTON=1 + -DLV_USE_LABEL=1 + -DLV_USE_IMAGE=1 + -I${PROJECT_DIR}/include + +extra_scripts = pre:copy_lv_conf.py diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..42bd0e9 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,91 @@ +#include +#include + +#include "pins.h" +#include "photo_view.h" +#include "sd_info.h" +#include "sd_put.h" +#include "touch.h" +#include "ui.h" + +static constexpr uint32_t DRAW_BUF_SIZE = + (SCREEN_WIDTH * SCREEN_HEIGHT / 10 * (LV_COLOR_DEPTH / 8)); +static uint32_t draw_buf[DRAW_BUF_SIZE / 4]; + +static uint32_t last_refresh_ms = 0; +static constexpr uint32_t REFRESH_MS = 10000; + +static void refresh_sd_ui(bool rescan_photos) { + SdCardInfo info = sd_read_info(); + + Serial.println("---- TF Card ----"); + if (!info.mounted) { + Serial.printf("Error: %s\n", info.error.c_str()); + } else { + Serial.printf("Type: %s FS: %s Cap: %s\n", info.type, info.filesystem, + format_bytes(info.card_bytes).c_str()); + } + + ui_update(info); + if (rescan_photos && info.mounted) { + ui_reload_album(); + } +} + +void setup() { + Serial.setRxBufferSize(16384); + Serial.begin(460800); + delay(200); + Serial.println(); + Serial.printf("LVGL %d.%d.%d | CYD Photo Album\n", lv_version_major(), + lv_version_minor(), lv_version_patch()); + Serial.println("BOOT_OK"); + + pinMode(TFT_BL_PIN, OUTPUT); + digitalWrite(TFT_BL_PIN, HIGH); + + touch_init(); + + if (!photo_view_init()) { + Serial.println("WARN: photo canvas alloc failed"); + } + + lv_init(); + + lv_display_t *disp = + lv_tft_espi_create(SCREEN_WIDTH, SCREEN_HEIGHT, draw_buf, sizeof(draw_buf)); + lv_display_set_rotation(disp, LV_DISPLAY_ROTATION_270); + + lv_indev_t *indev = lv_indev_create(); + lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER); + lv_indev_set_read_cb(indev, touch_read_cb); + lv_indev_set_display(indev, disp); + + ui_create(); + + if (!sd_init()) { + Serial.println("SD mount failed at boot"); + } + + refresh_sd_ui(true); + last_refresh_ms = millis(); +} + +void loop() { + if (sd_put_poll()) { + refresh_sd_ui(true); + } + + lv_timer_handler(); + lv_tick_inc(5); + ui_tick(); + + const uint32_t now = millis(); + if (now - last_refresh_ms >= REFRESH_MS) { + last_refresh_ms = now; + // Keep TF status fresh; do not rescan photos (avoids reset while viewing) + refresh_sd_ui(false); + } + + delay(5); +} diff --git a/src/photo_list.cpp b/src/photo_list.cpp new file mode 100644 index 0000000..0f63c14 --- /dev/null +++ b/src/photo_list.cpp @@ -0,0 +1,101 @@ +#include "photo_list.h" + +#include +#include + +static PhotoEntry g_list[PHOTO_MAX]; +static uint16_t g_count = 0; + +static bool ends_with_ci(const char *name, const char *ext) { + const size_t n = strlen(name); + const size_t e = strlen(ext); + if (n < e) { + return false; + } + for (size_t i = 0; i < e; i++) { + char a = name[n - e + i]; + char b = ext[i]; + if (a >= 'A' && a <= 'Z') { + a = (char)(a - 'A' + 'a'); + } + if (b >= 'A' && b <= 'Z') { + b = (char)(b - 'A' + 'a'); + } + if (a != b) { + return false; + } + } + return true; +} + +static bool classify(const char *name, PhotoKind *kind) { + if (ends_with_ci(name, ".jpg") || ends_with_ci(name, ".jpeg")) { + *kind = PHOTO_JPG; + return true; + } + if (ends_with_ci(name, ".png")) { + *kind = PHOTO_PNG; + return true; + } + if (ends_with_ci(name, ".gif")) { + *kind = PHOTO_GIF; + return true; + } + return false; +} + +static void add_file(const char *dir, const char *name, PhotoKind kind) { + if (g_count >= PHOTO_MAX) { + return; + } + PhotoEntry *e = &g_list[g_count]; + if (dir[0] == '/' && dir[1] == '\0') { + snprintf(e->path, sizeof(e->path), "/%s", name); + } else { + snprintf(e->path, sizeof(e->path), "%s/%s", dir, name); + } + e->kind = kind; + g_count++; +} + +static void scan_dir(const char *dir) { + File root = SD.open(dir); + if (!root || !root.isDirectory()) { + if (root) { + root.close(); + } + return; + } + + File f = root.openNextFile(); + while (f) { + if (!f.isDirectory()) { + const char *name = f.name(); + // SD lib may return full path; keep basename + const char *base = strrchr(name, '/'); + base = base ? base + 1 : name; + PhotoKind kind; + if (classify(base, &kind)) { + add_file(dir, base, kind); + } + } + f = root.openNextFile(); + } + root.close(); +} + +uint16_t photo_list_count() { return g_count; } + +const PhotoEntry *photo_list_get(uint16_t index) { + if (index >= g_count) { + return nullptr; + } + return &g_list[index]; +} + +uint16_t photo_list_scan() { + g_count = 0; + scan_dir("/photos"); + scan_dir("/"); + return g_count; +} diff --git a/src/photo_view.cpp b/src/photo_view.cpp new file mode 100644 index 0000000..edd5c84 --- /dev/null +++ b/src/photo_view.cpp @@ -0,0 +1,496 @@ +#include "photo_view.h" +#include "pins.h" + +#include +#include +#include +#include +#include +#include + +// Half-res canvas (fits ESP32 DRAM). UI shows it at 2x -> 320x240. +static constexpr uint16_t CANVAS_W = UI_HOR_RES / 2; +static constexpr uint16_t CANVAS_H = UI_VER_RES / 2; +static constexpr size_t CANVAS_PIXELS = (size_t)CANVAS_W * (size_t)CANVAS_H; + +static uint16_t g_pixels[CANVAS_PIXELS]; +static bool g_ready = false; +static lv_image_dsc_t g_dsc; +static uint16_t g_img_w = 0; +static uint16_t g_img_h = 0; + +static JPEGDEC *g_jpeg = nullptr; +static PNG *g_png = nullptr; +static AnimatedGIF *g_gif = nullptr; + +static File g_file; +static bool g_gif_open = false; +static uint32_t g_gif_next_ms = 0; +static int g_off_x = 0; +static int g_off_y = 0; +static int g_src_w = 0; +static int g_src_h = 0; +static int g_dst_w = 0; +static int g_dst_h = 0; + +static void canvas_clear(uint16_t color) { + for (size_t i = 0; i < CANVAS_PIXELS; i++) { + g_pixels[i] = color; + } +} + +static void canvas_sync_dsc() { + g_dsc.header.magic = LV_IMAGE_HEADER_MAGIC; + g_dsc.header.cf = LV_COLOR_FORMAT_RGB565; + g_dsc.header.flags = 0; + g_dsc.header.w = CANVAS_W; + g_dsc.header.h = CANVAS_H; + g_dsc.header.stride = CANVAS_W * 2; + g_dsc.data_size = CANVAS_PIXELS * 2; + g_dsc.data = (const uint8_t *)g_pixels; + g_dsc.reserved = nullptr; +} + +static void free_jpeg() { + if (g_jpeg) { + delete g_jpeg; + g_jpeg = nullptr; + } +} + +static void free_png() { + if (g_png) { + delete g_png; + g_png = nullptr; + } +} + +static void gif_close_session() { + if (g_gif_open && g_gif) { + g_gif->close(); + g_gif_open = false; + } + if (g_gif) { + delete g_gif; + g_gif = nullptr; + } +} + +static void fit_size(int nw, int nh, int *dw, int *dh) { + *dw = nw; + *dh = nh; + if (*dw <= (int)CANVAS_W && *dh <= (int)CANVAS_H) { + return; + } + const float sx = (float)CANVAS_W / (float)nw; + const float sy = (float)CANVAS_H / (float)nh; + const float s = sx < sy ? sx : sy; + *dw = (int)(nw * s + 0.5f); + *dh = (int)(nh * s + 0.5f); + if (*dw < 1) { + *dw = 1; + } + if (*dh < 1) { + *dh = 1; + } +} + +static int jpeg_scale_for(int w, int h) { + if (w > CANVAS_W * 4 || h > CANVAS_H * 4) { + return JPEG_SCALE_EIGHTH; + } + if (w > CANVAS_W * 2 || h > CANVAS_H * 2) { + return JPEG_SCALE_QUARTER; + } + if (w > CANVAS_W || h > CANVAS_H) { + return JPEG_SCALE_HALF; + } + return 0; +} + +static void *jpeg_open(const char *filename, int32_t *size) { + g_file = SD.open(filename); + if (!g_file) { + return nullptr; + } + *size = (int32_t)g_file.size(); + return &g_file; +} + +static void jpeg_close(void *handle) { + if (handle) { + ((File *)handle)->close(); + } +} + +static int32_t jpeg_read(JPEGFILE *handle, uint8_t *buffer, int32_t length) { + if (!handle || !handle->fHandle) { + return 0; + } + return (int32_t)((File *)handle->fHandle)->read(buffer, length); +} + +static int32_t jpeg_seek(JPEGFILE *handle, int32_t position) { + if (!handle || !handle->fHandle) { + return 0; + } + return ((File *)handle->fHandle)->seek(position) ? position : 0; +} + +static int jpeg_draw(JPEGDRAW *p) { + if (!p || !p->pPixels) { + return 0; + } + for (int row = 0; row < p->iHeight; row++) { + const int y = p->y + row; + if (y < 0 || y >= (int)CANVAS_H) { + continue; + } + const uint16_t *src = p->pPixels + row * p->iWidth; + for (int col = 0; col < p->iWidth; col++) { + const int x = p->x + col; + if (x < 0 || x >= (int)CANVAS_W) { + continue; + } + g_pixels[(size_t)y * CANVAS_W + (size_t)x] = src[col]; + } + } + return 1; +} + +static void *png_open(const char *filename, int32_t *size) { + g_file = SD.open(filename); + if (!g_file) { + return nullptr; + } + *size = (int32_t)g_file.size(); + return &g_file; +} + +static void png_close(void *handle) { + if (handle) { + ((File *)handle)->close(); + } +} + +static int32_t png_read(PNGFILE *handle, uint8_t *buffer, int32_t length) { + if (!handle || !handle->fHandle) { + return 0; + } + return (int32_t)((File *)handle->fHandle)->read(buffer, length); +} + +static int32_t png_seek(PNGFILE *handle, int32_t position) { + if (!handle || !handle->fHandle) { + return 0; + } + return ((File *)handle->fHandle)->seek(position) ? position : 0; +} + +static int png_draw(PNGDRAW *p) { + if (!p || !g_png || g_src_w <= 0 || g_dst_w <= 0) { + return 0; + } + + // Line buffer for source width (cap to avoid stack blow) + static uint16_t line[640]; + if (p->iWidth > 640) { + return 0; + } + g_png->getLineAsRGB565(p, line, PNG_RGB565_LITTLE_ENDIAN, 0x0000); + + // Nearest-neighbor: which dest rows map from this source row? + for (int dy = 0; dy < g_dst_h; dy++) { + const int sy = (dy * g_src_h) / g_dst_h; + if (sy != p->y) { + continue; + } + const int y = g_off_y + dy; + if (y < 0 || y >= (int)CANVAS_H) { + continue; + } + for (int dx = 0; dx < g_dst_w; dx++) { + const int sx = (dx * g_src_w) / g_dst_w; + if (sx < 0 || sx >= p->iWidth) { + continue; + } + const int x = g_off_x + dx; + if (x < 0 || x >= (int)CANVAS_W) { + continue; + } + g_pixels[(size_t)y * CANVAS_W + (size_t)x] = line[sx]; + } + } + return 1; +} + +static void *gif_open(const char *fname, int32_t *pSize) { + g_file = SD.open(fname); + if (!g_file) { + return nullptr; + } + *pSize = (int32_t)g_file.size(); + return &g_file; +} + +static void gif_close(void *pHandle) { + if (pHandle) { + ((File *)pHandle)->close(); + } +} + +static int32_t gif_read(GIFFILE *pFile, uint8_t *pBuf, int32_t iLen) { + if (!pFile || !pFile->fHandle) { + return 0; + } + int32_t n = (int32_t)((File *)pFile->fHandle)->read(pBuf, iLen); + pFile->iPos = (int32_t)((File *)pFile->fHandle)->position(); + return n; +} + +static int32_t gif_seek(GIFFILE *pFile, int32_t iPosition) { + if (!pFile || !pFile->fHandle) { + return 0; + } + ((File *)pFile->fHandle)->seek(iPosition); + pFile->iPos = (int32_t)((File *)pFile->fHandle)->position(); + return pFile->iPos; +} + +static void gif_draw(GIFDRAW *pDraw) { + if (!pDraw || g_src_w <= 0 || g_dst_w <= 0) { + return; + } + // Map source frame lines into scaled dest canvas + const int src_y = pDraw->iY + pDraw->y; + for (int dy = 0; dy < g_dst_h; dy++) { + const int sy = (dy * g_src_h) / g_dst_h; + if (sy != src_y) { + continue; + } + const int y = g_off_y + dy; + if (y < 0 || y >= (int)CANVAS_H) { + continue; + } + for (int dx = 0; dx < g_dst_w; dx++) { + const int sx = (dx * g_src_w) / g_dst_w; + const int lx = sx - pDraw->iX; + if (lx < 0 || lx >= pDraw->iWidth) { + continue; + } + const uint8_t idx = pDraw->pPixels[lx]; + if (pDraw->ucHasTransparency && idx == pDraw->ucTransparent) { + continue; + } + const int x = g_off_x + dx; + if (x < 0 || x >= (int)CANVAS_W) { + continue; + } + g_pixels[(size_t)y * CANVAS_W + (size_t)x] = pDraw->pPalette[idx]; + } + } +} + +static bool load_jpeg(const char *path) { + gif_close_session(); + free_png(); + free_jpeg(); + + g_jpeg = new JPEGDEC(); + if (!g_jpeg) { + return false; + } + + if (!g_jpeg->open(path, jpeg_open, jpeg_close, jpeg_read, jpeg_seek, + jpeg_draw)) { + Serial.printf("JPEG open fail: %s err=%d\n", path, g_jpeg->getLastError()); + free_jpeg(); + return false; + } + + const int nw = g_jpeg->getWidth(); + const int nh = g_jpeg->getHeight(); + const int scale = jpeg_scale_for(nw, nh); + int dw = nw; + int dh = nh; + if (scale == JPEG_SCALE_HALF) { + dw /= 2; + dh /= 2; + } else if (scale == JPEG_SCALE_QUARTER) { + dw /= 4; + dh /= 4; + } else if (scale == JPEG_SCALE_EIGHTH) { + dw /= 8; + dh /= 8; + } + + g_img_w = (uint16_t)dw; + g_img_h = (uint16_t)dh; + const int xoff = ((int)CANVAS_W - dw) / 2; + const int yoff = ((int)CANVAS_H - dh) / 2; + + canvas_clear(0x0000); + g_jpeg->setPixelType(RGB565_LITTLE_ENDIAN); + const bool ok = g_jpeg->decode(xoff, yoff, scale); + g_jpeg->close(); + free_jpeg(); + if (!ok) { + Serial.printf("JPEG decode fail: %s\n", path); + } + return ok; +} + +static bool load_png(const char *path) { + gif_close_session(); + free_jpeg(); + free_png(); + + g_png = new PNG(); + if (!g_png) { + return false; + } + + const int rc = + g_png->open(path, png_open, png_close, png_read, png_seek, png_draw); + if (rc != PNG_SUCCESS) { + Serial.printf("PNG open fail: %s rc=%d\n", path, rc); + free_png(); + return false; + } + + g_src_w = g_png->getWidth(); + g_src_h = g_png->getHeight(); + if (g_src_w <= 0 || g_src_h <= 0 || g_src_w > 640) { + Serial.printf("PNG bad size %dx%d\n", g_src_w, g_src_h); + g_png->close(); + free_png(); + return false; + } + + fit_size(g_src_w, g_src_h, &g_dst_w, &g_dst_h); + g_img_w = (uint16_t)g_dst_w; + g_img_h = (uint16_t)g_dst_h; + g_off_x = ((int)CANVAS_W - g_dst_w) / 2; + g_off_y = ((int)CANVAS_H - g_dst_h) / 2; + + canvas_clear(0x0000); + const int drc = g_png->decode(nullptr, 0); + g_png->close(); + free_png(); + if (drc != PNG_SUCCESS) { + Serial.printf("PNG decode fail: %s rc=%d\n", path, drc); + return false; + } + return true; +} + +static bool load_gif(const char *path) { + free_jpeg(); + free_png(); + gif_close_session(); + + g_gif = new AnimatedGIF(); + if (!g_gif) { + return false; + } + + g_gif->begin(GIF_PALETTE_RGB565_LE); + if (!g_gif->open(path, gif_open, gif_close, gif_read, gif_seek, gif_draw)) { + Serial.printf("GIF open fail: %s\n", path); + gif_close_session(); + return false; + } + + g_src_w = g_gif->getCanvasWidth(); + g_src_h = g_gif->getCanvasHeight(); + if (g_src_w <= 0 || g_src_h <= 0) { + gif_close_session(); + return false; + } + + fit_size(g_src_w, g_src_h, &g_dst_w, &g_dst_h); + g_img_w = (uint16_t)g_dst_w; + g_img_h = (uint16_t)g_dst_h; + g_off_x = ((int)CANVAS_W - g_dst_w) / 2; + g_off_y = ((int)CANVAS_H - g_dst_h) / 2; + g_gif_open = true; + + canvas_clear(0x0000); + int delay_ms = 0; + g_gif->playFrame(true, &delay_ms); + if (delay_ms < 20) { + delay_ms = 100; + } + g_gif_next_ms = millis() + (uint32_t)delay_ms; + return true; +} + +bool photo_view_init() { + if (g_ready) { + return true; + } + canvas_clear(0x0000); + canvas_sync_dsc(); + g_ready = true; + Serial.printf("photo_view: canvas %ux%u (%u bytes), free heap %u\n", + (unsigned)CANVAS_W, (unsigned)CANVAS_H, + (unsigned)(CANVAS_PIXELS * 2), (unsigned)ESP.getFreeHeap()); + return true; +} + +void photo_view_deinit() { + gif_close_session(); + free_jpeg(); + free_png(); + g_ready = false; + g_dsc.data = nullptr; +} + +bool photo_view_show(const PhotoEntry *entry) { + if (!entry || !photo_view_init()) { + return false; + } + + bool ok = false; + switch (entry->kind) { + case PHOTO_JPG: + ok = load_jpeg(entry->path); + break; + case PHOTO_PNG: + ok = load_png(entry->path); + break; + case PHOTO_GIF: + ok = load_gif(entry->path); + break; + } + canvas_sync_dsc(); + return ok; +} + +const lv_image_dsc_t *photo_view_dsc() { + return g_ready ? &g_dsc : nullptr; +} + +uint16_t photo_view_canvas_w() { return CANVAS_W; } +uint16_t photo_view_canvas_h() { return CANVAS_H; } +uint16_t photo_view_img_w() { return g_img_w; } +uint16_t photo_view_img_h() { return g_img_h; } + +bool photo_view_is_gif() { return g_gif_open; } + +bool photo_view_gif_tick() { + if (!g_gif_open || !g_gif) { + return false; + } + const uint32_t now = millis(); + if ((int32_t)(now - g_gif_next_ms) < 0) { + return false; + } + int delay_ms = 0; + g_gif->playFrame(true, &delay_ms); + if (delay_ms < 20) { + delay_ms = 100; + } + g_gif_next_ms = now + (uint32_t)delay_ms; + return true; +} diff --git a/src/sd_info.cpp b/src/sd_info.cpp new file mode 100644 index 0000000..88f9b53 --- /dev/null +++ b/src/sd_info.cpp @@ -0,0 +1,136 @@ +#include "sd_info.h" +#include "pins.h" + +#include +#include +#include + +static SPIClass sd_spi(VSPI); +static bool sd_ready = false; + +static const char *card_type_name(uint8_t t) { + switch (t) { + case CARD_MMC: return "MMC"; + case CARD_SD: return "SDSC"; + case CARD_SDHC: return "SDHC"; + case CARD_NONE: return "NONE"; + default: return "UNKNOWN"; + } +} + +static const char *detect_filesystem() { + File root = SD.open("/"); + if (!root) { + return "-"; + } + root.close(); + + uint64_t total = SD.totalBytes(); + if (total == 0) { + return "FAT"; + } + if (total > 32ULL * 1024ULL * 1024ULL) { + return "FAT32"; + } + if (total > 2ULL * 1024ULL * 1024ULL) { + return "FAT16"; + } + return "FAT12/16"; +} + +static uint32_t count_root_entries() { + File root = SD.open("/"); + if (!root || !root.isDirectory()) { + return 0; + } + uint32_t n = 0; + File f = root.openNextFile(); + while (f) { + n++; + f = root.openNextFile(); + } + root.close(); + return n; +} + +bool sd_init() { + if (sd_ready) { + return true; + } + + pinMode(SD_CS, OUTPUT); + digitalWrite(SD_CS, HIGH); + pinMode(CYD_TOUCH_CS, OUTPUT); + digitalWrite(CYD_TOUCH_CS, HIGH); + + sd_spi.begin(SD_SCK, SD_MISO, SD_MOSI, SD_CS); + + if (!SD.begin(SD_CS, sd_spi, 25000000)) { + sd_ready = false; + return false; + } + + sd_ready = true; + return true; +} + +void sd_deinit() { + if (sd_ready) { + SD.end(); + sd_ready = false; + } +} + +SdCardInfo sd_read_info() { + SdCardInfo info{}; + info.mounted = false; + info.present = false; + info.type = "NONE"; + info.filesystem = "-"; + info.card_bytes = 0; + info.total_bytes = 0; + info.used_bytes = 0; + info.free_bytes = 0; + info.root_files = 0; + + if (!sd_ready && !sd_init()) { + info.error = "Mount failed"; + return info; + } + + uint8_t type = SD.cardType(); + info.type = card_type_name(type); + + if (type == CARD_NONE) { + info.error = "No card"; + info.present = false; + return info; + } + + info.present = true; + info.mounted = true; + info.card_bytes = SD.cardSize(); + info.total_bytes = SD.totalBytes(); + info.used_bytes = SD.usedBytes(); + info.free_bytes = + (info.total_bytes > info.used_bytes) ? (info.total_bytes - info.used_bytes) : 0; + info.filesystem = detect_filesystem(); + info.root_files = count_root_entries(); + info.error = ""; + return info; +} + +String format_bytes(uint64_t bytes) { + const double b = static_cast(bytes); + char buf[48]; + if (bytes >= (1ULL << 30)) { + snprintf(buf, sizeof(buf), "%.2f GB", b / (1024.0 * 1024.0 * 1024.0)); + } else if (bytes >= (1ULL << 20)) { + snprintf(buf, sizeof(buf), "%.2f MB", b / (1024.0 * 1024.0)); + } else if (bytes >= (1ULL << 10)) { + snprintf(buf, sizeof(buf), "%.2f KB", b / 1024.0); + } else { + snprintf(buf, sizeof(buf), "%llu B", static_cast(bytes)); + } + return String(buf); +} diff --git a/src/sd_put.cpp b/src/sd_put.cpp new file mode 100644 index 0000000..2cd2623 --- /dev/null +++ b/src/sd_put.cpp @@ -0,0 +1,121 @@ +#include "sd_put.h" +#include "sd_info.h" + +#include + +static String read_line_timeout(uint32_t timeout_ms) { + String line; + const uint32_t start = millis(); + while (millis() - start < timeout_ms) { + while (Serial.available()) { + const char c = (char)Serial.read(); + if (c == '\n' || c == '\r') { + if (line.length()) { + return line; + } + continue; + } + line += c; + if (line.length() > 200) { + return line; + } + } + delay(1); + } + return line; +} + +bool sd_put_poll() { + if (!Serial.available()) { + return false; + } + + // 只在行首是 #PUT 时进入,避免吃掉普通日志 + if (Serial.peek() != '#') { + // 丢弃非协议噪声到换行,防止堵死 + while (Serial.available() && Serial.peek() != '#') { + const int c = Serial.read(); + if (c == '\n' || c == '\r') { + break; + } + } + return false; + } + + const String line = read_line_timeout(500); + if (!line.startsWith("#PUT ")) { + return false; + } + + // #PUT /paomo.mp3 4143009 + int sp1 = line.indexOf(' ', 5); + if (sp1 < 0) { + Serial.println("PUT_ERR bad args"); + return true; + } + const String path = line.substring(5, sp1); + const uint32_t size = (uint32_t)line.substring(sp1 + 1).toInt(); + if (path.length() < 2 || path[0] != '/' || size == 0 || size > 64UL * 1024UL * 1024UL) { + Serial.println("PUT_ERR bad path/size"); + return true; + } + + if (!sd_init()) { + Serial.println("PUT_ERR sd"); + return true; + } + + if (SD.exists(path.c_str())) { + SD.remove(path.c_str()); + } + + File f = SD.open(path.c_str(), FILE_WRITE); + if (!f) { + Serial.println("PUT_ERR open"); + return true; + } + + Serial.println("READY"); + Serial.flush(); + + // 窗口协议:每收满一块回 ACK,避免 RX 溢出 + constexpr size_t k_win = 4096; + uint8_t buf[k_win]; + uint32_t got = 0; + while (got < size) { + const uint32_t need = size - got; + const size_t chunk = need > k_win ? k_win : (size_t)need; + size_t n = 0; + const uint32_t wait_start = millis(); + while (n < chunk && millis() - wait_start < 30000) { + if (!Serial.available()) { + delay(1); + continue; + } + Serial.setTimeout(50); + const int r = Serial.readBytes(buf + n, chunk - n); + if (r > 0) { + n += (size_t)r; + } + } + if (n == 0) { + f.close(); + SD.remove(path.c_str()); + Serial.println("PUT_ERR timeout"); + return true; + } + if (f.write(buf, n) != n) { + f.close(); + SD.remove(path.c_str()); + Serial.println("PUT_ERR write"); + return true; + } + got += n; + Serial.printf("ACK %u\n", (unsigned)got); + Serial.flush(); + } + f.flush(); + f.close(); + Serial.println("DONE"); + return true; +} diff --git a/src/touch.cpp b/src/touch.cpp new file mode 100644 index 0000000..d43b68a --- /dev/null +++ b/src/touch.cpp @@ -0,0 +1,125 @@ +#include "touch.h" +#include "pins.h" + +#include + +/** + * SoftSPI XPT2046 + * BL↔TR 是矩形主对角线镜像(XY 对调),不是 180°(180° 会搞成 TL↔BR)。 + * 先按 rot2 得到旧 UI,再做对角修正,反变换喂给 LVGL 270。 + */ + +int16_t g_dbg_phys_x = -1; +int16_t g_dbg_phys_y = -1; +int16_t g_dbg_logic_x = -1; +int16_t g_dbg_logic_y = -1; +bool g_dbg_touch_down = false; + +static void soft_write(uint8_t data) { + for (int i = 7; i >= 0; --i) { + digitalWrite(CYD_TOUCH_MOSI, (data >> i) & 1); + digitalWrite(CYD_TOUCH_CLK, HIGH); + delayMicroseconds(2); + digitalWrite(CYD_TOUCH_CLK, LOW); + delayMicroseconds(2); + } +} + +static uint16_t soft_read12() { + uint16_t v = 0; + digitalWrite(CYD_TOUCH_CLK, HIGH); + delayMicroseconds(2); + digitalWrite(CYD_TOUCH_CLK, LOW); + delayMicroseconds(2); + for (int i = 0; i < 12; ++i) { + digitalWrite(CYD_TOUCH_CLK, HIGH); + delayMicroseconds(2); + v = (uint16_t)((v << 1) | (digitalRead(CYD_TOUCH_MISO) ? 1u : 0u)); + digitalWrite(CYD_TOUCH_CLK, LOW); + delayMicroseconds(2); + } + return v; +} + +static uint16_t xpt_read(uint8_t cmd) { + digitalWrite(CYD_TOUCH_CS, LOW); + soft_write(cmd); + uint16_t v = soft_read12(); + digitalWrite(CYD_TOUCH_CS, HIGH); + return v; +} + +static int16_t clampi(int32_t v, int32_t lo, int32_t hi) { + if (v < lo) { + return (int16_t)lo; + } + if (v > hi) { + return (int16_t)hi; + } + return (int16_t)v; +} + +void touch_init() { + pinMode(CYD_TOUCH_CS, OUTPUT); + pinMode(CYD_TOUCH_CLK, OUTPUT); + pinMode(CYD_TOUCH_MOSI, OUTPUT); + pinMode(CYD_TOUCH_MISO, INPUT); + pinMode(CYD_TOUCH_IRQ, INPUT); + digitalWrite(CYD_TOUCH_CS, HIGH); + digitalWrite(CYD_TOUCH_CLK, LOW); + Serial.println("Touch: SoftSPI + diagonal fix (BL/TR)"); +} + +void touch_read_cb(lv_indev_t * /*indev*/, lv_indev_data_t *data) { + uint16_t adc_x = xpt_read(0x90); + uint16_t adc_y = xpt_read(0xD0); + adc_x = (uint16_t)((adc_x + xpt_read(0x90)) / 2); + adc_y = (uint16_t)((adc_y + xpt_read(0xD0)) / 2); + + const bool irq = (digitalRead(CYD_TOUCH_IRQ) == LOW); + const bool in_range = + (adc_x > 80 && adc_x < 4000 && adc_y > 80 && adc_y < 4000); + + if (!in_range || (!irq && (adc_x < 200 || adc_y < 200))) { + data->state = LV_INDEV_STATE_RELEASED; + g_dbg_touch_down = false; + return; + } + + const int16_t p_x = (int16_t)adc_x; + const int16_t p_y = (int16_t)(4095 - adc_y); + + int16_t x = map(p_x, 200, 3700, 1, SCREEN_WIDTH); + int16_t y = map(p_y, 240, 3800, 1, SCREEN_HEIGHT); + x = clampi(x, 0, SCREEN_WIDTH - 1); + y = clampi(y, 0, SCREEN_HEIGHT - 1); + + // 旧 UI(LVGL270):(y, 239-x) — 该空间里 BL↔TR,做主对角线镜像 + // (ux,uy) -> (uy*(319/239), ux*(239/319)) + const int32_t old_ux = y; + const int32_t old_uy = (SCREEN_WIDTH - 1) - x; + int16_t ui_x = clampi((old_uy * (UI_HOR_RES - 1)) / (UI_VER_RES - 1), 0, + UI_HOR_RES - 1); + int16_t ui_y = clampi((old_ux * (UI_VER_RES - 1)) / (UI_HOR_RES - 1), 0, + UI_VER_RES - 1); + + // 反变换喂 LVGL,使最终落在 (ui_x, ui_y) + const int16_t feed_x = (int16_t)((SCREEN_WIDTH - 1) - ui_y); + const int16_t feed_y = ui_x; + + g_dbg_phys_x = x; + g_dbg_phys_y = y; + g_dbg_logic_x = ui_x; + g_dbg_logic_y = ui_y; + g_dbg_touch_down = true; + + static uint32_t last_log = 0; + if (millis() - last_log > 80) { + last_log = millis(); + Serial.printf("Touch UI(%d,%d)\n", ui_x, ui_y); + } + + data->point.x = feed_x; + data->point.y = feed_y; + data->state = LV_INDEV_STATE_PRESSED; +} diff --git a/src/ui.cpp b/src/ui.cpp new file mode 100644 index 0000000..90dfd2a --- /dev/null +++ b/src/ui.cpp @@ -0,0 +1,296 @@ +#include "ui.h" +#include "photo_list.h" +#include "photo_view.h" +#include "pins.h" + +#include +#include +#include +#include + +static constexpr uint32_t COL_BG = 0x0A1016; +static constexpr uint32_t COL_INK = 0xEAF0F6; +static constexpr uint32_t COL_MUTED = 0x7A8B9C; +static constexpr uint32_t COL_OK = 0x3DDC97; +static constexpr uint32_t COL_BAD = 0xE85D4C; +static constexpr uint32_t COL_ACCENT = 0x4FC3F7; +static constexpr uint32_t COL_BTN = 0x243140; +static constexpr uint32_t COL_BTN_HI = 0x1A4A6E; + +static lv_obj_t *lbl_title; +static lv_obj_t *lbl_chip; +static lv_obj_t *lbl_index; +static lv_obj_t *lbl_name; +static lv_obj_t *lbl_hint; +static lv_obj_t *img_photo; +static lv_obj_t *btn_prev; +static lv_obj_t *btn_next; + +static int16_t g_index = -1; +static char g_index_text[24] = "0/0"; +static char g_name_text[48] = "-"; + +static const char *basename_of(const char *path) { + const char *slash = strrchr(path, '/'); + return slash ? slash + 1 : path; +} + +static void style_btn(lv_obj_t *btn, uint32_t bg) { + lv_obj_set_style_radius(btn, 8, 0); + lv_obj_set_style_bg_color(btn, lv_color_hex(bg), 0); + lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0); + lv_obj_set_style_shadow_width(btn, 0, 0); + lv_obj_set_style_border_width(btn, 0, 0); + lv_obj_set_style_pad_all(btn, 0, 0); +} + +static void refresh_labels() { + const uint16_t n = photo_list_count(); + if (n == 0 || g_index < 0) { + snprintf(g_index_text, sizeof(g_index_text), "0/0"); + snprintf(g_name_text, sizeof(g_name_text), "No photos"); + } else { + snprintf(g_index_text, sizeof(g_index_text), "%d/%u", (int)(g_index + 1), + (unsigned)n); + const PhotoEntry *e = photo_list_get((uint16_t)g_index); + snprintf(g_name_text, sizeof(g_name_text), "%s", + e ? basename_of(e->path) : "-"); + } + if (lbl_index) { + lv_label_set_text(lbl_index, g_index_text); + } + if (lbl_name) { + lv_label_set_text(lbl_name, g_name_text); + } +} + +static void show_current() { + const uint16_t n = photo_list_count(); + if (n == 0) { + g_index = -1; + if (img_photo) { + lv_image_set_src(img_photo, nullptr); + } + if (lbl_hint) { + lv_label_set_text(lbl_hint, "Put jpg/png/gif in / or /photos"); + } + refresh_labels(); + return; + } + + if (g_index < 0 || g_index >= (int16_t)n) { + g_index = 0; + } + + const PhotoEntry *e = photo_list_get((uint16_t)g_index); + const bool ok = e && photo_view_show(e); + if (img_photo) { + const lv_image_dsc_t *dsc = photo_view_dsc(); + // Force refresh even when dsc pointer is unchanged + lv_image_set_src(img_photo, nullptr); + if (ok && dsc) { + lv_image_set_src(img_photo, dsc); + } + lv_obj_invalidate(img_photo); + } + if (lbl_hint) { + if (!ok) { + lv_label_set_text(lbl_hint, "Decode failed (size/format?)"); + } else if (photo_view_is_gif()) { + lv_label_set_text(lbl_hint, "GIF | tap L/R to switch"); + } else { + lv_label_set_text(lbl_hint, "Tap left=prev right=next"); + } + } + refresh_labels(); +} + +static void go_delta(int delta) { + const uint16_t n = photo_list_count(); + if (n == 0) { + return; + } + int16_t next = (int16_t)(g_index + delta); + while (next < 0) { + next = (int16_t)(next + (int16_t)n); + } + while (next >= (int16_t)n) { + next = (int16_t)(next - (int16_t)n); + } + if (next == g_index && n > 1) { + // still reload if single? skip + } + g_index = next; + show_current(); +} + +static void on_prev(lv_event_t *e) { + LV_UNUSED(e); + go_delta(-1); +} + +static void on_next(lv_event_t *e) { + LV_UNUSED(e); + go_delta(1); +} + +static void on_tap_zone(lv_event_t *e) { + lv_indev_t *indev = lv_indev_active(); + if (!indev) { + return; + } + lv_point_t p; + lv_indev_get_point(indev, &p); + if (p.x < (UI_HOR_RES / 2)) { + go_delta(-1); + } else { + go_delta(1); + } +} + +void ui_reload_album() { + const uint16_t n = photo_list_scan(); + Serial.printf("Photos found: %u\n", (unsigned)n); + for (uint16_t i = 0; i < n; i++) { + const PhotoEntry *e = photo_list_get(i); + if (e) { + Serial.printf(" [%u] %s\n", (unsigned)i, e->path); + } + } + if (n == 0) { + g_index = -1; + } else if (g_index < 0 || g_index >= (int16_t)n) { + g_index = 0; + } + show_current(); +} + +void ui_create() { + photo_view_init(); + + lv_obj_t *scr = lv_screen_active(); + lv_obj_set_style_bg_color(scr, lv_color_hex(COL_BG), 0); + lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0); + lv_obj_clear_flag(scr, LV_OBJ_FLAG_SCROLLABLE); + + // Full-screen tap zones (behind chrome) + lv_obj_t *tap = lv_obj_create(scr); + lv_obj_set_size(tap, UI_HOR_RES, UI_VER_RES); + lv_obj_set_pos(tap, 0, 0); + lv_obj_set_style_bg_opa(tap, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(tap, 0, 0); + lv_obj_set_style_pad_all(tap, 0, 0); + lv_obj_clear_flag(tap, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(tap, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(tap, on_tap_zone, LV_EVENT_CLICKED, nullptr); + + img_photo = lv_image_create(scr); + lv_obj_set_size(img_photo, photo_view_canvas_w(), photo_view_canvas_h()); + lv_image_set_scale(img_photo, 512); // 256=1x, 512=2x -> full 320x240 + lv_obj_align(img_photo, LV_ALIGN_CENTER, 0, 0); + lv_obj_clear_flag(img_photo, LV_OBJ_FLAG_CLICKABLE); + + // Top bar + lv_obj_t *top = lv_obj_create(scr); + lv_obj_set_size(top, UI_HOR_RES, 28); + lv_obj_set_pos(top, 0, 0); + lv_obj_set_style_bg_color(top, lv_color_hex(COL_BG), 0); + lv_obj_set_style_bg_opa(top, LV_OPA_80, 0); + lv_obj_set_style_border_width(top, 0, 0); + lv_obj_set_style_pad_all(top, 0, 0); + lv_obj_set_style_radius(top, 0, 0); + lv_obj_clear_flag(top, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_clear_flag(top, LV_OBJ_FLAG_CLICKABLE); + + lbl_title = lv_label_create(top); + lv_label_set_text(lbl_title, "ALBUM"); + lv_obj_set_style_text_font(lbl_title, &lv_font_montserrat_16, 0); + lv_obj_set_style_text_color(lbl_title, lv_color_hex(COL_ACCENT), 0); + lv_obj_set_pos(lbl_title, 8, 5); + + lbl_index = lv_label_create(top); + lv_label_set_text(lbl_index, "0/0"); + lv_obj_set_style_text_font(lbl_index, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(lbl_index, lv_color_hex(COL_INK), 0); + lv_obj_align(lbl_index, LV_ALIGN_TOP_MID, 0, 6); + + lbl_chip = lv_label_create(top); + lv_label_set_text(lbl_chip, "-"); + lv_obj_set_style_text_font(lbl_chip, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(lbl_chip, lv_color_hex(COL_MUTED), 0); + lv_obj_align(lbl_chip, LV_ALIGN_TOP_RIGHT, -8, 6); + + // Bottom bar + lv_obj_t *bot = lv_obj_create(scr); + lv_obj_set_size(bot, UI_HOR_RES, 40); + lv_obj_set_pos(bot, 0, UI_VER_RES - 40); + lv_obj_set_style_bg_color(bot, lv_color_hex(COL_BG), 0); + lv_obj_set_style_bg_opa(bot, LV_OPA_80, 0); + lv_obj_set_style_border_width(bot, 0, 0); + lv_obj_set_style_pad_all(bot, 0, 0); + lv_obj_set_style_radius(bot, 0, 0); + lv_obj_clear_flag(bot, LV_OBJ_FLAG_SCROLLABLE); + + btn_prev = lv_button_create(bot); + lv_obj_set_pos(btn_prev, 8, 6); + lv_obj_set_size(btn_prev, 56, 28); + lv_obj_add_flag(btn_prev, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(btn_prev, on_prev, LV_EVENT_CLICKED, nullptr); + style_btn(btn_prev, COL_BTN); + lv_obj_t *lp = lv_label_create(btn_prev); + lv_label_set_text(lp, "<"); + lv_obj_set_style_text_font(lp, &lv_font_montserrat_16, 0); + lv_obj_set_style_text_color(lp, lv_color_hex(COL_INK), 0); + lv_obj_center(lp); + + btn_next = lv_button_create(bot); + lv_obj_set_pos(btn_next, UI_HOR_RES - 64, 6); + lv_obj_set_size(btn_next, 56, 28); + lv_obj_add_flag(btn_next, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(btn_next, on_next, LV_EVENT_CLICKED, nullptr); + style_btn(btn_next, COL_BTN_HI); + lv_obj_t *ln = lv_label_create(btn_next); + lv_label_set_text(ln, ">"); + lv_obj_set_style_text_font(ln, &lv_font_montserrat_16, 0); + lv_obj_set_style_text_color(ln, lv_color_hex(COL_INK), 0); + lv_obj_center(ln); + + lbl_name = lv_label_create(bot); + lv_label_set_text(lbl_name, "-"); + lv_label_set_long_mode(lbl_name, LV_LABEL_LONG_CLIP); + lv_obj_set_width(lbl_name, UI_HOR_RES - 140); + lv_obj_set_style_text_font(lbl_name, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(lbl_name, lv_color_hex(COL_INK), 0); + lv_obj_align(lbl_name, LV_ALIGN_TOP_MID, 0, 4); + + lbl_hint = lv_label_create(bot); + lv_label_set_text(lbl_hint, "Tap left/right"); + lv_obj_set_style_text_font(lbl_hint, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(lbl_hint, lv_color_hex(COL_MUTED), 0); + lv_obj_align(lbl_hint, LV_ALIGN_TOP_MID, 0, 20); + + // Raise bars / buttons above tap target for hit testing + lv_obj_move_foreground(top); + lv_obj_move_foreground(bot); +} + +void ui_tick() { + if (photo_view_gif_tick() && img_photo) { + lv_obj_invalidate(img_photo); + } +} + +void ui_update(const SdCardInfo &info) { + if (!lbl_chip) { + return; + } + if (!info.mounted || !info.present) { + lv_label_set_text(lbl_chip, info.error.length() ? info.error.c_str() : "TF?"); + lv_obj_set_style_text_color(lbl_chip, lv_color_hex(COL_BAD), 0); + } else { + char chip[40]; + snprintf(chip, sizeof(chip), "OK %s", info.type); + lv_label_set_text(lbl_chip, chip); + lv_obj_set_style_text_color(lbl_chip, lv_color_hex(COL_OK), 0); + } +} diff --git a/tools/make_test_photos.py b/tools/make_test_photos.py new file mode 100644 index 0000000..316b0f1 --- /dev/null +++ b/tools/make_test_photos.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Generate 320x240 JPG/PNG/GIF samples into assets/photos.""" +from __future__ import annotations + +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont + +ROOT = Path(__file__).resolve().parents[1] +OUT = ROOT / "assets" / "photos" +W, H = 320, 240 + + +def font(size: int = 28) -> ImageFont.ImageFont: + for name in ( + r"C:\Windows\Fonts\msyh.ttc", + r"C:\Windows\Fonts\arial.ttf", + r"C:\Windows\Fonts\consola.ttf", + ): + try: + return ImageFont.truetype(name, size) + except OSError: + pass + return ImageFont.load_default() + + +def card(bg, accent, title: str, subtitle: str, path: Path, fmt: str) -> None: + im = Image.new("RGB", (W, H), bg) + d = ImageDraw.Draw(im) + for i in range(0, H, 12): + c = tuple(max(0, min(255, v + (i % 24) - 12)) for v in accent) + d.rectangle((0, i, W, i + 6), fill=c) + d.rectangle((16, 40, W - 16, H - 40), fill=bg) + d.rectangle((16, 40, W - 16, H - 40), outline=accent, width=3) + d.text((28, 70), title, fill=(234, 240, 246), font=font(36)) + d.text((28, 130), subtitle, fill=(122, 139, 156), font=font(20)) + d.text((28, 170), "CYD ALBUM TEST", fill=accent, font=font(16)) + if fmt == "JPEG": + im.save(path, "JPEG", quality=90) + else: + im.save(path, "PNG") + print(f"wrote {path} ({path.stat().st_size} bytes)") + + +def main() -> None: + OUT.mkdir(parents=True, exist_ok=True) + card((10, 16, 22), (79, 195, 247), "01 JPG", "tap left / right", OUT / "01_blue.jpg", "JPEG") + card((20, 12, 28), (232, 93, 76), "02 JPG", "red sample", OUT / "02_red.jpg", "JPEG") + card((8, 28, 20), (61, 220, 151), "03 PNG", "green sample", OUT / "03_green.png", "PNG") + + frames = [] + for i in range(6): + im = Image.new("RGB", (W, H), (10, 16, 22)) + d = ImageDraw.Draw(im) + d.text((24, 24), "04 GIF", fill=(234, 240, 246), font=font(32)) + d.text((24, 70), "animated", fill=(122, 139, 156), font=font(18)) + x = 20 + i * 40 + d.rectangle((x, 120, x + 80, 180), fill=(255, 200, 40)) + d.rectangle((20, 200, 300, 210), fill=(36, 49, 64)) + frames.append(im) + gif_path = OUT / "04_bounce.gif" + frames[0].save( + gif_path, + save_all=True, + append_images=frames[1:], + duration=120, + loop=0, + optimize=False, + ) + print(f"wrote {gif_path} ({gif_path.stat().st_size} bytes)") + print("DONE") + + +if __name__ == "__main__": + main() diff --git a/tools/sd_put.py b/tools/sd_put.py new file mode 100644 index 0000000..72b3fab --- /dev/null +++ b/tools/sd_put.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +"""Upload a file to the CYD TF card over serial (#PUT + ACK window).""" +from __future__ import annotations + +import argparse +import sys +import time +from pathlib import Path + +try: + import serial + from serial.tools import list_ports +except ImportError: + print("Need pyserial: pip install pyserial", file=sys.stderr) + sys.exit(1) + +WIN = 4096 + + +def pick_port(preferred: str | None) -> str: + if preferred: + return preferred + ports = list(list_ports.comports()) + for p in ports: + desc = (p.description or "") + (p.hwid or "") + if "USB" in desc.upper() or "UART" in desc.upper() or "CP210" in desc.upper() or "CH340" in desc.upper(): + return p.device + if ports: + return ports[0].device + raise SystemExit("No serial port found") + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("file", type=Path, help="Local file to upload") + ap.add_argument("--port", default=None) + ap.add_argument("--baud", type=int, default=460800) + ap.add_argument("--remote", default="/paomo.mp3") + args = ap.parse_args() + + path: Path = args.file + if not path.is_file(): + print(f"Missing file: {path}", file=sys.stderr) + return 1 + + size = path.stat().st_size + port = pick_port(args.port) + print(f"Port {port} {path.name} -> {args.remote} ({size} bytes)", flush=True) + + ser = serial.Serial() + ser.port = port + ser.baudrate = args.baud + ser.timeout = 1 + ser.dtr = False + ser.rts = False + ser.open() + # 硬复位 ESP32 + ser.rts = True + time.sleep(0.1) + ser.rts = False + print("waiting BOOT_OK...", flush=True) + deadline = time.time() + 12 + booted = False + while time.time() < deadline: + line = ser.readline().decode("utf-8", errors="ignore").strip() + if line: + print("<", line, flush=True) + if "BOOT_OK" in line: + booted = True + break + if not booted: + print("warn: BOOT_OK not seen, continuing", flush=True) + time.sleep(0.5) + ser.reset_input_buffer() + + cmd = f"#PUT {args.remote} {size}\n".encode("ascii") + print(f"send {cmd!r}", flush=True) + ser.write(cmd) + ser.flush() + + deadline = time.time() + 20 + while time.time() < deadline: + line = ser.readline().decode("utf-8", errors="ignore").strip() + if line: + print("<", line, flush=True) + if line == "READY": + break + if line.startswith("PUT_ERR"): + return 2 + else: + print("Timeout waiting READY", file=sys.stderr, flush=True) + return 3 + + sent = 0 + t0 = time.time() + with path.open("rb") as f: + while sent < size: + chunk = f.read(min(WIN, size - sent)) + if not chunk: + break + ser.write(chunk) + ser.flush() + sent += len(chunk) + + # 等 ACK + ack_deadline = time.time() + 30 + acked = False + while time.time() < ack_deadline: + line = ser.readline().decode("utf-8", errors="ignore").strip() + if not line: + continue + print("<", line, flush=True) + if line.startswith("PUT_ERR"): + return 4 + if line.startswith("ACK "): + try: + got = int(line.split()[1]) + except (IndexError, ValueError): + continue + if got >= sent: + acked = True + break + if line == "DONE" and sent >= size: + print("OK", flush=True) + return 0 + if not acked: + print(f"Timeout waiting ACK after {sent}", file=sys.stderr, flush=True) + return 4 + + if sent % (256 * 1024) < WIN or sent == size: + rate = sent / max(time.time() - t0, 0.001) + print(f"sent {sent}/{size} ({rate/1024:.1f} KB/s)", flush=True) + + deadline = time.time() + 30 + while time.time() < deadline: + line = ser.readline().decode("utf-8", errors="ignore").strip() + if line: + print("<", line, flush=True) + if line == "DONE": + print("OK", flush=True) + return 0 + if line.startswith("PUT_ERR"): + return 5 + + print("Timeout waiting DONE", file=sys.stderr, flush=True) + return 6 + + +if __name__ == "__main__": + sys.exit(main())