Add SetCfg to pick a config bank independently of the slot

This commit is contained in:
Armel FAUVEAU committed 2026-08-21 02:56:54 +02:00
1 parent 5b6174085b
commit e4a645a5c7
12 files changed
+502 -243

No files matched your search

+67 -29
View File
@@ -43,6 +43,7 @@
#include "ui.h"
#include "welcome.h"
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
#include "driver/mb_flash.h"
#include "multiboot.h"
#endif
@@ -177,6 +178,9 @@ const t_menu_item MenuList[] =
#ifdef ENABLE_FEAT_F4HWN_LOGO_SAV
{"SetSav", MENU_SET_SAV },
#endif
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
{"SetCfg", MENU_SET_CFG }, // load another settings bank (reboots)
#endif
#endif
// hidden menu items from here on
// enabled if pressing both the PTT and upper side button at power-on
@@ -601,6 +605,9 @@ static const uint8_t CatChannels[] = {
#endif
MENU_BCL, MENU_COMPAND, MENU_AM, MENU_TX_LOCK, MENU_PTT_ID, MENU_LIST_CH,
MENU_MEM_CH, MENU_DEL_CH, MENU_MEM_NAME,
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
MENU_SET_CFG,
#endif
};
static const uint8_t CatScan[] = {
MENU_S_LIST, MENU_S_PRI, MENU_S_PRI_CH_1, MENU_S_PRI_CH_2, MENU_SC_REV,
@@ -839,34 +846,27 @@ static void UI_MENU_DrawTopRightRoundedBadge(const char *text, const uint8_t lin
UI_PrintStringSmallNormalInverse(text, text_x, 0, line);
}
/* Single-line variant for tight gaps: unlike UI_PrintStringSmallNormalInverse,
* the rounded edge stays entirely inside `line` and never touches line - 1. */
static void UI_MENU_DrawInlineRoundedBadge(const char *text, const uint8_t line,
const uint8_t area_x1, const uint8_t area_x2)
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
/* Draw `text` (3x5 font) centred inside a fixed-width rounded inverse capsule:
* left edge `cap_left`, inclusive width `cap_w`, on framebuffer page `line`. Same
* capsule pattern as GUI_DisplaySmallestInverse (0x3E rounded ends, 0x7F body) but
* with the width decoupled from the text length, so two labels of different
* lengths (e.g. "SLOT 2" / "CFG 4") share one width and each stays centred. */
static void UI_MENU_DrawFixedCapsule(const char *text, uint8_t cap_left,
uint8_t cap_w, uint8_t line)
{
const size_t length = strlen(text);
const size_t char_pitch = ARRAY_SIZE(gFontSmall[0]) + 1u;
const size_t text_width = length * char_pitch;
const size_t capsule_width = text_width + 3u;
const uint8_t cap_right = (uint8_t)(cap_left + cap_w - 1u);
const uint8_t text_w = (uint8_t)(strlen(text) * 4u - 1u); /* 3x5 glyphs: 4 px/char, last one 3 px wide */
const uint8_t tx = (uint8_t)(cap_left + (cap_w - text_w) / 2u);
if (length == 0 || line >= FRAME_LINES || area_x2 <= area_x1) {
return;
}
GUI_DisplaySmallest(text, tx, (uint8_t)(line * 8u + 1u), false, true);
const size_t area_width = area_x2 - area_x1 + 1u;
if (capsule_width >= area_width)
return;
const uint8_t capsule_left = (uint8_t)(area_x1 + ((area_width - capsule_width) / 2u));
const uint8_t text_x = (uint8_t)(capsule_left + 1u);
const uint8_t x_end = (uint8_t)(text_x + text_width + 1u);
UI_PrintStringSmallNormal(text, text_x, 0, line);
gFrameBuffer[line][text_x - 1u] ^= 0x7Eu;
for (uint8_t x = text_x; x < x_end; x++)
gFrameBuffer[line][x] ^= 0xFFu;
gFrameBuffer[line][x_end] ^= 0x7Eu;
gFrameBuffer[line][cap_left] ^= 0x3Eu;
for (uint8_t x = (uint8_t)(cap_left + 1u); x < cap_right; x++)
gFrameBuffer[line][x] ^= 0x7Fu;
gFrameBuffer[line][cap_right] ^= 0x3Eu;
}
#endif
void UI_DisplayMenu(void)
{
@@ -1454,12 +1454,39 @@ void UI_DisplayMenu(void)
sprintf(String, "%s\n%s", AUTHOR_STRING_2, DISPLAY_VERSION_STRING_2);
UI_PrintStringSmallNormal(Edition, menu_item_x1 - 1, menu_item_x2, 6);
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
const uint8_t running_slot = MB_GetRunningSlot();
char slot_badge[2];
/* Two 3x5 inverse-capsule labels on one line (scan-list "label"
* style): the running firmware slot (M = Main) and the active
* config bank. They match unless SetCfg has pointed the bank at a
* different bank (e.g. SLOT 2 / CFG 4). */
const uint8_t fw_slot = MB_GetRunningSlot();
const uint8_t bank = MB_GetActiveBank();
char slot_lbl[8];
char cfg_lbl[8];
slot_badge[0] = (running_slot == 0u) ? 'M' : (char)('0' + running_slot);
slot_badge[1] = '\0';
UI_MENU_DrawInlineRoundedBadge(slot_badge, 5, menu_item_x1, menu_item_x2);
/* Only the last glyph varies (M / digit / ?), so poke it in place
* instead of pulling sprintf for a single character. */
strcpy(slot_lbl, "SLOT ?");
if (fw_slot == 0u)
slot_lbl[5] = 'M';
else if (fw_slot < MB_SLOT_COUNT)
slot_lbl[5] = (char)('0' + fw_slot);
strcpy(cfg_lbl, "CFG M"); /* bank 0 = base config, like SLOT M */
if (bank != 0u)
cfg_lbl[4] = (char)('0' + bank);
/* Both capsules share the wider label's width (6-char "SLOT x" ->
* 4*6+3 = 27 px); the shorter CFG text is centred inside its own.
* The two are drawn as one centred pair with a small gap, centred in
* the space between the separator bar (x=48) and the right screen
* edge, so they line up with the centred identity lines above. */
const uint8_t cap_w = (uint8_t)(4u * 6u + 3u); /* 27 */
const uint8_t cap_gap = 4u;
const uint8_t pair_w = (uint8_t)(2u * cap_w + cap_gap); /* 58 */
const uint8_t slot_left = (uint8_t)((48u + LCD_WIDTH - pair_w) / 2u); /* 59 */
const uint8_t cfg_left = (uint8_t)(slot_left + cap_w + cap_gap); /* 90 */
UI_MENU_DrawFixedCapsule(slot_lbl, slot_left, cap_w, 5);
UI_MENU_DrawFixedCapsule(cfg_lbl, cfg_left, cap_w, 5);
#endif
#else
sprintf(String, "%u.%02uV\n%u%%",
@@ -1582,6 +1609,14 @@ void UI_DisplayMenu(void)
strcpy(String, gSubMenu_SET_NAV[gSubMenuSelection]);
break;
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
case MENU_SET_CFG:
strcpy(String, "CFG M"); /* bank 0 = base config, like SysInfo */
if (gSubMenuSelection != 0)
String[4] = (char)('0' + gSubMenuSelection);
break;
#endif
case MENU_F1SHRT:
case MENU_F1LONG:
case MENU_F2SHRT:
@@ -1837,6 +1872,9 @@ void UI_DisplayMenu(void)
if ((m == MENU_RESET ||
m == MENU_MEM_CH ||
m == MENU_MEM_NAME ||
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
m == MENU_SET_CFG ||
#endif
m == MENU_DEL_CH) && gAskForConfirmation)
{ // display confirmation
char *pPrintStr = (gAskForConfirmation == 1) ? "SURE?" : "WAIT!";
+3
View File
@@ -153,6 +153,9 @@ enum
MENU_NOAA_S,
#endif
MENU_SET_NAV,
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
MENU_SET_CFG,
#endif
#ifdef ENABLE_FEAT_F4HWN_AUDIO
MENU_SET_AUD,
#endif
+82 -29
View File
@@ -1,5 +1,17 @@
/* Copyright 2026 F4HWN
* SPDX-License-Identifier: Apache-2.0
/* Copyright 2026 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <string.h>
@@ -13,12 +25,14 @@
#include "ui/helper.h"
#include "ui/multiboot.h"
static uint8_t gRunningSlot = MB_SLOT_BACKUP;
static uint8_t gRunningSlot = 0xFFu;
static uint8_t gActiveBank = 0u;
static uint8_t mb_remember_running_slot(uint8_t slot)
static uint8_t mb_remember_boot_state(uint8_t slot, uint8_t bank)
{
gRunningSlot = slot;
return slot;
gActiveBank = bank;
return bank;
}
uint8_t MB_GetRunningSlot(void)
@@ -26,6 +40,11 @@ uint8_t MB_GetRunningSlot(void)
return gRunningSlot;
}
uint8_t MB_GetActiveBank(void)
{
return gActiveBank;
}
static const char *mb_error_text(uint8_t err)
{
switch (err)
@@ -37,7 +56,7 @@ static const char *mb_error_text(uint8_t err)
case MB_ERR_SIZE: return "bad size";
case MB_ERR_CRC: return "CRC ERROR";
case MB_ERR_SPI: return "SPI ERROR";
case MB_ERR_SLOT: return "bad slot";
case MB_ERR_SLOT: return "bad index";
case MB_ERR_AUTH: return "auth";
case MB_ERR_RAM_LOAD: return "RAM LOAD ERROR";
default: return "error";
@@ -171,6 +190,23 @@ static KEY_Code_t mb_get_key(void)
}
}
/* Shown from the normal settings menu (SetCfg), not the boot selector, so it does
* NOT paint the "F4HWN MULTIBOOT" status banner - just a plain acknowledged message. */
void UI_MultibootShowConfigError(uint8_t err)
{
UI_DisplayClear();
UI_StatusClear();
UI_PrintStringSmallNormal("CFG ERROR", 2, 126, 2);
UI_PrintStringSmallNormal(mb_error_text(err), 2, 126, 4);
UI_PrintStringSmallNormal("Press any key", 2, 126, 6);
ST7565_BlitStatusLine();
ST7565_BlitFullScreen();
/* The MENU press that confirmed SetCfg may still be down; wait for a clean
* release first so it isn't consumed as the acknowledgement immediately. */
mb_wait_release();
(void)mb_get_key();
}
static void mb_scan_slots(mb_slot_header_t headers[MB_SLOT_COUNT], uint8_t status[MB_SLOT_COUNT])
{
mb_show_message("Scanning slots...", NULL, "Please wait");
@@ -301,13 +337,13 @@ static void mb_backup_progress(uint32_t done, uint32_t total)
ST7565_BlitFullScreen();
}
/* With no trustworthy profile, continuing would let normal boot-time settings
/* With no trustworthy bank, continuing would let normal boot-time settings
* writes modify an arbitrary bank. Keep the radio in a read-only error state;
* a power cycle can recover from a transient SPI fault. */
__attribute__((noreturn)) static void mb_profile_error_halt(void)
__attribute__((noreturn)) static void mb_state_error_halt(void)
{
BACKLIGHT_TurnOn();
mb_show_message("PROFILE ERROR", "Flash state unknown", "Restart radio");
mb_show_message("STATE ERROR", "Flash state unknown", "Restart radio");
for (;;)
SYSTEM_DelayMs(100);
}
@@ -339,10 +375,11 @@ void UI_MultibootSelector(void)
mb_wait_release();
mb_scan_slots(headers, status);
/* Pre-select the firmware currently running (its slot, from the marker), so
* the cursor lands on "where you are". If that slot isn't restorable (erased,
* bad CRC...), fall back to the first valid slot. */
selected = MB_GetActiveProfile();
/* Pre-select the exact firmware slot resolved at boot, independently of the
* active config bank (SetCfg can point the bank elsewhere), so the cursor
* lands on "where you are". An unknown or now-invalid slot falls back to the
* first valid slot below. */
selected = MB_GetRunningSlot();
if (selected >= MB_SLOT_COUNT || status[selected] != MB_OK)
{
for (uint8_t slot = 0; slot < MB_SLOT_COUNT; slot++)
@@ -394,13 +431,13 @@ void UI_MultibootSelector(void)
if (key != KEY_MENU)
continue;
/* Bind this slot to its own settings profile BEFORE reflashing. The
/* Bind this slot to its own settings bank BEFORE reflashing. The
* write is verified (read-back); if it can't be confirmed we must NOT
* reflash - otherwise the next boot could resolve to the wrong profile
* reflash - otherwise the next boot could resolve to the wrong bank
* (e.g. when two slots hold the same firmware image). */
if (MB_SetActiveProfile(selected) != MB_OK)
if (MB_SetActiveSlot(selected) != MB_OK)
{
mb_show_message("PROFILE ERROR", "Marker not saved", "Press any key");
mb_show_message("STATE ERROR", "Marker not saved", "Press any key");
(void)mb_get_key();
continue;
}
@@ -417,25 +454,25 @@ void UI_MultibootSelector(void)
}
/* Adopt the running internal firmware as Main: back it up into slot 0 and point
* the marker at profile 0. Reached when the firmware was installed outside
* the marker at bank 0. Reached when the firmware was installed outside
* multiboot (fresh radio, or a plain Flash-Firmware). */
static uint8_t mb_adopt_internal_as_main(void)
{
mb_backup_prepare();
BACKLIGHT_TurnOn();
if (MB_BackupInternalToSlot0(mb_backup_progress) == MB_OK)
(void)MB_SetActiveProfile(MB_SLOT_BACKUP);
(void)MB_SetActiveSlot(MB_SLOT_BACKUP);
return MB_SLOT_BACKUP;
}
uint8_t MB_BootResolveProfile(void)
uint8_t MB_BootResolveState(void)
{
mb_profile_state_t mark;
mb_state_t mark;
mb_mark_status_t ms = MB_MARK_IO;
for (uint8_t retry = 0; retry < 3u && ms == MB_MARK_IO; retry++)
{
ms = MB_ReadActiveProfile(&mark);
ms = MB_ReadActiveState(&mark);
if (ms == MB_MARK_IO)
SYSTEM_DelayMs(10);
}
@@ -444,21 +481,37 @@ uint8_t MB_BootResolveProfile(void)
* identity, so we don't even need the slot header. */
if (ms == MB_MARK_VALID)
{
if (MB_InternalMatchesProfile(&mark))
return mb_remember_running_slot(mark.index); /* slot named by the marker */
if (MB_InternalMatchesState(&mark))
return mb_remember_boot_state(mark.firmware_slot, mark.config_bank);
/* Marker read fine but internal no longer carries its identity -> the
* firmware was replaced outside multiboot (a plain Flash-Firmware). Adopt
* it as Main. Deliberately NOT a content scan here: a build that merely
* duplicates a user slot (or a marker that already points at such a slot)
* must still refresh Main. */
return mb_remember_running_slot(mb_adopt_internal_as_main());
return mb_remember_boot_state(mb_adopt_internal_as_main(), MB_SLOT_BACKUP);
}
/* Marker unreliable (MISSING / LEGACY / CORRUPT / IO): identify the running
* firmware by content, and never destroy Main on uncertainty - internal is
* adopted only when it matches no slot AND every read was clean, so a
* transient SPI error or a half-written marker can never destroy Main. */
/* An FMP1 record still names a coupled slot/bank; honour it before the
* content scan so a duplicate image in a lower slot cannot hijack the
* migration. Falls through to the scan below on mismatch or IO. */
if (ms == MB_MARK_LEGACY && mark.firmware_slot < MB_SLOT_COUNT)
{
mb_fw_match_t m = MB_FW_IO;
for (uint8_t retry = 0; retry < 3u && m == MB_FW_IO; retry++)
m = MB_InternalMatchesSlot(mark.firmware_slot);
if (m == MB_FW_MATCH)
{
(void)MB_SetActiveSlot(mark.firmware_slot);
return mb_remember_boot_state(mark.firmware_slot, mark.config_bank);
}
}
bool had_io = false;
for (uint8_t slot = 0; slot < MB_SLOT_COUNT; slot++)
{
@@ -467,8 +520,8 @@ uint8_t MB_BootResolveProfile(void)
m = MB_InternalMatchesSlot(slot);
if (m == MB_FW_MATCH)
{
(void)MB_SetActiveProfile(slot); /* record/repair the marker */
return mb_remember_running_slot(slot);
(void)MB_SetActiveSlot(slot); /* record/repair the marker */
return mb_remember_boot_state(slot, slot);
}
if (m == MB_FW_IO)
had_io = true;
@@ -483,8 +536,8 @@ uint8_t MB_BootResolveProfile(void)
bool main_exists = (main_status != MB_ERR_MAGIC &&
main_status != MB_ERR_NOT_COMMITTED);
if (main_exists)
mb_profile_error_halt();
mb_state_error_halt();
}
return mb_remember_running_slot(mb_adopt_internal_as_main());
return mb_remember_boot_state(mb_adopt_internal_as_main(), MB_SLOT_BACKUP);
}
+27 -9
View File
@@ -1,5 +1,17 @@
/* Copyright 2026 F4HWN
* SPDX-License-Identifier: Apache-2.0
/* Copyright 2026 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef UI_MULTIBOOT_H
@@ -11,14 +23,20 @@
* a successful restore resets the radio from the RAM-resident copier. */
void UI_MultibootSelector(void);
/* Resolve the active settings profile at boot, BEFORE any settings read.
* Detects a firmware installed outside multiboot (internal != slot[marker]) and,
* if so, discreetly self-backs it up into slot 0 and adopts profile 0. Returns
* the profile index (0..MB_SLOT_COUNT-1) to feed PY25Q16_SetProfileBase(). */
uint8_t MB_BootResolveProfile(void);
/* Show a blocking, acknowledged error screen for a failed SetCfg operation. */
void UI_MultibootShowConfigError(uint8_t err);
/* Slot selected by MB_BootResolveProfile for the current session. This is kept
* in RAM so UI callers do not have to reread the external-flash marker. */
/* Resolve the active config bank at boot, BEFORE any settings read. Detects a
* firmware installed outside multiboot (internal identity != marker) and, if so,
* discreetly self-backs it up into slot 0 and adopts slot 0 / bank 0. Returns the
* config bank (0..MB_BANK_COUNT-1) to feed PY25Q16_SetBankBase(); the exact
* firmware slot and bank are cached too (see MB_GetRunningSlot/MB_GetActiveBank). */
uint8_t MB_BootResolveState(void);
/* Exact firmware slot and active config bank resolved for this session. Both
* are cached in RAM so UI callers never have to reread the marker or scan slot
* headers merely to render their state. */
uint8_t MB_GetRunningSlot(void);
uint8_t MB_GetActiveBank(void);
#endif