diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 5de5d493..7adb6457 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -1,5 +1,6 @@ import logging import traceback +import os from embit.descriptor import Descriptor from embit.psbt import PSBT @@ -8,6 +9,7 @@ from typing import List from seedsigner.gui.renderer import Renderer from seedsigner.hardware.buttons import HardwareButtons +from seedsigner.hardware.microsd import MicroSD from seedsigner.views.screensaver import ScreensaverScreen from seedsigner.views.view import Destination, NotYetImplementedView, UnhandledExceptionView @@ -126,6 +128,9 @@ class Controller(Singleton): # TODO: Rename "storage" to something more indicative of its temp, in-memory state controller.storage = SeedStorage() controller.settings = Settings.get_instance() + + controller.microsd = MicroSD.get_instance() + controller.microsd.start_detection() # Store one working psbt in memory controller.psbt = None diff --git a/src/seedsigner/gui/components.py b/src/seedsigner/gui/components.py index 9e6ec211..8b91b1d7 100644 --- a/src/seedsigner/gui/components.py +++ b/src/seedsigner/gui/components.py @@ -58,6 +58,8 @@ class GUIConstants: BUTTON_BACKGROUND_COLOR = "#2c2c2c" BUTTON_HEIGHT = 32 BUTTON_SELECTED_FONT_COLOR = "black" + + NOTIFICATION_COLOR = "#00f100" @@ -102,7 +104,7 @@ class FontAwesomeIconConstants: UNLOCK = "\uf09c" QRCODE = "\uf029" X = "\u0058" - + SDCARD = "\uf7c2" class SeedSignerCustomIconConstants: @@ -568,6 +570,71 @@ class IconTextLine(BaseComponent): self.icon.render() +@dataclass +class ToastOverlay(BaseComponent): + icon_name: str = None + color: str = None + label_text: str = None + + def __post_init__(self): + super().__post_init__() + + self.icon = Icon( + image_draw=self.image_draw, + canvas=self.canvas, + screen_x=20, + screen_y=190, + icon_name=self.icon_name, + icon_size=30, + icon_color=self.color + ) + + self.label = TextArea( + image_draw=self.image_draw, + canvas=self.canvas, + text=self.label_text, + font_size=19, + 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, + allow_text_overflow=False + ) + + def render(self): + import time + from seedsigner.controller import Controller + + self.controller: Controller = Controller.get_instance() + self.current_screen = self.renderer.canvas.copy() + + # Special case when screensaver is running + if self.controller.screensaver._is_running: + self.controller.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 + while time.time() < t_end: + if self.controller.buttons.has_any_input(): + break + + self.renderer.show_image(self.current_screen) @dataclass class FormattedAddress(BaseComponent): diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index 552407bd..1fa064ac 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -10,8 +10,7 @@ from seedsigner.models.threads import BaseThread, ThreadsafeCounter from seedsigner.models.encode_qr import EncodeQR from seedsigner.models.settings import Settings, SettingsConstants -from ..components import (FontAwesomeIconConstants, GUIConstants, BaseComponent, Button, Icon, IconButton, LargeIconButton, SeedSignerCustomIconConstants, TopNav, - TextArea, load_image) +from ..components import (FontAwesomeIconConstants, GUIConstants, BaseComponent, Button, Icon, IconButton, LargeIconButton, SeedSignerCustomIconConstants, TopNav, TextArea, load_image, ToastOverlay) from seedsigner.hardware.buttons import HardwareButtonsConstants, HardwareButtons @@ -1113,3 +1112,38 @@ class KeyboardScreen(BaseTopNavScreen): self.title = f"Roll {self.cursor_position + 1}" """ return False + +class MicroSDToastScreen(BaseScreen): + """ + 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): + from seedsigner.hardware.microsd import MicroSD + + if self.action == MicroSD.ACTION__REMOVED: + + self.toast = ToastOverlay( + icon_name=FontAwesomeIconConstants.SDCARD, + color=GUIConstants.NOTIFICATION_COLOR, + label_text="MicroSD removed" + ) + + elif self.action == MicroSD.ACTION__INSERTED: + + self.toast = ToastOverlay( + icon_name=FontAwesomeIconConstants.SDCARD, + color=GUIConstants.NOTIFICATION_COLOR, + label_text="MicroSD inserted" + ) + + self.toast.render() \ No newline at end of file diff --git a/src/seedsigner/hardware/microsd.py b/src/seedsigner/hardware/microsd.py new file mode 100644 index 00000000..247df91b --- /dev/null +++ b/src/seedsigner/hardware/microsd.py @@ -0,0 +1,55 @@ +import time + +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): + + 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() diff --git a/src/seedsigner/helpers/qr.py b/src/seedsigner/helpers/qr.py index 27f10ac6..b986c6d7 100644 --- a/src/seedsigner/helpers/qr.py +++ b/src/seedsigner/helpers/qr.py @@ -42,12 +42,12 @@ class QR: else: border_str = "3" - cmd = f"""qrencode -m {border_str} -s 3 -l L --foreground=000000 --background={background_color} -t PNG -o "/dev/shm/qrcode.png" "{str(data)}" """ + cmd = f"""qrencode -m {border_str} -s 3 -l L --foreground=000000 --background={background_color} -t PNG -o "/tmp/qrcode.png" "{str(data)}" """ rv = subprocess.call(cmd, shell=True) # if qrencode fails, fall back to only encoder if rv != 0: return self.qrimage(data,width,height,border) - img = Image.open("/dev/shm/qrcode.png").resize((width,height), Image.NEAREST).convert("RGBA") + img = Image.open("/tmp/qrcode.png").resize((width,height), Image.NEAREST).convert("RGBA") return img diff --git a/src/seedsigner/models/settings.py b/src/seedsigner/models/settings.py index 2b57e45e..718a68c1 100644 --- a/src/seedsigner/models/settings.py +++ b/src/seedsigner/models/settings.py @@ -4,15 +4,16 @@ import os from typing import Any, List from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition - from .singleton import Singleton class Settings(Singleton): - SETTINGS_FILENAME = "settings.json" - + HOSTNAME = os.uname()[1] + SEEDSIGNER_OS = "seedsigner-os" + SETTINGS_FILENAME = "/mnt/microsd/settings.json" if HOSTNAME == SEEDSIGNER_OS else "settings.json" + @classmethod def get_instance(cls): # This is the only way to access the one and only instance @@ -100,9 +101,12 @@ class Settings(Singleton): # Special handling for toggling persistence if attr_name == SettingsConstants.SETTING__PERSISTENT_SETTINGS and value == SettingsConstants.OPTION__DISABLED: - os.remove(self.SETTINGS_FILENAME) - print(f"Removed {self.SETTINGS_FILENAME}") - + try: + os.remove(self.SETTINGS_FILENAME) + print(f"Removed {self.SETTINGS_FILENAME}") + except: + print(f"{self.SETTINGS_FILENAME} not found to be removed") + self._data[attr_name] = value self.save() @@ -167,3 +171,28 @@ class Settings(Singleton): return self._data[SettingsConstants.SETTING__DEBUG] == SettingsConstants.OPTION__ENABLED + def microsd_handler(action): + from seedsigner.hardware.microsd import MicroSD + + if Settings.HOSTNAME == Settings.SEEDSIGNER_OS: + + if action == MicroSD.ACTION__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." + + # 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 + 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" \ No newline at end of file diff --git a/src/seedsigner/models/settings_definition.py b/src/seedsigner/models/settings_definition.py index f1f12af6..83ebcddd 100644 --- a/src/seedsigner/models/settings_definition.py +++ b/src/seedsigner/models/settings_definition.py @@ -13,6 +13,9 @@ class SettingsConstants: (OPTION__ENABLED, "Enabled"), (OPTION__DISABLED, "Disabled"), ] + OPTIONS__ONLY_DISABLED = [ + (OPTION__DISABLED, "Disabled"), + ] OPTIONS__PROMPT_REQUIRED_DISABLED = [ (OPTION__PROMPT, "Prompt"), (OPTION__REQUIRED, "Required"), @@ -520,7 +523,14 @@ class SettingsDefinition: if __name__ == "__main__": import json + import os - output_file = "settings_definition.json" + hostname = os.uname()[1] + + if hostname == "seedsigner-os": + output_file = "/mnt/microsd/settings_definition.json" + else: + output_file = "settings_definition.json" + with open(output_file, 'w') as json_file: json.dump(SettingsDefinition.to_dict(), json_file, indent=4) diff --git a/src/seedsigner/views/screensaver.py b/src/seedsigner/views/screensaver.py index 29fb8da4..f863c15d 100644 --- a/src/seedsigner/views/screensaver.py +++ b/src/seedsigner/views/screensaver.py @@ -142,7 +142,7 @@ class ScreensaverScreen(LogoScreen): with self.renderer.lock: try: while True: - if self.buttons.has_any_input(): + if self.buttons.has_any_input() or self.buttons.override_ind: return self.stop() # Must crop the image to the exact display size diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index da8926e1..14a775d5 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -1,5 +1,6 @@ import hashlib import os + import time from embit.descriptor import Descriptor @@ -53,8 +54,6 @@ class ToolsMenuView(View): return Destination(ToolsAddressExplorerSelectSourceView) - - """**************************************************************************** Image entropy Views ****************************************************************************""" @@ -420,7 +419,6 @@ class ToolsCalcFinalWordDoneView(View): return Destination(SeedDiscardView) - """**************************************************************************** Address Explorer Views ****************************************************************************""" diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index 815e5932..41199df5 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from typing import List -from seedsigner.gui.components import FontAwesomeIconConstants +from seedsigner.gui.components import FontAwesomeIconConstants, GUIConstants from seedsigner.gui.screens import RET_CODE__POWER_BUTTON from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, DireWarningScreen, LargeButtonScreen, PowerOffScreen, ResetScreen, WarningScreen from seedsigner.models.threads import BaseThread @@ -188,10 +188,9 @@ class RestartView(View): # exiting. time.sleep(0.25) - # Kill the SeedSigner process; systemd will automatically restart it. - # `.*` is a wildcard to detect either `python`` or `python3` and with or - # without the `-u` flag. - call("kill $(ps aux | grep '[p]ython.*main.py' | awk '{print $2}')", shell=True) + # Kill the SeedSigner process; Running the process again. + # `.*` is a wildcard to detect either `python`` or `python3`. + call("kill $(pidof python*) & python /opt/src/main.py", shell=True) @@ -208,7 +207,7 @@ class PowerOffView(View): from subprocess import call while self.keep_running: time.sleep(5) - call("sudo shutdown --poweroff now", shell=True) + call("poweroff", shell=True) @@ -243,4 +242,4 @@ class UnhandledExceptionView(View): allow_text_overflow=True, # Fit what we can, let the rest go off the edges ).display() - return Destination(MainMenuView, clear_history=True) \ No newline at end of file + return Destination(MainMenuView, clear_history=True)