Files
wadamesh/test/test_touch_prefs_schema.cpp
T
Kaj SchittecatandClaude Opus 5 ee15fcb335 Fix two beta_76 regressions of mine, and advertise a position without your address
#415: a Lua app was covered by the app drawer whenever a message arrived.
Reported by jadestarwatcher, reproduced by mysterywavi, both on T-Deck. Mine,
from the #393 badge fix. Apps launched from the drawer deliberately leave it
alive underneath (appTileCb says so outright), and my badge refresh rebuilt it on
every unread-count change, with openAppDrawer() ending in a move_foreground that
threw it on top of the running app.

The refresh now runs only when nothing is drawn in front of the drawer, tested by
sibling order rather than by listing the tools, so a tool added later is covered
without anyone remembering to update a list. The status bar is excluded, since it
legitimately floats above the drawer at all times and treating it as covering
would have stopped the badges refreshing at all. Leaving the signature stale is
what makes it self-healing: the check runs again each tick, so the count is right
by the time the drawer is back in front, with no need to hook every close path.
Scroll position is now preserved across the rebuild too, which my change had also
been resetting.

#410: on the M9 the accent variants appeared but could not be selected. Also mine,
from the #387 work. The picker was handled below m9HandleArrowKey, which takes
LEFT and RIGHT for the caret and returns, so the arrows never reached it: the box
was drawn and nothing could touch it. Lifted above that call, as its own function
rather than a second copy of the logic.

#399, requested by @Danie10: an option to advertise a position near you rather
than your address. Settings, GPS, cycling exact / 100 m / 250 m / 1 km.

The displacement is derived from the node identity, so it is the same offset
every time. That is the whole point rather than an implementation detail: a fresh
random offset per advert would scatter points around the true position, and
averaging a night of them would recover the centre exactly. A fixed displacement
instead looks like a node that sits somewhere else, which is what a manually set
location already looks like. It applies only to our own adverts; the map and the
GPS page keep the real fix, because the aim is to tell other people less, not to
lie to yourself.

Schema v57. The host test caught the new field the moment it was added, which is
what it is for.

All nine S3 envs and both ESP32-P4 targets green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 09:57:25 +02:00

