touch: beta_35 — worker-thread history flush, radio memory guards, 30.5 KB DRAM freed, crash-safe prefs

- History flush runs on the core-0 worker (snapshot + storage-busy gate +
  bounded shutdown wait): kills the V4's multi-second ui:hist stalls and
  the refuses-to-wake-after-message symptom (SPIFFS GC off the UI thread).
- BLE + Wi-Fi runtime enables share the boot co-init heap guard (50 KB
  free + 20 KB block): refuse with a toast + revert the switch instead of
  panicking (BLE) or silently not starting while claiming on (Wi-Fi).
- Internal DRAM: static footprint 95,365 -> 64,908 B (30.5 KB freed):
  serial_interface -> PSRAM; 23 keyboard layout maps const'd -> flash;
  nine rings/tables -> psAlloc PSRAM (UITask/TouchPrefsStore/MyMesh);
  unused TinyUSB MSC + DFU-mode class drivers stubbed out of the link
  (usb_unused_class_stubs.c) — CDC + runtime-DFU untouched, double-flash
  verified over the stubbed stack.
- DataStore prefs writes are crash-safe (write .tmp, swap) with a
  self-healing loader (.tmp recovery) and a truthful save result up to
  the profile-name toast — fixes the boots-with-default-name-once report.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kaj Schittecat
2026-07-06 13:34:24 +02:00
co-authored by Claude Fable 5
parent 4e1ac2ca6b
commit 01900296f3
12 changed files with 394 additions and 99 deletions
+23 -3
View File
@@ -242,10 +242,19 @@ bool DataStore::saveMainIdentity(const mesh::LocalIdentity &identity) {
void DataStore::loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon) {
if (_fs->exists(_rp("/new_prefs"))) {
loadPrefsInt("/new_prefs", prefs, node_lat, node_lon); // new filename
} else if (_fs->exists(_rp("/new_prefs.tmp"))) {
// Main file gone but a staged copy exists: a reboot landed between the temp
// write and the swap (or the swap was torn). Recover from it — this is the
// "device booted with the default name once" failure mode.
MESH_DEBUG_PRINTLN("DataStore: /new_prefs missing, recovering from .tmp");
loadPrefsInt("/new_prefs.tmp", prefs, node_lat, node_lon);
savePrefs(prefs, node_lat, node_lon); // re-establish the main file
} else if (_fs->exists(_rp("/node_prefs"))) {
loadPrefsInt("/node_prefs", prefs, node_lat, node_lon);
savePrefs(prefs, node_lat, node_lon); // save to new filename
_fs->remove(_rp("/node_prefs")); // remove old
} else {
MESH_DEBUG_PRINTLN("DataStore: no prefs file found — using defaults");
}
}
@@ -339,8 +348,12 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
}
}
void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_lon) {
File file = openWrite(_fs, "/new_prefs");
bool DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_lon) {
// Write to a temp file first and swap it in afterwards: a reboot / power cut
// mid-write can then never destroy the only copy (the loader recovers from
// whichever file survived). SPIFFS has no atomic rename-over, so the swap is
// remove+rename — the loader handles the tiny between-steps window too.
File file = openWrite(_fs, "/new_prefs.tmp");
if (file) {
uint8_t pad[8];
memset(pad, 0, sizeof(pad));
@@ -377,10 +390,17 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
file.write((uint8_t *)&_prefs.autoadd_max_hops, sizeof(_prefs.autoadd_max_hops)); // 91
file.write((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 92
file.write((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 93
file.write((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 125
size_t last = file.write((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 125
file.close();
if (last != sizeof(_prefs.default_scope_key)) {
_fs->remove(_rp("/new_prefs.tmp")); // short write (storage full?) — keep the old main file
return false;
}
_fs->remove(_rp("/new_prefs"));
return _fs->rename(_rp("/new_prefs.tmp"), _rp("/new_prefs"));
}
return false;
}
void DataStore::loadContacts(DataStoreHost* host) {