fix(companion): prefs schema corruption, broken multi-scope, stale relay filter, cursor row

Follow-up review of f589b9b2 -- five defects in that commit's own changes.

- repeat_scope_only + repeat_extra_scopes were read/written in the MIDDLE of
  the prefs stream, beside their repeat_* siblings. loadPrefsInt()'s rd() is a
  plain sequential reader gated only on file.available(), with no per-field
  versioning, so on any pre-existing file those 25 bytes were taken from the
  fields that follow, shifting EVERY later field: repeater profile (incl. a
  float freq), track_shared_loc, all of loc_share_*, trail, bot, GPIO modes.
  Moved to the struct/file tail, sentinel bumped to 0xC0DE0027 with 0xC0DE0026
  marked burned. sizeof stays 2752 (confirmed by build); the tripwire procedure
  now spells out the append-only rule that "in struct order" left implicit.
- rebuildRepeatScopes() called getAutoKeyFor() with id 0 for every entry, but
  that cache is keyed on the id alone and ignores the name on a hit -- so every
  extra scope after the first silently got the first one's key, making the
  comma-separated list do nothing. Distinct id per scope now.
- interference_threshold had no load clamp, so an upgrader read 0x23 (35) out
  of the old file's sentinel tail instead of 0.
- CMD_SET_DEFAULT_FLOOD_SCOPE wrote default_scope_key without rebuilding the
  relay filter, so setting or clearing the scope from the app left the repeater
  filtering on the previous key until reboot. The on-device path already did.
- The keyboard preview derived the cursor's row a second time from byte
  offsets, disagreeing with the cursor_line the scroll window already computes:
  it pinned the cursor to the end of a full line (drawing '_' one character
  past the display width) at every wrap boundary. Use cursor_line directly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-08-21 11:02:05 +02:00
co-authored by Claude Opus 5
parent f589b9b2d1
commit 51c06b78c1
4 changed files with 77 additions and 40 deletions
+22 -16
View File
@@ -383,18 +383,8 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
(_prefs.repeat_min_snr < -20 || _prefs.repeat_min_snr > 10))
_prefs.repeat_min_snr = NodePrefs::REPEAT_SNR_DISABLED; // match the UI's -20..10 range
if (_prefs.repeat_suppress_dup > 1) _prefs.repeat_suppress_dup = 0;
// → 0xC0DE0025: append repeat_scope_only right after the other repeater
// forwarding filters. A pre-0x25 file has no byte here; clamp to 0 (off,
// unchanged forwarding behaviour for upgraders).
rd(&_prefs.repeat_scope_only, sizeof(_prefs.repeat_scope_only));
if (_prefs.repeat_scope_only > 1) _prefs.repeat_scope_only = 0;
// → 0xC0DE0026: append repeat_extra_scopes right after it. A pre-0x26 file
// has no bytes here; rd() zero-inits, which is already an empty string.
rd(_prefs.repeat_extra_scopes, sizeof(_prefs.repeat_extra_scopes));
_prefs.repeat_extra_scopes[sizeof(_prefs.repeat_extra_scopes) - 1] = '\0';
// NOTE: repeat_scope_only/repeat_extra_scopes are read at the TAIL, not here
// beside their siblings — see the append-only rule in NodePrefs.h.
rd(&_prefs.repeater_use_profile, sizeof(_prefs.repeater_use_profile));
rd(&_prefs.repeater_freq, sizeof(_prefs.repeater_freq));
rd(&_prefs.repeater_bw, sizeof(_prefs.repeater_bw));
@@ -582,12 +572,27 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
if (_prefs.keyboard_cardkb_compact > 1) _prefs.keyboard_cardkb_compact = 0;
// → 0xC0DE0024: append interference_threshold + cad_enabled at the tail.
// A pre-0x24 file has no bytes here; clamp to 0 (both off, matching the
// getters' previous hardcoded behaviour for upgraders).
// A pre-0x24 file has no bytes here, so these read that file's own 4-byte
// sentinel tail (0xC0DE0023 → 23 00 DE C0) rather than zeroes; clamp both to
// 0/off, matching the getters' previous hardcoded behaviour for upgraders.
// interference_threshold is dB above the measured noise floor (see
// RadioLibWrapper::isChannelActive()); anything past ~30 dB would never
// trigger anyway, so treat it as a stray byte and fall back to off.
rd(&_prefs.interference_threshold, sizeof(_prefs.interference_threshold));
if (_prefs.interference_threshold > 30) _prefs.interference_threshold = 0;
rd(&_prefs.cad_enabled, sizeof(_prefs.cad_enabled));
if (_prefs.cad_enabled > 1) _prefs.cad_enabled = 0;
// → 0xC0DE0027: append repeat_scope_only + repeat_extra_scopes at the tail.
// (0xC0DE0026 had them mid-stream, which shifted every later field by 25
// bytes when reading an older file — see NodePrefs.h.) A pre-0x27 file has
// no bytes here; clamp the flag to 0 (off, unchanged forwarding behaviour)
// and leave the name list zero-initialised, which is already an empty string.
rd(&_prefs.repeat_scope_only, sizeof(_prefs.repeat_scope_only));
if (_prefs.repeat_scope_only > 1) _prefs.repeat_scope_only = 0;
rd(_prefs.repeat_extra_scopes, sizeof(_prefs.repeat_extra_scopes));
_prefs.repeat_extra_scopes[sizeof(_prefs.repeat_extra_scopes) - 1] = '\0';
// Schema sentinel: bumped on layout changes. Mismatch means an older file
// (or a different schema); rd() already zero-inits any fields not present,
// so we just log it — next savePrefs writes the current sentinel.
@@ -748,8 +753,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
file.write((uint8_t *)&_prefs.repeat_delay_boost, sizeof(_prefs.repeat_delay_boost));
file.write((uint8_t *)&_prefs.repeat_min_snr, sizeof(_prefs.repeat_min_snr));
file.write((uint8_t *)&_prefs.repeat_suppress_dup, sizeof(_prefs.repeat_suppress_dup));
file.write((uint8_t *)&_prefs.repeat_scope_only, sizeof(_prefs.repeat_scope_only));
file.write((uint8_t *)_prefs.repeat_extra_scopes, sizeof(_prefs.repeat_extra_scopes));
// repeat_scope_only/repeat_extra_scopes are written at the TAIL — see loadPrefsInt().
file.write((uint8_t *)&_prefs.repeater_use_profile, sizeof(_prefs.repeater_use_profile));
file.write((uint8_t *)&_prefs.repeater_freq, sizeof(_prefs.repeater_freq));
file.write((uint8_t *)&_prefs.repeater_bw, sizeof(_prefs.repeater_bw));
@@ -804,6 +808,8 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
file.write((uint8_t *)&_prefs.keyboard_cardkb_compact, sizeof(_prefs.keyboard_cardkb_compact));
file.write((uint8_t *)&_prefs.interference_threshold, sizeof(_prefs.interference_threshold));
file.write((uint8_t *)&_prefs.cad_enabled, sizeof(_prefs.cad_enabled));
file.write((uint8_t *)&_prefs.repeat_scope_only, sizeof(_prefs.repeat_scope_only));
file.write((uint8_t *)_prefs.repeat_extra_scopes, sizeof(_prefs.repeat_extra_scopes));
// Tail sentinel — must be last. See NodePrefs::SCHEMA_SENTINEL. Its write is
// the one we check: once the flash fills, writes return 0, so a good