diff --git a/examples/companion_radio/ui-lvgl/BotScreen.h b/examples/companion_radio/ui-lvgl/BotScreen.h index aa4f6e25..bb706267 100644 --- a/examples/companion_radio/ui-lvgl/BotScreen.h +++ b/examples/companion_radio/ui-lvgl/BotScreen.h @@ -142,7 +142,7 @@ void UITask::botToggle(int row, bool on) { uint8_t* fl = botcfg::flag(_prefs, botcfg::row(botview::s_tab, row).kind); if (!fl) return; *fl = on ? 1 : 0; - the_mesh.savePrefs(); + prefsSave(); } // Tap on a value row: its editor. @@ -247,7 +247,7 @@ void UITask::botTextDone(bool ok) { char* t = botcfg::text(_prefs, botcfg::row(s_tab, s_row).kind, cap); if (t) { snprintf(t, cap, "%s", lv_textarea_get_text(_nav_ta)); - the_mesh.savePrefs(); + prefsSave(); } } navClosePopup(); @@ -260,7 +260,7 @@ void UITask::botPick(int idx) { botcfg::Kind k = botcfg::row(s_tab, s_row).kind; if (k == botcfg::CHANNEL) _prefs->bot_channel_idx = (uint8_t)idx; else if (k == botcfg::ROOM && idx >= 0 && idx < (int)(sizeof(s_rooms) / sizeof(s_rooms[0]))) botcfg::setRoom(_prefs, s_rooms[idx]); - the_mesh.savePrefs(); + prefsSave(); navClosePopup(); buildBot(); } @@ -271,7 +271,7 @@ void UITask::botHour(uint8_t act) { botcfg::Kind k = botcfg::row(s_tab, s_row).kind; uint8_t& h = botcfg::hour(_prefs, k); if (act == B_DONE) { - the_mesh.savePrefs(); + prefsSave(); navClosePopup(); buildBot(); return; diff --git a/examples/companion_radio/ui-lvgl/ChannelScreen.h b/examples/companion_radio/ui-lvgl/ChannelScreen.h index a854ed36..167b8339 100644 --- a/examples/companion_radio/ui-lvgl/ChannelScreen.h +++ b/examples/companion_radio/ui-lvgl/ChannelScreen.h @@ -128,7 +128,7 @@ void UITask::channelSet(uint8_t which, int v) { if (which == C_NOTIF) chanctl::setNotif(_prefs, s_idx, (uint8_t)v); else if (which == C_SCOPE) the_mesh.setChannelScope(s_idx, (uint8_t)v); else if (which == C_MELODY) { chanctl::setMelody(_prefs, s_idx, (uint8_t)v); hearMelody(v); } - the_mesh.savePrefs(); + prefsSave(); } void UITask::channelAction(uint8_t act) { @@ -139,7 +139,7 @@ void UITask::channelAction(uint8_t act) { case A_FAV: { bool on = !chanctl::favourite(_prefs, idx); chanctl::setFavourite(_prefs, idx, on); - the_mesh.savePrefs(); + prefsSave(); if (s_fav_btn) lv_obj_set_style_bg_color(s_fav_btn, lv_color_hex(on ? theme::ACCENT_DIM : theme::SURFACE), 0); showToast(on ? "Added to favourites" : "Removed from favourites", 1200); if (_screen == SCR_CHATS) { buildChats(); channelMenu(idx); } // list order / star diff --git a/examples/companion_radio/ui-lvgl/ClockScreen.h b/examples/companion_radio/ui-lvgl/ClockScreen.h index dc8258ed..72c24dd2 100644 --- a/examples/companion_radio/ui-lvgl/ClockScreen.h +++ b/examples/companion_radio/ui-lvgl/ClockScreen.h @@ -267,7 +267,7 @@ void UITask::setAlarm(int which, int v) { case 3: _prefs->alarm_repeat_mask = NodePrefs::alarmRepeatMaskForIdx((uint8_t)v); break; } _core->clock.onAlarmChanged(); - the_mesh.savePrefs(); + prefsSave(); if (which < 2 && clockview::s_alarm_sw) lv_obj_add_state(clockview::s_alarm_sw, LV_STATE_CHECKED); } diff --git a/examples/companion_radio/ui-lvgl/ConversationScreen.h b/examples/companion_radio/ui-lvgl/ConversationScreen.h index 9ea41d33..8f1aad9c 100644 --- a/examples/companion_radio/ui-lvgl/ConversationScreen.h +++ b/examples/companion_radio/ui-lvgl/ConversationScreen.h @@ -185,7 +185,7 @@ void UITask::conversationMenu(const uint8_t* pub_key) { void UITask::conversationNotif(int v) { if (!_prefs || v < 0 || v > 2) return; contactctl::setNotif(_prefs, convview::s_key, (uint8_t)v); - the_mesh.savePrefs(); + prefsSave(); } void UITask::conversationAction(uint8_t act) { @@ -405,6 +405,6 @@ void UITask::toggleChatFilter(uint8_t which) { case CF_ROOMS: _prefs->room_fav_only ^= 1; break; case CF_CONTACTS: _prefs->dm_show_all ^= 1; break; } - the_mesh.savePrefs(); + prefsSave(); if (_screen == SCR_CONTACTS) buildContacts(); else buildChats(); } diff --git a/examples/companion_radio/ui-lvgl/DeviceScreen.h b/examples/companion_radio/ui-lvgl/DeviceScreen.h index 13046042..1bde4561 100644 --- a/examples/companion_radio/ui-lvgl/DeviceScreen.h +++ b/examples/companion_radio/ui-lvgl/DeviceScreen.h @@ -139,7 +139,7 @@ void UITask::nodeNameDone(bool ok) { const char* t = lv_textarea_get_text(_nav_ta); if (!t[0]) { showToast("Name can't be empty"); return; } snprintf(_prefs->node_name, sizeof(_prefs->node_name), "%s", t); - the_mesh.savePrefs(); // getNodeName() and the self-advert read node_name live + prefsSave(); // getNodeName() and the self-advert read node_name live showToast("Name saved - others see it with your next advert", 3000); } navClosePopup(); @@ -512,7 +512,7 @@ void UITask::favAction(uint8_t act) { int slot = devview::s_fav_slot; if (act == devview::F_CHANGE) { favPickPopup(slot); return; } favslots::clear(_prefs, slot); - the_mesh.savePrefs(); + prefsSave(); navClosePopup(); favRefresh(); } @@ -559,7 +559,7 @@ void UITask::favPick(int code) { using namespace devview; if (code >= PICK_CONTACT) favslots::pinContact(_prefs, s_fav_slot, s_pick_keys[code - PICK_CONTACT]); else favslots::pinChannel(_prefs, s_fav_slot, (uint8_t)code); - the_mesh.savePrefs(); + prefsSave(); navClosePopup(); favRefresh(); } @@ -610,7 +610,7 @@ void UITask::pinTo(int slot) { else favslots::pinContact(_prefs, slot, s_pin_key); snprintf(msg, sizeof(msg), "Pinned to slot %d", slot + 1); } - the_mesh.savePrefs(); + prefsSave(); navClosePopup(); showToast(msg, 1500); } diff --git a/examples/companion_radio/ui-lvgl/HomeScreen.h b/examples/companion_radio/ui-lvgl/HomeScreen.h index 79ff7d66..026aede9 100644 --- a/examples/companion_radio/ui-lvgl/HomeScreen.h +++ b/examples/companion_radio/ui-lvgl/HomeScreen.h @@ -294,7 +294,7 @@ void UITask::buildHomeClock(lv_obj_t* box) { void UITask::homeFieldSet(int slot, int f) { if (!_prefs || slot < 0 || slot >= home::FIELDS || f < 0 || f >= home::F_COUNT) return; _prefs->dashboard_fields[slot] = (uint8_t)f; - the_mesh.savePrefs(); + prefsSave(); setHomePage(home::CLOCK); } diff --git a/examples/companion_radio/ui-lvgl/NavMap.h b/examples/companion_radio/ui-lvgl/NavMap.h index 687eb9b3..1a11f61d 100644 --- a/examples/companion_radio/ui-lvgl/NavMap.h +++ b/examples/companion_radio/ui-lvgl/NavMap.h @@ -419,7 +419,7 @@ void UITask::navFrameTrail() { void UITask::navSetTarget(uint8_t kind, const uint8_t* key, int32_t lat, int32_t lon, const char* name) { navmap::s_tb.stop(); // a chosen target replaces walking the trail back _core->locator.setTarget(kind, key, lat, lon, name); - the_mesh.savePrefs(); + prefsSave(); navmap::s_eta.reset(); char t[40]; snprintf(t, sizeof(t), "Navigating to %s", name); @@ -433,7 +433,7 @@ void UITask::navPick(int code) { case navmap::T_CLEAR: if (navmap::s_tb.active()) { navmap::s_tb.stop(); showToast("Track back stopped"); break; } _core->locator.clearTarget(); - the_mesh.savePrefs(); + prefsSave(); showToast("Target cleared"); break; case navmap::T_WAYPOINT: { @@ -682,7 +682,7 @@ void UITask::navRenameDone(bool ok) { _core->waypoints.rename(_nav_wp, lv_textarea_get_text(_nav_ta)); if (was_target) { // the bar shows the target's saved label snprintf(_prefs->locator_label, sizeof(_prefs->locator_label), "%s", _core->waypoints.at(_nav_wp).label); - the_mesh.savePrefs(); + prefsSave(); } rebuildMapMarkers(); layoutMap(); @@ -1008,7 +1008,7 @@ void UITask::navSetShareTarget(int sel) { _prefs->loc_share_target_type = navmap::s_share_kind[sel]; if (navmap::s_share_kind[sel] == 0) _prefs->loc_share_channel_idx = navmap::s_share_ch[sel]; else memcpy(_prefs->loc_share_dm_prefix, navmap::s_share_key[sel], NodePrefs::FAVOURITE_PREFIX_LEN); - the_mesh.savePrefs(); + prefsSave(); } // A new file in /sdcard/trails named by the local time, "trail-YYYYMMDD-HHMM" @@ -1243,7 +1243,7 @@ void UITask::navToolAction(uint8_t act) { if (_core->course.currentLocation(lat, lon)) showToast("Sharing your position"); else ensureGps(); } - the_mesh.savePrefs(); + prefsSave(); break; case navmap::TL_SHARE_ONCE: { int32_t lat, lon; diff --git a/examples/companion_radio/ui-lvgl/QuickScreen.h b/examples/companion_radio/ui-lvgl/QuickScreen.h index d9f4ca8b..c43867bd 100644 --- a/examples/companion_radio/ui-lvgl/QuickScreen.h +++ b/examples/companion_radio/ui-lvgl/QuickScreen.h @@ -166,7 +166,7 @@ void UITask::quickEditDone(bool ok) { using namespace qview; if (ok && _nav_ta && _prefs && s_edit >= 0) { msgtext::setQuick(_prefs, s_edit, lv_textarea_get_text(_nav_ta)); - the_mesh.savePrefs(); + prefsSave(); showToast(msgtext::quick(_prefs, s_edit)[0] ? "Quick message saved" : "Quick message cleared", 1200); } navClosePopup(); diff --git a/examples/companion_radio/ui-lvgl/RadioExtras.h b/examples/companion_radio/ui-lvgl/RadioExtras.h index 406cf472..c6590454 100644 --- a/examples/companion_radio/ui-lvgl/RadioExtras.h +++ b/examples/companion_radio/ui-lvgl/RadioExtras.h @@ -100,7 +100,7 @@ void UITask::presetAction(uint8_t act) { case P_USE: p->freq = u.freq; p->bw = u.bw; p->sf = u.sf; p->cr = u.cr; radioctl::applyParams(); - the_mesh.savePrefs(); + prefsSave(); navClosePopup(); rebuildRadio(); showToast("Preset in use"); @@ -115,7 +115,7 @@ void UITask::presetAction(uint8_t act) { } case P_DELETE_GO: radioctl::deleteUserPreset(p, s_slot); - the_mesh.savePrefs(); + prefsSave(); navClosePopup(); rebuildRadio(); showToast("Preset deleted"); @@ -157,7 +157,7 @@ void UITask::radioNameDone(bool ok) { if (s_naming == N_PRESET) { NodePrefs* p = _prefs; radioctl::saveUserPreset(p, name, p->freq, p->bw, p->sf, p->cr); - the_mesh.savePrefs(); + prefsSave(); toast = "Preset saved"; } else if (s_naming == N_SCOPE_ADD) { toast = the_mesh.addScope(name) ? "Scope added" : "The list is full"; diff --git a/examples/companion_radio/ui-lvgl/RadioScreen.h b/examples/companion_radio/ui-lvgl/RadioScreen.h index 845da4be..66febda9 100644 --- a/examples/companion_radio/ui-lvgl/RadioScreen.h +++ b/examples/companion_radio/ui-lvgl/RadioScreen.h @@ -126,7 +126,7 @@ void UITask::radioSet(int which, int v) { case R_TX: p->tx_power_dbm = (int8_t)(TX_MIN + v); radioctl::applyTxPower(p); break; case R_APC: p->tx_apc = (uint8_t)v; radioctl::applyApc(); break; } - the_mesh.savePrefs(); + prefsSave(); rebuildRadio(); // preset name / hints follow } @@ -173,7 +173,7 @@ void UITask::radioFreqDone(bool ok) { if (f < lo || f > hi) { showToast("Out of the radio's range"); return; } if (s_freq_rpt) { _prefs->repeater_freq = f; rptctl::applyProfile(); } else { _prefs->freq = f; radioctl::applyParams(); } - the_mesh.savePrefs(); + prefsSave(); } radioCloseFreq(); if (ok) { if (s_freq_rpt) rebuildRepeater(); else rebuildRadio(); } diff --git a/examples/companion_radio/ui-lvgl/RepeaterScreen.h b/examples/companion_radio/ui-lvgl/RepeaterScreen.h index 91f0a774..1e123612 100644 --- a/examples/companion_radio/ui-lvgl/RepeaterScreen.h +++ b/examples/companion_radio/ui-lvgl/RepeaterScreen.h @@ -123,7 +123,7 @@ void UITask::repeaterSet(int which, int v) { case RP_YIELD: p->repeat_delay_boost = (uint8_t)v; break; case RP_SNR: p->repeat_min_snr = rptctl::snrFromChoice(v); break; } - the_mesh.savePrefs(); + prefsSave(); if (which <= RP_CR) rebuildRepeater(); // hints, the profile rows and the preset name follow refreshStatusBar(); } @@ -148,7 +148,7 @@ void UITask::repeaterScopesPopup() { void UITask::repeaterScopeSet(uint8_t i, bool on) { rptctl::setExtraScope(_prefs, i, on); - the_mesh.savePrefs(); + prefsSave(); char sub[48]; extraScopesSummary(_prefs, sub, sizeof(sub)); if (rptview::s_scopes_sub) lv_label_set_text(rptview::s_scopes_sub, sub); diff --git a/examples/companion_radio/ui-lvgl/SoundScreen.h b/examples/companion_radio/ui-lvgl/SoundScreen.h index eb33704c..4f494393 100644 --- a/examples/companion_radio/ui-lvgl/SoundScreen.h +++ b/examples/companion_radio/ui-lvgl/SoundScreen.h @@ -250,7 +250,7 @@ void UITask::setSoundMode(int mode) { soundctl::setMode(_prefs, _buzzer, (uint8_t)mode, isClientConnected()); if (mode == soundctl::MODE_ON) notify(UIEventType::ack); // hear that it's on #endif - the_mesh.savePrefs(); + prefsSave(); refreshStatusBar(); if (mode == soundctl::MODE_AUTO) showToast(isClientConnected() ? "Silent while the app is connected" : "Sound on until the app connects"); } @@ -260,7 +260,7 @@ void UITask::setSoundVolume(int level) { #ifdef PIN_BUZZER soundctl::setVolume(_prefs, _buzzer, (uint8_t)level); #endif - the_mesh.savePrefs(); + prefsSave(); } // The Sound page's own rows: the mode at the top, the melodies at the bottom. @@ -357,7 +357,7 @@ void UITask::melodySave() { using namespace sndview; if (!s_dirty || !_prefs) return; soundctl::store(_prefs, s_slot, s_mel); - the_mesh.savePrefs(); + prefsSave(); s_dirty = false; showToast(s_slot ? "Melody 2 saved" : "Melody 1 saved", 1200); } @@ -446,7 +446,7 @@ void UITask::melodyAction(uint8_t act) { void UITask::conversationMelody(int v) { if (!_prefs || v < 0 || v >= soundctl::OVERRIDE_COUNT) return; contactctl::setMelody(_prefs, convview::s_key, (uint8_t)v); - the_mesh.savePrefs(); + prefsSave(); hearMelody(v); } diff --git a/examples/companion_radio/ui-lvgl/UITask.cpp b/examples/companion_radio/ui-lvgl/UITask.cpp index 5a5f55d7..3356a013 100644 --- a/examples/companion_radio/ui-lvgl/UITask.cpp +++ b/examples/companion_radio/ui-lvgl/UITask.cpp @@ -712,6 +712,25 @@ static void fmtDate(char* b, size_t n, const struct tm& ti) { snprintf(b, n, "%s %d %s %d", DOW[ti.tm_wday], ti.tm_mday, MON[ti.tm_mon], ti.tm_year + 1900); } +// Settings changed here reach flash once their screen is left (as on the L1: +// a run of switches is one write, and the flash wears less), just after the +// next screen has come in -- a save stalls the UI (0.1-0.6 s on the ESP32's +// SPIFFS, far less on LittleFS). Else when the screen goes off, or a minute on. NodePrefs +// itself changes at once; shutdown() saves whatever is pending. +static uint32_t s_prefs_dirty_ms = 0; // when the first unsaved change came (0: none) +static uint32_t s_prefs_save_at = 0; // the screen was left: save from then (0: not yet) +static void prefsSave() { if (!s_prefs_dirty_ms) s_prefs_dirty_ms = millis() | 1; } +static void prefsSaveSoon(uint32_t delay_ms) { + if (s_prefs_dirty_ms && !s_prefs_save_at) s_prefs_save_at = (millis() + delay_ms) | 1; +} +static void prefsFlush() { + if (!s_prefs_dirty_ms) return; + bool due = s_prefs_save_at ? (int32_t)(millis() - s_prefs_save_at) >= 0 : millis() - s_prefs_dirty_ms >= 60000; + if (!due) return; + s_prefs_dirty_ms = s_prefs_save_at = 0; + the_mesh.savePrefs(); +} + // A big clock (Home, the lock screen): the digits and, on a 12-hour clock, // AM / PM beside them in `small`, on the digits' baseline. clockFaceSet() // once a second; "--:--" until the time is known. @@ -821,7 +840,7 @@ void UITask::toggleMute() { if (!_prefs) return; bool on = soundctl::mode(_prefs) != soundctl::MODE_ON; soundctl::setMode(_prefs, _buzzer, on ? soundctl::MODE_ON : soundctl::MODE_OFF, isClientConnected()); - the_mesh.savePrefs(); + prefsSave(); if (on) _buzzer.playForced(soundctl::MEL_VOLUME); if (!_asleep) { showToast(on ? "Sound on" : "Sound off", 1200); refreshStatusBar(); } #endif @@ -871,6 +890,7 @@ void UITask::loop() { _core->loop(); drainCoreEvents(); + prefsFlush(); #if defined(UI_HEAP_REPORT) && defined(ESP32) // -D UI_HEAP_REPORT: internal / PSRAM heap once, 20 s after boot (the // framework comparison in docs/development/l2-roadmap.md). @@ -1005,6 +1025,7 @@ uint32_t UITask::idleMillis(uint32_t lv_next) { } void UITask::shutdown(bool restart) { + s_prefs_dirty_ms = s_prefs_save_at = 0; the_mesh.savePrefs(); the_mesh.saveRTCTime(); the_mesh.flushDirtyContacts(); @@ -1053,6 +1074,7 @@ void UITask::sleep() { if (_asleep) return; _asleep = true; if (_display) _display->turnOff(); + prefsSaveSoon(0); // nothing to stall now lvport::powerSave(true, _tap_wake); if ((_prefs && _prefs->auto_lock) || _pin[0]) lockScreen(); // Lock screen, or a screen PIN } @@ -1349,6 +1371,7 @@ lv_obj_t* UITask::newScreen(const char* title, bool with_back) { bool same = _screen == _shown_screen && strncmp(title ? title : "", _shown_title, sizeof(_shown_title) - 1) == 0; bool backward = _nav_back || screenDepth(_screen) < screenDepth(_shown_screen); _nav_back = false; + if (!same) prefsSaveSoon(anim::FADE_MS + 60); // left the screen they were changed on _shown_screen = _screen; snprintf(_shown_title, sizeof(_shown_title), "%s", title ? title : ""); _scr = scr; @@ -2174,7 +2197,7 @@ void UITask::nodeAction(uint8_t action) { if (e.has_prefix) navToNode(e.pub_key, e.lat_e6, e.lon_e6, e.name[0] ? e.name : "Node"); else { _core->locator.setTarget(0, nullptr, e.lat_e6, e.lon_e6, e.name[0] ? e.name : "Node"); - the_mesh.savePrefs(); + prefsSave(); openMap(true); navFrameTarget(); } @@ -2763,7 +2786,7 @@ static void onKeyboardAlphabet(lv_event_t* e) { static void onPrefSwitch(lv_event_t* e) { uint8_t* pref = (uint8_t*)lv_event_get_user_data(e); *pref = lv_obj_has_state((lv_obj_t*)lv_event_get_target(e), LV_STATE_CHECKED) ? 1 : 0; - the_mesh.savePrefs(); + prefsSave(); } static void onGpsSwitch(lv_event_t* e) { @@ -2848,7 +2871,7 @@ void UITask::setBrightnessPct(uint8_t pct, bool save) { _prefs->display_brightness_pct = pct; _prefs->display_brightness = (uint8_t)((pct + 12) / 25 > 4 ? 4 : (pct + 12) / 25); // nearest level, for anything reading it applyDisplayPrefs(); - if (save) the_mesh.savePrefs(); + if (save) prefsSave(); } static void onTapWake(lv_event_t* e) { @@ -3008,7 +3031,7 @@ void UITask::setSchemaValue(int idx, int v) { const settings::Setting& st = settings::ALL[idx]; settings::set(*_prefs, st, (uint8_t)v); if (st.changed) st.changed(*_core); - the_mesh.savePrefs(); + prefsSave(); if (st.offset == offsetof(NodePrefs, units_imperial) && _screen == SCR_SETTINGS_NAV) { // other labels depend on it lv_obj_t* body = _body; int32_t y = body ? lv_obj_get_scroll_y(body) : 0; @@ -3102,7 +3125,7 @@ void UITask::setKeyboardAlphabets(int main_idx, int alt_sel) { if (!_prefs) return; _prefs->keyboard_main_alphabet = (uint8_t)main_idx; _prefs->keyboard_alt_alphabet = (uint8_t)(alt_sel == 0 ? main_idx : alt_sel - 1); - the_mesh.savePrefs(); + prefsSave(); } #include "ConversationScreen.h"