diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index c0402c3d..a18a37c0 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -319,7 +319,7 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no rd(&_prefs.clock_12h, sizeof(_prefs.clock_12h)); rd(&_prefs.use_lemon_font, sizeof(_prefs.use_lemon_font)); rd(&_prefs.display_rotation, sizeof(_prefs.display_rotation)); - rd(_prefs.page_order, sizeof(_prefs.page_order)); + rd(_prefs.page_order, NodePrefs::PAGE_ORDER_LEN_V1); // tail slots read below (append-only) rd(&_prefs.joystick_rotation, sizeof(_prefs.joystick_rotation)); #if !FEAT_JOYSTICK_ROTATION_SETTING // No UI to change it on this build, so force the default — this also corrects @@ -461,6 +461,17 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no // which is out of range — fall back to the default of 2 resends. if (_prefs.dm_resend_count > 5) _prefs.dm_resend_count = 2; + // → 0xC0DE0019: page_order grew 11 → 13 so Shutdown and Map become reorderable. + // The extra slots are appended here at the tail (not inline) so a pre-0x19 save, + // whose order ended right after the old 11 bytes, still loads without shifting + // every field after it. On such files these bytes are the old sentinel tail or + // EOF, so clamp anything out of range back to 0 (empty); ensurePageOrderInit + // then appends the missing pages into the freed slots. + for (uint8_t i = NodePrefs::PAGE_ORDER_LEN_V1; i < NodePrefs::PAGE_ORDER_LEN; i++) { + rd(&_prefs.page_order[i], sizeof(_prefs.page_order[i])); + if (_prefs.page_order[i] > NodePrefs::HPB_COUNT) _prefs.page_order[i] = 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. @@ -586,7 +597,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_ file.write((uint8_t *)&_prefs.clock_12h, sizeof(_prefs.clock_12h)); file.write((uint8_t *)&_prefs.use_lemon_font, sizeof(_prefs.use_lemon_font)); file.write((uint8_t *)&_prefs.display_rotation, sizeof(_prefs.display_rotation)); - file.write((uint8_t *)_prefs.page_order, sizeof(_prefs.page_order)); + file.write((uint8_t *)_prefs.page_order, NodePrefs::PAGE_ORDER_LEN_V1); // head; tail slots written below file.write((uint8_t *)&_prefs.joystick_rotation, sizeof(_prefs.joystick_rotation)); file.write((uint8_t *)&_prefs.eink_full_refresh_every, sizeof(_prefs.eink_full_refresh_every)); file.write((uint8_t *)&_prefs.page_order_set, sizeof(_prefs.page_order_set)); @@ -641,6 +652,10 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_ file.write((uint8_t *)&_prefs.alarm_on, sizeof(_prefs.alarm_on)); file.write((uint8_t *)&_prefs.alarm_hour, sizeof(_prefs.alarm_hour)); file.write((uint8_t *)&_prefs.alarm_min, sizeof(_prefs.alarm_min)); + // page_order tail slots (see loadPrefsInt): entries beyond PAGE_ORDER_LEN_V1, + // appended here so the on-disk head stays the original 11 bytes. + file.write((uint8_t *)&_prefs.page_order[NodePrefs::PAGE_ORDER_LEN_V1], + NodePrefs::PAGE_ORDER_LEN - NodePrefs::PAGE_ORDER_LEN_V1); // 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 diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index d28408d3..54fd1206 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -125,9 +125,11 @@ struct NodePrefs { // persisted to file // Home screen page order: each byte = HomePageBit + 1. 0 terminates the list. // Validity gated by page_order_set magic (see below) — not by entry value range, // so a junk byte in 1..HPB_COUNT cannot trigger custom-order mode. - // (Array length = HPB_COUNT, declared as literal so the field offset is stable - // across builds that add new HomePageBit entries.) - uint8_t page_order[11]; + // Declared as a literal (not HPB_COUNT) so the field offset stays stable across + // builds that add HomePageBit entries. The first PAGE_ORDER_LEN_V1 bytes persist + // at this offset (backward-compatible); the remaining slots live at the file + // tail (see DataStore) so pre-0x0019 saves still load without shifting. + uint8_t page_order[13]; uint8_t joystick_rotation; // 0-3 steps CW; independent of display_rotation uint8_t eink_full_refresh_every; // index into {0,5,10,20,30}: full refresh every N partials (0=off) uint8_t page_order_set; // 0xA5 = page_order is user-configured; anything else = use default @@ -344,7 +346,7 @@ struct NodePrefs { // persisted to file // adding/removing/reordering fields in DataStore::savePrefs/loadPrefsInt so // older saves are detected on load and skipped (zero-init defaults kept). // High 24 bits identify the file format; low byte is the schema revision. - static const uint32_t SCHEMA_SENTINEL = 0xC0DE0018; + static const uint32_t SCHEMA_SENTINEL = 0xC0DE0019; // Bit-index for each home page. Used by page_order (entries store bit+1) and // by home_pages_mask. Single source of truth — both HomeScreen::pageBit/bitToPage @@ -366,10 +368,15 @@ struct NodePrefs { // persisted to file HPB_COUNT = 13, }; - // Length of the persisted page_order[] array. Stable across firmware versions - // (don't grow without a schema migration). When HPB_COUNT exceeds this, extra - // visible pages are appended at navigation time via buildVisibleOrder fallback. - static const uint8_t PAGE_ORDER_LEN = 11; + // Number of usable page_order[] slots — one per home page (== HPB_COUNT), so + // every page can be reordered. Any page still missing from a saved order is + // appended at navigation time via buildVisibleOrder's fallback. + static const uint8_t PAGE_ORDER_LEN = 13; + // Bytes of page_order persisted at the original file offset. Slots beyond this + // (PAGE_ORDER_LEN - PAGE_ORDER_LEN_V1) are stored at the file tail, so a + // pre-0x0019 save — whose order was exactly this long — loads without shifting + // every field written after page_order. + static const uint8_t PAGE_ORDER_LEN_V1 = 11; // Bitmasks for home_pages_mask (bit=1 → page visible; 0 field = all visible). // SETTINGS and QUICK_MSG have no mask bit — they're always visible. diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index 82a54986..9b22ba2d 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -309,9 +309,9 @@ class SettingsScreen : public UIScreen { memset(p->page_order, 0, sizeof(p->page_order)); } else { if (!has_fav) { - // Insert FAVOURITES right after CLOCK. If the array is full, the last - // entry (typically SHUTDOWN, which still appears via the missing-page - // fallback at end of nav) is overwritten to make room. + // Insert FAVOURITES right after CLOCK. Real orders are shorter than + // PAGE_ORDER_LEN so there's room; only a pathologically full order would + // drop its last entry, which buildVisibleOrder's fallback re-appends. int insert_at = clock_at + 1; int tail = (len < NodePrefs::PAGE_ORDER_LEN) ? len : NodePrefs::PAGE_ORDER_LEN - 1; for (int i = tail; i > insert_at; i--) p->page_order[i] = p->page_order[i - 1]; @@ -329,6 +329,9 @@ class SettingsScreen : public UIScreen { present |= (uint16_t)(1u << (v - 1)); cur_len++; } + // Every page has a slot now (PAGE_ORDER_LEN == HPB_COUNT), so all pages + // are required — any missing from a stale saved order (SHUTDOWN and MAP + // for pre-0x0019 upgraders) is appended into the free tail slots below. static const uint8_t REQUIRED[] = { NodePrefs::HPB_CLOCK, NodePrefs::HPB_FAVOURITES, NodePrefs::HPB_RECENT, NodePrefs::HPB_RADIO, @@ -339,26 +342,9 @@ class SettingsScreen : public UIScreen { #if UI_SENSORS_PAGE == 1 NodePrefs::HPB_SENSORS, #endif - NodePrefs::HPB_SETTINGS, NodePrefs::HPB_TOOLS, NodePrefs::HPB_QUICK_MSG, + NodePrefs::HPB_SETTINGS, NodePrefs::HPB_MAP, NodePrefs::HPB_TOOLS, + NodePrefs::HPB_QUICK_MSG, NodePrefs::HPB_SHUTDOWN, }; - // If any required page is missing, evict SHUTDOWN to make room — it is - // handled by buildVisibleOrder's fallback and need not be in the explicit - // list. This frees a slot regardless of whether the array is full or not, - // so multiple missing pages (e.g. TOOLS + QUICK_MSG) can all be appended. - bool any_missing = false; - for (int ri = 0; ri < (int)(sizeof(REQUIRED)/sizeof(REQUIRED[0])); ri++) { - if (!(present & (uint16_t)(1u << REQUIRED[ri]))) { any_missing = true; break; } - } - if (any_missing && (present & (uint16_t)(1u << NodePrefs::HPB_SHUTDOWN))) { - for (int i = 0; i < cur_len; i++) { - if (p->page_order[i] == NodePrefs::HPB_SHUTDOWN + 1) { - for (int j = i; j < cur_len - 1; j++) p->page_order[j] = p->page_order[j + 1]; - p->page_order[--cur_len] = 0; - present &= ~(uint16_t)(1u << NodePrefs::HPB_SHUTDOWN); - break; - } - } - } for (int ri = 0; ri < (int)(sizeof(REQUIRED)/sizeof(REQUIRED[0])); ri++) { uint8_t bit = REQUIRED[ri]; if (!(present & (uint16_t)(1u << bit)) && cur_len < NodePrefs::PAGE_ORDER_LEN) @@ -368,10 +354,9 @@ class SettingsScreen : public UIScreen { return; } } - // Default: CLOCK FAVOURITES RECENT RADIO BT ADVERT [GPS] [SENSORS] SETTINGS TOOLS MESSAGES - // SHUTDOWN is omitted from the explicit list (PAGE_ORDER_LEN = 11 leaves room for the - // common case GPS+SENSORS+all-others) and appended by buildVisibleOrder's missing-page - // fallback at the end of the navigation sequence. + // Default: CLOCK FAVOURITES RECENT RADIO BT ADVERT [GPS] [SENSORS] SETTINGS + // MAP TOOLS MESSAGES SHUTDOWN — mirrors the home-carousel enum order. Every + // page has an explicit slot now (PAGE_ORDER_LEN == HPB_COUNT). int j = 0; p->page_order[j++] = NodePrefs::HPB_CLOCK + 1; p->page_order[j++] = NodePrefs::HPB_FAVOURITES + 1; @@ -386,8 +371,10 @@ class SettingsScreen : public UIScreen { p->page_order[j++] = NodePrefs::HPB_SENSORS + 1; #endif p->page_order[j++] = NodePrefs::HPB_SETTINGS + 1; + p->page_order[j++] = NodePrefs::HPB_MAP + 1; p->page_order[j++] = NodePrefs::HPB_TOOLS + 1; p->page_order[j++] = NodePrefs::HPB_QUICK_MSG + 1; + p->page_order[j++] = NodePrefs::HPB_SHUTDOWN + 1; while (j < NodePrefs::PAGE_ORDER_LEN) p->page_order[j++] = 0; p->page_order_set = NodePrefs::PAGE_ORDER_MAGIC; } diff --git a/release-notes.md b/release-notes.md index 536697fe..de919341 100644 --- a/release-notes.md +++ b/release-notes.md @@ -1,3 +1,10 @@ +## MeshCore Solo Companion Firmware v1.22 + +### Fixes + +- **Home page visibility no longer resets on update** — the prefs schema-migration that turned the **Favourites** page on ran on *every* firmware update (any schema-version bump), so a page you'd deliberately hidden kept coming back after each release. It's now gated to the single upgrade that introduced Favourites, so your hidden pages stay hidden. +- **Map page in Home Pages settings** — the **Map** home page shipped with no entry under **Settings › Home Pages**, so it was always shown and couldn't be moved. It's now a first-class page there: **toggle its visibility** and **reorder it** like any other (visible by default; existing users keep it on). Reordering also now covers the **Shutdown** page, which was previously pinned to the end of the carousel. + ## MeshCore Solo Companion Firmware v1.21 ### What's new