From 71edfaa3154f0f74647a358f1d04e998b2b28736 Mon Sep 17 00:00:00 2001 From: Jean Do Date: Fri, 21 Jul 2023 17:12:08 -0400 Subject: [PATCH 01/61] tabs to 4-spaces each --- src/seedsigner/hardware/microsd.py | 90 +++++++++++++++--------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/src/seedsigner/hardware/microsd.py b/src/seedsigner/hardware/microsd.py index 247df91b..016e1f2f 100644 --- a/src/seedsigner/hardware/microsd.py +++ b/src/seedsigner/hardware/microsd.py @@ -7,49 +7,49 @@ from seedsigner.models.settings import Settings from seedsigner.gui.screens.screen import MicroSDToastScreen class MicroSD(Singleton, BaseThread): - - ACTION__INSERTED = "add" - ACTION__REMOVED = "remove" + + ACTION__INSERTED = "add" + ACTION__REMOVED = "remove" - settings_handler = None - - @classmethod - def get_instance(cls): - # This is the only way to access the one and only instance - if cls._instance is None: - # Instantiate the one and only instance - microsd = cls.__new__(cls) - cls._instance = microsd - - # explicitly call BaseThread __init__ since multiple class inheritance - BaseThread.__init__(microsd) - - return cls._instance - - def start_detection(self): - self.start() - - def run(self): - import os - - FIFO_PATH = "/tmp/mdev_fifo" - FIFO_MODE = 0o600 - action = "" - - # explicitly only microsd add/remove detection in seedsigner-os - if Settings.HOSTNAME == Settings.SEEDSIGNER_OS: - - if os.path.exists(FIFO_PATH): - os.remove(FIFO_PATH) - - os.mkfifo(FIFO_PATH, FIFO_MODE) - - while self.keep_running: - with open(FIFO_PATH) as fifo: - action = fifo.read() - print(f"fifo message: {action}") - - Settings.microsd_handler(action=action) - - toastscreen = MicroSDToastScreen(action=action) - toastscreen.display() + settings_handler = None + + @classmethod + def get_instance(cls): + # This is the only way to access the one and only instance + if cls._instance is None: + # Instantiate the one and only instance + microsd = cls.__new__(cls) + cls._instance = microsd + + # explicitly call BaseThread __init__ since multiple class inheritance + BaseThread.__init__(microsd) + + return cls._instance + + def start_detection(self): + self.start() + + def run(self): + import os + + FIFO_PATH = "/tmp/mdev_fifo" + FIFO_MODE = 0o600 + action = "" + + # explicitly only microsd add/remove detection in seedsigner-os + if Settings.HOSTNAME == Settings.SEEDSIGNER_OS: + + if os.path.exists(FIFO_PATH): + os.remove(FIFO_PATH) + + os.mkfifo(FIFO_PATH, FIFO_MODE) + + while self.keep_running: + with open(FIFO_PATH) as fifo: + action = fifo.read() + print(f"fifo message: {action}") + + Settings.microsd_handler(action=action) + + toastscreen = MicroSDToastScreen(action=action) + toastscreen.display() From b3ea9849230d395f4b230bacb235d00fa69959c4 Mon Sep 17 00:00:00 2001 From: Jean Do Date: Fri, 21 Jul 2023 18:28:45 -0400 Subject: [PATCH 02/61] warning before main menu if microsd inserted --- src/seedsigner/controller.py | 7 +++++- src/seedsigner/hardware/microsd.py | 38 ++++++++++++++++++++---------- src/seedsigner/views/view.py | 18 +++++++++++++- tests/test_flows_view.py | 4 +++- 4 files changed, 51 insertions(+), 16 deletions(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 0d8c3cdd..550ccc9e 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -274,7 +274,12 @@ class Controller(Singleton): if initial_destination: next_destination = initial_destination else: - next_destination = Destination(MainMenuView) + # remind the user to remove the microsd card + if self.settings.HOSTNAME == Settings.SEEDSIGNER_OS and self.microsd.is_inserted(): + next_destination = Destination(RemoveMicroSDWarningView) + else: + next_destination = Destination(MainMenuView) + while True: # Destination(None) is a special case; render the Home screen if next_destination.View_cls is None: diff --git a/src/seedsigner/hardware/microsd.py b/src/seedsigner/hardware/microsd.py index 016e1f2f..44f67eed 100644 --- a/src/seedsigner/hardware/microsd.py +++ b/src/seedsigner/hardware/microsd.py @@ -1,4 +1,5 @@ import time +import os from seedsigner.models.singleton import Singleton from seedsigner.models.threads import BaseThread @@ -8,11 +9,12 @@ from seedsigner.gui.screens.screen import MicroSDToastScreen class MicroSD(Singleton, BaseThread): + MOUNT_POINT = "/mnt/microsd" + FIFO_PATH = "/tmp/mdev_fifo" + FIFO_MODE = 0o600 ACTION__INSERTED = "add" ACTION__REMOVED = "remove" - settings_handler = None - @classmethod def get_instance(cls): # This is the only way to access the one and only instance @@ -28,24 +30,34 @@ class MicroSD(Singleton, BaseThread): def start_detection(self): self.start() - + + def is_inserted(self): + # could only be False in seedsigner-os, else True + if Settings.HOSTNAME == Settings.SEEDSIGNER_OS: + return os.path.exists(self.MOUNT_POINT) + else: + return True + + def run(self): - import os - - FIFO_PATH = "/tmp/mdev_fifo" - FIFO_MODE = 0o600 action = "" # explicitly only microsd add/remove detection in seedsigner-os if Settings.HOSTNAME == Settings.SEEDSIGNER_OS: + + # at start-up, get current status and inform Settings + if self.is_inserted(): + Settings.microsd_handler(self.ACTION__INSERTED) + else: + Settings.microsd_handler(self.ACTION__REMOVED) + + if os.path.exists(self.FIFO_PATH): + os.remove(self.FIFO_PATH) - if os.path.exists(FIFO_PATH): - os.remove(FIFO_PATH) - - os.mkfifo(FIFO_PATH, FIFO_MODE) - + os.mkfifo(self.FIFO_PATH, self.FIFO_MODE) + while self.keep_running: - with open(FIFO_PATH) as fifo: + with open(self.FIFO_PATH) as fifo: action = fifo.read() print(f"fifo message: {action}") diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index dfd59eb3..ce4cd939 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -381,5 +381,21 @@ class OptionDisabledView(View): show_back_button=False, allow_text_overflow=True, # Fit what we can, let the rest go off the edges ).display() - + + + +class RemoveMicroSDWarningView(View): + """ + Warning to remove the microsd + """ + def run(self): + self.run_screen( + WarningScreen, + title="Warning!", + status_headline="MicroSD is Inserted.", + text="It is strongly advised\nto remove it before\nloading any seeds.", + show_back_button=False, + button_data=["Continue"], + ) + return Destination(MainMenuView, clear_history=True) diff --git a/tests/test_flows_view.py b/tests/test_flows_view.py index 3f4d537a..349e6812 100644 --- a/tests/test_flows_view.py +++ b/tests/test_flows_view.py @@ -6,7 +6,7 @@ from base import FlowTest, FlowStep from seedsigner.gui.screens.screen import RET_CODE__POWER_BUTTON from seedsigner.models.settings import Settings from seedsigner.views.tools_views import ToolsCalcFinalWordNumWordsView, ToolsMenuView -from seedsigner.views.view import MainMenuView, NotYetImplementedView, PowerOptionsView, PowerOffView, RestartView, UnhandledExceptionView, View +from seedsigner.views.view import MainMenuView, NotYetImplementedView, PowerOptionsView, PowerOffView, RestartView, UnhandledExceptionView, View, RemoveMicroSDWarningView @@ -38,6 +38,7 @@ class TestViewFlows(FlowTest): # And again, but this time as if we were in the SeedSigner OS Settings.HOSTNAME = Settings.SEEDSIGNER_OS self.run_sequence([ + FlowStep(RemoveMicroSDWarningView, screen_return_value=0), FlowStep(MainMenuView, screen_return_value=RET_CODE__POWER_BUTTON), FlowStep(PowerOptionsView, button_data_selection=PowerOptionsView.POWER_OFF), FlowStep(PowerOffView), # returns BackStackView @@ -65,6 +66,7 @@ class TestViewFlows(FlowTest): """ Basic flow from any arbitrary View to the UnhandledExceptionView """ + Settings.HOSTNAME = "NOT seedsigner-os" self.run_sequence([ FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS), FlowStep(ToolsMenuView, button_data_selection=ToolsMenuView.KEYBOARD), From 5ede56ea0b12649266b846adbfb66e331e0a1463 Mon Sep 17 00:00:00 2001 From: Jean Do Date: Fri, 28 Jul 2023 16:23:17 -0400 Subject: [PATCH 03/61] MicroSD warning after leaving MainMenu --- src/seedsigner/controller.py | 6 +--- src/seedsigner/hardware/microsd.py | 4 +++ src/seedsigner/views/view.py | 31 ++++++++++++++----- tests/test_flows_seed.py | 48 ++++++++++++++++++++++++++---- tests/test_flows_view.py | 1 - 5 files changed, 71 insertions(+), 19 deletions(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 550ccc9e..9fce99d5 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -274,11 +274,7 @@ class Controller(Singleton): if initial_destination: next_destination = initial_destination else: - # remind the user to remove the microsd card - if self.settings.HOSTNAME == Settings.SEEDSIGNER_OS and self.microsd.is_inserted(): - next_destination = Destination(RemoveMicroSDWarningView) - else: - next_destination = Destination(MainMenuView) + next_destination = Destination(MainMenuView) while True: # Destination(None) is a special case; render the Home screen diff --git a/src/seedsigner/hardware/microsd.py b/src/seedsigner/hardware/microsd.py index 44f67eed..71e591c9 100644 --- a/src/seedsigner/hardware/microsd.py +++ b/src/seedsigner/hardware/microsd.py @@ -14,6 +14,7 @@ class MicroSD(Singleton, BaseThread): FIFO_MODE = 0o600 ACTION__INSERTED = "add" ACTION__REMOVED = "remove" + warn_to_remove = True @classmethod def get_instance(cls): @@ -62,6 +63,9 @@ class MicroSD(Singleton, BaseThread): print(f"fifo message: {action}") Settings.microsd_handler(action=action) + + if action == self.ACTION__INSERTED: + self.warn_to_remove = True toastscreen = MicroSDToastScreen(action=action) toastscreen.display() diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index ce4cd939..8dabb626 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -185,6 +185,17 @@ class MainMenuView(View): TOOLS = ("Tools", SeedSignerIconConstants.TOOLS) SETTINGS = ("Settings", SeedSignerIconConstants.SETTINGS) + # returns a Destination for: RemoveMicroSDWarning or next_view + def microsd_warning_or_next_view(self, next_view): + if (self.settings.HOSTNAME == Settings.SEEDSIGNER_OS + and self.controller.microsd.is_inserted() + and self.controller.microsd.warn_to_remove + and len(self.controller.storage.seeds) == 0): + self.controller.microsd.warn_to_remove = False + return Destination(RemoveMicroSDWarningView, view_args={'next_view': next_view}) + else: + return Destination(next_view) + def run(self): button_data = [self.SCAN, self.SEEDS, self.TOOLS, self.SETTINGS] selected_menu_num = self.run_screen( @@ -201,15 +212,15 @@ class MainMenuView(View): if button_data[selected_menu_num] == self.SCAN: from .scan_views import ScanView - return Destination(ScanView) + return self.microsd_warning_or_next_view(ScanView) elif button_data[selected_menu_num] == self.SEEDS: from .seed_views import SeedsMenuView - return Destination(SeedsMenuView) + return self.microsd_warning_or_next_view(SeedsMenuView) elif button_data[selected_menu_num] == self.TOOLS: from .tools_views import ToolsMenuView - return Destination(ToolsMenuView) + return self.microsd_warning_or_next_view(ToolsMenuView) elif button_data[selected_menu_num] == self.SETTINGS: from .settings_views import SettingsMenuView @@ -388,14 +399,20 @@ class RemoveMicroSDWarningView(View): """ Warning to remove the microsd """ + def __init__(self, next_view: View): + super().__init__() + self.next_view = next_view + def run(self): self.run_screen( WarningScreen, - title="Warning!", - status_headline="MicroSD is Inserted.", - text="It is strongly advised\nto remove it before\nloading any seeds.", + title="Best-Practice Tip", + status_icon_name=FontAwesomeIconConstants.SDCARD, + status_headline="Microsd is inserted!", + status_color="red", + text="For maximum security\nremove the MicroSD card\nbefore continuing.", show_back_button=False, button_data=["Continue"], ) - return Destination(MainMenuView, clear_history=True) + return Destination(self.next_view, clear_history=True) diff --git a/tests/test_flows_seed.py b/tests/test_flows_seed.py index fde32231..b4f590a6 100644 --- a/tests/test_flows_seed.py +++ b/tests/test_flows_seed.py @@ -5,23 +5,58 @@ from base import BaseTest, FlowTest, FlowStep from base import FlowTestRunScreenNotExecutedException, FlowTestInvalidButtonDataSelectionException from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON -from seedsigner.models.settings import SettingsConstants +from seedsigner.models.settings import Settings, SettingsConstants from seedsigner.models.seed import Seed -from seedsigner.views.view import MainMenuView, View, NetworkMismatchErrorView -from seedsigner.views import seed_views, scan_views, settings_views +from seedsigner.views.view import MainMenuView, RemoveMicroSDWarningView, View, NetworkMismatchErrorView +from seedsigner.views import seed_views, scan_views, settings_views, tools_views +def load_seed_into_decoder(view: scan_views.ScanView): + view.decoder.add_data("0000" * 11 + "0003") class TestSeedFlows(FlowTest): + def test_flow_thru_microsd_warning(self): + """ + Selecting "Scan", "Seeds" or "Tools from the MainMenuView in ss-os with microsd inserted + will flow to RemoveMicroSDWarningView -- conditionally, then on to the intended next view + """ + Settings.HOSTNAME = Settings.SEEDSIGNER_OS + # will be warned to remove microsd card + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS), + FlowStep(RemoveMicroSDWarningView, screen_return_value=0), + FlowStep(tools_views.ToolsMenuView, screen_return_value=RET_CODE__BACK_BUTTON), # backed-out + FlowStep(MainMenuView), + ]) + # but won't be warned again, because microsd.warn_to_remove was set False just above + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection=MainMenuView.SEEDS), + FlowStep(seed_views.SeedsMenuView, is_redirect=True), + FlowStep(seed_views.LoadSeedView, screen_return_value=RET_CODE__BACK_BUTTON), # backed-out + FlowStep(MainMenuView), + ]) + # unless the microsd was re-inserted, which resets microsd.warn_to_remove = True. + self.controller.microsd.warn_to_remove = True + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN), + FlowStep(RemoveMicroSDWarningView, screen_return_value=0), + FlowStep(scan_views.ScanView, before_run=load_seed_into_decoder), # this time, loaded a seed + FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE), + FlowStep(seed_views.SeedOptionsView), + ]) + # won't get warned again if a seed is loaded (like was done above); it's already too late + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection=MainMenuView.SEEDS), + FlowStep(seed_views.SeedsMenuView), + ]) + + def test_scan_seedqr_flow(self): """ Selecting "Scan" from the MainMenuView and scanning a SeedQR should enter the Finalize Seed flow and end at the SeedOptionsView. """ - def load_seed_into_decoder(view: scan_views.ScanView): - view.decoder.add_data("0000" * 11 + "0003") - self.run_sequence([ FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN), FlowStep(scan_views.ScanView, before_run=load_seed_into_decoder), # simulate read SeedQR; ret val is ignored @@ -36,6 +71,7 @@ class TestSeedFlows(FlowTest): the SeedOptionsView. """ def test_with_mnemonic(mnemonic): + Settings.HOSTNAME = "not seedsigner-os" sequence = [ FlowStep(MainMenuView, button_data_selection=MainMenuView.SEEDS), FlowStep(seed_views.SeedsMenuView, is_redirect=True), # When no seeds are loaded it auto-redirects to LoadSeedView diff --git a/tests/test_flows_view.py b/tests/test_flows_view.py index 349e6812..24c880e6 100644 --- a/tests/test_flows_view.py +++ b/tests/test_flows_view.py @@ -38,7 +38,6 @@ class TestViewFlows(FlowTest): # And again, but this time as if we were in the SeedSigner OS Settings.HOSTNAME = Settings.SEEDSIGNER_OS self.run_sequence([ - FlowStep(RemoveMicroSDWarningView, screen_return_value=0), FlowStep(MainMenuView, screen_return_value=RET_CODE__POWER_BUTTON), FlowStep(PowerOptionsView, button_data_selection=PowerOptionsView.POWER_OFF), FlowStep(PowerOffView), # returns BackStackView From 02abeb6262a7cda86f681e8726f006e69f0eb84d Mon Sep 17 00:00:00 2001 From: Jean Do Date: Fri, 28 Jul 2023 17:40:40 -0400 Subject: [PATCH 04/61] corrected last test and cleanup --- tests/test_flows_seed.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_flows_seed.py b/tests/test_flows_seed.py index b4f590a6..d7896218 100644 --- a/tests/test_flows_seed.py +++ b/tests/test_flows_seed.py @@ -46,10 +46,13 @@ class TestSeedFlows(FlowTest): FlowStep(seed_views.SeedOptionsView), ]) # won't get warned again if a seed is loaded (like was done above); it's already too late + self.controller.microsd.warn_to_remove = True self.run_sequence([ FlowStep(MainMenuView, button_data_selection=MainMenuView.SEEDS), FlowStep(seed_views.SeedsMenuView), ]) + # no warning shown so this flag not cleared, cleanup else following tests will warn + self.controller.microsd.warn_to_remove = False def test_scan_seedqr_flow(self): From ba2e96669c542e1dd5b3f90f92d1093f76d5b677 Mon Sep 17 00:00:00 2001 From: Jean Do Date: Wed, 2 Aug 2023 04:07:47 -0400 Subject: [PATCH 05/61] text changes; optimized condition --- src/seedsigner/views/view.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index 8dabb626..8dd9954a 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -185,12 +185,12 @@ class MainMenuView(View): TOOLS = ("Tools", SeedSignerIconConstants.TOOLS) SETTINGS = ("Settings", SeedSignerIconConstants.SETTINGS) - # returns a Destination for: RemoveMicroSDWarning or next_view + # returns a Destination for: RemoveMicroSDWarningView or next_view def microsd_warning_or_next_view(self, next_view): if (self.settings.HOSTNAME == Settings.SEEDSIGNER_OS - and self.controller.microsd.is_inserted() and self.controller.microsd.warn_to_remove - and len(self.controller.storage.seeds) == 0): + and len(self.controller.storage.seeds) == 0 + and self.controller.microsd.is_inserted()): self.controller.microsd.warn_to_remove = False return Destination(RemoveMicroSDWarningView, view_args={'next_view': next_view}) else: @@ -406,11 +406,10 @@ class RemoveMicroSDWarningView(View): def run(self): self.run_screen( WarningScreen, - title="Best-Practice Tip", + title="Security Tip", status_icon_name=FontAwesomeIconConstants.SDCARD, - status_headline="Microsd is inserted!", - status_color="red", - text="For maximum security\nremove the MicroSD card\nbefore continuing.", + status_headline="", + text="For maximum security,\nremove the MicroSD card\nbefore continuing.", show_back_button=False, button_data=["Continue"], ) From 2610b6229c8935417f722a2d7d7009a53213fa5c Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 5 Aug 2023 18:24:09 -0500 Subject: [PATCH 06/61] Basic first version --- src/seedsigner/gui/components.py | 70 ++++++++++++++++------------ src/seedsigner/gui/screens/screen.py | 39 ++++++++++++++++ src/seedsigner/views/view.py | 30 ++++-------- 3 files changed, 88 insertions(+), 51 deletions(-) diff --git a/src/seedsigner/gui/components.py b/src/seedsigner/gui/components.py index 737247a2..b85df47b 100644 --- a/src/seedsigner/gui/components.py +++ b/src/seedsigner/gui/components.py @@ -572,68 +572,78 @@ class ToastOverlay(BaseComponent): icon_name: str = None color: str = None label_text: str = None + height: int = 20 + font_size: int = 19 + duration: int = 3 # seconds + outline_thickness: int = 2 # pixels def __post_init__(self): super().__post_init__() + # TODO: change absolute value pixels to use GUIConstants self.icon = Icon( image_draw=self.image_draw, canvas=self.canvas, - screen_x=20, - screen_y=190, + screen_x=GUIConstants.EDGE_PADDING + self.outline_thickness + GUIConstants.EDGE_PADDING, icon_name=self.icon_name, icon_size=30, icon_color=self.color ) + self.icon.screen_y = self.canvas_height - self.height + int((self.height - self.icon.height)/2) self.label = TextArea( image_draw=self.image_draw, canvas=self.canvas, text=self.label_text, - font_size=19, + font_size=self.font_size, font_color=self.color, edge_padding=0, is_text_centered=False, - auto_line_break=False, - width=160, - height=20, - screen_x=55, - screen_y=195, + auto_line_break=True, + width=self.canvas_width - self.icon.screen_x - self.icon.width - GUIConstants.COMPONENT_PADDING - self.outline_thickness - GUIConstants.EDGE_PADDING, + screen_x=self.icon.screen_x + self.icon.width + GUIConstants.COMPONENT_PADDING, + screen_y=self.canvas_height - self.height + GUIConstants.EDGE_PADDING, allow_text_overflow=False ) - + + def render(self): import time from seedsigner.controller import Controller from seedsigner.hardware.buttons import HardwareButtons - + self.controller: Controller = Controller.get_instance() self.current_screen = self.renderer.canvas.copy() buttons = HardwareButtons.get_instance() - - # Special case when screensaver is running - if self.controller.is_screensaver_running: - buttons.override_ind = True - self.image_draw.rounded_rectangle( - ( GUIConstants.EDGE_PADDING + 2, self.canvas_height - 60, self.canvas_width - GUIConstants.EDGE_PADDING - 2, self.canvas_width - GUIConstants.EDGE_PADDING - 2), - fill=GUIConstants.BACKGROUND_COLOR, - radius=8, - outline=self.color, - width=2, - ) - - self.icon.render() - self.label.render() - - self.renderer.show_image() - - t_end = time.time() + 3 + # Special case when screensaver is running + buttons.override_ind = True + + with self.renderer.lock: + self.image_draw.rounded_rectangle( + ( GUIConstants.EDGE_PADDING, self.canvas_height - self.height, self.canvas_width - GUIConstants.EDGE_PADDING, self.canvas_height), + fill=GUIConstants.BACKGROUND_COLOR, + radius=8, + outline=self.color, + width=self.outline_thickness, + ) + + self.icon.render() + self.label.render() + + self.renderer.show_image() + + t_end = time.time() + self.duration + + # Persist until timeout... while time.time() < t_end: + # or hide it on button press if buttons.has_any_input(): break - - self.renderer.show_image(self.current_screen) + + # Restore the screen as it was before the toast + with self.renderer.lock: + self.renderer.show_image(self.current_screen) diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index d0005fb9..08962cfd 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -1222,6 +1222,8 @@ class KeyboardScreen(BaseTopNavScreen): """ return False + + class MicroSDToastScreen(BaseScreen): """ This screen is an overlay with special behavior with the ToastOverlay component. The ToastOverlay component overides all button @@ -1256,3 +1258,40 @@ class MicroSDToastScreen(BaseScreen): ) self.toast.render() + + + +@dataclass +class MainMenuScreen(LargeButtonScreen): + # Override LargeButtonScreen defaults + title_font_size: int = 26 + show_back_button: bool = False + show_power_button: bool = True + + + class SDCardNotificationToastThread(BaseThread): + def run(self): + print("Started SDCardNotificationToastThread") + activation_delay = 3 # seconds + toast = ToastOverlay( + icon_name=FontAwesomeIconConstants.SDCARD, + color=GUIConstants.NOTIFICATION_COLOR, + label_text="Security tip:\nRemove SD card", + duration=999, # seconds + font_size=GUIConstants.BODY_FONT_SIZE, + height=GUIConstants.BODY_FONT_SIZE * 2 + GUIConstants.BODY_LINE_SPACING + GUIConstants.EDGE_PADDING, + ) + + start = time.time() + has_rendered = False + while self.keep_running: + if time.time() - start > activation_delay and not has_rendered: + toast.render() + has_rendered = True + time.sleep(0.1) + + + def __post_init__(self): + super().__post_init__() + self.threads.append(self.SDCardNotificationToastThread()) + diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index 8dd9954a..d3e3c73c 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -185,45 +185,33 @@ class MainMenuView(View): TOOLS = ("Tools", SeedSignerIconConstants.TOOLS) SETTINGS = ("Settings", SeedSignerIconConstants.SETTINGS) - # returns a Destination for: RemoveMicroSDWarningView or next_view - def microsd_warning_or_next_view(self, next_view): - if (self.settings.HOSTNAME == Settings.SEEDSIGNER_OS - and self.controller.microsd.warn_to_remove - and len(self.controller.storage.seeds) == 0 - and self.controller.microsd.is_inserted()): - self.controller.microsd.warn_to_remove = False - return Destination(RemoveMicroSDWarningView, view_args={'next_view': next_view}) - else: - return Destination(next_view) def run(self): + from seedsigner.gui.screens.screen import MainMenuScreen button_data = [self.SCAN, self.SEEDS, self.TOOLS, self.SETTINGS] selected_menu_num = self.run_screen( - LargeButtonScreen, + MainMenuScreen, title="Home", - title_font_size=26, button_data=button_data, - show_back_button=False, - show_power_button=True, ) if selected_menu_num == RET_CODE__POWER_BUTTON: return Destination(PowerOptionsView) if button_data[selected_menu_num] == self.SCAN: - from .scan_views import ScanView - return self.microsd_warning_or_next_view(ScanView) + from seedsigner.views.scan_views import ScanView + return Destination(ScanView) elif button_data[selected_menu_num] == self.SEEDS: - from .seed_views import SeedsMenuView - return self.microsd_warning_or_next_view(SeedsMenuView) + from seedsigner.views.seed_views import SeedsMenuView + return Destination(SeedsMenuView) elif button_data[selected_menu_num] == self.TOOLS: - from .tools_views import ToolsMenuView - return self.microsd_warning_or_next_view(ToolsMenuView) + from seedsigner.views.tools_views import ToolsMenuView + return Destination(ToolsMenuView) elif button_data[selected_menu_num] == self.SETTINGS: - from .settings_views import SettingsMenuView + from seedsigner.views.settings_views import SettingsMenuView return Destination(SettingsMenuView) From b064de162a144b11230e720342031b275995098b Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 5 Aug 2023 18:42:57 -0500 Subject: [PATCH 07/61] Expanding Toast into edge padding * attends to actual microSD state --- src/seedsigner/gui/components.py | 8 ++++---- src/seedsigner/gui/screens/screen.py | 9 +++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/seedsigner/gui/components.py b/src/seedsigner/gui/components.py index b85df47b..1b44de5d 100644 --- a/src/seedsigner/gui/components.py +++ b/src/seedsigner/gui/components.py @@ -584,12 +584,12 @@ class ToastOverlay(BaseComponent): self.icon = Icon( image_draw=self.image_draw, canvas=self.canvas, - screen_x=GUIConstants.EDGE_PADDING + self.outline_thickness + GUIConstants.EDGE_PADDING, + screen_x=self.outline_thickness + 2*GUIConstants.EDGE_PADDING, # Push the icon further from the left edge than strictly necessary icon_name=self.icon_name, icon_size=30, icon_color=self.color ) - self.icon.screen_y = self.canvas_height - self.height + int((self.height - self.icon.height)/2) + self.icon.screen_y = self.canvas_height - self.height + int((self.height - self.icon.height)/2) - 1 # -1 fudge factor self.label = TextArea( image_draw=self.image_draw, @@ -600,7 +600,7 @@ class ToastOverlay(BaseComponent): edge_padding=0, is_text_centered=False, auto_line_break=True, - width=self.canvas_width - self.icon.screen_x - self.icon.width - GUIConstants.COMPONENT_PADDING - self.outline_thickness - GUIConstants.EDGE_PADDING, + width=self.canvas_width - self.icon.screen_x - self.icon.width - GUIConstants.COMPONENT_PADDING - self.outline_thickness, screen_x=self.icon.screen_x + self.icon.width + GUIConstants.COMPONENT_PADDING, screen_y=self.canvas_height - self.height + GUIConstants.EDGE_PADDING, allow_text_overflow=False @@ -621,7 +621,7 @@ class ToastOverlay(BaseComponent): with self.renderer.lock: self.image_draw.rounded_rectangle( - ( GUIConstants.EDGE_PADDING, self.canvas_height - self.height, self.canvas_width - GUIConstants.EDGE_PADDING, self.canvas_height), + (0, self.canvas_height - self.height, self.canvas_width, self.canvas_height), fill=GUIConstants.BACKGROUND_COLOR, radius=8, outline=self.color, diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index 08962cfd..0ebd75a1 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -1271,13 +1271,12 @@ class MainMenuScreen(LargeButtonScreen): class SDCardNotificationToastThread(BaseThread): def run(self): - print("Started SDCardNotificationToastThread") activation_delay = 3 # seconds toast = ToastOverlay( icon_name=FontAwesomeIconConstants.SDCARD, color=GUIConstants.NOTIFICATION_COLOR, label_text="Security tip:\nRemove SD card", - duration=999, # seconds + duration=999999, # persist "forever" font_size=GUIConstants.BODY_FONT_SIZE, height=GUIConstants.BODY_FONT_SIZE * 2 + GUIConstants.BODY_LINE_SPACING + GUIConstants.EDGE_PADDING, ) @@ -1292,6 +1291,8 @@ class MainMenuScreen(LargeButtonScreen): def __post_init__(self): + from seedsigner.controller import Controller super().__post_init__() - self.threads.append(self.SDCardNotificationToastThread()) - + if Controller.get_instance().microsd.is_inserted: + # Remind user they can/should remove the SD card + self.threads.append(self.SDCardNotificationToastThread()) From d75d71ba37b135204851627b6fdf0362754e2bd6 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 5 Aug 2023 18:50:51 -0500 Subject: [PATCH 08/61] persist for 1mil seconds --- src/seedsigner/gui/screens/screen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index 0ebd75a1..4ad10f8e 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -1276,7 +1276,7 @@ class MainMenuScreen(LargeButtonScreen): icon_name=FontAwesomeIconConstants.SDCARD, color=GUIConstants.NOTIFICATION_COLOR, label_text="Security tip:\nRemove SD card", - duration=999999, # persist "forever" + duration=1e6, # persist "forever" font_size=GUIConstants.BODY_FONT_SIZE, height=GUIConstants.BODY_FONT_SIZE * 2 + GUIConstants.BODY_LINE_SPACING + GUIConstants.EDGE_PADDING, ) From 6186ecb21c37562c47bd41447537d0ef1fab4d2d Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 5 Aug 2023 22:16:07 -0500 Subject: [PATCH 09/61] refactoring ToastOverlay; control moved to Screen --- src/seedsigner/gui/components.py | 52 ++++---------- src/seedsigner/gui/screens/screen.py | 101 +++++++++++++++++++++------ 2 files changed, 94 insertions(+), 59 deletions(-) diff --git a/src/seedsigner/gui/components.py b/src/seedsigner/gui/components.py index 1b44de5d..c7113801 100644 --- a/src/seedsigner/gui/components.py +++ b/src/seedsigner/gui/components.py @@ -35,6 +35,7 @@ class GUIConstants: ICON_FONT_SIZE = 22 ICON_INLINE_FONT_SIZE = 24 ICON_LARGE_BUTTON_SIZE = 48 + ICON_TOAST_FONT_SIZE = 30 ICON_PRIMARY_SCREEN_SIZE = 50 TOP_NAV_TITLE_FONT_NAME = "OpenSans-SemiBold" @@ -572,21 +573,19 @@ class ToastOverlay(BaseComponent): icon_name: str = None color: str = None label_text: str = None - height: int = 20 + height: int = GUIConstants.ICON_TOAST_FONT_SIZE + 2*GUIConstants.EDGE_PADDING font_size: int = 19 - duration: int = 3 # seconds outline_thickness: int = 2 # pixels def __post_init__(self): super().__post_init__() - # TODO: change absolute value pixels to use GUIConstants self.icon = Icon( image_draw=self.image_draw, canvas=self.canvas, screen_x=self.outline_thickness + 2*GUIConstants.EDGE_PADDING, # Push the icon further from the left edge than strictly necessary icon_name=self.icon_name, - icon_size=30, + icon_size=GUIConstants.ICON_TOAST_FONT_SIZE, icon_color=self.color ) self.icon.screen_y = self.canvas_height - self.height + int((self.height - self.icon.height)/2) - 1 # -1 fudge factor @@ -602,48 +601,25 @@ class ToastOverlay(BaseComponent): auto_line_break=True, width=self.canvas_width - self.icon.screen_x - self.icon.width - GUIConstants.COMPONENT_PADDING - self.outline_thickness, screen_x=self.icon.screen_x + self.icon.width + GUIConstants.COMPONENT_PADDING, - screen_y=self.canvas_height - self.height + GUIConstants.EDGE_PADDING, allow_text_overflow=False ) + self.label.screen_y = self.canvas_height - self.height + int((self.height - self.label.height)/2) def render(self): - import time - from seedsigner.controller import Controller - from seedsigner.hardware.buttons import HardwareButtons + self.image_draw.rounded_rectangle( + (0, self.canvas_height - self.height, self.canvas_width, self.canvas_height), + fill=GUIConstants.BACKGROUND_COLOR, + radius=8, + outline=self.color, + width=self.outline_thickness, + ) - self.controller: Controller = Controller.get_instance() - self.current_screen = self.renderer.canvas.copy() - buttons = HardwareButtons.get_instance() + self.icon.render() + self.label.render() - # Special case when screensaver is running - buttons.override_ind = True + self.renderer.show_image() - with self.renderer.lock: - self.image_draw.rounded_rectangle( - (0, self.canvas_height - self.height, self.canvas_width, self.canvas_height), - fill=GUIConstants.BACKGROUND_COLOR, - radius=8, - outline=self.color, - width=self.outline_thickness, - ) - - self.icon.render() - self.label.render() - - self.renderer.show_image() - - t_end = time.time() + self.duration - - # Persist until timeout... - while time.time() < t_end: - # or hide it on button press - if buttons.has_any_input(): - break - - # Restore the screen as it was before the toast - with self.renderer.lock: - self.renderer.show_image(self.current_screen) diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index 4ad10f8e..b6091155 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -1224,39 +1224,61 @@ class KeyboardScreen(BaseTopNavScreen): +@dataclass class MicroSDToastScreen(BaseScreen): + action: str = None + duration: int = 3 # seconds + """ This screen is an overlay with special behavior with the ToastOverlay component. The ToastOverlay component overides all button input and captures the existing screen content and stashes it to be restored once X second passes or any button is pressed. The display method on this screen will not complete until after the ToastOverlay render method is complete it's takeover of the screen. """ - def __init__(self, action): - self.action = action - self.toast = None - super().__init__() - - def _run(self): - return - - def _render(self): + + def __post_init__(self): from seedsigner.hardware.microsd import MicroSD - + super().__post_init__() + self.current_screen = self.renderer.canvas.copy() + if self.action == MicroSD.ACTION__REMOVED: - self.toast = ToastOverlay( icon_name=SeedSignerIconConstants.MICROSD, color=GUIConstants.NOTIFICATION_COLOR, - label_text="MicroSD removed" + label_text="SD card removed", ) elif self.action == MicroSD.ACTION__INSERTED: - self.toast = ToastOverlay( icon_name=SeedSignerIconConstants.MICROSD, color=GUIConstants.NOTIFICATION_COLOR, - label_text="MicroSD inserted" + label_text="SD card inserted" ) - + + + def _run(self): + t_end = time.time() + self.duration + + # Special case when screensaver is running + self.hw_inputs.override_ind = True + + try: + # Hold onto the Renderer lock so we're guaranteed to restore the original + # screen before any other listener can get a screen write in. + self.renderer.lock.acquire() + + # Persist until timeout... + while time.time() < t_end: + # or hide it on button press + if self.hw_inputs.has_any_input(): + break + finally: + # Restore the screen as it was before the toast + self.renderer.show_image(self.current_screen) + self.renderer.lock.release() + + + def _render(self): + """ Override Screen.render() so the whole screen isn't cleared """ self.toast.render() @@ -1271,23 +1293,51 @@ class MainMenuScreen(LargeButtonScreen): class SDCardNotificationToastThread(BaseThread): def run(self): + from seedsigner.controller import Controller + from seedsigner.gui.renderer import Renderer + from seedsigner.hardware.buttons import HardwareButtons + renderer = Renderer.get_instance() + controller = Controller.get_instance() + hw_inputs = HardwareButtons.get_instance() + + # Special case when screensaver is running + hw_inputs.override_ind = True + activation_delay = 3 # seconds toast = ToastOverlay( icon_name=FontAwesomeIconConstants.SDCARD, color=GUIConstants.NOTIFICATION_COLOR, label_text="Security tip:\nRemove SD card", - duration=1e6, # persist "forever" font_size=GUIConstants.BODY_FONT_SIZE, height=GUIConstants.BODY_FONT_SIZE * 2 + GUIConstants.BODY_LINE_SPACING + GUIConstants.EDGE_PADDING, ) start = time.time() has_rendered = False - while self.keep_running: - if time.time() - start > activation_delay and not has_rendered: - toast.render() - has_rendered = True - time.sleep(0.1) + self.previous_screen_state = None + try: + # Hold onto the Renderer lock so we're guaranteed to restore the original + # screen before any other listener can get a screen write in. + renderer.lock.acquire() + while self.keep_running: + if not controller.microsd.is_inserted: + # Card is removed! No need to keep running + break + if hw_inputs.has_any_input(): + # User has pressed a button, hide the toast + break + if time.time() - start > activation_delay and not has_rendered: + self.previous_screen_state = renderer.canvas.copy() + toast.render() + has_rendered = True + time.sleep(0.1) + finally: + if has_rendered: + renderer.show_image(self.previous_screen_state) + + # We're done, release the lock + renderer.lock.release() + def __post_init__(self): @@ -1296,3 +1346,12 @@ class MainMenuScreen(LargeButtonScreen): if Controller.get_instance().microsd.is_inserted: # Remind user they can/should remove the SD card self.threads.append(self.SDCardNotificationToastThread()) + + + # DEBUGGING + # def _run(self): + # from seedsigner.hardware.microsd import MicroSD + # toast = MicroSDToastScreen(action=MicroSD.ACTION__INSERTED) + # toast.display() + # return super()._run() + # DEBUGGING From 3f8f3935c90bd2e88cc03f09ee02dcfc5f04f682 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 5 Aug 2023 22:54:55 -0500 Subject: [PATCH 10/61] screensaver interrupt/resume --- src/seedsigner/controller.py | 3 ++- src/seedsigner/gui/components.py | 2 +- src/seedsigner/gui/screens/screen.py | 26 ++++++++++++-------------- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 9fce99d5..a48801af 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -181,7 +181,8 @@ class Controller(Singleton): controller.back_stack = BackStack() # Other behavior constants - controller.screensaver_activation_ms = 120 * 1000 + # controller.screensaver_activation_ms = 2 * 60 * 1000 + controller.screensaver_activation_ms = 20 * 1000 # DEBUGGING background_import_thread = BackgroundImportThread() background_import_thread.start() diff --git a/src/seedsigner/gui/components.py b/src/seedsigner/gui/components.py index c7113801..2a37f34a 100644 --- a/src/seedsigner/gui/components.py +++ b/src/seedsigner/gui/components.py @@ -571,7 +571,7 @@ class IconTextLine(BaseComponent): @dataclass class ToastOverlay(BaseComponent): icon_name: str = None - color: str = None + color: str = GUIConstants.NOTIFICATION_COLOR label_text: str = None height: int = GUIConstants.ICON_TOAST_FONT_SIZE + 2*GUIConstants.EDGE_PADDING font_size: int = 19 diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index b6091155..b17e8913 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -1240,19 +1240,10 @@ class MicroSDToastScreen(BaseScreen): super().__post_init__() self.current_screen = self.renderer.canvas.copy() - if self.action == MicroSD.ACTION__REMOVED: - self.toast = ToastOverlay( - icon_name=SeedSignerIconConstants.MICROSD, - color=GUIConstants.NOTIFICATION_COLOR, - label_text="SD card removed", - ) - - elif self.action == MicroSD.ACTION__INSERTED: - self.toast = ToastOverlay( - icon_name=SeedSignerIconConstants.MICROSD, - color=GUIConstants.NOTIFICATION_COLOR, - label_text="SD card inserted" - ) + self.toast = ToastOverlay( + icon_name=FontAwesomeIconConstants.SDCARD, + label_text="SD card removed" if self.action == MicroSD.ACTION__REMOVED else "SD card inserted", + ) def _run(self): @@ -1306,7 +1297,6 @@ class MainMenuScreen(LargeButtonScreen): activation_delay = 3 # seconds toast = ToastOverlay( icon_name=FontAwesomeIconConstants.SDCARD, - color=GUIConstants.NOTIFICATION_COLOR, label_text="Security tip:\nRemove SD card", font_size=GUIConstants.BODY_FONT_SIZE, height=GUIConstants.BODY_FONT_SIZE * 2 + GUIConstants.BODY_LINE_SPACING + GUIConstants.EDGE_PADDING, @@ -1326,6 +1316,14 @@ class MainMenuScreen(LargeButtonScreen): if hw_inputs.has_any_input(): # User has pressed a button, hide the toast break + if controller.is_screensaver_running: + # Have to release the lock so the screensaver can render + renderer.lock.release() + + # Immediately attempt to reacquire the lock, but in reality this + # thread will block here until the screensaver releases it. + time.sleep(0.1) + renderer.lock.acquire() if time.time() - start > activation_delay and not has_rendered: self.previous_screen_state = renderer.canvas.copy() toast.render() From 9a8ba98161140ae3909954e24e7cd8f9f03a23b0 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 5 Aug 2023 23:53:11 -0500 Subject: [PATCH 11/61] MicroSD is_inserted w/out blocking --- src/seedsigner/gui/screens/screen.py | 9 +++++---- src/seedsigner/hardware/microsd.py | 17 ++++++++++------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index b17e8913..9394e915 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -1256,7 +1256,7 @@ class MicroSDToastScreen(BaseScreen): # Hold onto the Renderer lock so we're guaranteed to restore the original # screen before any other listener can get a screen write in. self.renderer.lock.acquire() - + # Persist until timeout... while time.time() < t_end: # or hide it on button press @@ -1287,6 +1287,7 @@ class MainMenuScreen(LargeButtonScreen): from seedsigner.controller import Controller from seedsigner.gui.renderer import Renderer from seedsigner.hardware.buttons import HardwareButtons + from seedsigner.hardware.microsd import MicroSD renderer = Renderer.get_instance() controller = Controller.get_instance() hw_inputs = HardwareButtons.get_instance() @@ -1310,7 +1311,7 @@ class MainMenuScreen(LargeButtonScreen): # screen before any other listener can get a screen write in. renderer.lock.acquire() while self.keep_running: - if not controller.microsd.is_inserted: + if not MicroSD.is_inserted(): # Card is removed! No need to keep running break if hw_inputs.has_any_input(): @@ -1339,9 +1340,9 @@ class MainMenuScreen(LargeButtonScreen): def __post_init__(self): - from seedsigner.controller import Controller + from seedsigner.hardware.microsd import MicroSD super().__post_init__() - if Controller.get_instance().microsd.is_inserted: + if MicroSD.is_inserted(): # Remind user they can/should remove the SD card self.threads.append(self.SDCardNotificationToastThread()) diff --git a/src/seedsigner/hardware/microsd.py b/src/seedsigner/hardware/microsd.py index 71e591c9..790ffde4 100644 --- a/src/seedsigner/hardware/microsd.py +++ b/src/seedsigner/hardware/microsd.py @@ -1,12 +1,12 @@ -import time import os from seedsigner.models.singleton import Singleton from seedsigner.models.threads import BaseThread from seedsigner.models.settings import Settings -#from seedsigner.views.view import MicroSDToastView from seedsigner.gui.screens.screen import MicroSDToastScreen + + class MicroSD(Singleton, BaseThread): MOUNT_POINT = "/mnt/microsd" @@ -29,17 +29,20 @@ class MicroSD(Singleton, BaseThread): return cls._instance - def start_detection(self): - self.start() - def is_inserted(self): + @classmethod + def is_inserted(cls): # could only be False in seedsigner-os, else True if Settings.HOSTNAME == Settings.SEEDSIGNER_OS: - return os.path.exists(self.MOUNT_POINT) + return os.path.exists(MicroSD.MOUNT_POINT) else: return True + def start_detection(self): + self.start() + + def run(self): action = "" @@ -47,7 +50,7 @@ class MicroSD(Singleton, BaseThread): if Settings.HOSTNAME == Settings.SEEDSIGNER_OS: # at start-up, get current status and inform Settings - if self.is_inserted(): + if MicroSD.is_inserted(): Settings.microsd_handler(self.ACTION__INSERTED) else: Settings.microsd_handler(self.ACTION__REMOVED) From aaa875f7ad6eaa49eae98f7f37517084332df5c6 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 6 Aug 2023 14:13:59 -0500 Subject: [PATCH 12/61] move all Toast functionality into toast.py * Explicit toast control integrated into `Controller` * Coordinate toast vs screensaver Renderer.lock handoffs * Refactor existing toasts to new reusable paradigm --- src/seedsigner/controller.py | 44 +++++- src/seedsigner/gui/components.py | 55 ------- src/seedsigner/gui/screens/screen.py | 127 +--------------- src/seedsigner/gui/toast.py | 218 +++++++++++++++++++++++++++ src/seedsigner/hardware/microsd.py | 16 +- src/seedsigner/views/screensaver.py | 1 - 6 files changed, 269 insertions(+), 192 deletions(-) create mode 100644 src/seedsigner/gui/toast.py diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index a48801af..054cac30 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -130,6 +130,7 @@ class Controller(Singleton): back_stack: BackStack = None screensaver: 'ScreensaverScreen' = None + toast_notification_thread: 'BaseToastOverlayManagerThread' = None @classmethod @@ -182,7 +183,7 @@ class Controller(Singleton): # Other behavior constants # controller.screensaver_activation_ms = 2 * 60 * 1000 - controller.screensaver_activation_ms = 20 * 1000 # DEBUGGING + controller.screensaver_activation_ms = 15 * 1000 # DEBUGGING background_import_thread = BackgroundImportThread() background_import_thread.start() @@ -241,8 +242,9 @@ class Controller(Singleton): * initial_destination: The first View to run. If None, the MainMenuView is used. Only used by the test suite. """ - from .views import MainMenuView, BackStackView - from .views.screensaver import OpeningSplashScreen + from seedsigner.views import MainMenuView, BackStackView + from seedsigner.views.screensaver import OpeningSplashScreen + from seedsigner.gui.toast import RemoveSDCardToastManagerThread OpeningSplashScreen().start() @@ -276,6 +278,9 @@ class Controller(Singleton): next_destination = initial_destination else: next_destination = Destination(MainMenuView) + + # Set up our one-time toast notification tip to remove the SD card + self.activate_toast(RemoveSDCardToastManagerThread()) while True: # Destination(None) is a special case; render the Home screen @@ -356,6 +361,9 @@ class Controller(Singleton): from seedsigner.gui.renderer import Renderer if self.is_screensaver_running: self.screensaver.stop() + + if self.toast_notification_thread and self.toast_notification_thread.is_alive(): + self.toast_notification_thread.stop() # Clear the screen when exiting print("Clearing screen, exiting") @@ -368,12 +376,42 @@ class Controller(Singleton): def start_screensaver(self): + # If a toast is running, tell it to give up the Renderer.lock; it will then + # block until the screensaver is done, at which point the toast can re-acquire + # the Renderer.lock and resume where it left off. + if self.toast_notification_thread and self.toast_notification_thread.is_alive(): + print(f"Controller: settings toggle_render_lock for {self.toast_notification_thread.__class__.__name__}") + self.toast_notification_thread.toggle_renderer_lock() + + print("Controller: Starting screensaver") if not self.screensaver: # Do a lazy/late import and instantiation to reduce Controller initial startup time from seedsigner.views.screensaver import ScreensaverScreen from seedsigner.hardware.buttons import HardwareButtons self.screensaver = ScreensaverScreen(HardwareButtons.get_instance()) + + # Start the screensaver, but it will block until it can acquire the Renderer.lock. self.screensaver.start() + print("Controller: Screensaver started") + + + def activate_toast(self, toast_manager_thread: 'BaseToastOverlayManagerThread'): + """ + Ensures that the Controller has explicit control over which processes get to + claim the Renderer.lock and which need to (potentially) release it. + """ + if self.is_screensaver_running: + # New toast notifications break out of the Screensaver + self.screensaver.stop() + + if self.toast_notification_thread and self.toast_notification_thread.is_alive(): + # Can only run one toast at a time + print(f"Controller: stopping {self.toast_notification_thread.__class__.__name__}") + self.toast_notification_thread.stop() + + self.toast_notification_thread = toast_manager_thread + print(f"Controller: starting {self.toast_notification_thread.__class__.__name__}") + self.toast_notification_thread.start() def handle_exception(self, e) -> Destination: diff --git a/src/seedsigner/gui/components.py b/src/seedsigner/gui/components.py index 2a37f34a..fa8190f5 100644 --- a/src/seedsigner/gui/components.py +++ b/src/seedsigner/gui/components.py @@ -568,61 +568,6 @@ class IconTextLine(BaseComponent): -@dataclass -class ToastOverlay(BaseComponent): - icon_name: str = None - color: str = GUIConstants.NOTIFICATION_COLOR - label_text: str = None - height: int = GUIConstants.ICON_TOAST_FONT_SIZE + 2*GUIConstants.EDGE_PADDING - font_size: int = 19 - outline_thickness: int = 2 # pixels - - def __post_init__(self): - super().__post_init__() - - self.icon = Icon( - image_draw=self.image_draw, - canvas=self.canvas, - screen_x=self.outline_thickness + 2*GUIConstants.EDGE_PADDING, # Push the icon further from the left edge than strictly necessary - icon_name=self.icon_name, - icon_size=GUIConstants.ICON_TOAST_FONT_SIZE, - icon_color=self.color - ) - self.icon.screen_y = self.canvas_height - self.height + int((self.height - self.icon.height)/2) - 1 # -1 fudge factor - - self.label = TextArea( - image_draw=self.image_draw, - canvas=self.canvas, - text=self.label_text, - font_size=self.font_size, - font_color=self.color, - edge_padding=0, - is_text_centered=False, - auto_line_break=True, - width=self.canvas_width - self.icon.screen_x - self.icon.width - GUIConstants.COMPONENT_PADDING - self.outline_thickness, - screen_x=self.icon.screen_x + self.icon.width + GUIConstants.COMPONENT_PADDING, - allow_text_overflow=False - ) - self.label.screen_y = self.canvas_height - self.height + int((self.height - self.label.height)/2) - - - def render(self): - self.image_draw.rounded_rectangle( - (0, self.canvas_height - self.height, self.canvas_width, self.canvas_height), - fill=GUIConstants.BACKGROUND_COLOR, - radius=8, - outline=self.color, - width=self.outline_thickness, - ) - - self.icon.render() - self.label.render() - - self.renderer.show_image() - - - - @dataclass class FormattedAddress(BaseComponent): """ diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index 9394e915..a1d1dd34 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -2,7 +2,8 @@ import time from dataclasses import dataclass from PIL import Image, ImageDraw, ImageColor -from typing import Any, List, Tuple +from typing import Any, Callable, List, Tuple +from seedsigner.gui.components import ToastOverlay from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay from seedsigner.gui.renderer import Renderer @@ -1224,133 +1225,9 @@ class KeyboardScreen(BaseTopNavScreen): -@dataclass -class MicroSDToastScreen(BaseScreen): - action: str = None - duration: int = 3 # seconds - - """ - This screen is an overlay with special behavior with the ToastOverlay component. The ToastOverlay component overides all button - input and captures the existing screen content and stashes it to be restored once X second passes or any button is pressed. The - display method on this screen will not complete until after the ToastOverlay render method is complete it's takeover of the screen. - """ - - def __post_init__(self): - from seedsigner.hardware.microsd import MicroSD - super().__post_init__() - self.current_screen = self.renderer.canvas.copy() - - self.toast = ToastOverlay( - icon_name=FontAwesomeIconConstants.SDCARD, - label_text="SD card removed" if self.action == MicroSD.ACTION__REMOVED else "SD card inserted", - ) - - - def _run(self): - t_end = time.time() + self.duration - - # Special case when screensaver is running - self.hw_inputs.override_ind = True - - try: - # Hold onto the Renderer lock so we're guaranteed to restore the original - # screen before any other listener can get a screen write in. - self.renderer.lock.acquire() - - # Persist until timeout... - while time.time() < t_end: - # or hide it on button press - if self.hw_inputs.has_any_input(): - break - finally: - # Restore the screen as it was before the toast - self.renderer.show_image(self.current_screen) - self.renderer.lock.release() - - - def _render(self): - """ Override Screen.render() so the whole screen isn't cleared """ - self.toast.render() - - - @dataclass class MainMenuScreen(LargeButtonScreen): # Override LargeButtonScreen defaults title_font_size: int = 26 show_back_button: bool = False show_power_button: bool = True - - - class SDCardNotificationToastThread(BaseThread): - def run(self): - from seedsigner.controller import Controller - from seedsigner.gui.renderer import Renderer - from seedsigner.hardware.buttons import HardwareButtons - from seedsigner.hardware.microsd import MicroSD - renderer = Renderer.get_instance() - controller = Controller.get_instance() - hw_inputs = HardwareButtons.get_instance() - - # Special case when screensaver is running - hw_inputs.override_ind = True - - activation_delay = 3 # seconds - toast = ToastOverlay( - icon_name=FontAwesomeIconConstants.SDCARD, - label_text="Security tip:\nRemove SD card", - font_size=GUIConstants.BODY_FONT_SIZE, - height=GUIConstants.BODY_FONT_SIZE * 2 + GUIConstants.BODY_LINE_SPACING + GUIConstants.EDGE_PADDING, - ) - - start = time.time() - has_rendered = False - self.previous_screen_state = None - try: - # Hold onto the Renderer lock so we're guaranteed to restore the original - # screen before any other listener can get a screen write in. - renderer.lock.acquire() - while self.keep_running: - if not MicroSD.is_inserted(): - # Card is removed! No need to keep running - break - if hw_inputs.has_any_input(): - # User has pressed a button, hide the toast - break - if controller.is_screensaver_running: - # Have to release the lock so the screensaver can render - renderer.lock.release() - - # Immediately attempt to reacquire the lock, but in reality this - # thread will block here until the screensaver releases it. - time.sleep(0.1) - renderer.lock.acquire() - if time.time() - start > activation_delay and not has_rendered: - self.previous_screen_state = renderer.canvas.copy() - toast.render() - has_rendered = True - time.sleep(0.1) - finally: - if has_rendered: - renderer.show_image(self.previous_screen_state) - - # We're done, release the lock - renderer.lock.release() - - - - def __post_init__(self): - from seedsigner.hardware.microsd import MicroSD - super().__post_init__() - if MicroSD.is_inserted(): - # Remind user they can/should remove the SD card - self.threads.append(self.SDCardNotificationToastThread()) - - - # DEBUGGING - # def _run(self): - # from seedsigner.hardware.microsd import MicroSD - # toast = MicroSDToastScreen(action=MicroSD.ACTION__INSERTED) - # toast.display() - # return super()._run() - # DEBUGGING diff --git a/src/seedsigner/gui/toast.py b/src/seedsigner/gui/toast.py new file mode 100644 index 00000000..741f9507 --- /dev/null +++ b/src/seedsigner/gui/toast.py @@ -0,0 +1,218 @@ +from dataclasses import dataclass +from seedsigner.gui.components import BaseComponent, GUIConstants, Icon, TextArea +from seedsigner.models.threads import BaseThread + + + +@dataclass +class ToastOverlay(BaseComponent): + icon_name: str = None + color: str = GUIConstants.NOTIFICATION_COLOR + label_text: str = None + height: int = GUIConstants.ICON_TOAST_FONT_SIZE + 2*GUIConstants.EDGE_PADDING + font_size: int = 19 + outline_thickness: int = 2 # pixels + + def __post_init__(self): + super().__post_init__() + + self.icon = Icon( + image_draw=self.image_draw, + canvas=self.canvas, + screen_x=self.outline_thickness + 2*GUIConstants.EDGE_PADDING, # Push the icon further from the left edge than strictly necessary + icon_name=self.icon_name, + icon_size=GUIConstants.ICON_TOAST_FONT_SIZE, + icon_color=self.color + ) + self.icon.screen_y = self.canvas_height - self.height + int((self.height - self.icon.height)/2) - 1 # -1 fudge factor + + self.label = TextArea( + image_draw=self.image_draw, + canvas=self.canvas, + text=self.label_text, + font_size=self.font_size, + font_color=self.color, + edge_padding=0, + is_text_centered=False, + auto_line_break=True, + width=self.canvas_width - self.icon.screen_x - self.icon.width - GUIConstants.COMPONENT_PADDING - self.outline_thickness, + screen_x=self.icon.screen_x + self.icon.width + GUIConstants.COMPONENT_PADDING, + allow_text_overflow=False + ) + self.label.screen_y = self.canvas_height - self.height + int((self.height - self.label.height)/2) + + + def render(self): + self.image_draw.rounded_rectangle( + (0, self.canvas_height - self.height, self.canvas_width, self.canvas_height), + fill=GUIConstants.BACKGROUND_COLOR, + radius=8, + outline=self.color, + width=self.outline_thickness, + ) + + self.icon.render() + self.label.render() + + self.renderer.show_image() + + + + +class BaseToastOverlayManagerThread(BaseThread): + """ + The toast notification popup consists of a gui component (`ToastOverlay`) and this + manager thread that the Controller will use to coordinate handing off resources + between competing toasts, the screensaver, and the current underlying Screen. + + Controller should set BaseThread.keep_running = False to terminate the toast when it + needs to be removed or replaced. + + Controller should set toggle_renderer_lock = True to make the toast temporarily + release the Renderer.lock so another process (e.g. screensaver) can use it. The toast + thread will immediately try to reacquire the lock, but will have to block and wait + until it's available again. Note that this thread will be unresponsive while it + waits to reacquire the lock! + + Note: any process can call lock.release() but it simplifies the logic to try to keep + each process aware of whether it is currently holding the lock or not (i.e. it's + better for the "owner" thread to release the lock itself). + """ + def __init__(self, + activation_delay: int = 0, # seconds before toast is displayed + duration: int = 3, # seconds toast is displayed + ): + from seedsigner.controller import Controller + from seedsigner.gui.renderer import Renderer + from seedsigner.hardware.buttons import HardwareButtons + super().__init__() + self.activation_delay: int = activation_delay + self.duration: int = duration + self._toggle_renderer_lock: bool = False + + self.renderer = Renderer.get_instance() + self.controller = Controller.get_instance() + self.hw_inputs = HardwareButtons.get_instance() + + # Special case when screensaver is running + self.hw_inputs.override_ind = True + + self.toast = self.instantiate_toast() + + + def instantiate_toast(self) -> ToastOverlay: + raise Exception("Must be implemented by subclass") + + + def should_keep_running(self) -> bool: + """ Placeholder for custom exit conditions """ + return True + + + def toggle_renderer_lock(self): + self._toggle_renderer_lock = True + + + def run(self): + try: + print(f"{self.__class__.__name__}: started") + start = time.time() + has_rendered = False + self.previous_screen_state = None + if self.activation_delay > 0: + time.sleep(self.activation_delay) + + # Hold onto the Renderer lock so we're guaranteed to restore the original + # screen before any other listener can get a screen write in. + print(f"{self.__class__.__name__}: Acquiring lock") + self.renderer.lock.acquire() + print(f"{self.__class__.__name__}: Lock acquired") + while self.keep_running and self.should_keep_running(): + if self.hw_inputs.has_any_input(): + # User has pressed a button, hide the toast + print(f"{self.__class__.__name__}: Exiting due to user input") + break + + print(time.time(), self._toggle_renderer_lock) + + if self._toggle_renderer_lock: + # Controller has notified us that another process needs the lock + print(f"{self.__class__.__name__}: Releasing lock") + self._toggle_renderer_lock = False + self.renderer.lock.release() + + # pause to avoid race conditions reacquiring the lock + while not self.renderer.lock.locked(): + # Wait for a different process to grab the lock + time.sleep(0.1) + + # Block while waiting to reaquire the lock + print(f"{self.__class__.__name__}: Blocking to re-acquire lock") + self.renderer.lock.acquire() + print(f"{self.__class__.__name__}: Lock re-acquired") + + if not has_rendered: + self.previous_screen_state = self.renderer.canvas.copy() + print(f"{self.__class__.__name__}: Showing toast") + self.toast.render() + has_rendered = True + + if time.time() - start > self.activation_delay + self.duration and has_rendered: + print(f"{self.__class__.__name__}: Hiding toast") + break + + # Free up cpu resources for main thread + time.sleep(0.1) + + finally: + print(f"{self.__class__.__name__}: exiting") + if has_rendered and self.renderer.lock.locked(): + # As far as we know, we currently hold the Renderer.lock + self.renderer.show_image(self.previous_screen_state) + + # We're done, release the lock + self.renderer.lock.release() + + + +class RemoveSDCardToastManagerThread(BaseToastOverlayManagerThread): + def __init__(self): + super().__init__( + activation_delay=3, # seconds + duration=1e6, # seconds ("forever") + ) + + + def instantiate_toast(self) -> ToastOverlay: + return ToastOverlay( + icon_name=FontAwesomeIconConstants.SDCARD, + label_text="Security tip:\nRemove SD card", + font_size=GUIConstants.BODY_FONT_SIZE, + height=GUIConstants.BODY_FONT_SIZE * 2 + GUIConstants.BODY_LINE_SPACING + GUIConstants.EDGE_PADDING, + ) + + + def should_keep_running(self) -> bool: + """ Custom exit condition: keep running until the SD card is removed """ + from seedsigner.hardware.microsd import MicroSD + return MicroSD.is_inserted() + + + +class SDCardStateChangeToastManagerThread(BaseToastOverlayManagerThread): + def __init__(self, action: str, *args, **kwargs): + from seedsigner.hardware.microsd import MicroSD + if action not in [MicroSD.ACTION__INSERTED, MicroSD.ACTION__REMOVED]: + raise Exception(f"Invalid MicroSD action: {action}") + self.message = "SD card removed" if action == MicroSD.ACTION__REMOVED else "SD card inserted" + + super().__init__(*args, **kwargs) + + + def instantiate_toast(self) -> ToastOverlay: + return ToastOverlay( + icon_name=FontAwesomeIconConstants.SDCARD, + label_text=self.message, + ) + + diff --git a/src/seedsigner/hardware/microsd.py b/src/seedsigner/hardware/microsd.py index 790ffde4..90011adb 100644 --- a/src/seedsigner/hardware/microsd.py +++ b/src/seedsigner/hardware/microsd.py @@ -3,12 +3,10 @@ import os from seedsigner.models.singleton import Singleton from seedsigner.models.threads import BaseThread from seedsigner.models.settings import Settings -from seedsigner.gui.screens.screen import MicroSDToastScreen class MicroSD(Singleton, BaseThread): - MOUNT_POINT = "/mnt/microsd" FIFO_PATH = "/tmp/mdev_fifo" FIFO_MODE = 0o600 @@ -16,6 +14,7 @@ class MicroSD(Singleton, BaseThread): ACTION__REMOVED = "remove" warn_to_remove = True + @classmethod def get_instance(cls): # This is the only way to access the one and only instance @@ -23,12 +22,12 @@ class MicroSD(Singleton, BaseThread): # Instantiate the one and only instance microsd = cls.__new__(cls) cls._instance = microsd - + # explicitly call BaseThread __init__ since multiple class inheritance BaseThread.__init__(microsd) return cls._instance - + @classmethod def is_inserted(cls): @@ -44,6 +43,8 @@ class MicroSD(Singleton, BaseThread): def run(self): + from seedsigner.controller import Controller + from seedsigner.gui.toast import SDCardStateChangeToastManagerThread action = "" # explicitly only microsd add/remove detection in seedsigner-os @@ -64,11 +65,10 @@ class MicroSD(Singleton, BaseThread): with open(self.FIFO_PATH) as fifo: action = fifo.read() print(f"fifo message: {action}") - + Settings.microsd_handler(action=action) if action == self.ACTION__INSERTED: self.warn_to_remove = True - - toastscreen = MicroSDToastScreen(action=action) - toastscreen.display() + + Controller.get_instance().activate_toast(SDCardStateChangeToastManagerThread(action=action)) diff --git a/src/seedsigner/views/screensaver.py b/src/seedsigner/views/screensaver.py index f863c15d..9204690e 100644 --- a/src/seedsigner/views/screensaver.py +++ b/src/seedsigner/views/screensaver.py @@ -89,7 +89,6 @@ class OpeningSplashScreen(LogoScreen): - class ScreensaverScreen(LogoScreen): def __init__(self, buttons): super().__init__() From fec8d212b76b234e2c539d6c8d1676d401babb53 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 6 Aug 2023 14:26:52 -0500 Subject: [PATCH 13/61] Import bugfixes --- src/seedsigner/gui/screens/screen.py | 17 +++++++---------- src/seedsigner/gui/toast.py | 5 ++--- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index a1d1dd34..d82567b3 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -2,19 +2,16 @@ import time from dataclasses import dataclass from PIL import Image, ImageDraw, ImageColor -from typing import Any, Callable, List, Tuple -from seedsigner.gui.components import ToastOverlay +from typing import Any, List, Tuple + +from seedsigner.gui.components import (GUIConstants, + BaseComponent, Button, Icon, IconButton, LargeIconButton, + SeedSignerIconConstants, TopNav, TextArea, load_image) from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay from seedsigner.gui.renderer import Renderer - -from seedsigner.models.threads import BaseThread, ThreadsafeCounter -from seedsigner.models.settings import SettingsConstants - -from ..components import (FontAwesomeIconConstants, GUIConstants, BaseComponent, Button, Icon, IconButton, - LargeIconButton, SeedSignerIconConstants, TopNav, TextArea, load_image, ToastOverlay, - Fonts) - from seedsigner.hardware.buttons import HardwareButtonsConstants, HardwareButtons +from seedsigner.models.settings import SettingsConstants +from seedsigner.models.threads import BaseThread, ThreadsafeCounter # Must be huge numbers to avoid conflicting with the selected_button returned by the diff --git a/src/seedsigner/gui/toast.py b/src/seedsigner/gui/toast.py index 741f9507..5d4a0403 100644 --- a/src/seedsigner/gui/toast.py +++ b/src/seedsigner/gui/toast.py @@ -1,5 +1,6 @@ +import time from dataclasses import dataclass -from seedsigner.gui.components import BaseComponent, GUIConstants, Icon, TextArea +from seedsigner.gui.components import BaseComponent, FontAwesomeIconConstants, GUIConstants, Icon, TextArea from seedsigner.models.threads import BaseThread @@ -133,8 +134,6 @@ class BaseToastOverlayManagerThread(BaseThread): print(f"{self.__class__.__name__}: Exiting due to user input") break - print(time.time(), self._toggle_renderer_lock) - if self._toggle_renderer_lock: # Controller has notified us that another process needs the lock print(f"{self.__class__.__name__}: Releasing lock") From 2e3004ed2dd965ffbb4e62017e738b93e1d26e65 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 6 Aug 2023 15:03:50 -0500 Subject: [PATCH 14/61] New toast now cancels an active Screensaver --- src/seedsigner/controller.py | 1 + src/seedsigner/gui/toast.py | 7 ++++--- src/seedsigner/views/screensaver.py | 15 +++++++++------ 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 054cac30..279adba4 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -402,6 +402,7 @@ class Controller(Singleton): """ if self.is_screensaver_running: # New toast notifications break out of the Screensaver + print("Controller: stopping screensaver") self.screensaver.stop() if self.toast_notification_thread and self.toast_notification_thread.is_alive(): diff --git a/src/seedsigner/gui/toast.py b/src/seedsigner/gui/toast.py index 5d4a0403..a82fb33b 100644 --- a/src/seedsigner/gui/toast.py +++ b/src/seedsigner/gui/toast.py @@ -119,7 +119,7 @@ class BaseToastOverlayManagerThread(BaseThread): print(f"{self.__class__.__name__}: started") start = time.time() has_rendered = False - self.previous_screen_state = None + previous_screen_state = None if self.activation_delay > 0: time.sleep(self.activation_delay) @@ -151,7 +151,7 @@ class BaseToastOverlayManagerThread(BaseThread): print(f"{self.__class__.__name__}: Lock re-acquired") if not has_rendered: - self.previous_screen_state = self.renderer.canvas.copy() + previous_screen_state = self.renderer.canvas.copy() print(f"{self.__class__.__name__}: Showing toast") self.toast.render() has_rendered = True @@ -167,7 +167,8 @@ class BaseToastOverlayManagerThread(BaseThread): print(f"{self.__class__.__name__}: exiting") if has_rendered and self.renderer.lock.locked(): # As far as we know, we currently hold the Renderer.lock - self.renderer.show_image(self.previous_screen_state) + self.renderer.show_image(previous_screen_state) + print(f"{self.__class__.__name__}: restored previous screen state") # We're done, release the lock self.renderer.lock.release() diff --git a/src/seedsigner/views/screensaver.py b/src/seedsigner/views/screensaver.py index 9204690e..960df6c8 100644 --- a/src/seedsigner/views/screensaver.py +++ b/src/seedsigner/views/screensaver.py @@ -140,9 +140,9 @@ class ScreensaverScreen(LogoScreen): # never gives up the lock until it returns. with self.renderer.lock: try: - while True: + while self._is_running: if self.buttons.has_any_input() or self.buttons.override_ind: - return self.stop() + break # Must crop the image to the exact display size crop = self.image.crop(( @@ -175,20 +175,23 @@ class ScreensaverScreen(LogoScreen): self.increment_y = self.rand_increment() if self.increment_y > 0.0: self.increment_y *= -1.0 + except KeyboardInterrupt as e: # Exit triggered; close gracefully print("Shutting down Screensaver") - self.stop() # Have to let the interrupt bubble up to exit the main app raise e + finally: + self._is_running = False + + # Restore the original screen + self.renderer.show_image(self.last_screen) + def stop(self): - # Restore the original screen - self.renderer.show_image(self.last_screen) - self._is_running = False From 95da9acf6712a8e9b9cc76b9cbb13f1d7cb8c9bb Mon Sep 17 00:00:00 2001 From: kdmukai Date: Wed, 9 Aug 2023 09:36:50 -0500 Subject: [PATCH 15/61] Remove PR #410 RemoveMicroSDWarningView; post-rebase fixes --- src/seedsigner/gui/toast.py | 6 +++--- tests/base.py | 1 + tests/test_flows_seed.py | 41 ++----------------------------------- tests/test_flows_view.py | 2 +- 4 files changed, 7 insertions(+), 43 deletions(-) diff --git a/src/seedsigner/gui/toast.py b/src/seedsigner/gui/toast.py index a82fb33b..cb36663c 100644 --- a/src/seedsigner/gui/toast.py +++ b/src/seedsigner/gui/toast.py @@ -1,6 +1,6 @@ import time from dataclasses import dataclass -from seedsigner.gui.components import BaseComponent, FontAwesomeIconConstants, GUIConstants, Icon, TextArea +from seedsigner.gui.components import BaseComponent, FontAwesomeIconConstants, GUIConstants, Icon, SeedSignerIconConstants, TextArea from seedsigner.models.threads import BaseThread @@ -185,7 +185,7 @@ class RemoveSDCardToastManagerThread(BaseToastOverlayManagerThread): def instantiate_toast(self) -> ToastOverlay: return ToastOverlay( - icon_name=FontAwesomeIconConstants.SDCARD, + icon_name=SeedSignerIconConstants.SDCARD, label_text="Security tip:\nRemove SD card", font_size=GUIConstants.BODY_FONT_SIZE, height=GUIConstants.BODY_FONT_SIZE * 2 + GUIConstants.BODY_LINE_SPACING + GUIConstants.EDGE_PADDING, @@ -211,7 +211,7 @@ class SDCardStateChangeToastManagerThread(BaseToastOverlayManagerThread): def instantiate_toast(self) -> ToastOverlay: return ToastOverlay( - icon_name=FontAwesomeIconConstants.SDCARD, + icon_name=SeedSignerIconConstants.SDCARD, label_text=self.message, ) diff --git a/tests/base.py b/tests/base.py index 0628fb1a..fadf6ff7 100644 --- a/tests/base.py +++ b/tests/base.py @@ -7,6 +7,7 @@ from typing import Callable # These must precede any SeedSigner imports. sys.modules['seedsigner.gui.renderer'] = MagicMock() sys.modules['seedsigner.gui.screens.screensaver'] = MagicMock() +sys.modules['seedsigner.gui.toast'] = MagicMock() sys.modules['seedsigner.views.screensaver'] = MagicMock() sys.modules['seedsigner.hardware.buttons'] = MagicMock() sys.modules['seedsigner.hardware.camera'] = MagicMock() diff --git a/tests/test_flows_seed.py b/tests/test_flows_seed.py index d7896218..f4b83065 100644 --- a/tests/test_flows_seed.py +++ b/tests/test_flows_seed.py @@ -14,47 +14,10 @@ from seedsigner.views import seed_views, scan_views, settings_views, tools_views def load_seed_into_decoder(view: scan_views.ScanView): view.decoder.add_data("0000" * 11 + "0003") + + class TestSeedFlows(FlowTest): - def test_flow_thru_microsd_warning(self): - """ - Selecting "Scan", "Seeds" or "Tools from the MainMenuView in ss-os with microsd inserted - will flow to RemoveMicroSDWarningView -- conditionally, then on to the intended next view - """ - Settings.HOSTNAME = Settings.SEEDSIGNER_OS - # will be warned to remove microsd card - self.run_sequence([ - FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS), - FlowStep(RemoveMicroSDWarningView, screen_return_value=0), - FlowStep(tools_views.ToolsMenuView, screen_return_value=RET_CODE__BACK_BUTTON), # backed-out - FlowStep(MainMenuView), - ]) - # but won't be warned again, because microsd.warn_to_remove was set False just above - self.run_sequence([ - FlowStep(MainMenuView, button_data_selection=MainMenuView.SEEDS), - FlowStep(seed_views.SeedsMenuView, is_redirect=True), - FlowStep(seed_views.LoadSeedView, screen_return_value=RET_CODE__BACK_BUTTON), # backed-out - FlowStep(MainMenuView), - ]) - # unless the microsd was re-inserted, which resets microsd.warn_to_remove = True. - self.controller.microsd.warn_to_remove = True - self.run_sequence([ - FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN), - FlowStep(RemoveMicroSDWarningView, screen_return_value=0), - FlowStep(scan_views.ScanView, before_run=load_seed_into_decoder), # this time, loaded a seed - FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE), - FlowStep(seed_views.SeedOptionsView), - ]) - # won't get warned again if a seed is loaded (like was done above); it's already too late - self.controller.microsd.warn_to_remove = True - self.run_sequence([ - FlowStep(MainMenuView, button_data_selection=MainMenuView.SEEDS), - FlowStep(seed_views.SeedsMenuView), - ]) - # no warning shown so this flag not cleared, cleanup else following tests will warn - self.controller.microsd.warn_to_remove = False - - def test_scan_seedqr_flow(self): """ Selecting "Scan" from the MainMenuView and scanning a SeedQR should enter the diff --git a/tests/test_flows_view.py b/tests/test_flows_view.py index 24c880e6..6d032460 100644 --- a/tests/test_flows_view.py +++ b/tests/test_flows_view.py @@ -6,7 +6,7 @@ from base import FlowTest, FlowStep from seedsigner.gui.screens.screen import RET_CODE__POWER_BUTTON from seedsigner.models.settings import Settings from seedsigner.views.tools_views import ToolsCalcFinalWordNumWordsView, ToolsMenuView -from seedsigner.views.view import MainMenuView, NotYetImplementedView, PowerOptionsView, PowerOffView, RestartView, UnhandledExceptionView, View, RemoveMicroSDWarningView +from seedsigner.views.view import MainMenuView, NotYetImplementedView, PowerOptionsView, PowerOffView, RestartView, UnhandledExceptionView, View From d1e7450870dba8eff4a638cfce643c4777d3a263 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Wed, 9 Aug 2023 11:25:55 -0500 Subject: [PATCH 16/61] add support for screenshots with toast overlays --- src/seedsigner/gui/components.py | 1 - src/seedsigner/gui/toast.py | 25 +++++++++++++------------ tests/screenshot_generator/generator.py | 19 ++++++++++++++++--- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/seedsigner/gui/components.py b/src/seedsigner/gui/components.py index fa8190f5..00e242e3 100644 --- a/src/seedsigner/gui/components.py +++ b/src/seedsigner/gui/components.py @@ -139,7 +139,6 @@ class SeedSignerIconConstants: BRIGHTNESS = "\ue91d" MICROSD = "\ue91e" QRCODE = "\ue91f" - SDCARD = "\ue920" MIN_VALUE = SCAN MAX_VALUE = QRCODE diff --git a/src/seedsigner/gui/toast.py b/src/seedsigner/gui/toast.py index cb36663c..2b80f2db 100644 --- a/src/seedsigner/gui/toast.py +++ b/src/seedsigner/gui/toast.py @@ -26,7 +26,7 @@ class ToastOverlay(BaseComponent): icon_color=self.color ) self.icon.screen_y = self.canvas_height - self.height + int((self.height - self.icon.height)/2) - 1 # -1 fudge factor - + self.label = TextArea( image_draw=self.image_draw, canvas=self.canvas, @@ -99,7 +99,7 @@ class BaseToastOverlayManagerThread(BaseThread): self.hw_inputs.override_ind = True self.toast = self.instantiate_toast() - + def instantiate_toast(self) -> ToastOverlay: raise Exception("Must be implemented by subclass") @@ -155,7 +155,7 @@ class BaseToastOverlayManagerThread(BaseThread): print(f"{self.__class__.__name__}: Showing toast") self.toast.render() has_rendered = True - + if time.time() - start > self.activation_delay + self.duration and has_rendered: print(f"{self.__class__.__name__}: Hiding toast") break @@ -169,28 +169,30 @@ class BaseToastOverlayManagerThread(BaseThread): # As far as we know, we currently hold the Renderer.lock self.renderer.show_image(previous_screen_state) print(f"{self.__class__.__name__}: restored previous screen state") - + # We're done, release the lock self.renderer.lock.release() class RemoveSDCardToastManagerThread(BaseToastOverlayManagerThread): - def __init__(self): + def __init__(self, activation_delay=3): + # Note: activation_delay is configurable so the screenshot generator can get the + # toast to immediately render. super().__init__( - activation_delay=3, # seconds - duration=1e6, # seconds ("forever") + activation_delay=activation_delay, # seconds + duration=1e6, # seconds ("forever") ) def instantiate_toast(self) -> ToastOverlay: return ToastOverlay( - icon_name=SeedSignerIconConstants.SDCARD, + icon_name=SeedSignerIconConstants.MICROSD, label_text="Security tip:\nRemove SD card", font_size=GUIConstants.BODY_FONT_SIZE, height=GUIConstants.BODY_FONT_SIZE * 2 + GUIConstants.BODY_LINE_SPACING + GUIConstants.EDGE_PADDING, ) - + def should_keep_running(self) -> bool: """ Custom exit condition: keep running until the SD card is removed """ @@ -210,9 +212,8 @@ class SDCardStateChangeToastManagerThread(BaseToastOverlayManagerThread): def instantiate_toast(self) -> ToastOverlay: + print("instantiating toast!") return ToastOverlay( - icon_name=SeedSignerIconConstants.SDCARD, + icon_name=SeedSignerIconConstants.MICROSD, label_text=self.message, ) - - diff --git a/tests/screenshot_generator/generator.py b/tests/screenshot_generator/generator.py index b498fa2e..f280a91f 100644 --- a/tests/screenshot_generator/generator.py +++ b/tests/screenshot_generator/generator.py @@ -1,20 +1,27 @@ import embit import os import sys -from mock import Mock, MagicMock +import time +from mock import Mock, patch, MagicMock + # Prevent importing modules w/Raspi hardware dependencies. # These must precede any SeedSigner imports. sys.modules['seedsigner.hardware.ST7789'] = MagicMock() sys.modules['seedsigner.gui.screens.screensaver'] = MagicMock() sys.modules['seedsigner.views.screensaver'] = MagicMock() -sys.modules['seedsigner.hardware.buttons'] = MagicMock() +sys.modules['RPi'] = MagicMock() +sys.modules['RPi.GPIO'] = MagicMock() sys.modules['seedsigner.hardware.camera'] = MagicMock() sys.modules['seedsigner.hardware.microsd'] = MagicMock() from seedsigner.controller import Controller from seedsigner.gui.renderer import Renderer +from seedsigner.gui.toast import BaseToastOverlayManagerThread, RemoveSDCardToastManagerThread, SDCardStateChangeToastManagerThread +from seedsigner.hardware.buttons import HardwareButtons +from seedsigner.hardware.camera import Camera +from seedsigner.hardware.microsd import MicroSD from seedsigner.models.decode_qr import DecodeQR from seedsigner.models.qr_type import QRType from seedsigner.models.seed import Seed @@ -99,6 +106,9 @@ def test_generate_screenshots(target_locale): screenshot_sections = { "Main Menu Views": [ MainMenuView, + (MainMenuView, {}, 'MainMenuView_SDCardStateChangeToast_removed', SDCardStateChangeToastManagerThread(action=MicroSD.ACTION__REMOVED)), + (MainMenuView, {}, 'MainMenuView_SDCardStateChangeToast_inserted', SDCardStateChangeToastManagerThread(action=MicroSD.ACTION__INSERTED)), + (MainMenuView, {}, 'MainMenuView_RemoveSDCardToast', RemoveSDCardToastManagerThread(activation_delay=0)), PowerOptionsView, RestartView, PowerOffView, @@ -231,12 +241,15 @@ def test_generate_screenshots(target_locale): view_name = view_cls.__name__ elif len(screenshot) == 3: view_cls, view_args, view_name = screenshot + elif len(screenshot) == 4: + view_cls, view_args, view_name, toast_thread = screenshot else: view_cls = screenshot view_args = {} view_name = view_cls.__name__ + toast_thread = None - screencap_view(view_cls, view_name, view_args) + screencap_view(view_cls, view_name, view_args, toast_thread=toast_thread) readme += """ """ readme += f"""""" readme += """
{view_name}

