The T-Display P4 ships EIGHT images: panel (AMOLED/LCD) x radio (SX1262/LR2021)
x C6 stack (esp-hosted/ESP-AT). The C6 axis is new and it is the dangerous one,
because the two builds are identical apart from the artifact name they carry,
and flashing the wrong one at an older unit used to boot-loop the board.
scripts/build/p4-release.sh builds all eight and VERIFIES each image against the
OTA name baked into it before accepting it. That is the same string the device
builds its self-update URL from, so a disagreement with the filename is exactly
the failure that bricks people -- the T-Deck Max self-updating to Pro firmware,
and this table has been wrong three times. A mismatch aborts the release.
It earned that check immediately: building the eight by hand produced two
mislabelled images, and every mechanism meant to prevent it was itself broken.
* build.sh fullclean has NEVER worked from a plain shell. It ran `cmake -E
remove_directory` above the esp-idf/export.sh that puts cmake on PATH, so it
printed that it had removed the tree, exited 0, and removed nothing. Now a
plain rm -rf, and it verifies the directory is gone instead of assuming.
This matters beyond tidiness: the hosted and ESP-AT builds SHARE a build dir
(the C6 axis does not change P4_BUILD_TARGET), the backend is read from the
environment at CMake CONFIGURE time, and CMake does not reconfigure because
an env var changed -- so a clean that silently did nothing yields an
incremental build of the OTHER variant, with no configure output to show for
it. That is precisely how the two mislabelled images were produced.
* Deleting CMakeCache.txt alone is not a clean either; it leaves CMakeFiles/
behind and the next configure dies in CMakeDetermineCCompiler.
* Detecting failure by grepping a log for "error:" misses a failed CMake
configure, which prints "Configuring incomplete" and no such line -- and the
previous application.bin is still sitting in the directory to be picked up.
Exit codes now.
gen-flasher-meta.py gains the four ESP-AT entries, labelled "older unit, ESP-AT
C6": someone choosing in the web flasher has no serial log and needs to pick on
something they can check.
scripts/run-host-tests.sh runs test/ as one command. Nothing ever did, which is
why test_touch_prefs_schema had been failing to COMPILE since v64 appended
report_ping and report_done_n without updating its sums -- and a static_assert
that does not compile is not a guard. That one exists to catch the mid-struct
insert that broke v44 and shipped the poisoned beta_57 layout. Repaired, with a
note where the next person appending a field will read it. 17 pass, 0 fail, 2
skipped with their reasons stated rather than quietly dropped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
355 lines
16 KiB
C++
355 lines
16 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;
|
|
c.attaky_notify_enabled = 0;
|
|
c.attaky_notify_room_color = 0;
|
|
c.attaky_notify_dm_color = 1;
|
|
c.home_key_keeps_drawer = 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);
|
|
assert(migrated.attaky_notify_enabled == 0);
|
|
assert(migrated.attaky_notify_room_color == 0);
|
|
assert(migrated.attaky_notify_dm_color == 1);
|
|
|
|
// 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.
|
|
// EVERY sum below runs to the end of the struct, so each one has to list every
|
|
// field appended since. v64's report_ping + report_done_n were not added, so
|
|
// all five assertions stopped compiling -- and a test that does not compile is
|
|
// not a guard. This is the check that catches a mid-struct insert, which is
|
|
// what broke v44 and shipped the poisoned beta_57 layout. Append a field, add
|
|
// it here.
|
|
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::attaky_notify_enabled)
|
|
+ sizeof(Config::attaky_notify_room_color)
|
|
+ sizeof(Config::attaky_notify_dm_color)
|
|
+ sizeof(Config::telem_loc_exact)
|
|
+ sizeof(Config::home_key_keeps_drawer)
|
|
+ sizeof(Config::ble_kbd_mode) + sizeof(Config::ble_kbd_layout)
|
|
+ sizeof(Config::ble_kbd_addr) + sizeof(Config::ble_kbd_addr_type)
|
|
+ sizeof(Config::ble_kbd_name)
|
|
+ sizeof(Config::ble_kbd_back)
|
|
+ sizeof(Config::report_ping)
|
|
+ sizeof(Config::report_done_n) == 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 = 3; // Night high contrast (#544)
|
|
current.gps_fuzz_m = 150;
|
|
current.attaky_notify_enabled = 1;
|
|
current.attaky_notify_room_color = 6;
|
|
current.attaky_notify_dm_color = 4;
|
|
current.home_key_keeps_drawer = 1;
|
|
current.ble_kbd_mode = 1;
|
|
current.ble_kbd_layout = 4;
|
|
const uint8_t kbd_addr[6] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66};
|
|
memcpy(current.ble_kbd_addr, kbd_addr, sizeof kbd_addr);
|
|
current.ble_kbd_addr_type = 1;
|
|
strncpy(current.ble_kbd_name, "Test keyboard", sizeof current.ble_kbd_name - 1);
|
|
current.ble_kbd_back = 0x64;
|
|
migrated = safeDefaults();
|
|
assert(TouchPrefsSchema::overlayStored(migrated, ¤t, sizeof(current), &stored_version));
|
|
assert(stored_version == TouchPrefsSchema::CURRENT_VERSION);
|
|
assert(memcmp(&migrated, ¤t, 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 appended 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 a historical v55 blob must leave
|
|
// it at the Night default.
|
|
constexpr size_t v55_size = offsetof(Config, theme_mode);
|
|
static_assert(v55_size + sizeof(Config::theme_mode) + sizeof(Config::gps_fuzz_m)
|
|
+ sizeof(Config::attaky_notify_enabled)
|
|
+ sizeof(Config::attaky_notify_room_color)
|
|
+ sizeof(Config::attaky_notify_dm_color)
|
|
+ sizeof(Config::telem_loc_exact)
|
|
+ sizeof(Config::home_key_keeps_drawer)
|
|
+ sizeof(Config::ble_kbd_mode) + sizeof(Config::ble_kbd_layout)
|
|
+ sizeof(Config::ble_kbd_addr) + sizeof(Config::ble_kbd_addr_type)
|
|
+ sizeof(Config::ble_kbd_name)
|
|
+ sizeof(Config::ble_kbd_back)
|
|
+ sizeof(Config::report_ping)
|
|
+ sizeof(Config::report_done_n) == sizeof(Config),
|
|
"v55 is the current layout minus every byte appended since");
|
|
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);
|
|
assert(migrated.attaky_notify_enabled == 0);
|
|
assert(migrated.attaky_notify_room_color == 0);
|
|
assert(migrated.attaky_notify_dm_color == 1);
|
|
|
|
constexpr size_t v57_size = offsetof(Config, attaky_notify_enabled);
|
|
static_assert(v57_size + sizeof(Config::attaky_notify_enabled)
|
|
+ sizeof(Config::attaky_notify_room_color)
|
|
+ sizeof(Config::attaky_notify_dm_color)
|
|
+ sizeof(Config::telem_loc_exact)
|
|
+ sizeof(Config::home_key_keeps_drawer)
|
|
+ sizeof(Config::ble_kbd_mode) + sizeof(Config::ble_kbd_layout)
|
|
+ sizeof(Config::ble_kbd_addr) + sizeof(Config::ble_kbd_addr_type)
|
|
+ sizeof(Config::ble_kbd_name)
|
|
+ sizeof(Config::ble_kbd_back)
|
|
+ sizeof(Config::report_ping)
|
|
+ sizeof(Config::report_done_n) == sizeof(Config),
|
|
"v57 is the current layout minus the Attaky notification fields");
|
|
Config v57 = safeDefaults();
|
|
v57.ver = 57;
|
|
v57.gps_fuzz_m = 250;
|
|
v57.attaky_notify_enabled = 1; // outside the stored v57 extent
|
|
v57.attaky_notify_room_color = 6;
|
|
v57.attaky_notify_dm_color = 5;
|
|
migrated = safeDefaults();
|
|
assert(TouchPrefsSchema::overlayStored(migrated, &v57, v57_size, &stored_version));
|
|
assert(stored_version == 57);
|
|
assert(migrated.gps_fuzz_m == 250);
|
|
assert(migrated.attaky_notify_enabled == 0);
|
|
assert(migrated.attaky_notify_room_color == 0);
|
|
assert(migrated.attaky_notify_dm_color == 1);
|
|
|
|
// #491 appends the M9 Home-key drawer behavior at v60. A v59 blob keeps
|
|
// every prior setting, while the new option defaults OFF to preserve the
|
|
// established Commander/drawer toggle until the user opts in.
|
|
constexpr size_t v59_size = offsetof(Config, home_key_keeps_drawer);
|
|
static_assert(v59_size + sizeof(Config::home_key_keeps_drawer)
|
|
+ sizeof(Config::ble_kbd_mode) + sizeof(Config::ble_kbd_layout)
|
|
+ sizeof(Config::ble_kbd_addr) + sizeof(Config::ble_kbd_addr_type)
|
|
+ sizeof(Config::ble_kbd_name)
|
|
+ sizeof(Config::ble_kbd_back)
|
|
+ sizeof(Config::report_ping)
|
|
+ sizeof(Config::report_done_n) == sizeof(Config),
|
|
"v59 is the current layout minus the Home-key drawer option");
|
|
Config v59 = safeDefaults();
|
|
v59.ver = 59;
|
|
v59.telem_loc_exact = 1;
|
|
v59.home_key_keeps_drawer = 1; // outside the stored v59 extent
|
|
migrated = safeDefaults();
|
|
assert(TouchPrefsSchema::overlayStored(migrated, &v59, v59_size, &stored_version));
|
|
assert(stored_version == 59);
|
|
assert(migrated.telem_loc_exact == 1);
|
|
assert(migrated.home_key_keeps_drawer == 0);
|
|
|
|
// v61 appends the Bluetooth keyboard settings. A v60 blob keeps the Home-key
|
|
// option, and Bluetooth stays with the phone app with no keyboard paired,
|
|
// whatever bytes followed the old blob.
|
|
constexpr size_t v60_size = offsetof(Config, ble_kbd_mode);
|
|
static_assert(v60_size + sizeof(Config::ble_kbd_mode) + sizeof(Config::ble_kbd_layout)
|
|
+ sizeof(Config::ble_kbd_addr) + sizeof(Config::ble_kbd_addr_type)
|
|
+ sizeof(Config::ble_kbd_name)
|
|
+ sizeof(Config::ble_kbd_back)
|
|
+ sizeof(Config::report_ping)
|
|
+ sizeof(Config::report_done_n) == sizeof(Config),
|
|
"v60 is the current layout minus the Bluetooth keyboard settings");
|
|
Config v60 = safeDefaults();
|
|
v60.ver = 60;
|
|
v60.home_key_keeps_drawer = 1;
|
|
v60.ble_kbd_mode = 1; // outside the stored v60 extent
|
|
v60.ble_kbd_layout = 3;
|
|
v60.ble_kbd_addr[0] = 0xAA;
|
|
migrated = safeDefaults();
|
|
assert(TouchPrefsSchema::overlayStored(migrated, &v60, v60_size, &stored_version));
|
|
assert(stored_version == 60);
|
|
assert(migrated.home_key_keeps_drawer == 1);
|
|
assert(migrated.ble_kbd_mode == 0);
|
|
assert(migrated.ble_kbd_layout == 0);
|
|
assert(migrated.ble_kbd_addr[0] == 0);
|
|
assert(migrated.ble_kbd_name[0] == '\0');
|
|
assert(migrated.ble_kbd_back == 0);
|
|
|
|
// v62 appends the extra Back key. A v61 blob keeps the paired keyboard and
|
|
// its settings; only Esc goes back until the user picks another key.
|
|
constexpr size_t v61_size = offsetof(Config, ble_kbd_back);
|
|
static_assert(v61_size + sizeof(Config::ble_kbd_back)
|
|
+ sizeof(Config::report_ping)
|
|
+ sizeof(Config::report_done_n) == sizeof(Config),
|
|
"v61 is the current layout minus the Back key");
|
|
Config v61 = safeDefaults();
|
|
v61.ver = 61;
|
|
v61.ble_kbd_mode = 1;
|
|
v61.ble_kbd_layout = 4;
|
|
v61.ble_kbd_addr[5] = 0x4D;
|
|
v61.ble_kbd_addr_type = 1;
|
|
strncpy(v61.ble_kbd_name, "Sologic - Keyboard", sizeof v61.ble_kbd_name - 1);
|
|
v61.ble_kbd_back = 0x64; // outside the stored v61 extent
|
|
migrated = safeDefaults();
|
|
assert(TouchPrefsSchema::overlayStored(migrated, &v61, v61_size, &stored_version));
|
|
assert(stored_version == 61);
|
|
assert(migrated.ble_kbd_mode == 1);
|
|
assert(migrated.ble_kbd_layout == 4);
|
|
assert(migrated.ble_kbd_addr[5] == 0x4D);
|
|
assert(migrated.ble_kbd_addr_type == 1);
|
|
assert(strcmp(migrated.ble_kbd_name, "Sologic - Keyboard") == 0);
|
|
assert(migrated.ble_kbd_back == 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;
|
|
}
|