mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-01 10:21:59 +00:00
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:
@@ -68,6 +68,7 @@ static void onAdminPickRow(lv_event_t* e) { s_ui->adminPick((int)(uintptr_t)lv_
|
||||
namespace adminview {
|
||||
static const int PICK_MAX = 128;
|
||||
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
|
||||
@@ -80,27 +81,36 @@ void UITask::showAdminPick() {
|
||||
void UITask::buildAdminPick() {
|
||||
lv_obj_t* body = newScreen("Admin", true);
|
||||
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++) {
|
||||
ContactInfo c;
|
||||
if (!the_mesh.getContactByIdx(MAX_ANON_CONTACTS + i, c)) continue;
|
||||
if (c.type != ADV_TYPE_REPEATER && c.type != ADV_TYPE_ROOM) continue;
|
||||
bool fav = contactctl::favourite(c);
|
||||
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);
|
||||
if (contactctl::favourite(c) != (pass == 0)) continue;
|
||||
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++;
|
||||
}
|
||||
}
|
||||
if (rows) sectionTitle(body, "LOG IN TO");
|
||||
fillStart(rows, 8, &UITask::adminPickRow); // the rest as the loop goes (fill)
|
||||
if (!rows) {
|
||||
noteLabel(body, "No repeaters or room servers yet. They show up here once their advert is heard.",
|
||||
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) {
|
||||
if (row < 0 || row >= adminview::PICK_MAX) return;
|
||||
openAdmin(adminview::s_pick[row]);
|
||||
|
||||
@@ -391,6 +391,7 @@ void UITask::pinRemove() {
|
||||
void UITask::pinRowRefresh() {
|
||||
if (_screen != SCR_SETTINGS_NAV) return;
|
||||
buildSchemaSettings();
|
||||
fillFlush();
|
||||
if (_body) { lv_obj_update_layout(_body); lv_obj_scroll_by(_body, 0, -lv_obj_get_scroll_bottom(_body), LV_ANIM_OFF); }
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
LiveVal& lv = s_vals[i];
|
||||
if (!lv.in && !lv.val) return; // its section isn't built yet (fill)
|
||||
if (lv.in) {
|
||||
const char* slash = strchr(r.value, '/');
|
||||
char a[12];
|
||||
@@ -260,25 +261,11 @@ void UITask::buildDiag() {
|
||||
s_rows = 0;
|
||||
|
||||
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];
|
||||
s_rows = allRows(rows, _core->gpsEnabled());
|
||||
for (uint8_t sec = 0; sec < SEC_COUNT; sec++) {
|
||||
lv_obj_t* card = nullptr;
|
||||
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]);
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < s_rows; i++) s_vals[i] = { nullptr, nullptr, nullptr };
|
||||
fillStart(SEC_COUNT, 1, &UITask::diagSection);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -351,6 +338,27 @@ void UITask::buildDiag() {
|
||||
}
|
||||
|
||||
// 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() {
|
||||
using namespace diagview;
|
||||
if (_screen != SCR_DIAG || s_tab != TAB_LIVE || _nav_overlay) return;
|
||||
|
||||
@@ -444,11 +444,17 @@ static const char* resetReason() {
|
||||
// the address with the build's firmware.elf, or read the whole dump over USB).
|
||||
static bool crashSummary(char* out, size_t n) {
|
||||
#if ESP_ARDUINO_VERSION_MAJOR >= 3
|
||||
if (esp_core_dump_image_check() != ESP_OK) return false;
|
||||
esp_core_dump_summary_t sum;
|
||||
if (esp_core_dump_get_summary(&sum) != ESP_OK) return false;
|
||||
snprintf(out, n, "%.10s %08lx", sum.exc_task, (unsigned long)sum.exc_pc);
|
||||
return true;
|
||||
// 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;
|
||||
s_have = esp_core_dump_image_check() == ESP_OK && esp_core_dump_get_summary(&sum) == ESP_OK;
|
||||
if (s_have) snprintf(s_sum, sizeof(s_sum), "%.10s %08lx", sum.exc_task, (unsigned long)sum.exc_pc);
|
||||
}
|
||||
if (s_have) snprintf(out, n, "%s", s_sum);
|
||||
return s_have;
|
||||
#else
|
||||
(void)out; (void)n;
|
||||
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,
|
||||
"Higher = longer range");
|
||||
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;
|
||||
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;
|
||||
@@ -107,8 +113,6 @@ void UITask::buildRadio() {
|
||||
if (p->tx_apc) lv_obj_add_state(sw, LV_STATE_CHECKED);
|
||||
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);
|
||||
|
||||
buildRadioExtras(body); // my presets, scopes (RadioExtras.h)
|
||||
}
|
||||
|
||||
void UITask::radioSet(int which, int v) {
|
||||
@@ -134,6 +138,7 @@ void UITask::radioSet(int which, int v) {
|
||||
void UITask::rebuildRadio() {
|
||||
int32_t y = _body ? lv_obj_get_scroll_y(_body) : 0;
|
||||
buildRadio();
|
||||
fillFlush();
|
||||
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() {
|
||||
using namespace rptview;
|
||||
using radioview::rowDropdown;
|
||||
lv_obj_t* body = newScreen("Repeater", true);
|
||||
s_scopes_sub = nullptr; // made with its group (repeaterRelays)
|
||||
NodePrefs* p = _prefs;
|
||||
|
||||
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);
|
||||
lv_obj_add_event_cb(sw, onRptSwitch, LV_EVENT_VALUE_CHANGED, (void*)(uintptr_t)RP_ON);
|
||||
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");
|
||||
static const char* NET[] = { "Current", "Custom", "" };
|
||||
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,
|
||||
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);
|
||||
char v[12];
|
||||
int o = 0;
|
||||
@@ -102,6 +113,7 @@ void UITask::buildRepeater() {
|
||||
void UITask::rebuildRepeater() {
|
||||
int32_t y = _body ? lv_obj_get_scroll_y(_body) : 0;
|
||||
buildRepeater();
|
||||
fillFlush();
|
||||
if (_body) { lv_obj_update_layout(_body); lv_obj_scroll_to_y(_body, y, LV_ANIM_OFF); }
|
||||
}
|
||||
|
||||
|
||||
@@ -1060,6 +1060,32 @@ void UITask::loop() {
|
||||
lv_display_trigger_activity(NULL); // no sleeping mid-test
|
||||
next = millis() + 1200;
|
||||
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
|
||||
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); }
|
||||
@@ -1684,37 +1710,62 @@ namespace home { static void leave(); } // HomeScreen.h
|
||||
|
||||
static int s_th_rows_n = 0; // conversation bubbles built (refreshThread)
|
||||
|
||||
// A long list fills in over several loop passes: the first screenful at
|
||||
// once, the rest as time allows. A row costs 3-6 ms on the L2 (widgets live in
|
||||
// PSRAM), so 48 nodes or a few hundred contacts built in one go held the
|
||||
// screen for a second. `row(i)` makes row i from the data as it is then.
|
||||
// One list at a time; a new screen drops it.
|
||||
static struct {
|
||||
lv_obj_t* list = nullptr;
|
||||
int next = 0, n = 0;
|
||||
void (UITask::*row)(int) = nullptr;
|
||||
} s_fill;
|
||||
static const uint32_t FILL_BUDGET_US = 25000; // rows added per loop pass: about a frame's worth
|
||||
// A screen builds in pieces over several loop passes: what shows first at
|
||||
// once, the rest as time allows. A widget costs ~1 ms on the L2 (they live in
|
||||
// PSRAM), so 48 nodes, a few hundred contacts or a long settings page built
|
||||
// in one go held the screen for up to a second. The pieces queue in order:
|
||||
// fill(fn, from, to) makes fn(from) .. fn(to - 1), each from the data as it
|
||||
// is then; then(fn, arg) makes its piece at once unless others still wait,
|
||||
// keeping its place after them. A new screen drops what is left.
|
||||
struct FillJob { void (UITask::*fn)(int); int next, end; };
|
||||
static const int FILL_JOBS = 12;
|
||||
static FillJob s_fill[FILL_JOBS];
|
||||
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)) {
|
||||
s_fill.list = list;
|
||||
s_fill.n = n;
|
||||
s_fill.row = row;
|
||||
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::fill(void (UITask::*fn)(int), int from, int to) {
|
||||
if (from >= to) return;
|
||||
if (s_fill_n == FILL_JOBS) { while (from < to) (this->*fn)(from++); return; } // no room: now
|
||||
s_fill[s_fill_n++] = { fn, from, to };
|
||||
}
|
||||
|
||||
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() {
|
||||
if (!s_fill.list) return;
|
||||
uint32_t t0 = micros();
|
||||
do (this->*s_fill.row)(s_fill.next++);
|
||||
while (s_fill.next < s_fill.n && micros() - t0 < FILL_BUDGET_US);
|
||||
if (s_fill.next >= s_fill.n) s_fill.list = nullptr;
|
||||
while (s_fill_n) {
|
||||
fillStep();
|
||||
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) {
|
||||
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;
|
||||
_thread_list = _compose_ta = _keyboard = nullptr;
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
void UITask::contactRow(int i) {
|
||||
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;
|
||||
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 ────────────────────────────────────────────────────────────────────
|
||||
@@ -2450,14 +2501,14 @@ void UITask::refreshNearbyList() {
|
||||
// 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.
|
||||
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 && rows == n && lv_obj_check_type(lv_obj_get_child(_nearby_list, 0), &lv_button_class)) {
|
||||
if (n > 0 && built > 0 && built + fillPending() == 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);
|
||||
return;
|
||||
}
|
||||
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);
|
||||
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) {
|
||||
lv_obj_t* l = label(_nearby_list, "Nobody here yet. Tap Scan to look around.",
|
||||
THEME_FONT_BODY, theme::TEXT_MUTED);
|
||||
@@ -3433,13 +3484,19 @@ void UITask::buildSchemaSettings() {
|
||||
}
|
||||
}
|
||||
schemaRows(body, _settings_page);
|
||||
if (_settings_page == settings::PG_SOUND) buildSoundRows(body, false); // the melodies
|
||||
if (_settings_page == settings::PG_MESSAGES) {
|
||||
if (_settings_page == settings::PG_SOUND) then(&UITask::schemaExtras, settings::PG_SOUND); // after its groups
|
||||
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"
|
||||
if (c) _prune_lbl = actionRow(c, LV_SYMBOL_TRASH " Remove inactive contacts now", onPruneContacts, NULL, theme::FAIL);
|
||||
char sub[48];
|
||||
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,24 +3531,36 @@ void UITask::buildAboutPage(lv_obj_t* body) {
|
||||
// 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.
|
||||
|
||||
// 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) {
|
||||
uint8_t sec = 0xFF;
|
||||
lv_obj_t* card = 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++) {
|
||||
const settings::Setting& st = settings::ALL[i];
|
||||
if (settings::sectionPage(st.section) != page) continue;
|
||||
bool one = page == settings::PG_NAV && s_nav_section >= 0; // one section: its own page, no heading
|
||||
if (one && st.section != s_nav_section) continue;
|
||||
if (st.section != sec) {
|
||||
sec = st.section;
|
||||
card = s_sec_card[sec] = group(body, one ? nullptr : settings::sectionTitle(sec));
|
||||
if (sec == settings::SEC_SOUND) buildSoundRows(card, true); // On / Off / Auto
|
||||
}
|
||||
schemaRow(card, i);
|
||||
if (settings::sectionPage(st.section) != page || st.section == sec) continue;
|
||||
if (s_schema_one && st.section != s_nav_section) continue;
|
||||
sec = st.section;
|
||||
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
|
||||
for (int k = i; k < settings::COUNT && settings::ALL[k].section == sec; k++) schemaRow(card, k);
|
||||
}
|
||||
|
||||
// One schema setting as a row of `card`: a switch, a choice, or (brightness,
|
||||
// volume) a slider.
|
||||
void UITask::schemaRow(lv_obj_t* card, int i) {
|
||||
@@ -3550,6 +3619,7 @@ void UITask::setSchemaValue(int idx, int v) {
|
||||
lv_obj_t* body = _body;
|
||||
int32_t y = body ? lv_obj_get_scroll_y(body) : 0;
|
||||
buildSchemaSettings();
|
||||
fillFlush();
|
||||
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);
|
||||
// A group at a time (fillStart): the first shows at once. Coming back to
|
||||
// where it was scrolled needs them all first.
|
||||
fillStart(body, SETTINGS_GROUPS, restore ? SETTINGS_GROUPS : 1, &UITask::settingsGroup);
|
||||
if (restore) { lv_obj_update_layout(body); lv_obj_scroll_to_y(body, s_settings_y, LV_ANIM_OFF); }
|
||||
fillStart(SETTINGS_GROUPS, 1, &UITask::settingsGroup);
|
||||
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) {
|
||||
lv_obj_t* body = s_fill.list;
|
||||
lv_obj_t* body = _body;
|
||||
char sub[48];
|
||||
lv_obj_t* g;
|
||||
char v1[24], v2[24];
|
||||
|
||||
@@ -323,18 +323,26 @@ private:
|
||||
void buildChats();
|
||||
void buildContacts();
|
||||
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 fillFlush();
|
||||
int fillPending() const;
|
||||
void buildSettings();
|
||||
static const int SETTINGS_GROUPS = 5;
|
||||
void settingsGroup(int i);
|
||||
void buildSchemaSettings();
|
||||
void schemaRows(lv_obj_t* body, uint8_t page);
|
||||
void schemaSection(int i);
|
||||
void schemaExtras(int page);
|
||||
void showMapOptions(uint8_t section);
|
||||
void schemaRow(lv_obj_t* card, int idx);
|
||||
void buildKeyboardPage(lv_obj_t* body);
|
||||
void buildAboutPage(lv_obj_t* body);
|
||||
void buildAdminPick();
|
||||
void adminPickRow(int i);
|
||||
void buildOta();
|
||||
void refreshOta();
|
||||
void otaTick();
|
||||
@@ -390,10 +398,13 @@ private:
|
||||
void buildClock();
|
||||
void buildRadio();
|
||||
void rebuildRadio();
|
||||
void radioGroup(int i);
|
||||
void buildRadioExtras(lv_obj_t* body);
|
||||
void buildScopes();
|
||||
void buildRepeater();
|
||||
void rebuildRepeater();
|
||||
void repeaterGroup(int i);
|
||||
void repeaterRelays();
|
||||
void buildSoundRows(lv_obj_t* body, bool top);
|
||||
void buildMelodies();
|
||||
void buildQuickMsgs();
|
||||
@@ -415,6 +426,7 @@ private:
|
||||
void favRefresh();
|
||||
int unreadTotal(); // DMs, channels and rooms
|
||||
void buildDiag();
|
||||
void diagSection(int sec);
|
||||
void refreshDiag();
|
||||
void buildCompass();
|
||||
void refreshCompass();
|
||||
|
||||
Reference in New Issue
Block a user