mirror of
https://github.com/ALLFATHER-BV/wadamesh.git
synced 2026-09-23 15:45:38 +00:00
Bluetooth serves either the phone app or a keyboard (Settings > Bluetooth). Pairing lists keyboards in pairing mode and pairs with or without a code; the paired keyboard reconnects by itself. Keys are read the way phones and computers read them: the keyboard's Report Map says which report carries the keys and how, so media keys and touchpads are left alone, and the boot protocol is only the fallback. Key positions are translated with the chosen layout (US, UK, German, French, Belgian), with AltGr and dead keys. While a keyboard is connected: - text goes into the focused field, and touchscreen-only boards keep the on-screen keyboard down (a second tap on a field brings it up anyway) - the arrows, Tab, Enter and Page Up/Down drive the focus highlight - Esc is the back button; a "Back key" setting picks another key for keyboards whose Esc key types a character - tab hotkeys and their hints over the tab bar are switched on - Command tapped alone opens the emoji picker in a chat - the status bar shows a keyboard instead of the Bluetooth glyph The Bluetooth page is now a flex column with the choice on top, and the settings page refit keeps it scrollable when its content grows. Settings: prefs v61 (mode, layout, paired keyboard) and v62 (Back key). Tests: test/test_hid_report_map.cpp, test_touch_prefs_schema.cpp. Not on the P4 boards (Tanmatsu, T-Display P4). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
337 lines
15 KiB
C++
337 lines
15 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.
|
|
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),
|
|
"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;
|
|
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),
|
|
"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),
|
|
"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),
|
|
"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),
|
|
"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),
|
|
"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;
|
|
}
|