commit d75c252b9e27f004fda94ce0b83bcd96897a6e6b Author: DESKTOP-PUHFO60\dsx2016 Date: Wed Oct 28 08:04:11 2026 +0800 Add CYD web radio: WiFi provision + HTTP MP3 to DAC26. Supports 2.8/3.5 boards with on-screen station list; ships flashable firmware packages. Co-authored-by: Cursor diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..609b09c --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +.pio/ +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch +*.pyc +__pycache__/ +.DS_Store +Thumbs.db +*.elf +# Build dir bins stay out; firmware/ prebuilt packages stay in +!firmware/**/*.bin diff --git a/README.md b/README.md new file mode 100644 index 0000000..d99cc6d --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# CYD 网络电台(Web Radio) + +ESP32 Cheap Yellow Display 上网听 MP3 电台。显示/触控/配网对齐 [wifitest](../wifitest) 与 [CYD_PROJECT_GUIDE.md](../CYD_PROJECT_GUIDE.md);音频走板载喇叭 **GPIO26(DAC2)**。 + +| 板型 | env | 屏 / 触控 | 背光 | 喇叭 | +|------|-----|-----------|------|------| +| **2.8" · 2432S028R** | `cyd`(默认) | ILI9341 + XPT2046 SoftSPI | GPIO**21** | GPIO**26** | +| **3.5" · 3248S035C** | `cyd_35r` | ST7796 + GT911 I2C | GPIO**27** | GPIO**26** | + +> **禁止占用 GPIO25**:2.8 是触控 CLK,3.5 是 GT911 RST。固件在开内置 DAC 后强制只启用 DAC2(GPIO26)。 + +## 功能 + +1. **WiFi 配网**(复用 `wifi_prov` / `wifi_ui`):屏上扫网 + 键盘,或 SoftAP `CYD-Setup` 门户;凭证进 NVS。 +2. **连上后进电台页**:内置若干 SomaFM / Radio Swiss 等 HTTP MP3 流。 +3. **Prev / Play·Stop / Next**、音量、点列表切台;**WiFi Setup** 可返回配网(会停播)。 + +解码:`ESP8266Audio`(HTTP → 缓冲 → MP3 → I2S 内置 DAC)。 + +## 刷写 + +| 板型 | 目录 | +|------|------| +| 2.8" | [`firmware/cyd/`](firmware/cyd/) | +| 3.5" | [`firmware/cyd_35r/`](firmware/cyd_35r/) | + +```powershell +cd firmware\cyd # 或 firmware\cyd_35r +.\flash.ps1 +# 或 +.\flash.ps1 -Port COM3 +``` + +源码: + +```powershell +pio run -e cyd -t upload # 2.8 +pio run -e cyd_35r -t upload # 3.5 +pio device monitor -b 460800 +``` + +重新打包: + +```powershell +.\firmware\export_from_build.ps1 +.\firmware\export_from_build.ps1 -Env cyd_35r +``` + +## 使用 + +1. 首次无 WiFi:屏上 **Scan → 选网 → 密码 → Join**,或手机连 `CYD-Setup` 打开 `http://192.168.4.1/`。 +2. 连上后自动进 **Web Radio**;点台或 Play。 +3. 喇叭接 P4;音量从低开始调。 + +## 模块 + +| 文件 | 职责 | +|------|------| +| `touch.*` / `pins.h` | 双板触控与引脚 | +| `wifi_prov.*` / `wifi_ui.*` | 配网核心 + LVGL UI | +| `radio_player.*` | 电台列表 + HTTP MP3 + DAC26 | +| `radio_ui.*` | 电台界面 | +| `main.cpp` | 启动顺序与 loop | + +## 注意 + +- 流媒体需外网;部分源站可能间歇不可用,可改 `radio_player.cpp` 里 `k_stations[]`。 +- 分区用 `min_spiffs.csv`(解码库体积大)。 +- 3.5:`DRAW_BUF_DIV=20`、`LV_MEM_SIZE=32768`;外设脚仍是 **22+21**(本工程无外设接线)。 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..362d78e --- /dev/null +++ b/firmware/README.md @@ -0,0 +1,26 @@ +# 预编译固件(可随时刷写) + +| 目录 | 板子 | +|------|------| +| [`cyd/`](cyd/) | **2.8"** CYD ESP32-2432S028R(ILI9341) | +| [`cyd_35r/`](cyd_35r/) | **3.5"** CYD ESP32-3248S035C(ST7796 + GT911) | + +功能:**网络电台**(WiFi 配网 + HTTP MP3 → 喇叭 GPIO26) +版本信息见各目录下 `VERSION.txt`。 + +也可在 [`../../cydweb`](../../cydweb)(CYD Flasher)里通过浏览器串口刷写本目录固件。 + +## 直接刷写(无需重新编译) + +```powershell +cd firmware\cyd # 3.5": firmware\cyd_35r +.\flash.ps1 +.\flash.ps1 -Port COM3 +``` + +## 从源码重新打包 + +```powershell +.\firmware\export_from_build.ps1 +.\firmware\export_from_build.ps1 -Env cyd_35r +``` diff --git a/firmware/cyd/VERSION.txt b/firmware/cyd/VERSION.txt new file mode 100644 index 0000000..50ff9d9 --- /dev/null +++ b/firmware/cyd/VERSION.txt @@ -0,0 +1,15 @@ +name: CYD webradiotest (cyd) +git: unknown +date: 2026-10-28 +board: ESP32-2432S028R (ILI9341) +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 40C938AA38897BF31AAA472CDF3A942B6EA1D3EF3397591DC5D9DEC61B60E1E2 + partitions.bin 0A8B5720E7B77FF11F1462458C3A509DEE79224E5279898F26D6A2E3AE0517B7 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..1d0f476 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..a4922ed --- /dev/null +++ b/firmware/cyd/flash.ps1 @@ -0,0 +1,98 @@ +# Flash archived CYD Web Radio 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 460800 if needed." diff --git a/firmware/cyd/media/preview.html b/firmware/cyd/media/preview.html new file mode 100644 index 0000000..79613e4 --- /dev/null +++ b/firmware/cyd/media/preview.html @@ -0,0 +1,50 @@ + + + + +Web Radio + + + +
+
Web Radio
+
HomeWiFi -52dBm
+
SomaFM Groove Salad
+
Playing
+
Vol 60%
+
+ + + + + +
+
+
SomaFM Groove Salad
+
SomaFM Drone Zone
+
SomaFM Indie Pop
+
+
+
+ + diff --git a/firmware/cyd/meta.json b/firmware/cyd/meta.json new file mode 100644 index 0000000..83d88c5 --- /dev/null +++ b/firmware/cyd/meta.json @@ -0,0 +1,16 @@ +{ + "boardId": "esp32-2432s028", + "name": "CYD Web Radio", + "summary": "Internet MP3 radio with on-screen WiFi setup; speaker on GPIO26", + "description": "LVGL WiFi provisioning then station list. Streams HTTP MP3 via ESP8266Audio to internal DAC2 (GPIO26 only — never GPIO25). Credentials in NVS.", + "tags": [ + "wifi", + "radio", + "audio", + "mp3", + "touch" + ], + "media": { + "preview": "media/preview.html" + } +} diff --git a/firmware/cyd/partitions.bin b/firmware/cyd/partitions.bin new file mode 100644 index 0000000..21800fa Binary files /dev/null and b/firmware/cyd/partitions.bin differ diff --git a/firmware/cyd_35r/VERSION.txt b/firmware/cyd_35r/VERSION.txt new file mode 100644 index 0000000..06ea742 --- /dev/null +++ b/firmware/cyd_35r/VERSION.txt @@ -0,0 +1,15 @@ +name: CYD webradiotest (cyd_35r) +git: unknown +date: 2026-10-28 +board: ESP32-3248S035 (ST7796) +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 EE0B24F2E40451F72F48DF25803DF571FC406C80004641E93CCBABF0B15B175F + partitions.bin 0A8B5720E7B77FF11F1462458C3A509DEE79224E5279898F26D6A2E3AE0517B7 diff --git a/firmware/cyd_35r/boot_app0.bin b/firmware/cyd_35r/boot_app0.bin new file mode 100644 index 0000000..13562ca Binary files /dev/null and b/firmware/cyd_35r/boot_app0.bin differ diff --git a/firmware/cyd_35r/bootloader.bin b/firmware/cyd_35r/bootloader.bin new file mode 100644 index 0000000..d592359 Binary files /dev/null and b/firmware/cyd_35r/bootloader.bin differ diff --git a/firmware/cyd_35r/firmware.bin b/firmware/cyd_35r/firmware.bin new file mode 100644 index 0000000..8f7a002 Binary files /dev/null and b/firmware/cyd_35r/firmware.bin differ diff --git a/firmware/cyd_35r/flash.bat b/firmware/cyd_35r/flash.bat new file mode 100644 index 0000000..bf983b3 --- /dev/null +++ b/firmware/cyd_35r/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_35r/flash.ps1 b/firmware/cyd_35r/flash.ps1 new file mode 100644 index 0000000..d3e86f6 --- /dev/null +++ b/firmware/cyd_35r/flash.ps1 @@ -0,0 +1,98 @@ +# Flash archived CYD WiFi Prov 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 460800 if needed." diff --git a/firmware/cyd_35r/media/preview.html b/firmware/cyd_35r/media/preview.html new file mode 100644 index 0000000..79613e4 --- /dev/null +++ b/firmware/cyd_35r/media/preview.html @@ -0,0 +1,50 @@ + + + + +Web Radio + + + +
+
Web Radio
+
HomeWiFi -52dBm
+
SomaFM Groove Salad
+
Playing
+
Vol 60%
+
+ + + + + +
+
+
SomaFM Groove Salad
+
SomaFM Drone Zone
+
SomaFM Indie Pop
+
+
+
+ + diff --git a/firmware/cyd_35r/meta.json b/firmware/cyd_35r/meta.json new file mode 100644 index 0000000..2be6d42 --- /dev/null +++ b/firmware/cyd_35r/meta.json @@ -0,0 +1,17 @@ +{ + "boardId": "esp32-3248s035", + "name": "CYD Web Radio (3.5\")", + "summary": "Internet MP3 radio with on-screen WiFi setup; speaker on GPIO26", + "description": "LVGL WiFi provisioning then station list on 480x320. Streams HTTP MP3 via ESP8266Audio to internal DAC2 (GPIO26 only). ST7796 + GT911; backlight GPIO27.", + "tags": [ + "wifi", + "radio", + "audio", + "mp3", + "touch", + "cyd35" + ], + "media": { + "preview": "media/preview.html" + } +} diff --git a/firmware/cyd_35r/partitions.bin b/firmware/cyd_35r/partitions.bin new file mode 100644 index 0000000..21800fa Binary files /dev/null and b/firmware/cyd_35r/partitions.bin differ diff --git a/firmware/export_from_build.ps1 b/firmware/export_from_build.ps1 new file mode 100644 index 0000000..fab58a4 --- /dev/null +++ b/firmware/export_from_build.ps1 @@ -0,0 +1,71 @@ +# Export firmware bins from PlatformIO build. +# Default: 2.8" (cyd -> firmware/cyd) +# 3.5": .\firmware\export_from_build.ps1 -Env cyd_35r +# (only after: pio run -e cyd_35r) + +param( + [switch]$SkipBuild, + [ValidateSet("cyd", "cyd_35r")] + [string]$Env = "cyd" +) + +$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 $Env + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } +} + +$src = Join-Path $root (".pio\build\" + $Env) +if ($Env -eq "cyd_35r") { + $dstName = "cyd_35r" + $boardLine = "board: ESP32-3248S035 (ST7796)" + $boardIdHint = "esp32-3248s035" +} else { + $dstName = "cyd" + $boardLine = "board: ESP32-2432S028R (ILI9341)" + $boardIdHint = "esp32-2432s028" +} +$dst = Join-Path $root ("firmware\" + $dstName) +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 — build with: pio run -e $Env" } + 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 + +$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 webradiotest ($dstName)", + "git: $git", + "date: $date", + $boardLine, + "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 (env=$Env 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/pins.h b/include/pins.h new file mode 100644 index 0000000..bbb0fac --- /dev/null +++ b/include/pins.h @@ -0,0 +1,45 @@ +#pragma once + +// Board pins — 2.8" default; 3.5" when CYD_BOARD_3248S035 is set (env:cyd_35r) + +#if defined(CYD_BOARD_3248S035) +#define TFT_BL_PIN 27 +#define SCREEN_WIDTH 320 +#define SCREEN_HEIGHT 480 +#define UI_HOR_RES 480 +#define UI_VER_RES 320 +/* 3.5C capacitive GT911 (ESP32-3248S035C). GPIO33 is I2C SDA, not TOUCH_CS. + * Backlight is GPIO27; INT is GPIO21 — they are NOT the same pin. */ +#define CYD_GT911_SDA 33 +#define CYD_GT911_SCL 32 +#define CYD_GT911_INT 21 +#define CYD_GT911_RST 25 +#else +#define TFT_BL_PIN 21 +#define SCREEN_WIDTH 240 +#define SCREEN_HEIGHT 320 +#define UI_HOR_RES 320 +#define UI_VER_RES 240 +/* 2.8" CYD: dedicated SoftSPI touch bus. */ +#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 +#endif + +/* Map 320x240 design coordinates onto the active UI resolution (identity on 2.8"). */ +#define UX(x) ((int)(((long)(x) * (UI_HOR_RES)) / 320)) +#define UY(y) ((int)(((long)(y) * (UI_VER_RES)) / 240)) +#define UW(w) UX(w) +#define UH(h) UY(h) + +/* Partial frame buffer: 3.5 needs smaller ratio to fit DRAM. */ +#if defined(CYD_BOARD_3248S035) +#define DRAW_BUF_DIV 20 +#else +#define DRAW_BUF_DIV 10 +#endif + +/* On-board amp on P4 — DAC2 only. Never use GPIO25 (2.8 touch CLK / 3.5 GT911 RST). */ +#define SPEAKER_PIN 26 diff --git a/include/radio_player.h b/include/radio_player.h new file mode 100644 index 0000000..0019c6c --- /dev/null +++ b/include/radio_player.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +struct RadioStation { + const char *name; + const char *url; +}; + +uint8_t radio_station_count(); +const RadioStation *radio_station(uint8_t idx); + +void radio_player_begin(); +void radio_player_loop(); + +bool radio_player_play(uint8_t station_idx); +void radio_player_stop(); +bool radio_player_is_playing(); +bool radio_player_is_starting(); + +uint8_t radio_player_station(); +void radio_player_next(); +void radio_player_prev(); + +void radio_player_set_volume(uint8_t percent); +uint8_t radio_player_volume(); + +const char *radio_player_status(); diff --git a/include/radio_ui.h b/include/radio_ui.h new file mode 100644 index 0000000..8b4e32e --- /dev/null +++ b/include/radio_ui.h @@ -0,0 +1,10 @@ +#pragma once + +void radio_ui_create(); +void radio_ui_show(); +void radio_ui_tick(); +bool radio_ui_is_active(); + +/** Wired from WiFi setup: reopen provisioning screens. */ +using RadioUiWifiCb = void (*)(); +void radio_ui_set_wifi_cb(RadioUiWifiCb cb); diff --git a/include/touch.h b/include/touch.h new file mode 100644 index 0000000..20bcfb0 --- /dev/null +++ b/include/touch.h @@ -0,0 +1,7 @@ +#pragma once + +#include +#include + +void touch_init(); +void touch_read_cb(lv_indev_t *indev, lv_indev_data_t *data); diff --git a/include/wifi_prov.h b/include/wifi_prov.h new file mode 100644 index 0000000..bf403a9 --- /dev/null +++ b/include/wifi_prov.h @@ -0,0 +1,92 @@ +#pragma once + +/** + * 通用 WiFi 配网核心(无 LVGL 依赖) + * + * 流程: + * 1. wifi_prov_begin() 读 NVS,有凭证则尝试 STA 连接 + * 2. 失败 / 无凭证 → 业务层开屏上 UI 或 SoftAP 门户 + * 3. wifi_prov_save() + wifi_prov_connect_to() 完成配网 + * 4. loop 里调用 wifi_prov_loop() 做断线重连与门户处理 + */ + +#include +#include + +#ifndef WIFI_PROV_AP_SSID +#define WIFI_PROV_AP_SSID "CYD-Setup" +#endif + +#ifndef WIFI_PROV_CONNECT_TIMEOUT_MS +#define WIFI_PROV_CONNECT_TIMEOUT_MS 30000u +#endif + +/* Ignore transient WL_NO_SSID_AVAIL / CONNECT_FAILED right after WiFi.begin(). */ +#ifndef WIFI_PROV_FAIL_GRACE_MS +#define WIFI_PROV_FAIL_GRACE_MS 8000u +#endif + +#ifndef WIFI_PROV_RECONNECT_MS +#define WIFI_PROV_RECONNECT_MS 8000u +#endif + +static constexpr int WIFI_PROV_MAX_SCAN = 16; +static constexpr size_t WIFI_PROV_SSID_MAX = 32; +static constexpr size_t WIFI_PROV_PASS_MAX = 64; + +struct WifiScanItem { + char ssid[WIFI_PROV_SSID_MAX + 1]; + int32_t rssi; + wifi_auth_mode_t auth; +}; + +enum WifiProvState : uint8_t { + WIFI_PROV_IDLE = 0, + WIFI_PROV_CONNECTING, + WIFI_PROV_CONNECTED, + WIFI_PROV_FAILED, + WIFI_PROV_PORTAL, +}; + +using WifiProvStateCb = void (*)(WifiProvState state, void *user); + +void wifi_prov_set_state_cb(WifiProvStateCb cb, void *user = nullptr); + +/** 初始化 Preferences + WiFi STA;有凭证则异步开始连接。返回是否已有凭证。 */ +bool wifi_prov_begin(); + +bool wifi_prov_has_credentials(); +bool wifi_prov_is_connected(); +WifiProvState wifi_prov_state(); + +String wifi_prov_ssid(); +String wifi_prov_ip(); +int8_t wifi_prov_rssi(); + +void wifi_prov_save(const char *ssid, const char *password); +void wifi_prov_clear(); + +/** 用已存凭证连接(异步,结果看 wifi_prov_loop / state) */ +void wifi_prov_connect_saved(); + +/** 连接指定网络并在成功后写入 NVS */ +void wifi_prov_connect_to(const char *ssid, const char *password); + +/** 同步扫描(会阻塞数秒;门户 HTML 可用)。屏上 UI 请用异步接口。 */ +int wifi_prov_scan(WifiScanItem *out, int max_items); + +/** 异步扫描:start 后每 loop 调 poll;完成时写入 out 并返回条数,进行中返回 -1。 */ +bool wifi_prov_scan_start(); +bool wifi_prov_scan_busy(); +int wifi_prov_scan_poll(WifiScanItem *out, int max_items); + +/** + * SoftAP + 简易 Captive Portal(手机连 CYD-Setup → http://192.168.4.1) + * 与屏上配网可并行;配网成功后会自动 stop。 + */ +void wifi_prov_portal_start(const char *ap_ssid = WIFI_PROV_AP_SSID); +void wifi_prov_portal_stop(); +bool wifi_prov_portal_active(); + +/** 每 loop 调用:重连、门户 HTTP、状态回调 */ +void wifi_prov_loop(); diff --git a/include/wifi_ui.h b/include/wifi_ui.h new file mode 100644 index 0000000..03e0a67 --- /dev/null +++ b/include/wifi_ui.h @@ -0,0 +1,21 @@ +#pragma once + +/** + * LVGL 屏上配网 UI(依赖 wifi_prov) + * 复用:拷贝 wifi_ui.* + 在业务里 wifi_ui_create() / wifi_ui_tick() + */ + +void wifi_ui_create(); +void wifi_ui_tick(); + +/** 强制进入配网页(改密 / 首次) */ +void wifi_ui_show_setup(); + +/** 显示已连接状态页 */ +void wifi_ui_show_status(); + +bool wifi_ui_is_setup(); + +/** 连上后回调;未设置则仍进状态页。业务层可切到电台等主界面。 */ +using WifiUiConnectedCb = void (*)(); +void wifi_ui_set_connected_cb(WifiUiConnectedCb cb); diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..12f5b3a --- /dev/null +++ b/platformio.ini @@ -0,0 +1,116 @@ +; CYD Web Radio — WiFi + HTTP MP3 → DAC2 (GPIO26) +[env:cyd] +platform = espressif32 +board = esp32dev +framework = arduino +monitor_speed = 460800 +upload_speed = 460800 +board_build.partitions = min_spiffs.csv + +lib_deps = + bodmer/TFT_eSPI@^2.5.43 + lvgl/lvgl@9.2.2 + earlephilhower/ESP8266Audio@1.9.7 + +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=49152 + -DLV_FONT_MONTSERRAT_14=1 + -DLV_FONT_MONTSERRAT_16=1 + -DLV_FONT_MONTSERRAT_18=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_LIST=1 + -DLV_USE_TEXTAREA=1 + -DLV_USE_KEYBOARD=1 + -DLV_USE_BTNMATRIX=1 + -I${PROJECT_DIR}/include + +extra_scripts = pre:copy_lv_conf.py + +; 3.5" CYD ESP32-3248S035C (ST7796 + GT911). Standalone flags — see CYD_PROJECT_GUIDE.md +[env:cyd_35r] +platform = espressif32 +board = esp32dev +framework = arduino +monitor_speed = 460800 +upload_speed = 460800 +board_build.partitions = min_spiffs.csv +lib_deps = + bodmer/TFT_eSPI@^2.5.43 + lvgl/lvgl@9.2.2 + earlephilhower/ESP8266Audio@1.9.7 +extra_scripts = pre:copy_lv_conf.py +build_flags = + -DCORE_DEBUG_LEVEL=0 + -DUSER_SETUP_LOADED=1 + -DST7796_DRIVER=1 + -DTFT_WIDTH=320 + -DTFT_HEIGHT=480 + -DTFT_MISO=12 + -DTFT_MOSI=13 + -DTFT_SCLK=14 + -DTFT_CS=15 + -DTFT_DC=2 + -DTFT_RST=-1 + -DTFT_BL=27 + -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 + -DUSE_HSPI_PORT + -DSUPPORT_TRANSACTIONS + -DTFT_RGB_ORDER=TFT_BGR + -DCYD_BOARD_3248S035=1 + -DLV_CONF_SKIP + -DLV_COLOR_DEPTH=16 + -DLV_MEM_SIZE=32768 + -DLV_FONT_MONTSERRAT_14=1 + -DLV_FONT_MONTSERRAT_16=1 + -DLV_FONT_MONTSERRAT_18=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_LIST=1 + -DLV_USE_TEXTAREA=1 + -DLV_USE_KEYBOARD=1 + -DLV_USE_BTNMATRIX=1 + -I${PROJECT_DIR}/include diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..f5271d6 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,72 @@ +#include +#include + +#include "pins.h" +#include "radio_player.h" +#include "radio_ui.h" +#include "touch.h" +#include "wifi_prov.h" +#include "wifi_ui.h" + +static constexpr uint32_t DRAW_BUF_SIZE = + (SCREEN_WIDTH * SCREEN_HEIGHT / DRAW_BUF_DIV * (LV_COLOR_DEPTH / 8)); +static uint32_t draw_buf[DRAW_BUF_SIZE / 4]; + +static void on_wifi_connected() { radio_ui_show(); } + +static void on_open_wifi_setup() { wifi_ui_show_setup(); } + +void setup() { + Serial.begin(460800); + delay(200); + Serial.println(); + Serial.printf("LVGL %d.%d.%d | CYD Web Radio\n", lv_version_major(), + lv_version_minor(), lv_version_patch()); + Serial.println("BOOT_OK"); + Serial.printf("heap=%u BL=%d panel=%dx%d ui=%dx%d spk=%d\n", + (unsigned)ESP.getFreeHeap(), TFT_BL_PIN, SCREEN_WIDTH, + SCREEN_HEIGHT, UI_HOR_RES, UI_VER_RES, SPEAKER_PIN); + + pinMode(TFT_BL_PIN, OUTPUT); + digitalWrite(TFT_BL_PIN, HIGH); + + /* Keep DAC1 (GPIO25) free for touch CLK / GT911 RST. */ + pinMode(SPEAKER_PIN, OUTPUT); + digitalWrite(SPEAKER_PIN, LOW); + + touch_init(); + + 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); + + radio_player_begin(); + radio_ui_create(); + radio_ui_set_wifi_cb(on_open_wifi_setup); + + wifi_ui_set_connected_cb(on_wifi_connected); + wifi_prov_begin(); + wifi_ui_create(); + + if (wifi_prov_is_connected()) { + radio_ui_show(); + } + + Serial.printf("ui_ok heap=%u\n", (unsigned)ESP.getFreeHeap()); +} + +void loop() { + lv_timer_handler(); + lv_tick_inc(5); + wifi_prov_loop(); + wifi_ui_tick(); + radio_ui_tick(); + radio_player_loop(); + delay(5); +} diff --git a/src/radio_player.cpp b/src/radio_player.cpp new file mode 100644 index 0000000..156f0b8 --- /dev/null +++ b/src/radio_player.cpp @@ -0,0 +1,228 @@ +#include "radio_player.h" +#include "pins.h" + +#include +#include +#include +#include + +#include "AudioFileSourceBuffer.h" +#include "AudioFileSourceHTTPStream.h" +#include "AudioGeneratorMP3.h" +#include "AudioOutputI2S.h" + +/* Public MP3 icy streams — keep list short (heap + UI). */ +static const RadioStation k_stations[] = { + {"SomaFM Groove Salad", "http://ice1.somafm.com/groovesalad-128-mp3"}, + {"SomaFM Drone Zone", "http://ice1.somafm.com/dronezone-128-mp3"}, + {"SomaFM Indie Pop", "http://ice1.somafm.com/indiepop-128-mp3"}, + {"SomaFM Boot Liquor", "http://ice1.somafm.com/bootliquor-128-mp3"}, + {"SomaFM Left Coast", "http://ice1.somafm.com/leftcoast-128-mp3"}, + {"Radio Swiss Pop", "http://stream.srg-ssr.ch/m/rsp/mp3_128"}, +}; + +static constexpr uint8_t k_station_n = + (uint8_t)(sizeof(k_stations) / sizeof(k_stations[0])); + +/* HTTP pre-buffer: larger = fewer underruns, more heap. */ +static constexpr int k_buf_bytes = 4096; + +static AudioGeneratorMP3 *g_mp3 = nullptr; +static AudioFileSourceHTTPStream *g_http = nullptr; +static AudioFileSourceBuffer *g_buff = nullptr; +static AudioOutputI2S *g_out = nullptr; +static uint8_t *g_prebuf = nullptr; + +static uint8_t g_station = 0; +static uint8_t g_volume = 60; +static bool g_starting = false; +static const char *g_status = "Idle"; +static char g_status_buf[48]; + +uint8_t radio_station_count() { return k_station_n; } + +const RadioStation *radio_station(uint8_t idx) { + if (idx >= k_station_n) { + return nullptr; + } + return &k_stations[idx]; +} + +static void apply_volume() { + if (!g_out) { + return; + } + /* Soft gain 0..1 — board amp is loud; keep headroom. */ + float g = (float)g_volume / 100.0f; + if (g > 1.0f) { + g = 1.0f; + } + g_out->SetGain(g * 0.55f); +} + +static void release_dac25() { + /* INTERNAL_DAC enables both channels; force GPIO26 (LEFT/DAC2) only. */ + i2s_set_dac_mode(I2S_DAC_CHANNEL_LEFT_EN); + dac_output_disable(DAC_CHANNEL_1); + pinMode(25, INPUT); +} + +static void teardown() { + if (g_mp3) { + if (g_mp3->isRunning()) { + g_mp3->stop(); + } + delete g_mp3; + g_mp3 = nullptr; + } + if (g_buff) { + delete g_buff; + g_buff = nullptr; + } + if (g_http) { + delete g_http; + g_http = nullptr; + } + if (g_out) { + g_out->stop(); + delete g_out; + g_out = nullptr; + } + if (g_prebuf) { + free(g_prebuf); + g_prebuf = nullptr; + } + dac_output_disable(DAC_CHANNEL_1); + dac_output_disable(DAC_CHANNEL_2); + pinMode(SPEAKER_PIN, OUTPUT); + digitalWrite(SPEAKER_PIN, LOW); + g_starting = false; +} + +void radio_player_begin() { + g_status = "Ready"; +} + +void radio_player_stop() { + teardown(); + g_status = "Stopped"; +} + +bool radio_player_is_playing() { + return g_mp3 && g_mp3->isRunning(); +} + +bool radio_player_is_starting() { return g_starting; } + +uint8_t radio_player_station() { return g_station; } + +uint8_t radio_player_volume() { return g_volume; } + +void radio_player_set_volume(uint8_t percent) { + if (percent > 100) { + percent = 100; + } + g_volume = percent; + apply_volume(); +} + +const char *radio_player_status() { return g_status; } + +bool radio_player_play(uint8_t station_idx) { + if (station_idx >= k_station_n) { + g_status = "Bad station"; + return false; + } + if (WiFi.status() != WL_CONNECTED) { + g_status = "WiFi needed"; + return false; + } + + teardown(); + g_station = station_idx; + g_starting = true; + snprintf(g_status_buf, sizeof(g_status_buf), "Open %s", + k_stations[station_idx].name); + g_status = g_status_buf; + + g_prebuf = (uint8_t *)malloc(k_buf_bytes); + if (!g_prebuf) { + g_status = "No heap for buf"; + g_starting = false; + return false; + } + + /* port 0, INTERNAL_DAC=1, dma_buf_count=8 — then strip DAC1 (GPIO25). */ + g_out = new AudioOutputI2S(0, AudioOutputI2S::INTERNAL_DAC, 8); + if (!g_out) { + teardown(); + g_status = "No output"; + return false; + } + apply_volume(); + if (!g_out->begin()) { + teardown(); + g_status = "DAC begin fail"; + return false; + } + release_dac25(); + + g_http = new AudioFileSourceHTTPStream(k_stations[station_idx].url); + if (!g_http) { + teardown(); + g_status = "No HTTP src"; + return false; + } + g_http->SetReconnect(3, 2000); + + g_buff = new AudioFileSourceBuffer(g_http, g_prebuf, k_buf_bytes); + if (!g_buff) { + teardown(); + g_status = "No buffer"; + return false; + } + + g_mp3 = new AudioGeneratorMP3(); + if (!g_mp3) { + teardown(); + g_status = "No decoder"; + return false; + } + + if (!g_mp3->begin(g_buff, g_out)) { + teardown(); + g_status = "MP3 begin fail"; + return false; + } + + release_dac25(); + g_starting = false; + snprintf(g_status_buf, sizeof(g_status_buf), "Playing"); + g_status = g_status_buf; + Serial.printf("radio play [%u] %s\n", (unsigned)station_idx, + k_stations[station_idx].url); + return true; +} + +void radio_player_next() { + uint8_t n = (uint8_t)((g_station + 1) % k_station_n); + radio_player_play(n); +} + +void radio_player_prev() { + uint8_t n = + (uint8_t)((g_station + k_station_n - 1) % k_station_n); + radio_player_play(n); +} + +void radio_player_loop() { + if (!g_mp3) { + return; + } + if (g_mp3->isRunning()) { + if (!g_mp3->loop()) { + g_status = "Stream ended"; + teardown(); + } + } +} diff --git a/src/radio_ui.cpp b/src/radio_ui.cpp new file mode 100644 index 0000000..e90f2c6 --- /dev/null +++ b/src/radio_ui.cpp @@ -0,0 +1,318 @@ +#include "radio_ui.h" +#include "pins.h" +#include "radio_player.h" +#include "wifi_prov.h" + +#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_LINE = 0x1C2A38; +static constexpr uint32_t COL_OK = 0x3DDC97; +static constexpr uint32_t COL_ACCENT = 0x4FC3F7; +static constexpr uint32_t COL_BTN = 0x243140; +static constexpr uint32_t COL_BTN_ACC = 0x1A4A6E; +static constexpr uint32_t COL_BTN_WARN = 0x6B3A1E; + +static lv_obj_t *scr = nullptr; +static lv_obj_t *lbl_station = nullptr; +static lv_obj_t *lbl_status = nullptr; +static lv_obj_t *lbl_vol = nullptr; +static lv_obj_t *lbl_wifi = nullptr; +static lv_obj_t *list = nullptr; +static lv_obj_t *lbl_play = nullptr; +static bool g_active = false; +static uint32_t g_last_ui_ms = 0; +static RadioUiWifiCb g_wifi_cb = nullptr; + +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); + lv_obj_add_flag(btn, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_ext_click_area(btn, 12); +} + +static lv_obj_t *make_btn(lv_obj_t *parent, const char *text, uint32_t bg, + lv_event_cb_t cb, int w, int h) { + lv_obj_t *btn = lv_button_create(parent); + if (!btn) { + return nullptr; + } + lv_obj_set_size(btn, w, h); + style_btn(btn, bg); + lv_obj_t *lab = lv_label_create(btn); + if (lab) { + lv_label_set_text(lab, text); + lv_obj_set_style_text_color(lab, lv_color_hex(COL_INK), 0); + lv_obj_set_style_text_font(lab, &lv_font_montserrat_14, 0); + lv_obj_center(lab); + lv_obj_clear_flag(lab, LV_OBJ_FLAG_CLICKABLE); + } + if (cb) { + lv_obj_add_event_cb(btn, cb, LV_EVENT_CLICKED, nullptr); + } + return btn; +} + +static void refresh_labels() { + const RadioStation *st = radio_station(radio_player_station()); + if (lbl_station && st) { + lv_label_set_text(lbl_station, st->name); + } + if (lbl_status) { + lv_label_set_text(lbl_status, radio_player_status()); + lv_obj_set_style_text_color( + lbl_status, + lv_color_hex(radio_player_is_playing() ? COL_OK : COL_MUTED), 0); + } + if (lbl_vol) { + char buf[24]; + snprintf(buf, sizeof(buf), "Vol %u%%", (unsigned)radio_player_volume()); + lv_label_set_text(lbl_vol, buf); + } + if (lbl_wifi) { + if (wifi_prov_is_connected()) { + char buf[40]; + snprintf(buf, sizeof(buf), "%s %ddBm", wifi_prov_ssid().c_str(), + (int)wifi_prov_rssi()); + lv_label_set_text(lbl_wifi, buf); + } else { + lv_label_set_text(lbl_wifi, "WiFi offline"); + } + } + if (lbl_play) { + lv_label_set_text(lbl_play, + radio_player_is_playing() || radio_player_is_starting() + ? "Stop" + : "Play"); + } +} + +static void highlight_list() { + if (!list) { + return; + } + const uint8_t cur = radio_player_station(); + const uint32_t n = lv_obj_get_child_count(list); + for (uint32_t i = 0; i < n; i++) { + lv_obj_t *row = lv_obj_get_child(list, i); + if (!row) { + continue; + } + const bool on = (i == cur); + lv_obj_set_style_bg_color(row, lv_color_hex(on ? COL_BTN_ACC : COL_BTN), + 0); + } +} + +static void on_row(lv_event_t *e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) { + return; + } + intptr_t idx = (intptr_t)lv_event_get_user_data(e); + if (idx < 0 || idx >= radio_station_count()) { + return; + } + radio_player_play((uint8_t)idx); + highlight_list(); + refresh_labels(); +} + +static void on_play(lv_event_t *e) { + LV_UNUSED(e); + if (radio_player_is_playing() || radio_player_is_starting()) { + radio_player_stop(); + } else { + radio_player_play(radio_player_station()); + } + highlight_list(); + refresh_labels(); +} + +static void on_prev(lv_event_t *e) { + LV_UNUSED(e); + radio_player_prev(); + highlight_list(); + refresh_labels(); +} + +static void on_next(lv_event_t *e) { + LV_UNUSED(e); + radio_player_next(); + highlight_list(); + refresh_labels(); +} + +static void on_vol_down(lv_event_t *e) { + LV_UNUSED(e); + int v = (int)radio_player_volume() - 10; + if (v < 0) { + v = 0; + } + radio_player_set_volume((uint8_t)v); + refresh_labels(); +} + +static void on_vol_up(lv_event_t *e) { + LV_UNUSED(e); + int v = (int)radio_player_volume() + 10; + if (v > 100) { + v = 100; + } + radio_player_set_volume((uint8_t)v); + refresh_labels(); +} + +static void on_wifi(lv_event_t *e) { + LV_UNUSED(e); + radio_player_stop(); + g_active = false; + if (g_wifi_cb) { + g_wifi_cb(); + } +} + +static void build_list() { + if (!list) { + return; + } + lv_obj_clean(list); + const uint8_t n = radio_station_count(); + for (uint8_t i = 0; i < n; i++) { + const RadioStation *st = radio_station(i); + if (!st) { + continue; + } + lv_obj_t *row = lv_button_create(list); + lv_obj_set_width(row, LV_PCT(100)); + lv_obj_set_height(row, UH(32)); + style_btn(row, COL_BTN); + lv_obj_set_style_pad_left(row, 8, 0); + lv_obj_add_event_cb(row, on_row, LV_EVENT_CLICKED, (void *)(intptr_t)i); + + lv_obj_t *lab = lv_label_create(row); + lv_label_set_text(lab, st->name); + lv_obj_set_style_text_color(lab, lv_color_hex(COL_INK), 0); + lv_obj_set_style_text_font(lab, &lv_font_montserrat_14, 0); + lv_obj_align(lab, LV_ALIGN_LEFT_MID, 0, 0); + lv_obj_clear_flag(lab, LV_OBJ_FLAG_CLICKABLE); + } + highlight_list(); +} + +static void build() { + if (scr) { + return; + } + scr = lv_obj_create(NULL); + 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); + + lv_obj_t *title = lv_label_create(scr); + lv_label_set_text(title, "Web Radio"); + lv_obj_set_style_text_font(title, &lv_font_montserrat_18, 0); + lv_obj_set_style_text_color(title, lv_color_hex(COL_ACCENT), 0); + lv_obj_align(title, LV_ALIGN_TOP_LEFT, UX(10), UY(6)); + + lbl_wifi = lv_label_create(scr); + lv_label_set_text(lbl_wifi, ""); + lv_obj_set_style_text_font(lbl_wifi, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(lbl_wifi, lv_color_hex(COL_MUTED), 0); + lv_obj_align(lbl_wifi, LV_ALIGN_TOP_RIGHT, UX(-10), UY(8)); + + lbl_station = lv_label_create(scr); + lv_label_set_text(lbl_station, "-"); + lv_label_set_long_mode(lbl_station, LV_LABEL_LONG_SCROLL_CIRCULAR); + lv_obj_set_width(lbl_station, UI_HOR_RES - UW(20)); + lv_obj_set_style_text_font(lbl_station, &lv_font_montserrat_16, 0); + lv_obj_set_style_text_color(lbl_station, lv_color_hex(COL_INK), 0); + lv_obj_align(lbl_station, LV_ALIGN_TOP_LEFT, UX(10), UY(30)); + + lbl_status = lv_label_create(scr); + lv_label_set_text(lbl_status, "Idle"); + lv_obj_set_style_text_font(lbl_status, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(lbl_status, lv_color_hex(COL_MUTED), 0); + lv_obj_align(lbl_status, LV_ALIGN_TOP_LEFT, UX(10), UY(52)); + + lbl_vol = lv_label_create(scr); + lv_label_set_text(lbl_vol, "Vol 60%"); + lv_obj_set_style_text_font(lbl_vol, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(lbl_vol, lv_color_hex(COL_MUTED), 0); + lv_obj_align(lbl_vol, LV_ALIGN_TOP_RIGHT, UX(-10), UY(52)); + + lv_obj_t *bar = lv_obj_create(scr); + lv_obj_set_size(bar, UI_HOR_RES - UW(16), UH(42)); + lv_obj_align(bar, LV_ALIGN_TOP_MID, 0, UY(72)); + lv_obj_set_style_bg_opa(bar, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(bar, 0, 0); + lv_obj_set_style_pad_all(bar, 0, 0); + lv_obj_set_flex_flow(bar, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(bar, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, + LV_FLEX_ALIGN_CENTER); + lv_obj_clear_flag(bar, LV_OBJ_FLAG_SCROLLABLE); + + make_btn(bar, "Prev", COL_BTN, on_prev, UW(56), UH(38)); + lv_obj_t *play = make_btn(bar, "Play", COL_BTN_ACC, on_play, UW(72), UH(38)); + if (play) { + lbl_play = lv_obj_get_child(play, 0); + } + make_btn(bar, "Next", COL_BTN, on_next, UW(56), UH(38)); + make_btn(bar, "Vol-", COL_BTN, on_vol_down, UW(48), UH(38)); + make_btn(bar, "Vol+", COL_BTN, on_vol_up, UW(48), UH(38)); + + list = lv_obj_create(scr); + lv_obj_set_size(list, UI_HOR_RES - UW(16), UI_VER_RES - UH(158)); + lv_obj_align(list, LV_ALIGN_TOP_MID, 0, UY(118)); + lv_obj_set_style_bg_color(list, lv_color_hex(COL_LINE), 0); + lv_obj_set_style_border_width(list, 0, 0); + lv_obj_set_style_radius(list, 8, 0); + lv_obj_set_style_pad_all(list, 4, 0); + lv_obj_set_style_pad_row(list, 4, 0); + lv_obj_set_flex_flow(list, LV_FLEX_FLOW_COLUMN); + lv_obj_set_scroll_dir(list, LV_DIR_VER); + lv_obj_add_flag(list, LV_OBJ_FLAG_SCROLLABLE); + + lv_obj_t *foot = lv_obj_create(scr); + lv_obj_set_size(foot, UI_HOR_RES - UW(16), UH(36)); + lv_obj_align(foot, LV_ALIGN_BOTTOM_MID, 0, UY(-6)); + lv_obj_set_style_bg_opa(foot, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(foot, 0, 0); + lv_obj_set_style_pad_all(foot, 0, 0); + lv_obj_clear_flag(foot, LV_OBJ_FLAG_SCROLLABLE); + make_btn(foot, "WiFi Setup", COL_BTN_WARN, on_wifi, UW(120), UH(34)); + + build_list(); +} + +void radio_ui_set_wifi_cb(RadioUiWifiCb cb) { g_wifi_cb = cb; } + +void radio_ui_create() { build(); } + +void radio_ui_show() { + build(); + g_active = true; + highlight_list(); + refresh_labels(); + lv_screen_load(scr); +} + +bool radio_ui_is_active() { return g_active; } + +void radio_ui_tick() { + if (!g_active) { + return; + } + if (millis() - g_last_ui_ms < 400) { + return; + } + g_last_ui_ms = millis(); + refresh_labels(); +} diff --git a/src/touch.cpp b/src/touch.cpp new file mode 100644 index 0000000..fca8f59 --- /dev/null +++ b/src/touch.cpp @@ -0,0 +1,284 @@ +#include "touch.h" +#include "pins.h" + +#include + +#if defined(CYD_BOARD_3248S035) +#include +#endif + +/** + * 2.8": dedicated SoftSPI + BL/TR diagonal fix. + * 3.5C (3248S035C): capacitive GT911 on I2C SDA33/SCL32/INT21/RST25. + */ + +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 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; +} + +#if defined(CYD_BOARD_3248S035) + +/* Minimal GT911 — no config rewrite (TAMC setResolution can hang Wire). */ +static uint8_t s_gt_addr = 0x5D; +static constexpr uint16_t GT_POINT_INFO = 0x814E; +static constexpr uint16_t GT_POINT_1 = 0x814F; + +static bool gt_write8(uint16_t reg, uint8_t val) { + Wire.beginTransmission(s_gt_addr); + Wire.write((uint8_t)(reg >> 8)); + Wire.write((uint8_t)(reg & 0xFF)); + Wire.write(val); + return Wire.endTransmission() == 0; +} + +static bool gt_read(uint16_t reg, uint8_t *buf, uint8_t len) { + Wire.beginTransmission(s_gt_addr); + Wire.write((uint8_t)(reg >> 8)); + Wire.write((uint8_t)(reg & 0xFF)); + if (Wire.endTransmission(false) != 0) { + return false; + } + const uint8_t n = Wire.requestFrom(s_gt_addr, len); + if (n != len) { + return false; + } + for (uint8_t i = 0; i < len; i++) { + buf[i] = Wire.read(); + } + return true; +} + +static bool gt_probe(uint8_t addr) { + Wire.beginTransmission(addr); + return Wire.endTransmission() == 0; +} + +/* TAMC-compatible address select: INT low => 0x5D, INT high => 0x14 */ +static void gt_hw_reset(bool addr_0x14) { + pinMode(CYD_GT911_INT, OUTPUT); + pinMode(CYD_GT911_RST, OUTPUT); + digitalWrite(CYD_GT911_INT, LOW); + digitalWrite(CYD_GT911_RST, LOW); + delay(10); + digitalWrite(CYD_GT911_INT, addr_0x14 ? HIGH : LOW); + delay(1); + digitalWrite(CYD_GT911_RST, HIGH); + delay(5); + digitalWrite(CYD_GT911_INT, LOW); + delay(50); + /* Backlight is GPIO27 — leave INT as input for polling / optional IRQ. */ + pinMode(CYD_GT911_INT, INPUT); + delay(50); +} + +static bool gt_find_addr() { + for (uint8_t a : {uint8_t(0x5D), uint8_t(0x14)}) { + if (gt_probe(a)) { + s_gt_addr = a; + return true; + } + } + return false; +} + +void touch_init() { + Serial.println("Touch: GT911 I2C capacitive (3.5C) minimal"); + Wire.begin(CYD_GT911_SDA, CYD_GT911_SCL); + Wire.setClock(100000); +#if defined(WIRE_HAS_TIMEOUT) || defined(ESP32) + Wire.setTimeOut(50); +#endif + + /* Always hw-reset: probe-only leaves some boards with stuck buffer / bad INT. */ + Serial.println("GT911 hw reset 0x5D"); + gt_hw_reset(false); + if (!gt_find_addr()) { + Serial.println("GT911 not found — hw reset 0x14"); + gt_hw_reset(true); + gt_find_addr(); + } + gt_write8(GT_POINT_INFO, 0); + + Serial.printf("GT911 probe 0x5D=%s 0x14=%s using=0x%02X\n", + gt_probe(0x5D) ? "OK" : "no", gt_probe(0x14) ? "OK" : "no", + s_gt_addr); + Serial.printf("GT911 SDA=%d SCL=%d INT=%d RST=%d\n", CYD_GT911_SDA, + CYD_GT911_SCL, CYD_GT911_INT, CYD_GT911_RST); +} + +void touch_read_cb(lv_indev_t * /*indev*/, lv_indev_data_t *data) { + uint8_t info = 0; + static uint32_t last_log = 0; + const bool do_log = (millis() - last_log > 200); + + if (!gt_read(GT_POINT_INFO, &info, 1)) { + data->state = LV_INDEV_STATE_RELEASED; + g_dbg_touch_down = false; + if (do_log) { + last_log = millis(); + Serial.println("TouchProbe GT911 I2C err"); + } + return; + } + + const uint8_t touches = info & 0x0F; + const bool buffered = (info & 0x80) != 0; + + if (!buffered || touches == 0) { + if (buffered) { + gt_write8(GT_POINT_INFO, 0); + } + data->state = LV_INDEV_STATE_RELEASED; + g_dbg_touch_down = false; + return; + } + + uint8_t pt[7]; + if (!gt_read(GT_POINT_1, pt, 7)) { + gt_write8(GT_POINT_INFO, 0); + data->state = LV_INDEV_STATE_RELEASED; + g_dbg_touch_down = false; + return; + } + gt_write8(GT_POINT_INFO, 0); + + const int16_t raw_x = (int16_t)(pt[1] | (pt[2] << 8)); + const int16_t raw_y = (int16_t)(pt[3] | (pt[4] << 8)); + + /* Native 320x480 → LVGL 270. Mirror long axis so L/R matches UI + * (else preset 3 hits 10). Resulting UI ≈ (H-1-raw_y, raw_x). */ + const int16_t nx = clampi(raw_x, 0, SCREEN_WIDTH - 1); + const int16_t ny = clampi(raw_y, 0, SCREEN_HEIGHT - 1); + const int16_t feed_x = (int16_t)((SCREEN_WIDTH - 1) - nx); + const int16_t feed_y = (int16_t)((SCREEN_HEIGHT - 1) - ny); + const int16_t ui_x = (int16_t)((SCREEN_HEIGHT - 1) - ny); + const int16_t ui_y = nx; + + g_dbg_phys_x = raw_x; + g_dbg_phys_y = raw_y; + g_dbg_logic_x = ui_x; + g_dbg_logic_y = ui_y; + g_dbg_touch_down = true; + + if (do_log) { + last_log = millis(); + Serial.printf("Touch HIT GT911 raw(%d,%d) ~ui(%d,%d)\n", raw_x, raw_y, + ui_x, ui_y); + } + + data->point.x = feed_x; + data->point.y = feed_y; + data->state = LV_INDEV_STATE_PRESSED; +} + +#else + +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; +} + +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); + + 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); + + 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; +} + +#endif diff --git a/src/wifi_prov.cpp b/src/wifi_prov.cpp new file mode 100644 index 0000000..b861a83 --- /dev/null +++ b/src/wifi_prov.cpp @@ -0,0 +1,504 @@ +#include "wifi_prov.h" + +#include +#include +#include +#include + +static constexpr const char *NVS_NS = "wifi_prov"; +static constexpr const char *NVS_SSID = "ssid"; +static constexpr const char *NVS_PASS = "pass"; + +static Preferences g_prefs; +static WifiProvState g_state = WIFI_PROV_IDLE; +static WifiProvStateCb g_cb = nullptr; +static void *g_cb_user = nullptr; + +static char g_ssid[WIFI_PROV_SSID_MAX + 1] = {}; +static char g_pass[WIFI_PROV_PASS_MAX + 1] = {}; +static bool g_has_creds = false; + +static uint32_t g_connect_started_ms = 0; +static uint32_t g_last_reconnect_ms = 0; +static bool g_want_connect = false; + +static DNSServer *g_dns = nullptr; +static WebServer *g_http = nullptr; +static bool g_portal = false; +static char g_ap_ssid[33] = WIFI_PROV_AP_SSID; +static bool g_scan_async = false; + +static void set_state(WifiProvState s) { + if (g_state == s) { + return; + } + g_state = s; + if (g_cb) { + g_cb(s, g_cb_user); + } +} + +static void load_creds() { + g_has_creds = false; + g_ssid[0] = '\0'; + g_pass[0] = '\0'; + if (!g_prefs.begin(NVS_NS, true)) { + return; + } + String ssid = g_prefs.getString(NVS_SSID, ""); + String pass = g_prefs.getString(NVS_PASS, ""); + g_prefs.end(); + if (ssid.length() == 0 || ssid.length() > WIFI_PROV_SSID_MAX) { + return; + } + strncpy(g_ssid, ssid.c_str(), WIFI_PROV_SSID_MAX); + g_ssid[WIFI_PROV_SSID_MAX] = '\0'; + strncpy(g_pass, pass.c_str(), WIFI_PROV_PASS_MAX); + g_pass[WIFI_PROV_PASS_MAX] = '\0'; + g_has_creds = true; +} + +static const char *wifi_status_str(wl_status_t st) { + switch (st) { + case WL_IDLE_STATUS: + return "IDLE"; + case WL_NO_SSID_AVAIL: + return "NO_SSID"; + case WL_SCAN_COMPLETED: + return "SCAN_DONE"; + case WL_CONNECTED: + return "CONNECTED"; + case WL_CONNECT_FAILED: + return "FAIL"; + case WL_CONNECTION_LOST: + return "LOST"; + case WL_DISCONNECTED: + return "DISC"; + default: + return "?"; + } +} + +static void begin_sta_connect(const char *ssid, const char *password) { + if (g_scan_async) { + WiFi.scanDelete(); + g_scan_async = false; + } + + WiFi.mode(WIFI_STA); + WiFi.setAutoReconnect(true); + WiFi.persistent(false); + /* Full disconnect + settle — needed after async scan on ESP32. */ + WiFi.disconnect(true, true); + delay(200); + + const size_t plen = password ? strlen(password) : 0; + Serial.printf("[wifi] connecting to \"%s\" pass_len=%u\n", ssid, + (unsigned)plen); + WiFi.begin(ssid, password ? password : ""); + g_want_connect = true; + g_connect_started_ms = millis(); + g_last_reconnect_ms = millis(); + set_state(WIFI_PROV_CONNECTING); +} + +static String html_escape(const String &s) { + String o; + o.reserve(s.length()); + for (size_t i = 0; i < s.length(); ++i) { + const char c = s[i]; + if (c == '&') { + o += "&"; + } else if (c == '<') { + o += "<"; + } else if (c == '>') { + o += ">"; + } else if (c == '"') { + o += """; + } else { + o += c; + } + } + return o; +} + +static String portal_page() { + WifiScanItem items[WIFI_PROV_MAX_SCAN]; + const int n = wifi_prov_scan(items, WIFI_PROV_MAX_SCAN); + + String html; + html.reserve(2048); + html += F("" + "" + "CYD WiFi" + "" + "

