mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-02 19:02:00 +00:00
perf(ui-lvgl): lower clock when idle, fewer redraws, wake on BLE frames
- CPU at 80 MHz with the screen on after 2 s without touch or animation; a touch restores the full clock before its frame is drawn - setText(): periodic refreshes no longer redraw unchanged labels (status bar, Home, lock, Diag, GPS, compass, clock, map, nav, nearby) - map markers kept across the 3 s rebuild when unchanged - app connected: the loop blocks until a BLE frame arrives (serialFrameArrived / hasPendingFrames) instead of polling every 2 ms - PSRAM: boot frame sprite and the HTTP response buffer released Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -226,16 +226,16 @@ void UITask::refreshClock() {
|
|||||||
if (s_big) {
|
if (s_big) {
|
||||||
char b[16];
|
char b[16];
|
||||||
fmtRemaining(b, sizeof(b), _core->clock.timerRemainingMs());
|
fmtRemaining(b, sizeof(b), _core->clock.timerRemainingMs());
|
||||||
lv_label_set_text(s_big, b);
|
setText(s_big, b);
|
||||||
}
|
}
|
||||||
if (s_go_lbl) lv_label_set_text(s_go_lbl, running ? LV_SYMBOL_STOP " Stop" : LV_SYMBOL_PLAY " Start");
|
if (s_go_lbl) setText(s_go_lbl, running ? LV_SYMBOL_STOP " Stop" : LV_SYMBOL_PLAY " Start");
|
||||||
} else if (s_tab == TAB_STOPWATCH) {
|
} else if (s_tab == TAB_STOPWATCH) {
|
||||||
if (s_big) {
|
if (s_big) {
|
||||||
char b[16];
|
char b[16];
|
||||||
fmtStopwatch(b, sizeof(b), swElapsed());
|
fmtStopwatch(b, sizeof(b), swElapsed());
|
||||||
lv_label_set_text(s_big, b);
|
setText(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");
|
if (s_go_lbl) setText(s_go_lbl, s_sw_running ? LV_SYMBOL_PAUSE " Stop" : LV_SYMBOL_PLAY " Start");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -127,14 +127,14 @@ void UITask::refreshCompass() {
|
|||||||
lv_obj_invalidate(s_dial);
|
lv_obj_invalidate(s_dial);
|
||||||
}
|
}
|
||||||
if (have) {
|
if (have) {
|
||||||
lv_label_set_text_fmt(s_deg, "%03d", cog);
|
setTextFmt(s_deg, "%03d", cog);
|
||||||
lv_label_set_text(s_degsign, "\xC2\xB0");
|
setText(s_degsign, "\xC2\xB0");
|
||||||
lv_label_set_text(s_card, geo::bearingCardinal(cog));
|
setText(s_card, geo::bearingCardinal(cog));
|
||||||
lv_label_set_text(s_hint, "Course over ground");
|
setText(s_hint, "Course over ground");
|
||||||
} else {
|
} else {
|
||||||
lv_label_set_text(s_deg, "");
|
setText(s_deg, "");
|
||||||
lv_label_set_text(s_degsign, "");
|
setText(s_degsign, "");
|
||||||
lv_label_set_text(s_card, "--");
|
setText(s_card, "--");
|
||||||
lv_label_set_text(s_hint, hint);
|
setText(s_hint, hint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -288,21 +288,21 @@ void UITask::refreshLock() {
|
|||||||
clockFaceSet(s_lock_clock, &ti, _prefs);
|
clockFaceSet(s_lock_clock, &ti, _prefs);
|
||||||
char date[48];
|
char date[48];
|
||||||
fmtDate(date, sizeof(date), ti);
|
fmtDate(date, sizeof(date), ti);
|
||||||
if (s_lock_date) lv_label_set_text(s_lock_date, date);
|
if (s_lock_date) setText(s_lock_date, date);
|
||||||
}
|
}
|
||||||
int unread = unreadTotal();
|
int unread = unreadTotal();
|
||||||
if (!s_lock_date) { // PIN card: the count next to the clock, the keypad's line
|
if (!s_lock_date) { // PIN card: the count next to the clock, the keypad's line
|
||||||
if (unread > 0) lv_label_set_text_fmt(s_lock_unread, LV_SYMBOL_ENVELOPE " %d", unread);
|
if (unread > 0) setTextFmt(s_lock_unread, LV_SYMBOL_ENVELOPE " %d", unread);
|
||||||
else lv_label_set_text(s_lock_unread, "");
|
else setText(s_lock_unread, "");
|
||||||
if (!s_pin_msg) return;
|
if (!s_pin_msg) return;
|
||||||
bool was = _pin_tries.block_until != 0;
|
bool was = _pin_tries.block_until != 0;
|
||||||
if (_pin_tries.blocked()) lv_label_set_text_fmt(s_pin_msg, "Too many tries - wait %lu s", (unsigned long)_pin_tries.secondsLeft());
|
if (_pin_tries.blocked()) setTextFmt(s_pin_msg, "Too many tries - wait %lu s", (unsigned long)_pin_tries.secondsLeft());
|
||||||
else if (was) lv_label_set_text(s_pin_msg, "Enter PIN");
|
else if (was) setText(s_pin_msg, "Enter PIN");
|
||||||
else if (!lv_label_get_text(s_pin_msg)[0]) lv_label_set_text(s_pin_msg, "Enter PIN");
|
else if (!lv_label_get_text(s_pin_msg)[0]) setText(s_pin_msg, "Enter PIN");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (unread > 0) lv_label_set_text_fmt(s_lock_unread, LV_SYMBOL_ENVELOPE " %d new message%s", unread, unread == 1 ? "" : "s");
|
if (unread > 0) setTextFmt(s_lock_unread, LV_SYMBOL_ENVELOPE " %d new message%s", unread, unread == 1 ? "" : "s");
|
||||||
else lv_label_set_text(s_lock_unread, "");
|
else setText(s_lock_unread, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Screen PIN ────────────────────────────────────────────────────────────────
|
// ── Screen PIN ────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -91,13 +91,13 @@ static void setLive(int i, const diag::Row& r) {
|
|||||||
const char* slash = strchr(r.value, '/');
|
const char* slash = strchr(r.value, '/');
|
||||||
char a[12];
|
char a[12];
|
||||||
snprintf(a, sizeof(a), "%.*s", slash ? (int)(slash - r.value) : (int)strlen(r.value), r.value);
|
snprintf(a, sizeof(a), "%.*s", slash ? (int)(slash - r.value) : (int)strlen(r.value), r.value);
|
||||||
lv_label_set_text(lv.in, a);
|
setText(lv.in, a);
|
||||||
lv_label_set_text(lv.out, slash ? slash + 1 : "");
|
setText(lv.out, slash ? slash + 1 : "");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
char v[40];
|
char v[40];
|
||||||
showValue(r.label, r.value, v, sizeof(v));
|
showValue(r.label, r.value, v, sizeof(v));
|
||||||
lv_label_set_text(lv.val, v);
|
setText(lv.val, v);
|
||||||
if (!strcmp(r.label, "Errors")) lv_obj_set_style_text_color(lv.val, lv_color_hex(strcmp(r.value, "OK") ? theme::FAIL : theme::OK), 0);
|
if (!strcmp(r.label, "Errors")) lv_obj_set_style_text_color(lv.val, lv_color_hex(strcmp(r.value, "OK") ? theme::FAIL : theme::OK), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -324,13 +324,13 @@ void UITask::refreshGps() {
|
|||||||
char pos[32] = "", alt[16] = "", speed[16] = "", dop[32] = "";
|
char pos[32] = "", alt[16] = "", speed[16] = "", dop[32] = "";
|
||||||
bool fix = false;
|
bool fix = false;
|
||||||
if (!g) {
|
if (!g) {
|
||||||
lv_label_set_text(s_status, "Not available");
|
setText(s_status, "Not available");
|
||||||
lv_obj_set_style_text_color(s_status, lv_color_hex(theme::TEXT_MUTED), 0);
|
lv_obj_set_style_text_color(s_status, lv_color_hex(theme::TEXT_MUTED), 0);
|
||||||
} else if (off) {
|
} else if (off) {
|
||||||
lv_label_set_text(s_status, "GPS is off");
|
setText(s_status, "GPS is off");
|
||||||
lv_obj_set_style_text_color(s_status, lv_color_hex(theme::TEXT_MUTED), 0);
|
lv_obj_set_style_text_color(s_status, lv_color_hex(theme::TEXT_MUTED), 0);
|
||||||
} else if (!data) {
|
} else if (!data) {
|
||||||
lv_label_set_text(s_status, "No data");
|
setText(s_status, "No data");
|
||||||
lv_obj_set_style_text_color(s_status, lv_color_hex(theme::FAIL), 0);
|
lv_obj_set_style_text_color(s_status, lv_color_hex(theme::FAIL), 0);
|
||||||
#if defined(SEEED_WIO_TRACKER_L2)
|
#if defined(SEEED_WIO_TRACKER_L2)
|
||||||
snprintf(used, sizeof(used), "Nothing from the receiver (%lu B)", (unsigned long)gps.rxChars());
|
snprintf(used, sizeof(used), "Nothing from the receiver (%lu B)", (unsigned long)gps.rxChars());
|
||||||
@@ -342,10 +342,10 @@ void UITask::refreshGps() {
|
|||||||
for (int i = 0; i < s_n; i++) { if (s_sats[i].snr >= 0) tracked++; if (s_used_flag[i]) used_n++; }
|
for (int i = 0; i < s_n; i++) { if (s_sats[i].snr >= 0) tracked++; if (s_used_flag[i]) used_n++; }
|
||||||
fix = g->hasFix();
|
fix = g->hasFix();
|
||||||
if (fix) {
|
if (fix) {
|
||||||
lv_label_set_text(s_status, g->fix_mode == 2 ? "2D fix" : "3D fix");
|
setText(s_status, g->fix_mode == 2 ? "2D fix" : "3D fix");
|
||||||
lv_obj_set_style_text_color(s_status, lv_color_hex(theme::OK), 0);
|
lv_obj_set_style_text_color(s_status, lv_color_hex(theme::OK), 0);
|
||||||
} else {
|
} else {
|
||||||
lv_label_set_text(s_status, "Searching");
|
setText(s_status, "Searching");
|
||||||
lv_obj_set_style_text_color(s_status, lv_color_hex(theme::ACCENT), 0);
|
lv_obj_set_style_text_color(s_status, lv_color_hex(theme::ACCENT), 0);
|
||||||
}
|
}
|
||||||
snprintf(used, sizeof(used), "%d of %d used", fix ? (used_n ? used_n : g->sats_used) : 0, s_n);
|
snprintf(used, sizeof(used), "%d of %d used", fix ? (used_n ? used_n : g->sats_used) : 0, s_n);
|
||||||
@@ -362,7 +362,7 @@ void UITask::refreshGps() {
|
|||||||
}
|
}
|
||||||
if (fix && g->hdop > 0) snprintf(dop, sizeof(dop), "H %.1f V %.1f P %.1f", g->hdop, g->vdop, g->pdop);
|
if (fix && g->hdop > 0) snprintf(dop, sizeof(dop), "H %.1f V %.1f P %.1f", g->hdop, g->vdop, g->pdop);
|
||||||
}
|
}
|
||||||
lv_label_set_text(s_used, used);
|
setText(s_used, used);
|
||||||
infoSet(s_inview, inview);
|
infoSet(s_inview, inview);
|
||||||
infoSet(s_search, search);
|
infoSet(s_search, search);
|
||||||
infoSet(s_ttff, ttff);
|
infoSet(s_ttff, ttff);
|
||||||
@@ -378,7 +378,7 @@ void UITask::refreshGps() {
|
|||||||
int n = 0, u = 0;
|
int n = 0, u = 0;
|
||||||
for (int i = 0; i < s_n; i++) if (s_sats[i].sys == k) { n++; if (s_used_flag[i]) u++; }
|
for (int i = 0; i < s_n; i++) if (s_sats[i].sys == k) { n++; if (s_used_flag[i]) u++; }
|
||||||
if (!n) { lv_obj_add_flag(s_sys_pill[k], LV_OBJ_FLAG_HIDDEN); continue; }
|
if (!n) { lv_obj_add_flag(s_sys_pill[k], LV_OBJ_FLAG_HIDDEN); continue; }
|
||||||
lv_label_set_text_fmt(s_sys_pill[k], "%c %s %d/%d", GpsSky::sysLetter(k), GpsSky::sysName(k), u, n);
|
setTextFmt(s_sys_pill[k], "%c %s %d/%d", GpsSky::sysLetter(k), GpsSky::sysName(k), u, n);
|
||||||
lv_obj_remove_flag(s_sys_pill[k], LV_OBJ_FLAG_HIDDEN);
|
lv_obj_remove_flag(s_sys_pill[k], LV_OBJ_FLAG_HIDDEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -729,7 +729,7 @@ void UITask::homeMapLayout() {
|
|||||||
}
|
}
|
||||||
const char* hint = !mapview::s_available ? "No map on the SD card"
|
const char* hint = !mapview::s_available ? "No map on the SD card"
|
||||||
: !shown && !s_pending ? "No map tiles for this area" : nullptr;
|
: !shown && !s_pending ? "No map tiles for this area" : nullptr;
|
||||||
if (hint) { lv_label_set_text(s_hint, hint); lv_obj_remove_flag(s_hint, LV_OBJ_FLAG_HIDDEN); }
|
if (hint) { setText(s_hint, hint); lv_obj_remove_flag(s_hint, LV_OBJ_FLAG_HIDDEN); }
|
||||||
else lv_obj_add_flag(s_hint, LV_OBJ_FLAG_HIDDEN);
|
else lv_obj_add_flag(s_hint, LV_OBJ_FLAG_HIDDEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -786,14 +786,14 @@ void UITask::refreshHome() {
|
|||||||
if (known) {
|
if (known) {
|
||||||
char date[48];
|
char date[48];
|
||||||
fmtDate(date, sizeof(date), ti);
|
fmtDate(date, sizeof(date), ti);
|
||||||
lv_label_set_text(_home_date, date);
|
setText(_home_date, date);
|
||||||
} else {
|
} else {
|
||||||
lv_label_set_text(_home_date, "time not synced");
|
setText(_home_date, "time not synced");
|
||||||
}
|
}
|
||||||
for (int i = 0; i < FIELDS; i++) {
|
for (int i = 0; i < FIELDS; i++) {
|
||||||
if (!s_field_val[i] || !_prefs) continue;
|
if (!s_field_val[i] || !_prefs) continue;
|
||||||
char v[24];
|
char v[24];
|
||||||
homeFieldText(_prefs->dashboard_fields[i], v, sizeof(v));
|
homeFieldText(_prefs->dashboard_fields[i], v, sizeof(v));
|
||||||
lv_label_set_text(s_field_val[i], v);
|
setText(s_field_val[i], v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,6 +101,19 @@ static lgfx::LGFX_Device* s_gfx = nullptr;
|
|||||||
static bool s_swallow = false; // ignore the touch that woke the display until it lifts
|
static bool s_swallow = false; // ignore the touch that woke the display until it lifts
|
||||||
static uint32_t s_flush_us = 0; // UI_PERF_TEST: time spent in flushCb
|
static uint32_t s_flush_us = 0; // UI_PERF_TEST: time spent in flushCb
|
||||||
|
|
||||||
|
// The CPU clock down to 80 MHz (LoRa, BLE and WiFi all run at that) with the
|
||||||
|
// screen off (powerSave()), and with it on while nobody touches it and nothing
|
||||||
|
// moves (the loop decides); a touch brings the full clock back before the
|
||||||
|
// frame it causes is drawn.
|
||||||
|
static uint32_t s_full_mhz = 0;
|
||||||
|
static bool s_cpu_slow = false;
|
||||||
|
static void cpuSlow(bool on) {
|
||||||
|
if (on == s_cpu_slow) return;
|
||||||
|
if (!s_full_mhz) s_full_mhz = getCpuFrequencyMhz();
|
||||||
|
s_cpu_slow = on;
|
||||||
|
setCpuFrequencyMhz(on ? 80 : s_full_mhz);
|
||||||
|
}
|
||||||
|
|
||||||
static void flushCb(lv_display_t* disp, const lv_area_t* area, uint8_t* px_map) {
|
static void flushCb(lv_display_t* disp, const lv_area_t* area, uint8_t* px_map) {
|
||||||
#ifdef UI_PERF_TEST
|
#ifdef UI_PERF_TEST
|
||||||
uint32_t t = micros();
|
uint32_t t = micros();
|
||||||
@@ -128,6 +141,7 @@ static void touchCb(lv_indev_t* indev, lv_indev_data_t* data) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (down) {
|
if (down) {
|
||||||
|
cpuSlow(false); // the frame this touch causes: at the full clock
|
||||||
data->state = LV_INDEV_STATE_PRESSED;
|
data->state = LV_INDEV_STATE_PRESSED;
|
||||||
data->point.x = tp.x;
|
data->point.x = tp.x;
|
||||||
data->point.y = tp.y;
|
data->point.y = tp.y;
|
||||||
@@ -139,6 +153,7 @@ static void touchCb(lv_indev_t* indev, lv_indev_data_t* data) {
|
|||||||
static bool begin() {
|
static bool begin() {
|
||||||
s_gfx = display.lgfxDevice();
|
s_gfx = display.lgfxDevice();
|
||||||
s_gfx->setColorDepth(16);
|
s_gfx->setColorDepth(16);
|
||||||
|
display.releaseFrameBuffer();
|
||||||
|
|
||||||
lv_display_t* disp = lv_display_create(s_gfx->width(), s_gfx->height());
|
lv_display_t* disp = lv_display_create(s_gfx->width(), s_gfx->height());
|
||||||
lv_display_set_flush_cb(disp, flushCb);
|
lv_display_set_flush_cb(disp, flushCb);
|
||||||
@@ -169,19 +184,22 @@ static bool touched() {
|
|||||||
static void swallowTouch() { s_swallow = true; }
|
static void swallowTouch() { s_swallow = true; }
|
||||||
|
|
||||||
// ── Power ────────────────────────────────────────────────────────────────────
|
// ── Power ────────────────────────────────────────────────────────────────────
|
||||||
// Screen off: the CPU clock down (LoRa, BLE and WiFi all run at 80 MHz) and
|
// Screen off: the clock down and the touch panel asleep unless a tap is to
|
||||||
// the touch panel asleep unless a tap is to wake the screen. Screen on: back.
|
// wake the screen. Screen on: back.
|
||||||
static bool s_touch_asleep = false;
|
static bool s_touch_asleep = false;
|
||||||
static void powerSave(bool on, bool keep_touch) {
|
static void powerSave(bool on, bool keep_touch) {
|
||||||
static uint32_t full_mhz = 0;
|
cpuSlow(on);
|
||||||
if (!full_mhz) full_mhz = getCpuFrequencyMhz();
|
|
||||||
setCpuFrequencyMhz(on ? 80 : full_mhz);
|
|
||||||
if (on && !keep_touch) { board.touchSleep(); s_touch_asleep = true; }
|
if (on && !keep_touch) { board.touchSleep(); s_touch_asleep = true; }
|
||||||
else if (!on && s_touch_asleep) { board.touchWake(); s_touch_asleep = false; }
|
else if (!on && s_touch_asleep) { board.touchWake(); s_touch_asleep = false; }
|
||||||
}
|
}
|
||||||
// Nothing due for `ms`: the loop task blocks and the core idles, clock-gated,
|
// Nothing due for `ms`: the loop task blocks and the core idles, clock-gated,
|
||||||
// instead of spinning.
|
// instead of spinning -- woken early by a frame from the app (below).
|
||||||
static void idle(uint32_t ms) { if (ms) vTaskDelay(pdMS_TO_TICKS(ms)); }
|
static TaskHandle_t s_loop_task = nullptr;
|
||||||
|
static void idle(uint32_t ms) {
|
||||||
|
if (!ms) return;
|
||||||
|
if (!s_loop_task) s_loop_task = xTaskGetCurrentTaskHandle();
|
||||||
|
ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(ms));
|
||||||
|
}
|
||||||
|
|
||||||
// Backlight 1-100 % (the brightness slider), on the LP5814 PWM.
|
// Backlight 1-100 % (the brightness slider), on the LP5814 PWM.
|
||||||
static void setBacklightPct(uint8_t pct) {
|
static void setBacklightPct(uint8_t pct) {
|
||||||
@@ -496,10 +514,12 @@ static int netState() {
|
|||||||
default: return NET_CONNECTING;
|
default: return NET_CONNECTING;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
static void fetchFreeBuffer();
|
||||||
static void netEnd() {
|
static void netEnd() {
|
||||||
if (s_net_scanning) { s_net_scanning = false; WiFi.scanDelete(); }
|
if (s_net_scanning) { s_net_scanning = false; WiFi.scanDelete(); }
|
||||||
WiFi.disconnect(true);
|
WiFi.disconnect(true);
|
||||||
WiFi.mode(WIFI_OFF);
|
WiFi.mode(WIFI_OFF);
|
||||||
|
fetchFreeBuffer();
|
||||||
}
|
}
|
||||||
// The network joined (Settings > WiFi marks it), "" when not connected.
|
// The network joined (Settings > WiFi marks it), "" when not connected.
|
||||||
static void netSsid(char* out, size_t n) {
|
static void netSsid(char* out, size_t n) {
|
||||||
@@ -651,6 +671,14 @@ static void fetchAbandon() {
|
|||||||
else fetchRelease();
|
else fetchRelease();
|
||||||
}
|
}
|
||||||
static const char* fetchError() { return s_ferr; }
|
static const char* fetchError() { return s_ferr; }
|
||||||
|
// WiFi off: the response buffer (grown to the biggest tile or update chunk,
|
||||||
|
// up to 512 KB of PSRAM) given back, unless a request is still finishing.
|
||||||
|
static void fetchFreeBuffer() {
|
||||||
|
if (s_fstate != F_IDLE) return;
|
||||||
|
heap_caps_free(s_fbuf);
|
||||||
|
s_fbuf = nullptr;
|
||||||
|
s_fcap = s_flen = 0;
|
||||||
|
}
|
||||||
|
|
||||||
#elif defined(SIM_PLATFORM) && defined(__EMSCRIPTEN__)
|
#elif defined(SIM_PLATFORM) && defined(__EMSCRIPTEN__)
|
||||||
// Browser simulator (variants/sim/build_wasm_lvgl.sh): SimLcdDisplay blits to
|
// Browser simulator (variants/sim/build_wasm_lvgl.sh): SimLcdDisplay blits to
|
||||||
@@ -719,6 +747,7 @@ static bool begin() {
|
|||||||
static bool touched() { return SimLcdDisplay::touchState().down; }
|
static bool touched() { return SimLcdDisplay::touchState().down; }
|
||||||
static void swallowTouch() { s_swallow = true; }
|
static void swallowTouch() { s_swallow = true; }
|
||||||
static void powerSave(bool, bool) {} // the browser's page loop runs the sim
|
static void powerSave(bool, bool) {} // the browser's page loop runs the sim
|
||||||
|
static void cpuSlow(bool) {}
|
||||||
static void idle(uint32_t) {}
|
static void idle(uint32_t) {}
|
||||||
static void setBacklightPct(uint8_t pct) { (void)pct; } // the browser canvas has no backlight
|
static void setBacklightPct(uint8_t pct) { (void)pct; } // the browser canvas has no backlight
|
||||||
|
|
||||||
@@ -870,3 +899,11 @@ static void loadHomeApps(char* out, size_t n) { nvs::getStr("mc_ui", "apps", out
|
|||||||
static void saveHomeApps(const char* v) { nvs::putStr("mc_ui", "apps", v); }
|
static void saveHomeApps(const char* v) { nvs::putStr("mc_ui", "apps", v); }
|
||||||
|
|
||||||
} // namespace lvport
|
} // namespace lvport
|
||||||
|
|
||||||
|
#if defined(SEEED_WIO_TRACKER_L2)
|
||||||
|
// A frame from the app (SerialBLEInterface, the BLE stack's task): the loop,
|
||||||
|
// blocked in lvport::idle(), reads it now rather than at the next timeout.
|
||||||
|
void serialFrameArrived() {
|
||||||
|
if (TaskHandle_t t = lvport::s_loop_task) xTaskNotifyGive(t);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -116,11 +116,12 @@ static void drawGrid(lv_event_t* e) {
|
|||||||
// Markers, remembered so layout can reposition them. `idx` is the Nearby row
|
// Markers, remembered so layout can reposition them. `idx` is the Nearby row
|
||||||
// (MK_NODE), the waypoint index (MK_WAYPOINT) or the live-share slot (MK_LIVE).
|
// (MK_NODE), the waypoint index (MK_WAYPOINT) or the live-share slot (MK_LIVE).
|
||||||
enum : uint8_t { MK_NODE, MK_WAYPOINT, MK_LIVE };
|
enum : uint8_t { MK_NODE, MK_WAYPOINT, MK_LIVE };
|
||||||
struct Mark { int32_t lat_e6, lon_e6; int idx; uint8_t kind; lv_obj_t* obj; lv_obj_t* dot; };
|
struct Mark { int32_t lat_e6, lon_e6; int idx; uint8_t kind; uint32_t col, text_hash; lv_obj_t* obj; lv_obj_t* dot; };
|
||||||
static const int MAX_MARKS = NearbyModel::MAX_NEARBY > WaypointStore::CAPACITY + LiveTrackStore::CAPACITY
|
static const int MAX_MARKS = NearbyModel::MAX_NEARBY > WaypointStore::CAPACITY + LiveTrackStore::CAPACITY
|
||||||
? NearbyModel::MAX_NEARBY : WaypointStore::CAPACITY + LiveTrackStore::CAPACITY; // Nearby map, or waypoints + live shares
|
? NearbyModel::MAX_NEARBY : WaypointStore::CAPACITY + LiveTrackStore::CAPACITY; // Nearby map, or waypoints + live shares
|
||||||
static Mark* s_marks = psramBuf<Mark>(MAX_MARKS);
|
static Mark* s_marks = psramBuf<Mark>(MAX_MARKS);
|
||||||
static int s_mark_count = 0;
|
static int s_mark_count = 0;
|
||||||
|
static int s_mark_prev = 0; // during a rebuild: how many the last one left, reused where unchanged
|
||||||
static int s_drag = 0; // px moved in the current press: a drag isn't a tap
|
static int s_drag = 0; // px moved in the current press: a drag isn't a tap
|
||||||
static bool s_available = false; // provider has data; checked when the map opens, not per frame
|
static bool s_available = false; // provider has data; checked when the map opens, not per frame
|
||||||
static double s_left = 0, s_top = 0; // world px (at the current zoom) of the view's top-left
|
static double s_left = 0, s_top = 0; // world px (at the current zoom) of the view's top-left
|
||||||
@@ -419,7 +420,7 @@ void UITask::layoutMap() {
|
|||||||
double lat = atan(sinh(M_PI * (1 - 2 * _map_cy / (double)(1 << _map_z)))) * 180.0 / M_PI;
|
double lat = atan(sinh(M_PI * (1 - 2 * _map_cy / (double)(1 << _map_z)))) * 180.0 / M_PI;
|
||||||
mapview::s_grid = { left, top, mapview::gridStep(_map_z, lat, _prefs && _prefs->units_imperial, 56, sl, sizeof(sl)) };
|
mapview::s_grid = { left, top, mapview::gridStep(_map_z, lat, _prefs && _prefs->units_imperial, 56, sl, sizeof(sl)) };
|
||||||
if (mapview::s_scale_lbl) {
|
if (mapview::s_scale_lbl) {
|
||||||
lv_label_set_text(mapview::s_scale_lbl, sl);
|
setText(mapview::s_scale_lbl, sl);
|
||||||
lv_obj_set_width(mapview::s_scale_bar, (int)lround(mapview::s_grid.step_px));
|
lv_obj_set_width(mapview::s_scale_bar, (int)lround(mapview::s_grid.step_px));
|
||||||
}
|
}
|
||||||
lv_obj_invalidate(_map_area);
|
lv_obj_invalidate(_map_area);
|
||||||
@@ -494,20 +495,20 @@ void UITask::layoutMap() {
|
|||||||
}
|
}
|
||||||
if (_map_nav) layoutNav();
|
if (_map_nav) layoutNav();
|
||||||
|
|
||||||
if (over) lv_label_set_text_fmt(_map_zoom_lbl, "z%d (map z%d)", _map_z, _map_z - over); // magnified
|
if (over) setTextFmt(_map_zoom_lbl, "z%d (map z%d)", _map_z, _map_z - over); // magnified
|
||||||
else if (mapview::s_provider == &mapview::s_vector) // spike: how long a tile takes
|
else if (mapview::s_provider == &mapview::s_vector) // spike: how long a tile takes
|
||||||
{
|
{
|
||||||
uint32_t ld, fl, ln;
|
uint32_t ld, fl, ln;
|
||||||
mapview::s_vector.lastSplit(ld, fl, ln);
|
mapview::s_vector.lastSplit(ld, fl, ln);
|
||||||
lv_label_set_text_fmt(_map_zoom_lbl, "z%d vt %lu ms (r%lu a%lu l%lu)", _map_z, (unsigned long)mapview::s_vector.lastMs(),
|
setTextFmt(_map_zoom_lbl, "z%d vt %lu ms (r%lu a%lu l%lu)", _map_z, (unsigned long)mapview::s_vector.lastMs(),
|
||||||
(unsigned long)ld, (unsigned long)fl, (unsigned long)ln);
|
(unsigned long)ld, (unsigned long)fl, (unsigned long)ln);
|
||||||
}
|
}
|
||||||
else lv_label_set_text_fmt(_map_zoom_lbl, "z%d", _map_z);
|
else setTextFmt(_map_zoom_lbl, "z%d", _map_z);
|
||||||
const char* hint = !have_provider ? "No map on the SD card.\nPut tiles in /maps (tools/maps)."
|
const char* hint = !have_provider ? "No map on the SD card.\nPut tiles in /maps (tools/maps)."
|
||||||
: mapview::s_dl.liveQueued() > 0 ? nullptr // being fetched
|
: mapview::s_dl.liveQueued() > 0 ? nullptr // being fetched
|
||||||
: (!_map_pending && shown == 0 && missing > 0)
|
: (!_map_pending && shown == 0 && missing > 0)
|
||||||
? "No map here at this zoom -\nzoom out or download the area" : nullptr;
|
? "No map here at this zoom -\nzoom out or download the area" : nullptr;
|
||||||
if (hint && !areaSelecting()) { lv_label_set_text(_map_hint, hint); lv_obj_remove_flag(_map_hint, LV_OBJ_FLAG_HIDDEN); }
|
if (hint && !areaSelecting()) { setText(_map_hint, hint); lv_obj_remove_flag(_map_hint, LV_OBJ_FLAG_HIDDEN); }
|
||||||
else lv_obj_add_flag(_map_hint, LV_OBJ_FLAG_HIDDEN);
|
else lv_obj_add_flag(_map_hint, LV_OBJ_FLAG_HIDDEN);
|
||||||
areaLayout();
|
areaLayout();
|
||||||
}
|
}
|
||||||
@@ -585,10 +586,12 @@ void UITask::mapLoop() {
|
|||||||
|
|
||||||
void UITask::rebuildMapMarkers() {
|
void UITask::rebuildMapMarkers() {
|
||||||
if (!_map_marks) return;
|
if (!_map_marks) return;
|
||||||
for (int k = 0; k < mapview::s_mark_count; k++) lv_obj_delete(mapview::s_marks[k].obj);
|
mapview::s_mark_prev = mapview::s_mark_count;
|
||||||
mapview::s_mark_count = 0;
|
mapview::s_mark_count = 0;
|
||||||
if (_map_nav) rebuildNavMarkers();
|
if (_map_nav) rebuildNavMarkers();
|
||||||
else rebuildNodeMarkers();
|
else rebuildNodeMarkers();
|
||||||
|
for (int k = mapview::s_mark_count; k < mapview::s_mark_prev; k++) lv_obj_delete(mapview::s_marks[k].obj);
|
||||||
|
mapview::s_mark_prev = 0;
|
||||||
lv_obj_update_layout(_map_marks); // so layoutMap() knows where each dot sits in its marker
|
lv_obj_update_layout(_map_marks); // so layoutMap() knows where each dot sits in its marker
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -604,9 +607,22 @@ void UITask::rebuildNodeMarkers() {
|
|||||||
|
|
||||||
// One clickable object per marker: a dot plus the name beside it. The object
|
// One clickable object per marker: a dot plus the name beside it. The object
|
||||||
// is placed so the dot's centre is on the spot, from where layout actually put
|
// is placed so the dot's centre is on the spot, from where layout actually put
|
||||||
// the dot inside it (Mark::dot) -- the name may be taller than the dot.
|
// the dot inside it (Mark::dot) -- the name may be taller than the dot. The
|
||||||
|
// markers are rebuilt every few seconds: one the same as the last time in its
|
||||||
|
// slot is kept as it is, so an unchanged map isn't redrawn under it.
|
||||||
void UITask::addMapMark(uint8_t kind, int idx, int32_t lat_e6, int32_t lon_e6, uint32_t col, const char* text) {
|
void UITask::addMapMark(uint8_t kind, int idx, int32_t lat_e6, int32_t lon_e6, uint32_t col, const char* text) {
|
||||||
if (!_map_marks || mapview::s_mark_count >= mapview::MAX_MARKS) return;
|
if (!_map_marks || mapview::s_mark_count >= mapview::MAX_MARKS) return;
|
||||||
|
uint32_t h = 2166136261u; // FNV-1a of the name
|
||||||
|
for (const char* c = text; *c; c++) h = (h ^ (uint8_t)*c) * 16777619u;
|
||||||
|
int k = mapview::s_mark_count;
|
||||||
|
if (k < mapview::s_mark_prev) {
|
||||||
|
mapview::Mark& old = mapview::s_marks[k];
|
||||||
|
if (old.kind == kind && old.idx == idx && old.lat_e6 == lat_e6 && old.lon_e6 == lon_e6 && old.col == col && old.text_hash == h) {
|
||||||
|
mapview::s_mark_count++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lv_obj_delete(old.obj);
|
||||||
|
}
|
||||||
lv_obj_t* m = lv_obj_create(_map_marks);
|
lv_obj_t* m = lv_obj_create(_map_marks);
|
||||||
lv_obj_remove_style_all(m);
|
lv_obj_remove_style_all(m);
|
||||||
lv_obj_set_size(m, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
lv_obj_set_size(m, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||||
@@ -628,7 +644,7 @@ void UITask::addMapMark(uint8_t kind, int idx, int32_t lat_e6, int32_t lon_e6, u
|
|||||||
lv_obj_remove_flag(name, LV_OBJ_FLAG_CLICKABLE);
|
lv_obj_remove_flag(name, LV_OBJ_FLAG_CLICKABLE);
|
||||||
|
|
||||||
mapview::Mark& mk = mapview::s_marks[mapview::s_mark_count++];
|
mapview::Mark& mk = mapview::s_marks[mapview::s_mark_count++];
|
||||||
mk.lat_e6 = lat_e6; mk.lon_e6 = lon_e6; mk.idx = idx; mk.kind = kind; mk.obj = m; mk.dot = dot;
|
mk.lat_e6 = lat_e6; mk.lon_e6 = lon_e6; mk.idx = idx; mk.kind = kind; mk.col = col; mk.text_hash = h; mk.obj = m; mk.dot = dot;
|
||||||
}
|
}
|
||||||
|
|
||||||
void UITask::mapPan(int dx, int dy) {
|
void UITask::mapPan(int dx, int dy) {
|
||||||
@@ -968,24 +984,24 @@ void UITask::mapDownloadTick() {
|
|||||||
next_ui = millis() + 500;
|
next_ui = millis() + 500;
|
||||||
if (_map_dl_pill) {
|
if (_map_dl_pill) {
|
||||||
if (dl.state() == mapview::TileDownloader::CONNECTING) {
|
if (dl.state() == mapview::TileDownloader::CONNECTING) {
|
||||||
lv_label_set_text(_map_dl_pill, LV_SYMBOL_WIFI " Connecting...");
|
setText(_map_dl_pill, LV_SYMBOL_WIFI " Connecting...");
|
||||||
lv_obj_remove_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN);
|
lv_obj_remove_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN);
|
||||||
} else if (dl.active()) {
|
} else if (dl.active()) {
|
||||||
if (dl.failed())
|
if (dl.failed())
|
||||||
lv_label_set_text_fmt(_map_dl_pill, LV_SYMBOL_DOWNLOAD " %lu / %lu " LV_SYMBOL_WARNING " %lu",
|
setTextFmt(_map_dl_pill, LV_SYMBOL_DOWNLOAD " %lu / %lu " LV_SYMBOL_WARNING " %lu",
|
||||||
(unsigned long)dl.processed(), (unsigned long)dl.total(), (unsigned long)dl.failed());
|
(unsigned long)dl.processed(), (unsigned long)dl.total(), (unsigned long)dl.failed());
|
||||||
else
|
else
|
||||||
lv_label_set_text_fmt(_map_dl_pill, LV_SYMBOL_DOWNLOAD " %lu / %lu", (unsigned long)dl.processed(),
|
setTextFmt(_map_dl_pill, LV_SYMBOL_DOWNLOAD " %lu / %lu", (unsigned long)dl.processed(),
|
||||||
(unsigned long)dl.total());
|
(unsigned long)dl.total());
|
||||||
lv_obj_remove_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN);
|
lv_obj_remove_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN);
|
||||||
} else if (dl.liveConnecting()) {
|
} else if (dl.liveConnecting()) {
|
||||||
lv_label_set_text(_map_dl_pill, LV_SYMBOL_WIFI " Connecting...");
|
setText(_map_dl_pill, LV_SYMBOL_WIFI " Connecting...");
|
||||||
lv_obj_remove_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN);
|
lv_obj_remove_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN);
|
||||||
} else if (dl.liveQueued() > 0) {
|
} else if (dl.liveQueued() > 0) {
|
||||||
lv_label_set_text_fmt(_map_dl_pill, LV_SYMBOL_DOWNLOAD " live %d", dl.liveQueued());
|
setTextFmt(_map_dl_pill, LV_SYMBOL_DOWNLOAD " live %d", dl.liveQueued());
|
||||||
lv_obj_remove_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN);
|
lv_obj_remove_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN);
|
||||||
} else if (mapview::s_available && dl.savedJob(s_job_tmp)) {
|
} else if (mapview::s_available && dl.savedJob(s_job_tmp)) {
|
||||||
lv_label_set_text(_map_dl_pill, LV_SYMBOL_DOWNLOAD " Resume?");
|
setText(_map_dl_pill, LV_SYMBOL_DOWNLOAD " Resume?");
|
||||||
lv_obj_remove_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN);
|
lv_obj_remove_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN);
|
||||||
} else {
|
} else {
|
||||||
lv_obj_add_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN);
|
lv_obj_add_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
|||||||
@@ -290,7 +290,7 @@ void UITask::refreshNavBar() {
|
|||||||
navmap::fmtDuration(left, sizeof(left), _core->live_share.remainingSecs());
|
navmap::fmtDuration(left, sizeof(left), _core->live_share.remainingSecs());
|
||||||
o += snprintf(t + o, sizeof(t) - o, "%sLIVE %s", o ? " " : "", left);
|
o += snprintf(t + o, sizeof(t) - o, "%sLIVE %s", o ? " " : "", left);
|
||||||
}
|
}
|
||||||
lv_label_set_text(_nav_rec, t);
|
setText(_nav_rec, t);
|
||||||
if (o) lv_obj_remove_flag(_nav_rec, LV_OBJ_FLAG_HIDDEN); else lv_obj_add_flag(_nav_rec, LV_OBJ_FLAG_HIDDEN);
|
if (o) lv_obj_remove_flag(_nav_rec, LV_OBJ_FLAG_HIDDEN); else lv_obj_add_flag(_nav_rec, LV_OBJ_FLAG_HIDDEN);
|
||||||
}
|
}
|
||||||
int32_t tlat, tlon;
|
int32_t tlat, tlon;
|
||||||
@@ -298,23 +298,23 @@ void UITask::refreshNavBar() {
|
|||||||
bool person = false, set = false;
|
bool person = false, set = false;
|
||||||
bool pos = navCurrentTarget(tlat, tlon, name, sizeof(name), person, set);
|
bool pos = navCurrentTarget(tlat, tlon, name, sizeof(name), person, set);
|
||||||
if (!set) {
|
if (!set) {
|
||||||
lv_label_set_text(_nav_title, "No target");
|
setText(_nav_title, "No target");
|
||||||
lv_obj_set_style_text_color(_nav_title, lv_color_hex(theme::TEXT_MUTED), 0);
|
lv_obj_set_style_text_color(_nav_title, lv_color_hex(theme::TEXT_MUTED), 0);
|
||||||
lv_label_set_text(_nav_info, "Tap here to pick a target, or hold the map on a spot");
|
setText(_nav_info, "Tap here to pick a target, or hold the map on a spot");
|
||||||
lv_obj_add_flag(_nav_clear, LV_OBJ_FLAG_HIDDEN);
|
lv_obj_add_flag(_nav_clear, LV_OBJ_FLAG_HIDDEN);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lv_obj_set_style_text_color(_nav_title, lv_color_hex(theme::ACCENT), 0);
|
lv_obj_set_style_text_color(_nav_title, lv_color_hex(theme::ACCENT), 0);
|
||||||
lv_obj_remove_flag(_nav_clear, LV_OBJ_FLAG_HIDDEN);
|
lv_obj_remove_flag(_nav_clear, LV_OBJ_FLAG_HIDDEN);
|
||||||
lv_label_set_text_fmt(_nav_title, "%s %s", navmap::s_tb.active() ? LV_SYMBOL_LOOP : person ? UI_SYMBOL_USERS : UI_SYMBOL_FLAG,
|
setTextFmt(_nav_title, "%s %s", navmap::s_tb.active() ? LV_SYMBOL_LOOP : person ? UI_SYMBOL_USERS : UI_SYMBOL_FLAG,
|
||||||
name);
|
name);
|
||||||
if (!pos) {
|
if (!pos) {
|
||||||
lv_label_set_text(_nav_info, "Position unknown (not shared recently)");
|
setText(_nav_info, "Position unknown (not shared recently)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int32_t lat, lon;
|
int32_t lat, lon;
|
||||||
if (!_core->course.currentLocation(lat, lon)) {
|
if (!_core->course.currentLocation(lat, lon)) {
|
||||||
lv_label_set_text(_nav_info, _core->gpsEnabled() || !_core->gpsAvailable()
|
setText(_nav_info, _core->gpsEnabled() || !_core->gpsAvailable()
|
||||||
? "Waiting for a GPS fix..." : "GPS is off - tap the crosshair to turn it on");
|
? "Waiting for a GPS fix..." : "GPS is off - tap the crosshair to turn it on");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -327,9 +327,9 @@ void UITask::refreshNavBar() {
|
|||||||
int cog;
|
int cog;
|
||||||
if (_core->course.currentCourse(cog)) snprintf(hdg, sizeof(hdg), " - you %d\xC2\xB0", cog);
|
if (_core->course.currentCourse(cog)) snprintf(hdg, sizeof(hdg), " - you %d\xC2\xB0", cog);
|
||||||
if (navmap::s_eta.eta(km, eta, sizeof(eta)))
|
if (navmap::s_eta.eta(km, eta, sizeof(eta)))
|
||||||
lv_label_set_text_fmt(_nav_info, "%s %d\xC2\xB0 %s%s - ETA %s", dist, to, geo::bearingCardinal(to), hdg, eta);
|
setTextFmt(_nav_info, "%s %d\xC2\xB0 %s%s - ETA %s", dist, to, geo::bearingCardinal(to), hdg, eta);
|
||||||
else
|
else
|
||||||
lv_label_set_text_fmt(_nav_info, "%s %d\xC2\xB0 %s%s", dist, to, geo::bearingCardinal(to), hdg);
|
setTextFmt(_nav_info, "%s %d\xC2\xB0 %s%s", dist, to, geo::bearingCardinal(to), hdg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Target selection ──────────────────────────────────────────────────────────
|
// ── Target selection ──────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -350,7 +350,7 @@ void UITask::refreshMelody() {
|
|||||||
lightNote(s_play_all ? melodyNote() : -1);
|
lightNote(s_play_all ? melodyNote() : -1);
|
||||||
if (!s_play_lbl || playing == s_playing_shown) return;
|
if (!s_play_lbl || playing == s_playing_shown) return;
|
||||||
s_playing_shown = playing;
|
s_playing_shown = playing;
|
||||||
lv_label_set_text(s_play_lbl, playing ? LV_SYMBOL_STOP " Stop" : LV_SYMBOL_PLAY " Play");
|
setText(s_play_lbl, playing ? LV_SYMBOL_STOP " Stop" : LV_SYMBOL_PLAY " Play");
|
||||||
}
|
}
|
||||||
|
|
||||||
void UITask::melodySave() {
|
void UITask::melodySave() {
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ void UITask::pollStorage() {
|
|||||||
if (s_sd_bar && !s_sd_read) {
|
if (s_sd_bar && !s_sd_read) {
|
||||||
s_sd_read = true;
|
s_sd_read = true;
|
||||||
s_sd_ok = lvport::sdInfo(s_sd_total, s_sd_used, s_sd_card);
|
s_sd_ok = lvport::sdInfo(s_sd_total, s_sd_used, s_sd_card);
|
||||||
if (!s_sd_ok) lv_label_set_text(s_sd_lbl, "Can't read the card");
|
if (!s_sd_ok) setText(s_sd_lbl, "Can't read the card");
|
||||||
else if (s_sd_card > s_sd_total + s_sd_total / 4) { // most of the card sits outside the partition
|
else if (s_sd_card > s_sd_total + s_sd_total / 4) { // most of the card sits outside the partition
|
||||||
char c[16], t[16], line[160];
|
char c[16], t[16], line[160];
|
||||||
fmtBytes(c, sizeof(c), s_sd_card);
|
fmtBytes(c, sizeof(c), s_sd_card);
|
||||||
@@ -337,7 +337,7 @@ void UITask::pollStorage() {
|
|||||||
fmtBytes(b, sizeof(b), mapview::s_live_cache.bytes());
|
fmtBytes(b, sizeof(b), mapview::s_live_cache.bytes());
|
||||||
if (mapview::s_live_cache.clearing()) snprintf(t, sizeof(t), "Deleting... %lu left", (unsigned long)mapview::s_live_cache.count());
|
if (mapview::s_live_cache.clearing()) snprintf(t, sizeof(t), "Deleting... %lu left", (unsigned long)mapview::s_live_cache.count());
|
||||||
else snprintf(t, sizeof(t), "%lu tiles, %s", (unsigned long)mapview::s_live_cache.count(), b);
|
else snprintf(t, sizeof(t), "%lu tiles, %s", (unsigned long)mapview::s_live_cache.count(), b);
|
||||||
lv_label_set_text(s_live_info, t);
|
setText(s_live_info, t);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -133,6 +133,23 @@ static lv_obj_t* label(lv_obj_t* parent, const char* text, const lv_font_t* font
|
|||||||
lv_obj_set_style_text_color(l, lv_color_hex(color), 0);
|
lv_obj_set_style_text_color(l, lv_color_hex(color), 0);
|
||||||
return l;
|
return l;
|
||||||
}
|
}
|
||||||
|
// A label's text, set only when it differs: LVGL redraws a label on every
|
||||||
|
// set_text, the same text or not, and the refreshes that run every second
|
||||||
|
// mostly write back what's already there.
|
||||||
|
static void setText(lv_obj_t* l, const char* text) {
|
||||||
|
const char* cur = lv_label_get_text(l);
|
||||||
|
if (cur && strcmp(cur, text) == 0) return;
|
||||||
|
lv_label_set_text(l, text);
|
||||||
|
}
|
||||||
|
static void setTextFmt(lv_obj_t* l, const char* fmt, ...) __attribute__((format(printf, 2, 3)));
|
||||||
|
static void setTextFmt(lv_obj_t* l, const char* fmt, ...) {
|
||||||
|
char b[160];
|
||||||
|
va_list ap;
|
||||||
|
va_start(ap, fmt);
|
||||||
|
vsnprintf(b, sizeof(b), fmt, ap);
|
||||||
|
va_end(ap);
|
||||||
|
setText(l, b);
|
||||||
|
}
|
||||||
// A sentence across the parent's width, wrapping: a hint, a note, a status.
|
// A sentence across the parent's width, wrapping: a hint, a note, a status.
|
||||||
static lv_obj_t* noteLabel(lv_obj_t* parent, const char* text, const lv_font_t* font = THEME_FONT_SMALL,
|
static lv_obj_t* noteLabel(lv_obj_t* parent, const char* text, const lv_font_t* font = THEME_FONT_SMALL,
|
||||||
uint32_t color = theme::TEXT_MUTED) {
|
uint32_t color = theme::TEXT_MUTED) {
|
||||||
@@ -296,7 +313,7 @@ static void infoSet(lv_obj_t* value, const char* text) {
|
|||||||
lv_obj_t* row = lv_obj_get_parent(value);
|
lv_obj_t* row = lv_obj_get_parent(value);
|
||||||
if (!text || !text[0]) { lv_obj_add_flag(row, LV_OBJ_FLAG_HIDDEN); return; }
|
if (!text || !text[0]) { lv_obj_add_flag(row, LV_OBJ_FLAG_HIDDEN); return; }
|
||||||
lv_obj_remove_flag(row, LV_OBJ_FLAG_HIDDEN);
|
lv_obj_remove_flag(row, LV_OBJ_FLAG_HIDDEN);
|
||||||
lv_label_set_text(value, text);
|
setText(value, text);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A bare flex row / column sized to its content, taps passing through.
|
// A bare flex row / column sized to its content, taps passing through.
|
||||||
@@ -828,9 +845,9 @@ static void clockFaceSet(lv_obj_t* f, const struct tm* ti, const NodePrefs* p) {
|
|||||||
lv_obj_t* ap = lv_obj_get_child(f, 1);
|
lv_obj_t* ap = lv_obj_get_child(f, 1);
|
||||||
char clk[12] = "--:--";
|
char clk[12] = "--:--";
|
||||||
if (ti) fmtClock(clk, sizeof(clk), *ti, p, false, true);
|
if (ti) fmtClock(clk, sizeof(clk), *ti, p, false, true);
|
||||||
lv_label_set_text(lv_obj_get_child(f, 0), clk);
|
setText(lv_obj_get_child(f, 0), clk);
|
||||||
bool h12 = ti && p && p->clock_12h;
|
bool h12 = ti && p && p->clock_12h;
|
||||||
if (h12) lv_label_set_text(ap, ti->tm_hour < 12 ? "AM" : "PM");
|
if (h12) setText(ap, ti->tm_hour < 12 ? "AM" : "PM");
|
||||||
if (h12 != !lv_obj_has_flag(ap, LV_OBJ_FLAG_HIDDEN)) {
|
if (h12 != !lv_obj_has_flag(ap, LV_OBJ_FLAG_HIDDEN)) {
|
||||||
if (h12) lv_obj_remove_flag(ap, LV_OBJ_FLAG_HIDDEN);
|
if (h12) lv_obj_remove_flag(ap, LV_OBJ_FLAG_HIDDEN);
|
||||||
else lv_obj_add_flag(ap, LV_OBJ_FLAG_HIDDEN);
|
else lv_obj_add_flag(ap, LV_OBJ_FLAG_HIDDEN);
|
||||||
@@ -1110,6 +1127,7 @@ void UITask::loop() {
|
|||||||
uint32_t sig = threadSignature();
|
uint32_t sig = threadSignature();
|
||||||
if (_thread_dirty || sig != _thread_sig) refreshThread();
|
if (_thread_dirty || sig != _thread_sig) refreshThread();
|
||||||
}
|
}
|
||||||
|
lvport::cpuSlow(!cpuNeeded());
|
||||||
lv_next = lv_timer_handler();
|
lv_next = lv_timer_handler();
|
||||||
}
|
}
|
||||||
lvport::idle(idleMillis(lv_next));
|
lvport::idle(idleMillis(lv_next));
|
||||||
@@ -1117,13 +1135,14 @@ void UITask::loop() {
|
|||||||
|
|
||||||
// How long loop() may sleep: nothing is due before then. Not at all while the
|
// How long loop() may sleep: nothing is due before then. Not at all while the
|
||||||
// mesh has packets queued; briefly while a melody plays (its notes are timed
|
// mesh has packets queued; briefly while a melody plays (its notes are timed
|
||||||
// here) or the app is connected (its frames are read one a pass).
|
// here) or frames to or from the app wait (read one a pass, sent spaced out).
|
||||||
|
// A frame arriving from the app over BLE wakes the loop (serialFrameArrived).
|
||||||
uint32_t UITask::idleMillis(uint32_t lv_next) {
|
uint32_t UITask::idleMillis(uint32_t lv_next) {
|
||||||
if (the_mesh.hasPendingWork()) return 0;
|
if (the_mesh.hasPendingWork()) return 0;
|
||||||
#ifdef PIN_BUZZER
|
#ifdef PIN_BUZZER
|
||||||
if (_buzzer.isPlaying()) return 1;
|
if (_buzzer.isPlaying()) return 1;
|
||||||
#endif
|
#endif
|
||||||
if (isClientConnected()) return 2;
|
if (isClientConnected() && _serial->hasPendingFrames()) return 2;
|
||||||
if (_asleep) return 50; // as often as the buttons and tap to wake are polled
|
if (_asleep) return 50; // as often as the buttons and tap to wake are polled
|
||||||
return lv_next < 10 ? lv_next : 10; // LVGL's next timer: a refresh, an animation, input
|
return lv_next < 10 ? lv_next : 10; // LVGL's next timer: a refresh, an animation, input
|
||||||
}
|
}
|
||||||
@@ -1429,9 +1448,9 @@ void UITask::refreshStatusBar() {
|
|||||||
if (localTime(_prefs, ti)) {
|
if (localTime(_prefs, ti)) {
|
||||||
char clk[12];
|
char clk[12];
|
||||||
fmtClock(clk, sizeof(clk), ti, _prefs, true);
|
fmtClock(clk, sizeof(clk), ti, _prefs, true);
|
||||||
lv_label_set_text(_status_time, clk);
|
setText(_status_time, clk);
|
||||||
} else {
|
} else {
|
||||||
lv_label_set_text(_status_time, "--:--");
|
setText(_status_time, "--:--");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Battery: the icon, then % or volts per Settings > Battery display. The
|
// Battery: the icon, then % or volts per Settings > Battery display. The
|
||||||
@@ -1446,7 +1465,7 @@ void UITask::refreshStatusBar() {
|
|||||||
case battery::VOLTAGE: snprintf(level, sizeof(level), " %u.%02u V", mv / 1000, (mv % 1000) / 10); break;
|
case battery::VOLTAGE: snprintf(level, sizeof(level), " %u.%02u V", mv / 1000, (mv % 1000) / 10); break;
|
||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
lv_label_set_text_fmt(_status_batt, "%s%s", batt, level);
|
setTextFmt(_status_batt, "%s%s", batt, level);
|
||||||
lv_obj_t* left_of = _status_batt; // the icons pack up to this
|
lv_obj_t* left_of = _status_batt; // the icons pack up to this
|
||||||
if (_board->isExternalPowered()) {
|
if (_board->isExternalPowered()) {
|
||||||
lv_obj_remove_flag(_status_chg, LV_OBJ_FLAG_HIDDEN);
|
lv_obj_remove_flag(_status_chg, LV_OBJ_FLAG_HIDDEN);
|
||||||
@@ -2246,8 +2265,8 @@ void UITask::refreshScanPopup() {
|
|||||||
if (!_scan_list) return;
|
if (!_scan_list) return;
|
||||||
_scan->refreshScan();
|
_scan->refreshScan();
|
||||||
int n = _scan->count();
|
int n = _scan->count();
|
||||||
if (_scanning) lv_label_set_text_fmt(_scan_status, LV_SYMBOL_REFRESH " Listening for replies... %d", n);
|
if (_scanning) setTextFmt(_scan_status, LV_SYMBOL_REFRESH " Listening for replies... %d", n);
|
||||||
else lv_label_set_text_fmt(_scan_status, "%d node%s answered the discover request", n, n == 1 ? "" : "s");
|
else setTextFmt(_scan_status, "%d node%s answered the discover request", n, n == 1 ? "" : "s");
|
||||||
|
|
||||||
uint32_t sig = (uint32_t)n + (_scanning ? 0x10000u : 0);
|
uint32_t sig = (uint32_t)n + (_scanning ? 0x10000u : 0);
|
||||||
for (int i = 0; i < n; i++) {
|
for (int i = 0; i < n; i++) {
|
||||||
@@ -2309,10 +2328,10 @@ void UITask::refreshNearbyList() {
|
|||||||
int n = _nearby->count();
|
int n = _nearby->count();
|
||||||
|
|
||||||
if (_nearby_sort_lbl)
|
if (_nearby_sort_lbl)
|
||||||
lv_label_set_text(_nearby_sort_lbl, _nearby->sortMode() == NearbyModel::SORT_TIME ? "Recent" : "Dist");
|
setText(_nearby_sort_lbl, _nearby->sortMode() == NearbyModel::SORT_TIME ? "Recent" : "Dist");
|
||||||
int32_t lat, lon;
|
int32_t lat, lon;
|
||||||
bool gps = _nearby->ownPosition(lat, lon);
|
bool gps = _nearby->ownPosition(lat, lon);
|
||||||
lv_label_set_text_fmt(_nearby_status, "%d node%s%s", n, n == 1 ? "" : "s",
|
setTextFmt(_nearby_status, "%d node%s%s", n, n == 1 ? "" : "s",
|
||||||
gps ? "" : " - no GPS fix, distances unknown");
|
gps ? "" : " - no GPS fix, distances unknown");
|
||||||
if (sig == _nearby_sig) return;
|
if (sig == _nearby_sig) return;
|
||||||
_nearby_sig = sig;
|
_nearby_sig = sig;
|
||||||
@@ -2455,12 +2474,12 @@ void UITask::refreshNode() {
|
|||||||
int16_t out = 0, back = 0; uint32_t rtt = 0;
|
int16_t out = 0, back = 0; uint32_t rtt = 0;
|
||||||
_core->ping.getResult(out, back, rtt);
|
_core->ping.getResult(out, back, rtt);
|
||||||
if (!_core->ping.isActive() && (out || back || rtt)) {
|
if (!_core->ping.isActive() && (out || back || rtt)) {
|
||||||
lv_label_set_text_fmt(_node_ping, "Ping %lu ms - SNR out %.1f back %.1f", (unsigned long)rtt,
|
setTextFmt(_node_ping, "Ping %lu ms - SNR out %.1f back %.1f", (unsigned long)rtt,
|
||||||
out / 4.0f, back / 4.0f);
|
out / 4.0f, back / 4.0f);
|
||||||
_pinging = false;
|
_pinging = false;
|
||||||
} else if (millis() - _ping_started_ms > 3000) {
|
} else if (millis() - _ping_started_ms > 3000) {
|
||||||
_core->ping.clear();
|
_core->ping.clear();
|
||||||
lv_label_set_text(_node_ping, "Ping: no reply");
|
setText(_node_ping, "Ping: no reply");
|
||||||
_pinging = false;
|
_pinging = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3459,6 +3478,19 @@ void UITask::setKeyboardAlphabets(int main_idx, int alt_sel) {
|
|||||||
#include "StorageScreen.h"
|
#include "StorageScreen.h"
|
||||||
#include "Splash.h"
|
#include "Splash.h"
|
||||||
|
|
||||||
|
// The screen on, the full CPU clock while it's worth having: touched in the
|
||||||
|
// last CPU_IDLE_MS, something moving, map tiles to decode, a download or an
|
||||||
|
// update on WiFi. Otherwise 80 MHz (lvport::cpuSlow) -- a clock ticking over,
|
||||||
|
// a message being read.
|
||||||
|
static constexpr uint32_t CPU_IDLE_MS = 2000;
|
||||||
|
bool UITask::cpuNeeded() {
|
||||||
|
if (lv_display_get_inactive_time(NULL) < CPU_IDLE_MS || lv_anim_count_running() > 0) return true;
|
||||||
|
if (wifiInUse() || mapview::s_dl.liveQueued() > 0) return true;
|
||||||
|
if (_screen == SCR_MAP && _map_pending) return true;
|
||||||
|
if (_screen == SCR_HOME && home::mini::s_area && home::mini::s_pending) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
#if defined(SIM_PLATFORM) && defined(__EMSCRIPTEN__)
|
#if defined(SIM_PLATFORM) && defined(__EMSCRIPTEN__)
|
||||||
// Sim tests: straight to a screen by name.
|
// Sim tests: straight to a screen by name.
|
||||||
extern "C" EMSCRIPTEN_KEEPALIVE void sim_open(const char* name) {
|
extern "C" EMSCRIPTEN_KEEPALIVE void sim_open(const char* name) {
|
||||||
|
|||||||
@@ -419,6 +419,7 @@ private:
|
|||||||
bool _wake_down = false;
|
bool _wake_down = false;
|
||||||
uint32_t autoOffMillis() const;
|
uint32_t autoOffMillis() const;
|
||||||
uint32_t idleMillis(uint32_t lv_next);
|
uint32_t idleMillis(uint32_t lv_next);
|
||||||
|
bool cpuNeeded();
|
||||||
void checkLowBattery();
|
void checkLowBattery();
|
||||||
|
|
||||||
#ifdef PIN_BUZZER
|
#ifdef PIN_BUZZER
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ void UITask::pollWifiScan() {
|
|||||||
lv_obj_center(label(b, text, THEME_FONT_SMALL, known ? theme::ACCENT : theme::TEXT));
|
lv_obj_center(label(b, text, THEME_FONT_SMALL, known ? theme::ACCENT : theme::TEXT));
|
||||||
}
|
}
|
||||||
if (n > 0) lv_obj_remove_flag(_wifi_list, LV_OBJ_FLAG_HIDDEN);
|
if (n > 0) lv_obj_remove_flag(_wifi_list, LV_OBJ_FLAG_HIDDEN);
|
||||||
lv_label_set_text(_wifi_status, n > 0 ? "Tap a network, then enter its password." : "No networks found.");
|
setText(_wifi_status, n > 0 ? "Tap a network, then enter its password." : "No networks found.");
|
||||||
if (!wifiInUse()) lvport::netEnd(); // the scan switched the radio on
|
if (!wifiInUse()) lvport::netEnd(); // the scan switched the radio on
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ public:
|
|||||||
// isConnected() so single-transport interfaces are unchanged.
|
// isConnected() so single-transport interfaces are unchanged.
|
||||||
virtual bool isClientConnected() const { return isConnected(); }
|
virtual bool isClientConnected() const { return isConnected(); }
|
||||||
|
|
||||||
|
// For a loop that sleeps between passes: frames waiting to be read or sent.
|
||||||
|
// An interface that can't tell says true (poll it often).
|
||||||
|
virtual bool hasPendingFrames() const { return true; }
|
||||||
|
|
||||||
virtual bool isWriteBusy() const = 0;
|
virtual bool isWriteBusy() const = 0;
|
||||||
virtual size_t writeFrame(const uint8_t src[], size_t len) = 0;
|
virtual size_t writeFrame(const uint8_t src[], size_t len) = 0;
|
||||||
virtual size_t checkRecvFrame(uint8_t dest[]) = 0;
|
virtual size_t checkRecvFrame(uint8_t dest[]) = 0;
|
||||||
|
|||||||
@@ -52,6 +52,11 @@ public:
|
|||||||
bool isClientConnected() const override { return isBLEConnected(); }
|
bool isClientConnected() const override { return isBLEConnected(); }
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// USB can't tell without reading it.
|
||||||
|
bool hasPendingFrames() const override {
|
||||||
|
return (_ble_enabled && _ble.isConnected()) ? _ble.hasPendingFrames() : true;
|
||||||
|
}
|
||||||
|
|
||||||
bool isWriteBusy() const override {
|
bool isWriteBusy() const override {
|
||||||
return (_ble_enabled && _ble.isConnected()) ? _ble.isWriteBusy() : _usb.isWriteBusy();
|
return (_ble_enabled && _ble.isConnected()) ? _ble.isWriteBusy() : _usb.isWriteBusy();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
|
|
||||||
#define ADVERT_RESTART_DELAY 1000 // millis
|
#define ADVERT_RESTART_DELAY 1000 // millis
|
||||||
|
|
||||||
|
__attribute__((weak)) void serialFrameArrived() {}
|
||||||
|
|
||||||
// As the nRF52 interface: 20-30 ms for the first 30 s (the app finds it at
|
// As the nRF52 interface: 20-30 ms for the first 30 s (the app finds it at
|
||||||
// once), then 152.5 ms. Left alone, the library advertises every 30-60 ms
|
// once), then 152.5 ms. Left alone, the library advertises every 30-60 ms
|
||||||
// for as long as nothing is connected. Units of 0.625 ms.
|
// for as long as nothing is connected. Units of 0.625 ms.
|
||||||
@@ -187,6 +189,7 @@ void SerialBLEInterface::onWrite(BLECharacteristic* pCharacteristic, esp_ble_gat
|
|||||||
if (xQueueSend(recv_queue, &frame, 0) != pdTRUE) {
|
if (xQueueSend(recv_queue, &frame, 0) != pdTRUE) {
|
||||||
BLE_DEBUG_PRINTLN("ERROR: onWrite(), recv_queue is full!");
|
BLE_DEBUG_PRINTLN("ERROR: onWrite(), recv_queue is full!");
|
||||||
}
|
}
|
||||||
|
serialFrameArrived();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
#include <freertos/FreeRTOS.h>
|
#include <freertos/FreeRTOS.h>
|
||||||
#include <freertos/queue.h>
|
#include <freertos/queue.h>
|
||||||
|
|
||||||
|
// A frame from the app was queued (the BLE stack's task). Weak, empty by
|
||||||
|
// default: a firmware whose loop blocks between passes defines it to wake it.
|
||||||
|
void serialFrameArrived();
|
||||||
|
|
||||||
class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLEServerCallbacks, BLECharacteristicCallbacks {
|
class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLEServerCallbacks, BLECharacteristicCallbacks {
|
||||||
BLEServer *pServer;
|
BLEServer *pServer;
|
||||||
BLEService *pService;
|
BLEService *pService;
|
||||||
@@ -102,6 +106,7 @@ public:
|
|||||||
|
|
||||||
bool isConnected() const override;
|
bool isConnected() const override;
|
||||||
|
|
||||||
|
bool hasPendingFrames() const override { return send_queue_len > 0 || uxQueueMessagesWaiting(recv_queue) > 0; }
|
||||||
bool isWriteBusy() const override;
|
bool isWriteBusy() const override;
|
||||||
size_t writeFrame(const uint8_t src[], size_t len) override;
|
size_t writeFrame(const uint8_t src[], size_t len) override;
|
||||||
size_t checkRecvFrame(uint8_t dest[]) override;
|
size_t checkRecvFrame(uint8_t dest[]) override;
|
||||||
|
|||||||
@@ -182,6 +182,9 @@ public:
|
|||||||
|
|
||||||
// direct access to the LGFX device (LVGL flush/touch glue)
|
// direct access to the LGFX device (LVGL flush/touch glue)
|
||||||
lgfx::LGFX_Device* lgfxDevice() { return &disp; }
|
lgfx::LGFX_Device* lgfxDevice() { return &disp; }
|
||||||
|
// LVGL draws straight to the panel: the frame sprite (76 KB of PSRAM) only
|
||||||
|
// carried the boot "Loading..." -- given back once LVGL takes over.
|
||||||
|
void releaseFrameBuffer() { buffer.deleteSprite(); }
|
||||||
|
|
||||||
// reliable touch read in logical (UI_ZOOM-scaled) coords; returns false when
|
// reliable touch read in logical (UI_ZOOM-scaled) coords; returns false when
|
||||||
// not touched (LGFXDisplay::getTouch reads an uninitialized point on release)
|
// not touched (LGFXDisplay::getTouch reads an uninitialized point on release)
|
||||||
|
|||||||
Reference in New Issue
Block a user