From b9006d14ddb7b10b1af7b8ff1b6b1c0312193d76 Mon Sep 17 00:00:00 2001 From: Jared Dohrman Date: Thu, 9 Jul 2026 17:19:05 +1000 Subject: [PATCH] fix(prefs): move EastMesh NodePrefs fields behind a reserved gap Upstream's persisted fields end at 295. EastMesh's own fields sat immediately after, so any future upstream append would shift them and conflict. Base the EastMesh block at 512 instead, with the 295..511 gap reserved for upstream growth. Upstream can now append fields with no change to EastMesh offsets; a static_assert fails the build if upstream ever overruns the gap. EastMesh fields now occupy 512..741 (was 295..524). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/helpers/CommonCLI.cpp | 73 ++++++++++++++++++++++++++++++--------- 1 file changed, 57 insertions(+), 16 deletions(-) diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 0ab1bef7..90721033 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -27,6 +27,39 @@ static bool isValidName(const char *n) { return true; } +// NodePrefs is persisted at fixed byte offsets, so a field's position is fixed by the +// order it is written, not by its position in the struct. Upstream's fields keep their +// upstream offsets and end at PREFS_UPSTREAM_END; EastMesh-only fields live above +// PREFS_EASTMESH_BASE, behind a reserved gap, so upstream can append fields without +// shifting them. Raise PREFS_UPSTREAM_END when upstream's block grows. +#define PREFS_UPSTREAM_END 295 +#define PREFS_EASTMESH_BASE 512 + +static_assert(PREFS_UPSTREAM_END <= PREFS_EASTMESH_BASE, + "upstream NodePrefs fields have overrun the EastMesh reserved gap"); + +static void writePrefsGap(File& file, int num_bytes) { + uint8_t pad[16]; + memset(pad, 0, sizeof(pad)); + while (num_bytes > 0) { + int n = num_bytes < (int)sizeof(pad) ? num_bytes : (int)sizeof(pad); + file.write(pad, n); + num_bytes -= n; + } +} + +static void skipPrefsGap(File& file, int num_bytes) { + uint8_t pad[16]; + while (num_bytes > 0) { + int avail = file.available(); + if (avail <= 0) break; // prefs file predates the gap; leave defaults in place + int n = num_bytes < (int)sizeof(pad) ? num_bytes : (int)sizeof(pad); + if (n > avail) n = avail; + file.read(pad, n); + num_bytes -= n; + } +} + void CommonCLI::loadPrefs(FILESYSTEM* fs) { if (fs->exists("/com_prefs")) { loadPrefsInt(fs, "/com_prefs"); // new filename @@ -93,26 +126,30 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292 file.read((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 293 file.read((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294 - // upstream layout ends at 295; EastMesh-only fields are appended below + // upstream layout ends here (PREFS_UPSTREAM_END) + + skipPrefsGap(file, PREFS_EASTMESH_BASE - PREFS_UPSTREAM_END); // reserved for upstream growth + + // EastMesh-only fields, based at PREFS_EASTMESH_BASE if (file.available() >= (int)sizeof(_prefs->fan_mode)) { - file.read((uint8_t *)&_prefs->fan_mode, sizeof(_prefs->fan_mode)); // 295 + file.read((uint8_t *)&_prefs->fan_mode, sizeof(_prefs->fan_mode)); // 512 } if (file.available() >= (int)sizeof(_prefs->fan_timeout_secs)) { - file.read((uint8_t *)&_prefs->fan_timeout_secs, sizeof(_prefs->fan_timeout_secs)); // 296 + file.read((uint8_t *)&_prefs->fan_timeout_secs, sizeof(_prefs->fan_timeout_secs)); // 513 } if (file.available() >= (int)sizeof(_prefs->bridge_peer_host)) { - file.read((uint8_t *)&_prefs->bridge_peer_host, sizeof(_prefs->bridge_peer_host)); // 298 + file.read((uint8_t *)&_prefs->bridge_peer_host, sizeof(_prefs->bridge_peer_host)); // 515 } if (file.available() >= (int)sizeof(_prefs->bridge_peer_port)) { - file.read((uint8_t *)&_prefs->bridge_peer_port, sizeof(_prefs->bridge_peer_port)); // 362 + file.read((uint8_t *)&_prefs->bridge_peer_port, sizeof(_prefs->bridge_peer_port)); // 579 } if (file.available() >= (int)sizeof(_prefs->bridge_peer_username)) { - file.read((uint8_t *)&_prefs->bridge_peer_username, sizeof(_prefs->bridge_peer_username)); // 364 + file.read((uint8_t *)&_prefs->bridge_peer_username, sizeof(_prefs->bridge_peer_username)); // 581 } if (file.available() >= (int)sizeof(_prefs->bridge_peer_password)) { - file.read((uint8_t *)&_prefs->bridge_peer_password, sizeof(_prefs->bridge_peer_password)); // 429 + file.read((uint8_t *)&_prefs->bridge_peer_password, sizeof(_prefs->bridge_peer_password)); // 646 } - // next: 525 + // next: 742 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -211,14 +248,18 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292 file.write((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 293 file.write((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294 - // upstream layout ends at 295; EastMesh-only fields are appended below - file.write((uint8_t *)&_prefs->fan_mode, sizeof(_prefs->fan_mode)); // 295 - file.write((uint8_t *)&_prefs->fan_timeout_secs, sizeof(_prefs->fan_timeout_secs)); // 296 - file.write((uint8_t *)&_prefs->bridge_peer_host, sizeof(_prefs->bridge_peer_host)); // 298 - file.write((uint8_t *)&_prefs->bridge_peer_port, sizeof(_prefs->bridge_peer_port)); // 362 - file.write((uint8_t *)&_prefs->bridge_peer_username, sizeof(_prefs->bridge_peer_username)); // 364 - file.write((uint8_t *)&_prefs->bridge_peer_password, sizeof(_prefs->bridge_peer_password)); // 429 - // next: 525 + // upstream layout ends here (PREFS_UPSTREAM_END) + + writePrefsGap(file, PREFS_EASTMESH_BASE - PREFS_UPSTREAM_END); // reserved for upstream growth + + // EastMesh-only fields, based at PREFS_EASTMESH_BASE + file.write((uint8_t *)&_prefs->fan_mode, sizeof(_prefs->fan_mode)); // 512 + file.write((uint8_t *)&_prefs->fan_timeout_secs, sizeof(_prefs->fan_timeout_secs)); // 513 + file.write((uint8_t *)&_prefs->bridge_peer_host, sizeof(_prefs->bridge_peer_host)); // 515 + file.write((uint8_t *)&_prefs->bridge_peer_port, sizeof(_prefs->bridge_peer_port)); // 579 + file.write((uint8_t *)&_prefs->bridge_peer_username, sizeof(_prefs->bridge_peer_username)); // 581 + file.write((uint8_t *)&_prefs->bridge_peer_password, sizeof(_prefs->bridge_peer_password)); // 646 + // next: 742 file.close(); }