mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-06 07:28:24 +00:00
Merge pull request #275 from SeedSigner/0.5.1-ram
SeedSigner OS App Changes (0.5.1-ram branch) into dev branch
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
****************************************************************************"""
|
||||
|
||||
@@ -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)
|
||||
return Destination(MainMenuView, clear_history=True)
|
||||
|
||||
Reference in New Issue
Block a user