Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
6e8796f
refactor(MockUI): introduce TitledScreen base class for all screens
maggo83 Feb 26, 2026
75eadf0
feat(MockUI): Phase 1 – scale UI constants for 800×480 touch display
maggo83 Feb 26, 2026
8a04e0b
fix(MockUI): disable scroll/drag on all container levels
maggo83 Feb 26, 2026
641f20c
UI: scale text fields, help modal, and tour buttons to larger font/size
maggo83 Feb 26, 2026
8ddb0ec
Increase Helper Button Size
maggo83 Mar 2, 2026
e6b34b1
symbol_lib: upgrade icons to 42×42px rendered from SVG via Inkscape
maggo83 Mar 2, 2026
59acbd5
lock_screen: resize PIN keypad for 800×480
maggo83 Mar 3, 2026
f22fb01
feat: add Restart Tour to DeviceMenu, i18n tour/modal strings, fix st…
maggo83 Mar 3, 2026
7413f23
refactor: move tour step definitions to NavigationController, decoupl…
maggo83 Mar 4, 2026
6d282d2
Tests: add device tests for guided tour (9/9 passing)
maggo83 Mar 4, 2026
f75e0f6
Tests: add device test for help icon popup (1/1 passing)
maggo83 Mar 4, 2026
4e039c7
refactor: remove dead menu_id parameter from GenericMenu\n\nThe menu_…
maggo83 Mar 4, 2026
52a58de
refactor: remove dead *args/**kwargs from GenericMenu hierarchy
maggo83 Mar 4, 2026
bbeec1b
refactor: overhaul GenericMenu hierarchy with template method pattern
maggo83 Mar 4, 2026
8c7d0bc
refactor: overhaul GenericMenu hierarchy with template method pattern
maggo83 Mar 4, 2026
8912669
fix: restore simulate target for MockUI on unix port
maggo83 Mar 5, 2026
3c961ca
chore: ignore simulator runtime state files
maggo83 Mar 5, 2026
26a9099
fix: resolve i18n and sim_control in unix simulator
maggo83 Mar 5, 2026
a5ffc77
main_menu: remove Settings section from main menu
maggo83 Mar 5, 2026
2886bba
add_wallet_menu: move keyboard entry before SD/flash options
maggo83 Mar 5, 2026
392bb49
locked_menu: fw version as subtitle, larger PIN fonts; i18n cleanup
maggo83 Mar 5, 2026
ac547c9
wallet_menu: inline name editing with full-screen custom keyboard
maggo83 Mar 5, 2026
cd0ad38
seedphrase_menu: split store/clear into dedicated sub-menus
maggo83 Mar 5, 2026
81baf67
refactor: restructure settings submenus for new display/menu size
maggo83 Mar 5, 2026
6a10f3c
Merge branch 'Rework_menus_and_titles_after_resize' of https://github…
maggo83 Mar 5, 2026
ff09050
fix: update device tests broken by settings menu restructure
maggo83 Mar 5, 2026
9432eee
Merge pull request #3 from maggo83/Rework_menus_and_titles_after_resize
maggo83 Mar 6, 2026
5f83692
Merge pull request #1 from maggo83/Tests-Add-device-tests-for-tour-an…
maggo83 Mar 6, 2026
a07358b
Merge pull request #2 from maggo83/fix/simulate-mockui
maggo83 Mar 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ release
.DS_Store
.direnv

# Bitcoin Icons source assets — regenerate via generate_btc_icons.py
data/

# RAG code scanner
.rag/.venv/
.rag/chroma_db/
Expand All @@ -20,3 +23,6 @@ build
scenarios/MockUI/src/MockUI/i18n/translation_keys.py
scenarios/MockUI/src/MockUI/i18n/language_config.json

# Simulator runtime state files (generated at runtime, not for version control)
ui_state_config.json

4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -162,15 +162,15 @@ mockui: $(TARGET_DIR) mpy-cross build-i18n build-flash-image $(MPY_DIR)/ports/st
@ls -lh $(TARGET_DIR)/mockui.bin

# unixport (simulator)
unix: $(TARGET_DIR) mpy-cross $(MPY_DIR)/ports/unix
unix: $(TARGET_DIR) mpy-cross build-i18n $(MPY_DIR)/ports/unix
@echo Building binary with frozen files
make -C $(MPY_DIR)/ports/unix \
USER_C_MODULES=$(USER_C_MODULES) \
FROZEN_MANIFEST=$(FROZEN_MANIFEST_UNIX) \
CFLAGS_EXTRA='-DMP_CONFIGFILE="<mpconfigport_specter.h>"' && \
cp $(MPY_DIR)/ports/unix/build-standard/micropython $(TARGET_DIR)/micropython_unix

SCRIPT ?= mock_ui.py
SCRIPT ?= mockui_fw/main.py

simulate: unix
$(TARGET_DIR)/micropython_unix scenarios/$(SCRIPT)
Expand Down
4 changes: 4 additions & 0 deletions manifests/mockui-shared.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# MockUI package — platform-independent Python code.
# Included by both the hardware (mockui.py) and unix simulator (unix.py) manifests.
# Do NOT add hardware-specific or simulator-specific freezes here.
freeze('../scenarios/MockUI/src')
11 changes: 3 additions & 8 deletions manifests/mockui.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
# MockUI firmware manifest
# Include display wrapper and common libs
# MockUI firmware manifest (hardware — STM32F469 Discovery)
include('../f469-disco/manifests/disco.py')
include('mockui-shared.py')
# platform.py and config_default.py needed for SDRAM init
freeze('../src', ('platform.py', 'config_default.py'))
# MockUI package — only src/ is frozen; tests/ stay out of firmware.
freeze('../scenarios/MockUI/src')
# Other scenario modules
freeze('../scenarios', ('address_navigator.py', 'udisplay_demo.py'))
freeze('../scenarios/sim_control')
# boot.py and main.py entry points (frozen at root level)
# boot.py and main.py entry points
freeze('../scenarios/mockui_fw')
7 changes: 3 additions & 4 deletions manifests/playground.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Playground firmware manifest (hardware — includes demo scenarios)
include('../f469-disco/manifests/disco.py')
# MockUI package — only src/ is frozen; tests/ stay out of firmware.
freeze('../scenarios/MockUI/src')
# Other scenario modules (mockui_fw excluded: its boot.py would conflict with boot/main)
include('mockui-shared.py')
# Demo/scenario modules (mockui_fw excluded: its boot.py would conflict with boot/main)
freeze('../scenarios', ('address_navigator.py', 'udisplay_demo.py'))
freeze('../scenarios/sim_control')
freeze('../boot/main')
5 changes: 5 additions & 0 deletions manifests/unix.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
# Unix simulator manifest
include('../f469-disco/manifests/unix.py')
include('mockui-shared.py')
freeze('../src')
# sim_control: TCP control server for sim_cli.py / MCP — simulator only.
# Frozen from parent dir so the package name 'sim_control' is preserved.
freeze('../scenarios', ('sim_control/__init__.py', 'sim_control/control_server.py', 'sim_control/widget_tree.py'))
2 changes: 1 addition & 1 deletion mcp-servers/lvgl-sim/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
# Project root
PROJECT_ROOT = Path(__file__).parent.parent.parent
SIMULATOR_BIN = PROJECT_ROOT / "bin" / "micropython_unix"
SIMULATOR_SCRIPT = PROJECT_ROOT / "scenarios" / "mock_ui.py"
SIMULATOR_SCRIPT = PROJECT_ROOT / "scenarios" / "mockui_fw" / "main.py"
CONTROL_PORT = 9876


Expand Down
4 changes: 2 additions & 2 deletions mcp-servers/lvgl-sim/sim_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,13 +218,13 @@ def restart():
import time

# Kill existing simulator
subprocess.run(['pkill', '-f', 'micropython_unix.*mock_ui'], capture_output=True)
subprocess.run(['pkill', '-f', 'micropython_unix.*mockui_fw'], capture_output=True)
time.sleep(1)

# Start new one
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
subprocess.Popen(
[f'{project_root}/bin/micropython_unix', f'{project_root}/scenarios/mock_ui.py', '--control'],
[f'{project_root}/bin/micropython_unix', f'{project_root}/scenarios/mockui_fw/main.py', '--control'],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
)
time.sleep(2)
Expand Down
9 changes: 5 additions & 4 deletions scenarios/MockUI/src/MockUI/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@
)

from .device import (
DeviceMenu,
SecuritySettingsMenu,
FirmwareMenu,
InterfacesMenu,
BackupsMenu,
SecurityMenu,
SecurityFeaturesMenu,
StorageMenu,
SettingsMenu,
PreferencesMenu,
)

from .tour import GuidedTour
Expand All @@ -38,15 +39,15 @@
"GREEN", "ORANGE", "RED",
"MainMenu",
"WalletMenu",
"DeviceMenu",
"SecuritySettingsMenu",
"SpecterState",
"Wallet",
"ActionScreen",
"UIState",
"DeviceBar",
"WalletBar",
"SeedPhraseMenu",
"SecurityMenu",
"SecurityFeaturesMenu",
"InterfacesMenu",
"BackupsMenu",
"FirmwareMenu",
Expand Down
14 changes: 9 additions & 5 deletions scenarios/MockUI/src/MockUI/basic/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .ui_consts import BTN_HEIGHT, BTN_WIDTH, BACK_BTN_HEIGHT, BACK_BTN_WIDTH, MENU_PCT, PAD_SIZE, SWITCH_HEIGHT, SWITCH_WIDTH, STATUS_BTN_HEIGHT, STATUS_BTN_WIDTH, BTC_ICON_WIDTH, ONE_LETTER_SYMBOL_WIDTH, TWO_LETTER_SYMBOL_WIDTH, THREE_LETTER_SYMBOL_WIDTH, GREEN, ORANGE, RED, GREEN_HEX, ORANGE_HEX, RED_HEX, WHITE_HEX, BLACK_HEX, TITLE_PADDING, MODAL_WIDTH_PCT, MODAL_HEIGHT_PCT, EXPLAINER_WIDTH_PCT, EXPLAINER_HEIGHT_PCT, EXPLAINER_OVERLAY_OPA
from .ui_consts import BTN_HEIGHT, BTN_WIDTH, BACK_BTN_HEIGHT, BACK_BTN_WIDTH, MENU_PCT, PAD_SIZE, SWITCH_HEIGHT, SWITCH_WIDTH, STATUS_BTN_HEIGHT, STATUS_BTN_WIDTH, STATUS_BAR_PCT, CONTENT_PCT, BTC_ICON_WIDTH, BTC_ICON_ZOOM, ONE_LETTER_SYMBOL_WIDTH, TWO_LETTER_SYMBOL_WIDTH, THREE_LETTER_SYMBOL_WIDTH, MENU_TITLE_FONT_SIZE, MENU_ITEM_FONT_SIZE, GREEN, ORANGE, RED, GREEN_HEX, ORANGE_HEX, RED_HEX, WHITE_HEX, BLACK_HEX, TITLE_ROW_HEIGHT, TITLE_PADDING, MODAL_WIDTH_PCT, MODAL_HEIGHT_PCT, EXPLAINER_WIDTH_PCT, EXPLAINER_HEIGHT_PCT, EXPLAINER_OVERLAY_OPA, PIN_BTN_WIDTH, PIN_BTN_HEIGHT
from .titled_screen import TitledScreen
from .main_menu import MainMenu
from .locked_menu import LockedMenu
from .device_bar import DeviceBar
Expand All @@ -12,15 +13,18 @@
__all__ = ["BTN_HEIGHT", "BTN_WIDTH", "BACK_BTN_HEIGHT", "BACK_BTN_WIDTH",
"MENU_PCT",
"PAD_SIZE",
"TITLE_PADDING",
"TITLE_ROW_HEIGHT", "TITLE_PADDING",
"MODAL_WIDTH_PCT", "MODAL_HEIGHT_PCT",
"EXPLAINER_WIDTH_PCT", "EXPLAINER_HEIGHT_PCT", "EXPLAINER_OVERLAY_OPA",
"SWITCH_HEIGHT", "SWITCH_WIDTH",
"STATUS_BTN_HEIGHT", "STATUS_BTN_WIDTH",
"BTC_ICON_WIDTH",
"STATUS_BTN_HEIGHT", "STATUS_BTN_WIDTH",
"PIN_BTN_WIDTH", "PIN_BTN_HEIGHT",
"STATUS_BAR_PCT", "CONTENT_PCT",
"BTC_ICON_WIDTH", "BTC_ICON_ZOOM",
"MENU_TITLE_FONT_SIZE", "MENU_ITEM_FONT_SIZE",
"ONE_LETTER_SYMBOL_WIDTH", "TWO_LETTER_SYMBOL_WIDTH", "THREE_LETTER_SYMBOL_WIDTH",
"GREEN", "ORANGE", "RED",
"GREEN_HEX", "ORANGE_HEX", "RED_HEX", "WHITE_HEX", "BLACK_HEX",
"MainMenu", "LockedMenu", "DeviceBar", "WalletBar", "ActionScreen", "GenericMenu", "ModalOverlay", "NavigationController",
"MainMenu", "LockedMenu", "DeviceBar", "WalletBar", "ActionScreen", "GenericMenu", "TitledScreen", "ModalOverlay", "NavigationController",
"BTC_ICONS"
]
75 changes: 19 additions & 56 deletions scenarios/MockUI/src/MockUI/basic/action_screen.py
Original file line number Diff line number Diff line change
@@ -1,66 +1,29 @@
import lvgl as lv
from .ui_consts import BTN_HEIGHT, BTN_WIDTH, BACK_BTN_WIDTH, BACK_BTN_HEIGHT
from .symbol_lib import BTC_ICONS
from .ui_consts import BTN_HEIGHT, BTN_WIDTH
from .titled_screen import TitledScreen

class ActionScreen(TitledScreen):
"""Generic action screen for menu items."""
def __init__(self, title, parent):
# TitledScreen creates title_bar (with optional back_btn + title_lbl) and body
super().__init__(title, parent)

class ActionScreen(lv.obj):
"""Generic action screen for menu items"""
def __init__(self, title, parent, *args, **kwargs):
# parent is the NavigationController (not necessarily the LVGL parent)
# attach to parent's `content` container when available so the status bar stays visible
lv_parent = getattr(parent, "content", parent)
super().__init__(lv_parent, *args, **kwargs)

# Get i18n manager from parent (always available via NavigationController)
self.t = parent.i18n.t

# discover navigation callback and shared state from parent
self.on_navigate = getattr(parent, "on_navigate", None)

# Fill parent
self.set_width(lv.pct(100))
self.set_height(lv.pct(100))
# Remove padding from base object to allow full-width content
self.set_style_pad_all(0, 0)
# Remove border
self.set_style_border_width(0, 0)

# If ui_state has history, show back button to the left of the title
if parent.ui_state and parent.ui_state.history and len(parent.ui_state.history) > 0:
self.back_btn = lv.button(self)
self.back_btn.set_size(BACK_BTN_HEIGHT, BACK_BTN_WIDTH)
self.back_ico = lv.image(self.back_btn)
BTC_ICONS.CARET_LEFT.add_to_parent(self.back_ico)
self.back_ico.center()
# wire back to navigation callback: wrap handler in a lambda so the
# LVGL binding's argument passing doesn't mismatch the method signature.
self.back_btn.add_event_cb(lambda e: self.on_back(e), lv.EVENT.CLICKED, None)


# Title
self.title = lv.label(self)
self.title.set_text(title)
self.title.set_style_text_align(lv.TEXT_ALIGN.CENTER, 0)
# smaller title offset for a tighter layout
self.title.align(lv.ALIGN.TOP_MID, 0, 18)

# Message
self.msg = lv.label(self)
# Message – placed inside body
self.msg = lv.label(self.body)
self.msg.set_text(self.t("ACTION_SCREEN_PREFIX") + title)
self.msg.set_style_text_align(lv.TEXT_ALIGN.CENTER, 0)
# smaller gap between title and message
self.msg.align_to(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 12)
self.msg.align(lv.ALIGN.TOP_MID, 0, 20)

# Back button
self.back_btn = lv.button(self)
self.back_btn.set_width(lv.pct(BTN_WIDTH))
self.back_btn.set_height(BTN_HEIGHT)
back_lbl = lv.label(self.back_btn)
# Back button – placed inside body below the message
self.action_back_btn = lv.button(self.body)
self.action_back_btn.set_width(lv.pct(BTN_WIDTH))
self.action_back_btn.set_height(BTN_HEIGHT)
back_lbl = lv.label(self.action_back_btn)
back_lbl.set_text(self.t("ACTION_SCREEN_BACK"))
back_lbl.set_style_text_font(lv.font_montserrat_22, 0)
back_lbl.center()
self.back_btn.align_to(self.msg, lv.ALIGN.OUT_BOTTOM_MID, 0, 40)
self.back_btn.add_event_cb(self.on_back, lv.EVENT.CLICKED, None)

def on_back(self, e):
if e.get_code() == lv.EVENT.CLICKED:
# navigate back to provided origin menu
self.on_navigate(None)
self.action_back_btn.align_to(self.msg, lv.ALIGN.OUT_BOTTOM_MID, 0, 40)
self.action_back_btn.add_event_cb(self.on_back, lv.EVENT.CLICKED, None)
25 changes: 7 additions & 18 deletions scenarios/MockUI/src/MockUI/basic/device_bar.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import lvgl as lv
from ..helpers import Battery
from .ui_consts import BTC_ICON_WIDTH, GREEN_HEX, ORANGE_HEX, RED_HEX, STATUS_BTN_HEIGHT, STATUS_BTN_WIDTH, THREE_LETTER_SYMBOL_WIDTH
from .ui_consts import BTC_ICON_WIDTH, GREEN_HEX, ORANGE_HEX, RED_HEX, STATUS_BTN_HEIGHT, STATUS_BTN_WIDTH
from .symbol_lib import BTC_ICONS


Expand All @@ -26,7 +26,7 @@ def __init__(self, parent, height_pct=5, *args, **kwargs):

# LEFT SECTION: Lock button
self.left_container = lv.obj(self)
self.left_container.set_width(STATUS_BTN_WIDTH + 10)
self.left_container.set_width(STATUS_BTN_WIDTH)
self.left_container.set_height(lv.pct(100))
self.left_container.set_layout(lv.LAYOUT.FLEX)
self.left_container.set_flex_flow(lv.FLEX_FLOW.ROW)
Expand All @@ -43,7 +43,7 @@ def __init__(self, parent, height_pct=5, *args, **kwargs):

# CENTER SECTION: Peripheral indicators
self.center_container = lv.obj(self)
self.center_container.set_width(BTC_ICON_WIDTH * 4 + 40)
self.center_container.set_width(BTC_ICON_WIDTH * 4 + 30)
self.center_container.set_height(lv.pct(100))
self.center_container.set_layout(lv.LAYOUT.FLEX)
self.center_container.set_flex_flow(lv.FLEX_FLOW.ROW)
Expand Down Expand Up @@ -75,9 +75,9 @@ def __init__(self, parent, height_pct=5, *args, **kwargs):
ico.add_flag(lv.obj.FLAG.CLICKABLE)
ico.add_event_cb(self.peripheral_ico_clicked, lv.EVENT.CLICKED, None)

# RIGHT SECTION: Battery, Language, Settings, Power (in that order)
# RIGHT SECTION: Battery, Settings, Power (in that order)
self.right_container = lv.obj(self)
self.right_container.set_width(STATUS_BTN_WIDTH * 2 + THREE_LETTER_SYMBOL_WIDTH + 70)
self.right_container.set_width(STATUS_BTN_WIDTH * 3)
self.right_container.set_height(lv.pct(100))
self.right_container.set_layout(lv.LAYOUT.FLEX)
self.right_container.set_flex_flow(lv.FLEX_FLOW.ROW)
Expand All @@ -90,13 +90,6 @@ def __init__(self, parent, height_pct=5, *args, **kwargs):
self.batt_icon.VALUE = parent.specter_state.battery_pct
self.batt_icon.update()

# Language indicator (clickable selector) - always visible
self.lang_lbl = lv.label(self.right_container)
self.lang_lbl.set_text("")
self.lang_lbl.set_width(THREE_LETTER_SYMBOL_WIDTH)
self.lang_lbl.add_flag(lv.obj.FLAG.CLICKABLE)
self.lang_lbl.add_event_cb(self.lang_clicked, lv.EVENT.CLICKED, None)

# Settings button
self.settings_btn = lv.button(self.right_container)
self.settings_btn.set_size(STATUS_BTN_WIDTH, STATUS_BTN_HEIGHT)
Expand All @@ -114,8 +107,8 @@ def __init__(self, parent, height_pct=5, *args, **kwargs):
self.power_btn.add_event_cb(self.power_cb, lv.EVENT.CLICKED, None)

# Apply smaller font to labels
self.font = lv.font_montserrat_12
labels = [self.lang_lbl, self.power_lbl]
self.font = lv.font_montserrat_16
labels = [self.power_lbl]
for lbl in labels:
lbl.set_style_text_font(self.font, 0)

Expand Down Expand Up @@ -170,10 +163,6 @@ def refresh(self, state):
self.batt_icon.VALUE = 100
self.batt_icon.update()

# Language (always visible)
lang_code = self.parent.i18n.get_language()
self.lang_lbl.set_text(self._truncate(lang_code.upper(), 3))

# Lock icon (always visible, but changes based on state)
if locked:
BTC_ICONS.LOCK.add_to_parent(self.lock_ico)
Expand Down
Loading
Loading