mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-09 03:06:39 +00:00
- Home > Map is the navigation map: waypoints (list, add at GPS / by coordinates / long-press spot), live shares, trail recording + tools (save, load, GPX export, track back), live share controls, target bar with ETA; the nodes map opens from Nearby. - Map: smooth panning (no decode during a drag, idle read-ahead, cached coarser stand-ins), overzoom limited to 2 levels, resumable downloads, map credit behind a "©" and in Settings > About; target ring and trail computed in double. - Home in swipeable pages; Clock (alarm, timer, stopwatch, ring card). - Settings rendered from the Core schema (display & power with a brightness slider, messages & contacts, trail / live share / alerts), GPS switch, Radio screen; low-battery shutdown. - WioTrackerL2Display: honour the 0-4 brightness level. - docs: L2 hardware checklist, ui-core notes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
309 lines
13 KiB
C++
309 lines
13 KiB
C++
#pragma once
|
|
// Clock tools (Home > Clock): alarm, countdown timer and stopwatch -- ui-new's
|
|
// Tools > Clock on a touch screen. The alarm (NodePrefs alarm_*) and the
|
|
// countdown run in the Core's ClockEngine, so they fire on any screen and while
|
|
// the display sleeps; the stopwatch is view state here, as in ui-new. Whatever
|
|
// fires brings up a full-screen card with Dismiss (silent: the L2 speaker has
|
|
// no driver yet).
|
|
//
|
|
// Single-TU fragment: included by ui-lvgl/UITask.cpp after NavMap.h.
|
|
|
|
namespace clockview {
|
|
|
|
enum : uint8_t { TAB_ALARM, TAB_TIMER, TAB_STOPWATCH, TAB_COUNT };
|
|
enum : uint8_t { ACT_START_STOP, ACT_RESET };
|
|
|
|
static uint8_t s_tab = TAB_ALARM;
|
|
static uint8_t s_timer_h = 0, s_timer_m = 5, s_timer_s = 0; // countdown to start
|
|
static bool s_sw_running = false;
|
|
static uint32_t s_sw_start = 0, s_sw_accum = 0;
|
|
static bool s_shown_running = false; // timer state the screen was built for
|
|
static lv_obj_t* s_big = nullptr; // timer remaining / stopwatch reading
|
|
static lv_obj_t* s_go_lbl = nullptr; // Start / Stop
|
|
static lv_obj_t* s_ring = nullptr; // "Timer done" card on the top layer
|
|
static lv_obj_t* s_ring_lbl = nullptr;
|
|
static lv_obj_t* s_alarm_sw = nullptr; // follows a time change (setting it arms the alarm)
|
|
|
|
static char s_opts24[24 * 3], s_opts60[60 * 3]; // "00\n01\n..." for the rollers
|
|
static void buildOptions() {
|
|
if (s_opts24[0]) return;
|
|
int o = 0;
|
|
for (int i = 0; i < 24; i++) o += snprintf(s_opts24 + o, sizeof(s_opts24) - o, i ? "\n%02d" : "%02d", i);
|
|
o = 0;
|
|
for (int i = 0; i < 60; i++) o += snprintf(s_opts60 + o, sizeof(s_opts60) - o, i ? "\n%02d" : "%02d", i);
|
|
}
|
|
|
|
static uint32_t swElapsed() { return s_sw_accum + (s_sw_running ? millis() - s_sw_start : 0); }
|
|
|
|
// Countdown: rounds up, so "00:01" shows until it actually fires.
|
|
static void fmtRemaining(char* b, int n, uint32_t ms) {
|
|
uint32_t t = (ms + 999) / 1000;
|
|
if (t >= 3600) snprintf(b, n, "%lu:%02lu:%02lu", (unsigned long)(t / 3600), (unsigned long)(t / 60 % 60), (unsigned long)(t % 60));
|
|
else snprintf(b, n, "%02lu:%02lu", (unsigned long)(t / 60), (unsigned long)(t % 60));
|
|
}
|
|
static void fmtStopwatch(char* b, int n, uint32_t ms) {
|
|
uint32_t t = ms / 1000;
|
|
if (t >= 3600) snprintf(b, n, "%lu:%02lu:%02lu", (unsigned long)(t / 3600), (unsigned long)(t / 60 % 60), (unsigned long)(t % 60));
|
|
else snprintf(b, n, "%02lu:%02lu.%lu", (unsigned long)(t / 60), (unsigned long)(t % 60), (unsigned long)(ms / 100 % 10));
|
|
}
|
|
|
|
static lv_obj_t* roller(lv_obj_t* parent, const char* opts, int sel, lv_event_cb_t cb, uintptr_t which) {
|
|
lv_obj_t* r = lv_roller_create(parent);
|
|
lv_roller_set_options(r, opts, LV_ROLLER_MODE_INFINITE);
|
|
lv_roller_set_selected(r, sel, LV_ANIM_OFF);
|
|
lv_obj_set_size(r, 60, 84); // three rows; leaves room for the button below
|
|
lv_obj_set_style_text_font(r, THEME_FONT_TITLE, 0);
|
|
lv_obj_set_style_text_font(r, THEME_FONT_LARGE, LV_PART_SELECTED);
|
|
lv_obj_set_style_text_line_space(r, 4, 0);
|
|
lv_obj_set_style_text_color(r, lv_color_hex(theme::TEXT_MUTED), 0);
|
|
lv_obj_set_style_bg_color(r, lv_color_hex(theme::SURFACE), 0);
|
|
lv_obj_set_style_border_width(r, 0, 0);
|
|
lv_obj_set_style_bg_color(r, lv_color_hex(theme::SURFACE_2), LV_PART_SELECTED);
|
|
lv_obj_set_style_text_color(r, lv_color_hex(theme::ACCENT), LV_PART_SELECTED);
|
|
lv_obj_add_event_cb(r, cb, LV_EVENT_VALUE_CHANGED, (void*)which);
|
|
return r;
|
|
}
|
|
|
|
static lv_obj_t* colon(lv_obj_t* parent) { return label(parent, ":", THEME_FONT_LARGE, theme::TEXT_MUTED); }
|
|
|
|
static lv_obj_t* row(lv_obj_t* parent) {
|
|
lv_obj_t* r = lv_obj_create(parent);
|
|
styleSurface(r, theme::BG);
|
|
lv_obj_remove_flag(r, LV_OBJ_FLAG_SCROLLABLE);
|
|
lv_obj_set_size(r, LV_PCT(100), LV_SIZE_CONTENT);
|
|
lv_obj_set_flex_flow(r, LV_FLEX_FLOW_ROW);
|
|
lv_obj_set_flex_align(r, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
|
lv_obj_set_style_pad_column(r, theme::GAP, 0);
|
|
return r;
|
|
}
|
|
|
|
} // namespace clockview
|
|
|
|
static void onOpenClock(lv_event_t* e) { (void)e; s_ui->showClock(); }
|
|
static void onClockTab(lv_event_t* e) { s_ui->clockTab((int)(uintptr_t)lv_event_get_user_data(e)); }
|
|
static void onClockAct(lv_event_t* e) { s_ui->clockAction((uint8_t)(uintptr_t)lv_event_get_user_data(e)); }
|
|
static void onRingDismiss(lv_event_t* e) { (void)e; s_ui->dismissRing(); }
|
|
|
|
// Alarm: hour / minute rollers, on switch, repeat. Saved on every change.
|
|
static void onAlarmRoller(lv_event_t* e) {
|
|
s_ui->setAlarm((int)(uintptr_t)lv_event_get_user_data(e), (int)lv_roller_get_selected((lv_obj_t*)lv_event_get_target(e)));
|
|
}
|
|
static void onAlarmSwitch(lv_event_t* e) {
|
|
s_ui->setAlarm(2, lv_obj_has_state((lv_obj_t*)lv_event_get_target(e), LV_STATE_CHECKED) ? 1 : 0);
|
|
}
|
|
static void onAlarmRepeat(lv_event_t* e) {
|
|
s_ui->setAlarm(3, (int)lv_dropdown_get_selected((lv_obj_t*)lv_event_get_target(e)));
|
|
}
|
|
static void onTimerRoller(lv_event_t* e) {
|
|
int v = (int)lv_roller_get_selected((lv_obj_t*)lv_event_get_target(e));
|
|
switch ((uintptr_t)lv_event_get_user_data(e)) {
|
|
case 0: clockview::s_timer_h = (uint8_t)v; break;
|
|
case 1: clockview::s_timer_m = (uint8_t)v; break;
|
|
default: clockview::s_timer_s = (uint8_t)v; break;
|
|
}
|
|
}
|
|
|
|
void UITask::showClock() {
|
|
_screen = SCR_CLOCK;
|
|
buildClock();
|
|
}
|
|
|
|
void UITask::clockTab(int tab) {
|
|
if (tab < 0 || tab >= clockview::TAB_COUNT) return;
|
|
clockview::s_tab = (uint8_t)tab;
|
|
buildClock();
|
|
}
|
|
|
|
void UITask::buildClock() {
|
|
using namespace clockview;
|
|
buildOptions();
|
|
lv_obj_t* body = newScreen("Clock", true);
|
|
lv_obj_set_style_pad_row(body, 6, 0);
|
|
lv_obj_remove_flag(body, LV_OBJ_FLAG_SCROLLABLE);
|
|
s_big = s_go_lbl = s_alarm_sw = nullptr;
|
|
|
|
// Tabs
|
|
lv_obj_t* tabs = row(body);
|
|
static const char* const NAMES[TAB_COUNT] = { LV_SYMBOL_BELL " Alarm", UI_SYMBOL_CLOCK " Timer",
|
|
UI_SYMBOL_STOPWATCH " Stopwatch" };
|
|
for (int t = 0; t < TAB_COUNT; t++) {
|
|
lv_obj_t* b = lv_button_create(tabs);
|
|
lv_obj_set_height(b, 32);
|
|
lv_obj_set_flex_grow(b, 1);
|
|
lv_obj_set_style_shadow_width(b, 0, 0);
|
|
lv_obj_set_style_radius(b, theme::RADIUS, 0);
|
|
lv_obj_set_style_bg_color(b, lv_color_hex(t == s_tab ? theme::ACCENT_DIM : theme::SURFACE), 0);
|
|
lv_obj_add_event_cb(b, onClockTab, LV_EVENT_CLICKED, (void*)(uintptr_t)t);
|
|
lv_obj_center(label(b, NAMES[t], THEME_FONT_SMALL, theme::TEXT));
|
|
}
|
|
|
|
if (s_tab == TAB_ALARM) {
|
|
lv_obj_t* r = row(body);
|
|
roller(r, s_opts24, _prefs->alarm_hour % 24, onAlarmRoller, 0);
|
|
colon(r);
|
|
roller(r, s_opts60, _prefs->alarm_min % 60, onAlarmRoller, 1);
|
|
lv_obj_t* col = lv_obj_create(r);
|
|
styleSurface(col, theme::BG);
|
|
lv_obj_remove_flag(col, LV_OBJ_FLAG_SCROLLABLE);
|
|
lv_obj_set_size(col, 120, LV_SIZE_CONTENT);
|
|
lv_obj_set_flex_flow(col, LV_FLEX_FLOW_COLUMN);
|
|
lv_obj_set_flex_align(col, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
|
|
lv_obj_set_style_pad_row(col, 6, 0);
|
|
lv_obj_set_style_pad_left(col, 8, 0);
|
|
label(col, "Alarm on", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
|
lv_obj_t* sw = s_alarm_sw = lv_switch_create(col);
|
|
lv_obj_set_size(sw, 50, 26);
|
|
if (_prefs->alarm_on) lv_obj_add_state(sw, LV_STATE_CHECKED);
|
|
lv_obj_add_event_cb(sw, onAlarmSwitch, LV_EVENT_VALUE_CHANGED, NULL);
|
|
label(col, "Repeat", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
|
lv_obj_t* dd = lv_dropdown_create(col);
|
|
char opts[48];
|
|
int o = 0;
|
|
for (uint8_t i = 0; i < NodePrefs::ALARM_REPEAT_COUNT; i++)
|
|
o += snprintf(opts + o, sizeof(opts) - o, "%s%s", i ? "\n" : "", i ? NodePrefs::alarmRepeatLabel(i) : "Once");
|
|
lv_dropdown_set_options(dd, opts);
|
|
lv_dropdown_set_selected(dd, NodePrefs::alarmRepeatIdxForMask(_prefs->alarm_repeat_mask));
|
|
lv_obj_set_width(dd, 112);
|
|
lv_obj_add_event_cb(dd, onAlarmRepeat, LV_EVENT_VALUE_CHANGED, NULL);
|
|
bool synced = rtc_clock.getCurrentTime() > 1000000000UL;
|
|
label(body, synced ? "Rings at this local time (Settings time zone)." : "The clock isn't set yet: the alarm waits for a time sync.",
|
|
THEME_FONT_SMALL, theme::TEXT_MUTED);
|
|
return;
|
|
}
|
|
|
|
if (s_tab == TAB_TIMER) {
|
|
s_shown_running = _core->clock.isTimerRunning();
|
|
if (s_shown_running) {
|
|
s_big = label(body, "", THEME_FONT_CLOCK, theme::TEXT);
|
|
lv_obj_set_width(s_big, LV_PCT(100));
|
|
lv_obj_set_style_text_align(s_big, LV_TEXT_ALIGN_CENTER, 0);
|
|
lv_obj_set_style_pad_ver(s_big, 18, 0);
|
|
} else {
|
|
lv_obj_t* r = row(body);
|
|
roller(r, s_opts24, s_timer_h, onTimerRoller, 0);
|
|
colon(r);
|
|
roller(r, s_opts60, s_timer_m, onTimerRoller, 1);
|
|
colon(r);
|
|
roller(r, s_opts60, s_timer_s, onTimerRoller, 2);
|
|
}
|
|
} else { // stopwatch
|
|
s_big = label(body, "", THEME_FONT_CLOCK, theme::TEXT);
|
|
lv_obj_set_width(s_big, LV_PCT(100));
|
|
lv_obj_set_style_text_align(s_big, LV_TEXT_ALIGN_CENTER, 0);
|
|
lv_obj_set_style_pad_ver(s_big, 18, 0);
|
|
}
|
|
|
|
lv_obj_t* acts = row(body);
|
|
lv_obj_t* go = lv_button_create(acts);
|
|
lv_obj_set_size(go, 150, 40);
|
|
lv_obj_set_style_shadow_width(go, 0, 0);
|
|
lv_obj_set_style_radius(go, theme::RADIUS, 0);
|
|
lv_obj_set_style_bg_color(go, lv_color_hex(theme::ACCENT_DIM), 0);
|
|
lv_obj_add_event_cb(go, onClockAct, LV_EVENT_CLICKED, (void*)(uintptr_t)ACT_START_STOP);
|
|
s_go_lbl = label(go, "", THEME_FONT_BODY, theme::TEXT);
|
|
lv_obj_center(s_go_lbl);
|
|
if (s_tab == TAB_STOPWATCH) {
|
|
lv_obj_t* rs = lv_button_create(acts);
|
|
lv_obj_set_size(rs, 100, 40);
|
|
lv_obj_set_style_shadow_width(rs, 0, 0);
|
|
lv_obj_set_style_radius(rs, theme::RADIUS, 0);
|
|
lv_obj_set_style_bg_color(rs, lv_color_hex(theme::SURFACE), 0);
|
|
lv_obj_add_event_cb(rs, onClockAct, LV_EVENT_CLICKED, (void*)(uintptr_t)ACT_RESET);
|
|
lv_obj_center(label(rs, LV_SYMBOL_REFRESH " Reset", THEME_FONT_BODY, theme::TEXT));
|
|
}
|
|
refreshClock();
|
|
}
|
|
|
|
// From loop() while SCR_CLOCK is shown: the running readouts.
|
|
void UITask::refreshClock() {
|
|
using namespace clockview;
|
|
if (s_tab == TAB_TIMER) {
|
|
bool running = _core->clock.isTimerRunning();
|
|
if (running != s_shown_running) { buildClock(); return; } // started elsewhere / just fired
|
|
if (s_big) {
|
|
char b[16];
|
|
fmtRemaining(b, sizeof(b), _core->clock.timerRemainingMs());
|
|
lv_label_set_text(s_big, b);
|
|
}
|
|
if (s_go_lbl) lv_label_set_text(s_go_lbl, running ? LV_SYMBOL_STOP " Stop" : LV_SYMBOL_PLAY " Start");
|
|
} else if (s_tab == TAB_STOPWATCH) {
|
|
if (s_big) {
|
|
char b[16];
|
|
fmtStopwatch(b, sizeof(b), swElapsed());
|
|
lv_label_set_text(s_big, b);
|
|
}
|
|
if (s_go_lbl) lv_label_set_text(s_go_lbl, s_sw_running ? LV_SYMBOL_PAUSE " Stop" : LV_SYMBOL_PLAY " Start");
|
|
}
|
|
}
|
|
|
|
void UITask::clockAction(uint8_t act) {
|
|
using namespace clockview;
|
|
if (s_tab == TAB_TIMER) {
|
|
if (_core->clock.isTimerRunning()) _core->clock.stopTimer();
|
|
else {
|
|
uint32_t ms = ((uint32_t)s_timer_h * 3600 + s_timer_m * 60 + s_timer_s) * 1000UL;
|
|
if (ms == 0) { showToast("Set a time first"); return; }
|
|
_core->clock.startTimer(ms);
|
|
}
|
|
buildClock();
|
|
} else if (s_tab == TAB_STOPWATCH) {
|
|
if (act == ACT_RESET) { s_sw_accum = 0; s_sw_start = millis(); }
|
|
else if (s_sw_running) { s_sw_accum += millis() - s_sw_start; s_sw_running = false; }
|
|
else { s_sw_start = millis(); s_sw_running = true; }
|
|
refreshClock();
|
|
}
|
|
}
|
|
|
|
// which: 0 hour, 1 minute, 2 on/off, 3 repeat preset.
|
|
void UITask::setAlarm(int which, int v) {
|
|
if (!_prefs) return;
|
|
switch (which) {
|
|
case 0: _prefs->alarm_hour = (uint8_t)v; _prefs->alarm_on = 1; break; // setting a time arms it
|
|
case 1: _prefs->alarm_min = (uint8_t)v; _prefs->alarm_on = 1; break;
|
|
case 2: _prefs->alarm_on = (uint8_t)v; break;
|
|
case 3: _prefs->alarm_repeat_mask = NodePrefs::alarmRepeatMaskForIdx((uint8_t)v); break;
|
|
}
|
|
_core->clock.onAlarmChanged();
|
|
the_mesh.savePrefs();
|
|
if (which < 2 && clockview::s_alarm_sw) lv_obj_add_state(clockview::s_alarm_sw, LV_STATE_CHECKED);
|
|
}
|
|
|
|
// ── Ring card (any screen) ────────────────────────────────────────────────────
|
|
|
|
void UITask::showRing(const char* text) {
|
|
using namespace clockview;
|
|
wake();
|
|
if (!s_ring) {
|
|
s_ring = lv_obj_create(lv_layer_top());
|
|
lv_obj_remove_style_all(s_ring);
|
|
lv_obj_set_size(s_ring, LV_PCT(100), LV_PCT(100));
|
|
lv_obj_set_style_bg_color(s_ring, lv_color_hex(theme::BG), 0);
|
|
lv_obj_set_style_bg_opa(s_ring, LV_OPA_COVER, 0);
|
|
lv_obj_add_flag(s_ring, LV_OBJ_FLAG_CLICKABLE);
|
|
lv_obj_set_flex_flow(s_ring, LV_FLEX_FLOW_COLUMN);
|
|
lv_obj_set_flex_align(s_ring, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
|
lv_obj_set_style_pad_row(s_ring, 14, 0);
|
|
label(s_ring, LV_SYMBOL_BELL, THEME_FONT_LARGE, theme::ACCENT);
|
|
s_ring_lbl = label(s_ring, "", THEME_FONT_LARGE, theme::TEXT);
|
|
lv_obj_t* b = lv_button_create(s_ring);
|
|
lv_obj_set_size(b, 200, 56);
|
|
lv_obj_set_style_shadow_width(b, 0, 0);
|
|
lv_obj_set_style_radius(b, theme::RADIUS, 0);
|
|
lv_obj_set_style_bg_color(b, lv_color_hex(theme::ACCENT_DIM), 0);
|
|
lv_obj_add_event_cb(b, onRingDismiss, LV_EVENT_CLICKED, NULL);
|
|
lv_obj_center(label(b, "Dismiss", THEME_FONT_LARGE, theme::TEXT));
|
|
}
|
|
lv_label_set_text(s_ring_lbl, text);
|
|
lv_obj_remove_flag(s_ring, LV_OBJ_FLAG_HIDDEN);
|
|
}
|
|
|
|
void UITask::dismissRing() {
|
|
_core->clock.dismissRing();
|
|
hideRing();
|
|
}
|
|
|
|
void UITask::hideRing() {
|
|
if (clockview::s_ring) lv_obj_add_flag(clockview::s_ring, LV_OBJ_FLAG_HIDDEN);
|
|
if (_screen == SCR_CLOCK) buildClock(); // e.g. the timer's Start again
|
|
}
|