\n""" From 69871db10bcb4171e51e3955a5249bada2dbc495 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Wed, 9 Aug 2023 11:36:15 -0500 Subject: [PATCH 17/61] Final positioning fixes --- src/seedsigner/gui/toast.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/seedsigner/gui/toast.py b/src/seedsigner/gui/toast.py index 2b80f2db..5a890866 100644 --- a/src/seedsigner/gui/toast.py +++ b/src/seedsigner/gui/toast.py @@ -25,7 +25,7 @@ class ToastOverlay(BaseComponent): icon_size=GUIConstants.ICON_TOAST_FONT_SIZE, icon_color=self.color ) - self.icon.screen_y = self.canvas_height - self.height + int((self.height - self.icon.height)/2) - 1 # -1 fudge factor + self.icon.screen_y = self.canvas_height - self.height + self.outline_thickness + int((self.height - self.icon.height)/2) self.label = TextArea( image_draw=self.image_draw, @@ -40,7 +40,7 @@ class ToastOverlay(BaseComponent): screen_x=self.icon.screen_x + self.icon.width + GUIConstants.COMPONENT_PADDING, allow_text_overflow=False ) - self.label.screen_y = self.canvas_height - self.height + int((self.height - self.label.height)/2) + self.label.screen_y = self.canvas_height - self.height + self.outline_thickness + int((self.height - self.label.height)/2) def render(self): From be85899805d8ce70188b3aa6af467431c3c2ba5e Mon Sep 17 00:00:00 2001 From: kdmukai Date: Wed, 9 Aug 2023 11:43:57 -0500 Subject: [PATCH 18/61] restore default screensaver activation timing --- src/seedsigner/controller.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 279adba4..30a50076 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -182,8 +182,7 @@ class Controller(Singleton): controller.back_stack = BackStack() # Other behavior constants - # controller.screensaver_activation_ms = 2 * 60 * 1000 - controller.screensaver_activation_ms = 15 * 1000 # DEBUGGING + controller.screensaver_activation_ms = 2 * 60 * 1000 # two minutes background_import_thread = BackgroundImportThread() background_import_thread.start() From 2f832738463627650feade4eb9e4a5ea4016f1c1 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Wed, 9 Aug 2023 14:04:59 -0500 Subject: [PATCH 19/61] Make notification text more neutral --- src/seedsigner/gui/toast.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/seedsigner/gui/toast.py b/src/seedsigner/gui/toast.py index 5a890866..5774ecf6 100644 --- a/src/seedsigner/gui/toast.py +++ b/src/seedsigner/gui/toast.py @@ -25,7 +25,7 @@ class ToastOverlay(BaseComponent): icon_size=GUIConstants.ICON_TOAST_FONT_SIZE, icon_color=self.color ) - self.icon.screen_y = self.canvas_height - self.height + self.outline_thickness + int((self.height - self.icon.height)/2) + self.icon.screen_y = self.canvas_height - self.height + int((self.height - self.icon.height)/2) self.label = TextArea( image_draw=self.image_draw, @@ -40,7 +40,7 @@ class ToastOverlay(BaseComponent): screen_x=self.icon.screen_x + self.icon.width + GUIConstants.COMPONENT_PADDING, allow_text_overflow=False ) - self.label.screen_y = self.canvas_height - self.height + self.outline_thickness + int((self.height - self.label.height)/2) + self.label.screen_y = self.canvas_height - self.height + int((self.height - self.label.height)/2) def render(self): @@ -188,7 +188,7 @@ class RemoveSDCardToastManagerThread(BaseToastOverlayManagerThread): def instantiate_toast(self) -> ToastOverlay: return ToastOverlay( icon_name=SeedSignerIconConstants.MICROSD, - label_text="Security tip:\nRemove SD card", + label_text="You can remove\nthe SD card now", font_size=GUIConstants.BODY_FONT_SIZE, height=GUIConstants.BODY_FONT_SIZE * 2 + GUIConstants.BODY_LINE_SPACING + GUIConstants.EDGE_PADDING, ) From 45843d65d29e0465de67de4bc291ee847e32c315 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 11 Aug 2023 08:36:59 -0500 Subject: [PATCH 20/61] Light refactor; add detection loop sleep --- src/seedsigner/gui/toast.py | 4 ++- src/seedsigner/hardware/microsd.py | 19 +++++------- src/seedsigner/models/settings.py | 32 +++++++++++++------- src/seedsigner/models/settings_definition.py | 4 ++- 4 files changed, 35 insertions(+), 24 deletions(-) diff --git a/src/seedsigner/gui/toast.py b/src/seedsigner/gui/toast.py index 5774ecf6..6b6a27bc 100644 --- a/src/seedsigner/gui/toast.py +++ b/src/seedsigner/gui/toast.py @@ -1,6 +1,6 @@ import time from dataclasses import dataclass -from seedsigner.gui.components import BaseComponent, FontAwesomeIconConstants, GUIConstants, Icon, SeedSignerIconConstants, TextArea +from seedsigner.gui.components import BaseComponent, GUIConstants, Icon, SeedSignerIconConstants, TextArea from seedsigner.models.threads import BaseThread @@ -203,6 +203,8 @@ class RemoveSDCardToastManagerThread(BaseToastOverlayManagerThread): class SDCardStateChangeToastManagerThread(BaseToastOverlayManagerThread): def __init__(self, action: str, *args, **kwargs): + # Note: we could just directly detect the MicroSD status here, but passing it in + # via `action` lets us simulate the state we want in the screenshot generator. from seedsigner.hardware.microsd import MicroSD if action not in [MicroSD.ACTION__INSERTED, MicroSD.ACTION__REMOVED]: raise Exception(f"Invalid MicroSD action: {action}") diff --git a/src/seedsigner/hardware/microsd.py b/src/seedsigner/hardware/microsd.py index 90011adb..9a3ee315 100644 --- a/src/seedsigner/hardware/microsd.py +++ b/src/seedsigner/hardware/microsd.py @@ -1,4 +1,5 @@ import os +import time from seedsigner.models.singleton import Singleton from seedsigner.models.threads import BaseThread @@ -12,7 +13,6 @@ class MicroSD(Singleton, BaseThread): FIFO_MODE = 0o600 ACTION__INSERTED = "add" ACTION__REMOVED = "remove" - warn_to_remove = True @classmethod @@ -31,10 +31,10 @@ class MicroSD(Singleton, BaseThread): @classmethod def is_inserted(cls): - # could only be False in seedsigner-os, else True if Settings.HOSTNAME == Settings.SEEDSIGNER_OS: return os.path.exists(MicroSD.MOUNT_POINT) else: + # Always True for Raspi OS return True @@ -51,10 +51,9 @@ class MicroSD(Singleton, BaseThread): if Settings.HOSTNAME == Settings.SEEDSIGNER_OS: # at start-up, get current status and inform Settings - if MicroSD.is_inserted(): - Settings.microsd_handler(self.ACTION__INSERTED) - else: - Settings.microsd_handler(self.ACTION__REMOVED) + Settings.handle_microsd_state_change( + action=MicroSD.ACTION__INSERTED if MicroSD.is_inserted else MicroSD.ACTION__REMOVED + ) if os.path.exists(self.FIFO_PATH): os.remove(self.FIFO_PATH) @@ -66,9 +65,7 @@ class MicroSD(Singleton, BaseThread): action = fifo.read() print(f"fifo message: {action}") - Settings.microsd_handler(action=action) - - if action == self.ACTION__INSERTED: - self.warn_to_remove = True - + Settings.handle_microsd_state_change(action=action) Controller.get_instance().activate_toast(SDCardStateChangeToastManagerThread(action=action)) + + time.sleep(0.1) diff --git a/src/seedsigner/models/settings.py b/src/seedsigner/models/settings.py index eaf9c247..7b5f4602 100644 --- a/src/seedsigner/models/settings.py +++ b/src/seedsigner/models/settings.py @@ -213,7 +213,6 @@ class Settings(Singleton): return display_names - """ Intentionally keeping the properties very limited to avoid an expectation of boilerplate property code for every SettingsEntry. @@ -227,28 +226,39 @@ class Settings(Singleton): return self._data[SettingsConstants.SETTING__DEBUG] == SettingsConstants.OPTION__ENABLED - def microsd_handler(action): + def handle_microsd_state_change(action: str): + """ + Enables/Disables the Persistent Settings option based on the MicroSD card state. + """ from seedsigner.hardware.microsd import MicroSD - + if Settings.HOSTNAME == Settings.SEEDSIGNER_OS: - if action == MicroSD.ACTION__INSERTED: - # restore persistent settings back to defaults + # SD card was just inserted. + # Restore persistent settings back to defaults entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__PERSISTENT_SETTINGS) entry.selection_options = SettingsConstants.OPTIONS__ENABLED_DISABLED - entry.help_text = "Store Settings on SD card." - + entry.help_text = SettingsConstants.PERSISTENT_SETTINGS__SD_INSERTED__HELP_TEXT + + # TODO: Perhaps prompt the user if the current settings (not including persistent + # settings) should overwrite the settings on disk, if they differ: + # - Overwrite settings on the SD? + # - Load settings from SD? # if Settings file exists (meaning persistent settings was previously enabled), write out current settings to disk if os.path.exists(Settings.SETTINGS_FILENAME): # enable persistent settings first, then save Settings.get_instance()._data[SettingsConstants.SETTING__PERSISTENT_SETTINGS] = SettingsConstants.OPTION__ENABLED Settings.get_instance().save() - + elif action == MicroSD.ACTION__REMOVED: - # set persistent settings to disabled value directly + # SD card was just removed. + # Set persistent settings to disabled value directly Settings.get_instance()._data[SettingsConstants.SETTING__PERSISTENT_SETTINGS] = SettingsConstants.OPTION__DISABLED - + # set persistent settings to only have disabled as an option, adding additional help text that microSD is removed entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__PERSISTENT_SETTINGS) entry.selection_options = SettingsConstants.OPTIONS__ONLY_DISABLED - entry.help_text = "MicroSD card is removed" + entry.help_text = SettingsConstants.PERSISTENT_SETTINGS__SD_REMOVED__HELP_TEXT + + else: + raise Exception(f"Invalid MicroSD action: {action}") diff --git a/src/seedsigner/models/settings_definition.py b/src/seedsigner/models/settings_definition.py index 5c9e4b99..76534cec 100644 --- a/src/seedsigner/models/settings_definition.py +++ b/src/seedsigner/models/settings_definition.py @@ -101,6 +101,8 @@ class SettingsConstants: if network == SettingsConstants.REGTEST: return "regtest" + PERSISTENT_SETTINGS__SD_INSERTED__HELP_TEXT = "Store Settings on SD card" + PERSISTENT_SETTINGS__SD_REMOVED__HELP_TEXT = "Insert SD card to enable" SINGLE_SIG = "ss" MULTISIG = "ms" @@ -354,7 +356,7 @@ class SettingsDefinition: attr_name=SettingsConstants.SETTING__PERSISTENT_SETTINGS, abbreviated_name="persistent", display_name="Persistent settings", - help_text="Store Settings on SD card.", + help_text=SettingsConstants.PERSISTENT_SETTINGS__SD_INSERTED__HELP_TEXT, default_value=SettingsConstants.OPTION__DISABLED), SettingsEntry(category=SettingsConstants.CATEGORY__WALLET, From 7aa3fd926740663ec25f6c89c1e56ab9dceebaec Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 14 Aug 2023 00:23:19 -0500 Subject: [PATCH 21/61] fixes after rebase --- tests/screenshot_generator/generator.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/screenshot_generator/generator.py b/tests/screenshot_generator/generator.py index f280a91f..3e9494a4 100644 --- a/tests/screenshot_generator/generator.py +++ b/tests/screenshot_generator/generator.py @@ -213,11 +213,18 @@ def test_generate_screenshots(target_locale): readme = f"""# SeedSigner Screenshots\n""" - def screencap_view(view_cls: View, view_name: str, view_args: dict={}): + def screencap_view(view_cls: View, view_name: str, view_args: dict={}, toast_thread: BaseToastOverlayManagerThread = None): screenshot_renderer.set_screenshot_filename(f"{view_name}.png") try: print(f"Running {view_name}") - view_cls(**view_args).run() + try: + view_cls(**view_args).run() + except ScreenshotComplete: + if toast_thread is not None: + controller.activate_toast(toast_thread) + while controller.toast_notification_thread.is_alive(): + time.sleep(0.1) + raise ScreenshotComplete() except ScreenshotComplete: # Slightly hacky way to exit ScreenshotRenderer as expected pass @@ -226,6 +233,10 @@ def test_generate_screenshots(target_locale): # Something else went wrong print(repr(e)) raise e + finally: + if toast_thread: + toast_thread.stop() + for section_name, screenshot_list in screenshot_sections.items(): subdir = section_name.lower().replace(" ", "_") From fb4a8d89fc7fbcc869b88cd67ed4145471a76dd2 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 14 Aug 2023 21:42:48 -0500 Subject: [PATCH 22/61] Toast pre-empted by user input; TextArea rendering fix --- src/seedsigner/gui/components.py | 7 ++++++- src/seedsigner/gui/toast.py | 29 +++++++++++++++++++++-------- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/seedsigner/gui/components.py b/src/seedsigner/gui/components.py index 00e242e3..2ee75395 100644 --- a/src/seedsigner/gui/components.py +++ b/src/seedsigner/gui/components.py @@ -370,6 +370,9 @@ class TextArea(BaseComponent): def render(self): # Render to a temp img scaled up by self.supersampling_factor, then resize down # with bicubic resampling. + # Add a `resample_padding` above and below when supersampling to avoid edge + # effects (resized text that's right up against the top/bottom gets slightly + # dimmer at the edge otherwise). # TODO: Store resulting super-sampled image as a member var in __post_init__ and # just re-paste it here. if self.font_size < 20 and (not self.supersampling_factor or self.supersampling_factor == 1): @@ -415,7 +418,9 @@ class TextArea(BaseComponent): if self.supersampling_factor > 1.0: resized = img.resize((self.width, self.height + 2*resample_padding), Image.LANCZOS) sharpened = resized.filter(ImageFilter.SHARPEN) - img = sharpened.crop((0, resample_padding, self.width, self.height + 2*resample_padding)) + + # Crop args are actually (left, top, WIDTH, HEIGHT) + img = sharpened.crop((0, resample_padding, self.width, self.height + resample_padding)) self.canvas.paste(img, (self.screen_x, self.screen_y)) diff --git a/src/seedsigner/gui/toast.py b/src/seedsigner/gui/toast.py index 6b6a27bc..7f4b3a50 100644 --- a/src/seedsigner/gui/toast.py +++ b/src/seedsigner/gui/toast.py @@ -40,10 +40,17 @@ class ToastOverlay(BaseComponent): screen_x=self.icon.screen_x + self.icon.width + GUIConstants.COMPONENT_PADDING, allow_text_overflow=False ) - self.label.screen_y = self.canvas_height - self.height + int((self.height - self.label.height)/2) + # Single-line toast messages need their vertical centering nudged down to account + # for TextArea including below the baseline in its height calculation. + below_baseline = self.label.text_height_below_baseline if len(self.label.text_lines) == 1 else 0 + + # Vertically center the message within the toast (for single- or multi-line + # messages). + self.label.screen_y = self.canvas_height - self.height + self.outline_thickness + int((self.height - 2*self.outline_thickness - (self.label.height - below_baseline))/2) def render(self): + # Render the toast's solid background self.image_draw.rounded_rectangle( (0, self.canvas_height - self.height, self.canvas_width, self.canvas_height), fill=GUIConstants.BACKGROUND_COLOR, @@ -52,6 +59,7 @@ class ToastOverlay(BaseComponent): width=self.outline_thickness, ) + # Draw the toast visual elements self.icon.render() self.label.render() @@ -115,19 +123,24 @@ class BaseToastOverlayManagerThread(BaseThread): def run(self): - try: - print(f"{self.__class__.__name__}: started") - start = time.time() - has_rendered = False - previous_screen_state = None - if self.activation_delay > 0: - time.sleep(self.activation_delay) + print(f"{self.__class__.__name__}: started") + start = time.time() + while time.time() - start < self.activation_delay: + if self.hw_inputs.has_any_input(): + # User has pressed a button, cancel the toast + print(f"{self.__class__.__name__}: Canceling toast due to user input") + return + time.sleep(0.1) + try: # Hold onto the Renderer lock so we're guaranteed to restore the original # screen before any other listener can get a screen write in. print(f"{self.__class__.__name__}: Acquiring lock") self.renderer.lock.acquire() print(f"{self.__class__.__name__}: Lock acquired") + + has_rendered = False + previous_screen_state = None while self.keep_running and self.should_keep_running(): if self.hw_inputs.has_any_input(): # User has pressed a button, hide the toast From 83dde335ca5df3b482f072e7d1cb659a7afcad00 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Tue, 15 Aug 2023 21:27:05 -0500 Subject: [PATCH 23/61] Detect network mismatch for TESTNET/REGTEST w/MAINNET message --- src/seedsigner/views/seed_views.py | 28 +++++++++++--------- tests/test_flows_seed.py | 41 +++++++++++++++++++----------- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 6de06db8..2a7bb5b8 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -1991,19 +1991,23 @@ class SeedSignMessageConfirmAddressView(View): if not addr_format["clean_match"]: raise Exception("Signing messages for custom derivation paths not supported") - if addr_format["network"] != SettingsConstants.MAINNET: - # We're in either Testnet or Regtest or...? - if self.settings.get_value(SettingsConstants.SETTING__NETWORK) in [SettingsConstants.TESTNET, SettingsConstants.REGTEST]: - addr_format["network"] = self.settings.get_value(SettingsConstants.SETTING__NETWORK) - else: - from seedsigner.views.view import NetworkMismatchErrorView - self.set_redirect(Destination(NetworkMismatchErrorView, view_args=dict(text=f"Current network setting ({self.settings.get_value_display_name(SettingsConstants.SETTING__NETWORK)}) doesn't match {self.derivation_path}"))) + # addr_format["network"] can be MAINNET or [TESTNET, REGTEST] + message_network = addr_format["network"] + if type(message_network) == str: + message_network = [message_network] - # cleanup. Note: We could leave this in place so the user can resume the - # flow, but for now we avoid complications and keep things simple. - self.controller.resume_main_flow = None - self.controller.sign_message_data = None - return + if self.settings.get_value(SettingsConstants.SETTING__NETWORK) in message_network: + # Does nothing for MAINNET, but uses current setting to decide between TESTNET and REGTEST + addr_format["network"] = self.settings.get_value(SettingsConstants.SETTING__NETWORK) + else: + from seedsigner.views.view import NetworkMismatchErrorView + self.set_redirect(Destination(NetworkMismatchErrorView, view_args=dict(text=f"Current network setting ({self.settings.get_value_display_name(SettingsConstants.SETTING__NETWORK)}) doesn't match {self.derivation_path}"))) + + # cleanup. Note: We could leave this in place so the user can resume the + # flow, but for now we avoid complications and keep things simple. + self.controller.resume_main_flow = None + self.controller.sign_message_data = None + return xpub = seed.get_xpub(wallet_path=self.derivation_path, network=addr_format["network"]) embit_network = embit_utils.get_embit_network_name(addr_format["network"]) diff --git a/tests/test_flows_seed.py b/tests/test_flows_seed.py index f4b83065..e2387448 100644 --- a/tests/test_flows_seed.py +++ b/tests/test_flows_seed.py @@ -1,3 +1,4 @@ +from typing import Callable import pytest # Must import test base before the Controller @@ -420,19 +421,29 @@ class TestMessageSigningFlows(FlowTest): # Ensure message signing is enabled self.settings.set_value(SettingsConstants.SETTING__MESSAGE_SIGNING, SettingsConstants.OPTION__ENABLED) - # Ensure we're configured for mainnet - self.settings.set_value(SettingsConstants.SETTING__NETWORK, SettingsConstants.MAINNET) + def expect_network_mismatch_error(load_message: Callable[[str], None]): + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN), + FlowStep(scan_views.ScanView, before_run=load_message), # simulate read message QR; ret val is ignored + FlowStep(seed_views.SeedSignMessageStartView, is_redirect=True), + FlowStep(seed_views.SeedSelectSeedView, button_data_selection=seed_views.SeedSelectSeedView.SCAN_SEED), + FlowStep(scan_views.ScanView, before_run=self.load_seed_into_decoder), # simulate read SeedQR; ret val is ignored + FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE), + FlowStep(seed_views.SeedOptionsView, is_redirect=True), + FlowStep(seed_views.SeedSignMessageConfirmMessageView, before_run=self.inject_mesage_as_paged_message, screen_return_value=0), + FlowStep(seed_views.SeedSignMessageConfirmAddressView, is_redirect=True), + FlowStep(NetworkMismatchErrorView), + FlowStep(settings_views.SettingsEntryUpdateSelectionView), + ]) - self.run_sequence([ - FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN), - FlowStep(scan_views.ScanView, before_run=self.load_testnet_message_into_decoder), # simulate read message QR; ret val is ignored - FlowStep(seed_views.SeedSignMessageStartView, is_redirect=True), - FlowStep(seed_views.SeedSelectSeedView, button_data_selection=seed_views.SeedSelectSeedView.SCAN_SEED), - FlowStep(scan_views.ScanView, before_run=self.load_seed_into_decoder), # simulate read SeedQR; ret val is ignored - FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE), - FlowStep(seed_views.SeedOptionsView, is_redirect=True), - FlowStep(seed_views.SeedSignMessageConfirmMessageView, before_run=self.inject_mesage_as_paged_message, screen_return_value=0), - FlowStep(seed_views.SeedSignMessageConfirmAddressView, is_redirect=True), - FlowStep(NetworkMismatchErrorView), - FlowStep(settings_views.SettingsEntryUpdateSelectionView), - ]) + # MAINNET settings vs TESTNET derivation path with the message + self.settings.set_value(SettingsConstants.SETTING__NETWORK, SettingsConstants.MAINNET) + expect_network_mismatch_error(self.load_testnet_message_into_decoder) + + # TESTNET settings vs MAINNET derivation path with the message + self.settings.set_value(SettingsConstants.SETTING__NETWORK, SettingsConstants.TESTNET) + expect_network_mismatch_error(self.load_short_message_into_decoder) + + # REGTEST settings vs MAINNET derivation path with the message + self.settings.set_value(SettingsConstants.SETTING__NETWORK, SettingsConstants.REGTEST) + expect_network_mismatch_error(self.load_short_message_into_decoder) From d685e5e301a747c64c7d74c1af03b25bd263a33f Mon Sep 17 00:00:00 2001 From: kdmukai Date: Tue, 15 Aug 2023 21:30:57 -0500 Subject: [PATCH 24/61] simplify network test --- src/seedsigner/views/seed_views.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 2a7bb5b8..bb77abdb 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -1992,11 +1992,7 @@ class SeedSignMessageConfirmAddressView(View): raise Exception("Signing messages for custom derivation paths not supported") # addr_format["network"] can be MAINNET or [TESTNET, REGTEST] - message_network = addr_format["network"] - if type(message_network) == str: - message_network = [message_network] - - if self.settings.get_value(SettingsConstants.SETTING__NETWORK) in message_network: + if self.settings.get_value(SettingsConstants.SETTING__NETWORK) in addr_format["network"]: # Does nothing for MAINNET, but uses current setting to decide between TESTNET and REGTEST addr_format["network"] = self.settings.get_value(SettingsConstants.SETTING__NETWORK) else: From 3e58e6240b4c78ae4ef2b455936153bccc464cc4 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Tue, 15 Aug 2023 21:35:09 -0500 Subject: [PATCH 25/61] minor cleanup --- src/seedsigner/views/seed_views.py | 2 +- tests/test_flows_seed.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index bb77abdb..2b86f417 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -1991,7 +1991,7 @@ class SeedSignMessageConfirmAddressView(View): if not addr_format["clean_match"]: raise Exception("Signing messages for custom derivation paths not supported") - # addr_format["network"] can be MAINNET or [TESTNET, REGTEST] + # Note: addr_format["network"] can be MAINNET or [TESTNET, REGTEST] if self.settings.get_value(SettingsConstants.SETTING__NETWORK) in addr_format["network"]: # Does nothing for MAINNET, but uses current setting to decide between TESTNET and REGTEST addr_format["network"] = self.settings.get_value(SettingsConstants.SETTING__NETWORK) diff --git a/tests/test_flows_seed.py b/tests/test_flows_seed.py index e2387448..bb081bb3 100644 --- a/tests/test_flows_seed.py +++ b/tests/test_flows_seed.py @@ -421,7 +421,7 @@ class TestMessageSigningFlows(FlowTest): # Ensure message signing is enabled self.settings.set_value(SettingsConstants.SETTING__MESSAGE_SIGNING, SettingsConstants.OPTION__ENABLED) - def expect_network_mismatch_error(load_message: Callable[[str], None]): + def expect_network_mismatch_error(load_message: Callable): self.run_sequence([ FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN), FlowStep(scan_views.ScanView, before_run=load_message), # simulate read message QR; ret val is ignored From a6dd199e5254684d72222c35f8b6877b8e558ee1 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Wed, 16 Aug 2023 07:50:50 -0500 Subject: [PATCH 26/61] Detect network mismatch earlier --- src/seedsigner/views/seed_views.py | 54 +++++++++++++----------------- tests/test_flows_seed.py | 6 ---- 2 files changed, 24 insertions(+), 36 deletions(-) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 2b86f417..16733ca8 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -1921,23 +1921,37 @@ class SeedSignMessageStartView(View): self.derivation_path = derivation_path self.message = message + # calculate the actual receive address + addr_format = embit_utils.parse_derivation_path(derivation_path) + if not addr_format["clean_match"]: + raise NotYetImplementedView("Signing messages for custom derivation paths not supported") + + # Note: addr_format["network"] can be MAINNET or [TESTNET, REGTEST] + if self.settings.get_value(SettingsConstants.SETTING__NETWORK) not in addr_format["network"]: + from seedsigner.views.view import NetworkMismatchErrorView + self.set_redirect(Destination(NetworkMismatchErrorView, view_args=dict(text=f"Current network setting ({self.settings.get_value_display_name(SettingsConstants.SETTING__NETWORK)}) doesn't match {self.derivation_path}"))) + + # cleanup. Note: We could leave this in place so the user can resume the + # flow, but for now we avoid complications and keep things simple. + self.controller.resume_main_flow = None + return + data = self.controller.sign_message_data if not data: data = {} self.controller.sign_message_data = data data["derivation_path"] = derivation_path data["message"] = message + data["addr_format"] = addr_format # May be None self.seed_num = data.get("seed_num") - - def run(self): if self.seed_num is not None: # We already know which seed we're signing with - return Destination(SeedSignMessageConfirmMessageView, skip_current_view=True) + self.set_redirect(Destination(SeedSignMessageConfirmMessageView, skip_current_view=True)) else: - return Destination(SeedSelectSeedView, view_args=dict(flow=Controller.FLOW__SIGN_MESSAGE), skip_current_view=True) + self.set_redirect(Destination(SeedSelectSeedView, view_args=dict(flow=Controller.FLOW__SIGN_MESSAGE), skip_current_view=True)) @@ -1979,34 +1993,14 @@ class SeedSignMessageConfirmAddressView(View): def __init__(self): super().__init__() data = self.controller.sign_message_data - self.seed_num = data.get("seed_num") + seed = self.controller.storage.seeds[data.get("seed_num")] self.derivation_path = data.get("derivation_path") + addr_format = data.get("addr_format") - if self.seed_num is None or not self.derivation_path: - raise Exception("Routing error: sign_message_data hasn't been set") - - # calculate the actual receive address - seed = self.controller.storage.seeds[self.seed_num] - addr_format = embit_utils.parse_derivation_path(self.derivation_path) - if not addr_format["clean_match"]: - raise Exception("Signing messages for custom derivation paths not supported") - - # Note: addr_format["network"] can be MAINNET or [TESTNET, REGTEST] - if self.settings.get_value(SettingsConstants.SETTING__NETWORK) in addr_format["network"]: - # Does nothing for MAINNET, but uses current setting to decide between TESTNET and REGTEST - addr_format["network"] = self.settings.get_value(SettingsConstants.SETTING__NETWORK) - else: - from seedsigner.views.view import NetworkMismatchErrorView - self.set_redirect(Destination(NetworkMismatchErrorView, view_args=dict(text=f"Current network setting ({self.settings.get_value_display_name(SettingsConstants.SETTING__NETWORK)}) doesn't match {self.derivation_path}"))) - - # cleanup. Note: We could leave this in place so the user can resume the - # flow, but for now we avoid complications and keep things simple. - self.controller.resume_main_flow = None - self.controller.sign_message_data = None - return - - xpub = seed.get_xpub(wallet_path=self.derivation_path, network=addr_format["network"]) - embit_network = embit_utils.get_embit_network_name(addr_format["network"]) + # Current settings will differentiate TESTNET and REGTEST (since derivation path + # alone doesn't specify which one we're using). + xpub = seed.get_xpub(wallet_path=self.derivation_path, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)) + embit_network = embit_utils.get_embit_network_name(self.settings.get_value(SettingsConstants.SETTING__NETWORK)) self.address = embit_utils.get_single_sig_address(xpub=xpub, script_type=addr_format["script_type"], index=addr_format["index"], is_change=addr_format["is_change"], embit_network=embit_network) diff --git a/tests/test_flows_seed.py b/tests/test_flows_seed.py index bb081bb3..d2ebc7d2 100644 --- a/tests/test_flows_seed.py +++ b/tests/test_flows_seed.py @@ -426,12 +426,6 @@ class TestMessageSigningFlows(FlowTest): FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN), FlowStep(scan_views.ScanView, before_run=load_message), # simulate read message QR; ret val is ignored FlowStep(seed_views.SeedSignMessageStartView, is_redirect=True), - FlowStep(seed_views.SeedSelectSeedView, button_data_selection=seed_views.SeedSelectSeedView.SCAN_SEED), - FlowStep(scan_views.ScanView, before_run=self.load_seed_into_decoder), # simulate read SeedQR; ret val is ignored - FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE), - FlowStep(seed_views.SeedOptionsView, is_redirect=True), - FlowStep(seed_views.SeedSignMessageConfirmMessageView, before_run=self.inject_mesage_as_paged_message, screen_return_value=0), - FlowStep(seed_views.SeedSignMessageConfirmAddressView, is_redirect=True), FlowStep(NetworkMismatchErrorView), FlowStep(settings_views.SettingsEntryUpdateSelectionView), ]) From 05e2909603471258f45145686225450c717b04cb Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 18 Aug 2023 07:23:57 -0500 Subject: [PATCH 27/61] Bugfix & improvements when message signing is disabled --- src/seedsigner/views/scan_views.py | 19 ++++++------- src/seedsigner/views/seed_views.py | 6 +++- src/seedsigner/views/view.py | 26 +++++++++++++---- tests/screenshot_generator/generator.py | 13 +++++++-- tests/test_flows_seed.py | 38 ++++++++++++++++++++++++- 5 files changed, 81 insertions(+), 21 deletions(-) diff --git a/src/seedsigner/views/scan_views.py b/src/seedsigner/views/scan_views.py index 2b83d03b..9c4e5990 100644 --- a/src/seedsigner/views/scan_views.py +++ b/src/seedsigner/views/scan_views.py @@ -136,19 +136,16 @@ class ScanView(View): ) elif self.decoder.is_sign_message: - if self.settings.get_value(SettingsConstants.SETTING__MESSAGE_SIGNING) == SettingsConstants.OPTION__ENABLED: - from seedsigner.views.seed_views import SeedSignMessageStartView - qr_data = self.decoder.get_qr_data() + from seedsigner.views.seed_views import SeedSignMessageStartView + qr_data = self.decoder.get_qr_data() - return Destination( - SeedSignMessageStartView, - view_args=dict( - derivation_path=qr_data["derivation_path"], - message=qr_data["message"], - ) + return Destination( + SeedSignMessageStartView, + view_args=dict( + derivation_path=qr_data["derivation_path"], + message=qr_data["message"], ) - else: - return Destination(OptionDisabledView, view_args=dict(error_msg="Message signing is currently disabled in Settings")) + ) else: return Destination(NotYetImplementedView) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 16733ca8..a58b1f7e 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -23,7 +23,7 @@ from seedsigner.models.seed import InvalidSeedException, Seed from seedsigner.models.settings import Settings, SettingsConstants from seedsigner.models.settings_definition import SettingsDefinition from seedsigner.models.threads import BaseThread, ThreadsafeCounter -from seedsigner.views.view import NotYetImplementedView, View, Destination, BackStackView, MainMenuView +from seedsigner.views.view import NotYetImplementedView, OptionDisabledView, View, Destination, BackStackView, MainMenuView @@ -1921,6 +1921,10 @@ class SeedSignMessageStartView(View): self.derivation_path = derivation_path self.message = message + if self.settings.get_value(SettingsConstants.SETTING__MESSAGE_SIGNING) == SettingsConstants.OPTION__DISABLED: + self.set_redirect(Destination(OptionDisabledView, view_args=dict(settings_attr=SettingsConstants.SETTING__MESSAGE_SIGNING))) + return + # calculate the actual receive address addr_format = embit_utils.parse_derivation_path(derivation_path) if not addr_format["clean_match"]: diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index d3e3c73c..3f507a73 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -5,6 +5,7 @@ from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerIconCo from seedsigner.gui.screens import RET_CODE__POWER_BUTTON, RET_CODE__BACK_BUTTON from seedsigner.gui.screens.screen import BaseScreen, DireWarningScreen, LargeButtonScreen, PowerOffScreen, PowerOffNotRequiredScreen, ResetScreen, WarningScreen from seedsigner.models.settings import Settings, SettingsConstants +from seedsigner.models.settings_definition import SettingsDefinition from seedsigner.models.threads import BaseThread @@ -331,7 +332,7 @@ class ErrorView(View): class NetworkMismatchErrorView(ErrorView): title: str = "Network Mismatch" show_back_button: bool = False - button_text: str = "Change Settings" + button_text: str = "Change Setting" next_destination: Destination = None @@ -368,18 +369,33 @@ class UnhandledExceptionView(View): @dataclass class OptionDisabledView(View): - error_msg: str + UPDATE_SETTING = "Update Setting" + DONE = "Done" + settings_attr: str + + def __post_init__(self): + super().__post_init__() + self.settings_entry = SettingsDefinition.get_settings_entry(self.settings_attr) + self.error_msg = f"\"{self.settings_entry.display_name}\" is currently disabled in Settings." def run(self): - WarningScreen( + button_data = [self.UPDATE_SETTING, self.DONE] + selected_menu_num = self.run_screen( + WarningScreen, title="Option Disabled", status_headline=None, text=self.error_msg, - button_data=["OK"], + button_data=button_data, show_back_button=False, allow_text_overflow=True, # Fit what we can, let the rest go off the edges - ).display() + ) + + if button_data[selected_menu_num] == self.UPDATE_SETTING: + from seedsigner.views.settings_views import SettingsEntryUpdateSelectionView + return Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=self.settings_attr), clear_history=True) + else: + return Destination(MainMenuView, clear_history=True) diff --git a/tests/screenshot_generator/generator.py b/tests/screenshot_generator/generator.py index 3e9494a4..9afac77d 100644 --- a/tests/screenshot_generator/generator.py +++ b/tests/screenshot_generator/generator.py @@ -3,6 +3,9 @@ import os import sys import time from mock import Mock, patch, MagicMock +from seedsigner.helpers import embit_utils + +from seedsigner.models.settings import Settings # Prevent importing modules w/Raspi hardware dependencies. @@ -28,7 +31,7 @@ from seedsigner.models.seed import Seed from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition from seedsigner.views import (MainMenuView, PowerOptionsView, RestartView, NotYetImplementedView, UnhandledExceptionView, psbt_views, seed_views, settings_views, tools_views) -from seedsigner.views.view import NetworkMismatchErrorView, PowerOffView, View +from seedsigner.views.view import NetworkMismatchErrorView, OptionDisabledView, PowerOffView, View from .utils import ScreenshotComplete, ScreenshotRenderer @@ -80,10 +83,12 @@ def test_generate_screenshots(target_locale): controller.multisig_wallet_descriptor = embit.descriptor.Descriptor.from_string(MULTISIG_WALLET_DESCRIPTOR) # Message signing data + derivation_path = "m/84h/0h/0h/0/0" controller.sign_message_data = { "seed_num": 0, - "derivation_path": "m/84h/0h/0h/0/0", + "derivation_path": derivation_path, "message": "I attest that I control this bitcoin address blah blah blah", + "addr_format": embit_utils.parse_derivation_path(derivation_path) } # Automatically populate all Settings options Views @@ -116,6 +121,7 @@ def test_generate_screenshots(target_locale): (UnhandledExceptionView, dict(error=UnhandledExceptionViewFood)), (settings_views.SettingsIngestSettingsQRView, dict(data="settings::v1 name=factory_reset")), NetworkMismatchErrorView, + (OptionDisabledView, dict(settings_attr=SettingsConstants.SETTING__MESSAGE_SIGNING)), ], @@ -231,7 +237,8 @@ def test_generate_screenshots(target_locale): print(f"Completed {view_name}") except Exception as e: # Something else went wrong - print(repr(e)) + from traceback import print_exc + print_exc() raise e finally: if toast_thread: diff --git a/tests/test_flows_seed.py b/tests/test_flows_seed.py index d2ebc7d2..521d77bc 100644 --- a/tests/test_flows_seed.py +++ b/tests/test_flows_seed.py @@ -8,7 +8,7 @@ from base import FlowTestRunScreenNotExecutedException, FlowTestInvalidButtonDat from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON from seedsigner.models.settings import Settings, SettingsConstants from seedsigner.models.seed import Seed -from seedsigner.views.view import MainMenuView, RemoveMicroSDWarningView, View, NetworkMismatchErrorView +from seedsigner.views.view import MainMenuView, OptionDisabledView, RemoveMicroSDWarningView, View, NetworkMismatchErrorView from seedsigner.views import seed_views, scan_views, settings_views, tools_views @@ -441,3 +441,39 @@ class TestMessageSigningFlows(FlowTest): # REGTEST settings vs MAINNET derivation path with the message self.settings.set_value(SettingsConstants.SETTING__NETWORK, SettingsConstants.REGTEST) expect_network_mismatch_error(self.load_short_message_into_decoder) + + + + + def test_sign_message_option_disabled(self): + """ + Should redirect to OptionDisabledView if a `signmessage` QR is scanned with + message signing disabled. + + Should offer the option to route directly to enable that settings or return to + MainMenuView. + """ + # Ensure message signing is disabled + self.settings.set_value(SettingsConstants.SETTING__MESSAGE_SIGNING, SettingsConstants.OPTION__DISABLED) + + sequence = [ + FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN), + FlowStep(scan_views.ScanView, before_run=self.load_short_message_into_decoder), # simulate read message QR; ret val is ignored + FlowStep(seed_views.SeedSignMessageStartView, is_redirect=True), + ] + + # First test routing to update the setting + self.run_sequence( + sequence + [ + FlowStep(OptionDisabledView, button_data_selection=OptionDisabledView.UPDATE_SETTING, is_redirect=True), + FlowStep(settings_views.SettingsEntryUpdateSelectionView), + ] + ) + + # Now test exiting to Main Menu + self.run_sequence( + sequence + [ + FlowStep(OptionDisabledView, button_data_selection=OptionDisabledView.DONE, is_redirect=True), + FlowStep(MainMenuView), + ] + ) From 52423a9af2371745e440bbddaa5d00b56e4fcb7d Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 18 Aug 2023 09:57:35 -0500 Subject: [PATCH 28/61] final TextArea, screen tweaks --- src/seedsigner/gui/components.py | 60 +++++++++------------ src/seedsigner/gui/screens/tools_screens.py | 17 +++--- src/seedsigner/gui/toast.py | 8 +-- src/seedsigner/models/seed_storage.py | 5 ++ src/seedsigner/views/tools_views.py | 30 +++++------ 5 files changed, 59 insertions(+), 61 deletions(-) diff --git a/src/seedsigner/gui/components.py b/src/seedsigner/gui/components.py index 2ee75395..7290a98e 100644 --- a/src/seedsigner/gui/components.py +++ b/src/seedsigner/gui/components.py @@ -287,6 +287,7 @@ class TextArea(BaseComponent): supersampling_factor: int = 1 auto_line_break: bool = True allow_text_overflow: bool = False + height_ignores_below_baseline: bool = False # If True, characters that render below the baseline (e.g. "pqgy") will not affect the final height calculation def __post_init__(self): @@ -316,11 +317,9 @@ class TextArea(BaseComponent): font = Fonts.get_font(self.font_name, self.font_size) # Note: from the baseline anchor, `top` is a negative number while `bottom` - # conveys the pixels used below the baseline (e.g. in "py"). - # For consistency, ensure we have a full-height character above baseline. - # Also include some "below baseline" chars. - measurement_chars = "Agjpqy" - (left, top, right, bottom) = font.getbbox(self.text + measurement_chars, anchor="ls") + # conveys the height of the pixels that rendered below the baseline, if any + # (e.g. "py" in "python"). + (left, top, right, bottom) = font.getbbox(self.text, anchor="ls") self.text_height_above_baseline = -1 * top self.text_height_below_baseline = bottom @@ -333,12 +332,14 @@ class TextArea(BaseComponent): # Calculate the actual height if len(self.text_lines) == 1: - total_text_height = self.text_height_above_baseline + self.text_height_below_baseline + total_text_height = self.text_height_above_baseline + if not self.height_ignores_below_baseline: + total_text_height += self.text_height_below_baseline else: # Multiply for the number of lines plus the spacer total_text_height = self.text_height_above_baseline * len(self.text_lines) + self.line_spacing * (len(self.text_lines) - 1) - if re.findall(f"[gjpqy]", self.text_lines[-1]["text"]): + if not self.height_ignores_below_baseline and re.findall(f"[gjpqy]", self.text_lines[-1]["text"]): # Last line has at least one char that dips below baseline total_text_height += self.text_height_below_baseline @@ -356,15 +357,7 @@ class TextArea(BaseComponent): else: # Vertically center the text's starting point - if len(self.text_lines) == 1: - # For consistency when used in TopNav and elsewhere, ignore the - # text's pixels below the baseline. - # In other words: "Home" and "Something" will get the same text_y, - # even though the "g" dips below baseline. - self.text_y += int(self.height - (total_text_height - self.text_height_below_baseline))/2 - else: - # Vertically center for the full height. - self.text_y += int(self.height - (total_text_height))/2 + self.text_y += int(self.height - total_text_height)/2 def render(self): @@ -373,26 +366,28 @@ class TextArea(BaseComponent): # Add a `resample_padding` above and below when supersampling to avoid edge # effects (resized text that's right up against the top/bottom gets slightly # dimmer at the edge otherwise). - # TODO: Store resulting super-sampled image as a member var in __post_init__ and - # just re-paste it here. if self.font_size < 20 and (not self.supersampling_factor or self.supersampling_factor == 1): self.supersampling_factor = 2 + actual_text_height = self.height + if self.height_ignores_below_baseline: + # Even though we're ignoring the pixels below the baseline for spacing + # purposes, we have to make sure we don't crop those pixels out during the + # supersampling operations here. + actual_text_height += self.text_height_below_baseline + resample_padding = 10 if self.supersampling_factor > 1.0 else 0 img = Image.new( "RGBA", ( self.width * self.supersampling_factor, - (self.height + 2*resample_padding) * self.supersampling_factor + (actual_text_height + 2*resample_padding) * self.supersampling_factor ), self.background_color ) draw = ImageDraw.Draw(img) - # draw.line((0, resample_padding * self.supersampling_factor, self.width * self.supersampling_factor, resample_padding * self.supersampling_factor), fill="blue", width=1) - # draw.line((0, (resample_padding + self.height) * self.supersampling_factor, self.width * self.supersampling_factor, (resample_padding + self.height) * self.supersampling_factor), fill="red", width=1) cur_y = (self.text_y + resample_padding) * self.supersampling_factor - supersampled_font = Fonts.get_font(self.font_name, int(self.supersampling_factor * self.font_size)) if self.is_text_centered: @@ -412,15 +407,20 @@ class TextArea(BaseComponent): text_x = self.min_text_x + int(line["text_width"]/2) draw.text((text_x * self.supersampling_factor, cur_y), line["text"], fill=self.font_color, font=supersampled_font, anchor=anchor) + + # Debugging: show the exact vertical extents of each line of text + # draw.line((0, cur_y - self.text_height_above_baseline * self.supersampling_factor, self.width * self.supersampling_factor, cur_y - self.text_height_above_baseline * self.supersampling_factor), fill="red", width=int(self.supersampling_factor)) + # draw.line((0, cur_y, self.width * self.supersampling_factor, cur_y), fill="red", width=int(self.supersampling_factor)) + cur_y += (self.text_height_above_baseline + self.line_spacing) * self.supersampling_factor # Crop off the top_padding and resize the result down to onscreen size if self.supersampling_factor > 1.0: - resized = img.resize((self.width, self.height + 2*resample_padding), Image.LANCZOS) + resized = img.resize((self.width, actual_text_height + 2*resample_padding), Image.LANCZOS) sharpened = resized.filter(ImageFilter.SHARPEN) # Crop args are actually (left, top, WIDTH, HEIGHT) - img = sharpened.crop((0, resample_padding, self.width, self.height + resample_padding)) + img = sharpened.crop((0, resample_padding, self.width, actual_text_height + resample_padding)) self.canvas.paste(img, (self.screen_x, self.screen_y)) @@ -1218,7 +1218,7 @@ class TopNav(BaseComponent): icon_name=SeedSignerIconConstants.BACK, icon_size=GUIConstants.ICON_INLINE_FONT_SIZE, screen_x=GUIConstants.EDGE_PADDING, - screen_y=GUIConstants.EDGE_PADDING, + screen_y=GUIConstants.EDGE_PADDING - 1, # Text can't perfectly vertically center relative to the button; shifting it down 1px looks better. width=GUIConstants.TOP_NAV_BUTTON_SIZE, height=GUIConstants.TOP_NAV_BUTTON_SIZE, ) @@ -1262,6 +1262,7 @@ class TopNav(BaseComponent): is_text_centered=True, font_name=self.font_name, font_size=self.font_size, + height_ignores_below_baseline=True, # Consistently vertically center text, ignoring chars that render below baseline (e.g. "pqyj") ) @@ -1289,15 +1290,6 @@ class TopNav(BaseComponent): self.right_button.is_selected = self.is_selected self.right_button.render() - # self.image_draw.text( - # (self.text_x, self.text_y), - # self.text, - # font=self.font, - # fill=self.font_color, - # stroke_width=1, - # stroke_fill=GUIConstants.BACKGROUND_COLOR, - # ) - def linear_interp(a, b, t): diff --git a/src/seedsigner/gui/screens/tools_screens.py b/src/seedsigner/gui/screens/tools_screens.py index 4e8ad2f6..aa413fbd 100644 --- a/src/seedsigner/gui/screens/tools_screens.py +++ b/src/seedsigner/gui/screens/tools_screens.py @@ -240,12 +240,13 @@ class ToolsCalcFinalWordScreen(ButtonListScreen): self.components.append(TextArea( text=f"""Your input: \"{selection_text}\"""", - screen_y=self.top_nav.height, + screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING - 2, # Nudge to last line doesn't get too close to "Next" button + height_ignores_below_baseline=True, # Keep the next line (bits display) snugged up, regardless of text rendering below the baseline )) # ...and that entropy's associated 11 bits - screen_y=self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING - self.components.append(TextArea( + screen_y = self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING + first_bits_line = TextArea( text=keeper_selected_bits, font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME, font_size=bit_font_size, @@ -253,10 +254,13 @@ class ToolsCalcFinalWordScreen(ButtonListScreen): screen_x=bit_display_x, screen_y=screen_y, is_text_centered=False, - )) + ) + self.components.append(first_bits_line) # Render the least significant bits that will be replaced by the checksum in a # de-emphasized font color. + if "_" in discard_selected_bits: + screen_y += int(first_bits_line.height/2) # center the underscores vertically like hypens self.components.append(TextArea( text=discard_selected_bits, font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME, @@ -272,7 +276,7 @@ class ToolsCalcFinalWordScreen(ButtonListScreen): self.components.append(TextArea( text="Checksum", edge_padding=0, - screen_y=self.components[-1].screen_y + self.components[-1].height + 2*GUIConstants.COMPONENT_PADDING, + screen_y=first_bits_line.screen_y + first_bits_line.height + 2*GUIConstants.COMPONENT_PADDING, )) # ...and its actual bits. Prepend spacers to keep vertical alignment @@ -288,7 +292,7 @@ class ToolsCalcFinalWordScreen(ButtonListScreen): font_size=bit_font_size, edge_padding=0, screen_x=bit_display_x, - screen_y=screen_y, + screen_y=screen_y + int(first_bits_line.height/2), # center the underscores vertically like hypens is_text_centered=False, )) @@ -308,6 +312,7 @@ class ToolsCalcFinalWordScreen(ButtonListScreen): self.components.append(TextArea( text=f"""Final Word: \"{self.actual_final_word}\"""", screen_y=self.components[-1].screen_y + self.components[-1].height + 2*GUIConstants.COMPONENT_PADDING, + height_ignores_below_baseline=True, # Keep the next line (bits display) snugged up, regardless of text rendering below the baseline )) # Once again show the bits that came from the user's entropy... diff --git a/src/seedsigner/gui/toast.py b/src/seedsigner/gui/toast.py index 7f4b3a50..6d37e0b9 100644 --- a/src/seedsigner/gui/toast.py +++ b/src/seedsigner/gui/toast.py @@ -38,15 +38,12 @@ class ToastOverlay(BaseComponent): auto_line_break=True, width=self.canvas_width - self.icon.screen_x - self.icon.width - GUIConstants.COMPONENT_PADDING - self.outline_thickness, screen_x=self.icon.screen_x + self.icon.width + GUIConstants.COMPONENT_PADDING, - allow_text_overflow=False + allow_text_overflow=False, ) - # Single-line toast messages need their vertical centering nudged down to account - # for TextArea including below the baseline in its height calculation. - below_baseline = self.label.text_height_below_baseline if len(self.label.text_lines) == 1 else 0 # Vertically center the message within the toast (for single- or multi-line # messages). - self.label.screen_y = self.canvas_height - self.height + self.outline_thickness + int((self.height - 2*self.outline_thickness - (self.label.height - below_baseline))/2) + self.label.screen_y = self.canvas_height - self.height + self.outline_thickness + int((self.height - 2*self.outline_thickness - self.label.height)/2) def render(self): @@ -67,7 +64,6 @@ class ToastOverlay(BaseComponent): - class BaseToastOverlayManagerThread(BaseThread): """ The toast notification popup consists of a gui component (`ToastOverlay`) and this diff --git a/src/seedsigner/models/seed_storage.py b/src/seedsigner/models/seed_storage.py index dcc30a7a..5b6c3049 100644 --- a/src/seedsigner/models/seed_storage.py +++ b/src/seedsigner/models/seed_storage.py @@ -63,6 +63,11 @@ class SeedStorage: def update_pending_mnemonic(self, word: str, index: int): + """ + Replaces the nth word in the pending mnemonic. + + * may specify a negative `index` (e.g. -1 is the last word). + """ if index >= len(self._pending_mnemonic): raise Exception(f"index {index} is too high") self._pending_mnemonic[index] = word diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index b8249f6f..71740c55 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass import hashlib import os import time @@ -313,8 +314,7 @@ class ToolsCalcFinalWordFinalizePromptView(View): class ToolsCalcFinalWordCoinFlipsView(View): def run(self): - mnemonic = self.controller.storage.pending_mnemonic - mnemonic_length = len(mnemonic) + mnemonic_length = len(self.controller.storage.pending_mnemonic) if mnemonic_length == 12: total_flips = 7 @@ -329,21 +329,13 @@ class ToolsCalcFinalWordCoinFlipsView(View): return Destination(BackStackView) else: - print(ret_val) - binary_string = ret_val + "0" * (11 - total_flips) - wordlist_index = int(binary_string, 2) - wordlist = Seed.get_wordlist(self.controller.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)) - word = wordlist[wordlist_index] - self.controller.storage.update_pending_mnemonic(word, mnemonic_length - 1) - return Destination(ToolsCalcFinalWordShowFinalWordView, view_args=dict(coin_flips=ret_val)) +@dataclass class ToolsCalcFinalWordShowFinalWordView(View): - def __init__(self, coin_flips=None): - super().__init__() - self.coin_flips = coin_flips + coin_flips: str = None def run(self): @@ -353,6 +345,13 @@ class ToolsCalcFinalWordShowFinalWordView(View): # * 7 bits to a 12-word seed (plus 4-bit checksum) from seedsigner.helpers import mnemonic_generation + if self.coin_flips: + binary_string = self.coin_flips + "0" * (11 - len(self.coin_flips)) + wordlist_index = int(binary_string, 2) + wordlist = Seed.get_wordlist(self.controller.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)) + word = wordlist[wordlist_index] + self.controller.storage.update_pending_mnemonic(word, -1) + mnemonic = self.controller.storage.pending_mnemonic mnemonic_length = len(mnemonic) wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) @@ -362,7 +361,7 @@ class ToolsCalcFinalWordShowFinalWordView(View): mnemonic=self.controller.storage.pending_mnemonic, wordlist_language_code=wordlist_language_code, ) - self.controller.storage.update_pending_mnemonic(final_mnemonic[-1], mnemonic_length - 1) + self.controller.storage.update_pending_mnemonic(final_mnemonic[-1], -1) # Prep the user's selected word (if there was one) and the actual final word for # the display. @@ -383,14 +382,15 @@ class ToolsCalcFinalWordShowFinalWordView(View): NEXT = "Next" button_data = [NEXT] - selected_menu_num = ToolsCalcFinalWordScreen( + selected_menu_num = self.run_screen( + ToolsCalcFinalWordScreen, title="Final Word Calc", button_data=button_data, selected_final_word=selected_final_word, selected_final_bits=selected_final_bits, checksum_bits=checksum_bits, actual_final_word=actual_final_word, - ).display() + ) if selected_menu_num == RET_CODE__BACK_BUTTON: return Destination(BackStackView) From 98e8a3d64e0557218dd95743d282c8821be7d53a Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 18 Aug 2023 09:58:02 -0500 Subject: [PATCH 29/61] Added `ToolsCalcFinalWordShowFinalWordView` screenshots --- tests/screenshot_generator/generator.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/screenshot_generator/generator.py b/tests/screenshot_generator/generator.py index 3e9494a4..177f287e 100644 --- a/tests/screenshot_generator/generator.py +++ b/tests/screenshot_generator/generator.py @@ -68,6 +68,12 @@ def test_generate_screenshots(target_locale): controller.storage.set_pending_seed(seed_24) UnhandledExceptionViewFood = ["IndexError", "line 1, in some_buggy_code.py", "list index out of range"] + # Pending mnemonic for ToolsCalcFinalWordShowFinalWordView + controller.storage.init_pending_mnemonic(num_words=12) + for i, word in enumerate(mnemonic_12[:11]): + controller.storage.update_pending_mnemonic(word=word, index=i) + controller.storage.update_pending_mnemonic(word="satoshi", index=11) # random last word; not supposed to be a valid checksum (yet) + # Load a PSBT into memory BASE64_PSBT_1 = """cHNidP8BAP06AQIAAAAC5l4E3oEjI+H0im8t/K2nLmF5iJFdKEiuQs8ESveWJKcAAAAAAP3///8iBZMRhYIq4s/LmnTmKBi79M8ITirmsbO++63evK4utwAAAAAA/f///wZYQuoDAAAAACIAIAW5jm3UnC5fyjKCUZ8LTzjENtb/ioRTaBMXeSXsB3n+bK2fCgAAAAAWABReJY7akT1+d+jx475yBRWORdBd7VxbUgUAAAAAFgAU4wj9I/jB3GjNQudNZAca+7g9R16iWtYOAAAAABYAFIotPApLZlfscg8f3ppKqO3qA5nv7BnMFAAAAAAiACAs6SGc8qv4FwuNl0G0SpMZG8ODUEk5RXiWUcuzzw5iaRSfAhMAAAAAIgAgW0f5QxQIgVCGQqKzsvfkXZjUxdFop5sfez6Pt8mUbmZ1AgAAAAEAkgIAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////BQIRAgEB/////wJAvkAlAAAAACIAIIRPoo2LvkrwrhrYFhLhlP43izxbA4Eo6Y6iFFiQYdXRAAAAAAAAAAAmaiSqIant4vYcP3HR3v0/qZnfo2lTdVxpBol5mWK0i+vYNpdOjPkAAAAAAQErQL5AJQAAAAAiACCET6KNi75K8K4a2BYS4ZT+N4s8WwOBKOmOohRYkGHV0QEFR1EhArGhNdUqlR4BAOLGTMrY2ZJYTQNRudp7fU7i8crRJqgEIQNDxn7PjUzvsP6KYw4s7dmoZE0qO1K6MaM+2ScRZ7hyxFKuIgYCsaE11SqVHgEA4sZMytjZklhNA1G52nt9TuLxytEmqAQcc8XaCjAAAIABAACAAAAAgAIAAIAAAAAAAwAAACIGA0PGfs+NTO+w/opjDizt2ahkTSo7Uroxoz7ZJxFnuHLEHCK94akwAACAAQAAgAAAAIACAACAAAAAAAMAAAAAAQCSAgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8FAhACAQH/////AkC+QCUAAAAAIgAghE+ijYu+SvCuGtgWEuGU/jeLPFsDgSjpjqIUWJBh1dEAAAAAAAAAACZqJKohqe3i9hw/cdHe/T+pmd+jaVN1XGkGiXmZYrSL69g2l06M+QAAAAABAStAvkAlAAAAACIAIIRPoo2LvkrwrhrYFhLhlP43izxbA4Eo6Y6iFFiQYdXRAQVHUSECsaE11SqVHgEA4sZMytjZklhNA1G52nt9TuLxytEmqAQhA0PGfs+NTO+w/opjDizt2ahkTSo7Uroxoz7ZJxFnuHLEUq4iBgKxoTXVKpUeAQDixkzK2NmSWE0DUbnae31O4vHK0SaoBBxzxdoKMAAAgAEAAIAAAACAAgAAgAAAAAADAAAAIgYDQ8Z+z41M77D+imMOLO3ZqGRNKjtSujGjPtknEWe4csQcIr3hqTAAAIABAACAAAAAgAIAAIAAAAAAAwAAAAABAUdRIQJ5XLCBS0hdo4NANq4lNhimzhyHj7dvObmPAwNj8L2xASEC9mwwoH28/WHnxbb6z05sJ/lHuvrLs/wOooHgFn5ulI1SriICAnlcsIFLSF2jg0A2riU2GKbOHIePt285uY8DA2PwvbEBHCK94akwAACAAQAAgAAAAIACAACAAQAAAAEAAAAiAgL2bDCgfbz9YefFtvrPTmwn+Ue6+suz/A6igeAWfm6UjRxzxdoKMAAAgAEAAIAAAACAAgAAgAEAAAABAAAAAAAAAAEBR1EhAgpbWcEh7rgvRE5UaCcqzWL/TR1B/DS8UeZsKVEvuKLrIQOwLg0emiQbbxafIh69Xjtpj4eclsMhKq1y/7vYDdE7LVKuIgICCltZwSHuuC9ETlRoJyrNYv9NHUH8NLxR5mwpUS+4ouscc8XaCjAAAIABAACAAAAAgAIAAIAAAAAABQAAACICA7AuDR6aJBtvFp8iHr1eO2mPh5yWwyEqrXL/u9gN0TstHCK94akwAACAAQAAgAAAAIACAACAAAAAAAUAAAAAAQFHUSECk50GLh/YhZaLJkDq/dugU3H/WvE6rTgQuY6N57pI4ykhA/H8MdLVP9SA/Hg8l3hvibSaC1bCBzwz7kTW+rsEZ8uFUq4iAgKTnQYuH9iFlosmQOr926BTcf9a8TqtOBC5jo3nukjjKRxzxdoKMAAAgAEAAIAAAACAAgAAgAAAAAAGAAAAIgID8fwx0tU/1ID8eDyXeG+JtJoLVsIHPDPuRNb6uwRny4UcIr3hqTAAAIABAACAAAAAgAIAAIAAAAAABgAAAAA=""" decoder = DecodeQR() @@ -201,7 +207,8 @@ def test_generate_screenshots(target_locale): tools_views.ToolsCalcFinalWordNumWordsView, tools_views.ToolsCalcFinalWordFinalizePromptView, tools_views.ToolsCalcFinalWordCoinFlipsView, - #(tools_views.ToolsCalcFinalWordShowFinalWordView, dict(coin_flips=3)), + (tools_views.ToolsCalcFinalWordShowFinalWordView, {}, "ToolsCalcFinalWordShowFinalWordView_pick_word"), + (tools_views.ToolsCalcFinalWordShowFinalWordView, dict(coin_flips="0010101"), "ToolsCalcFinalWordShowFinalWordView_coin_flips"), #tools_views.ToolsCalcFinalWordDoneView, tools_views.ToolsAddressExplorerSelectSourceView, tools_views.ToolsAddressExplorerAddressTypeView, @@ -231,7 +238,8 @@ def test_generate_screenshots(target_locale): print(f"Completed {view_name}") except Exception as e: # Something else went wrong - print(repr(e)) + from traceback import print_exc + print_exc() raise e finally: if toast_thread: From 2c040d8fe9e6c5ddf3f28e089e22fdd100c90cc7 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 18 Aug 2023 10:07:21 -0500 Subject: [PATCH 30/61] Pull setup logic out of View's run() --- src/seedsigner/views/tools_views.py | 39 +++++++++++++---------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index 71740c55..26ba518a 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -333,20 +333,17 @@ class ToolsCalcFinalWordCoinFlipsView(View): -@dataclass class ToolsCalcFinalWordShowFinalWordView(View): - coin_flips: str = None - - - def run(self): + def __init__(self, coin_flips: str = None): + super().__init__() # Construct the actual final word. The user's selected_final_word # contributes: # * 3 bits to a 24-word seed (plus 8-bit checksum) # * 7 bits to a 12-word seed (plus 4-bit checksum) from seedsigner.helpers import mnemonic_generation - if self.coin_flips: - binary_string = self.coin_flips + "0" * (11 - len(self.coin_flips)) + if coin_flips: + binary_string = coin_flips + "0" * (11 - len(coin_flips)) wordlist_index = int(binary_string, 2) wordlist = Seed.get_wordlist(self.controller.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)) word = wordlist[wordlist_index] @@ -365,31 +362,31 @@ class ToolsCalcFinalWordShowFinalWordView(View): # Prep the user's selected word (if there was one) and the actual final word for # the display. - if self.coin_flips: - selected_final_word = None - selected_final_bits = self.coin_flips + if coin_flips: + self.selected_final_word = None + self.selected_final_bits = coin_flips else: # Convert the user's final word selection into its binary index equivalent - selected_final_word = mnemonic[-1] - selected_final_bits = format(wordlist.index(selected_final_word), '011b') + self.selected_final_word = mnemonic[-1] + self.selected_final_bits = format(wordlist.index(self.selected_final_word), '011b') # And grab the actual final word's checksum bits - actual_final_word = self.controller.storage.pending_mnemonic[-1] - if mnemonic_length == 12: - checksum_bits = format(wordlist.index(actual_final_word), '011b')[-4:] - else: - checksum_bits = format(wordlist.index(actual_final_word), '011b')[-8:] + self.actual_final_word = self.controller.storage.pending_mnemonic[-1] + num_checksum_bits = 4 if mnemonic_length == 12 else 8 + self.checksum_bits = format(wordlist.index(self.actual_final_word), '011b')[-num_checksum_bits:] + + def run(self): NEXT = "Next" button_data = [NEXT] selected_menu_num = self.run_screen( ToolsCalcFinalWordScreen, title="Final Word Calc", button_data=button_data, - selected_final_word=selected_final_word, - selected_final_bits=selected_final_bits, - checksum_bits=checksum_bits, - actual_final_word=actual_final_word, + selected_final_word=self.selected_final_word, + selected_final_bits=self.selected_final_bits, + checksum_bits=self.checksum_bits, + actual_final_word=self.actual_final_word, ) if selected_menu_num == RET_CODE__BACK_BUTTON: From 743a1fcb0fae5ed5c8a2471c1db2d74351cfb1ef Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 18 Aug 2023 10:17:48 -0500 Subject: [PATCH 31/61] additional comments --- src/seedsigner/views/tools_views.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index 26ba518a..866252b2 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -343,24 +343,33 @@ class ToolsCalcFinalWordShowFinalWordView(View): from seedsigner.helpers import mnemonic_generation if coin_flips: + # fill the last bits (what will eventually be the checksum) with zeros binary_string = coin_flips + "0" * (11 - len(coin_flips)) + + # retrieve the matching word for the resulting index wordlist_index = int(binary_string, 2) wordlist = Seed.get_wordlist(self.controller.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)) word = wordlist[wordlist_index] + + # update the pending mnemonic with our new "final" (pre-checksum) word self.controller.storage.update_pending_mnemonic(word, -1) - mnemonic = self.controller.storage.pending_mnemonic - mnemonic_length = len(mnemonic) wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) - wordlist = Seed.get_wordlist(wordlist_language_code) + # Now calculate the REAL final word (has a proper checksum) final_mnemonic = mnemonic_generation.calculate_checksum( mnemonic=self.controller.storage.pending_mnemonic, wordlist_language_code=wordlist_language_code, ) + + # Update our pending mnemonic with the real final word self.controller.storage.update_pending_mnemonic(final_mnemonic[-1], -1) - # Prep the user's selected word (if there was one) and the actual final word for + mnemonic = self.controller.storage.pending_mnemonic + mnemonic_length = len(mnemonic) + wordlist = Seed.get_wordlist(wordlist_language_code) + + # Prep the user's selected word / coin flips and the actual final word for # the display. if coin_flips: self.selected_final_word = None From 53e6205ef36dc16804f97081fa2cf52b0fbc05b8 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 18 Aug 2023 10:24:58 -0500 Subject: [PATCH 32/61] bugfix --- src/seedsigner/views/tools_views.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index 866252b2..89580e5e 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -342,6 +342,19 @@ class ToolsCalcFinalWordShowFinalWordView(View): # * 7 bits to a 12-word seed (plus 4-bit checksum) from seedsigner.helpers import mnemonic_generation + wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) + wordlist = Seed.get_wordlist(wordlist_language_code) + + # Prep the user's selected word / coin flips and the actual final word for + # the display. + if coin_flips: + self.selected_final_word = None + self.selected_final_bits = coin_flips + else: + # Convert the user's final word selection into its binary index equivalent + self.selected_final_word = self.controller.storage.pending_mnemonic[-1] + self.selected_final_bits = format(wordlist.index(self.selected_final_word), '011b') + if coin_flips: # fill the last bits (what will eventually be the checksum) with zeros binary_string = coin_flips + "0" * (11 - len(coin_flips)) @@ -354,8 +367,6 @@ class ToolsCalcFinalWordShowFinalWordView(View): # update the pending mnemonic with our new "final" (pre-checksum) word self.controller.storage.update_pending_mnemonic(word, -1) - wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) - # Now calculate the REAL final word (has a proper checksum) final_mnemonic = mnemonic_generation.calculate_checksum( mnemonic=self.controller.storage.pending_mnemonic, @@ -367,17 +378,6 @@ class ToolsCalcFinalWordShowFinalWordView(View): mnemonic = self.controller.storage.pending_mnemonic mnemonic_length = len(mnemonic) - wordlist = Seed.get_wordlist(wordlist_language_code) - - # Prep the user's selected word / coin flips and the actual final word for - # the display. - if coin_flips: - self.selected_final_word = None - self.selected_final_bits = coin_flips - else: - # Convert the user's final word selection into its binary index equivalent - self.selected_final_word = mnemonic[-1] - self.selected_final_bits = format(wordlist.index(self.selected_final_word), '011b') # And grab the actual final word's checksum bits self.actual_final_word = self.controller.storage.pending_mnemonic[-1] From a8a97ff351d3f08fa1004c29ecf6aa2cffb92e71 Mon Sep 17 00:00:00 2001 From: Jean Do Date: Sat, 19 Aug 2023 07:25:29 -0400 Subject: [PATCH 33/61] .is_inserted() as method at startup --- src/seedsigner/hardware/microsd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/seedsigner/hardware/microsd.py b/src/seedsigner/hardware/microsd.py index 9a3ee315..4b9ace99 100644 --- a/src/seedsigner/hardware/microsd.py +++ b/src/seedsigner/hardware/microsd.py @@ -52,7 +52,7 @@ class MicroSD(Singleton, BaseThread): # at start-up, get current status and inform Settings Settings.handle_microsd_state_change( - action=MicroSD.ACTION__INSERTED if MicroSD.is_inserted else MicroSD.ACTION__REMOVED + action=MicroSD.ACTION__INSERTED if MicroSD.is_inserted() else MicroSD.ACTION__REMOVED ) if os.path.exists(self.FIFO_PATH): From 6bbe23667c45e846caa8da7590302de6f18cd0b7 Mon Sep 17 00:00:00 2001 From: Jean Do Date: Sat, 19 Aug 2023 12:28:54 -0400 Subject: [PATCH 34/61] less err-prone as an unsettable @property --- src/seedsigner/gui/toast.py | 2 +- src/seedsigner/hardware/microsd.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/seedsigner/gui/toast.py b/src/seedsigner/gui/toast.py index 7f4b3a50..188a9d1f 100644 --- a/src/seedsigner/gui/toast.py +++ b/src/seedsigner/gui/toast.py @@ -210,7 +210,7 @@ class RemoveSDCardToastManagerThread(BaseToastOverlayManagerThread): def should_keep_running(self) -> bool: """ Custom exit condition: keep running until the SD card is removed """ from seedsigner.hardware.microsd import MicroSD - return MicroSD.is_inserted() + return MicroSD.get_instance().is_inserted diff --git a/src/seedsigner/hardware/microsd.py b/src/seedsigner/hardware/microsd.py index 4b9ace99..26065c03 100644 --- a/src/seedsigner/hardware/microsd.py +++ b/src/seedsigner/hardware/microsd.py @@ -29,8 +29,8 @@ class MicroSD(Singleton, BaseThread): return cls._instance - @classmethod - def is_inserted(cls): + @property + def is_inserted(self): if Settings.HOSTNAME == Settings.SEEDSIGNER_OS: return os.path.exists(MicroSD.MOUNT_POINT) else: @@ -52,7 +52,7 @@ class MicroSD(Singleton, BaseThread): # at start-up, get current status and inform Settings Settings.handle_microsd_state_change( - action=MicroSD.ACTION__INSERTED if MicroSD.is_inserted() else MicroSD.ACTION__REMOVED + action=MicroSD.ACTION__INSERTED if self.is_inserted else MicroSD.ACTION__REMOVED ) if os.path.exists(self.FIFO_PATH): From 096b4902bdc676c30781785bc815b6a876599482 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 21 Aug 2023 18:40:17 -0500 Subject: [PATCH 35/61] derive correct signmessage address --- src/seedsigner/helpers/embit_utils.py | 15 +++++++++++++-- src/seedsigner/views/seed_views.py | 2 +- tests/test_embit_utils.py | 13 ++++++++++--- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/seedsigner/helpers/embit_utils.py b/src/seedsigner/helpers/embit_utils.py index c33153e8..b32d5d0a 100644 --- a/src/seedsigner/helpers/embit_utils.py +++ b/src/seedsigner/helpers/embit_utils.py @@ -142,12 +142,23 @@ def parse_derivation_path(derivation_path: str) -> dict: } details = dict() + details["wallet_derivation_path"] = "/".join(sections[:-2]) details["script_type"] = lookups["script_types"].get(sections[1]) if not details["script_type"]: details["script_type"] = SettingsConstants.CUSTOM_DERIVATION details["network"] = lookups["networks"].get(sections[2]) - details["is_change"] = sections[-2] == "1" - details["index"] = int(sections[-1]) + + # Check if there's a standard change path + if sections[-2] in ["0", "1"]: + details["is_change"] = sections[-2] == "1" + else: + details["is_change"] = None + + # Check if there's a standard address index + if sections[-1].isdigit(): + details["index"] = int(sections[-1]) + else: + details["index"] = None details["clean_match"] = True for k, v in details.items(): diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 6de06db8..a02e7401 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -2005,7 +2005,7 @@ class SeedSignMessageConfirmAddressView(View): self.controller.sign_message_data = None return - xpub = seed.get_xpub(wallet_path=self.derivation_path, network=addr_format["network"]) + xpub = seed.get_xpub(wallet_path=addr_format["wallet_derivation_path"], network=addr_format["network"]) embit_network = embit_utils.get_embit_network_name(addr_format["network"]) self.address = embit_utils.get_single_sig_address(xpub=xpub, script_type=addr_format["script_type"], index=addr_format["index"], is_change=addr_format["is_change"], embit_network=embit_network) diff --git a/tests/test_embit_utils.py b/tests/test_embit_utils.py index 3f22b93d..c3158d79 100644 --- a/tests/test_embit_utils.py +++ b/tests/test_embit_utils.py @@ -369,8 +369,11 @@ def test_parse_derivation_path(): (SC.TESTNET, SC.CUSTOM_DERIVATION, True): "m/45'/1'/0'/1/5", (SC.REGTEST, SC.CUSTOM_DERIVATION, True): "m/45'/1'/0'/1/5", - # CRAZY custom derivation path - (None, SC.CUSTOM_DERIVATION, False): "m/879345978543'/908327034508534983495'/9085098430894380959043'/0/5", + # CRAZY custom derivation paths + (None, SC.CUSTOM_DERIVATION, False, 5): "m/879345978543'/908327034508534983495'/9085098430894380959043'/0/5", + (None, SC.CUSTOM_DERIVATION, None, 5): "m/9'/78/5", + (None, SC.CUSTOM_DERIVATION, None, 5): "m/9'/78'/5", + (None, SC.CUSTOM_DERIVATION, None, None): "m/9'/78'/5'", } for expected_result, derivation_path in vectors_args.items(): @@ -389,4 +392,8 @@ def test_parse_derivation_path(): assert(actual_result["script_type"] == expected_result[1]) assert(actual_result["is_change"] == expected_result[2]) - assert(actual_result["index"] == int(derivation_path.split("/")[-1])) + + if len(expected_result) == 4: + assert(actual_result["index"] == expected_result[3]) + else: + assert(actual_result["index"] == int(derivation_path.split("/")[-1])) From bd7b25ed98aefb3a488f76cdd5331c5d3e301440 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 21 Aug 2023 18:46:16 -0500 Subject: [PATCH 36/61] additional safety around "wallet_derivation_path" --- src/seedsigner/helpers/embit_utils.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/seedsigner/helpers/embit_utils.py b/src/seedsigner/helpers/embit_utils.py index b32d5d0a..da8f827a 100644 --- a/src/seedsigner/helpers/embit_utils.py +++ b/src/seedsigner/helpers/embit_utils.py @@ -142,7 +142,6 @@ def parse_derivation_path(derivation_path: str) -> dict: } details = dict() - details["wallet_derivation_path"] = "/".join(sections[:-2]) details["script_type"] = lookups["script_types"].get(sections[1]) if not details["script_type"]: details["script_type"] = SettingsConstants.CUSTOM_DERIVATION @@ -153,13 +152,19 @@ def parse_derivation_path(derivation_path: str) -> dict: details["is_change"] = sections[-2] == "1" else: details["is_change"] = None - + # Check if there's a standard address index if sections[-1].isdigit(): details["index"] = int(sections[-1]) else: details["index"] = None + if details["is_change"] is not None and details["index"] is not None: + # standard change and addr index; safe to truncate to the wallet level + details["wallet_derivation_path"] = "/".join(sections[:-2]) + else: + details["wallet_derivation_path"] = None + details["clean_match"] = True for k, v in details.items(): if v is None: From ef80fd3913489f211d66998de532b3c8ce48a9b4 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 21 Aug 2023 18:48:25 -0500 Subject: [PATCH 37/61] Update test_embit_utils.py --- tests/test_embit_utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_embit_utils.py b/tests/test_embit_utils.py index c3158d79..3e85662c 100644 --- a/tests/test_embit_utils.py +++ b/tests/test_embit_utils.py @@ -371,9 +371,12 @@ def test_parse_derivation_path(): # CRAZY custom derivation paths (None, SC.CUSTOM_DERIVATION, False, 5): "m/879345978543'/908327034508534983495'/9085098430894380959043'/0/5", + + # non-standard change and/or index (None, SC.CUSTOM_DERIVATION, None, 5): "m/9'/78/5", (None, SC.CUSTOM_DERIVATION, None, 5): "m/9'/78'/5", (None, SC.CUSTOM_DERIVATION, None, None): "m/9'/78'/5'", + (None, SC.CUSTOM_DERIVATION, False, None): "m/9'/0/5'", } for expected_result, derivation_path in vectors_args.items(): From d2e0d96781ea857c3d7c7d949fc7f4024ded2c23 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 21 Aug 2023 18:53:39 -0500 Subject: [PATCH 38/61] Update seed_views.py --- src/seedsigner/views/seed_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index a02e7401..7056b978 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -1988,7 +1988,7 @@ class SeedSignMessageConfirmAddressView(View): # calculate the actual receive address seed = self.controller.storage.seeds[self.seed_num] addr_format = embit_utils.parse_derivation_path(self.derivation_path) - if not addr_format["clean_match"]: + if not addr_format["clean_match"] or addr_format["script_type"] == SettingsConstants.CUSTOM_DERIVATION: raise Exception("Signing messages for custom derivation paths not supported") if addr_format["network"] != SettingsConstants.MAINNET: From 928c4ab19a7534d9588f97cb4719ef8a76c8a2c5 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Tue, 22 Aug 2023 07:29:50 -0500 Subject: [PATCH 39/61] Minor test case update --- tests/test_embit_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_embit_utils.py b/tests/test_embit_utils.py index 3e85662c..1b7bfaa8 100644 --- a/tests/test_embit_utils.py +++ b/tests/test_embit_utils.py @@ -361,7 +361,7 @@ def test_parse_derivation_path(): (SC.TESTNET, SC.TAPROOT, True): "m/86'/1'/0'/1/5", (SC.REGTEST, SC.TAPROOT, True): "m/86'/1'/0'/1/5", - # Try a typical custom derivation path (Unchained Capital) + # Try a typical custom derivation path (Unchained vault keys) (SC.MAINNET, SC.CUSTOM_DERIVATION, False): "m/45'/0'/0'/0/5", (SC.TESTNET, SC.CUSTOM_DERIVATION, False): "m/45'/1'/0'/0/5", (SC.REGTEST, SC.CUSTOM_DERIVATION, False): "m/45'/1'/0'/0/5", @@ -370,7 +370,7 @@ def test_parse_derivation_path(): (SC.REGTEST, SC.CUSTOM_DERIVATION, True): "m/45'/1'/0'/1/5", # CRAZY custom derivation paths - (None, SC.CUSTOM_DERIVATION, False, 5): "m/879345978543'/908327034508534983495'/9085098430894380959043'/0/5", + (None, SC.CUSTOM_DERIVATION, False, 5): "m/123'/9083270/9083270/9083270/9083270/0/5", # non-standard change and/or index (None, SC.CUSTOM_DERIVATION, None, 5): "m/9'/78/5", From 8bb4bd1b6fee5d63f462552d397695d998d7fa34 Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Tue, 22 Aug 2023 21:44:13 -0400 Subject: [PATCH 40/61] Resolve conflicts in seed_views.py --- src/seedsigner/views/seed_views.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 46f5238b..15c3fde9 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -1997,15 +1997,17 @@ class SeedSignMessageConfirmAddressView(View): def __init__(self): super().__init__() data = self.controller.sign_message_data - seed = self.controller.storage.seeds[data.get("seed_num")] + seed_num = data.get("seed_num") self.derivation_path = data.get("derivation_path") - addr_format = data.get("addr_format") - if self.seed_num is None or not self.derivation_path: + if seed_num is None or not self.derivation_path: raise Exception("Routing error: sign_message_data hasn't been set") + seed = self.controller.storage.seeds[seed_num] + addr_format = data.get("addr_format") + # calculate the actual receive address - seed = self.controller.storage.seeds[self.seed_num] + seed = self.controller.storage.seeds[seed_num] addr_format = embit_utils.parse_derivation_path(self.derivation_path) if not addr_format["clean_match"] or addr_format["script_type"] == SettingsConstants.CUSTOM_DERIVATION: raise Exception("Signing messages for custom derivation paths not supported") From 288512ac027d0110947bf587c9009ddb6690e3ea Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Tue, 22 Aug 2023 23:06:56 -0400 Subject: [PATCH 41/61] Fixes issue #449 and creates unit test to prove it's fixed --- src/seedsigner/views/psbt_views.py | 11 ++++++--- tests/test_flows_psbt.py | 37 ++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/seedsigner/views/psbt_views.py b/src/seedsigner/views/psbt_views.py index b9cfa064..9c0d9c60 100644 --- a/src/seedsigner/views/psbt_views.py +++ b/src/seedsigner/views/psbt_views.py @@ -513,6 +513,8 @@ class PSBTSignedQRDisplayView(View): class PSBTSigningErrorView(View): + SELECT_DIFF_SEED = "Select Diff Seed" + def run(self): psbt_parser: PSBTParser = self.controller.psbt_parser if not psbt_parser: @@ -520,15 +522,18 @@ class PSBTSigningErrorView(View): return Destination(MainMenuView) # Just a WarningScreen here; only use DireWarningScreen for true security risks. - selected_menu_num = WarningScreen( + selected_menu_num = self.run_screen( + WarningScreen, title="PSBT Error", status_icon_name=SeedSignerIconConstants.WARNING, status_headline="Signing Failed", text="Signing with this seed did not add a valid signature.", - button_data=["Select Diff Seed"], - ).display() + button_data=[self.SELECT_DIFF_SEED] + ) if selected_menu_num == 0: + # clear seed selected for psbt signing since it did not add a valid signature + self.controller.psbt_seed = None return Destination(PSBTSelectSeedView, clear_history=True) if selected_menu_num == RET_CODE__BACK_BUTTON: diff --git a/tests/test_flows_psbt.py b/tests/test_flows_psbt.py index be06b8e1..02b71446 100644 --- a/tests/test_flows_psbt.py +++ b/tests/test_flows_psbt.py @@ -54,3 +54,40 @@ class TestPSBTFlows(FlowTest): FlowStep(psbt_views.PSBTSignedQRDisplayView), FlowStep(MainMenuView) ]) + + def test_scan_multisig_psbt_seed_already_signed_flow(self): + + def load_psbt_into_decoder(view: scan_views.ScanView): + view.decoder.add_data("cHNidP8BAIkCAAAAAc9dCSh2RcRPfHaT5bNVBpbg0jAekRLqOK+bpN/QA0jeAAAAAAD9////AtAHAAAAAAAAIlEg24shYsV3IRCzlgmMKjAsR4Ad9tX896z7zDAi5q0TU9H3CgAAAAAAACIAIByGQg/VP2aRID62ty40E64HYZeRRsKRGLt8J/76R6stQ04FAE8BBDWHzwSLLGdzgAAAAq3q6nR20JnHR+vKrBQdWxN9C7xU8zNX942mVF7AQpl2ArrdLwVlkGxaatQJ4wwkvypNBKbwOq9hXGLNlKi7rZWAFDUxzXUwAACAAQAAgAAAAIACAACATwEENYfPBHOCZmWAAAACmH6KTXIny0vueRgQFBq4M6oMuG8f1QM0I/RzKQ03bCgCHrF0fyUtV0+FD2N34u/woqb8MAt/o+7Ed58RddhY8zYUCUjSaDAAAIABAACAAAAAgAIAAIAAAQEriBMAAAAAAAAiACBY4WsjDgJXLj3VW222jU1tkIIhT26ce/2efH73BWGGBiICAqyfkrdUO662QBrdvJcSOZMFxniD7M1awm9U0Kb5XCm5RzBEAiAPkQTY84YjFFkpD6MI2cc5rJySqws5fsTQA/8XEZFpbAIgTNVykbEH4Z7bqyzhhy6lty0K8rtCUDCaHNv+47NNIWgBAQMEAQAAAAEFR1IhApL4XO+VE1pPYn5wnRFyJQKVSc9TX2dO6KIBH6jwvgPaIQKsn5K3VDuutkAa3byXEjmTBcZ4g+zNWsJvVNCm+VwpuVKuIgYCkvhc75UTWk9ifnCdEXIlApVJz1NfZ07oogEfqPC+A9ocNTHNdTAAAIABAACAAAAAgAIAAIAAAAAAAAAAACIGAqyfkrdUO662QBrdvJcSOZMFxniD7M1awm9U0Kb5XCm5HAlI0mgwAACAAQAAgAAAAIACAACAAAAAAAAAAAAAAAEBR1IhApYXaczuYbBM/A+EH639Ir2yIB4PxL46dK/I1V1O9aHgIQLa02HCI/+EP+9gGpxHskjYWFN5hZzXY7RRvwV4UF42ylKuIgIClhdpzO5hsEz8D4Qfrf0ivbIgHg/Evjp0r8jVXU71oeAcNTHNdTAAAIABAACAAAAAgAIAAIABAAAAAAAAACICAtrTYcIj/4Q/72AanEeySNhYU3mFnNdjtFG/BXhQXjbKHAlI0mgwAACAAQAAgAAAAIACAACAAQAAAAAAAAAA") + + def load_seed_into_decoder(view: scan_views.ScanView): + view.decoder.add_data("073318950739065415961602009907670428187212261116") + + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN), + FlowStep(scan_views.ScanView, before_run=load_psbt_into_decoder), # simulate read PSBT; ret val is ignored + FlowStep(psbt_views.PSBTSelectSeedView, button_data_selection=psbt_views.PSBTSelectSeedView.SCAN_SEED), + FlowStep(scan_views.ScanSeedQRView, before_run=load_seed_into_decoder), + FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE), + FlowStep(seed_views.SeedOptionsView, is_redirect=True), + FlowStep(psbt_views.PSBTOverviewView), + FlowStep(psbt_views.PSBTMathView), + FlowStep(psbt_views.PSBTAddressDetailsView, button_data_selection=0), + FlowStep(psbt_views.PSBTChangeDetailsView, button_data_selection=psbt_views.PSBTChangeDetailsView.NEXT), + FlowStep(psbt_views.PSBTFinalizeView, button_data_selection=psbt_views.PSBTFinalizeView.APPROVE_PSBT), + FlowStep(psbt_views.PSBTSigningErrorView, button_data_selection=psbt_views.PSBTSigningErrorView.SELECT_DIFF_SEED), + FlowStep(psbt_views.PSBTSelectSeedView, button_data_selection=psbt_views.PSBTSelectSeedView.SCAN_SEED), + FlowStep(scan_views.ScanSeedQRView, before_run=load_seed_into_decoder), + FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.PASSPHRASE), + FlowStep(seed_views.SeedAddPassphraseView, screen_return_value="abc"), + FlowStep(seed_views.SeedReviewPassphraseView, button_data_selection=seed_views.SeedReviewPassphraseView.DONE), + FlowStep(seed_views.SeedOptionsView, is_redirect=True), + FlowStep(psbt_views.PSBTOverviewView), + FlowStep(psbt_views.PSBTMathView), + FlowStep(psbt_views.PSBTAddressDetailsView, button_data_selection=0), + FlowStep(psbt_views.PSBTChangeDetailsView, button_data_selection=psbt_views.PSBTChangeDetailsView.NEXT), + FlowStep(psbt_views.PSBTFinalizeView, button_data_selection=psbt_views.PSBTFinalizeView.APPROVE_PSBT), + FlowStep(psbt_views.PSBTSignedQRDisplayView), + FlowStep(MainMenuView), + ]) + From edcd36d5db025ca2481614a98ddd227c2fc93ca9 Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Tue, 22 Aug 2023 23:17:28 -0400 Subject: [PATCH 42/61] Bump version in setup.py to 0.7.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 82dcfb5d..fb6eff2c 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh: setuptools.setup( name="seedsigner", - version="0.6.0", + version="0.7.0", author="SeedSigner", author_email="author@example.com", description="Build an offline, airgapped Bitcoin signing device for less than $50!", From 91b02061b0535915f42f272be3c85c79726b7cac Mon Sep 17 00:00:00 2001 From: kdmukai Date: Wed, 23 Aug 2023 07:54:07 -0500 Subject: [PATCH 43/61] bugfix for entering image entropy via long click fixes #450 --- src/seedsigner/gui/screens/tools_screens.py | 65 ++++++++++++--------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/src/seedsigner/gui/screens/tools_screens.py b/src/seedsigner/gui/screens/tools_screens.py index aa413fbd..13fa33be 100644 --- a/src/seedsigner/gui/screens/tools_screens.py +++ b/src/seedsigner/gui/screens/tools_screens.py @@ -1,13 +1,13 @@ +import time + from dataclasses import dataclass from typing import Any from PIL.Image import Image -from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay from seedsigner.hardware.camera import Camera -from seedsigner.gui.components import FontAwesomeIconConstants, Fonts, FormattedAddress, GUIConstants, IconTextLine, SeedSignerIconConstants, TextArea +from seedsigner.gui.components import FontAwesomeIconConstants, Fonts, GUIConstants, IconTextLine, SeedSignerIconConstants, TextArea -from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, BaseScreen, BaseTopNavScreen, ButtonListScreen, KeyboardScreen +from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, BaseScreen, ButtonListScreen, KeyboardScreen from seedsigner.hardware.buttons import HardwareButtonsConstants -from seedsigner.helpers.qr import QR from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition @@ -22,7 +22,7 @@ class ToolsImageEntropyLivePreviewScreen(BaseScreen): super().__post_init__() self.camera = Camera.get_instance() - self.camera.start_video_stream_mode(resolution=(240, 240), framerate=24, format="rgb") + self.camera.start_video_stream_mode(resolution=(self.canvas_width, self.canvas_height), framerate=24, format="rgb") def _run(self): @@ -32,27 +32,7 @@ class ToolsImageEntropyLivePreviewScreen(BaseScreen): instructions_font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, GUIConstants.BUTTON_FONT_SIZE) while True: - frame = self.camera.read_video_stream(as_image=True) - if frame is not None: - self.renderer.canvas.paste(frame) - - self.renderer.draw.text( - xy=( - int(self.renderer.canvas_width/2), - self.renderer.canvas_height - GUIConstants.EDGE_PADDING - ), - text="< back | click joystick", - fill=GUIConstants.BODY_FONT_COLOR, - font=instructions_font, - stroke_width=4, - stroke_fill=GUIConstants.BACKGROUND_COLOR, - anchor="ms" - ) - self.renderer.show_image() - - if len(preview_images) < max_entropy_frames: - preview_images.append(frame) - + # Check for BACK button press if self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_LEFT): # Have to manually update last input time since we're not in a wait_for loop self.hw_inputs.update_last_input_time() @@ -60,7 +40,15 @@ class ToolsImageEntropyLivePreviewScreen(BaseScreen): self.camera.stop_video_stream_mode() return RET_CODE__BACK_BUTTON - elif self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_PRESS): + frame = self.camera.read_video_stream(as_image=True) + + if frame is None: + # Camera probably isn't ready yet + time.sleep(0.01) + continue + + # Check for joystick click to take final entropy image + if self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_PRESS): # Have to manually update last input time since we're not in a wait_for loop self.hw_inputs.update_last_input_time() self.camera.stop_video_stream_mode() @@ -83,6 +71,29 @@ class ToolsImageEntropyLivePreviewScreen(BaseScreen): return preview_images + # If we're still here, it's just another preview frame loop + self.renderer.canvas.paste(frame) + + self.renderer.draw.text( + xy=( + int(self.renderer.canvas_width/2), + self.renderer.canvas_height - GUIConstants.EDGE_PADDING + ), + text="< back | click joystick", + fill=GUIConstants.BODY_FONT_COLOR, + font=instructions_font, + stroke_width=4, + stroke_fill=GUIConstants.BACKGROUND_COLOR, + anchor="ms" + ) + self.renderer.show_image() + + if len(preview_images) == max_entropy_frames: + # Keep a moving window of the last n preview frames; pop the oldest + # before we add the currest frame. + preview_images.pop(0) + preview_images.append(frame) + @dataclass From 430a741dac6ee4fea549018b1fc6c9a74dbc512e Mon Sep 17 00:00:00 2001 From: Conrad Donovan <135184930+conraddonovan16@users.noreply.github.com> Date: Thu, 24 Aug 2023 14:13:48 +0000 Subject: [PATCH 44/61] Removing debugging prints --- src/seedsigner/views/tools_views.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index b8249f6f..fa83a56f 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -223,9 +223,7 @@ class ToolsDiceEntropyEntryView(View): if ret == RET_CODE__BACK_BUTTON: return Destination(BackStackView) - print(f"Dice rolls: {ret}") dice_seed_phrase = mnemonic_generation.generate_mnemonic_from_dice(ret) - print(f"""Mnemonic: "{dice_seed_phrase}" """) # Add the mnemonic as an in-memory Seed seed = Seed(dice_seed_phrase, wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)) From 915b69f0a6cc7821eaaed50179fc6610230ae3e0 Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Thu, 24 Aug 2023 11:29:12 -0400 Subject: [PATCH 45/61] Set 0.7.0 to RC1 --- src/seedsigner/controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 30a50076..c21efe9c 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -93,7 +93,7 @@ class Controller(Singleton): rather than at the top in order avoid circular imports. """ - VERSION = "0.7.0" + VERSION = "0.7.0-rc1" # Declare class member vars with type hints to enable richer IDE support throughout # the code. From 580cf3dca31c185f4060926957487f2b5108a601 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 28 Aug 2023 08:37:50 -0500 Subject: [PATCH 46/61] Reset state upon reading an invalid QR fixes #456 --- src/seedsigner/views/scan_views.py | 7 +++++-- tests/test_flows_seed.py | 28 +++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/seedsigner/views/scan_views.py b/src/seedsigner/views/scan_views.py index 9c4e5990..17f71333 100644 --- a/src/seedsigner/views/scan_views.py +++ b/src/seedsigner/views/scan_views.py @@ -151,12 +151,15 @@ class ScanView(View): return Destination(NotYetImplementedView) elif self.decoder.is_invalid: + # For now, don't even try to re-do the attempted operation, just reset and + # start everything over. + self.controller.resume_main_flow = None return Destination(ErrorView, view_args=dict( title="Error", status_headline="Unknown QR Type", text="QRCode is invalid or is a data format not yet supported.", - button_text="Back", - next_destination=Destination(BackStackView, skip_current_view=True), + button_text="Done", + next_destination=Destination(MainMenuView, clear_history=True), )) return Destination(MainMenuView) diff --git a/tests/test_flows_seed.py b/tests/test_flows_seed.py index 521d77bc..b4afd531 100644 --- a/tests/test_flows_seed.py +++ b/tests/test_flows_seed.py @@ -8,7 +8,7 @@ from base import FlowTestRunScreenNotExecutedException, FlowTestInvalidButtonDat from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON from seedsigner.models.settings import Settings, SettingsConstants from seedsigner.models.seed import Seed -from seedsigner.views.view import MainMenuView, OptionDisabledView, RemoveMicroSDWarningView, View, NetworkMismatchErrorView +from seedsigner.views.view import ErrorView, MainMenuView, OptionDisabledView, RemoveMicroSDWarningView, View, NetworkMismatchErrorView from seedsigner.views import seed_views, scan_views, settings_views, tools_views @@ -477,3 +477,29 @@ class TestMessageSigningFlows(FlowTest): FlowStep(MainMenuView), ] ) + + + def test_sign_message_invalid_qr_flow(self): + """ + Should clear `Controller.resume_main_flow` and redirect to ErrorView if an + invalid signmessage QR is scanned. + + The error view should then forward to MainMenuView. + """ + # Ensure message signing is enabled + self.settings.set_value(SettingsConstants.SETTING__MESSAGE_SIGNING, SettingsConstants.OPTION__ENABLED) + + def load_invalid_signmessage_qr(view: scan_views.ScanView): + view.decoder.add_data("this text will not make sense to the decoder") + + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN), + FlowStep(scan_views.ScanView, before_run=self.load_seed_into_decoder), # simulate read SeedQR; ret val is ignored + FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE), + FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.SIGN_MESSAGE), + FlowStep(scan_views.ScanView, before_run=load_invalid_signmessage_qr), # simulate read message QR; ret val is ignored + FlowStep(ErrorView), + FlowStep(MainMenuView), + ]) + + assert(self.controller.resume_main_flow is None) From 3a3ac12eaf9264f515b384befc055567ccdfa02e Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 28 Aug 2023 08:48:43 -0500 Subject: [PATCH 47/61] proper assert syntax --- tests/test_flows_seed.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_flows_seed.py b/tests/test_flows_seed.py index b4afd531..3aedef94 100644 --- a/tests/test_flows_seed.py +++ b/tests/test_flows_seed.py @@ -502,4 +502,4 @@ class TestMessageSigningFlows(FlowTest): FlowStep(MainMenuView), ]) - assert(self.controller.resume_main_flow is None) + assert self.controller.resume_main_flow is None From df9df468f8e4a37e3b7c9af2ecd2a0f7723ac4a2 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 28 Aug 2023 09:10:29 -0500 Subject: [PATCH 48/61] Additional edge padding on warning screens --- src/seedsigner/gui/screens/screen.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index d82567b3..b229857d 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -826,6 +826,7 @@ class LargeIconStatusScreen(ButtonListScreen): status_color: str = GUIConstants.SUCCESS_COLOR status_headline: str = "Success!" # The colored text under the large icon text: str = "" # The body text of the screen + text_edge_padding: int = GUIConstants.EDGE_PADDING button_data: list = None allow_text_overflow: bool = False @@ -861,7 +862,7 @@ class LargeIconStatusScreen(ButtonListScreen): height=self.buttons[0].screen_y - next_y, text=self.text, width=self.canvas_width, - edge_padding=GUIConstants.EDGE_PADDING, # Don't render all the way up to the far left/right edges + edge_padding=self.text_edge_padding, # Don't render all the way up to the far left/right edges screen_y=next_y, allow_text_overflow=self.allow_text_overflow, )) @@ -935,6 +936,7 @@ class WarningEdgesThread(BaseThread): @dataclass class WarningEdgesMixin: status_color: str = GUIConstants.WARNING_COLOR + text_edge_padding: int = 2 * GUIConstants.EDGE_PADDING def __post_init__(self): super().__post_init__() From 0883c8ebd577e06f28a342843dc844e15470c14d Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 28 Aug 2023 09:11:42 -0500 Subject: [PATCH 49/61] screenshot generator: dedicated section for errors; added generic ErrorView --- tests/screenshot_generator/generator.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/tests/screenshot_generator/generator.py b/tests/screenshot_generator/generator.py index 88a010ba..c50e719b 100644 --- a/tests/screenshot_generator/generator.py +++ b/tests/screenshot_generator/generator.py @@ -31,7 +31,7 @@ from seedsigner.models.seed import Seed from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition from seedsigner.views import (MainMenuView, PowerOptionsView, RestartView, NotYetImplementedView, UnhandledExceptionView, psbt_views, seed_views, settings_views, tools_views) -from seedsigner.views.view import NetworkMismatchErrorView, OptionDisabledView, PowerOffView, View +from seedsigner.views.view import ErrorView, NetworkMismatchErrorView, OptionDisabledView, PowerOffView, View from .utils import ScreenshotComplete, ScreenshotRenderer @@ -123,13 +123,7 @@ def test_generate_screenshots(target_locale): PowerOptionsView, RestartView, PowerOffView, - NotYetImplementedView, - (UnhandledExceptionView, dict(error=UnhandledExceptionViewFood)), - (settings_views.SettingsIngestSettingsQRView, dict(data="settings::v1 name=factory_reset")), - NetworkMismatchErrorView, - (OptionDisabledView, dict(settings_attr=SettingsConstants.SETTING__MESSAGE_SIGNING)), - - + (settings_views.SettingsIngestSettingsQRView, dict(data="settings::v1 name=Uncle_Jim's_noob_mode")), ], "Seed Views": [ seed_views.SeedsMenuView, @@ -222,6 +216,18 @@ def test_generate_screenshots(target_locale): #tools_views.ToolsAddressExplorerAddressView, ], "Settings Views": settings_views_list, + "Misc Error Views": [ + NotYetImplementedView, + (UnhandledExceptionView, dict(error=UnhandledExceptionViewFood)), + NetworkMismatchErrorView, + (OptionDisabledView, dict(settings_attr=SettingsConstants.SETTING__MESSAGE_SIGNING)), + (ErrorView, dict( + title="Error", + status_headline="Unknown QR Type", + text="QRCode is invalid or is a data format not yet supported.", + button_text="Back", + )), + ], } readme = f"""# SeedSigner Screenshots\n""" From 554657fa758cc0609b8a9e9272743247ddc59eb6 Mon Sep 17 00:00:00 2001 From: Conrad Donovan <135184930+conraddonovan16@users.noreply.github.com> Date: Mon, 28 Aug 2023 14:21:43 +0000 Subject: [PATCH 50/61] remove parentheses from assert statements --- src/pyzbar | 1 + src/seedsigner/helpers/ur2/bytewords.py | 4 +-- .../helpers/ur2/fountain_decoder.py | 2 +- .../helpers/ur2/fountain_encoder.py | 10 +++---- src/seedsigner/helpers/ur2/random_sampler.py | 4 +-- src/seedsigner/helpers/ur2/utils.py | 2 +- tests/test_embit_utils.py | 28 +++++++++---------- tests/test_settings.py | 22 +++++++-------- tests/test_settingsqr_decoder.py | 8 +++--- 9 files changed, 41 insertions(+), 40 deletions(-) create mode 160000 src/pyzbar diff --git a/src/pyzbar b/src/pyzbar new file mode 160000 index 00000000..c3c23782 --- /dev/null +++ b/src/pyzbar @@ -0,0 +1 @@ +Subproject commit c3c237821c6a20b17953efe59b90df0b514a1c03 diff --git a/src/seedsigner/helpers/ur2/bytewords.py b/src/seedsigner/helpers/ur2/bytewords.py index b7e8432a..f93b0cef 100644 --- a/src/seedsigner/helpers/ur2/bytewords.py +++ b/src/seedsigner/helpers/ur2/bytewords.py @@ -126,7 +126,7 @@ class Bytewords: elif style == Bytewords_Style_minimal: return encode_minimal(bytes) else: - assert(False) + assert False @staticmethod def decode(style, str): @@ -137,4 +137,4 @@ class Bytewords: elif style == Bytewords_Style_minimal: return decode(str, 0, 2) else: - assert(False) + assert False diff --git a/src/seedsigner/helpers/ur2/fountain_decoder.py b/src/seedsigner/helpers/ur2/fountain_decoder.py index c0b3f873..69eb64e3 100644 --- a/src/seedsigner/helpers/ur2/fountain_decoder.py +++ b/src/seedsigner/helpers/ur2/fountain_decoder.py @@ -255,7 +255,7 @@ class FountainDecoder: elif self.is_failure(): return 'Exception: {}'.format(self.result) else: - assert(False) + assert False def print_part(self, p): print('part indexes: {}'.format(self.indexes_to_string(p.indexes))) diff --git a/src/seedsigner/helpers/ur2/fountain_encoder.py b/src/seedsigner/helpers/ur2/fountain_encoder.py index d539e460..ac43bb62 100644 --- a/src/seedsigner/helpers/ur2/fountain_encoder.py +++ b/src/seedsigner/helpers/ur2/fountain_encoder.py @@ -84,7 +84,7 @@ class Part: class FountainEncoder: def __init__(self, message, max_fragment_len, first_seq_num = 0, min_fragment_len = 10): - assert(len(message) <= MAX_UINT32) + assert len(message) <= MAX_UINT32 self.message_len = len(message) self.checksum = crc32_int(message) self.fragment_len = FountainEncoder.find_nominal_fragment_length(self.message_len, min_fragment_len, max_fragment_len) @@ -93,9 +93,9 @@ class FountainEncoder: @staticmethod def find_nominal_fragment_length(message_len, min_fragment_len, max_fragment_len): - assert(message_len > 0) - assert(min_fragment_len > 0) - assert(max_fragment_len >= min_fragment_len) + assert message_len > 0 + assert min_fragment_len > 0 + assert max_fragment_len >= min_fragment_len max_fragment_count = message_len // min_fragment_len fragment_len = None @@ -104,7 +104,7 @@ class FountainEncoder: if fragment_len <= max_fragment_len: break - assert(fragment_len != None) + assert fragment_len != None return fragment_len diff --git a/src/seedsigner/helpers/ur2/random_sampler.py b/src/seedsigner/helpers/ur2/random_sampler.py index 087789e8..d1b80411 100644 --- a/src/seedsigner/helpers/ur2/random_sampler.py +++ b/src/seedsigner/helpers/ur2/random_sampler.py @@ -9,11 +9,11 @@ class RandomSampler: def __init__(self, probs): for p in probs: - assert(p > 0) + assert p > 0 # Normalize given probabilities total = sum(probs) - assert(total > 0) + assert total > 0 n = len(probs) diff --git a/src/seedsigner/helpers/ur2/utils.py b/src/seedsigner/helpers/ur2/utils.py index 4e88c363..31a0dfe1 100644 --- a/src/seedsigner/helpers/ur2/utils.py +++ b/src/seedsigner/helpers/ur2/utils.py @@ -57,7 +57,7 @@ def join_bytes(list_of_ba): def xor_into(target, source): count = len(target) - assert(count == len(source)) # Must be the same length + assert count == len(source) # Must be the same length for i in range(count): target[i] ^= source[i] diff --git a/tests/test_embit_utils.py b/tests/test_embit_utils.py index 1b7bfaa8..562d6aa2 100644 --- a/tests/test_embit_utils.py +++ b/tests/test_embit_utils.py @@ -331,12 +331,12 @@ def test_parse_derivation_path(): derivation_path = "m/84'/0'/0'/0/0" result = embit_utils.parse_derivation_path(derivation_path) - assert(result["script_type"] == SC.NATIVE_SEGWIT) - assert(result["network"] == SC.MAINNET) + assert result["script_type"] == SC.NATIVE_SEGWIT + assert result["network"] == SC.MAINNET result = embit_utils.parse_derivation_path(derivation_path.replace("'", "h")) - assert(result["script_type"] == SC.NATIVE_SEGWIT) - assert(result["network"] == SC.MAINNET) + assert result["script_type"] == SC.NATIVE_SEGWIT + assert result["network"] == SC.MAINNET # Now exhaustively test supported permutations vectors_args = { @@ -383,20 +383,20 @@ def test_parse_derivation_path(): actual_result = embit_utils.parse_derivation_path(derivation_path) if expected_result[0] == SC.MAINNET: - assert(actual_result["network"] == expected_result[0]) - assert(actual_result["clean_match"] is True) + assert actual_result["network"] == expected_result[0] + assert actual_result["clean_match"] is True elif expected_result[0] is None: - assert(actual_result["network"] is None) - assert(actual_result["clean_match"] is False) + assert actual_result["network"] is None + assert actual_result["clean_match"] is False else: # Testnet and regtest are returned as a list since the parser can't tell which is intended - assert(expected_result[0] in actual_result["network"]) - assert(actual_result["clean_match"] is True) + assert expected_result[0] in actual_result["network"] + assert actual_result["clean_match"] is True - assert(actual_result["script_type"] == expected_result[1]) - assert(actual_result["is_change"] == expected_result[2]) + assert actual_result["script_type"] == expected_result[1] + assert actual_result["is_change"] == expected_result[2] if len(expected_result) == 4: - assert(actual_result["index"] == expected_result[3]) + assert actual_result["index"] == expected_result[3] else: - assert(actual_result["index"] == int(derivation_path.split("/")[-1])) + assert actual_result["index"] == int(derivation_path.split("/")[-1]) diff --git a/tests/test_settings.py b/tests/test_settings.py index 38ca68c1..76026086 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -37,17 +37,17 @@ class TestSettings(BaseTest): # Now parse the settingsqr_data config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data) - assert(config_name == settings_name) + assert config_name == settings_name self.settings.update(new_settings=settings_update_dict) # Now verify that the settings were updated correctly - assert(self.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) == SettingsConstants.OPTION__ENABLED) - assert(self.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__ENABLED) + assert self.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) == SettingsConstants.OPTION__ENABLED + assert self.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__ENABLED coordinators = self.settings.get_value(SettingsConstants.SETTING__COORDINATORS) - assert(SettingsConstants.COORDINATOR__BLUE_WALLET not in coordinators) - assert(SettingsConstants.COORDINATOR__SPARROW in coordinators) - assert(SettingsConstants.COORDINATOR__SPECTER_DESKTOP in coordinators) + assert SettingsConstants.COORDINATOR__BLUE_WALLET not in coordinators + assert SettingsConstants.COORDINATOR__SPARROW in coordinators + assert SettingsConstants.COORDINATOR__SPECTER_DESKTOP in coordinators def test_settingsqr_version(self): @@ -61,7 +61,7 @@ class TestSettings(BaseTest): settingsqr_data = "settings::v2 name=Foo" with pytest.raises(InvalidSettingsQRData) as e: Settings.parse_settingsqr(settingsqr_data) - assert("Unsupported SettingsQR version" in str(e.value)) + assert "Unsupported SettingsQR version" in str(e.value) # Should also fail if version omitted settingsqr_data = "settings name=Foo" @@ -79,8 +79,8 @@ class TestSettings(BaseTest): settingsqr_data = "settings::v1 name=Foo favorite_food=bacon xpub_export=D" config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data) - assert("favorite_food" not in settings_update_dict) - assert("xpub_export" in settings_update_dict) + assert "favorite_food" not in settings_update_dict + assert "xpub_export" in settings_update_dict # Accepts update with no Exceptions self.settings.update(new_settings=settings_update_dict) @@ -91,7 +91,7 @@ class TestSettings(BaseTest): settingsqr_data = "settings::v1 name=Foo xpub_export=Yep" with pytest.raises(InvalidSettingsQRData) as e: Settings.parse_settingsqr(settingsqr_data) - assert("xpub_export" in str(e.value)) + assert "xpub_export" in str(e.value) def test_settingsqr_parses_line_break_separators(self): @@ -99,7 +99,7 @@ class TestSettings(BaseTest): settingsqr_data = "settings::v1\nname=Foo\nsigs=ss,ms\nscripts=nat,nes,tr\nxpub_export=E\n" config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data) - assert(len(settings_update_dict.keys()) == 3) + assert len(settings_update_dict.keys()) == 3 # Accepts update with no Exceptions self.settings.update(new_settings=settings_update_dict) diff --git a/tests/test_settingsqr_decoder.py b/tests/test_settingsqr_decoder.py index ac2720b3..bb124c4f 100644 --- a/tests/test_settingsqr_decoder.py +++ b/tests/test_settingsqr_decoder.py @@ -14,11 +14,11 @@ class TestSettingsQRDecoder: # Now parse the settings_qr_str decoder = DecodeQR() status = decoder.add_data(settings_qr_str) - assert(decoder.is_settings) - assert(status == DecodeQRStatus.COMPLETE) + assert decoder.is_settings + assert status == DecodeQRStatus.COMPLETE data = decoder.get_settings_data() - assert(data == settings_qr_str) + assert data == settings_qr_str def test_settingsqr_version(self): @@ -26,4 +26,4 @@ class TestSettingsQRDecoder: settings_qr_str = "name=Foo" decoder = DecodeQR() status = decoder.add_data(settings_qr_str) - assert(status == DecodeQRStatus.INVALID) + assert status == DecodeQRStatus.INVALID From 97029f429bd27a864979cb680438638633359f87 Mon Sep 17 00:00:00 2001 From: Conrad Donovan <135184930+conraddonovan16@users.noreply.github.com> Date: Mon, 28 Aug 2023 14:26:23 +0000 Subject: [PATCH 51/61] removing extra space --- src/seedsigner/helpers/ur2/bytewords.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/seedsigner/helpers/ur2/bytewords.py b/src/seedsigner/helpers/ur2/bytewords.py index f93b0cef..a1204ed2 100644 --- a/src/seedsigner/helpers/ur2/bytewords.py +++ b/src/seedsigner/helpers/ur2/bytewords.py @@ -126,7 +126,7 @@ class Bytewords: elif style == Bytewords_Style_minimal: return encode_minimal(bytes) else: - assert False + assert False @staticmethod def decode(style, str): From 3011211cb95654853fa823e37c8e15dc79ad12c1 Mon Sep 17 00:00:00 2001 From: Conrad Donovan <135184930+conraddonovan16@users.noreply.github.com> Date: Wed, 30 Aug 2023 16:42:41 +0000 Subject: [PATCH 52/61] removing emulator directory --- src/pyzbar | 1 - 1 file changed, 1 deletion(-) delete mode 160000 src/pyzbar diff --git a/src/pyzbar b/src/pyzbar deleted file mode 160000 index c3c23782..00000000 --- a/src/pyzbar +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c3c237821c6a20b17953efe59b90df0b514a1c03 From 01461ccce49a3f7c633c3180f1c5797624c134dd Mon Sep 17 00:00:00 2001 From: kdmukai Date: Wed, 30 Aug 2023 23:22:48 -0500 Subject: [PATCH 53/61] Add SettingsQR update info * Move SettingsQR confirmation screen out of scan_screens.py --- src/seedsigner/gui/screens/scan_screens.py | 33 --------------- .../gui/screens/settings_screens.py | 41 +++++++++++++++++++ src/seedsigner/views/settings_views.py | 13 +++++- 3 files changed, 52 insertions(+), 35 deletions(-) diff --git a/src/seedsigner/gui/screens/scan_screens.py b/src/seedsigner/gui/screens/scan_screens.py index 26a53bae..13d3dcf7 100644 --- a/src/seedsigner/gui/screens/scan_screens.py +++ b/src/seedsigner/gui/screens/scan_screens.py @@ -174,36 +174,3 @@ class ScanScreen(BaseScreen): self.camera.stop_video_stream_mode() break - - -@dataclass -class SettingsUpdatedScreen(ButtonListScreen): - config_name: str = None - title: str = "Settings QR" - is_bottom_list: bool = True - - def __post_init__(self): - # Customize defaults - self.button_data = ["Home"] - self.show_back_button = False - - super().__post_init__() - - start_y = self.top_nav.height + 20 - if self.config_name: - self.config_name_textarea = TextArea( - text=f'"{self.config_name}"', - is_text_centered=True, - auto_line_break=True, - screen_y=start_y - ) - self.components.append(self.config_name_textarea) - start_y = self.config_name_textarea.screen_y + 50 - - self.components.append(TextArea( - text="Settings imported successfully!", - is_text_centered=True, - auto_line_break=True, - screen_y=start_y - )) - diff --git a/src/seedsigner/gui/screens/settings_screens.py b/src/seedsigner/gui/screens/settings_screens.py index c09f1e27..548284ad 100644 --- a/src/seedsigner/gui/screens/settings_screens.py +++ b/src/seedsigner/gui/screens/settings_screens.py @@ -296,3 +296,44 @@ class DonateScreen(BaseTopNavScreen): supersampling_factor=1, screen_y=self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING )) + + + +@dataclass +class SettingsQRConfirmationScreen(ButtonListScreen): + config_name: str = None + title: str = "Settings QR" + is_bottom_list: bool = True + + def __post_init__(self): + from seedsigner.hardware.microsd import MicroSD + from seedsigner.models.settings import Settings, SettingsConstants + + # Customize defaults + self.button_data = ["Home"] + self.show_back_button = False + super().__post_init__() + + settings = Settings.get_instance() + if MicroSD.get_instance().is_inserted and settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__ENABLED: + status_message = "Persistent Settings enabled. Settings saved to SD card." + else: + status_message = "Settings updated in temporary memory" + + start_y = self.top_nav.height + 20 + if self.config_name: + self.config_name_textarea = TextArea( + text=f'"{self.config_name}"', + is_text_centered=True, + auto_line_break=True, + screen_y=start_y + ) + self.components.append(self.config_name_textarea) + start_y = self.config_name_textarea.screen_y + 50 + + self.components.append(TextArea( + text=status_message, + is_text_centered=True, + auto_line_break=True, + screen_y=start_y + )) diff --git a/src/seedsigner/views/settings_views.py b/src/seedsigner/views/settings_views.py index ca9c36dc..17a98726 100644 --- a/src/seedsigner/views/settings_views.py +++ b/src/seedsigner/views/settings_views.py @@ -1,5 +1,6 @@ import logging from seedsigner.gui.components import SeedSignerIconConstants +from seedsigner.hardware.microsd import MicroSD from .view import View, Destination, MainMenuView @@ -195,13 +196,21 @@ class SettingsIngestSettingsQRView(View): # May raise an Exception which will bubble up to the Controller to display to the # user. self.config_name, settings_update_dict = Settings.parse_settingsqr(data) + + persistent_settings = settings_update_dict.get(SettingsConstants.SETTING__PERSISTENT_SETTINGS) + if persistent_settings == SettingsConstants.OPTION__ENABLED and not MicroSD.get_instance().is_inserted: + # SettingsQR wants to enable persistent settings, but no MicroSD is inserted. + # For the sake of simplicity we just ignore that setting for now. + # TODO: Can consider a warning screen instead that gives the user some options. + del settings_update_dict[SettingsConstants.SETTING__PERSISTENT_SETTINGS] + self.settings.update(settings_update_dict) def run(self): - from seedsigner.gui.screens.scan_screens import SettingsUpdatedScreen + from seedsigner.gui.screens.settings_screens import SettingsQRConfirmationScreen self.run_screen( - SettingsUpdatedScreen, + SettingsQRConfirmationScreen, config_name=self.config_name ) From afcc042052b11e61a2c11ac0d2b89645037bbf9d Mon Sep 17 00:00:00 2001 From: kdmukai Date: Wed, 30 Aug 2023 23:24:09 -0500 Subject: [PATCH 54/61] SettingsQR tests; mocked `MicroSD.is_inserted` --- tests/base.py | 24 +++++++++++++-- tests/test_flows_settings.py | 58 ++++++++++++++++++++++++++++++++++-- tests/test_settings.py | 1 + 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/tests/base.py b/tests/base.py index fadf6ff7..e6ac5621 100644 --- a/tests/base.py +++ b/tests/base.py @@ -1,6 +1,6 @@ import sys from dataclasses import dataclass -from mock import MagicMock, patch +from mock import MagicMock, Mock, patch from typing import Callable # Prevent importing modules w/Raspi hardware dependencies. @@ -11,17 +11,27 @@ sys.modules['seedsigner.gui.toast'] = MagicMock() sys.modules['seedsigner.views.screensaver'] = MagicMock() sys.modules['seedsigner.hardware.buttons'] = MagicMock() sys.modules['seedsigner.hardware.camera'] = MagicMock() -sys.modules['seedsigner.hardware.microsd'] = MagicMock() from seedsigner.controller import Controller, FlowBasedTestException, StopFlowBasedTest from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, RET_CODE__POWER_BUTTON +from seedsigner.hardware.microsd import MicroSD from seedsigner.models.settings import Settings from seedsigner.views.view import Destination, MainMenuView, View + class BaseTest: + class MockMicroSD(Mock): + """ + A test suite-friendly replacement for `MicroSD` that gives a test explicit + control over the reported state of the SD card. + """ + # Tests are free to directly manipulate this attribute as needed + is_inserted: bool = True + + @classmethod def setup_class(cls): # Ensure there are no on-disk artifacts after running tests. @@ -30,6 +40,13 @@ class BaseTest: # Mock out the loading screen so it can't spawn. View classes must import locally! patch('seedsigner.gui.screens.screen.LoadingScreenThread').start() + # Instantiate the mocked MicroSD; hold on to the instance so tests can manipulate + # it later. + cls.mock_microsd = BaseTest.MockMicroSD() + + # And mock it over `MicroSD`'s instance + MicroSD.get_instance = Mock(return_value=cls.mock_microsd) + @classmethod def teardown_class(cls): @@ -62,11 +79,12 @@ class BaseTest: def setup_method(self): - """ Guarantee a clean/default Controller and Settings state for each test case """ + """ Guarantee a clean/default Controller, Settings, & MicroSD state for each test case """ BaseTest.reset_controller() BaseTest.reset_settings() self.controller = Controller.get_instance() self.settings = Settings.get_instance() + self.mock_microsd.is_inserted = True def teardown_method(self): diff --git a/tests/test_flows_settings.py b/tests/test_flows_settings.py index 1fb394a2..d9a62574 100644 --- a/tests/test_flows_settings.py +++ b/tests/test_flows_settings.py @@ -1,4 +1,5 @@ import os +from typing import Callable # Must import test base before the Controller from base import FlowTest, FlowStep @@ -6,13 +7,13 @@ from base import FlowTest, FlowStep from seedsigner.models.settings import Settings from seedsigner.models.settings_definition import SettingsDefinition, SettingsConstants from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON +from seedsigner.hardware.microsd import MicroSD from seedsigner.views.view import MainMenuView -from seedsigner.views import settings_views +from seedsigner.views import scan_views, settings_views class TestSettingsFlows(FlowTest): - def test_persistent_settings(self): """ Basic flow from MainMenuView to enable/disable persistent settings """ # Which option are we testing? @@ -67,3 +68,56 @@ class TestSettingsFlows(FlowTest): FlowStep(settings_views.DonateView), FlowStep(settings_views.SettingsMenuView), ]) + + + def test_settingsqr(self): + """ + Scanning a SettingsQR should present the success screen and then return to + MainMenuView. + """ + def load_persistent_settingsqr_into_decoder(view: scan_views.ScanView): + settingsqr_data_persistent: str = "settings::v1 name=Total_noob_mode persistent=E coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E" + view.decoder.add_data(settingsqr_data_persistent) + + def load_not_persistent_settingsqr_into_decoder(view: scan_views.ScanView): + settingsqr_data_not_persistent: str = "settings::v1 name=Ephemeral_noob_mode persistent=D coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E" + view.decoder.add_data(settingsqr_data_not_persistent) + + def _run_test(initial_setting_state: str, load_settingsqr_into_decoder: Callable, expected_setting_state: str): + self.settings.set_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS, initial_setting_state) + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN), + FlowStep(scan_views.ScanView, before_run=load_settingsqr_into_decoder), # simulate read message QR; ret val is ignored + FlowStep(settings_views.SettingsIngestSettingsQRView), # ret val is ignored + FlowStep(MainMenuView), + ]) + + assert self.settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == expected_setting_state + + + # First load a SettingsQR that enables persistent settings + self.mock_microsd.is_inserted = True + assert MicroSD.get_instance().is_inserted is True + + _run_test( + initial_setting_state=SettingsConstants.OPTION__DISABLED, + load_settingsqr_into_decoder=load_persistent_settingsqr_into_decoder, + expected_setting_state=SettingsConstants.OPTION__ENABLED + ) + + # Then one that disables it + _run_test( + initial_setting_state=SettingsConstants.OPTION__ENABLED, + load_settingsqr_into_decoder=load_not_persistent_settingsqr_into_decoder, + expected_setting_state=SettingsConstants.OPTION__DISABLED + ) + + # Now try to enable persistent settings when the SD card is not inserted + self.mock_microsd.is_inserted = False + assert MicroSD.get_instance().is_inserted is False + + _run_test( + initial_setting_state=SettingsConstants.OPTION__DISABLED, + load_settingsqr_into_decoder=load_persistent_settingsqr_into_decoder, + expected_setting_state=SettingsConstants.OPTION__DISABLED + ) diff --git a/tests/test_settings.py b/tests/test_settings.py index 76026086..94ac626a 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -8,6 +8,7 @@ from seedsigner.models.settings_definition import SettingsConstants class TestSettings(BaseTest): @classmethod def setup_class(cls): + super().setup_class() cls.settings = Settings.get_instance() From 68bd13147eadc7d084cdfa5cb4358c41ef16b93d Mon Sep 17 00:00:00 2001 From: kdmukai Date: Wed, 30 Aug 2023 23:25:10 -0500 Subject: [PATCH 55/61] SettingsQR status update screens added to screenshot generator --- tests/screenshot_generator/generator.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/screenshot_generator/generator.py b/tests/screenshot_generator/generator.py index c50e719b..83a58135 100644 --- a/tests/screenshot_generator/generator.py +++ b/tests/screenshot_generator/generator.py @@ -110,10 +110,11 @@ def test_generate_screenshots(target_locale): continue settings_views_list.append((settings_views.SettingsEntryUpdateSelectionView, dict(attr_name=settings_entry.attr_name), f"SettingsEntryUpdateSelectionView_{settings_entry.attr_name}")) - settings_views_list.append(settings_views.IOTestView) - settings_views_list.append(settings_views.DonateView) + settingsqr_data_persistent = "settings::v1 name=Total_noob_mode persistent=E coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E" + settingsqr_data_not_persistent = "settings::v1 name=Ephemeral_noob_mode persistent=D coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E" + screenshot_sections = { "Main Menu Views": [ MainMenuView, @@ -123,7 +124,6 @@ def test_generate_screenshots(target_locale): PowerOptionsView, RestartView, PowerOffView, - (settings_views.SettingsIngestSettingsQRView, dict(data="settings::v1 name=Uncle_Jim's_noob_mode")), ], "Seed Views": [ seed_views.SeedsMenuView, @@ -215,7 +215,12 @@ def test_generate_screenshots(target_locale): tools_views.ToolsAddressExplorerAddressListView, #tools_views.ToolsAddressExplorerAddressView, ], - "Settings Views": settings_views_list, + "Settings Views": settings_views_list + [ + settings_views.IOTestView, + settings_views.DonateView, + (settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_persistent), "SettingsIngestSettingsQRView_persistent"), + (settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_not_persistent), "SettingsIngestSettingsQRView_not_persistent"), + ], "Misc Error Views": [ NotYetImplementedView, (UnhandledExceptionView, dict(error=UnhandledExceptionViewFood)), @@ -227,7 +232,7 @@ def test_generate_screenshots(target_locale): text="QRCode is invalid or is a data format not yet supported.", button_text="Back", )), - ], + ] } readme = f"""# SeedSigner Screenshots\n""" From a29e348b30dc9074f26ae0dd7aac429a7758c712 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Thu, 31 Aug 2023 06:55:11 -0500 Subject: [PATCH 56/61] Make Screen dumb again --- src/seedsigner/gui/screens/settings_screens.py | 12 ++---------- src/seedsigner/views/settings_views.py | 10 ++++++++-- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/seedsigner/gui/screens/settings_screens.py b/src/seedsigner/gui/screens/settings_screens.py index 548284ad..55fa5dd0 100644 --- a/src/seedsigner/gui/screens/settings_screens.py +++ b/src/seedsigner/gui/screens/settings_screens.py @@ -303,23 +303,15 @@ class DonateScreen(BaseTopNavScreen): class SettingsQRConfirmationScreen(ButtonListScreen): config_name: str = None title: str = "Settings QR" + status_message: str = "Settings updated..." is_bottom_list: bool = True def __post_init__(self): - from seedsigner.hardware.microsd import MicroSD - from seedsigner.models.settings import Settings, SettingsConstants - # Customize defaults self.button_data = ["Home"] self.show_back_button = False super().__post_init__() - settings = Settings.get_instance() - if MicroSD.get_instance().is_inserted and settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__ENABLED: - status_message = "Persistent Settings enabled. Settings saved to SD card." - else: - status_message = "Settings updated in temporary memory" - start_y = self.top_nav.height + 20 if self.config_name: self.config_name_textarea = TextArea( @@ -332,7 +324,7 @@ class SettingsQRConfirmationScreen(ButtonListScreen): start_y = self.config_name_textarea.screen_y + 50 self.components.append(TextArea( - text=status_message, + text=self.status_message, is_text_centered=True, auto_line_break=True, screen_y=start_y diff --git a/src/seedsigner/views/settings_views.py b/src/seedsigner/views/settings_views.py index 17a98726..8eca039c 100644 --- a/src/seedsigner/views/settings_views.py +++ b/src/seedsigner/views/settings_views.py @@ -205,13 +205,19 @@ class SettingsIngestSettingsQRView(View): del settings_update_dict[SettingsConstants.SETTING__PERSISTENT_SETTINGS] self.settings.update(settings_update_dict) - + + if MicroSD.get_instance().is_inserted and self.settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__ENABLED: + self.status_message = "Persistent Settings enabled. Settings saved to SD card." + else: + self.status_message = "Settings updated in temporary memory" + def run(self): from seedsigner.gui.screens.settings_screens import SettingsQRConfirmationScreen self.run_screen( SettingsQRConfirmationScreen, - config_name=self.config_name + config_name=self.config_name, + status_message=self.status_message, ) # Only one exit point From 892c3538a478efcf9bace5afd848aaa6119c3575 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Thu, 31 Aug 2023 06:58:10 -0500 Subject: [PATCH 57/61] More detailed comment --- tests/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/base.py b/tests/base.py index e6ac5621..496e1b86 100644 --- a/tests/base.py +++ b/tests/base.py @@ -28,7 +28,8 @@ class BaseTest: A test suite-friendly replacement for `MicroSD` that gives a test explicit control over the reported state of the SD card. """ - # Tests are free to directly manipulate this attribute as needed + # Tests are free to directly manipulate this attribute as needed (it's reset to + # True before each test in `BaseTest.setup_method()`). is_inserted: bool = True From d810d12d64be659feb14ba6c7427f847f92cb541 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Thu, 31 Aug 2023 18:44:40 -0500 Subject: [PATCH 58/61] bugfix; caught by @jdlcdl --- src/seedsigner/models/settings.py | 6 ++++++ src/seedsigner/views/settings_views.py | 7 ------- tests/test_flows_settings.py | 15 +++++++++++++++ 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/seedsigner/models/settings.py b/src/seedsigner/models/settings.py index 7b5f4602..f0b1c97f 100644 --- a/src/seedsigner/models/settings.py +++ b/src/seedsigner/models/settings.py @@ -87,6 +87,12 @@ class Settings(Singleton): values = value for v in values: if v not in [opt[0] for opt in settings_entry.selection_options]: + if settings_entry.attr_name == SettingsConstants.SETTING__PERSISTENT_SETTINGS and v == SettingsConstants.OPTION__ENABLED: + # Special case: trying to enable Persistent Settings when + # DISABLED is the only option allowed (because the SD card is not + # inserted. Explicitly set to DISABLED. + value = SettingsConstants.OPTION__DISABLED + break raise InvalidSettingsQRData(f"""{abbreviated_name} = '{v}' is not valid""") updated_settings[settings_entry.attr_name] = value diff --git a/src/seedsigner/views/settings_views.py b/src/seedsigner/views/settings_views.py index 8eca039c..e2a742ca 100644 --- a/src/seedsigner/views/settings_views.py +++ b/src/seedsigner/views/settings_views.py @@ -196,13 +196,6 @@ class SettingsIngestSettingsQRView(View): # May raise an Exception which will bubble up to the Controller to display to the # user. self.config_name, settings_update_dict = Settings.parse_settingsqr(data) - - persistent_settings = settings_update_dict.get(SettingsConstants.SETTING__PERSISTENT_SETTINGS) - if persistent_settings == SettingsConstants.OPTION__ENABLED and not MicroSD.get_instance().is_inserted: - # SettingsQR wants to enable persistent settings, but no MicroSD is inserted. - # For the sake of simplicity we just ignore that setting for now. - # TODO: Can consider a warning screen instead that gives the user some options. - del settings_update_dict[SettingsConstants.SETTING__PERSISTENT_SETTINGS] self.settings.update(settings_update_dict) diff --git a/tests/test_flows_settings.py b/tests/test_flows_settings.py index d9a62574..a6eb34d9 100644 --- a/tests/test_flows_settings.py +++ b/tests/test_flows_settings.py @@ -1,6 +1,8 @@ import os from typing import Callable +from mock import PropertyMock, patch + # Must import test base before the Controller from base import FlowTest, FlowStep @@ -116,6 +118,19 @@ class TestSettingsFlows(FlowTest): self.mock_microsd.is_inserted = False assert MicroSD.get_instance().is_inserted is False + # Have to jump through some hoops to completely simulate the SD card being + # removed; we need Settings to restrict Persistent Settings to only allow + # DISABLED. + with patch('seedsigner.models.settings.Settings.HOSTNAME', new_callable=PropertyMock) as mock_hostname: + # Must identify itself as SeedSigner OS to trigger the SD card removal logic + mock_hostname.return_value = Settings.SEEDSIGNER_OS + Settings.handle_microsd_state_change(MicroSD.ACTION__REMOVED) + + selection_options = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__PERSISTENT_SETTINGS).selection_options + assert len(selection_options) == 1 + assert selection_options[0][0] == SettingsConstants.OPTION__DISABLED + assert self.settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__DISABLED + _run_test( initial_setting_state=SettingsConstants.OPTION__DISABLED, load_settingsqr_into_decoder=load_persistent_settingsqr_into_decoder, From 4572ed373ed817f3e93dd647c9f3b09aa58d0a89 Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Mon, 4 Sep 2023 23:51:44 -0400 Subject: [PATCH 59/61] Update controller.py --- src/seedsigner/controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index c21efe9c..30a50076 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -93,7 +93,7 @@ class Controller(Singleton): rather than at the top in order avoid circular imports. """ - VERSION = "0.7.0-rc1" + VERSION = "0.7.0" # Declare class member vars with type hints to enable richer IDE support throughout # the code. From 668f07b59622c97c347507c3eb191d9a28bf2dde Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Tue, 12 Sep 2023 08:59:25 -0400 Subject: [PATCH 60/61] Update main README.md to show 0.7.0 as the latests release and link to the correct release files. --- README.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 7ef232ce..f32a39ec 100644 --- a/README.md +++ b/README.md @@ -82,27 +82,27 @@ Notes: ## A Special Note On Minimizing Trust As is the nature of pre-packaged software downloads, downloading and using the prepared SeedSigner release images means implicitly placing trust in the individual preparing those images; in our project the release images are prepared and signed by the eponymous creator of the project, SeedSigner "the person". That individual is additionally the only person in possession of the PGP keys that are used to sign the release images. -However, one of the many advantages of the open source software model is that the need for this kind of trust can be negated by our users' ability to (1) review the project's source code and (2) assemble the operating image necessary to use the software themselves. From our project's inception, instructions to build a SeedSigner operating image (using precisely the same process that is used to create the prepared release images) have been made availabile. We have put a lot of thought and work into making these instructions easy to understand and follow, even for less technical users. These instructions can be found [here](docs/manual_installation.md). +However, one of the many advantages of the open source software model is that the need for this kind of trust can be negated by our users' ability to (1) review the project's source code and (2) assemble the operating image necessary to use the software themselves. From our project's inception, instructions to build a SeedSigner operating image (using precisely the same process that is used to create the prepared release images) have been made available. We have put a lot of thought and work into making these instructions easy to understand and follow, even for less technical users. These instructions can be found [here](docs/manual_installation.md). ## Downloading the Software -Download the current Version (0.6.0) software image that is compatible with your Raspberry Pi Hardware. The Pi Zero 1.3 is the most common and recommended board. +Download the current Version (0.7.0) software image that is compatible with your Raspberry Pi Hardware. The Pi Zero 1.3 is the most common and recommended board. | Board | Download Image Link/Name | | --------------------- | --------------------------------- | -|**[Raspberry Pi Zero 1.3](https://www.raspberrypi.com/products/raspberry-pi-zero/)** |[`seedsigner_os.0.6.0.pi0.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner_os.0.6.0.pi0.img) | -|[Raspberry Pi Zero W](https://www.raspberrypi.com/products/raspberry-pi-zero-w/) |[`seedsigner_os.0.6.0.pi0.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner_os.0.6.0.pi0.img) | -|[Raspberry Pi Zero 2 W](https://www.raspberrypi.com/products/raspberry-pi-zero-2-w/) |[`seedsigner_os.0.6.0.pi02w.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner_os.0.6.0.pi02w.img) | -|[Raspberry Pi 2 Model B](https://www.raspberrypi.com/products/raspberry-pi-2-model-b/) |[`seedsigner_os.0.6.0.pi2.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner_os.0.6.0.pi2.img) | -|[Raspberry Pi 3 Model B](https://www.raspberrypi.com/products/raspberry-pi-3-model-b/) |[`seedsigner_os.0.6.0.pi02w.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner_os.0.6.0.pi02w.img) | -|[Raspberry Pi 4 Model B](https://www.raspberrypi.com/products/raspberry-pi-4-model-b/) |[`seedsigner_os.0.6.0.pi4.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner_os.0.6.0.pi4.img) | -|[Raspberry Pi 400](https://www.raspberrypi.com/products/raspberry-pi-400-unit/) |[`seedsigner_os.0.6.0.pi4.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner_os.0.6.0.pi4.img) | +|**[Raspberry Pi Zero 1.3](https://www.raspberrypi.com/products/raspberry-pi-zero/)** |[`seedsigner_os.0.7.0.pi0.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.7.0/seedsigner_os.0.7.0.pi0.img) | +|[Raspberry Pi Zero W](https://www.raspberrypi.com/products/raspberry-pi-zero-w/) |[`seedsigner_os.0.7.0.pi0.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.7.0/seedsigner_os.0.7.0.pi0.img) | +|[Raspberry Pi Zero 2 W](https://www.raspberrypi.com/products/raspberry-pi-zero-2-w/) |[`seedsigner_os.0.7.0.pi02w.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.7.0/seedsigner_os.0.7.0.pi02w.img) | +|[Raspberry Pi 2 Model B](https://www.raspberrypi.com/products/raspberry-pi-2-model-b/) |[`seedsigner_os.0.7.0.pi2.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.7.0/seedsigner_os.0.7.0.pi2.img) | +|[Raspberry Pi 3 Model B](https://www.raspberrypi.com/products/raspberry-pi-3-model-b/) |[`seedsigner_os.0.7.0.pi02w.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.7.0/seedsigner_os.0.7.0.pi02w.img) | +|[Raspberry Pi 4 Model B](https://www.raspberrypi.com/products/raspberry-pi-4-model-b/) |[`seedsigner_os.0.7.0.pi4.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.7.0/seedsigner_os.0.7.0.pi4.img) | +|[Raspberry Pi 400](https://www.raspberrypi.com/products/raspberry-pi-400-unit/) |[`seedsigner_os.0.7.0.pi4.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.7.0/seedsigner_os.0.7.0.pi4.img) | Note: If you have physically removed the WiFi component from your board, you will still use the image file of the original(un-modified) hardware. (Our files are compiled/based on the *processor* architecture). Although it is better to spend a few minutes upfront to determine which specific Pi hardware/model you have, if you are still unsure which hardware you have, you can try using the pi0.img file. Making an incorrect choice here will not ruin your board, because this is software, not firmware. **also download** these 2 signature verification files to the same folder -[The Plaintext manifest file](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner.0.6.0.sha256) -[The Signature of the manifest file](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner.0.6.0.sha256.sig) +[The Plaintext manifest file](https://github.com/SeedSigner/seedsigner/releases/download/0.7.0/seedsigner.0.7.0.manifest.txt) +[The Signature of the manifest file](https://github.com/SeedSigner/seedsigner/releases/download/0.7.0/seedsigner.0.7.0.manifest.txt.sig) Users familiar with older versions of the SeedSigner software might be surprised with how fast their software downloads now are, because since version 0.6.0 the software image files are now 100x smaller! Each image file is now under 42 Megabytes so your downloads and verifications will be very quick now (and might even seem *too* quick)! @@ -131,7 +131,7 @@ The result should confirm that 1 key was *either* imported or updated. *Ignore* Next, you will run the *verify* command on the signature (.sig) file. (*Verify* must be run from inside the same folder that you downloaded the files into earlier. The `*`'s in this command will auto-fill the version from your current folder, so it should be copied and pasted as-is.) ``` -gpg --verify seedsigner.0.6.*.sha256.sig +gpg --verify seedsigner.0.7.*.manifest.sig ``` When the verify command completes successfully, it should display output like this: @@ -201,21 +201,21 @@ Now that you have confirmed that you do have the real SeedSigner Project's Publi **On Linux or OSX:** Run this command ``` -shasum -a 256 --ignore-missing --check seedsigner.0.6.*.sha256 +shasum -a 256 --ignore-missing --check seedsigner.0.7.*.manifest.txt ``` **On Windows (inside Powershell):** Run this command ``` -CertUtil -hashfile seedsigner_os.0.6.0.Insert_Your_Pi_Models_binary_here_For_Example_pi02w.img SHA256 +CertUtil -hashfile seedsigner_os.0.7.0.Insert_Your_Pi_Models_binary_here_For_Example_pi02w.img SHA256 ``` On Windows, you must then manually compare the resulting file hash value to the corresponding hash value shown inside the .SHA256 cleartext file.
Wait up to 30 seconds for the command to complete, and it should display: ``` -seedsigner_os.0.6.x.[Your_Pi_Model_For_Example:pi02w].img: OK +seedsigner_os.0.7.x.[Your_Pi_Model_For_Example:pi02w].img: OK ``` -**If you receive the "OK" message** for your **seedsigner_os.0.6.x.[Your_Pi_Model_For_Example:pi02w].img file**, as shown above, then your verification is fully complete! +**If you receive the "OK" message** for your **seedsigner_os.0.7.x.[Your_Pi_Model_For_Example:pi02w].img file**, as shown above, then your verification is fully complete! **All of your downloaded files have now been confirmed as both authentic and unaltered!** You can proceed to create/write your MicroSD card๐Ÿ˜„๐Ÿ˜„ !! If your file result shows "FAILED", then you must stop here immediately. Do not continue. Contact us for assistance at the Telegram group address above. From 5e6523ae6bc823175cc3d2c474d9ae3fff5b96a9 Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Tue, 12 Sep 2023 09:41:25 -0400 Subject: [PATCH 61/61] change manifest back to sha256 but with a txt file extension --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f32a39ec..adbefc55 100644 --- a/README.md +++ b/README.md @@ -101,8 +101,8 @@ Download the current Version (0.7.0) software image that is compatible with your Note: If you have physically removed the WiFi component from your board, you will still use the image file of the original(un-modified) hardware. (Our files are compiled/based on the *processor* architecture). Although it is better to spend a few minutes upfront to determine which specific Pi hardware/model you have, if you are still unsure which hardware you have, you can try using the pi0.img file. Making an incorrect choice here will not ruin your board, because this is software, not firmware. **also download** these 2 signature verification files to the same folder -[The Plaintext manifest file](https://github.com/SeedSigner/seedsigner/releases/download/0.7.0/seedsigner.0.7.0.manifest.txt) -[The Signature of the manifest file](https://github.com/SeedSigner/seedsigner/releases/download/0.7.0/seedsigner.0.7.0.manifest.txt.sig) +[The Plaintext manifest file](https://github.com/SeedSigner/seedsigner/releases/download/0.7.0/seedsigner.0.7.0.sha256.txt) +[The Signature of the manifest file](https://github.com/SeedSigner/seedsigner/releases/download/0.7.0/seedsigner.0.7.0.sha256.txt.sig) Users familiar with older versions of the SeedSigner software might be surprised with how fast their software downloads now are, because since version 0.6.0 the software image files are now 100x smaller! Each image file is now under 42 Megabytes so your downloads and verifications will be very quick now (and might even seem *too* quick)! @@ -131,7 +131,7 @@ The result should confirm that 1 key was *either* imported or updated. *Ignore* Next, you will run the *verify* command on the signature (.sig) file. (*Verify* must be run from inside the same folder that you downloaded the files into earlier. The `*`'s in this command will auto-fill the version from your current folder, so it should be copied and pasted as-is.) ``` -gpg --verify seedsigner.0.7.*.manifest.sig +gpg --verify seedsigner.0.7.*.sha256.txt.sig ``` When the verify command completes successfully, it should display output like this: @@ -201,7 +201,7 @@ Now that you have confirmed that you do have the real SeedSigner Project's Publi **On Linux or OSX:** Run this command ``` -shasum -a 256 --ignore-missing --check seedsigner.0.7.*.manifest.txt +shasum -a 256 --ignore-missing --check seedsigner.0.7.*.sha256.txt ``` **On Windows (inside Powershell):** Run this command