perf(l2): heavy screens build in pieces over several loop passes

The list fill becomes a small job queue (fill/then/fillStart, fillTick with a
25 ms budget per pass). Settings pages build section by section in
schemaRows, with the trailing extras (melodies, prune, quick messages) queued
after them so the order stays; Radio and Repeater build by group, Admin's
repeater pick like the contact list, Diag's Live tab by section. Rebuilds
that restore the scroll flush the queue first. Screens went from 220-360 ms
to 80-175 ms (build + first frame).

Diag's crash line read the core dump in flash every second (~44 ms each);
it is read once now. The perf run times every page and dismisses the USB
popup, which had covered all earlier measurements.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-09-30 08:53:15 +02:00
co-authored by Claude Opus 5.5
parent 34f739c45b
commit 039485bf34
8 changed files with 205 additions and 81 deletions
+17 -7
View File
@@ -68,6 +68,7 @@ static void onAdminPickRow(lv_event_t* e) { s_ui->adminPick((int)(uintptr_t)lv_
namespace adminview { namespace adminview {
static const int PICK_MAX = 128; static const int PICK_MAX = 128;
static uint8_t (*s_pick)[PUB_KEY_SIZE] = psramBuf<uint8_t[PUB_KEY_SIZE]>(PICK_MAX); static uint8_t (*s_pick)[PUB_KEY_SIZE] = psramBuf<uint8_t[PUB_KEY_SIZE]>(PICK_MAX);
static uint16_t* s_pick_raw = psramBuf<uint16_t>(PICK_MAX); // their raw table index, for the row
} }
// Home > Admin: every repeater and room server, favourites first (ui-new's // Home > Admin: every repeater and room server, favourites first (ui-new's
@@ -80,27 +81,36 @@ void UITask::showAdminPick() {
void UITask::buildAdminPick() { void UITask::buildAdminPick() {
lv_obj_t* body = newScreen("Admin", true); lv_obj_t* body = newScreen("Admin", true);
int rows = 0; int rows = 0;
for (int pass = 0; pass < 2; pass++) { for (int pass = 0; pass < 2; pass++) { // favourites first
for (int i = 0; i < the_mesh.getNumContacts() && rows < adminview::PICK_MAX; i++) { for (int i = 0; i < the_mesh.getNumContacts() && rows < adminview::PICK_MAX; i++) {
ContactInfo c; ContactInfo c;
if (!the_mesh.getContactByIdx(MAX_ANON_CONTACTS + i, c)) continue; if (!the_mesh.getContactByIdx(MAX_ANON_CONTACTS + i, c)) continue;
if (c.type != ADV_TYPE_REPEATER && c.type != ADV_TYPE_ROOM) continue; if (c.type != ADV_TYPE_REPEATER && c.type != ADV_TYPE_ROOM) continue;
bool fav = contactctl::favourite(c); if (contactctl::favourite(c) != (pass == 0)) continue;
if (fav != (pass == 0)) continue;
if (!rows) sectionTitle(body, "LOG IN TO");
char name[48];
snprintf(name, sizeof(name), "%s%s", fav ? UI_SYMBOL_STAR " " : "", c.name);
memcpy(adminview::s_pick[rows], c.id.pub_key, PUB_KEY_SIZE); memcpy(adminview::s_pick[rows], c.id.pub_key, PUB_KEY_SIZE);
listRow(body, name, c.type == ADV_TYPE_ROOM ? "Room server" : "Repeater", onAdminPickRow, (void*)(uintptr_t)rows); adminview::s_pick_raw[rows] = (uint16_t)(MAX_ANON_CONTACTS + i);
rows++; rows++;
} }
} }
if (rows) sectionTitle(body, "LOG IN TO");
fillStart(rows, 8, &UITask::adminPickRow); // the rest as the loop goes (fill)
if (!rows) { if (!rows) {
noteLabel(body, "No repeaters or room servers yet. They show up here once their advert is heard.", noteLabel(body, "No repeaters or room servers yet. They show up here once their advert is heard.",
THEME_FONT_BODY, theme::TEXT_MUTED); THEME_FONT_BODY, theme::TEXT_MUTED);
} }
} }
void UITask::adminPickRow(int i) {
ContactInfo c;
if (!the_mesh.getContactByIdx(adminview::s_pick_raw[i], c) || memcmp(c.id.pub_key, adminview::s_pick[i], PUB_KEY_SIZE) != 0) {
listRow(_body, "?", NULL, onAdminPickRow, (void*)(uintptr_t)i); // deleted meanwhile
return;
}
char name[48];
snprintf(name, sizeof(name), "%s%s", contactctl::favourite(c) ? UI_SYMBOL_STAR " " : "", c.name);
listRow(_body, name, c.type == ADV_TYPE_ROOM ? "Room server" : "Repeater", onAdminPickRow, (void*)(uintptr_t)i);
}
void UITask::adminPick(int row) { void UITask::adminPick(int row) {
if (row < 0 || row >= adminview::PICK_MAX) return; if (row < 0 || row >= adminview::PICK_MAX) return;
openAdmin(adminview::s_pick[row]); openAdmin(adminview::s_pick[row]);
@@ -391,6 +391,7 @@ void UITask::pinRemove() {
void UITask::pinRowRefresh() { void UITask::pinRowRefresh() {
if (_screen != SCR_SETTINGS_NAV) return; if (_screen != SCR_SETTINGS_NAV) return;
buildSchemaSettings(); buildSchemaSettings();
fillFlush();
if (_body) { lv_obj_update_layout(_body); lv_obj_scroll_by(_body, 0, -lv_obj_get_scroll_bottom(_body), LV_ANIM_OFF); } if (_body) { lv_obj_update_layout(_body); lv_obj_scroll_by(_body, 0, -lv_obj_get_scroll_bottom(_body), LV_ANIM_OFF); }
} }
+25 -17
View File
@@ -87,6 +87,7 @@ static void showValue(const char* core, const char* v, char* out, size_t n) {
static void setLive(int i, const diag::Row& r) { static void setLive(int i, const diag::Row& r) {
LiveVal& lv = s_vals[i]; LiveVal& lv = s_vals[i];
if (!lv.in && !lv.val) return; // its section isn't built yet (fill)
if (lv.in) { if (lv.in) {
const char* slash = strchr(r.value, '/'); const char* slash = strchr(r.value, '/');
char a[12]; char a[12];
@@ -260,25 +261,11 @@ void UITask::buildDiag() {
s_rows = 0; s_rows = 0;
if (s_tab == TAB_LIVE) { if (s_tab == TAB_LIVE) {
// Section by section, in ui-core's order within each. // Section by section, the first at once, the others as the loop goes (fill).
diag::Row rows[diag::MAX_ROWS + EXTRA]; diag::Row rows[diag::MAX_ROWS + EXTRA];
s_rows = allRows(rows, _core->gpsEnabled()); s_rows = allRows(rows, _core->gpsEnabled());
for (uint8_t sec = 0; sec < SEC_COUNT; sec++) { for (int i = 0; i < s_rows; i++) s_vals[i] = { nullptr, nullptr, nullptr };
lv_obj_t* card = nullptr; fillStart(SEC_COUNT, 1, &UITask::diagSection);
for (int i = 0; i < s_rows; i++) {
const Name* nm = nameOf(rows[i].label);
if ((nm ? nm->sec : SEC_DEVICE) != sec) continue;
if (!card) {
sectionTitle(s_list, SEC_TITLE[sec]);
card = infoCard(s_list);
if (sec == SEC_PACKETS) { lv_obj_t *a, *b; pairLine(card, "", &a, &b, true); }
}
s_vals[i] = { nullptr, nullptr, nullptr };
if (nm && nm->pair) pairLine(card, nm->text, &s_vals[i].in, &s_vals[i].out, false);
else s_vals[i].val = infoRow(card, nm ? nm->text : rows[i].label, "");
setLive(i, rows[i]);
}
}
return; return;
} }
@@ -351,6 +338,27 @@ void UITask::buildDiag() {
} }
// From loop(), once a second: the Live values in place. // From loop(), once a second: the Live values in place.
// One Live section, in ui-core's order within it.
void UITask::diagSection(int sec) {
using namespace diagview;
diag::Row rows[diag::MAX_ROWS + EXTRA];
int n = allRows(rows, _core->gpsEnabled());
if (n != s_rows) return; // changed meanwhile: refreshDiag() builds it again
lv_obj_t* card = nullptr;
for (int i = 0; i < n; i++) {
const Name* nm = nameOf(rows[i].label);
if ((nm ? nm->sec : SEC_DEVICE) != sec) continue;
if (!card) {
sectionTitle(s_list, SEC_TITLE[sec]);
card = infoCard(s_list);
if (sec == SEC_PACKETS) { lv_obj_t *a, *b; pairLine(card, "", &a, &b, true); }
}
if (nm && nm->pair) pairLine(card, nm->text, &s_vals[i].in, &s_vals[i].out, false);
else s_vals[i].val = infoRow(card, nm ? nm->text : rows[i].label, "");
setLive(i, rows[i]);
}
}
void UITask::refreshDiag() { void UITask::refreshDiag() {
using namespace diagview; using namespace diagview;
if (_screen != SCR_DIAG || s_tab != TAB_LIVE || _nav_overlay) return; if (_screen != SCR_DIAG || s_tab != TAB_LIVE || _nav_overlay) return;
+10 -4
View File
@@ -444,11 +444,17 @@ static const char* resetReason() {
// the address with the build's firmware.elf, or read the whole dump over USB). // the address with the build's firmware.elf, or read the whole dump over USB).
static bool crashSummary(char* out, size_t n) { static bool crashSummary(char* out, size_t n) {
#if ESP_ARDUINO_VERSION_MAJOR >= 3 #if ESP_ARDUINO_VERSION_MAJOR >= 3
if (esp_core_dump_image_check() != ESP_OK) return false; // Read once: checking the dump in flash takes ~45 ms, and it stays the same
// until the next restart (Diagnostics asks every second).
static int8_t s_have = -1;
static char s_sum[24];
if (s_have < 0) {
esp_core_dump_summary_t sum; esp_core_dump_summary_t sum;
if (esp_core_dump_get_summary(&sum) != ESP_OK) return false; s_have = esp_core_dump_image_check() == ESP_OK && esp_core_dump_get_summary(&sum) == ESP_OK;
snprintf(out, n, "%.10s %08lx", sum.exc_task, (unsigned long)sum.exc_pc); if (s_have) snprintf(s_sum, sizeof(s_sum), "%.10s %08lx", sum.exc_task, (unsigned long)sum.exc_pc);
return true; }
if (s_have) snprintf(out, n, "%s", s_sum);
return s_have;
#else #else
(void)out; (void)n; (void)out; (void)n;
return false; return false;
@@ -96,8 +96,14 @@ void UITask::buildRadio() {
paramRows(g, p, radioctl::currentPreset(p), p->freq, p->sf, p->bw, p->cr, onRadioDropdown, IDS, onRadioFreq, paramRows(g, p, radioctl::currentPreset(p), p->freq, p->sf, p->bw, p->cr, onRadioDropdown, IDS, onRadioFreq,
"Higher = longer range"); "Higher = longer range");
groupNote(body, "Everyone you talk to needs the same settings."); groupNote(body, "Everyone you talk to needs the same settings.");
fill(&UITask::radioGroup, 1, 3); // the rest as the loop goes
}
g = group(body, "TRANSMIT"); void UITask::radioGroup(int i) {
using namespace radioview;
NodePrefs* p = _prefs;
if (i == 2) { buildRadioExtras(_body); return; } // my presets, scopes (RadioExtras.h)
lv_obj_t* g = group(_body, "TRANSMIT");
int o = 0; int o = 0;
for (int v = TX_MIN; v <= TX_MAX; v++) o += snprintf(s_opts + o, OPTS_LEN - o, v > TX_MIN ? "\n%d dBm" : "%d dBm", v); for (int v = TX_MIN; v <= TX_MAX; v++) o += snprintf(s_opts + o, OPTS_LEN - o, v > TX_MIN ? "\n%d dBm" : "%d dBm", v);
int tx = p->tx_power_dbm < TX_MIN ? TX_MIN : p->tx_power_dbm > TX_MAX ? TX_MAX : p->tx_power_dbm; int tx = p->tx_power_dbm < TX_MIN ? TX_MIN : p->tx_power_dbm > TX_MAX ? TX_MAX : p->tx_power_dbm;
@@ -107,8 +113,6 @@ void UITask::buildRadio() {
if (p->tx_apc) lv_obj_add_state(sw, LV_STATE_CHECKED); if (p->tx_apc) lv_obj_add_state(sw, LV_STATE_CHECKED);
if (p->client_repeat) lv_obj_add_state(sw, LV_STATE_DISABLED); if (p->client_repeat) lv_obj_add_state(sw, LV_STATE_DISABLED);
lv_obj_add_event_cb(sw, onRadioSwitch, LV_EVENT_VALUE_CHANGED, (void*)(uintptr_t)R_APC); lv_obj_add_event_cb(sw, onRadioSwitch, LV_EVENT_VALUE_CHANGED, (void*)(uintptr_t)R_APC);
buildRadioExtras(body); // my presets, scopes (RadioExtras.h)
} }
void UITask::radioSet(int which, int v) { void UITask::radioSet(int which, int v) {
@@ -134,6 +138,7 @@ void UITask::radioSet(int which, int v) {
void UITask::rebuildRadio() { void UITask::rebuildRadio() {
int32_t y = _body ? lv_obj_get_scroll_y(_body) : 0; int32_t y = _body ? lv_obj_get_scroll_y(_body) : 0;
buildRadio(); buildRadio();
fillFlush();
if (_body) { lv_obj_update_layout(_body); lv_obj_scroll_to_y(_body, y, LV_ANIM_OFF); } if (_body) { lv_obj_update_layout(_body); lv_obj_scroll_to_y(_body, y, LV_ANIM_OFF); }
} }
@@ -48,8 +48,8 @@ void UITask::showRepeater() {
void UITask::buildRepeater() { void UITask::buildRepeater() {
using namespace rptview; using namespace rptview;
using radioview::rowDropdown;
lv_obj_t* body = newScreen("Repeater", true); lv_obj_t* body = newScreen("Repeater", true);
s_scopes_sub = nullptr; // made with its group (repeaterRelays)
NodePrefs* p = _prefs; NodePrefs* p = _prefs;
lv_obj_t* g = group(body, nullptr); lv_obj_t* g = group(body, nullptr);
@@ -57,8 +57,14 @@ void UITask::buildRepeater() {
if (p->client_repeat) lv_obj_add_state(sw, LV_STATE_CHECKED); if (p->client_repeat) lv_obj_add_state(sw, LV_STATE_CHECKED);
lv_obj_add_event_cb(sw, onRptSwitch, LV_EVENT_VALUE_CHANGED, (void*)(uintptr_t)RP_ON); lv_obj_add_event_cb(sw, onRptSwitch, LV_EVENT_VALUE_CHANGED, (void*)(uintptr_t)RP_ON);
groupNote(body, "Uses more battery; auto power pauses."); groupNote(body, "Uses more battery; auto power pauses.");
fill(&UITask::repeaterGroup, 1, 3); // the rest as the loop goes
}
g = group(body, "NETWORK"); void UITask::repeaterGroup(int i) {
using namespace rptview;
if (i == 2) { repeaterRelays(); return; }
NodePrefs* p = _prefs;
lv_obj_t* g = group(_body, "NETWORK");
lv_obj_t* row = settingRow(g, "Relay on", p->repeater_use_profile ? "Its own frequency" : "Your chat frequency"); lv_obj_t* row = settingRow(g, "Relay on", p->repeater_use_profile ? "Its own frequency" : "Your chat frequency");
static const char* NET[] = { "Current", "Custom", "" }; static const char* NET[] = { "Current", "Custom", "" };
radioview::rowSegmented(row, NET, p->repeater_use_profile ? 1 : 0, 150, onRptNetwork, 0); radioview::rowSegmented(row, NET, p->repeater_use_profile ? 1 : 0, 150, onRptNetwork, 0);
@@ -67,8 +73,13 @@ void UITask::buildRepeater() {
radioview::paramRows(g, p, rptctl::currentPreset(p), p->repeater_freq, p->repeater_sf, p->repeater_bw, radioview::paramRows(g, p, rptctl::currentPreset(p), p->repeater_freq, p->repeater_sf, p->repeater_bw,
p->repeater_cr, onRptDropdown, IDS, onRptFreq, nullptr); p->repeater_cr, onRptDropdown, IDS, onRptFreq, nullptr);
} }
}
g = group(body, "WHAT IT RELAYS"); void UITask::repeaterRelays() {
using namespace rptview;
using radioview::rowDropdown;
NodePrefs* p = _prefs;
lv_obj_t* g = group(_body, "WHAT IT RELAYS");
switchRow(g, "Skip adverts", "Relay messages, not adverts", &p->repeat_skip_adverts); switchRow(g, "Skip adverts", "Relay messages, not adverts", &p->repeat_skip_adverts);
char v[12]; char v[12];
int o = 0; int o = 0;
@@ -102,6 +113,7 @@ void UITask::buildRepeater() {
void UITask::rebuildRepeater() { void UITask::rebuildRepeater() {
int32_t y = _body ? lv_obj_get_scroll_y(_body) : 0; int32_t y = _body ? lv_obj_get_scroll_y(_body) : 0;
buildRepeater(); buildRepeater();
fillFlush();
if (_body) { lv_obj_update_layout(_body); lv_obj_scroll_to_y(_body, y, LV_ANIM_OFF); } if (_body) { lv_obj_update_layout(_body); lv_obj_scroll_to_y(_body, y, LV_ANIM_OFF); }
} }
+114 -44
View File
@@ -1060,6 +1060,32 @@ void UITask::loop() {
lv_display_trigger_activity(NULL); // no sleeping mid-test lv_display_trigger_activity(NULL); // no sleeping mid-test
next = millis() + 1200; next = millis() + 1200;
if (step == 0) unlockScreen(); // a PIN lock would be drawn over everything if (step == 0) unlockScreen(); // a PIN lock would be drawn over everything
usbTap(false); // nor the USB drive question ("Keep card")
if (step == 0) { // once: every settings page and most screens, built and drawn once
auto one = [this](const char* what, int page, void (UITask::*fn)()) {
uint32_t t = micros();
if (fn) (this->*fn)(); else showSchemaSettings(page);
uint32_t b = micros() - t;
lv_refr_now(NULL);
uint32_t n = 0;
lv_obj_tree_walk(lv_screen_active(), [](lv_obj_t*, void* u) { ++*(uint32_t*)u; return LV_OBJ_TREE_WALK_NEXT; }, &n);
Serial.printf("PERF screen %-12s %2d build %5.1f ms, with first frame %5.1f ms, %3lu objects\n", what, page,
b / 1000.0f, (micros() - t) / 1000.0f, (unsigned long)n);
};
for (int pg = 0; pg < PG_ALL; pg++) one("page", pg, nullptr);
one("radio", 0, &UITask::showRadio);
one("diag", 0, &UITask::showDiag);
one("clock", 0, &UITask::showClock);
one("compass", 0, &UITask::showCompass);
one("scopes", 0, &UITask::showScopes);
one("repeater", 0, &UITask::showRepeater);
one("bot", 0, &UITask::showBot);
one("quick", 0, &UITask::showQuickMsgs);
one("admin", 0, &UITask::showAdminPick);
one("contacts", 0, &UITask::showContacts);
one("chats", 0, &UITask::showChats);
showHome();
}
if (step % 10 == 0 && step > 0) { // a static full-screen redraw of Home, for the baseline if (step % 10 == 0 && step > 0) { // a static full-screen redraw of Home, for the baseline
uint32_t fl0 = lvport::s_flush_us, t = micros(); uint32_t fl0 = lvport::s_flush_us, t = micros();
for (int i = 0; i < 10; i++) { lv_obj_invalidate(lv_screen_active()); lv_refr_now(NULL); } for (int i = 0; i < 10; i++) { lv_obj_invalidate(lv_screen_active()); lv_refr_now(NULL); }
@@ -1684,37 +1710,62 @@ namespace home { static void leave(); } // HomeScreen.h
static int s_th_rows_n = 0; // conversation bubbles built (refreshThread) static int s_th_rows_n = 0; // conversation bubbles built (refreshThread)
// A long list fills in over several loop passes: the first screenful at // A screen builds in pieces over several loop passes: what shows first at
// once, the rest as time allows. A row costs 3-6 ms on the L2 (widgets live in // once, the rest as time allows. A widget costs ~1 ms on the L2 (they live in
// PSRAM), so 48 nodes or a few hundred contacts built in one go held the // PSRAM), so 48 nodes, a few hundred contacts or a long settings page built
// screen for a second. `row(i)` makes row i from the data as it is then. // in one go held the screen for up to a second. The pieces queue in order:
// One list at a time; a new screen drops it. // fill(fn, from, to) makes fn(from) .. fn(to - 1), each from the data as it
static struct { // is then; then(fn, arg) makes its piece at once unless others still wait,
lv_obj_t* list = nullptr; // keeping its place after them. A new screen drops what is left.
int next = 0, n = 0; struct FillJob { void (UITask::*fn)(int); int next, end; };
void (UITask::*row)(int) = nullptr; static const int FILL_JOBS = 12;
} s_fill; static FillJob s_fill[FILL_JOBS];
static const uint32_t FILL_BUDGET_US = 25000; // rows added per loop pass: about a frame's worth static int s_fill_n = 0;
static const uint32_t FILL_BUDGET_US = 25000; // pieces made per loop pass: about a frame's worth
void UITask::fillStart(lv_obj_t* list, int n, int first, void (UITask::*row)(int)) { void UITask::fill(void (UITask::*fn)(int), int from, int to) {
s_fill.list = list; if (from >= to) return;
s_fill.n = n; if (s_fill_n == FILL_JOBS) { while (from < to) (this->*fn)(from++); return; } // no room: now
s_fill.row = row; s_fill[s_fill_n++] = { fn, from, to };
s_fill.next = 0; }
while (s_fill.next < n && s_fill.next < first) (this->*row)(s_fill.next++);
if (s_fill.next >= n) s_fill.list = nullptr; void UITask::then(void (UITask::*fn)(int), int arg) {
if (s_fill_n) fill(fn, arg, arg + 1);
else (this->*fn)(arg);
}
// Rows 0 .. n-1 of a list: the first `first` at once, the rest queued.
void UITask::fillStart(int n, int first, void (UITask::*row)(int)) {
int now = first < n ? first : n;
for (int i = 0; i < now; i++) (this->*row)(i);
fill(row, now, n);
}
void UITask::fillStep() {
FillJob& j = s_fill[0];
(this->*j.fn)(j.next++); // may queue more, behind
if (j.next >= j.end) memmove(s_fill, s_fill + 1, --s_fill_n * sizeof(FillJob));
} }
void UITask::fillTick() { void UITask::fillTick() {
if (!s_fill.list) return;
uint32_t t0 = micros(); uint32_t t0 = micros();
do (this->*s_fill.row)(s_fill.next++); while (s_fill_n) {
while (s_fill.next < s_fill.n && micros() - t0 < FILL_BUDGET_US); fillStep();
if (s_fill.next >= s_fill.n) s_fill.list = nullptr; if (micros() - t0 >= FILL_BUDGET_US) break;
}
}
// Everything still queued, now: before scrolling to where the screen was.
void UITask::fillFlush() { while (s_fill_n) fillStep(); }
int UITask::fillPending() const {
int n = 0;
for (int k = 0; k < s_fill_n; k++) n += s_fill[k].end - s_fill[k].next;
return n;
} }
lv_obj_t* UITask::newScreen(const char* title, bool with_back) { lv_obj_t* UITask::newScreen(const char* title, bool with_back) {
s_fill.list = nullptr; // its list goes with the screen s_fill_n = 0; // what was still to be built goes with the screen
_home_clock = _home_date = _home_unread = nullptr; _home_clock = _home_date = _home_unread = nullptr;
_thread_list = _compose_ta = _keyboard = nullptr; _thread_list = _compose_ta = _keyboard = nullptr;
s_th_rows_n = 0; // the bubbles went with the screen s_th_rows_n = 0; // the bubbles went with the screen
@@ -2187,14 +2238,14 @@ void UITask::buildContacts() {
s_contact_raw[rows] = (uint16_t)(MAX_ANON_CONTACTS + i); s_contact_raw[rows] = (uint16_t)(MAX_ANON_CONTACTS + i);
rows++; rows++;
} }
fillStart(body, rows, 8, &UITask::contactRow); fillStart(rows, 8, &UITask::contactRow);
if (rows == 0) label(body, fav_only ? "No favourites - tap All" : "No contacts yet", THEME_FONT_BODY, theme::TEXT_MUTED); if (rows == 0) label(body, fav_only ? "No favourites - tap All" : "No contacts yet", THEME_FONT_BODY, theme::TEXT_MUTED);
} }
void UITask::contactRow(int i) { void UITask::contactRow(int i) {
ContactInfo c; ContactInfo c;
bool same = the_mesh.getContactByIdx(s_contact_raw[i], c) && memcmp(c.id.pub_key, s_contact_rows[i], PUB_KEY_SIZE) == 0; bool same = the_mesh.getContactByIdx(s_contact_raw[i], c) && memcmp(c.id.pub_key, s_contact_rows[i], PUB_KEY_SIZE) == 0;
listRow(s_fill.list, same ? c.name : "?", NULL, onOpenContactRow, (void*)(uintptr_t)i); // "?": deleted meanwhile listRow(_body, same ? c.name : "?", NULL, onOpenContactRow, (void*)(uintptr_t)i); // "?": deleted meanwhile
} }
// ── Nearby ──────────────────────────────────────────────────────────────────── // ── Nearby ────────────────────────────────────────────────────────────────────
@@ -2450,14 +2501,14 @@ void UITask::refreshNearbyList() {
// change): the rows rewritten in place, not built again. Rows still to // change): the rows rewritten in place, not built again. Rows still to
// come while the list fills in are made from the model as it is then. // come while the list fills in are made from the model as it is then.
int built = (int)lv_obj_get_child_count(_nearby_list); int built = (int)lv_obj_get_child_count(_nearby_list);
int rows = s_fill.list == _nearby_list ? s_fill.n : built; if (n > 0 && built > 0 && built + fillPending() == n && lv_obj_check_type(lv_obj_get_child(_nearby_list, 0), &lv_button_class)) {
if (n > 0 && built > 0 && rows == n && lv_obj_check_type(lv_obj_get_child(_nearby_list, 0), &lv_button_class)) {
for (int i = 0; i < built; i++) nearbyRowSet(lv_obj_get_child(_nearby_list, i), i); for (int i = 0; i < built; i++) nearbyRowSet(lv_obj_get_child(_nearby_list, i), i);
return; return;
} }
int32_t scroll = lv_obj_get_scroll_y(_nearby_list); int32_t scroll = lv_obj_get_scroll_y(_nearby_list);
s_fill_n = 0; // rows still queued: of the old list
lv_obj_clean(_nearby_list); lv_obj_clean(_nearby_list);
fillStart(_nearby_list, n, 6 + scroll / (theme::ROW_H + theme::GAP), &UITask::nearbyRow); // down to where it was fillStart(n, 6 + scroll / (theme::ROW_H + theme::GAP), &UITask::nearbyRow); // down to where it was
if (n == 0) { if (n == 0) {
lv_obj_t* l = label(_nearby_list, "Nobody here yet. Tap Scan to look around.", lv_obj_t* l = label(_nearby_list, "Nobody here yet. Tap Scan to look around.",
THEME_FONT_BODY, theme::TEXT_MUTED); THEME_FONT_BODY, theme::TEXT_MUTED);
@@ -3433,13 +3484,19 @@ void UITask::buildSchemaSettings() {
} }
} }
schemaRows(body, _settings_page); schemaRows(body, _settings_page);
if (_settings_page == settings::PG_SOUND) buildSoundRows(body, false); // the melodies if (_settings_page == settings::PG_SOUND) then(&UITask::schemaExtras, settings::PG_SOUND); // after its groups
if (_settings_page == settings::PG_MESSAGES) { if (_settings_page == settings::PG_MESSAGES) then(&UITask::schemaExtras, settings::PG_MESSAGES);
}
// What a page adds to its schema groups, once they are there.
void UITask::schemaExtras(int page) {
if (page == settings::PG_SOUND) buildSoundRows(_body, false); // the melodies
if (page == settings::PG_MESSAGES) {
lv_obj_t* c = s_sec_card[settings::SEC_CONTACTS]; // the action that goes with "Contact expiry" lv_obj_t* c = s_sec_card[settings::SEC_CONTACTS]; // the action that goes with "Contact expiry"
if (c) _prune_lbl = actionRow(c, LV_SYMBOL_TRASH " Remove inactive contacts now", onPruneContacts, NULL, theme::FAIL); if (c) _prune_lbl = actionRow(c, LV_SYMBOL_TRASH " Remove inactive contacts now", onPruneContacts, NULL, theme::FAIL);
char sub[48]; char sub[48];
snprintf(sub, sizeof(sub), "%d of %d set - sent with one tap", msgtext::quickUsed(_prefs), msgtext::QUICK_COUNT); snprintf(sub, sizeof(sub), "%d of %d set - sent with one tap", msgtext::quickUsed(_prefs), msgtext::QUICK_COUNT);
listRow(s_sec_card[settings::SEC_MESSAGES], "Quick messages", sub, onOpenQuickMsgs, NULL); if (lv_obj_t* m = s_sec_card[settings::SEC_MESSAGES]) listRow(m, "Quick messages", sub, onOpenQuickMsgs, NULL);
} }
} }
@@ -3474,22 +3531,34 @@ void UITask::buildAboutPage(lv_obj_t* body) {
// A page's schema rows, one group per section (Settings pages and the tools' // A page's schema rows, one group per section (Settings pages and the tools'
// options). Each section's card is kept in s_sec_card for rows added after. // options). Each section's card is kept in s_sec_card for rows added after.
// On a screen's body the first group shows at once and the others follow
// (fill); a popup's list gets them all now.
static lv_obj_t* s_schema_body = nullptr;
static bool s_schema_one = false; // one section: its own page, no heading
void UITask::schemaRows(lv_obj_t* body, uint8_t page) { void UITask::schemaRows(lv_obj_t* body, uint8_t page) {
uint8_t sec = 0xFF;
lv_obj_t* card = nullptr;
for (lv_obj_t*& c : s_sec_card) c = nullptr; for (lv_obj_t*& c : s_sec_card) c = nullptr;
s_schema_body = body;
s_schema_one = page == settings::PG_NAV && s_nav_section >= 0;
uint8_t sec = 0xFF;
bool first = true;
for (int i = 0; i < settings::COUNT; i++) { for (int i = 0; i < settings::COUNT; i++) {
const settings::Setting& st = settings::ALL[i]; const settings::Setting& st = settings::ALL[i];
if (settings::sectionPage(st.section) != page) continue; if (settings::sectionPage(st.section) != page || st.section == sec) continue;
bool one = page == settings::PG_NAV && s_nav_section >= 0; // one section: its own page, no heading if (s_schema_one && st.section != s_nav_section) continue;
if (one && st.section != s_nav_section) continue;
if (st.section != sec) {
sec = st.section; sec = st.section;
card = s_sec_card[sec] = group(body, one ? nullptr : settings::sectionTitle(sec)); if (first || body != _body) schemaSection(i);
else fill(&UITask::schemaSection, i, i + 1);
first = false;
}
}
// The group of the section setting i starts, with its rows.
void UITask::schemaSection(int i) {
uint8_t sec = settings::ALL[i].section;
lv_obj_t* card = s_sec_card[sec] = group(s_schema_body, s_schema_one ? nullptr : settings::sectionTitle(sec));
if (sec == settings::SEC_SOUND) buildSoundRows(card, true); // On / Off / Auto if (sec == settings::SEC_SOUND) buildSoundRows(card, true); // On / Off / Auto
} for (int k = i; k < settings::COUNT && settings::ALL[k].section == sec; k++) schemaRow(card, k);
schemaRow(card, i);
}
} }
// One schema setting as a row of `card`: a switch, a choice, or (brightness, // One schema setting as a row of `card`: a switch, a choice, or (brightness,
@@ -3550,6 +3619,7 @@ void UITask::setSchemaValue(int idx, int v) {
lv_obj_t* body = _body; lv_obj_t* body = _body;
int32_t y = body ? lv_obj_get_scroll_y(body) : 0; int32_t y = body ? lv_obj_get_scroll_y(body) : 0;
buildSchemaSettings(); buildSchemaSettings();
fillFlush();
if (_body) { lv_obj_update_layout(_body); lv_obj_scroll_to_y(_body, y, LV_ANIM_OFF); } if (_body) { lv_obj_update_layout(_body); lv_obj_scroll_to_y(_body, y, LV_ANIM_OFF); }
} }
} }
@@ -3569,12 +3639,12 @@ void UITask::buildSettings() {
lv_obj_t* body = newScreen("Settings", true); lv_obj_t* body = newScreen("Settings", true);
// A group at a time (fillStart): the first shows at once. Coming back to // A group at a time (fillStart): the first shows at once. Coming back to
// where it was scrolled needs them all first. // where it was scrolled needs them all first.
fillStart(body, SETTINGS_GROUPS, restore ? SETTINGS_GROUPS : 1, &UITask::settingsGroup); fillStart(SETTINGS_GROUPS, 1, &UITask::settingsGroup);
if (restore) { lv_obj_update_layout(body); lv_obj_scroll_to_y(body, s_settings_y, LV_ANIM_OFF); } if (restore) { fillFlush(); lv_obj_update_layout(body); lv_obj_scroll_to_y(body, s_settings_y, LV_ANIM_OFF); }
} }
void UITask::settingsGroup(int i) { void UITask::settingsGroup(int i) {
lv_obj_t* body = s_fill.list; lv_obj_t* body = _body;
char sub[48]; char sub[48];
lv_obj_t* g; lv_obj_t* g;
char v1[24], v2[24]; char v1[24], v2[24];
+13 -1
View File
@@ -323,18 +323,26 @@ private:
void buildChats(); void buildChats();
void buildContacts(); void buildContacts();
void contactRow(int i); void contactRow(int i);
void fillStart(lv_obj_t* list, int n, int first, void (UITask::*row)(int)); void fill(void (UITask::*fn)(int), int from, int to);
void then(void (UITask::*fn)(int), int arg);
void fillStart(int n, int first, void (UITask::*row)(int));
void fillStep();
void fillTick(); void fillTick();
void fillFlush();
int fillPending() const;
void buildSettings(); void buildSettings();
static const int SETTINGS_GROUPS = 5; static const int SETTINGS_GROUPS = 5;
void settingsGroup(int i); void settingsGroup(int i);
void buildSchemaSettings(); void buildSchemaSettings();
void schemaRows(lv_obj_t* body, uint8_t page); void schemaRows(lv_obj_t* body, uint8_t page);
void schemaSection(int i);
void schemaExtras(int page);
void showMapOptions(uint8_t section); void showMapOptions(uint8_t section);
void schemaRow(lv_obj_t* card, int idx); void schemaRow(lv_obj_t* card, int idx);
void buildKeyboardPage(lv_obj_t* body); void buildKeyboardPage(lv_obj_t* body);
void buildAboutPage(lv_obj_t* body); void buildAboutPage(lv_obj_t* body);
void buildAdminPick(); void buildAdminPick();
void adminPickRow(int i);
void buildOta(); void buildOta();
void refreshOta(); void refreshOta();
void otaTick(); void otaTick();
@@ -390,10 +398,13 @@ private:
void buildClock(); void buildClock();
void buildRadio(); void buildRadio();
void rebuildRadio(); void rebuildRadio();
void radioGroup(int i);
void buildRadioExtras(lv_obj_t* body); void buildRadioExtras(lv_obj_t* body);
void buildScopes(); void buildScopes();
void buildRepeater(); void buildRepeater();
void rebuildRepeater(); void rebuildRepeater();
void repeaterGroup(int i);
void repeaterRelays();
void buildSoundRows(lv_obj_t* body, bool top); void buildSoundRows(lv_obj_t* body, bool top);
void buildMelodies(); void buildMelodies();
void buildQuickMsgs(); void buildQuickMsgs();
@@ -415,6 +426,7 @@ private:
void favRefresh(); void favRefresh();
int unreadTotal(); // DMs, channels and rooms int unreadTotal(); // DMs, channels and rooms
void buildDiag(); void buildDiag();
void diagSection(int sec);
void refreshDiag(); void refreshDiag();
void buildCompass(); void buildCompass();
void refreshCompass(); void refreshCompass();