208 lines
8.3 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
#include <assert.h>
#include <string.h>
#include "helpers/esp32/TouchPrefsSchema.h"
using TouchPrefsSchema::Config;
static Config safeDefaults() {
Config c = {};
c.magic = TouchPrefsSchema::MAGIC;
c.ver = TouchPrefsSchema::CURRENT_VERSION;
c.bright = 100;
c.rx_queue = 1;
c.web_mirror = 0;
c.remote_mode = 0;
c.remote_landscape = 1;
c.web_terminal = 0;
c.map_tile_debug = 0;
c.hist_sync_after = 2;
c.hist_per_chat = 250;
c.p4_antenna = 0;
c.retry_echo = 1;
c.console_monitor = 1;
c.boot_wifi_time = 0;
c.boot_wifi_open = 0;
c.theme_mode = 0;
c.gps_fuzz_m = 0;
return c;
}
int main() {
constexpr size_t v43_size = offsetof(Config, retry_echo);
constexpr size_t suffix_offset = offsetof(Config, web_mirror);
constexpr size_t v43_suffix_size = v43_size - suffix_offset;
static_assert(offsetof(Config, retry_echo) ==
offsetof(Config, p4_antenna) + sizeof(Config::p4_antenna),
"v45 retry field moved");
// The tail is append-only, so a blob written by an older firmware is always a
// strict PREFIX of the current layout. Asserting an exact size delta pins the
// test to one release and goes stale the moment the next field lands (it did:
// this used to demand "v45 appends exactly one byte"). Assert the invariant
// that actually matters instead — every historical blob is shorter, and the
// fields it never stored keep their defaults.
static_assert(v43_size < sizeof(Config), "v43 blobs must be a strict prefix of the current layout");
// A v43 blob has the current layout truncated before the newly appended
// retry byte. Every established field must survive byte-for-byte, and every
// field appended since must fail safe to its default.
Config v43 = safeDefaults();
v43.ver = 43;
v43.bright = 67;
v43.rx_queue = 0;
v43.web_mirror = 1;
v43.remote_mode = 1;
v43.remote_landscape = 0;
v43.web_terminal = 1;
v43.map_tile_debug = 1;
v43.hist_sync_after = 7;
v43.hist_per_chat = 0x1234;
v43.p4_antenna = 0x56;
v43.retry_echo = 0; // outside the stored v43 extent
v43.boot_wifi_time = 1; // ditto — must NOT reach the migrated config
v43.boot_wifi_open = 1;
Config migrated = safeDefaults();
uint8_t stored_version = 0;
assert(TouchPrefsSchema::overlayStored(migrated, &v43, v43_size, &stored_version));
assert(stored_version == 43);
assert(migrated.bright == 67);
assert(migrated.rx_queue == 0);
assert(migrated.web_mirror == 1);
assert(migrated.remote_mode == 1);
assert(migrated.remote_landscape == 0);
assert(migrated.web_terminal == 1);
assert(migrated.map_tile_debug == 1);
assert(migrated.hist_sync_after == 7);
assert(migrated.hist_per_chat == 0x1234);
assert(migrated.p4_antenna == 0x56);
assert(migrated.retry_echo == 1);
assert(migrated.boot_wifi_time == 0);
assert(migrated.boot_wifi_open == 0);
assert(migrated.loud_alerts == 0);
assert(migrated.theme_mode == 0);
assert(migrated.gps_fuzz_m == 0);
// Reproduce beta 57 exactly: retry was inserted before the old suffix, then
// the v43 bytes were copied and the full shifted v44 structure was written.
uint8_t broken_v44[sizeof(Config)] = {};
memcpy(broken_v44, &v43, suffix_offset);
broken_v44[offsetof(Config, ver)] = TouchPrefsSchema::BROKEN_MID_INSERT_VERSION;
broken_v44[suffix_offset] = 1; // inserted retry_echo, overwriting old web_mirror
memcpy(broken_v44 + suffix_offset + 1,
reinterpret_cast<const uint8_t*>(&v43) + suffix_offset,
v43_suffix_size);
const Config defaults = safeDefaults();
migrated = defaults;
stored_version = 0;
assert(TouchPrefsSchema::overlayStored(migrated, broken_v44,
sizeof(broken_v44), &stored_version));
assert(stored_version == 44);
assert(migrated.bright == 67); // unambiguous prefix is preserved
assert(migrated.rx_queue == 0);
assert(memcmp(reinterpret_cast<const uint8_t*>(&migrated) + suffix_offset,
reinterpret_cast<const uint8_t*>(&defaults) + suffix_offset,
sizeof(Config) - suffix_offset) == 0); // ambiguous suffix fails safe
// #383 appended boot_wifi_time + boot_wifi_open at the tail. A v53 blob is the
// current layout minus exactly those two bytes: everything before them must
// survive, and both opt-ins must come back OFF rather than inheriting whatever
// byte followed the blob — an inherited 1 would spend boot time on a Wi-Fi
// session the user never asked for.
constexpr size_t v53_size = offsetof(Config, boot_wifi_time);
static_assert(v53_size + sizeof(Config::boot_wifi_time) + sizeof(Config::boot_wifi_open)
+ sizeof(Config::loud_alerts) + sizeof(Config::theme_mode)
+ sizeof(Config::gps_fuzz_m) == sizeof(Config),
"v53 is the current layout minus every byte appended since");
Config v53 = safeDefaults();
v53.ver = 53;
v53.bright = 42;
v53.retry_echo = 0;
v53.console_mode = 1;
v53.console_monitor = 0;
v53.kb_force_legacy = 1;
v53.boot_wifi_time = 1; // outside the stored v53 extent
v53.boot_wifi_open = 1;
migrated = safeDefaults();
migrated.boot_wifi_time = 0;
migrated.boot_wifi_open = 0;
stored_version = 0;
assert(TouchPrefsSchema::overlayStored(migrated, &v53, v53_size, &stored_version));
assert(stored_version == 53);
assert(migrated.bright == 42);
assert(migrated.retry_echo == 0);
assert(migrated.console_mode == 1);
assert(migrated.console_monitor == 0);
assert(migrated.kb_force_legacy == 1);
assert(migrated.boot_wifi_time == 0);
assert(migrated.boot_wifi_open == 0);
// Once rewritten at the current version the whole structure is authoritative,
// including an explicit retry opt-out, a deliberately enabled Remote Mode, and
// both #383 opt-ins turned on.
Config current = safeDefaults();
current.remote_mode = 1;
current.remote_landscape = 0;
current.retry_echo = 0;
current.boot_wifi_time = 1;
current.boot_wifi_open = 1;
current.loud_alerts = 1;
current.theme_mode = 1;
current.gps_fuzz_m = 150;
migrated = safeDefaults();
assert(TouchPrefsSchema::overlayStored(migrated, &current, sizeof(current), &stored_version));
assert(stored_version == TouchPrefsSchema::CURRENT_VERSION);
assert(memcmp(&migrated, &current, sizeof(current)) == 0);
// Once rewritten as v45, every field through retry_echo is authoritative;
// fields appended by later versions retain their safe defaults.
constexpr size_t v45_size = offsetof(Config, app_hide);
Config v45 = safeDefaults();
v45.ver = 45;
v45.remote_mode = 1;
v45.remote_landscape = 0;
v45.retry_echo = 0;
migrated = safeDefaults();
assert(TouchPrefsSchema::overlayStored(migrated, &v45, v45_size, &stored_version));
assert(stored_version == 45);
assert(migrated.remote_mode == 1);
assert(migrated.remote_landscape == 0);
assert(migrated.retry_echo == 0);
assert(migrated.app_hide == safeDefaults().app_hide);
assert(migrated.theme_mode == 0);
// v56 appends only theme_mode. It was written as v54 on the contributing
// branch, but v54 and v55 were taken by boot_wifi_* and loud_alerts before it
// merged, so the field sits behind those and the blob one version short of
// current is the one that must leave it at the Night default.
constexpr size_t v55_size = offsetof(Config, theme_mode);
static_assert(offsetof(Config, gps_fuzz_m) + sizeof(Config::gps_fuzz_m) == sizeof(Config),
"gps_fuzz_m must remain last");
static_assert(v55_size + sizeof(Config::theme_mode) + sizeof(Config::gps_fuzz_m) == sizeof(Config),
"v55 is the current layout minus theme_mode and gps_fuzz_m");
Config v55 = safeDefaults();
v55.ver = 55;
v55.kb_force_legacy = 1;
v55.loud_alerts = 1;
v55.theme_mode = 1; // outside the stored v55 extent
migrated = safeDefaults();
assert(TouchPrefsSchema::overlayStored(migrated, &v55, v55_size, &stored_version));
assert(stored_version == 55);
assert(migrated.kb_force_legacy == 1);
assert(migrated.loud_alerts == 1);
assert(migrated.theme_mode == 0);
assert(migrated.gps_fuzz_m == 0);
Config invalid = safeDefaults();
uint8_t garbage[sizeof(Config)] = {};
assert(!TouchPrefsSchema::overlayStored(invalid, garbage, sizeof(garbage)));
assert(memcmp(&invalid, &defaults, sizeof(defaults)) == 0);
return 0;
}