fix: no flash writes while typing, and a clock that survives a power loss

Contacts and paths are saved a while after the change and never while the
user is active, so a send no longer stutters on the 53 KB contact rewrite
(a path that came back unchanged isn't written at all). The clock is saved
every 30 minutes and restored no earlier than the firmware's build day.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-10-08 23:41:32 +02:00
co-authored by Claude Opus 5.5
parent aaf35351d7
commit 9ef7d5d981
5 changed files with 70 additions and 17 deletions
+33 -12
View File
@@ -489,24 +489,45 @@ void DataStore::saveRTCTime() {
}
}
// The day this firmware was built (__DATE__, "Mmm dd yyyy"), 00:00 UTC: the
// clock can't be any earlier than that.
static uint32_t buildDayEpoch() {
static const char M[] = "JanFebMarAprMayJunJulAugSepOctNovDec";
const char* d = __DATE__;
int m = 1;
while (m <= 12 && strncmp(M + 3 * (m - 1), d, 3) != 0) m++;
int day = atoi(d + 4), y = atoi(d + 7);
if (m > 12 || day < 1 || y < 2024) return 0;
y -= m <= 2; // days from 1970-01-01 (H. Hinnant's days_from_civil)
int era = y / 400, yoe = y - era * 400;
int doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + day - 1;
int doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
return (uint32_t)(era * 146097 + doe - 719468) * 86400UL;
}
void DataStore::restoreRTCTime() {
File file = openRead(_fs, "/rtc_save");
uint32_t t = 0;
if (file) {
uint32_t t = 0;
file.read((uint8_t *)&t, sizeof(t));
file.close();
#ifndef SIM_PLATFORM
// Real hardware has no other way to know the time before a GPS fix or
// a phone/CLI sync, so restoring the last-known saved time is the
// right call there. The sim's RTCClock (SimRTCClock.h) is already
// backed by the real host wall clock (time(NULL)) from the moment it's
// constructed -- overwriting that with a stale save from a previous
// visit (persisted via IDBFS, see the site's "returning visitor" note)
// would make a returning instance's on-screen clock drift away from
// the visitor's own real time instead of just showing it.
if (t > 1000000000UL) _clock->setCurrentTime(t);
#endif
}
// Saved at power-off and every half hour (MyMesh::loop()); a flash or a
// reset in between can leave it far behind -- never earlier than the build.
uint32_t built = buildDayEpoch();
if (t < built) t = built;
#ifndef SIM_PLATFORM
// Real hardware has no other way to know the time before a GPS fix or
// a phone/CLI sync, so restoring the last-known saved time is the
// right call there. The sim's RTCClock (SimRTCClock.h) is already
// backed by the real host wall clock (time(NULL)) from the moment it's
// constructed -- overwriting that with a stale save from a previous
// visit (persisted via IDBFS, see the site's "returning visitor" note)
// would make a returning instance's on-screen clock drift away from
// the visitor's own real time instead of just showing it.
// Only ever forward: a board with its own RTC chip may already know better.
if (t > 1000000000UL && t > _clock->getCurrentTime()) _clock->setCurrentTime(t);
#endif
}
void DataStore::loadContacts(DataStoreHost* host) {
+26 -4
View File
@@ -121,6 +121,9 @@
#define DIRECT_SEND_PERHOP_FACTOR 6.0f
#define DIRECT_SEND_PERHOP_EXTRA_MILLIS 250
#define LAZY_CONTACTS_WRITE_DELAY 5000
#define LAZY_PATHS_WRITE_DELAY 60000 // a path learned (a DM's ACK brings one back): no hurry
#define USER_QUIET_MILLIS 15000 // the contacts write stalls the loop: not while the keys are in use
#define RTC_SAVE_INTERVAL_MILLIS (30 * 60 * 1000UL) // the time restored at boot after a reset or a flash
#define PUBLIC_GROUP_PSK "izOH6cXN6mrJ5e26oRXNcg=="
@@ -546,7 +549,9 @@ void MyMesh::onContactPathUpdated(const ContactInfo &contact) {
memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE);
_serial->writeFrame(out_frame, 1 + PUB_KEY_SIZE); // NOTE: app may not be connected
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
// Only the path changed: lost on a reset it costs one flood, so its write
// waits; the same path heard again isn't written at all.
if (!_path_same && !dirty_paths_expiry) dirty_paths_expiry = futureMillis(LAZY_PATHS_WRITE_DELAY);
}
ContactInfo* MyMesh::processAck(const uint8_t *data) {
@@ -1356,7 +1361,11 @@ bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t i
}
}
// let base class handle received path and data
int path_bytes = (out_path_len & 63) * ((out_path_len >> 6) + 1);
_path_same = contact.out_path_len == out_path_len && path_bytes <= MAX_PATH_SIZE
&& memcmp(contact.out_path, out_path, path_bytes) == 0;
bool r = BaseChatMesh::onContactPathRecv(contact, in_path, in_path_len, out_path, out_path_len, extra_type, extra, extra_len);
_path_same = false;
// A flood DM's ACK comes back folded into the return path, which the base
// class matches without going through onAckRecv() -- so the UI's delivery
// marker never heard of it and kept resending. Tell it here too; it only
@@ -3455,9 +3464,22 @@ void MyMesh::loop() {
}
// is there are pending dirty contacts write needed?
if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) {
saveContacts();
dirty_contacts_expiry = 0;
// Rewriting the whole file blocks for up to a second or two on the nRF52:
// put off while the user is pressing keys.
if ((dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry))
|| (dirty_paths_expiry && millisHasNowPassed(dirty_paths_expiry))) {
if (millis() - _last_input_ms < USER_QUIET_MILLIS) {
if (dirty_contacts_expiry) dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
if (dirty_paths_expiry) dirty_paths_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
} else {
saveContacts();
dirty_contacts_expiry = dirty_paths_expiry = 0;
}
}
if (millis() - _last_rtc_save_ms > RTC_SAVE_INTERVAL_MILLIS && millis() - _last_input_ms >= USER_QUIET_MILLIS) {
_store->saveRTCTime();
_last_rtc_save_ms = millis();
}
if (_prefs.advert_auto_interval_sec > 0 && millisHasNowPassed(_next_auto_advert_ms)) {
+9 -1
View File
@@ -428,7 +428,11 @@ public:
// this before rebooting; UITask::shutdown() (low-battery auto-shutdown,
// long-press power-off) needs the same flush or a whole session's worth
// of learned contacts can be lost.
void flushDirtyContacts() { if (dirty_contacts_expiry) { saveContacts(); dirty_contacts_expiry = 0; } }
void flushDirtyContacts() {
if (dirty_contacts_expiry || dirty_paths_expiry) { saveContacts(); dirty_contacts_expiry = dirty_paths_expiry = 0; }
}
// A key press / touch: the lazy contacts write waits until the user pauses.
void noteUserInput() { _last_input_ms = millis(); }
DataStore* getDataStore() const { return _store; }
void applyApc(); // (re)initialise Adaptive Power Control from prefs
// Adaptive Power Control is suppressed while repeating: a repeater wants full,
@@ -649,6 +653,10 @@ private:
uint32_t sign_data_len;
uint32_t sign_data_cap; // what the malloc got: at most MAX_SIGN_DATA_LEN
unsigned long dirty_contacts_expiry;
unsigned long dirty_paths_expiry = 0; // only paths changed: written later (not pending work)
unsigned long _last_input_ms = 0;
unsigned long _last_rtc_save_ms = 0;
bool _path_same = false; // onContactPathRecv(): the path heard is the one held
unsigned long _bot_last_ch_reply_ms;
unsigned long _bot_last_room_reply_ms;
unsigned long _next_auto_advert_ms;
@@ -1561,6 +1561,7 @@ void UITask::loop() {
if (lvport::touched()) { lvport::swallowTouch(); wake(); }
}
} else {
if (lv_display_get_inactive_time(NULL) < 1000) the_mesh.noteUserInput();
// Locked, nobody's using it: the lock screen goes dark after LOCK_OFF_MS
// even with auto-off at Never (a woken-up pocket, a boot PIN, a message).
uint32_t aoff = autoOffMillis();
@@ -3663,6 +3663,7 @@ bool UITask::addWaypoint(int32_t lat, int32_t lon, const char* label) {
}
char UITask::checkDisplayOn(char c) {
the_mesh.noteUserInput();
if (_display != NULL) {
if (!_display->isOn()) {
_display->turnOn();