WiFi Setup

" + "
" + "" + "" + "" + "" + "
" + "

AP: "); + html += html_escape(String(g_ap_ssid)); + html += F(" · http://192.168.4.1/

"); + return html; +} + +static void handle_root() { + g_http->send(200, "text/html", portal_page()); +} + +static void handle_save() { + String ssid = g_http->arg("ssid_manual"); + ssid.trim(); + if (ssid.length() == 0) { + ssid = g_http->arg("ssid"); + ssid.trim(); + } + String pass = g_http->arg("pass"); + + g_http->send(200, "text/html", + F("

Connecting...

" + "

You can close this page. Device will leave setup AP.

")); + + if (ssid.length() > 0) { + delay(200); + wifi_prov_portal_stop(); + wifi_prov_connect_to(ssid.c_str(), pass.c_str()); + } +} + +static void handle_captive() { + g_http->sendHeader("Location", "http://192.168.4.1/", true); + g_http->send(302, "text/plain", ""); +} + +static void handle_not_found() { + handle_captive(); +} + +void wifi_prov_set_state_cb(WifiProvStateCb cb, void *user) { + g_cb = cb; + g_cb_user = user; +} + +bool wifi_prov_begin() { + load_creds(); + WiFi.mode(WIFI_STA); + WiFi.setAutoReconnect(true); + WiFi.persistent(false); + + if (g_has_creds) { + begin_sta_connect(g_ssid, g_pass); + } else { + set_state(WIFI_PROV_IDLE); + Serial.println("[wifi] no credentials"); + } + return g_has_creds; +} + +bool wifi_prov_has_credentials() { return g_has_creds; } + +bool wifi_prov_is_connected() { + return WiFi.status() == WL_CONNECTED; +} + +WifiProvState wifi_prov_state() { return g_state; } + +String wifi_prov_ssid() { + if (WiFi.status() == WL_CONNECTED) { + return WiFi.SSID(); + } + return String(g_ssid); +} + +String wifi_prov_ip() { + if (WiFi.status() != WL_CONNECTED) { + return String(); + } + return WiFi.localIP().toString(); +} + +int8_t wifi_prov_rssi() { + if (WiFi.status() != WL_CONNECTED) { + return 0; + } + return (int8_t)WiFi.RSSI(); +} + +void wifi_prov_save(const char *ssid, const char *password) { + if (!ssid || !ssid[0]) { + return; + } + strncpy(g_ssid, ssid, WIFI_PROV_SSID_MAX); + g_ssid[WIFI_PROV_SSID_MAX] = '\0'; + if (password) { + strncpy(g_pass, password, WIFI_PROV_PASS_MAX); + } else { + g_pass[0] = '\0'; + } + g_pass[WIFI_PROV_PASS_MAX] = '\0'; + g_has_creds = true; + + if (g_prefs.begin(NVS_NS, false)) { + g_prefs.putString(NVS_SSID, g_ssid); + g_prefs.putString(NVS_PASS, g_pass); + g_prefs.end(); + } + Serial.printf("[wifi] saved \"%s\"\n", g_ssid); +} + +void wifi_prov_clear() { + g_ssid[0] = '\0'; + g_pass[0] = '\0'; + g_has_creds = false; + g_want_connect = false; + if (g_prefs.begin(NVS_NS, false)) { + g_prefs.clear(); + g_prefs.end(); + } + WiFi.disconnect(true, true); + set_state(WIFI_PROV_IDLE); + Serial.println("[wifi] credentials cleared"); +} + +void wifi_prov_connect_saved() { + if (!g_has_creds) { + set_state(WIFI_PROV_FAILED); + return; + } + begin_sta_connect(g_ssid, g_pass); +} + +void wifi_prov_connect_to(const char *ssid, const char *password) { + wifi_prov_save(ssid, password ? password : ""); + begin_sta_connect(g_ssid, g_pass); +} + +static int fill_scan_results(WifiScanItem *out, int max_items, int found) { + if (found <= 0) { + WiFi.scanDelete(); + return 0; + } + const int n = found < max_items ? found : max_items; + for (int i = 0; i < n; ++i) { + String s = WiFi.SSID(i); + strncpy(out[i].ssid, s.c_str(), WIFI_PROV_SSID_MAX); + out[i].ssid[WIFI_PROV_SSID_MAX] = '\0'; + out[i].rssi = WiFi.RSSI(i); + out[i].auth = WiFi.encryptionType(i); + } + WiFi.scanDelete(); + return n; +} + +int wifi_prov_scan(WifiScanItem *out, int max_items) { + if (!out || max_items <= 0) { + return 0; + } + if (max_items > WIFI_PROV_MAX_SCAN) { + max_items = WIFI_PROV_MAX_SCAN; + } + if (g_scan_async) { + WiFi.scanDelete(); + g_scan_async = false; + } + + if (g_portal) { + WiFi.mode(WIFI_AP_STA); + } else { + WiFi.mode(WIFI_STA); + } + + const int found = WiFi.scanNetworks(/*async=*/false, /*hidden=*/true); + return fill_scan_results(out, max_items, found); +} + +bool wifi_prov_scan_start() { + if (g_scan_async) { + return true; + } + if (g_portal) { + WiFi.mode(WIFI_AP_STA); + } else { + WiFi.mode(WIFI_STA); + } + /* async — does not block LVGL loop */ + const int16_t rc = WiFi.scanNetworks(/*async=*/true, /*hidden=*/true); + if (rc == WIFI_SCAN_FAILED) { + Serial.println("[wifi] scan start failed"); + return false; + } + g_scan_async = true; + Serial.println("[wifi] scan async started"); + return true; +} + +bool wifi_prov_scan_busy() { return g_scan_async; } + +int wifi_prov_scan_poll(WifiScanItem *out, int max_items) { + if (!g_scan_async) { + return 0; + } + if (!out || max_items <= 0) { + return 0; + } + if (max_items > WIFI_PROV_MAX_SCAN) { + max_items = WIFI_PROV_MAX_SCAN; + } + + const int16_t found = WiFi.scanComplete(); + if (found == WIFI_SCAN_RUNNING) { + return -1; + } + g_scan_async = false; + if (found == WIFI_SCAN_FAILED) { + WiFi.scanDelete(); + Serial.println("[wifi] scan failed"); + return 0; + } + const int n = fill_scan_results(out, max_items, found); + Serial.printf("[wifi] scan done n=%d\n", n); + return n; +} + +void wifi_prov_portal_start(const char *ap_ssid) { + if (g_portal) { + return; + } + if (ap_ssid && ap_ssid[0]) { + strncpy(g_ap_ssid, ap_ssid, sizeof(g_ap_ssid) - 1); + g_ap_ssid[sizeof(g_ap_ssid) - 1] = '\0'; + } + + WiFi.mode(WIFI_AP_STA); + WiFi.softAP(g_ap_ssid); + delay(100); + + g_dns = new DNSServer(); + g_http = new WebServer(80); + g_dns->start(53, "*", WiFi.softAPIP()); + + g_http->on("/", HTTP_GET, handle_root); + g_http->on("/save", HTTP_POST, handle_save); + g_http->on("/generate_204", HTTP_GET, handle_captive); + g_http->on("/gen_204", HTTP_GET, handle_captive); + g_http->on("/hotspot-detect.html", HTTP_GET, handle_root); + g_http->on("/connecttest.txt", HTTP_GET, handle_captive); + g_http->on("/ncsi.txt", HTTP_GET, handle_captive); + g_http->onNotFound(handle_not_found); + g_http->begin(); + + g_portal = true; + set_state(WIFI_PROV_PORTAL); + Serial.printf("[wifi] portal AP \"%s\" @ %s\n", g_ap_ssid, + WiFi.softAPIP().toString().c_str()); +} + +void wifi_prov_portal_stop() { + if (!g_portal) { + return; + } + if (g_http) { + g_http->stop(); + delete g_http; + g_http = nullptr; + } + if (g_dns) { + g_dns->stop(); + delete g_dns; + g_dns = nullptr; + } + WiFi.softAPdisconnect(true); + g_portal = false; + Serial.println("[wifi] portal stopped"); +} + +bool wifi_prov_portal_active() { return g_portal; } + +void wifi_prov_loop() { + if (g_portal && g_dns && g_http) { + g_dns->processNextRequest(); + g_http->handleClient(); + } + + const wl_status_t st = WiFi.status(); + const uint32_t now = millis(); + + if (g_want_connect || g_state == WIFI_PROV_CONNECTING) { + if (st == WL_CONNECTED) { + g_want_connect = false; + if (g_portal) { + wifi_prov_portal_stop(); + } + set_state(WIFI_PROV_CONNECTED); + Serial.printf("[wifi] connected %s ip=%s rssi=%d\n", WiFi.SSID().c_str(), + WiFi.localIP().toString().c_str(), WiFi.RSSI()); + return; + } + + const uint32_t elapsed = now - g_connect_started_ms; + static uint32_t last_prog = 0; + if (now - last_prog >= 2000) { + last_prog = now; + Serial.printf("[wifi] wait %lus status=%s(%d)\n", + (unsigned long)(elapsed / 1000UL), wifi_status_str(st), + (int)st); + } + + /* Do NOT treat NO_SSID / CONNECT_FAILED as instant fail — after scan/ + * disconnect ESP32 often reports NO_SSID for several seconds. */ + const bool hard_timeout = elapsed >= WIFI_PROV_CONNECT_TIMEOUT_MS; + const bool early_fail = + elapsed >= WIFI_PROV_FAIL_GRACE_MS && + (st == WL_CONNECT_FAILED || st == WL_NO_SSID_AVAIL); + + if (hard_timeout || early_fail) { + g_want_connect = false; + set_state(WIFI_PROV_FAILED); + Serial.printf("[wifi] connect failed after %lus status=%s\n", + (unsigned long)(elapsed / 1000UL), wifi_status_str(st)); + return; + } + } + + /* Dropped while marked connected → actually restart STA. */ + if (g_has_creds && !g_portal && g_state == WIFI_PROV_CONNECTED && + st != WL_CONNECTED) { + Serial.println("[wifi] link lost — reconnect"); + begin_sta_connect(g_ssid, g_pass); + return; + } + + if (g_has_creds && !g_portal && st != WL_CONNECTED && + g_state != WIFI_PROV_CONNECTING && + now - g_last_reconnect_ms >= WIFI_PROV_RECONNECT_MS) { + g_last_reconnect_ms = now; + begin_sta_connect(g_ssid, g_pass); + } +} diff --git a/src/wifi_ui.cpp b/src/wifi_ui.cpp new file mode 100644 index 0000000..f71721a --- /dev/null +++ b/src/wifi_ui.cpp @@ -0,0 +1,647 @@ +#include "wifi_ui.h" +#include "pins.h" +#include "wifi_prov.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_LINE = 0x1C2A38; +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_ACC = 0x1A4A6E; +static constexpr uint32_t COL_BTN_WARN = 0x6B3A1E; + +/* 3.5" LV_MEM=32K: dual screens + 16 heavy rows OOMs. Cap list hard. */ +static constexpr int WIFI_UI_LIST_MAX = 8; + +static lv_obj_t *scr_status = nullptr; +static lv_obj_t *scr_setup = nullptr; +static lv_obj_t *lbl_title = nullptr; +static lv_obj_t *lbl_ssid = nullptr; +static lv_obj_t *lbl_ip = nullptr; +static lv_obj_t *lbl_rssi = nullptr; +static lv_obj_t *lbl_hint = nullptr; + +static lv_obj_t *lbl_setup_status = nullptr; +static lv_obj_t *list_wifi = nullptr; +static lv_obj_t *overlay = nullptr; +static lv_obj_t *dlg = nullptr; +static lv_obj_t *lbl_dlg_ssid = nullptr; +static lv_obj_t *ta_pass = nullptr; +static lv_obj_t *btn_show_pass = nullptr; +static lv_obj_t *lbl_show_pass = nullptr; +static lv_obj_t *kb = nullptr; + +static WifiScanItem g_items[WIFI_PROV_MAX_SCAN]; +static int g_item_n = 0; +static char g_pick_ssid[WIFI_PROV_SSID_MAX + 1] = {}; +static bool g_on_setup = false; +static bool g_pass_visible = false; +static bool g_scan_pending = false; +static bool g_auto_scan_armed = false; +static uint32_t g_auto_scan_at = 0; +static WifiProvState g_last_shown = WIFI_PROV_IDLE; +static uint32_t g_last_status_ms = 0; +static WifiUiConnectedCb g_connected_cb = nullptr; + +static void on_scan(lv_event_t *e); +static void on_portal(lv_event_t *e); +static void on_join(lv_event_t *e); +static void on_dlg_back(lv_event_t *e); +static void on_toggle_pass(lv_event_t *e); +static void on_kb(lv_event_t *e); +static void on_change_wifi(lv_event_t *e); +static void on_forget(lv_event_t *e); +static void on_row_join(lv_event_t *e); +static void build_status(); +static void build_setup(); +static void ensure_pass_dialog(); + +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); + lv_obj_add_flag(btn, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_ext_click_area(btn, 12); +} + +static lv_obj_t *make_btn(lv_obj_t *parent, const char *text, uint32_t bg, + lv_event_cb_t cb, int w, int h) { + lv_obj_t *btn = lv_button_create(parent); + if (!btn) { + return nullptr; + } + lv_obj_set_size(btn, w, h); + style_btn(btn, bg); + lv_obj_t *lab = lv_label_create(btn); + if (lab) { + lv_label_set_text(lab, text); + lv_obj_set_style_text_color(lab, lv_color_hex(COL_INK), 0); + lv_obj_set_style_text_font(lab, &lv_font_montserrat_14, 0); + lv_obj_center(lab); + lv_obj_clear_flag(lab, LV_OBJ_FLAG_CLICKABLE); + } + if (cb) { + lv_obj_add_event_cb(btn, cb, LV_EVENT_CLICKED, nullptr); + } + return btn; +} + +static void hide_password_dialog() { + if (overlay) { + lv_obj_add_flag(overlay, LV_OBJ_FLAG_HIDDEN); + } + if (kb) { + lv_obj_add_flag(kb, LV_OBJ_FLAG_HIDDEN); + } +} + +static void set_pass_visible(bool visible) { + g_pass_visible = visible; + if (ta_pass) { + lv_textarea_set_password_mode(ta_pass, !visible); + } + if (lbl_show_pass) { + lv_label_set_text(lbl_show_pass, visible ? "Hide" : "Show"); + } +} + +static void ensure_keyboard() { + if (kb || !scr_setup || !ta_pass) { + return; + } + kb = lv_keyboard_create(scr_setup); + if (!kb) { + Serial.println("[ui] OOM: keyboard"); + return; + } + lv_obj_set_size(kb, UI_HOR_RES, UH(110)); + lv_obj_align(kb, LV_ALIGN_BOTTOM_MID, UX(0), UY(0)); + lv_keyboard_set_mode(kb, LV_KEYBOARD_MODE_TEXT_LOWER); + lv_keyboard_set_textarea(kb, ta_pass); + lv_obj_add_event_cb(kb, on_kb, LV_EVENT_ALL, nullptr); + lv_obj_add_flag(kb, LV_OBJ_FLAG_HIDDEN); +} + +static void ensure_pass_dialog() { + if (overlay || !scr_setup) { + return; + } + + overlay = lv_obj_create(scr_setup); + if (!overlay) { + Serial.println("[ui] OOM: pass overlay"); + return; + } + lv_obj_set_size(overlay, UI_HOR_RES, UI_VER_RES); + lv_obj_set_style_bg_color(overlay, lv_color_hex(0x000000), 0); + lv_obj_set_style_bg_opa(overlay, LV_OPA_70, 0); + lv_obj_set_style_border_width(overlay, 0, 0); + lv_obj_set_style_pad_all(overlay, 0, 0); + lv_obj_clear_flag(overlay, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(overlay, LV_OBJ_FLAG_HIDDEN); + + dlg = lv_obj_create(overlay); + if (!dlg) { + return; + } + lv_obj_set_size(dlg, UI_HOR_RES - UW(20), UH(100)); + lv_obj_align(dlg, LV_ALIGN_TOP_MID, UX(0), UY(8)); + lv_obj_set_style_bg_color(dlg, lv_color_hex(0x15202C), 0); + lv_obj_set_style_border_width(dlg, 0, 0); + lv_obj_set_style_radius(dlg, 10, 0); + lv_obj_set_style_pad_all(dlg, 8, 0); + lv_obj_clear_flag(dlg, LV_OBJ_FLAG_SCROLLABLE); + + lbl_dlg_ssid = lv_label_create(dlg); + if (lbl_dlg_ssid) { + lv_label_set_text(lbl_dlg_ssid, ""); + lv_obj_set_style_text_color(lbl_dlg_ssid, lv_color_hex(COL_ACCENT), 0); + lv_obj_set_style_text_font(lbl_dlg_ssid, &lv_font_montserrat_14, 0); + lv_obj_align(lbl_dlg_ssid, LV_ALIGN_TOP_LEFT, UX(0), UY(0)); + } + + lv_obj_t *pass_row = lv_obj_create(dlg); + if (pass_row) { + lv_obj_set_size(pass_row, UI_HOR_RES - UW(40), UH(32)); + lv_obj_align(pass_row, LV_ALIGN_TOP_LEFT, UX(0), UY(22)); + lv_obj_set_style_bg_opa(pass_row, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(pass_row, 0, 0); + lv_obj_set_style_pad_all(pass_row, 0, 0); + lv_obj_set_flex_flow(pass_row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(pass_row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, + LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_column(pass_row, UW(6), 0); + lv_obj_clear_flag(pass_row, LV_OBJ_FLAG_SCROLLABLE); + + ta_pass = lv_textarea_create(pass_row); + if (ta_pass) { + lv_obj_set_size(ta_pass, UI_HOR_RES - UW(40) - UW(62), UH(32)); + lv_textarea_set_one_line(ta_pass, true); + lv_textarea_set_password_mode(ta_pass, true); + lv_textarea_set_placeholder_text(ta_pass, "Password"); + lv_obj_set_style_text_font(ta_pass, &lv_font_montserrat_14, 0); + } + + btn_show_pass = lv_button_create(pass_row); + if (btn_show_pass) { + lv_obj_set_size(btn_show_pass, UW(56), UH(28)); + style_btn(btn_show_pass, COL_BTN); + lbl_show_pass = lv_label_create(btn_show_pass); + if (lbl_show_pass) { + lv_label_set_text(lbl_show_pass, "Show"); + lv_obj_set_style_text_color(lbl_show_pass, lv_color_hex(COL_INK), 0); + lv_obj_set_style_text_font(lbl_show_pass, &lv_font_montserrat_14, 0); + lv_obj_center(lbl_show_pass); + lv_obj_clear_flag(lbl_show_pass, LV_OBJ_FLAG_CLICKABLE); + } + lv_obj_add_event_cb(btn_show_pass, on_toggle_pass, LV_EVENT_CLICKED, + nullptr); + } + } + + lv_obj_t *row = lv_obj_create(dlg); + if (row) { + lv_obj_set_size(row, UI_HOR_RES - UW(40), UH(28)); + lv_obj_align(row, LV_ALIGN_BOTTOM_MID, UX(0), UY(0)); + lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(row, 0, 0); + lv_obj_set_style_pad_all(row, 0, 0); + lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(row, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, + LV_FLEX_ALIGN_CENTER); + lv_obj_clear_flag(row, LV_OBJ_FLAG_SCROLLABLE); + make_btn(row, "Back", COL_BTN, on_dlg_back, UW(70), UH(26)); + make_btn(row, "Join", COL_BTN_ACC, on_join, UW(70), UH(26)); + } +} + +static void show_password_dialog(const char *ssid) { + ensure_pass_dialog(); + strncpy(g_pick_ssid, ssid, WIFI_PROV_SSID_MAX); + g_pick_ssid[WIFI_PROV_SSID_MAX] = '\0'; + if (lbl_dlg_ssid) { + lv_label_set_text(lbl_dlg_ssid, g_pick_ssid); + } + if (ta_pass) { + lv_textarea_set_text(ta_pass, ""); + } + set_pass_visible(false); + ensure_keyboard(); + if (overlay) { + lv_obj_clear_flag(overlay, LV_OBJ_FLAG_HIDDEN); + lv_obj_move_foreground(overlay); + } + if (kb) { + lv_obj_clear_flag(kb, LV_OBJ_FLAG_HIDDEN); + lv_obj_move_foreground(kb); + } +} + +static void set_setup_status(const char *msg) { + if (lbl_setup_status) { + lv_label_set_text(lbl_setup_status, msg); + } +} + +static void on_row_join(lv_event_t *e) { + const int idx = (int)(intptr_t)lv_event_get_user_data(e); + if (idx < 0 || idx >= g_item_n) { + return; + } + if (g_items[idx].auth == WIFI_AUTH_OPEN) { + set_setup_status("Connecting..."); + wifi_prov_connect_to(g_items[idx].ssid, ""); + return; + } + show_password_dialog(g_items[idx].ssid); +} + +static void sort_items_by_rssi() { + for (int i = 0; i + 1 < g_item_n; ++i) { + for (int j = i + 1; j < g_item_n; ++j) { + if (g_items[j].rssi > g_items[i].rssi) { + WifiScanItem tmp = g_items[i]; + g_items[i] = g_items[j]; + g_items[j] = tmp; + } + } + } +} + +static void rebuild_list() { + if (!list_wifi) { + return; + } + lv_obj_clean(list_wifi); + + if (g_item_n <= 0) { + lv_obj_t *lab = lv_label_create(list_wifi); + if (lab) { + lv_label_set_text(lab, "No networks. Tap Scan."); + lv_obj_set_style_text_color(lab, lv_color_hex(COL_MUTED), 0); + } + return; + } + + sort_items_by_rssi(); + const int n = + g_item_n > WIFI_UI_LIST_MAX ? WIFI_UI_LIST_MAX : g_item_n; + + /* One button per SSID — avoid nested row/label/button (OOM on 3.5). */ + for (int i = 0; i < n; ++i) { + char line[56]; + snprintf(line, sizeof(line), "%s %ddBm", g_items[i].ssid, + (int)g_items[i].rssi); + + lv_obj_t *btn = lv_button_create(list_wifi); + if (!btn) { + Serial.printf("[ui] OOM at wifi row %d/%d\n", i, n); + break; + } + lv_obj_set_width(btn, lv_pct(100)); + lv_obj_set_height(btn, UH(40)); + lv_obj_set_style_bg_color(btn, lv_color_hex(0x1A4A6E), 0); + lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0); + lv_obj_set_style_radius(btn, 6, 0); + lv_obj_set_style_border_width(btn, 0, 0); + lv_obj_set_style_shadow_width(btn, 0, 0); + lv_obj_set_style_margin_bottom(btn, 4, 0); + lv_obj_set_ext_click_area(btn, 8); + + lv_obj_t *lab = lv_label_create(btn); + if (!lab) { + break; + } + lv_label_set_text(lab, line); + lv_label_set_long_mode(lab, LV_LABEL_LONG_DOT); + lv_obj_set_width(lab, lv_pct(92)); + lv_obj_set_style_text_color(lab, lv_color_hex(COL_INK), 0); + lv_obj_set_style_text_font(lab, &lv_font_montserrat_14, 0); + lv_obj_center(lab); + lv_obj_clear_flag(lab, LV_OBJ_FLAG_CLICKABLE); + + lv_obj_add_event_cb(btn, on_row_join, LV_EVENT_CLICKED, + (void *)(intptr_t)i); + } + + if (g_item_n > WIFI_UI_LIST_MAX) { + Serial.printf("[ui] showing top %d of %d by RSSI\n", n, g_item_n); + } +} + +static void do_scan() { + if (g_scan_pending || wifi_prov_scan_busy()) { + set_setup_status("Scanning..."); + return; + } + set_setup_status("Scanning..."); + if (!wifi_prov_scan_start()) { + set_setup_status("Scan failed"); + return; + } + g_scan_pending = true; +} + +static void on_scan(lv_event_t *e) { + LV_UNUSED(e); + Serial.println("[ui] Scan clicked"); + do_scan(); +} + +static void on_portal(lv_event_t *e) { + LV_UNUSED(e); + Serial.println("[ui] Phone AP clicked"); + wifi_prov_portal_start(); + set_setup_status("Phone: join CYD-Setup"); +} + +static void on_join(lv_event_t *e) { + LV_UNUSED(e); + char pass_buf[WIFI_PROV_PASS_MAX + 1] = {}; + if (ta_pass) { + const char *p = lv_textarea_get_text(ta_pass); + if (p) { + strncpy(pass_buf, p, WIFI_PROV_PASS_MAX); + pass_buf[WIFI_PROV_PASS_MAX] = '\0'; + } + } + Serial.printf("[ui] Join \"%s\" pass_len=%u\n", g_pick_ssid, + (unsigned)strlen(pass_buf)); + hide_password_dialog(); + set_setup_status("Connecting..."); + wifi_prov_connect_to(g_pick_ssid, pass_buf); +} + +static void on_dlg_back(lv_event_t *e) { + LV_UNUSED(e); + hide_password_dialog(); +} + +static void on_toggle_pass(lv_event_t *e) { + LV_UNUSED(e); + set_pass_visible(!g_pass_visible); +} + +static void on_kb(lv_event_t *e) { + const lv_event_code_t code = lv_event_get_code(e); + if (code == LV_EVENT_READY) { + on_join(e); + } else if (code == LV_EVENT_CANCEL) { + hide_password_dialog(); + } +} + +static void on_change_wifi(lv_event_t *e) { + LV_UNUSED(e); + wifi_ui_show_setup(); + do_scan(); +} + +static void on_forget(lv_event_t *e) { + LV_UNUSED(e); + wifi_prov_clear(); + wifi_ui_show_setup(); + set_setup_status("Cleared. Scan to setup."); +} + +static void refresh_status_labels() { + if (!lbl_ssid) { + return; + } + const WifiProvState st = wifi_prov_state(); + if (st == WIFI_PROV_CONNECTED && wifi_prov_is_connected()) { + lv_label_set_text(lbl_title, "WiFi Connected"); + lv_obj_set_style_text_color(lbl_title, lv_color_hex(COL_OK), 0); + lv_label_set_text(lbl_ssid, wifi_prov_ssid().c_str()); + char ip[40]; + snprintf(ip, sizeof(ip), "IP %s", wifi_prov_ip().c_str()); + lv_label_set_text(lbl_ip, ip); + char rs[24]; + snprintf(rs, sizeof(rs), "RSSI %d dBm", (int)wifi_prov_rssi()); + lv_label_set_text(lbl_rssi, rs); + lv_label_set_text(lbl_hint, "Ready for other projects"); + } else if (st == WIFI_PROV_CONNECTING) { + lv_label_set_text(lbl_title, "Connecting..."); + lv_obj_set_style_text_color(lbl_title, lv_color_hex(COL_ACCENT), 0); + lv_label_set_text(lbl_ssid, wifi_prov_ssid().c_str()); + lv_label_set_text(lbl_ip, "IP —"); + lv_label_set_text(lbl_rssi, ""); + lv_label_set_text(lbl_hint, "Please wait"); + } else if (st == WIFI_PROV_FAILED) { + lv_label_set_text(lbl_title, "Connect failed"); + lv_obj_set_style_text_color(lbl_title, lv_color_hex(COL_BAD), 0); + lv_label_set_text(lbl_ssid, wifi_prov_ssid().c_str()); + lv_label_set_text(lbl_ip, "IP —"); + lv_label_set_text(lbl_rssi, ""); + lv_label_set_text(lbl_hint, "Tap Change WiFi"); + } else { + lv_label_set_text(lbl_title, "Not configured"); + lv_obj_set_style_text_color(lbl_title, lv_color_hex(COL_MUTED), 0); + lv_label_set_text(lbl_ssid, "—"); + lv_label_set_text(lbl_ip, "IP —"); + lv_label_set_text(lbl_rssi, ""); + lv_label_set_text(lbl_hint, "Open setup"); + } +} + +static void build_status() { + if (scr_status) { + return; + } + scr_status = lv_obj_create(NULL); + lv_obj_set_style_bg_color(scr_status, lv_color_hex(COL_BG), 0); + lv_obj_set_style_bg_opa(scr_status, LV_OPA_COVER, 0); + lv_obj_clear_flag(scr_status, LV_OBJ_FLAG_SCROLLABLE); + + lbl_title = lv_label_create(scr_status); + lv_obj_set_style_text_font(lbl_title, &lv_font_montserrat_20, 0); + lv_obj_set_style_text_color(lbl_title, lv_color_hex(COL_OK), 0); + lv_obj_align(lbl_title, LV_ALIGN_TOP_MID, UX(0), UY(16)); + + lbl_ssid = lv_label_create(scr_status); + lv_obj_set_style_text_font(lbl_ssid, &lv_font_montserrat_18, 0); + lv_obj_set_style_text_color(lbl_ssid, lv_color_hex(COL_INK), 0); + lv_obj_align(lbl_ssid, LV_ALIGN_TOP_MID, UX(0), UY(52)); + + lbl_ip = lv_label_create(scr_status); + lv_obj_set_style_text_font(lbl_ip, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(lbl_ip, lv_color_hex(COL_ACCENT), 0); + lv_obj_align(lbl_ip, LV_ALIGN_TOP_MID, UX(0), UY(86)); + + lbl_rssi = lv_label_create(scr_status); + lv_obj_set_style_text_font(lbl_rssi, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(lbl_rssi, lv_color_hex(COL_MUTED), 0); + lv_obj_align(lbl_rssi, LV_ALIGN_TOP_MID, UX(0), UY(110)); + + lbl_hint = lv_label_create(scr_status); + 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, UX(0), UY(140)); + + lv_obj_t *row = lv_obj_create(scr_status); + lv_obj_set_size(row, UI_HOR_RES - UW(24), UH(44)); + lv_obj_align(row, LV_ALIGN_BOTTOM_MID, UX(0), UY(-12)); + lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(row, 0, 0); + lv_obj_set_style_pad_all(row, 0, 0); + lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(row, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, + LV_FLEX_ALIGN_CENTER); + lv_obj_clear_flag(row, LV_OBJ_FLAG_SCROLLABLE); + + make_btn(row, "Change WiFi", COL_BTN_ACC, on_change_wifi, UW(140), UH(40)); + make_btn(row, "Forget", COL_BTN_WARN, on_forget, UW(100), UH(40)); +} + +static void build_setup() { + if (scr_setup) { + return; + } + scr_setup = lv_obj_create(NULL); + lv_obj_set_style_bg_color(scr_setup, lv_color_hex(COL_BG), 0); + lv_obj_set_style_bg_opa(scr_setup, LV_OPA_COVER, 0); + lv_obj_clear_flag(scr_setup, LV_OBJ_FLAG_SCROLLABLE); + + lv_obj_t *title = lv_label_create(scr_setup); + lv_label_set_text(title, "WiFi Setup"); + lv_obj_set_style_text_font(title, &lv_font_montserrat_18, 0); + lv_obj_set_style_text_color(title, lv_color_hex(COL_INK), 0); + lv_obj_align(title, LV_ALIGN_TOP_LEFT, UX(10), UY(8)); + + lbl_setup_status = lv_label_create(scr_setup); + lv_label_set_text(lbl_setup_status, "Tap Scan"); + lv_obj_set_style_text_font(lbl_setup_status, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(lbl_setup_status, lv_color_hex(COL_MUTED), 0); + lv_obj_align(lbl_setup_status, LV_ALIGN_TOP_RIGHT, UX(-10), UY(10)); + + lv_obj_t *bar = lv_obj_create(scr_setup); + lv_obj_set_size(bar, UI_HOR_RES - UW(16), UH(48)); + lv_obj_align(bar, LV_ALIGN_TOP_MID, UX(0), UY(34)); + lv_obj_set_style_bg_opa(bar, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(bar, 0, 0); + lv_obj_set_style_pad_all(bar, 0, 0); + lv_obj_set_flex_flow(bar, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(bar, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, + LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_column(bar, UW(8), 0); + lv_obj_clear_flag(bar, LV_OBJ_FLAG_SCROLLABLE); + + make_btn(bar, "Scan", COL_BTN_ACC, on_scan, UW(90), UH(42)); + make_btn(bar, "Phone AP", COL_BTN, on_portal, UW(110), UH(42)); + make_btn(bar, "Status", COL_BTN, [](lv_event_t *e) { + LV_UNUSED(e); + wifi_ui_show_status(); + }, UW(90), UH(42)); + + list_wifi = lv_obj_create(scr_setup); + lv_obj_set_size(list_wifi, UI_HOR_RES - UW(16), UI_VER_RES - UH(92)); + lv_obj_align(list_wifi, LV_ALIGN_TOP_MID, UX(0), UY(86)); + lv_obj_set_style_bg_color(list_wifi, lv_color_hex(COL_LINE), 0); + lv_obj_set_style_border_width(list_wifi, 0, 0); + lv_obj_set_style_radius(list_wifi, 8, 0); + lv_obj_set_style_pad_all(list_wifi, 4, 0); + lv_obj_set_flex_flow(list_wifi, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(list_wifi, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, + LV_FLEX_ALIGN_START); + lv_obj_set_scroll_dir(list_wifi, LV_DIR_VER); + lv_obj_add_flag(list_wifi, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(list_wifi, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_flag(list_wifi, LV_OBJ_FLAG_SCROLL_ELASTIC); + lv_obj_add_flag(list_wifi, LV_OBJ_FLAG_SCROLL_MOMENTUM); + /* Password dialog + keyboard created lazily — keep LV_MEM free for scan list. */ +} + +void wifi_ui_set_connected_cb(WifiUiConnectedCb cb) { g_connected_cb = cb; } + +void wifi_ui_create() { + if (wifi_prov_has_credentials()) { + build_status(); + /* Stay on status until CONNECTED; callback switches to radio. */ + wifi_ui_show_status(); + } else { + build_setup(); + wifi_ui_show_setup(); + set_setup_status("Scanning..."); + /* Defer auto-scan so first frame paints; async so UI stays alive. */ + g_auto_scan_armed = true; + g_auto_scan_at = millis() + 800; + } +} + +void wifi_ui_show_setup() { + if (!scr_setup) { + build_setup(); + } + g_on_setup = true; + hide_password_dialog(); + lv_screen_load(scr_setup); +} + +void wifi_ui_show_status() { + if (!scr_status) { + build_status(); + } + g_on_setup = false; + hide_password_dialog(); + refresh_status_labels(); + lv_screen_load(scr_status); +} + +bool wifi_ui_is_setup() { return g_on_setup; } + +void wifi_ui_tick() { + if (g_auto_scan_armed && (int32_t)(millis() - g_auto_scan_at) >= 0) { + g_auto_scan_armed = false; + do_scan(); + } + + if (g_scan_pending) { + const int n = wifi_prov_scan_poll(g_items, WIFI_PROV_MAX_SCAN); + if (n >= 0) { + g_scan_pending = false; + g_item_n = n; + rebuild_list(); + char buf[48]; + const int shown = + g_item_n > WIFI_UI_LIST_MAX ? WIFI_UI_LIST_MAX : g_item_n; + snprintf(buf, sizeof(buf), "Found %d (top %d)", g_item_n, shown); + set_setup_status(buf); + } + } + + const WifiProvState st = wifi_prov_state(); + + if (st != g_last_shown) { + g_last_shown = st; + if (st == WIFI_PROV_CONNECTED) { + if (g_connected_cb) { + g_connected_cb(); + } else { + wifi_ui_show_status(); + } + } else if (st == WIFI_PROV_FAILED && g_on_setup) { + set_setup_status("Failed — retry"); + } else if (st == WIFI_PROV_CONNECTING && g_on_setup) { + set_setup_status("Connecting..."); + } else if (st == WIFI_PROV_PORTAL && g_on_setup) { + set_setup_status("Phone: CYD-Setup"); + } + } + + if (!g_on_setup && !g_connected_cb && + (millis() - g_last_status_ms >= 500)) { + g_last_status_ms = millis(); + refresh_status_labels(); + } +}