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>
This commit is contained in:
2026-10-18 18:58:21 +08:00
parent b44bd7d3f0
commit 54a70d09e2
8 changed files with 620 additions and 224 deletions

View File

@@ -1,5 +1,16 @@
# Changelog
## 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,12 +1,14 @@
# CYD TF + Speaker
# CYD Pomodoro
ESP32-2432S028R:LVGL 横屏显示 TF 卡信息,触控开关喇叭并播放音乐。
ESP32-2432S028R:LVGL 横屏番茄钟,可选内置提示音,触控喇叭播放。
## 功能
- SoftSPI 触摸(已标定,勿改 `src/touch.cpp` 映射)
- TF 卡信息(VSPI)
- 喇叭 GPIO26,默认音量 **20%**;按钮:Speaker ON/OFF、Play Music
- 番茄钟:预设 1 / 3 / 5 / 10 分钟;`Set` 自定义输入;`+/-` 微调;Start / Pause / Reset
- 结束音效:内置 Beep 列表自选(默认 Alarm),点名称可试听
- 喇叭 GPIO26 + 音量滑条;TF 卡状态显示在右上角
- 基线标签:`v1.0-beep-wav`(改番茄钟前的 Beep + WAV 界面)
## 构建 / 烧录

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,13 @@ 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

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

@@ -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, 0, 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, 0, 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, 0, 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,43 +382,29 @@ 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_style_text_color(lbl_title, lv_color_hex(COL_TOMATO), 0);
lv_obj_set_pos(lbl_title, 12, 4);
lbl_chip = lv_label_create(scr);
@@ -249,15 +413,70 @@ void ui_create() {
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);
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, 0, 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, 0, 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, 12, 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, 104, 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, 12, 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, 52, 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, -12, 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, 12, 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, 8, 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, -8, 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);
}