Compare commits

8 Commits

Author SHA1 Message Date
3711e37f28 Clarify 2.8 vs 3.5 pin/touch differences in README.
Point to CYD_PROJECT_GUIDE section A for side-by-side board reference.
2026-10-25 10:40:25 +08:00
71fde27f05 Fix 3.5" backlight to GPIO27 and document CYD board pitfalls.
Align cyd_35r with 3248S035C pinout (BL!=INT) and point to CYD_PROJECT_GUIDE.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-25 10:38:23 +08:00
0a58ed8a79 Add optional cyd_35r (ESP32-3248S035) with 480x320 UI; default remains 2.8. 2026-10-23 16:24:19 +08:00
51938cd76a Fix preview.html to native 320x240 without internal scale.
Card host now scales the iframe; remove broken transform that clipped UI to the corner.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-19 22:14:21 +08:00
69d37005b7 Add HTML on-device UI preview for flasher catalog.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-19 22:10:15 +08:00
652f4a0499 Add meta.json for Pomodoro firmware catalog metadata.
Enables rich summary, tags, and future media fields in CYD Flasher.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-19 19:45:25 +08:00
5afe3971fe Export Pomodoro firmware archive and document web flasher.
Refresh firmware/cyd from the Pomodoro build and point README at cydweb for browser flashing.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-19 19:38:54 +08:00
54a70d09e2 Add Pomodoro UI with 1/3/5/10 presets and custom keypad.
Replace Beep/WAV list screen with a timer; pick duration via presets or Set numpad (1-99 min), and keep selectable alarm sounds.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-18 18:58:21 +08:00
16 changed files with 1008 additions and 278 deletions

View File

@@ -1,5 +1,21 @@
# Changelog
## 2026-10-25 — 3.5" backlight pin correction
- `cyd_35r`: `TFT_BL` / `TFT_BL_PIN` **GPIO27** (was wrongly 21; 21 is GT911 INT).
- `../CYD_PROJECT_GUIDE.md` §A:2.8 vs 3.5 对照(触控 2046/911、背光、外设脚 22/27 vs 22)。
## 2026-10-18 — Pomodoro presets + custom minutes
- Duration presets: 1 / 3 / 5 / 10 minutes; `Set` opens numeric keypad (1–99).
- Keep `+/-`, Start/Pause/Reset, sound picker, speaker + volume.
## 2026-10-18 — Pomodoro timer UI
- Tag baseline `v1.0-beep-wav` (Beep + WAV list UI).
- New Pomodoro screen with Start/Pause/Reset and selectable alarm sound.
- Keep speaker toggle + volume slider; TF status in header.
## 2026-10-18 — WAV via DAC2 only (touch-safe)
- Drop I2S INTERNAL_DAC (it enables GPIO25 and kills touch).

View File

@@ -1,17 +1,43 @@
# CYD TF + Speaker
# CYD Pomodoro
ESP32-2432S028R:LVGL 横屏显示 TF 卡信息,触控开关喇叭并播放音乐。
默认板:**ESP32-2432S028R**(2.8"):LVGL 横屏番茄钟,可选内置提示音,触控喇叭播放。
可选 **3.5" C**:`pio run -e cyd_35r`。
| | 2.8" `cyd` | 3.5C `cyd_35r` |
|--|------------|----------------|
| 屏 | ILI9341 · UI 320×240 | ST7796 · UI 480×320 |
| 触控 | **XPT2046** SoftSPI | **GT911** I2C |
| 背光 | GPIO**21** | GPIO**27** |
| 外设首选 | CN1 **IO22 + IO27** | 优先 **IO22**(21=INT,27=BL) |
完整对照与避坑:[`../CYD_PROJECT_GUIDE.md`](../CYD_PROJECT_GUIDE.md) **§A**。
## 功能
- SoftSPI 触摸(已标定,勿改 `src/touch.cpp` 映射)
- TF 卡信息(VSPI)
- 喇叭 GPIO26,默认音量 **20%**;按钮:Speaker ON/OFF、Play Music
- SoftSPI 触摸(2.8,已标定,勿改 `src/touch.cpp` 映射);3.5C 为 GT911 I2C
- 番茄钟:预设 1 / 3 / 5 / 10 分钟;`Set` 自定义输入;`+/-` 微调;Start / Pause / Reset
- 结束音效:内置 Beep 列表自选(默认 Alarm),点名称可试听
- 喇叭 GPIO26 + 音量滑条;TF 卡状态显示在右上角
- 基线标签:`v1.0-beep-wav`(改番茄钟前的 Beep + WAV 界面)
## 构建 / 烧录
```bash
# 2.8"
pio run -e cyd -t upload --upload-port COM3
# 3.5" 电容板
pio run -e cyd_35r -t upload --upload-port COM3
```
或 `firmware/cyd/flash.bat`。
或 `firmware/cyd/flash.bat`(2.8 预编译包)。
浏览器刷写(推荐):在 [`../cydweb`](../cydweb) 运行 `npm run dev`,打开 http://localhost:3000 ,选择 **CYD Pomodoro**。
更新预编译包:
```powershell
.\firmware\export_from_build.ps1
# 仅复制已有构建:
.\firmware\export_from_build.ps1 -SkipBuild
```

View File

@@ -3,9 +3,11 @@
当前归档目录:[`cyd/`](cyd/)
对应板子:**CYD ESP32-2432S028R**(ILI9341)
功能:TF 卡信息 + 触屏扬声器控制
功能:**番茄钟(Pomodoro)** + 触控喇叭 + TF 状态
版本信息见 [`cyd/VERSION.txt`](cyd/VERSION.txt)。
也可在 [`../../cydweb`](../../cydweb)(CYD Flasher)里通过浏览器串口刷写本目录固件。
## 直接刷写(无需重新编译)
1. USB 连接板子

View File

@@ -1,8 +1,8 @@
name: CYD TF card + touch speaker (cyd)
git: 7c6eb9a
date: 2026-10-17
name: CYD Pomodoro (cyd)
git: 54a70d0
date: 2026-10-19
board: ESP32-2432S028R (ILI9341)
features: TF/SD info, SoftSPI touch, GPIO26 speaker
features: Pomodoro timer, SoftSPI touch, GPIO26 speaker, TF status
flash_size: 4MB
offsets:
0x1000 bootloader.bin
@@ -12,5 +12,5 @@ offsets:
sha256:
bootloader.bin 3D234A7471F67B013686DABD4DEE7C1FA915C9928463616A94BC9297ACF1ABF8
boot_app0.bin F94C5D786A7A8FAB06AC5D10E33BF37711A6697636DC037559EA19CC410A17F0
firmware.bin C2BD52279AFB3E40C45358CA05493EF0255F708858A3F5132417AEAC0B6EE25F
firmware.bin 4C064140E8F8E0D2DD55D0085F7974FA57CAF5A24DFFACC9B3ECF03B576A033B
partitions.bin 148B959CBFF1C38AA8E1D5C0BA9D612C54997B945E56A63F41223EEF650653A1

Binary file not shown.

View File

@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>POMO</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
html,body{width:320px;height:240px;overflow:hidden;background:#000;margin:0}
.bezel{width:320px;height:240px;background:#1a1408}
.screen{width:320px;height:240px;background:#0A1016;color:#EAF0F6;font:12px/1.2 system-ui,sans-serif;
position:relative;overflow:hidden;}
.title{position:absolute;left:12px;top:4px;color:#E85D4C;font-size:20px;font-weight:700}
.chip{position:absolute;right:12px;top:8px;color:#7A8B9C}
.pm{position:absolute;left:12px;top:32px;width:40px;height:44px;border:0;border-radius:8px;background:#243140;color:#EAF0F6;font-size:18px}
.pp{left:auto;right:12px}
.timer{position:absolute;left:0;right:0;top:34px;text-align:center;font-size:28px;font-weight:700;color:#4FC3F7}
.status{position:absolute;left:0;right:0;top:74px;text-align:center;color:#7A8B9C}
.presets{position:absolute;left:12px;right:12px;top:94px;display:flex;gap:6px}
.presets button{flex:1;height:30px;border:0;border-radius:6px;background:#15202C;color:#EAF0F6;font-weight:600}
.presets button.on{background:#1E3A4A}
.snd{position:absolute;left:12px;top:134px;color:#7A8B9C}
.sndrow{position:absolute;left:68px;right:12px;top:128px;display:flex;gap:4px;align-items:center}
.sndrow button{height:28px;border:0;border-radius:6px;background:#15202C;color:#EAF0F6;min-width:32px}
.sndrow .name{flex:1;height:28px;border-radius:6px;background:#15202C;display:grid;place-items:center}
.vol{position:absolute;left:12px;top:164px;color:#7A8B9C}
.slider{position:absolute;left:52px;right:56px;top:168px;height:14px;border-radius:4px;background:#1C2A38;overflow:hidden}
.slider>i{display:block;height:100%;width:40%;background:#3DDC97}
.volpct{position:absolute;right:12px;top:164px}
.audio{position:absolute;left:12px;top:188px;color:#7A8B9C;font-size:11px}
.acts{position:absolute;left:12px;right:12px;bottom:10px;display:flex;gap:8px}
.acts button{flex:1;height:32px;border:0;border-radius:8px;font-weight:700;color:#EAF0F6}
.start{background:#1A4A6E}.reset{background:#243140}
</style>
</head>
<body>
<div class="bezel"><div class="screen" id="s">
<div class="title">POMO</div><div class="chip">TF OK</div>
<button class="pm" type="button">-</button>
<div class="timer">05:00</div>
<button class="pm pp" type="button">+</button>
<div class="status">5m - READY</div>
<div class="presets">
<button type="button">1</button><button type="button">3</button>
<button class="on" type="button">5</button><button type="button">10</button>
<button type="button">Set</button>
</div>
<div class="snd">SOUND</div>
<div class="sndrow"><button type="button">&lt;</button><div class="name">Alarm</div><button type="button">&gt;</button></div>
<div class="vol">VOL</div><div class="slider"><i></i></div><div class="volpct">40%</div>
<div class="audio">Speaker OFF</div>
<div class="acts"><button class="start" type="button">START</button><button class="reset" type="button">RESET</button></div>
</div></div>
</body>
</html>

10
firmware/cyd/meta.json Normal file
View File

@@ -0,0 +1,10 @@
{
"boardId": "esp32-2432s028",
"name": "CYD Pomodoro",
"summary": "Landscape Pomodoro timer with touch, speaker, and TF status",
"description": "Presets 1/3/5/10 minutes, custom duration, alarm sound picker, volume slider on GPIO26.",
"tags": ["pomodoro", "touch", "speaker", "timer"],
"media": {
"preview": "media/preview.html"
}
}

View File

@@ -1,9 +1,13 @@
# Rebuild (optional) and refresh firmware/cyd from PlatformIO build output.
# Usage:
# .\firmware\export_from_build.ps1
# .\firmware\export_from_build.ps1 -SkipBuild
# 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)
param(
[switch]$SkipBuild,
[ValidateSet("cyd", "cyd_35r")]
[string]$Env = "cyd"
)
$ErrorActionPreference = "Stop"
$root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
@@ -12,17 +16,26 @@ Set-Location $root
$env:Path = "C:\Users\dsx2016\AppData\Local\Programs\Python\Python312\Scripts;" + $env:Path
if (-not $SkipBuild) {
pio run -e cyd
pio run -e $Env
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
}
$src = Join-Path $root ".pio\build\cyd"
$dst = Join-Path $root "firmware\cyd"
$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" }
if (-not (Test-Path $p)) { Write-Error "Missing build artifact: $p — build with: pio run -e $Env" }
Copy-Item $p $dst -Force
}
@@ -32,17 +45,15 @@ if (-not (Test-Path $bootApp)) {
}
Copy-Item $bootApp (Join-Path $dst "boot_app0.bin") -Force
# Keep flash scripts if already present
$git = ""
try { $git = (git -C $root rev-parse --short HEAD).Trim() } catch { $git = "unknown" }
$date = Get-Date -Format "yyyy-MM-dd"
$hashes = Get-ChildItem (Join-Path $dst "*.bin") | Get-FileHash -Algorithm SHA256
$lines = @(
"name: CYD TF card + touch speaker (cyd)",
"name: CYD tftest ($dstName)",
"git: $git",
"date: $date",
"board: ESP32-2432S028R (ILI9341)",
"features: TF/SD info, SoftSPI touch, GPIO26 speaker",
$boardLine,
"flash_size: 4MB",
"offsets:",
" 0x1000 bootloader.bin",
@@ -55,4 +66,22 @@ foreach ($h in $hashes) {
$lines += (" {0,-15} {1}" -f (Split-Path $h.Path -Leaf), $h.Hash)
}
$lines | Set-Content -Encoding utf8 (Join-Path $dst "VERSION.txt")
Write-Host "Exported -> $dst (git $git)"
# Keep / refresh meta.json boardId for catalog filtering
$metaPath = Join-Path $dst "meta.json"
if (Test-Path $metaPath) {
try {
$meta = Get-Content $metaPath -Raw | ConvertFrom-Json
$meta.boardId = $boardIdHint
if ($Env -eq "cyd_35r" -and $meta.name -notmatch "3\.5") {
$meta.name = ($meta.name -replace "\s*$","") + " (3.5\")"
}
$meta | ConvertTo-Json -Depth 8 | Set-Content -Encoding utf8 $metaPath
} catch {
Write-Host "meta.json update skipped: $_"
}
} elseif ($Env -eq "cyd_35r") {
Write-Host "Note: add firmware/cyd_35r/meta.json with boardId=$boardIdHint for the web catalog"
}
Write-Host "Exported -> $dst (env=$Env git=$git)"

View File

@@ -1,24 +1,49 @@
#pragma once
// CYD ESP32-2432S028R
// 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
// TF / microSD (VSPI;触摸用 SoftSPI,不抢总线)
#define SD_MISO 19
#define SD_MOSI 23
#define SD_SCK 18
#define SD_CS 5
#define SPEAKER_PIN 26
#define 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
#define SCREEN_WIDTH 240
#define SCREEN_HEIGHT 320
#define UI_HOR_RES 320
#define UI_VER_RES 240
/* 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
#define SD_MISO 19
#define SD_MOSI 23
#define SD_SCK 18
#define SD_CS 5
#define SPEAKER_PIN 26

25
include/pomodoro.h Normal file
View File

@@ -0,0 +1,25 @@
#pragma once
#include <stdint.h>
enum PomoState : uint8_t {
POMO_IDLE = 0,
POMO_RUNNING = 1,
POMO_PAUSED = 2,
POMO_DONE = 3,
};
void pomo_init();
void pomo_tick();
PomoState pomo_state();
uint32_t pomo_remaining_sec();
uint16_t pomo_duration_minutes(); // last configured length
void pomo_set_minutes(uint16_t minutes); // 1..99; idle/paused/done only
void pomo_start_pause();
void pomo_reset();
void pomo_adjust_minutes(int8_t delta);
const char *pomo_state_name();
bool pomo_consume_alarm();

View File

@@ -5,9 +5,6 @@
void ui_create();
void ui_update(const SdCardInfo &info);
void ui_tick(); // refresh timer + fire alarm sound
void ui_refresh_audio_buttons();
void ui_refresh_lists();
void ui_set_touch_debug(int16_t lx, int16_t ly, bool down);
// 0=Beep 1=WAV
uint8_t ui_active_tab();

View File

@@ -43,13 +43,38 @@ build_flags =
-DLV_FONT_MONTSERRAT_16=1
-DLV_FONT_MONTSERRAT_18=1
-DLV_FONT_MONTSERRAT_20=1
-DLV_FONT_MONTSERRAT_28=1
-DLV_USE_TFT_ESPI=1
-DLV_USE_FLEX=1
-DLV_USE_BUTTON=1
-DLV_USE_LABEL=1
-DLV_USE_BAR=1
-DLV_USE_SLIDER=1
-DLV_USE_TABVIEW=1
-I${PROJECT_DIR}/include
extra_scripts = pre:copy_lv_conf.py
; Optional 3.5" CYD ESP32-3248S035C (ST7796 + GT911 capacitive). Default [env:cyd].
; Build: pio run -e cyd_35r
; Keep TOUCH_CS=-1 — GPIO33 is GT911 SDA, not resistive touch CS.
; TFT_BL=GPIO27 (not 21 — 21 is GT911 INT). See ../CYD_PROJECT_GUIDE.md §1.1
[env:cyd_35r]
extends = env:cyd
build_unflags =
-DILI9341_2_DRIVER=1
-DTFT_WIDTH=240
-DTFT_HEIGHT=320
-DTFT_BL=21
-DLV_MEM_SIZE=49152
build_flags =
${env:cyd.build_flags}
-DST7796_DRIVER=1
-DTFT_WIDTH=320
-DTFT_HEIGHT=480
-DTFT_BL=27
-DCYD_BOARD_3248S035=1
-DUSE_HSPI_PORT
-DSUPPORT_TRANSACTIONS
-DTFT_RGB_ORDER=TFT_BGR
-DLV_MEM_SIZE=32768

View File

@@ -14,8 +14,9 @@ static constexpr uint32_t DRAW_BUF_SIZE =
static uint32_t draw_buf[DRAW_BUF_SIZE / 4];
static uint32_t last_refresh_ms = 0;
static uint32_t last_btn_ui_ms = 0;
static constexpr uint32_t REFRESH_MS = 4000;
static uint32_t last_ui_ms = 0;
static constexpr uint32_t REFRESH_MS = 8000;
static constexpr uint32_t UI_TICK_MS = 200;
static void refresh_sd_ui() {
SdCardInfo info = sd_read_info();
@@ -28,7 +29,6 @@ static void refresh_sd_ui() {
format_bytes(info.card_bytes).c_str());
if (!audio_is_playing()) {
wav_list_scan();
ui_refresh_lists();
}
}
@@ -40,7 +40,7 @@ void setup() {
Serial.begin(460800);
delay(200);
Serial.println();
Serial.printf("LVGL %d.%d.%d | CYD Beep + WAV\n", lv_version_major(),
Serial.printf("LVGL %d.%d.%d | CYD Pomodoro\n", lv_version_major(),
lv_version_minor(), lv_version_patch());
Serial.println("BOOT_OK");
@@ -69,7 +69,7 @@ void setup() {
refresh_sd_ui();
last_refresh_ms = millis();
last_btn_ui_ms = millis();
last_ui_ms = millis();
}
void loop() {
@@ -84,20 +84,15 @@ void loop() {
audio_tick();
const uint32_t now = millis();
if (now - last_ui_ms >= UI_TICK_MS) {
last_ui_ms = now;
ui_tick();
}
if (!playing && now - last_refresh_ms >= REFRESH_MS) {
last_refresh_ms = now;
refresh_sd_ui();
}
if (now - last_btn_ui_ms >= 200) {
last_btn_ui_ms = now;
static bool was_playing = false;
ui_refresh_audio_buttons();
if (playing != was_playing) {
was_playing = playing;
ui_refresh_lists();
}
}
delay(playing ? 2 : 5);
}

143
src/pomodoro.cpp Normal file
View File

@@ -0,0 +1,143 @@
#include "pomodoro.h"
#include <Arduino.h>
static constexpr uint16_t MIN_MINUTES = 1;
static constexpr uint16_t MAX_MINUTES = 99;
static constexpr uint16_t DEFAULT_MINUTES = 5;
static PomoState g_state = POMO_IDLE;
static uint32_t g_duration_ms = DEFAULT_MINUTES * 60UL * 1000UL;
static uint32_t g_remain_ms = DEFAULT_MINUTES * 60UL * 1000UL;
static uint32_t g_last_ms = 0;
static bool g_alarm_pending = false;
static uint32_t clamp_minutes_ms(uint16_t minutes) {
if (minutes < MIN_MINUTES) {
minutes = MIN_MINUTES;
}
if (minutes > MAX_MINUTES) {
minutes = MAX_MINUTES;
}
return (uint32_t)minutes * 60UL * 1000UL;
}
void pomo_init() {
g_state = POMO_IDLE;
g_duration_ms = clamp_minutes_ms(DEFAULT_MINUTES);
g_remain_ms = g_duration_ms;
g_last_ms = 0;
g_alarm_pending = false;
}
PomoState pomo_state() {
return g_state;
}
uint32_t pomo_remaining_sec() {
return (g_remain_ms + 999UL) / 1000UL;
}
uint16_t pomo_duration_minutes() {
return (uint16_t)((g_duration_ms + 59999UL) / 60000UL);
}
const char *pomo_state_name() {
switch (g_state) {
case POMO_RUNNING:
return "RUN";
case POMO_PAUSED:
return "PAUSE";
case POMO_DONE:
return "DONE";
case POMO_IDLE:
default:
return "READY";
}
}
void pomo_set_minutes(uint16_t minutes) {
if (g_state == POMO_RUNNING) {
return;
}
g_duration_ms = clamp_minutes_ms(minutes);
g_remain_ms = g_duration_ms;
g_state = POMO_IDLE;
g_alarm_pending = false;
}
void pomo_start_pause() {
if (g_state == POMO_DONE) {
g_remain_ms = g_duration_ms;
g_state = POMO_IDLE;
}
if (g_state == POMO_RUNNING) {
g_state = POMO_PAUSED;
return;
}
if (g_remain_ms == 0) {
g_remain_ms = g_duration_ms;
}
g_state = POMO_RUNNING;
g_last_ms = millis();
g_alarm_pending = false;
}
void pomo_reset() {
g_remain_ms = g_duration_ms;
g_state = POMO_IDLE;
g_alarm_pending = false;
}
void pomo_adjust_minutes(int8_t delta) {
if (g_state == POMO_RUNNING) {
return;
}
int32_t mins = (int32_t)pomo_duration_minutes() + (int32_t)delta;
if (mins < (int32_t)MIN_MINUTES) {
mins = MIN_MINUTES;
}
if (mins > (int32_t)MAX_MINUTES) {
mins = MAX_MINUTES;
}
// When paused mid-timer, adjust remaining by same delta minutes from current remain
if (g_state == POMO_PAUSED) {
int32_t sec = (int32_t)(g_remain_ms / 1000UL) + (int32_t)delta * 60;
if (sec < 60) {
sec = 60;
}
if (sec > (int32_t)MAX_MINUTES * 60) {
sec = (int32_t)MAX_MINUTES * 60;
}
g_remain_ms = (uint32_t)sec * 1000UL;
g_duration_ms = g_remain_ms;
g_alarm_pending = false;
return;
}
pomo_set_minutes((uint16_t)mins);
}
void pomo_tick() {
if (g_state != POMO_RUNNING) {
return;
}
const uint32_t now = millis();
const uint32_t elapsed = now - g_last_ms;
g_last_ms = now;
if (elapsed >= g_remain_ms) {
g_remain_ms = 0;
g_state = POMO_DONE;
g_alarm_pending = true;
return;
}
g_remain_ms -= elapsed;
}
bool pomo_consume_alarm() {
if (!g_alarm_pending) {
return false;
}
g_alarm_pending = false;
return true;
}

View File

@@ -3,10 +3,13 @@
#include <Arduino.h>
#if defined(CYD_BOARD_3248S035)
#include <Wire.h>
#endif
/**
* SoftSPI XPT2046
* BL↔TR 是矩形主对角线镜像(XY 对调),不是 180°(180° 会搞成 TL↔BR)。
* 先按 rot2 得到旧 UI,再做对角修正,反变换喂给 LVGL 270。
* 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;
@@ -15,6 +18,173 @@ 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
if (!gt_find_addr()) {
Serial.println("GT911 not found — 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();
}
}
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);
@@ -49,16 +219,6 @@ static uint16_t xpt_read(uint8_t cmd) {
return v;
}
static int16_t clampi(int32_t v, int32_t lo, int32_t hi) {
if (v < lo) {
return (int16_t)lo;
}
if (v > hi) {
return (int16_t)hi;
}
return (int16_t)v;
}
void touch_init() {
pinMode(CYD_TOUCH_CS, OUTPUT);
pinMode(CYD_TOUCH_CLK, OUTPUT);
@@ -94,8 +254,6 @@ void touch_read_cb(lv_indev_t * /*indev*/, lv_indev_data_t *data) {
x = clampi(x, 0, SCREEN_WIDTH - 1);
y = clampi(y, 0, SCREEN_HEIGHT - 1);
// 旧 UI(LVGL270):(y, 239-x) — 该空间里 BL↔TR,做主对角线镜像
// (ux,uy) -> (uy*(319/239), ux*(239/319))
const int32_t old_ux = y;
const int32_t old_uy = (SCREEN_WIDTH - 1) - x;
int16_t ui_x = clampi((old_uy * (UI_HOR_RES - 1)) / (UI_VER_RES - 1), 0,
@@ -103,7 +261,6 @@ void touch_read_cb(lv_indev_t * /*indev*/, lv_indev_data_t *data) {
int16_t ui_y = clampi((old_ux * (UI_VER_RES - 1)) / (UI_HOR_RES - 1), 0,
UI_VER_RES - 1);
// 反变换喂 LVGL,使最终落在 (ui_x, ui_y)
const int16_t feed_x = (int16_t)((SCREEN_WIDTH - 1) - ui_y);
const int16_t feed_y = ui_x;
@@ -123,3 +280,5 @@ void touch_read_cb(lv_indev_t * /*indev*/, lv_indev_data_t *data) {
data->point.y = feed_y;
data->state = LV_INDEV_STATE_PRESSED;
}
#endif

View File

@@ -1,10 +1,12 @@
#include "ui.h"
#include "audio_test.h"
#include "pins.h"
#include "wav_list.h"
#include "pomodoro.h"
#include <Arduino.h>
#include <lvgl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static constexpr uint32_t COL_BG = 0x0A1016;
@@ -18,21 +20,35 @@ static constexpr uint32_t COL_BTN_OFF = 0x243140;
static constexpr uint32_t COL_BTN_ON = 0x1B5E4A;
static constexpr uint32_t COL_BTN_PLAY = 0x1A4A6E;
static constexpr uint32_t COL_BTN_STOP = 0x8B2E2E;
static constexpr uint32_t COL_ROW = 0x15202C;
static constexpr uint32_t COL_ROW_SEL = 0x1E3A4A;
static constexpr uint32_t COL_MODE = 0x15202C;
static constexpr uint32_t COL_MODE_ON = 0x1E3A4A;
static constexpr uint32_t COL_TOMATO = 0xE85D4C;
static constexpr uint16_t PRESETS[] = {1, 3, 5, 10};
static constexpr int PRESET_N = 4;
static lv_obj_t *lbl_chip;
static lv_obj_t *lbl_audio;
static lv_obj_t *lbl_timer;
static lv_obj_t *lbl_status;
static lv_obj_t *lbl_sound;
static lv_obj_t *lbl_vol;
static lv_obj_t *lbl_audio;
static lv_obj_t *slider_vol;
static lv_obj_t *tabview;
static lv_obj_t *list_beep;
static lv_obj_t *list_wav;
static lv_obj_t *btn_speaker;
static lv_obj_t *btn_play;
static lv_obj_t *btn_start;
static lv_obj_t *btn_reset;
static lv_obj_t *lbl_speaker;
static lv_obj_t *lbl_play;
static uint8_t g_tab = 0;
static lv_obj_t *lbl_start;
static lv_obj_t *btn_preset[PRESET_N];
static lv_obj_t *btn_custom;
static lv_obj_t *panel_custom;
static lv_obj_t *lbl_custom_val;
static char g_timer_text[8] = "05:00";
static char g_status_text[40] = "5m - READY";
static char g_sound_text[28] = "Alarm";
static char g_custom_buf[4] = "";
static char g_custom_disp[16] = "min: _";
static void style_btn(lv_obj_t *btn, uint32_t bg) {
lv_obj_set_style_radius(btn, 10, 0);
@@ -52,18 +68,94 @@ static void sync_vol_label() {
lv_label_set_text(lbl_vol, buf);
}
static void style_row(lv_obj_t *row, bool selected, bool playing) {
uint32_t bg = selected ? COL_ROW_SEL : COL_ROW;
if (playing) {
bg = COL_BTN_PLAY;
static void refresh_preset_highlight() {
const uint16_t mins = pomo_duration_minutes();
bool matched = false;
for (int i = 0; i < PRESET_N; i++) {
if (!btn_preset[i]) {
continue;
}
const bool match = mins == PRESETS[i];
if (match) {
matched = true;
}
style_btn(btn_preset[i], match ? COL_MODE_ON : COL_MODE);
}
if (btn_custom) {
style_btn(btn_custom, matched ? COL_MODE : COL_MODE_ON);
}
}
static void refresh_timer_labels() {
const uint32_t sec = pomo_remaining_sec();
const uint32_t mm = sec / 60U;
const uint32_t ss = sec % 60U;
snprintf(g_timer_text, sizeof(g_timer_text), "%02u:%02u", (unsigned)mm,
(unsigned)ss);
if (lbl_timer) {
lv_label_set_text(lbl_timer, g_timer_text);
const uint32_t color = (pomo_state() == POMO_DONE) ? COL_TOMATO : COL_ACCENT;
lv_obj_set_style_text_color(lbl_timer, lv_color_hex(color), 0);
}
snprintf(g_status_text, sizeof(g_status_text), "%um - %s",
(unsigned)pomo_duration_minutes(), pomo_state_name());
if (lbl_status) {
lv_label_set_text(lbl_status, g_status_text);
}
if (lbl_start) {
if (pomo_state() == POMO_RUNNING) {
lv_label_set_text(lbl_start, "PAUSE");
} else if (pomo_state() == POMO_DONE) {
lv_label_set_text(lbl_start, "AGAIN");
} else {
lv_label_set_text(lbl_start, "START");
}
}
if (btn_start) {
style_btn(btn_start,
pomo_state() == POMO_RUNNING ? COL_BTN_STOP : COL_BTN_PLAY);
}
refresh_preset_highlight();
}
static void refresh_sound_label() {
const int8_t sel = sound_selected();
if (sel < 0) {
snprintf(g_sound_text, sizeof(g_sound_text), "-");
} else {
snprintf(g_sound_text, sizeof(g_sound_text), "%s", sound_name((uint8_t)sel));
}
if (lbl_sound) {
lv_label_set_text(lbl_sound, g_sound_text);
}
}
static void sync_custom_label() {
if (g_custom_buf[0] == '\0') {
snprintf(g_custom_disp, sizeof(g_custom_disp), "min: _");
} else {
snprintf(g_custom_disp, sizeof(g_custom_disp), "min: %s", g_custom_buf);
}
if (lbl_custom_val) {
lv_label_set_text(lbl_custom_val, g_custom_disp);
}
}
static void custom_hide() {
if (panel_custom) {
lv_obj_add_flag(panel_custom, LV_OBJ_FLAG_HIDDEN);
}
}
static void custom_show() {
g_custom_buf[0] = '\0';
sync_custom_label();
if (panel_custom) {
lv_obj_clear_flag(panel_custom, LV_OBJ_FLAG_HIDDEN);
lv_obj_move_foreground(panel_custom);
}
lv_obj_set_style_bg_color(row, lv_color_hex(bg), 0);
lv_obj_set_style_bg_opa(row, LV_OPA_COVER, 0);
lv_obj_set_style_radius(row, 8, 0);
lv_obj_set_style_border_width(row, 0, 0);
lv_obj_set_style_shadow_width(row, 0, 0);
lv_obj_set_style_pad_left(row, 10, 0);
lv_obj_set_style_pad_right(row, 10, 0);
}
static void on_speaker_clicked(lv_event_t *e) {
@@ -72,20 +164,130 @@ static void on_speaker_clicked(lv_event_t *e) {
ui_refresh_audio_buttons();
}
static void on_play_clicked(lv_event_t *e) {
static void on_start_clicked(lv_event_t *e) {
LV_UNUSED(e);
if (audio_is_playing()) {
audio_stop();
} else if (g_tab == 1) {
const int8_t sel = wav_list_selected();
if (sel >= 0) {
audio_play_wav_index((uint8_t)sel);
}
} else {
audio_play_selected();
}
pomo_start_pause();
refresh_timer_labels();
}
static void on_reset_clicked(lv_event_t *e) {
LV_UNUSED(e);
pomo_reset();
audio_stop();
refresh_timer_labels();
ui_refresh_audio_buttons();
}
static void on_preset_clicked(lv_event_t *e) {
const int idx = (int)(intptr_t)lv_event_get_user_data(e);
if (idx < 0 || idx >= PRESET_N) {
return;
}
pomo_set_minutes(PRESETS[idx]);
refresh_timer_labels();
}
static void on_custom_open(lv_event_t *e) {
LV_UNUSED(e);
if (pomo_state() == POMO_RUNNING) {
return;
}
custom_show();
}
static void on_custom_cancel(lv_event_t *e) {
LV_UNUSED(e);
custom_hide();
}
static void on_custom_ok(lv_event_t *e) {
LV_UNUSED(e);
if (g_custom_buf[0] == '\0') {
custom_hide();
return;
}
int mins = atoi(g_custom_buf);
if (mins < 1) {
mins = 1;
}
if (mins > 99) {
mins = 99;
}
pomo_set_minutes((uint16_t)mins);
custom_hide();
refresh_timer_labels();
}
static void on_custom_digit(lv_event_t *e) {
const int d = (int)(intptr_t)lv_event_get_user_data(e);
const size_t len = strlen(g_custom_buf);
if (d == -1) { // clear
g_custom_buf[0] = '\0';
sync_custom_label();
return;
}
if (len >= 2) {
return;
}
if (len == 0 && d == 0) {
return; // no leading zero
}
g_custom_buf[len] = (char)('0' + d);
g_custom_buf[len + 1] = '\0';
sync_custom_label();
}
static void on_minus_clicked(lv_event_t *e) {
LV_UNUSED(e);
pomo_adjust_minutes(-1);
refresh_timer_labels();
}
static void on_plus_clicked(lv_event_t *e) {
LV_UNUSED(e);
pomo_adjust_minutes(1);
refresh_timer_labels();
}
static void on_sound_prev(lv_event_t *e) {
LV_UNUSED(e);
const uint8_t n = sound_count();
if (n == 0) {
return;
}
int8_t sel = sound_selected();
if (sel <= 0) {
sel = (int8_t)(n - 1);
} else {
sel--;
}
sound_set_selected(sel);
refresh_sound_label();
}
static void on_sound_next(lv_event_t *e) {
LV_UNUSED(e);
const uint8_t n = sound_count();
if (n == 0) {
return;
}
int8_t sel = sound_selected();
sel++;
if (sel >= (int8_t)n) {
sel = 0;
}
sound_set_selected(sel);
refresh_sound_label();
}
static void on_sound_preview(lv_event_t *e) {
LV_UNUSED(e);
const int8_t sel = sound_selected();
if (sel < 0) {
return;
}
audio_play_sound((uint8_t)sel);
ui_refresh_audio_buttons();
ui_refresh_lists();
}
static void on_vol_changed(lv_event_t *e) {
@@ -94,107 +296,83 @@ static void on_vol_changed(lv_event_t *e) {
sync_vol_label();
}
static void on_beep_clicked(lv_event_t *e) {
const int idx = (int)(intptr_t)lv_event_get_user_data(e);
if (idx < 0 || idx >= (int)sound_count()) {
return;
static lv_obj_t *make_small_btn(lv_obj_t *parent, const char *txt, int x, int y,
int w, int h, lv_event_cb_t cb, void *ud) {
lv_obj_t *btn = lv_button_create(parent);
lv_obj_set_pos(btn, x, y);
lv_obj_set_size(btn, w, h);
lv_obj_add_flag(btn, LV_OBJ_FLAG_CLICKABLE);
if (cb) {
lv_obj_add_event_cb(btn, cb, LV_EVENT_CLICKED, ud);
}
audio_play_sound((uint8_t)idx);
ui_refresh_audio_buttons();
ui_refresh_lists();
style_btn(btn, COL_BTN_OFF);
lv_obj_t *lbl = lv_label_create(btn);
lv_label_set_text(lbl, txt);
lv_obj_set_style_text_font(lbl, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(lbl, lv_color_hex(COL_INK), 0);
lv_obj_center(lbl);
return btn;
}
static void on_wav_clicked(lv_event_t *e) {
const int idx = (int)(intptr_t)lv_event_get_user_data(e);
if (idx < 0 || idx >= (int)wav_list_count()) {
return;
}
audio_play_wav_index((uint8_t)idx);
ui_refresh_audio_buttons();
ui_refresh_lists();
}
static void build_custom_panel(lv_obj_t *scr) {
panel_custom = lv_obj_create(scr);
lv_obj_set_size(panel_custom, UI_HOR_RES, UI_VER_RES);
lv_obj_set_pos(panel_custom, UX(0), UY(0));
lv_obj_set_style_bg_color(panel_custom, lv_color_hex(COL_BG), 0);
lv_obj_set_style_bg_opa(panel_custom, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(panel_custom, 0, 0);
lv_obj_set_style_pad_all(panel_custom, 0, 0);
lv_obj_set_style_radius(panel_custom, 0, 0);
lv_obj_clear_flag(panel_custom, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(panel_custom, LV_OBJ_FLAG_HIDDEN);
static void on_tab_changed(lv_event_t *e) {
lv_obj_t *tv = (lv_obj_t *)lv_event_get_target(e);
g_tab = (uint8_t)lv_tabview_get_tab_active(tv);
ui_refresh_lists();
}
lv_obj_t *title = lv_label_create(panel_custom);
lv_label_set_text(title, "Custom minutes");
lv_obj_set_style_text_font(title, &lv_font_montserrat_16, 0);
lv_obj_set_style_text_color(title, lv_color_hex(COL_ACCENT), 0);
lv_obj_align(title, LV_ALIGN_TOP_MID, UX(0), UY(8));
static void fill_list_beep() {
if (!list_beep) {
return;
}
lv_obj_clean(list_beep);
const uint8_t n = sound_count();
const int8_t sel = sound_selected();
const bool playing = audio_is_playing() && g_tab == 0;
lbl_custom_val = lv_label_create(panel_custom);
lv_label_set_text(lbl_custom_val, "min: _");
lv_obj_set_style_text_font(lbl_custom_val, &lv_font_montserrat_28, 0);
lv_obj_set_style_text_color(lbl_custom_val, lv_color_hex(COL_INK), 0);
lv_obj_align(lbl_custom_val, LV_ALIGN_TOP_MID, UX(0), UY(36));
for (uint8_t i = 0; i < n; i++) {
lv_obj_t *row = lv_button_create(list_beep);
lv_obj_set_width(row, lv_pct(100));
lv_obj_set_height(row, 32);
lv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(row, on_beep_clicked, LV_EVENT_CLICKED, (void *)(intptr_t)i);
style_row(row, sel == (int8_t)i, playing && sel == (int8_t)i);
lv_obj_t *lbl = lv_label_create(row);
char line[36];
snprintf(line, sizeof(line), "%u %s", (unsigned)(i + 1), sound_name(i));
lv_label_set_text(lbl, line);
lv_obj_set_style_text_font(lbl, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(lbl, lv_color_hex(COL_INK), 0);
lv_obj_align(lbl, LV_ALIGN_LEFT_MID, 0, 0);
}
}
static void fill_list_wav() {
if (!list_wav) {
return;
}
lv_obj_clean(list_wav);
const uint8_t n = wav_list_count();
if (n == 0) {
lv_obj_t *empty = lv_label_create(list_wav);
lv_label_set_text(empty, "No .wav on TF\nPut in / or /wav");
lv_obj_set_style_text_font(empty, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(empty, lv_color_hex(COL_MUTED), 0);
return;
}
const int8_t sel = wav_list_selected();
const bool playing = audio_is_playing() && g_tab == 1;
for (uint8_t i = 0; i < n; i++) {
const WavTrack *t = wav_list_get(i);
if (!t) {
continue;
// Numpad 3x4
static const char *keys[12] = {"1", "2", "3", "4", "5", "6",
"7", "8", "9", "C", "0", "OK"};
const int kw = 90;
const int kh = 32;
const int ox = 16;
const int oy = 72;
const int gap = 6;
for (int i = 0; i < 12; i++) {
const int r = i / 3;
const int c = i % 3;
const int x = ox + c * (kw + gap);
const int y = oy + r * (kh + gap);
lv_event_cb_t cb = on_custom_digit;
void *ud = nullptr;
if (i <= 8) {
ud = (void *)(intptr_t)(i + 1); // 1..9
} else if (i == 9) {
ud = (void *)(intptr_t)-1; // clear
} else if (i == 10) {
ud = (void *)(intptr_t)0; // 0
} else {
cb = on_custom_ok;
}
lv_obj_t *b = make_small_btn(panel_custom, keys[i], x, y, kw, kh, cb, ud);
if (i == 11) {
style_btn(b, COL_BTN_PLAY);
} else if (i == 9) {
style_btn(b, COL_BTN_STOP);
}
lv_obj_t *row = lv_button_create(list_wav);
lv_obj_set_width(row, lv_pct(100));
lv_obj_set_height(row, 32);
lv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(row, on_wav_clicked, LV_EVENT_CLICKED, (void *)(intptr_t)i);
style_row(row, sel == (int8_t)i, playing && sel == (int8_t)i);
lv_obj_t *lbl = lv_label_create(row);
char line[40];
snprintf(line, sizeof(line), "%u %s", (unsigned)(i + 1), t->name);
lv_label_set_text(lbl, line);
lv_obj_set_style_text_font(lbl, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(lbl, lv_color_hex(COL_INK), 0);
lv_label_set_long_mode(lbl, LV_LABEL_LONG_SCROLL_CIRCULAR);
lv_obj_set_width(lbl, lv_pct(100));
lv_obj_align(lbl, LV_ALIGN_LEFT_MID, 0, 0);
}
}
void ui_refresh_lists() {
fill_list_beep();
fill_list_wav();
}
uint8_t ui_active_tab() {
return g_tab;
make_small_btn(panel_custom, "Cancel", 16, UI_VER_RES - 34, UI_HOR_RES - 32,
28, on_custom_cancel, nullptr);
}
void ui_refresh_audio_buttons() {
@@ -204,60 +382,101 @@ void ui_refresh_audio_buttons() {
if (btn_speaker) {
style_btn(btn_speaker, audio_is_enabled() ? COL_BTN_ON : COL_BTN_OFF);
}
if (lbl_play) {
lv_label_set_text(lbl_play, audio_is_playing() ? "STOP" : "PLAY");
}
if (btn_play) {
style_btn(btn_play, audio_is_playing() ? COL_BTN_STOP : COL_BTN_PLAY);
}
if (lbl_audio) {
lv_label_set_text(lbl_audio, audio_last_status());
}
if (slider_vol && (int32_t)audio_get_volume() != lv_slider_get_value(slider_vol)) {
if (slider_vol &&
(int32_t)audio_get_volume() != lv_slider_get_value(slider_vol)) {
lv_slider_set_value(slider_vol, audio_get_volume(), LV_ANIM_OFF);
}
sync_vol_label();
}
static void style_scroll_list(lv_obj_t *list) {
lv_obj_set_size(list, lv_pct(100), lv_pct(100));
lv_obj_set_style_bg_opa(list, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(list, 0, 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_clear_flag(list, LV_OBJ_FLAG_CLICKABLE);
}
void ui_create() {
pomo_init();
sound_set_selected(11); // Alarm
lv_obj_t *scr = lv_screen_active();
lv_obj_set_style_bg_color(scr, lv_color_hex(COL_BG), 0);
lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0);
lv_obj_clear_flag(scr, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t *lbl_title = lv_label_create(scr);
lv_label_set_text(lbl_title, "CYD");
lv_label_set_text(lbl_title, "POMO");
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_ACCENT), 0);
lv_obj_set_pos(lbl_title, 12, 4);
lv_obj_set_style_text_color(lbl_title, lv_color_hex(COL_TOMATO), 0);
lv_obj_set_pos(lbl_title, UX(12), UY(4));
lbl_chip = lv_label_create(scr);
lv_label_set_text(lbl_chip, "-");
lv_obj_set_style_text_font(lbl_chip, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(lbl_chip, lv_color_hex(COL_MUTED), 0);
lv_obj_align(lbl_chip, LV_ALIGN_TOP_RIGHT, -12, 8);
lv_obj_align(lbl_chip, LV_ALIGN_TOP_RIGHT, UX(-12), UY(8));
make_small_btn(scr, "-", 12, 32, 40, 44, on_minus_clicked, nullptr);
lbl_timer = lv_label_create(scr);
lv_label_set_text(lbl_timer, "05:00");
lv_obj_set_style_text_font(lbl_timer, &lv_font_montserrat_28, 0);
lv_obj_set_style_text_color(lbl_timer, lv_color_hex(COL_ACCENT), 0);
lv_obj_align(lbl_timer, LV_ALIGN_TOP_MID, UX(0), UY(34));
make_small_btn(scr, "+", UI_HOR_RES - 52, 32, 40, 44, on_plus_clicked,
nullptr);
lbl_status = lv_label_create(scr);
lv_label_set_text(lbl_status, "5m - READY");
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_MID, UX(0), UY(74));
// Presets: 1 3 5 10 + Set
const int gap = 6;
const int pw = (UI_HOR_RES - 24 - gap * 4) / 5;
static const char *ptxt[PRESET_N] = {"1", "3", "5", "10"};
for (int i = 0; i < PRESET_N; i++) {
btn_preset[i] =
make_small_btn(scr, ptxt[i], 12 + i * (pw + gap), 94, pw, 30,
on_preset_clicked, (void *)(intptr_t)i);
}
btn_custom = make_small_btn(scr, "Set", 12 + PRESET_N * (pw + gap), 94, pw,
30, on_custom_open, nullptr);
// Sound
lv_obj_t *snd_cap = lv_label_create(scr);
lv_label_set_text(snd_cap, "SOUND");
lv_obj_set_style_text_font(snd_cap, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(snd_cap, lv_color_hex(COL_MUTED), 0);
lv_obj_set_pos(snd_cap, UX(12), UY(134));
make_small_btn(scr, "<", 68, 128, 32, 28, on_sound_prev, nullptr);
lv_obj_t *btn_snd = lv_button_create(scr);
lv_obj_set_pos(btn_snd, UX(104), UY(128));
lv_obj_set_size(btn_snd, UI_HOR_RES - 104 - 48, 28);
lv_obj_add_flag(btn_snd, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(btn_snd, on_sound_preview, LV_EVENT_CLICKED, nullptr);
style_btn(btn_snd, COL_MODE);
lbl_sound = lv_label_create(btn_snd);
lv_label_set_text(lbl_sound, "Alarm");
lv_obj_set_style_text_font(lbl_sound, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(lbl_sound, lv_color_hex(COL_INK), 0);
lv_obj_center(lbl_sound);
make_small_btn(scr, ">", UI_HOR_RES - 44, 128, 32, 28, on_sound_next,
nullptr);
// Volume
lv_obj_t *vol_cap = lv_label_create(scr);
lv_label_set_text(vol_cap, "VOL");
lv_obj_set_style_text_font(vol_cap, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(vol_cap, lv_color_hex(COL_MUTED), 0);
lv_obj_set_pos(vol_cap, 12, 30);
lv_obj_set_pos(vol_cap, UX(12), UY(164));
slider_vol = lv_slider_create(scr);
lv_obj_set_size(slider_vol, UI_HOR_RES - 12 - 56 - 44, 14);
lv_obj_set_pos(slider_vol, 52, 30);
lv_obj_set_pos(slider_vol, UX(52), UY(164));
lv_slider_set_range(slider_vol, 0, 100);
lv_slider_set_value(slider_vol, audio_get_volume(), LV_ANIM_OFF);
lv_obj_set_style_bg_color(slider_vol, lv_color_hex(COL_LINE), LV_PART_MAIN);
@@ -267,97 +486,101 @@ void ui_create() {
lv_obj_set_style_bg_opa(slider_vol, LV_OPA_COVER, LV_PART_INDICATOR);
lv_obj_set_style_bg_color(slider_vol, lv_color_hex(COL_INK), LV_PART_KNOB);
lv_obj_set_style_pad_all(slider_vol, 4, LV_PART_KNOB);
lv_obj_add_event_cb(slider_vol, on_vol_changed, LV_EVENT_VALUE_CHANGED, nullptr);
lv_obj_add_event_cb(slider_vol, on_vol_changed, LV_EVENT_VALUE_CHANGED,
nullptr);
lbl_vol = lv_label_create(scr);
lv_label_set_text(lbl_vol, "40%");
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_INK), 0);
lv_obj_align(lbl_vol, LV_ALIGN_TOP_RIGHT, -12, 30);
lv_obj_align(lbl_vol, LV_ALIGN_TOP_RIGHT, UX(-12), UY(164));
lbl_audio = lv_label_create(scr);
lv_label_set_text(lbl_audio, "Speaker OFF");
lv_obj_set_style_text_font(lbl_audio, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(lbl_audio, lv_color_hex(COL_MUTED), 0);
lv_obj_set_pos(lbl_audio, 12, 48);
lv_obj_set_pos(lbl_audio, UX(12), UY(182));
// Tabs: Beep | WAV
tabview = lv_tabview_create(scr);
lv_obj_set_pos(tabview, 8, 66);
lv_obj_set_size(tabview, UI_HOR_RES - 16, 110);
lv_tabview_set_tab_bar_size(tabview, 28);
lv_obj_set_style_bg_color(tabview, lv_color_hex(COL_BG), 0);
lv_obj_add_event_cb(tabview, on_tab_changed, LV_EVENT_VALUE_CHANGED, nullptr);
lv_obj_t *tab_beep = lv_tabview_add_tab(tabview, "Beep");
lv_obj_t *tab_wav = lv_tabview_add_tab(tabview, "WAV");
lv_obj_set_style_bg_color(tab_beep, lv_color_hex(COL_BG), 0);
lv_obj_set_style_bg_color(tab_wav, lv_color_hex(COL_BG), 0);
lv_obj_set_style_pad_all(tab_beep, 0, 0);
lv_obj_set_style_pad_all(tab_wav, 0, 0);
list_beep = lv_obj_create(tab_beep);
style_scroll_list(list_beep);
list_wav = lv_obj_create(tab_wav);
style_scroll_list(list_wav);
const int btn_y = 184;
const int btn_h = 48;
const int gap = 10;
const int btn_w = (UI_HOR_RES - 24 - gap) / 2;
const int btn_y = 200;
const int btn_h = 36;
const int bgap = 8;
const int btn_w = (UI_HOR_RES - 24 - bgap * 2) / 3;
btn_speaker = lv_button_create(scr);
lv_obj_set_pos(btn_speaker, 12, btn_y);
lv_obj_set_size(btn_speaker, btn_w, btn_h);
lv_obj_add_flag(btn_speaker, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(btn_speaker, on_speaker_clicked, LV_EVENT_CLICKED, nullptr);
lv_obj_add_event_cb(btn_speaker, on_speaker_clicked, LV_EVENT_CLICKED,
nullptr);
style_btn(btn_speaker, COL_BTN_OFF);
lv_obj_t *spk_cap = lv_label_create(btn_speaker);
lv_label_set_text(spk_cap, "SPEAKER");
lv_label_set_text(spk_cap, "SPK");
lv_obj_set_style_text_font(spk_cap, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(spk_cap, lv_color_hex(COL_MUTED), 0);
lv_obj_align(spk_cap, LV_ALIGN_TOP_MID, 0, 4);
lv_obj_align(spk_cap, LV_ALIGN_LEFT_MID, UX(8), UY(0));
lbl_speaker = lv_label_create(btn_speaker);
lv_label_set_text(lbl_speaker, "OFF");
lv_obj_set_style_text_font(lbl_speaker, &lv_font_montserrat_16, 0);
lv_obj_set_style_text_font(lbl_speaker, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(lbl_speaker, lv_color_hex(COL_INK), 0);
lv_obj_align(lbl_speaker, LV_ALIGN_BOTTOM_MID, 0, -6);
lv_obj_align(lbl_speaker, LV_ALIGN_RIGHT_MID, UX(-8), UY(0));
btn_play = lv_button_create(scr);
lv_obj_set_pos(btn_play, 12 + btn_w + gap, btn_y);
lv_obj_set_size(btn_play, btn_w, btn_h);
lv_obj_add_flag(btn_play, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(btn_play, on_play_clicked, LV_EVENT_CLICKED, nullptr);
style_btn(btn_play, COL_BTN_PLAY);
btn_start = lv_button_create(scr);
lv_obj_set_pos(btn_start, 12 + btn_w + bgap, btn_y);
lv_obj_set_size(btn_start, btn_w, btn_h);
lv_obj_add_flag(btn_start, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(btn_start, on_start_clicked, LV_EVENT_CLICKED, nullptr);
style_btn(btn_start, COL_BTN_PLAY);
lv_obj_t *play_cap = lv_label_create(btn_play);
lv_label_set_text(play_cap, "SOUND");
lv_obj_set_style_text_font(play_cap, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(play_cap, lv_color_hex(COL_MUTED), 0);
lv_obj_align(play_cap, LV_ALIGN_TOP_MID, 0, 4);
lbl_start = lv_label_create(btn_start);
lv_label_set_text(lbl_start, "START");
lv_obj_set_style_text_font(lbl_start, &lv_font_montserrat_16, 0);
lv_obj_set_style_text_color(lbl_start, lv_color_hex(COL_INK), 0);
lv_obj_center(lbl_start);
lbl_play = lv_label_create(btn_play);
lv_label_set_text(lbl_play, "PLAY");
lv_obj_set_style_text_font(lbl_play, &lv_font_montserrat_16, 0);
lv_obj_set_style_text_color(lbl_play, lv_color_hex(COL_INK), 0);
lv_obj_align(lbl_play, LV_ALIGN_BOTTOM_MID, 0, -6);
btn_reset = lv_button_create(scr);
lv_obj_set_pos(btn_reset, 12 + (btn_w + bgap) * 2, btn_y);
lv_obj_set_size(btn_reset, btn_w, btn_h);
lv_obj_add_flag(btn_reset, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(btn_reset, on_reset_clicked, LV_EVENT_CLICKED, nullptr);
style_btn(btn_reset, COL_BTN_OFF);
lv_obj_t *lbl_rst = lv_label_create(btn_reset);
lv_label_set_text(lbl_rst, "RESET");
lv_obj_set_style_text_font(lbl_rst, &lv_font_montserrat_16, 0);
lv_obj_set_style_text_color(lbl_rst, lv_color_hex(COL_INK), 0);
lv_obj_center(lbl_rst);
build_custom_panel(scr);
refresh_sound_label();
refresh_timer_labels();
ui_refresh_audio_buttons();
ui_refresh_lists();
}
void ui_set_touch_debug(int16_t /*lx*/, int16_t /*ly*/, bool /*down*/) {}
void ui_tick() {
pomo_tick();
if (pomo_consume_alarm()) {
const int8_t sel = sound_selected();
if (sel >= 0) {
audio_play_sound((uint8_t)sel);
}
}
refresh_timer_labels();
ui_refresh_audio_buttons();
}
void ui_update(const SdCardInfo &info) {
if (!info.mounted || !info.present) {
lv_label_set_text(lbl_chip, info.error.length() ? info.error.c_str() : "FAIL");
lv_label_set_text(lbl_chip,
info.error.length() ? info.error.c_str() : "TF?");
lv_obj_set_style_text_color(lbl_chip, lv_color_hex(COL_BAD), 0);
} else {
char chip[40];
snprintf(chip, sizeof(chip), "OK %s W%u", info.type, (unsigned)wav_list_count());
snprintf(chip, sizeof(chip), "OK %s", info.type);
lv_label_set_text(lbl_chip, chip);
lv_obj_set_style_text_color(lbl_chip, lv_color_hex(COL_OK), 0);
}