From 9f774a0035221eac5df2813a52f5d9b545a5995c Mon Sep 17 00:00:00 2001 From: kdmukai Date: Wed, 23 Jun 2021 22:35:30 -0500 Subject: [PATCH 01/39] testing singleton pattern --- seedsigner/controller.py | 27 ++++++++++++++++++------- seedsigner/views/io_test_view.py | 15 ++++++++------ seedsigner/views/menu_view.py | 14 ++++++++----- seedsigner/views/seed_tools_view.py | 7 +++++-- seedsigner/views/settings_tools_view.py | 13 +++++++----- seedsigner/views/signing_tools_view.py | 6 ++++-- seedsigner/views/view.py | 8 ++++---- 7 files changed, 59 insertions(+), 31 deletions(-) diff --git a/seedsigner/controller.py b/seedsigner/controller.py index 5c27e406..504badcd 100644 --- a/seedsigner/controller.py +++ b/seedsigner/controller.py @@ -21,15 +21,28 @@ from generic_ur2_wallet import GenericUR2Wallet class Controller: - VERSION = "0.4.1a1" + _instance = None + + @classmethod + def get_instance(cls): + if cls._instance is None: + raise Exception("Singleton not instantiated") + return cls._instance + + def __init__(self, config) -> None: - controller = self + if self._instance: + raise Exception("Singleton cannot be reinstantiated") # settings self.DEBUG = config.getboolean("system", "DEBUG") + display_settings = {} + display_settings["background_color"] = config.get("display", "BACKGROUND_COLOR") + display_settings["text_color"] = config.get("display", "TEXT_COLOR") + # Input Buttons self.buttons = Buttons() @@ -39,11 +52,11 @@ class Controller: self.wallet = self.wallet_klass() # Views - self.menu_view = MenuView(controller) - self.seed_tools_view = SeedToolsView(controller) - self.io_test_view = IOTestView(controller) - self.signing_tools_view = SigningToolsView(controller, self.storage) - self.settings_tools_view = SettingsToolsView(controller) + self.menu_view = MenuView() + self.seed_tools_view = SeedToolsView() + self.io_test_view = IOTestView() + self.signing_tools_view = SigningToolsView(self.storage) + self.settings_tools_view = SettingsToolsView() # Then start seperate background camera process with two queues for communication # CameraProcess handles connecting to camera hardware and passing back barcode data via from camera queue diff --git a/seedsigner/views/io_test_view.py b/seedsigner/views/io_test_view.py index 887792d1..270e5009 100644 --- a/seedsigner/views/io_test_view.py +++ b/seedsigner/views/io_test_view.py @@ -6,11 +6,14 @@ from threading import Timer from view import View from buttons import B from camera_process import CameraPoll +from controller import Controller +controller = Controller.get_instance() + class IOTestView(View): - def __init__(self, controller) -> None: - View.__init__(self, controller) + def __init__(self) -> None: + View.__init__(self) self.camera_loop_timer = None self.redraw = False self.redraw_complete = False @@ -24,10 +27,10 @@ class IOTestView(View): self.qr_text = "Scan ANY QR Code" # initialize camera - self.controller.to_camera_queue.put(["start"]) + controller.to_camera_queue.put(["start"]) # First get blocking, this way it's clear when the camera is ready for the end user - self.controller.from_camera_queue.get() + controller.from_camera_queue.get() self.camera_loop_timer = CameraPoll(0.05, self.get_camera_data) # it auto-starts, no need of rt.start() @@ -56,7 +59,7 @@ class IOTestView(View): def get_camera_data(self): try: - data = self.controller.from_camera_queue.get(False) + data = controller.from_camera_queue.get(False) if data[0] != "nodata": self.draw_scan_detected() except: @@ -98,7 +101,7 @@ class IOTestView(View): def c_button(self): self.camera_loop_timer.stop() - self.controller.to_camera_queue.put(["stop"]) + controller.to_camera_queue.put(["stop"]) def up_button(self): if self.redraw == False and self.redraw_complete == True: diff --git a/seedsigner/views/menu_view.py b/seedsigner/views/menu_view.py index 9667cbe6..6cd39886 100644 --- a/seedsigner/views/menu_view.py +++ b/seedsigner/views/menu_view.py @@ -8,10 +8,14 @@ from seed_storage import SeedStorage import time import re +from controller import Controller +controller = Controller.get_instance() + + class MenuView(View): - def __init__(self, controller) -> None: - View.__init__(self, controller) + def __init__(self) -> None: + View.__init__(self) self.menu_lines = [] self.selected_menu_num = 1 @@ -113,8 +117,8 @@ class MenuView(View): lines = ["... [ Return to Main ]", "Input / Output Tests", "Current Network: ", "Wallet: ", "Version Info", "Donate to SeedSigner"] input = 0 - lines[2] = lines[2].replace("", self.controller.wallet.get_network()) - lines[3] = lines[3].replace("", self.controller.wallet.get_name()) + lines[2] = lines[2].replace("", controller.wallet.get_network()) + lines[3] = lines[3].replace("", controller.wallet.get_name()) # Draw Menu self.draw_menu(lines, 1) @@ -235,7 +239,7 @@ class MenuView(View): def draw_menu(self, lines, selected_menu_num = 1, title = None, bottom = None) -> None: if title == None: - t = "SeedSigner v" + self.controller.VERSION + t = "SeedSigner v" + controller.VERSION else: t = title diff --git a/seedsigner/views/seed_tools_view.py b/seedsigner/views/seed_tools_view.py index 36f265a5..e9fa025d 100644 --- a/seedsigner/views/seed_tools_view.py +++ b/seedsigner/views/seed_tools_view.py @@ -7,6 +7,9 @@ import time # Internal file class dependencies from view import View from buttons import B +from controller import Controller +controller = Controller.get_instance() + class SeedToolsView(View): @@ -14,8 +17,8 @@ class SeedToolsView(View): ALPHABET = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"] - def __init__(self, controller) -> None: - View.__init__(self, controller) + def __init__(self) -> None: + View.__init__(self) # Gather words and seed display information self.words = [] diff --git a/seedsigner/views/settings_tools_view.py b/seedsigner/views/settings_tools_view.py index 6d0f545a..1fd5b4c0 100644 --- a/seedsigner/views/settings_tools_view.py +++ b/seedsigner/views/settings_tools_view.py @@ -2,11 +2,14 @@ from view import View from qr import QR from buttons import B +from controller import Controller +controller = Controller.get_instance() + class SettingsToolsView(View): - def __init__(self, controller) -> None: - View.__init__(self, controller) + def __init__(self) -> None: + View.__init__(self) self.qr = QR() self.donate_image = None @@ -26,7 +29,7 @@ class SettingsToolsView(View): ### Display Network Selection def display_current_network(self) -> str: - r = self.controller.menu_view.display_generic_selection_menu(["... [ Return to Settings ]", "Mainnet", "Testnet"], "Which Network?") + r = controller.menu_view.display_generic_selection_menu(["... [ Return to Settings ]", "Mainnet", "Testnet"], "Which Network?") if r == 2: return "main" elif r == 3: @@ -37,7 +40,7 @@ class SettingsToolsView(View): ### Display Wallet Selection def display_wallet_selection(self) ->str: - r = self.controller.menu_view.display_generic_selection_menu(["... [ Return to Settings ]", "Specter Desktop", "Blue Wallet Vault", "Sparrow Multisig", "UR 2.0 Multisig"], "Which Wallet?") + r = controller.menu_view.display_generic_selection_menu(["... [ Return to Settings ]", "Specter Desktop", "Blue Wallet Vault", "Sparrow Multisig", "UR 2.0 Multisig"], "Which Wallet?") if r == 2: return "Specter Desktop" elif r == 3: @@ -58,7 +61,7 @@ class SettingsToolsView(View): def display_version_info(self): line1 = "SeedSigner" - line2 = "Version v" + self.controller.VERSION + line2 = "Version v" + controller.VERSION line3 = "built for use with" line4 = "Specter-desktop" line5 = "v1.1.0 or higher" diff --git a/seedsigner/views/signing_tools_view.py b/seedsigner/views/signing_tools_view.py index 5b925af3..746fe029 100644 --- a/seedsigner/views/signing_tools_view.py +++ b/seedsigner/views/signing_tools_view.py @@ -1,14 +1,16 @@ # Internal file class dependencies from view import View from buttons import Buttons, B +from controller import Controller +controller = Controller.get_instance() # External Dependencies import time class SigningToolsView(View): - def __init__(self, controller, seed_storage) -> None: - View.__init__(self, controller) + def __init__(self, seed_storage) -> None: + View.__init__(self) self.seed_storage = seed_storage ### diff --git a/seedsigner/views/view.py b/seedsigner/views/view.py index 5f46cf70..0cc6a889 100644 --- a/seedsigner/views/view.py +++ b/seedsigner/views/view.py @@ -5,6 +5,8 @@ from PIL import ImageFont import ST7789 import spidev as SPI from multiprocessing import Queue +from controller import Controller +controller = Controller.get_instance() ### Generic View Class to Instatiate Display ### Static Class variables are used for display @@ -34,7 +36,6 @@ class View: DC = 25 BL = 24 - controller = None buttons = None canvas_width = 0 canvas_height = 0 @@ -44,11 +45,10 @@ class View: device = 0 disp = None - def __init__(self, controller) -> None: + def __init__(self) -> None: # Global Singleton - View.controller = controller - View.buttons = View.controller.buttons + View.buttons = controller.buttons View.canvas_width = View.WIDTH View.canvas_height = View.HEIGHT From 7e2edc578708061c12de48750b4e08a448a0d8e8 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Thu, 24 Jun 2021 18:04:42 -0500 Subject: [PATCH 02/39] Controller truly singleton-ized --- src/main.py | 6 +- src/seedsigner/controller.py | 67 +++++++++++++++------ src/seedsigner/views/io_test_view.py | 10 ++- src/seedsigner/views/menu_view.py | 9 +-- src/seedsigner/views/seed_tools_view.py | 2 - src/seedsigner/views/settings_tools_view.py | 8 +-- src/seedsigner/views/signing_tools_view.py | 2 - src/seedsigner/views/view.py | 8 +-- 8 files changed, 67 insertions(+), 45 deletions(-) diff --git a/src/main.py b/src/main.py index 1bb2a4b2..deb8df17 100644 --- a/src/main.py +++ b/src/main.py @@ -8,5 +8,9 @@ from seedsigner.controller import Controller config = configparser.ConfigParser() config.read("settings.ini") -controller = Controller(config) +# One-time setup to intialize the one and only Controller +Controller.configure_instance(config) + +# Get the one and only Controller instance and start our main loop +controller = Controller.get_instance() controller.start() diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 694c1ce2..443e024c 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -11,49 +11,78 @@ from .models import (SeedStorage, SpecterDesktopMultisigWallet, BlueVaultWallet, SparrowMultiSigWallet, GenericUR2Wallet) + class Controller: + """ + The Controller is a globally available singleton that maintains SeedSigner state. + + It only makes sense to ever have a single Controller instance so it is + implemented here as a singleton. One departure from the typical singleton pattern + is the addition of a `configure_instance()` call to pass run-time settings into + the Controller. + + Any code that needs to interact with the one and only Controller can just run: + ``` + from seedsigner.controller import Controller + controller = Controller.get_instance() + ``` + """ VERSION = "0.4.1a1" _instance = None + + def __init__(self): + # Singleton pattern must prevent normal instantiation + raise Exception("Cannot directly instantiate the Controller. Access via Controller.get_instance()") + + @classmethod def get_instance(cls): - if cls._instance is None: - raise Exception("Singleton not instantiated") - return cls._instance + # This is the only way to access the one and only Controller + if cls._instance: + return cls._instance + else: + raise Exception("Must call Controller.configure_instance(config) first") - def __init__(self, config) -> None: - if self._instance: - raise Exception("Singleton cannot be reinstantiated") + @classmethod + def configure_instance(cls, config=None): + # Must be called before the first get_instance() call + if cls._instance: + raise Exception("Instance already configured") + + # Instantiate the one and only Controller instance + controller = cls.__new__(cls) + cls._instance = controller # settings - self.DEBUG = config.getboolean("system", "DEBUG") + controller.DEBUG = config.getboolean("system", "DEBUG") display_settings = {} display_settings["background_color"] = config.get("display", "BACKGROUND_COLOR") display_settings["text_color"] = config.get("display", "TEXT_COLOR") # Input Buttons - self.buttons = Buttons() + controller.buttons = Buttons() # models - self.storage = SeedStorage() - self.wallet_klass = globals()["SpecterDesktopMultisigWallet"] - self.wallet = self.wallet_klass() + controller.storage = SeedStorage() + controller.wallet_klass = globals()["SpecterDesktopMultisigWallet"] + controller.wallet = controller.wallet_klass() # Views - self.menu_view = MenuView() - self.seed_tools_view = SeedToolsView() - self.io_test_view = IOTestView() - self.signing_tools_view = SigningToolsView(self.storage) - self.settings_tools_view = SettingsToolsView() + controller.menu_view = MenuView() + controller.seed_tools_view = SeedToolsView() + controller.io_test_view = IOTestView() + controller.signing_tools_view = SigningToolsView(controller.storage) + controller.settings_tools_view = SettingsToolsView() # Then start seperate background camera process with two queues for communication # CameraProcess handles connecting to camera hardware and passing back barcode data via from camera queue - self.from_camera_queue = Queue() - self.to_camera_queue = Queue() - p = Process(target=CameraProcess.start, args=(self.from_camera_queue, self.to_camera_queue)) + controller.from_camera_queue = Queue() + controller.to_camera_queue = Queue() + p = Process(target=CameraProcess.start, args=(controller.from_camera_queue, controller.to_camera_queue)) p.start() diff --git a/src/seedsigner/views/io_test_view.py b/src/seedsigner/views/io_test_view.py index 45707e8c..2ae248a9 100644 --- a/src/seedsigner/views/io_test_view.py +++ b/src/seedsigner/views/io_test_view.py @@ -3,8 +3,6 @@ import threading from threading import Timer # Internal file class dependencies -from controller import Controller -controller = Controller.get_instance() from . import View from seedsigner.helpers import B, CameraPoll @@ -26,10 +24,10 @@ class IOTestView(View): self.qr_text = "Scan ANY QR Code" # initialize camera - controller.to_camera_queue.put(["start"]) + self.controller.to_camera_queue.put(["start"]) # First get blocking, this way it's clear when the camera is ready for the end user - controller.from_camera_queue.get() + self.controller.from_camera_queue.get() self.camera_loop_timer = CameraPoll(0.05, self.get_camera_data) # it auto-starts, no need of rt.start() @@ -58,7 +56,7 @@ class IOTestView(View): def get_camera_data(self): try: - data = controller.from_camera_queue.get(False) + data = self.controller.from_camera_queue.get(False) if data[0] != "nodata": self.draw_scan_detected() except: @@ -100,7 +98,7 @@ class IOTestView(View): def c_button(self): self.camera_loop_timer.stop() - controller.to_camera_queue.put(["stop"]) + self.controller.to_camera_queue.put(["stop"]) def up_button(self): if self.redraw == False and self.redraw_complete == True: diff --git a/src/seedsigner/views/menu_view.py b/src/seedsigner/views/menu_view.py index 547f2a79..c1a0caf2 100644 --- a/src/seedsigner/views/menu_view.py +++ b/src/seedsigner/views/menu_view.py @@ -7,9 +7,6 @@ from seedsigner.models import SeedStorage import time import re -from controller import Controller -controller = Controller.get_instance() - class MenuView(View): @@ -116,8 +113,8 @@ class MenuView(View): lines = ["... [ Return to Main ]", "Input / Output Tests", "Current Network: ", "Wallet: ", "Version Info", "Donate to SeedSigner"] input = 0 - lines[2] = lines[2].replace("", controller.wallet.get_network()) - lines[3] = lines[3].replace("", controller.wallet.get_name()) + lines[2] = lines[2].replace("", self.controller.wallet.get_network()) + lines[3] = lines[3].replace("", self.controller.wallet.get_name()) # Draw Menu self.draw_menu(lines, 1) @@ -238,7 +235,7 @@ class MenuView(View): def draw_menu(self, lines, selected_menu_num = 1, title = None, bottom = None) -> None: if title == None: - t = "SeedSigner v" + controller.VERSION + t = "SeedSigner v" + self.controller.VERSION else: t = title diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index 56aa9510..9047a2ca 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -4,8 +4,6 @@ from embit.bip39 import mnemonic_to_bytes, mnemonic_from_bytes import time # Internal file class dependencies -from controller import Controller -controller = Controller.get_instance() from . import View from seedsigner.helpers import B diff --git a/src/seedsigner/views/settings_tools_view.py b/src/seedsigner/views/settings_tools_view.py index 1e3a5f6f..95dd066b 100644 --- a/src/seedsigner/views/settings_tools_view.py +++ b/src/seedsigner/views/settings_tools_view.py @@ -1,6 +1,4 @@ # SeedSigner file class dependencies -from controller import Controller -controller = Controller.get_instance() from . import View from seedsigner.helpers import B, QR @@ -29,7 +27,7 @@ class SettingsToolsView(View): ### Display Network Selection def display_current_network(self) -> str: - r = controller.menu_view.display_generic_selection_menu(["... [ Return to Settings ]", "Mainnet", "Testnet"], "Which Network?") + r = self.controller.menu_view.display_generic_selection_menu(["... [ Return to Settings ]", "Mainnet", "Testnet"], "Which Network?") if r == 2: return "main" elif r == 3: @@ -40,7 +38,7 @@ class SettingsToolsView(View): ### Display Wallet Selection def display_wallet_selection(self) ->str: - r = controller.menu_view.display_generic_selection_menu(["... [ Return to Settings ]", "Specter Desktop", "Blue Wallet Vault", "Sparrow Multisig", "UR 2.0 Multisig"], "Which Wallet?") + r = self.controller.menu_view.display_generic_selection_menu(["... [ Return to Settings ]", "Specter Desktop", "Blue Wallet Vault", "Sparrow Multisig", "UR 2.0 Multisig"], "Which Wallet?") if r == 2: return "Specter Desktop" elif r == 3: @@ -61,7 +59,7 @@ class SettingsToolsView(View): def display_version_info(self): line1 = "SeedSigner" - line2 = "Version v" + controller.VERSION + line2 = "Version v" + self.controller.VERSION line3 = "built for use with" line4 = "Specter-desktop" line5 = "v1.1.0 or higher" diff --git a/src/seedsigner/views/signing_tools_view.py b/src/seedsigner/views/signing_tools_view.py index 38a505b6..046d4a3a 100644 --- a/src/seedsigner/views/signing_tools_view.py +++ b/src/seedsigner/views/signing_tools_view.py @@ -1,6 +1,4 @@ # Internal file class dependencies -from controller import Controller -controller = Controller.get_instance() from . import View from seedsigner.helpers import Buttons, B diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index 425089e6..0b8d6f5a 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -2,9 +2,6 @@ from PIL import Image, ImageDraw, ImageFont import spidev as SPI from multiprocessing import Queue -from controller import Controller -controller = Controller.get_instance() - from seedsigner.helpers import ST7789 ### Generic View Class to Instatiate Display @@ -45,9 +42,12 @@ class View: disp = None def __init__(self) -> None: + # Import here to avoid circular imports + from seedsigner.controller import Controller + self.controller = Controller.get_instance() # Global Singleton - View.buttons = controller.buttons + View.buttons = self.controller.buttons View.canvas_width = View.WIDTH View.canvas_height = View.HEIGHT From d14e00a1ea6c0cd41f032aa4936bd68e23b69599 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 26 Jun 2021 09:19:00 -0500 Subject: [PATCH 03/39] Update comments --- src/seedsigner/controller.py | 2 ++ src/seedsigner/views/view.py | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index d7e22f83..e8aaa3b1 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -26,6 +26,8 @@ class Controller: from seedsigner.controller import Controller controller = Controller.get_instance() ``` + Note: In many/most cases you'll need to do the Controller import within a method + rather than at the top in order avoid circular imports. """ VERSION = "0.4.1" diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index 0b8d6f5a..38100135 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -46,7 +46,6 @@ class View: from seedsigner.controller import Controller self.controller = Controller.get_instance() - # Global Singleton View.buttons = self.controller.buttons View.canvas_width = View.WIDTH From 5c36041ed4134bb7b5b2c3a564f88694e684ccc9 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 26 Jun 2021 11:12:33 -0500 Subject: [PATCH 04/39] Adds initial test suite setup --- README.md | 4 ++++ pytest.ini | 2 ++ setup.py | 26 +++++++++++++++++++++++++ tests/README.md | 26 +++++++++++++++++++++++++ tests/requirements.txt | 2 ++ tests/test_controller.py | 42 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 102 insertions(+) create mode 100644 pytest.ini create mode 100644 setup.py create mode 100644 tests/README.md create mode 100644 tests/requirements.txt create mode 100644 tests/test_controller.py diff --git a/README.md b/README.md index d75b8581..4730337e 100644 --- a/README.md +++ b/README.md @@ -172,3 +172,7 @@ sudo reboot It will take about a minute after the Pi is powered on for the GUI to launch -- be patient! _Reminder: If you used option #2, [return the guide](docs/usb_relay.md) to remove the internet access over USB configuration._ + + +## Run the tests +see: [tests/README.md](tests/README.md) diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..de19c9f0 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +testpaths = tests \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..66fa1c99 --- /dev/null +++ b/setup.py @@ -0,0 +1,26 @@ +import setuptools + +with open("README.md", "r", encoding="utf-8") as fh: + long_description = fh.read() + +setuptools.setup( + name="seedsigner", + version="0.4.1", + author="SeedSigner", + author_email="author@example.com", + description="Build an offline, airgapped Bitcoin signing device for less than $50!", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/SeedSigner/seedsigner", + project_urls={ + "Bug Tracker": "https://github.com/SeedSigner/seedsigner/issues", + }, + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], + package_dir={"": "src"}, + packages=setuptools.find_packages(where="src"), + python_requires=">=3.6", +) \ No newline at end of file diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..aa79d2a7 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,26 @@ +# Running Tests + +You must install the test-runner dependencies: +``` +pip3 install -r tests/requirements.txt +``` + +Then make the `seedsigner` python module visible/importable to the tests by installing it: +``` +pip3 install -e . +``` + +Run the whole test suite: +``` +pytest +``` + +Run a specific test file: +``` +pytest tests/test_this_file.py +``` + +Run a specific test: +``` +pytest tests/test_this_file.py::test_this_specific_test +``` diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 00000000..7f265749 --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,2 @@ +pytest==6.2.4 +mock==4.0.3 \ No newline at end of file diff --git a/tests/test_controller.py b/tests/test_controller.py new file mode 100644 index 00000000..001002d1 --- /dev/null +++ b/tests/test_controller.py @@ -0,0 +1,42 @@ +import configparser +import pytest +from mock import MagicMock +from seedsigner.controller import Controller + + + +def test_singleton_init_fails(): + """ The Controller should not allow any code to instantiate it via Controller() """ + with pytest.raises(Exception): + c = Controller() + +def test_singleton_get_instance_without_configure_fails(): + """ Calling get_instance() without first calling configure_instance() should fail """ + with pytest.raises(Exception): + c = Controller.get_instance() + +def test_singleton_get_instance_preserves_state(): + """ Changes to the Controller singleton should be preserved across calls to get_instance() """ + settings = """ + [system] + DEBUG = True + DEFAULT_LANGUAGE = en + + [display] + BACKGROUND_COLOR = #000 + TEXT_COLOR = orange + """ + config = configparser.ConfigParser() + config.read_string(settings) + + # Initialize the instance and verify that it read the config settings + Controller.configure_instance(config) + controller = Controller.get_instance() + assert controller.display_settings["text_color"] == "orange" + + # Change a value in the instance... + controller.display_settings["text_color"] = "purple" + + # ...get a new copy of the instance and confirm change + controller = Controller.get_instance() + assert controller.display_settings["text_color"] == "purple" From c7274613c4dee1fc95b1bc05047bd6ab01d4df07 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 3 Jul 2021 07:54:16 -0500 Subject: [PATCH 05/39] Initial proof of concept --- src/seedsigner/controller.py | 3 + .../resources/fonts/RobotoCondensed-Light.ttf | Bin 0 -> 167568 bytes src/seedsigner/views/view.py | 95 +++++++++++++++++- 3 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 src/seedsigner/resources/fonts/RobotoCondensed-Light.ttf diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index b23b56b2..23933dea 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -80,6 +80,9 @@ class Controller: def show_main_menu(self, sub_menu = 0): ret_val = sub_menu while True: + self.menu_view.display_qwerty() + self.buttons.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS]) + ret_val = self.menu_view.display_main_menu(ret_val) if ret_val == Path.MAIN_MENU: diff --git a/src/seedsigner/resources/fonts/RobotoCondensed-Light.ttf b/src/seedsigner/resources/fonts/RobotoCondensed-Light.ttf new file mode 100644 index 0000000000000000000000000000000000000000..43dd8f42bec90404439cdd5fe49f4acf8de920d7 GIT binary patch literal 167568 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 KWNQ= len(lines[selected["y"]]): + # This line is too short to land here + selected["y"] = 0 + + elif input == B.KEY_UP: + selected["y"] -= 1 + if selected["y"] < 0: + # Loop it back to the bottom + selected["y"] = len(lines) - 1 + if selected["x"] >= len(lines[selected["y"]]): + # This line is too short to land here + selected["y"] -= 1 + + # Before we render the next frame, undo our selected letter + View.draw.rectangle((cur_x, cur_y, cur_x + x_width, cur_y + y_height), fill=0) + tw, th = View.draw.textsize(letter, font=View.ROBOTOCONDENSED24) + View.draw.text((cur_x + int((x_width - tw) / 2), cur_y + int((y_height - th)/2)), letter, fill=View.color, font=View.ROBOTOCONDENSED24) + View.draw.rectangle((cur_x, cur_y, cur_x + x_width, cur_y + y_height), outline="#333") + + return + + ### ### Power Off Screen ### From e50e41b5d28d27e10c9293cc279d2d2f788ca1e6 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 3 Jul 2021 07:57:26 -0500 Subject: [PATCH 06/39] commit to rename branch --- src/seedsigner/views/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index 8bff8407..76c8bf9b 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -258,7 +258,7 @@ class View: View.draw.text((cur_x + int((x_width - tw) / 2), cur_y + int((y_height - th)/2)), letter, fill="BLACK", font=View.ROBOTOCONDENSED24) View.DispShowImage() - # Joystick is too responsive with check_release=False; slow down our input rate + # Joystick is too responsive with check_release=False; slow down the input rate time.sleep(0.03) input = self.buttons.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_LEFT, B.KEY_RIGHT, B.KEY_PRESS], check_release=False) From e2b9f127b1e681667ad5645e414334eb43aa3542 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 3 Jul 2021 13:52:47 -0500 Subject: [PATCH 07/39] work in progress seed entry keyboard UI --- src/seedsigner/controller.py | 128 +++++++---- src/seedsigner/helpers/__init__.py | 1 + src/seedsigner/helpers/keyboard.py | 202 ++++++++++++++++++ .../resources/fonts/RobotoCondensed-Bold.ttf | Bin 0 -> 169352 bytes .../fonts/RobotoCondensed-Regular.ttf | Bin 0 -> 169848 bytes src/seedsigner/views/view.py | 94 +------- 6 files changed, 304 insertions(+), 121 deletions(-) create mode 100644 src/seedsigner/helpers/keyboard.py create mode 100644 src/seedsigner/resources/fonts/RobotoCondensed-Bold.ttf create mode 100644 src/seedsigner/resources/fonts/RobotoCondensed-Regular.ttf diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 23933dea..d14b8584 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -6,7 +6,7 @@ from subprocess import call # Internal file class dependencies from .views import (View, MenuView, SeedToolsView,SigningToolsView, SettingsToolsView, IOTestView) -from .helpers import Buttons, B, CameraProcess,Path +from .helpers import Buttons, B, CameraProcess, Path, Keyboard from .models import (SeedStorage, SpecterDesktopMultisigWallet, BlueVaultWallet, SparrowMultiSigWallet, GenericUR2Wallet, Wallet) @@ -79,44 +79,100 @@ class Controller: def show_main_menu(self, sub_menu = 0): ret_val = sub_menu + + word_font = View.ROBOTOCONDENSED_BOLD_26 + View.draw.text((0, 3), "<", fill=View.color, font=word_font) + + title = f"#11:" + word_offset = int(View.canvas_width - word_font.getsize(title)[0] - 1)/2 + View.draw.text((word_offset, 2), title, fill=View.color, font=word_font) + + for idx, word in enumerate(["champion", "mushroom", "satoshi"]): + word_offset = View.canvas_width - word_font.getsize(word)[0] - 1 + View.draw.text((word_offset, 39 + (60*idx)), word, fill=View.color, font=word_font) + View.DispShowImage() + + # draw, charset=["1234567890abcdefghijklmnopqrstuvwxyz"], self.selected_char="a", rows=4, cols=10, rect=(0,40, 240,240), font=View.ROBOTOCONDENSED_LIGHT_24 + keyboard = Keyboard(self.menu_view.draw,charset="abcdefghijklmnopqrstuvwxyz", rows=5, cols=6, rect=(0,40, 120,240)) + + cur_word = "" while True: - self.menu_view.display_qwerty() - self.buttons.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS]) - - ret_val = self.menu_view.display_main_menu(ret_val) + input = self.buttons.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_RIGHT, B.KEY_LEFT, B.KEY_PRESS], check_release=False, release_keys=[B.KEY_PRESS]) + ret_val = keyboard.update_from_input(input) + if ret_val in Keyboard.EXIT_DIRECTIONS: + raise Exception("no exit directions yet") + elif ret_val in Keyboard.ADDITIONAL_KEYS: + if input == B.KEY_PRESS and ret_val == Keyboard.KEY_BACKSPACE: + cur_word = cur_word[:-2] + " " + elif ret_val == Keyboard.KEY_BACKSPACE: + cur_word = cur_word[:-1] + " " + else: + if input == B.KEY_PRESS: + if cur_word[-1] == " ": + # clicking same letter twice in a row + cur_word = cur_word[:-1] + ret_val + " " + else: + # Keep the last letter by advancing + cur_word += " " + else: + # Replace the last letter w/the currently selected one + cur_word = cur_word[:-1] + ret_val - if ret_val == Path.MAIN_MENU: - ret_val = Path.MAIN_MENU - elif ret_val == Path.GEN_LAST_WORD: - ret_val = self.show_generate_last_word_tool() - elif ret_val == Path.DICE_GEN_SEED: - ret_val = self.show_create_seed_with_dice_tool() - elif ret_val == Path.SAVE_SEED: - ret_val = self.show_store_a_seed_tool() - elif ret_val == Path.PASSPHRASE_SEED: - ret_val = self.show_add_a_passphrase_tool() - elif ret_val == Path.DELETE_PASSPHRASE: - ret_val = self.show_delete_a_passphrase_tool() - elif ret_val == Path.GEN_XPUB: - ret_val = self.show_generate_xpub() - elif ret_val == Path.SIGN_TRANSACTION: - ret_val = self.show_sign_transaction() - elif ret_val == Path.IO_TEST_TOOL: - ret_val = self.show_io_test_tool() - elif ret_val == Path.VERSION_INFO: - ret_val = self.show_version_info() - elif ret_val == Path.CURRENT_NETWORK: - ret_val = self.show_current_network_tool() - elif ret_val == Path.WALLET: - ret_val = self.show_wallet_tool() - elif ret_val == Path.QR_DENSITY_SETTING: - ret_val = self.show_qr_density_tool() - elif ret_val == Path.DONATE: - ret_val = self.show_donate_tool() - elif ret_val == Path.POWER_OFF: - ret_val = self.show_power_off() + title = f"#11: {cur_word}" + # Clear the area first + View.draw.rectangle((20,0, View.canvas_width, 39), fill="black") - raise Exception("Unhandled case") + cursor_block_width = 20 + cursor_block_height = 33 + + # Draw n-1 of the selected letters + tw, th = word_font.getsize(title[:-1]) + word_offset = int(View.canvas_width - tw + cursor_block_width)/2 + View.draw.text((word_offset, 2), title[:-1], fill=View.color, font=word_font) + + cursor_block_offset = word_offset + tw + 2 + tw, th = word_font.getsize(cur_word[-1:]) + View.draw.rectangle((cursor_block_offset,2, cursor_block_offset + cursor_block_width, cursor_block_height), fill=View.color) + View.draw.text((cursor_block_offset + 1, 2), cur_word[-1:], fill="black", font=word_font) + + + View.DispShowImage() + + + # ret_val = self.menu_view.display_main_menu(ret_val) + + # if ret_val == Path.MAIN_MENU: + # ret_val = Path.MAIN_MENU + # elif ret_val == Path.GEN_LAST_WORD: + # ret_val = self.show_generate_last_word_tool() + # elif ret_val == Path.DICE_GEN_SEED: + # ret_val = self.show_create_seed_with_dice_tool() + # elif ret_val == Path.SAVE_SEED: + # ret_val = self.show_store_a_seed_tool() + # elif ret_val == Path.PASSPHRASE_SEED: + # ret_val = self.show_add_a_passphrase_tool() + # elif ret_val == Path.DELETE_PASSPHRASE: + # ret_val = self.show_delete_a_passphrase_tool() + # elif ret_val == Path.GEN_XPUB: + # ret_val = self.show_generate_xpub() + # elif ret_val == Path.SIGN_TRANSACTION: + # ret_val = self.show_sign_transaction() + # elif ret_val == Path.IO_TEST_TOOL: + # ret_val = self.show_io_test_tool() + # elif ret_val == Path.VERSION_INFO: + # ret_val = self.show_version_info() + # elif ret_val == Path.CURRENT_NETWORK: + # ret_val = self.show_current_network_tool() + # elif ret_val == Path.WALLET: + # ret_val = self.show_wallet_tool() + # elif ret_val == Path.QR_DENSITY_SETTING: + # ret_val = self.show_qr_density_tool() + # elif ret_val == Path.DONATE: + # ret_val = self.show_donate_tool() + # elif ret_val == Path.POWER_OFF: + # ret_val = self.show_power_off() + + # raise Exception("Unhandled case") ### Power Off diff --git a/src/seedsigner/helpers/__init__.py b/src/seedsigner/helpers/__init__.py index 242c25db..f5ebe908 100644 --- a/src/seedsigner/helpers/__init__.py +++ b/src/seedsigner/helpers/__init__.py @@ -1,5 +1,6 @@ from .buttons import * from .camera_process import * +from .keyboard import Keyboard from .path import * from .qr import * from .ST7789 import * \ No newline at end of file diff --git a/src/seedsigner/helpers/keyboard.py b/src/seedsigner/helpers/keyboard.py new file mode 100644 index 00000000..f21dcafc --- /dev/null +++ b/src/seedsigner/helpers/keyboard.py @@ -0,0 +1,202 @@ +from . import B + + + +class Keyboard: + WRAP_TOP = "wrap_top" + WRAP_BOTTOM = "wrap_bottom" + WRAP_LEFT = "wrap_left" + WRAP_RIGHT = "wrap_right" + + EXIT_TOP = "exit_top" + EXIT_BOTTOM = "exit_bottom" + EXIT_LEFT = "exit_left" + EXIT_RIGHT = "exit_right" + EXIT_DIRECTIONS = [EXIT_TOP, EXIT_BOTTOM, EXIT_LEFT, EXIT_RIGHT] + + ENTER_TOP = "enter_top" + ENTER_BOTTOM = "enter_bottom" + ENTER_LEFT = "enter_left" + ENTER_RIGHT = "enter_right" + + KEY_BACKSPACE = "del" + REGULAR_KEY_FONT = "regular" + COMPACT_KEY_FONT = "compact" + + ADDITIONAL_KEYS = { + KEY_BACKSPACE: { + "font": COMPACT_KEY_FONT, + "key_size": 2, + } + } + + def __init__(self, draw, charset="1234567890abcdefghijklmnopqrstuvwxyz", selected_char="a", rows=4, cols=10, rect=(0,40, 240,240), font=None): + # Import here to avoid circular import problems + from seedsigner.views import View + + self.draw = draw + self.charset = charset + self.rows = rows + self.cols = cols + self.rect = rect + if font: + self.font = font + else: + self.font = View.ROBOTOCONDENSED_REGULAR_24 + + additional_keys = 2 # backspace + if rows * cols < len(charset) + additional_keys: + raise Exception(f"charset will not fit in a {rows}x{cols} layout") + + self.lines = [] + for i in range(0, rows): + self.lines.append(list(charset[i*cols:(i+1)*cols])) + self.lines[-1].append(Keyboard.KEY_BACKSPACE) + print(self.lines) + + self.x_start = rect[0] + self.y_start = rect[1] + self.x_gap = 1 + self.x_width = int((rect[2] - rect[0]) / cols) - self.x_gap + self.y_gap = 6 + self.y_height = int((rect[3] - rect[1]) / rows) - self.y_gap + + # Render the base keyboard + self.draw.rectangle(self.rect, outline=0, fill=0) + + self.additonal_key_compact_font = View.ROBOTOCONDENSED_BOLD_18 + + self.cur_x = self.x_start + self.cur_y = self.y_start + for row_num, line in enumerate(self.lines): + self.cur_x = 0 + for letter in line: + self._render_key(letter, is_selected=False) + self.cur_x += self.x_width + self.x_gap + self.cur_y += self.y_height + self.y_gap + + View.DispShowImage() + + for j, line in enumerate(self.lines): + if selected_char in line: + self.selected = {"x": line.index(selected_char), "y": j} + + # Render the initial highlighted character + self.update_from_input(input=None) + + + + def _render_key(self, letter, is_selected=False): + # Import here to avoid circular import problems + from seedsigner.views import View + + font = self.font + key_size = 1 + if letter in Keyboard.ADDITIONAL_KEYS: + if Keyboard.ADDITIONAL_KEYS[letter]["font"] == Keyboard.COMPACT_KEY_FONT: + font = self.additonal_key_compact_font + key_size = Keyboard.ADDITIONAL_KEYS[letter]["key_size"] + + if is_selected: + rect_color = View.color + font_color = "black" + else: + rect_color = "black" + font_color = View.color + + self.draw.rectangle((self.cur_x, self.cur_y, self.cur_x + self.x_width * key_size, self.cur_y + self.y_height), outline="#333", fill=rect_color) + tw, th = self.draw.textsize(letter, font=font) + self.draw.text((self.cur_x + int((self.x_width * key_size - tw) / 2), self.cur_y + int((self.y_height - th)/2)), letter, fill=font_color, font=font) + + + + def update_from_input(self, input, auto_wrap=[WRAP_TOP, WRAP_BOTTOM, WRAP_LEFT, WRAP_RIGHT], enter_from=None): + """ + Managing code must handle its own input/update loop since other action buttons + will be active on the same screen outside of the keyboard rect (e.g. "Ok", + "Back", etc). Pass relevant input here to update the keyboard. + + `auto_wrap` specifies which edges the keyboard is allowed to loop back when + navigating past the end. + + `enter_from` tells the keyboard that the external UI has caused a loop back + navigation. + (e.g. pressing up from a submit button below the keyboard = ENTER_BOTTOM) + + Returns the character currently highlighted or one of the EXIT_* codes if the + user has navigated off the keyboard past an edge that is not in `auto_wrap`. + """ + # Import here to avoid circular import problems + from seedsigner.views import View + + letter = self.lines[self.selected["y"]][self.selected["x"]] + + # Before we update, undo our previously self.selected letter + self._render_key(letter, is_selected=False) + + if input == B.KEY_RIGHT: + self.selected["x"] += 1 + if self.selected["x"] == len(self.lines[self.selected["y"]]): + if Keyboard.WRAP_RIGHT in auto_wrap: + # Loop it back to the right side + self.selected["x"] = 0 + else: + # Notify controlling loop that we've left the keyboard + return Keyboard.EXIT_RIGHT + + elif input == B.KEY_LEFT: + self.selected["x"] -= 1 + if self.selected["x"] < 0: + if Keyboard.WRAP_LEFT in auto_wrap: + # Loop it back to the left side + self.selected["x"] = len(self.lines[self.selected["y"]]) - 1 + else: + # Notify controlling loop that we've left the keyboard + return Keyboard.EXIT_LEFT + + elif input == B.KEY_DOWN: + self.selected["y"] += 1 + if self.selected["y"] == len(self.lines): + if Keyboard.WRAP_BOTTOM in auto_wrap: + # Loop it back to the top + self.selected["y"] = 0 + else: + # Notify controlling loop that we've left the keyboard + return Keyboard.EXIT_BOTTOM + + if self.selected["x"] >= len(self.lines[self.selected["y"]]): + if Keyboard.WRAP_BOTTOM in auto_wrap: + # This line is too short to land here + self.selected["y"] = 0 + else: + # Notify controlling loop that we've left the keyboard + return Keyboard.EXIT_BOTTOM + + elif input == B.KEY_UP: + self.selected["y"] -= 1 + if self.selected["y"] < 0: + if Keyboard.WRAP_TOP in auto_wrap: + # Loop it back to the bottom + self.selected["y"] = len(self.lines) - 1 + else: + # Notify controlling loop that we've left the keyboard + return Keyboard.EXIT_TOP + if self.selected["x"] >= len(self.lines[self.selected["y"]]): + if Keyboard.WRAP_TOP in auto_wrap: + # This line is too short to land here + self.selected["y"] -= 1 + else: + # Notify controlling loop that we've left the keyboard + return Keyboard.EXIT_TOP + + # Render the newly self.selected letter + self.cur_x = self.selected["x"] * (self.x_width + self.x_gap) + self.cur_y = self.y_start + (self.selected["y"] * (self.y_height + self.y_gap)) + letter = self.lines[self.selected["y"]][self.selected["x"]] + self._render_key(letter, is_selected=True) + + View.DispShowImage() + + return letter + + diff --git a/src/seedsigner/resources/fonts/RobotoCondensed-Bold.ttf b/src/seedsigner/resources/fonts/RobotoCondensed-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..7fe31289ce91cbc8e94d23e5b01413dce22f682d GIT binary patch literal 169352 zcmbTf2YeJ&+Xp&lW_C9{yV-0u1=1TekV@|%ga8SU(0lK_zEr7Vp(_Yd6;Kh_42U2I zihzPG7LZ~=1w=qlX-cvu_kYgpWM_E!zWd$#gY52XnR(80p58_vf*^$8K^9{3+T^x9 z(8{$y5W|NEf;cFzsIcq5;|i7tV&o@+Ftl!7*OFGZ7F_Kqi1Ypt1jow4u4(D(_m;*A zYXiAlI#e5ZYkO_`w5r)~zvLAeRpbf?xF5 zfiospnu*`wcs;%!J8t0Ep)KAR^(Nr!fc4&-Fn;os%CW)_z|(+zK4H?(31{|fI4Ows z8-U3nn1m7d-Vu{akOZ^fD+CC^LXqGSa(lSMw5U?i*{ml&5O5&)H|wd$qF@VYsF>6z zi~8ilpIm<5D*@^of$AHg)fM!8z4~!+wm8Wq5u06#ld|k_;t}^Av1{FH^p9UYAg&7+ zXpzbFc&ABr&lP8u2a5l3_Y?ar6Z-)$VS44Cf*{?)dZL6FVY3nw8^=x(12lQo6M!WI zXh^r>z^Wba?ZD#rc0}TnNVXl-14Nu*wySztc&T9Vu39d0Rkym#{L#P6<)2pSqx;mK zUC@QckTRE#)fLLW76wFFUE%zlXttUJDZ!qQmBIfqqzpU$90~jnkM;yHYf0mMP2Mf5 zdF0!sdz&uB-~P{P9{jS&zWPgREFl}{FtT9}iP}rH(gEymFTJ*h4ioc86bMh~Q&~gW zYhEYR5hf{8-88I0-A}%S+LHaSN5gO)<7C{{rM?zL|a#mbuSVnrbxT;O3 zSJt#Uv~JzYyXdvtoUA6zx^I24&Hl|V?>S1MI&^57OXhbSP_xaPZR2-d3_X9T`os5U zOzYDnuU|%oIh)74`K{yQk0Ty@Hf>&Su)8p@^0s`!bP8M+DWnRSLIYu=A~$TrRshPv zX5cZ3Ki1%nIZvbVB&WVINMW?xvtfjSe??+op z)4hjERuUs4S5}&mBPOv|Iba!kW;VD*WtS``$3|9`iYgB$=|9WiA301Z#5r)y!@!InIB|CO5lN)rH-n()BE5CM-Cv|{?Y6twyGo2Kw2}weIVY*T? zJ%a&T)9MN?V~^1ueE9-Jz8c|ZyuumBiBFu2aIC(O_^A>e>C4U#X)Uepr4S`n{V

~I={b^x(UKJE%*oDH@zt*H{G1lYwyZe3 zrRYe)-El|h@$6^bJoF>kQP}pnIla)gO+W->Rveqxl9msj zPJi1?|Lri~7oSX$Yw9?pc)T<@Z+K#CZllb+Lhw;D={sWQQp%|ilNoDKrIX1cojgK` z*sC8pXBtCCi!~Vc&7cj%e&#~KCRi0qh@Jl`FwG_NO%DmlahgTRVGVIwl0`9P#>lUx zXH8rrEu5M)?aNW3SagNFN^+aMI*R7gD^2LPwBdve%}6uy^7lA%1Dv_G=gh&LGs|hN zKqi4hMbVOy9b(V2iYYl^VId)6?dji4%bc`GTsSqeNI6LF(^3X?l-NXi39>Uth*gD!jLnq!HkU;}$cKX+BES#fxK>h*Ny?CtoF=

DoM{$$EDjN`gAYOk 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%0DU#ZV22;Wq-=+R?>KOj9!dJif=ymlW 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 z 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 literal 0 HcmV?d00001 diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index 76c8bf9b..b6786ebe 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -37,7 +37,15 @@ class View: font_path = os.path.join(pathlib.Path(__file__).parent.resolve(), "..", "resources", "fonts") print(font_path) - ROBOTOCONDENSED24 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Light.ttf"), 24) + ROBOTOCONDENSED_BOLD_18 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Bold.ttf"), 18) + ROBOTOCONDENSED_BOLD_24 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Bold.ttf"), 24) + ROBOTOCONDENSED_BOLD_25 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Bold.ttf"), 25) + ROBOTOCONDENSED_BOLD_26 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Bold.ttf"), 26) + ROBOTOCONDENSED_LIGHT_16 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Light.ttf"), 16) + ROBOTOCONDENSED_LIGHT_24 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Light.ttf"), 24) + ROBOTOCONDENSED_REGULAR_16 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Regular.ttf"), 16) + ROBOTOCONDENSED_REGULAR_20 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Regular.ttf"), 20) + ROBOTOCONDENSED_REGULAR_24 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Regular.ttf"), 24) RST = 27 DC = 25 @@ -216,90 +224,6 @@ class View: View.DispShowImage() return - - - def display_qwerty(self): - lines = [ - "1234567890", - "abcdefghij", - "klmnopqrst", - "uvwxyz" - ] - - y_start = 60 - cur_y = y_start - y_height = 34 - y_gap = 6 - x_width = 22 - x_gap = 1 - - # Set up the blank keyboard first - View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) - - for line in lines: - cur_x = 0 - for letter in line: - tw, th = View.draw.textsize(letter, font=View.ROBOTOCONDENSED24) - View.draw.text((cur_x + int((x_width - tw) / 2), cur_y + int((y_height - th)/2)), letter, fill=View.color, font=View.ROBOTOCONDENSED24) - View.draw.rectangle((cur_x, cur_y, cur_x + x_width, cur_y + y_height), outline="#333") - cur_x += x_width + x_gap - cur_y += y_height + y_gap - - View.DispShowImage() - - selected = {"x": 0, "y": 1} # col 0, row 1: "a" - while True: - # Render the selected letter - cur_x = selected["x"] * (x_width + x_gap) - cur_y = y_start + (selected["y"] * (y_height + y_gap)) - letter = lines[selected["y"]][selected["x"]] - View.draw.rectangle((cur_x, cur_y, cur_x + x_width, cur_y + y_height), fill=View.color) - tw, th = View.draw.textsize(letter, font=View.ROBOTOCONDENSED24) - View.draw.text((cur_x + int((x_width - tw) / 2), cur_y + int((y_height - th)/2)), letter, fill="BLACK", font=View.ROBOTOCONDENSED24) - View.DispShowImage() - - # Joystick is too responsive with check_release=False; slow down the input rate - time.sleep(0.03) - - input = self.buttons.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_LEFT, B.KEY_RIGHT, B.KEY_PRESS], check_release=False) - - if input == B.KEY_RIGHT: - selected["x"] += 1 - if selected["x"] == len(lines[selected["y"]]): - # Loop it back to the right side - selected["x"] = 0 - - elif input == B.KEY_LEFT: - selected["x"] -= 1 - if selected["x"] < 0: - # Loop it back to the left side - selected["x"] = len(lines[selected["y"]]) - 1 - - elif input == B.KEY_DOWN: - selected["y"] += 1 - if selected["y"] == len(lines): - # Loop it back to the top - selected["y"] = 0 - if selected["x"] >= len(lines[selected["y"]]): - # This line is too short to land here - selected["y"] = 0 - - elif input == B.KEY_UP: - selected["y"] -= 1 - if selected["y"] < 0: - # Loop it back to the bottom - selected["y"] = len(lines) - 1 - if selected["x"] >= len(lines[selected["y"]]): - # This line is too short to land here - selected["y"] -= 1 - - # Before we render the next frame, undo our selected letter - View.draw.rectangle((cur_x, cur_y, cur_x + x_width, cur_y + y_height), fill=0) - tw, th = View.draw.textsize(letter, font=View.ROBOTOCONDENSED24) - View.draw.text((cur_x + int((x_width - tw) / 2), cur_y + int((y_height - th)/2)), letter, fill=View.color, font=View.ROBOTOCONDENSED24) - View.draw.rectangle((cur_x, cur_y, cur_x + x_width, cur_y + y_height), outline="#333") - - return ### From 6b232923bb16bf94fdef41c47eb5d8cdd994d259 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 4 Jul 2021 15:28:10 -0500 Subject: [PATCH 08/39] Manual seed entry functioning, minus back button --- src/seedsigner/controller.py | 141 ++++-------- src/seedsigner/helpers/keyboard.py | 289 ++++++++++++++++-------- src/seedsigner/views/seed_tools_view.py | 204 ++++++++++++++++- src/seedsigner/views/view.py | 3 +- 4 files changed, 439 insertions(+), 198 deletions(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index d14b8584..68cb7b83 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -6,7 +6,7 @@ from subprocess import call # Internal file class dependencies from .views import (View, MenuView, SeedToolsView,SigningToolsView, SettingsToolsView, IOTestView) -from .helpers import Buttons, B, CameraProcess, Path, Keyboard +from .helpers import Buttons, B, CameraProcess, Path from .models import (SeedStorage, SpecterDesktopMultisigWallet, BlueVaultWallet, SparrowMultiSigWallet, GenericUR2Wallet, Wallet) @@ -79,100 +79,41 @@ class Controller: def show_main_menu(self, sub_menu = 0): ret_val = sub_menu - - word_font = View.ROBOTOCONDENSED_BOLD_26 - View.draw.text((0, 3), "<", fill=View.color, font=word_font) - - title = f"#11:" - word_offset = int(View.canvas_width - word_font.getsize(title)[0] - 1)/2 - View.draw.text((word_offset, 2), title, fill=View.color, font=word_font) - - for idx, word in enumerate(["champion", "mushroom", "satoshi"]): - word_offset = View.canvas_width - word_font.getsize(word)[0] - 1 - View.draw.text((word_offset, 39 + (60*idx)), word, fill=View.color, font=word_font) - View.DispShowImage() - - # draw, charset=["1234567890abcdefghijklmnopqrstuvwxyz"], self.selected_char="a", rows=4, cols=10, rect=(0,40, 240,240), font=View.ROBOTOCONDENSED_LIGHT_24 - keyboard = Keyboard(self.menu_view.draw,charset="abcdefghijklmnopqrstuvwxyz", rows=5, cols=6, rect=(0,40, 120,240)) - - cur_word = "" while True: - input = self.buttons.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_RIGHT, B.KEY_LEFT, B.KEY_PRESS], check_release=False, release_keys=[B.KEY_PRESS]) - ret_val = keyboard.update_from_input(input) - if ret_val in Keyboard.EXIT_DIRECTIONS: - raise Exception("no exit directions yet") - elif ret_val in Keyboard.ADDITIONAL_KEYS: - if input == B.KEY_PRESS and ret_val == Keyboard.KEY_BACKSPACE: - cur_word = cur_word[:-2] + " " - elif ret_val == Keyboard.KEY_BACKSPACE: - cur_word = cur_word[:-1] + " " - else: - if input == B.KEY_PRESS: - if cur_word[-1] == " ": - # clicking same letter twice in a row - cur_word = cur_word[:-1] + ret_val + " " - else: - # Keep the last letter by advancing - cur_word += " " - else: - # Replace the last letter w/the currently selected one - cur_word = cur_word[:-1] + ret_val + ret_val = self.menu_view.display_main_menu(ret_val) - title = f"#11: {cur_word}" - # Clear the area first - View.draw.rectangle((20,0, View.canvas_width, 39), fill="black") + if ret_val == Path.MAIN_MENU: + ret_val = Path.MAIN_MENU + elif ret_val == Path.GEN_LAST_WORD: + ret_val = self.show_generate_last_word_tool() + elif ret_val == Path.DICE_GEN_SEED: + ret_val = self.show_create_seed_with_dice_tool() + elif ret_val == Path.SAVE_SEED: + ret_val = self.show_store_a_seed_tool() + elif ret_val == Path.PASSPHRASE_SEED: + ret_val = self.show_add_a_passphrase_tool() + elif ret_val == Path.DELETE_PASSPHRASE: + ret_val = self.show_delete_a_passphrase_tool() + elif ret_val == Path.GEN_XPUB: + ret_val = self.show_generate_xpub() + elif ret_val == Path.SIGN_TRANSACTION: + ret_val = self.show_sign_transaction() + elif ret_val == Path.IO_TEST_TOOL: + ret_val = self.show_io_test_tool() + elif ret_val == Path.VERSION_INFO: + ret_val = self.show_version_info() + elif ret_val == Path.CURRENT_NETWORK: + ret_val = self.show_current_network_tool() + elif ret_val == Path.WALLET: + ret_val = self.show_wallet_tool() + elif ret_val == Path.QR_DENSITY_SETTING: + ret_val = self.show_qr_density_tool() + elif ret_val == Path.DONATE: + ret_val = self.show_donate_tool() + elif ret_val == Path.POWER_OFF: + ret_val = self.show_power_off() - cursor_block_width = 20 - cursor_block_height = 33 - - # Draw n-1 of the selected letters - tw, th = word_font.getsize(title[:-1]) - word_offset = int(View.canvas_width - tw + cursor_block_width)/2 - View.draw.text((word_offset, 2), title[:-1], fill=View.color, font=word_font) - - cursor_block_offset = word_offset + tw + 2 - tw, th = word_font.getsize(cur_word[-1:]) - View.draw.rectangle((cursor_block_offset,2, cursor_block_offset + cursor_block_width, cursor_block_height), fill=View.color) - View.draw.text((cursor_block_offset + 1, 2), cur_word[-1:], fill="black", font=word_font) - - - View.DispShowImage() - - - # ret_val = self.menu_view.display_main_menu(ret_val) - - # if ret_val == Path.MAIN_MENU: - # ret_val = Path.MAIN_MENU - # elif ret_val == Path.GEN_LAST_WORD: - # ret_val = self.show_generate_last_word_tool() - # elif ret_val == Path.DICE_GEN_SEED: - # ret_val = self.show_create_seed_with_dice_tool() - # elif ret_val == Path.SAVE_SEED: - # ret_val = self.show_store_a_seed_tool() - # elif ret_val == Path.PASSPHRASE_SEED: - # ret_val = self.show_add_a_passphrase_tool() - # elif ret_val == Path.DELETE_PASSPHRASE: - # ret_val = self.show_delete_a_passphrase_tool() - # elif ret_val == Path.GEN_XPUB: - # ret_val = self.show_generate_xpub() - # elif ret_val == Path.SIGN_TRANSACTION: - # ret_val = self.show_sign_transaction() - # elif ret_val == Path.IO_TEST_TOOL: - # ret_val = self.show_io_test_tool() - # elif ret_val == Path.VERSION_INFO: - # ret_val = self.show_version_info() - # elif ret_val == Path.CURRENT_NETWORK: - # ret_val = self.show_current_network_tool() - # elif ret_val == Path.WALLET: - # ret_val = self.show_wallet_tool() - # elif ret_val == Path.QR_DENSITY_SETTING: - # ret_val = self.show_qr_density_tool() - # elif ret_val == Path.DONATE: - # ret_val = self.show_donate_tool() - # elif ret_val == Path.POWER_OFF: - # ret_val = self.show_power_off() - - # raise Exception("Unhandled case") + raise Exception("Unhandled case") ### Power Off @@ -200,9 +141,9 @@ class Controller: # display menu to select 12 or 24 word seed for last word ret_val = self.menu_view.display_12_24_word_menu("... [ Return to Seed Tools ]") if ret_val == Path.SEED_WORD_12: - seed_phrase = self.seed_tools_view.display_gather_words_screen(11) + seed_phrase = self.seed_tools_view.display_manual_seed_entry(11) elif ret_val == Path.SEED_WORD_24: - seed_phrase = self.seed_tools_view.display_gather_words_screen(23) + seed_phrase = self.seed_tools_view.display_manual_seed_entry(23) else: return Path.SEED_TOOLS_SUB_MENU @@ -291,9 +232,9 @@ class Controller: # display menu to select 12 or 24 word seed for last word ret_val = self.menu_view.display_qr_12_24_word_menu("... [ Return to Seed Tools ]") if ret_val == Path.SEED_WORD_12: - seed_phrase = self.seed_tools_view.display_gather_words_screen(12) + seed_phrase = self.seed_tools_view.display_manual_seed_entry(12) elif ret_val == Path.SEED_WORD_24: - seed_phrase = self.seed_tools_view.display_gather_words_screen(24) + seed_phrase = self.seed_tools_view.display_manual_seed_entry(24) elif ret_val == Path.SEED_WORD_QR: seed_phrase = self.seed_tools_view.read_seed_phrase_qr() else: @@ -390,9 +331,9 @@ class Controller: # display menu to select 12 or 24 word seed for last word ret_val = self.menu_view.display_qr_12_24_word_menu("... [ Return to Sign Tools ]") if ret_val == Path.SEED_WORD_12: - seed_phrase = self.seed_tools_view.display_gather_words_screen(12) + seed_phrase = self.seed_tools_view.display_manual_seed_entry(12) elif ret_val == Path.SEED_WORD_24: - seed_phrase = self.seed_tools_view.display_gather_words_screen(24) + seed_phrase = self.seed_tools_view.display_manual_seed_entry(24) elif ret_val == Path.SEED_WORD_QR: # TODO Add Functionality here? or maybe return to another seed tools menu? return Path.SIGNING_TOOLS_SUB_MENU @@ -445,9 +386,9 @@ class Controller: # display menu to select 12 or 24 word seed for last word ret_val = self.menu_view.display_qr_12_24_word_menu("... [ Return to Sign Tools ]") if ret_val == Path.SEED_WORD_12: - seed_phrase = self.seed_tools_view.display_gather_words_screen(12) + seed_phrase = self.seed_tools_view.display_manual_seed_entry(12) elif ret_val == Path.SEED_WORD_24: - seed_phrase = self.seed_tools_view.display_gather_words_screen(24) + seed_phrase = self.seed_tools_view.display_manual_seed_entry(24) elif ret_val == Path.SEED_WORD_QR: # TODO Add Functionality here? or maybe return to another seed tools menu? return Path.SIGNING_TOOLS_SUB_MENU diff --git a/src/seedsigner/helpers/keyboard.py b/src/seedsigner/helpers/keyboard.py index f21dcafc..3fa1d98a 100644 --- a/src/seedsigner/helpers/keyboard.py +++ b/src/seedsigner/helpers/keyboard.py @@ -1,3 +1,5 @@ +from dataclasses import dataclass + from . import B @@ -19,18 +21,75 @@ class Keyboard: ENTER_LEFT = "enter_left" ENTER_RIGHT = "enter_right" - KEY_BACKSPACE = "del" REGULAR_KEY_FONT = "regular" COMPACT_KEY_FONT = "compact" + KEY_BACKSPACE = { + "letter": "del", + "font": COMPACT_KEY_FONT, + "size": 2, + } ADDITIONAL_KEYS = { - KEY_BACKSPACE: { - "font": COMPACT_KEY_FONT, - "key_size": 2, - } + KEY_BACKSPACE["letter"]: KEY_BACKSPACE, } - def __init__(self, draw, charset="1234567890abcdefghijklmnopqrstuvwxyz", selected_char="a", rows=4, cols=10, rect=(0,40, 240,240), font=None): + @dataclass + class Key: + """ + Simple python3.x dataclass (akin to a strut) to store info about each + individual key in the keyboard and its state. Attrs with defaults must be + listed last. + """ + letter: str + screen_x: int + screen_y: int + keyboard: any + size: int = 1 + is_active: bool = True + is_selected: bool = False + is_additional_key: bool = False + + def render_key(self): + font = self.keyboard.font + if self.letter in Keyboard.ADDITIONAL_KEYS: + if Keyboard.ADDITIONAL_KEYS[self.letter]["font"] == Keyboard.COMPACT_KEY_FONT: + font = self.keyboard.additonal_key_compact_font + + outline_color = "#333" + if not self.is_active: + rect_color = self.keyboard.background_color + font_color = "#666" # Show the letter but render as gray + if self.is_selected: + # Inactive, selected just gets highlighted outline + outline_color = self.keyboard.highlight_color + elif self.is_selected: + rect_color = self.keyboard.highlight_color # Render solid background with the UI's hero color + font_color = self.keyboard.background_color + else: + rect_color = self.keyboard.background_color + font_color = self.keyboard.highlight_color + + self.keyboard.draw.rectangle((self.screen_x, self.screen_y, self.screen_x + self.keyboard.x_width * self.size, self.screen_y + self.keyboard.y_height), outline=outline_color, fill=rect_color) + tw, th = self.keyboard.draw.textsize(self.letter, font=font) + self.keyboard.draw.text((self.screen_x + int((self.keyboard.x_width * self.size - tw) / 2), self.screen_y + int((self.keyboard.y_height - th)/2)), self.letter, fill=font_color, font=font) + + + + def __init__(self, + draw, + charset="1234567890abcdefghijklmnopqrstuvwxyz", + selected_char="a", + rows=4, + cols=10, + rect=(0,40, 240,240), + font=None, + additional_keys=[KEY_BACKSPACE], + auto_wrap=[WRAP_TOP, WRAP_BOTTOM, WRAP_LEFT, WRAP_RIGHT]): + """ + `auto_wrap` specifies which edges the keyboard is allowed to loop back when + navigating past the end. + """ + # Import here to avoid circular import problems from seedsigner.views import View @@ -43,17 +102,23 @@ class Keyboard: self.font = font else: self.font = View.ROBOTOCONDENSED_REGULAR_24 + self.auto_wrap = auto_wrap + self.background_color = "black" + self.highlight_color = View.color - additional_keys = 2 # backspace - if rows * cols < len(charset) + additional_keys: - raise Exception(f"charset will not fit in a {rows}x{cols} layout") + # Does the specified layout work? + additional_key_spaces = 0 + for additional_key in additional_keys: + additional_key_spaces += additional_key["size"] # e.g. backspace takes up 2 slots + if rows * cols < len(charset) + additional_key_spaces: + raise Exception(f"charset will not fit in a {rows}x{cols} layout | additional_keys: {additional_keys}") - self.lines = [] - for i in range(0, rows): - self.lines.append(list(charset[i*cols:(i+1)*cols])) - self.lines[-1].append(Keyboard.KEY_BACKSPACE) - print(self.lines) + if not selected_char: + raise Exception("`selected_char` cannot be None") + # Set up the rendering and state params + self.active_keys = list(self.charset) + self.additonal_key_compact_font = View.ROBOTOCONDENSED_BOLD_18 self.x_start = rect[0] self.y_start = rect[1] self.x_gap = 1 @@ -61,142 +126,178 @@ class Keyboard: self.y_gap = 6 self.y_height = int((rect[3] - rect[1]) / rows) - self.y_gap - # Render the base keyboard - self.draw.rectangle(self.rect, outline=0, fill=0) + # Two-dimensional list of Key obj row data + self.keys = [] + self.selected_key = {"x": 0, "y": 0} # Indices in the `keys` 2D list + cur_y = self.y_start + for i in range(0, rows): + cur_row = [] + cur_x = self.x_start + for j, letter in enumerate(charset[i*cols:(i+1)*cols]): + is_selected = False + if letter == selected_char: + is_selected = True + self.selected_key["y"] = i + self.selected_key["x"] = j + cur_row.append(self.Key( + letter=letter, + screen_x=cur_x, + screen_y=cur_y, + is_selected=is_selected, + keyboard=self + )) + cur_x += self.x_width + self.x_gap + self.keys.append(cur_row) + if i < rows -1: + # increment to the next row and continue + cur_y += self.y_height + self.y_gap + else: + # It's the last row; add the additional keys at the end + for additional_key in additional_keys: + self.keys[-1].append(self.Key( + letter=additional_key["letter"], + screen_x=cur_x, + screen_y=cur_y, + keyboard=self, + size=additional_key["size"], + is_additional_key=True, + )) + cur_x += self.x_width + self.x_gap - self.additonal_key_compact_font = View.ROBOTOCONDENSED_BOLD_18 - - self.cur_x = self.x_start - self.cur_y = self.y_start - for row_num, line in enumerate(self.lines): - self.cur_x = 0 - for letter in line: - self._render_key(letter, is_selected=False) - self.cur_x += self.x_width + self.x_gap - self.cur_y += self.y_height + self.y_gap - - View.DispShowImage() - - for j, line in enumerate(self.lines): - if selected_char in line: - self.selected = {"x": line.index(selected_char), "y": j} + # Render the keys + self.render_keys() # Render the initial highlighted character self.update_from_input(input=None) - - def _render_key(self, letter, is_selected=False): - # Import here to avoid circular import problems - from seedsigner.views import View - - font = self.font - key_size = 1 - if letter in Keyboard.ADDITIONAL_KEYS: - if Keyboard.ADDITIONAL_KEYS[letter]["font"] == Keyboard.COMPACT_KEY_FONT: - font = self.additonal_key_compact_font - key_size = Keyboard.ADDITIONAL_KEYS[letter]["key_size"] - - if is_selected: - rect_color = View.color - font_color = "black" - else: - rect_color = "black" - font_color = View.color - - self.draw.rectangle((self.cur_x, self.cur_y, self.cur_x + self.x_width * key_size, self.cur_y + self.y_height), outline="#333", fill=rect_color) - tw, th = self.draw.textsize(letter, font=font) - self.draw.text((self.cur_x + int((self.x_width * key_size - tw) / 2), self.cur_y + int((self.y_height - th)/2)), letter, fill=font_color, font=font) + def update_active_keys(self, active_keys): + self.active_keys = active_keys + for i, row_keys in enumerate(self.keys): + for j, key in enumerate(row_keys): + if key.letter not in self.active_keys and key.letter not in Keyboard.ADDITIONAL_KEYS: + # Note: ADDITIONAL_KEYS are never deactivated. + key.is_active = False + else: + key.is_active = True + def render_keys(self, selected_letter=None): + """ + Renders just the keys of the keyboard. Useful when you need to redraw just + that section, as in when changing `active_keys` or swapping to alternate + charsets (e.g. alpha to special symbols). - def update_from_input(self, input, auto_wrap=[WRAP_TOP, WRAP_BOTTOM, WRAP_LEFT, WRAP_RIGHT], enter_from=None): + Does NOT call View.DispShowImage to avoid multiple calls on the same screen. + """ + # Start with a clear screen + self.draw.rectangle(self.rect, outline=0, fill=0) + + for i, row_keys in enumerate(self.keys): + for j, key in enumerate(row_keys): + if selected_letter and key.letter == selected_letter: + key.is_selected = True + self.selected_key["y"] = i + self.selected_key["x"] = j + key.render_key() + + + def update_from_input(self, input, enter_from=None): """ Managing code must handle its own input/update loop since other action buttons will be active on the same screen outside of the keyboard rect (e.g. "Ok", "Back", etc). Pass relevant input here to update the keyboard. - `auto_wrap` specifies which edges the keyboard is allowed to loop back when - navigating past the end. - `enter_from` tells the keyboard that the external UI has caused a loop back navigation. (e.g. pressing up from a submit button below the keyboard = ENTER_BOTTOM) Returns the character currently highlighted or one of the EXIT_* codes if the user has navigated off the keyboard past an edge that is not in `auto_wrap`. + + Does NOT call View.DispShowImage to avoid multiple calls on the same screen. """ - # Import here to avoid circular import problems - from seedsigner.views import View + key = self.keys[self.selected_key["y"]][self.selected_key["x"]] - letter = self.lines[self.selected["y"]][self.selected["x"]] - - # Before we update, undo our previously self.selected letter - self._render_key(letter, is_selected=False) + # Before we update, undo our previously self.selected_key key + key.is_selected = False + key.render_key() if input == B.KEY_RIGHT: - self.selected["x"] += 1 - if self.selected["x"] == len(self.lines[self.selected["y"]]): - if Keyboard.WRAP_RIGHT in auto_wrap: + self.selected_key["x"] += 1 + if self.selected_key["x"] == len(self.keys[self.selected_key["y"]]): + if Keyboard.WRAP_RIGHT in self.auto_wrap: # Loop it back to the right side - self.selected["x"] = 0 + self.selected_key["x"] = 0 else: # Notify controlling loop that we've left the keyboard return Keyboard.EXIT_RIGHT elif input == B.KEY_LEFT: - self.selected["x"] -= 1 - if self.selected["x"] < 0: - if Keyboard.WRAP_LEFT in auto_wrap: + self.selected_key["x"] -= 1 + if self.selected_key["x"] < 0: + if Keyboard.WRAP_LEFT in self.auto_wrap: # Loop it back to the left side - self.selected["x"] = len(self.lines[self.selected["y"]]) - 1 + self.selected_key["x"] = len(self.keys[self.selected_key["y"]]) - 1 else: # Notify controlling loop that we've left the keyboard return Keyboard.EXIT_LEFT elif input == B.KEY_DOWN: - self.selected["y"] += 1 - if self.selected["y"] == len(self.lines): - if Keyboard.WRAP_BOTTOM in auto_wrap: + self.selected_key["y"] += 1 + if self.selected_key["y"] == len(self.keys): + if Keyboard.WRAP_BOTTOM in self.auto_wrap: # Loop it back to the top - self.selected["y"] = 0 + self.selected_key["y"] = 0 else: # Notify controlling loop that we've left the keyboard return Keyboard.EXIT_BOTTOM - if self.selected["x"] >= len(self.lines[self.selected["y"]]): - if Keyboard.WRAP_BOTTOM in auto_wrap: + if self.selected_key["x"] >= len(self.keys[self.selected_key["y"]]): + if Keyboard.WRAP_BOTTOM in self.auto_wrap: # This line is too short to land here - self.selected["y"] = 0 + self.selected_key["y"] = 0 else: # Notify controlling loop that we've left the keyboard return Keyboard.EXIT_BOTTOM elif input == B.KEY_UP: - self.selected["y"] -= 1 - if self.selected["y"] < 0: - if Keyboard.WRAP_TOP in auto_wrap: + self.selected_key["y"] -= 1 + if self.selected_key["y"] < 0: + if Keyboard.WRAP_TOP in self.auto_wrap: # Loop it back to the bottom - self.selected["y"] = len(self.lines) - 1 + self.selected_key["y"] = len(self.keys) - 1 else: # Notify controlling loop that we've left the keyboard return Keyboard.EXIT_TOP - if self.selected["x"] >= len(self.lines[self.selected["y"]]): - if Keyboard.WRAP_TOP in auto_wrap: + if self.selected_key["x"] >= len(self.keys[self.selected_key["y"]]): + if Keyboard.WRAP_TOP in self.auto_wrap: # This line is too short to land here - self.selected["y"] -= 1 + self.selected_key["y"] -= 1 else: # Notify controlling loop that we've left the keyboard return Keyboard.EXIT_TOP - # Render the newly self.selected letter - self.cur_x = self.selected["x"] * (self.x_width + self.x_gap) - self.cur_y = self.y_start + (self.selected["y"] * (self.y_height + self.y_gap)) - letter = self.lines[self.selected["y"]][self.selected["x"]] - self._render_key(letter, is_selected=True) + # Render the newly self.selected_key letter + key = self.keys[self.selected_key["y"]][self.selected_key["x"]] + key.is_selected = True + key.render_key() - View.DispShowImage() - - return letter + return key.letter + + + def set_selected_key(self, selected_letter): + # De-select the current selected_key + self.keys[self.selected_key["y"]][self.selected_key["x"]].is_selected = False + + # Find the new selected_key + for i, row_keys in enumerate(self.keys): + for j, key in enumerate(row_keys): + if selected_letter and key.letter == selected_letter: + key.is_selected = True + self.selected_key["y"] = i + self.selected_key["x"] = j + return + raise Exception(f"""`selected_letter` "{selected_letter}" not found in keyboard""") diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index b32097ae..09b49aca 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -7,7 +7,7 @@ import time # Internal file class dependencies from . import View -from seedsigner.helpers import B, QR, CameraPoll +from seedsigner.helpers import B, QR, CameraPoll, Keyboard class SeedToolsView(View): @@ -80,6 +80,204 @@ class SeedToolsView(View): self.draw_gather_words() + + def display_manual_seed_entry(self, num_words): + self.seed_length = num_words + self.reset() + + cur_word = 0 + while len(self.words) < self.seed_length: + initial_letters = ["a"] + if len(self.words) >= cur_word + 1: + initial_letters = list(self.words[cur_word]) + ret_val = self.draw_seed_word_keyboard_entry(num_word=cur_word + 1, initial_letters=initial_letters) + + if ret_val == "BACK" and len(self.words) > 0: + # Reload previous word + cur_word -= 1 + if cur_word < 0: + return [] + + else: + self.words.append(ret_val) + cur_word += 1 + if cur_word == self.seed_length: + return self.words + + + def draw_seed_word_keyboard_entry(self, num_word, initial_letters=["a"]): + def render_possible_matches(highlight_word=None): + """ Internal helper method to render the KEY 1, 2, 3 word candidates. + (has access to all vars in the parent's context) + """ + matches = [i for i in SeedToolsView.SEEDWORDS if i.startswith("".join(self.letters).strip())][:3] + if matches: + self.possible_words = matches + if len(self.possible_words) > 0: + # Clear the right panel + View.draw.rectangle((keyboard_width, text_entry_display_height, View.canvas_width, View.canvas_height), fill="black") + + word_font = View.ROBOTOCONDENSED_BOLD_26 + for slot, word in enumerate(self.possible_words): + tw, th = word_font.getsize(word) + word_offset = View.canvas_width - tw - 1 + slot_y = 39 + (60*slot) + if highlight_word and word == highlight_word: + View.draw.rectangle((word_offset - 3, slot_y - 3, 240, slot_y + th + 6), fill=View.color) + View.draw.text((word_offset, slot_y), word, fill="black", font=word_font) + else: + View.draw.text((word_offset, slot_y), word, fill=View.color, font=word_font) + + + def render_text_entry_display(draw=View.draw): + """ Internal helper method to render the live text entry display at the top + along with the "back" arrow. + (has access to all vars in the parent's context) + """ + draw.rectangle((0,0, View.canvas_width,text_entry_display_height), fill="black") + + # Set up the "back" arrow in the upper left + word_font = View.ROBOTOCONDENSED_BOLD_26 + draw.text((0, 3), "<", fill=View.color, font=word_font) + + # Render the live text entry display + title = f"#{num_word}: {''.join(self.letters)}" + + cursor_block_width = 20 + cursor_block_height = 33 + + # Draw n-1 of the selected letters + tw, th = word_font.getsize(title[:-1]) + word_offset = int(View.canvas_width - tw + cursor_block_width)/2 + draw.text((word_offset, 2), title[:-1], fill=View.color, font=word_font) + + # Draw the highlighted cursor block + cursor_block_offset = word_offset + tw + 2 + draw.rectangle((cursor_block_offset,2, cursor_block_offset + cursor_block_width, cursor_block_height), fill=View.color) + draw.text((cursor_block_offset + 1, 2), ''.join(self.letters[-1:]), fill="black", font=word_font) + + + # Clear the screen + View.draw.rectangle((0,0, View.canvas_width,View.canvas_height), fill="black") + + + # Set up the keyboard params + keyboard_width = 120 + text_entry_display_height = 39 + + # TODO: support other BIP39 languages/charsets + keyboard = Keyboard(View.draw, charset="abcdefghijklmnopqrstuvwxyz", rows=5, cols=6, rect=(0,text_entry_display_height + 1, keyboard_width,240)) + + # Initialize the current letters/current matches + self.letters = initial_letters + self.calc_possible_alphabet() + keyboard.update_active_keys(active_keys=self.possible_alphabet) + keyboard.set_selected_key(selected_letter=self.letters[-1]) + keyboard.render_keys() + render_possible_matches() + + # Render the top text entry display + render_text_entry_display() + + View.DispShowImage() + + # Start the interactive update loop + while True: + input = View.buttons.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_RIGHT, B.KEY_LEFT, B.KEY_PRESS, B.KEY1, B.KEY2, B.KEY3], check_release=True, release_keys=[B.KEY_PRESS, B.KEY1, B.KEY2, B.KEY3]) + ret_val = keyboard.update_from_input(input) + if ret_val in Keyboard.EXIT_DIRECTIONS: + raise Exception("no exit directions yet") + + elif ret_val in Keyboard.ADDITIONAL_KEYS: + if input == B.KEY_PRESS and ret_val == Keyboard.KEY_BACKSPACE["letter"]: + self.letters = self.letters[:-2] + self.letters.append(" ") + + # Reactivate keys after deleting last letter + self.calc_possible_alphabet() + keyboard.update_active_keys(active_keys=self.possible_alphabet) + keyboard.render_keys() + + elif ret_val == Keyboard.KEY_BACKSPACE["letter"]: + # We're just hovering over DEL but haven't clicked. Show blank (" ") + # in the live text entry display at the top. + self.letters = self.letters[:-1] + self.letters.append(" ") + + else: + # Has the user made a final selection of a candidate word? + final_selection = None + if input == B.KEY1: + if self.possible_words[0]: + final_selection = self.possible_words[0] + elif input == B.KEY2: + if self.possible_words[1]: + final_selection = self.possible_words[1] + elif input == B.KEY3: + if self.possible_words[2]: + final_selection = self.possible_words[2] + + if final_selection: + # Animate the selection storage, then return the word to the caller + render_possible_matches(highlight_word=final_selection) + View.DispShowImage() + + return final_selection + + elif input == B.KEY_PRESS and ret_val in self.possible_alphabet: + # User has locked in the current letter + if self.letters[-1] != " ": + # We'll save that locked in letter next but for now update the + # live text entry display with blank (" ") so that we don't try + # to autocalc matches against a second copy of the letter they + # just selected. e.g. They KEY_PRESS on "s" to build "mus". If + # we advance the live block cursor AND display "s" in it, the + # current word would then be "muss" with no matches. If "mus" + # can get us to our match, we don't want it to disappear right + # as we KEY_PRESS. + self.letters.append(" ") + else: + # clicked same letter twice in a row. Because of the above, an + # immediate second click of the same letter would lock in "ap " + # (note the space) instead of "app". So we replace that trailing + # space with the correct repeated letter and then, as above, + # append a trailing blank. + self.letters = self.letters[:-1] + self.letters.append(ret_val) + self.letters.append(" ") + + # Recalc and deactivate keys after advancing + self.calc_possible_alphabet() + keyboard.update_active_keys(active_keys=self.possible_alphabet) + + # Move the selection cursor to an active Key using the next letter in + # the top possible_words match. + if self.possible_words and len(self.possible_words[0]) >= len(self.letters): + keyboard.set_selected_key(self.possible_words[0][len(self.letters) - 1]) + keyboard.render_keys() + + else: + # Live joystick movement; haven't locked this new letter in yet. + # Replace the last letter w/the currently selected one. But don't + # call `calc_possible_alphabet()` because we want to still be able + # to freely float to a different letter; only update the active + # keyboard keys when a selection has been locked in (KEY_PRESS) or + # removed ("del"). + self.letters = self.letters[:-1] + if ret_val in self.possible_alphabet: + self.letters.append(ret_val) + else: + self.letters.append(" ") + + # Update the KEY 1, 2, 3 word candidates + render_possible_matches() + + # Render the text entry display and cursor block + render_text_entry_display() + + View.DispShowImage() + + def gather_words_up(self): View.draw.polygon([(8 + ((len(self.letters)-1)*30), 85) , (14 + ((len(self.letters)-1)*30), 69) , (20 + ((len(self.letters)-1)*30), 85 )], outline=View.color, fill=View.color) View.DispShowImage() @@ -885,11 +1083,11 @@ class SeedToolsView(View): return def calc_possible_alphabet(self, new_letter = False): - if (len(self.letters) > 1 and new_letter == False) or (len(self.letters) > 0 and new_letter == True): + if (self.letters and len(self.letters) > 1 and new_letter == False) or (len(self.letters) > 0 and new_letter == True): search_letters = self.letters[:] if new_letter == False: search_letters.pop() - possible_words = [i for i in SeedToolsView.SEEDWORDS if i.startswith("".join(search_letters))] + possible_words = [i for i in SeedToolsView.SEEDWORDS if i.startswith("".join(search_letters).strip())] letter_num = len(search_letters) possible_letters = [] for word in possible_words: diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index b6786ebe..9a86fd37 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -35,7 +35,6 @@ class View: COURIERNEW20 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/courbd.ttf', 20) font_path = os.path.join(pathlib.Path(__file__).parent.resolve(), "..", "resources", "fonts") - print(font_path) ROBOTOCONDENSED_BOLD_18 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Bold.ttf"), 18) ROBOTOCONDENSED_BOLD_24 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Bold.ttf"), 24) @@ -101,11 +100,13 @@ class View: while (cur_x != end_x or cur_y != end_y) and (rate_x != 0 or rate_y != 0): cur_x += rate_x if (rate_x > 0 and cur_x > end_x) or (rate_x < 0 and cur_x < end_x): + # We've moved too far; back up and undo that last move. cur_x -= rate_x rate_x = 0 cur_y += rate_y if (rate_y > 0 and cur_y > end_y) or (rate_y < 0 and cur_y < end_y): + # We've moved too far; back up and undo that last move. cur_y -= rate_y rate_y = 0 From 9dbcce0dd77e5b79d847fd3b26ee16ac07c55345 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 4 Jul 2021 16:23:13 -0500 Subject: [PATCH 09/39] "back" button functioning --- src/seedsigner/helpers/keyboard.py | 29 ++++++++--- src/seedsigner/views/seed_tools_view.py | 69 ++++++++++++++++++------- 2 files changed, 72 insertions(+), 26 deletions(-) diff --git a/src/seedsigner/helpers/keyboard.py b/src/seedsigner/helpers/keyboard.py index 3fa1d98a..3da73771 100644 --- a/src/seedsigner/helpers/keyboard.py +++ b/src/seedsigner/helpers/keyboard.py @@ -29,8 +29,12 @@ class Keyboard: "font": COMPACT_KEY_FONT, "size": 2, } + KEY_PREVIOUS_PAGE = { + "letter": "prev" + } ADDITIONAL_KEYS = { KEY_BACKSPACE["letter"]: KEY_BACKSPACE, + KEY_PREVIOUS_PAGE["letter"]: KEY_PREVIOUS_PAGE, } @dataclass @@ -230,7 +234,9 @@ class Keyboard: # Loop it back to the right side self.selected_key["x"] = 0 else: - # Notify controlling loop that we've left the keyboard + # Undo selection change and notify controlling loop that we've left + # the keyboard + self.selected_key["x"] -= 1 return Keyboard.EXIT_RIGHT elif input == B.KEY_LEFT: @@ -240,7 +246,9 @@ class Keyboard: # Loop it back to the left side self.selected_key["x"] = len(self.keys[self.selected_key["y"]]) - 1 else: - # Notify controlling loop that we've left the keyboard + # Undo selection change and notify controlling loop that we've left + # the keyboard + self.selected_key["x"] += 1 return Keyboard.EXIT_LEFT elif input == B.KEY_DOWN: @@ -250,7 +258,9 @@ class Keyboard: # Loop it back to the top self.selected_key["y"] = 0 else: - # Notify controlling loop that we've left the keyboard + # Undo selection change and notify controlling loop that we've left + # the keyboard + self.selected_key["y"] -= 1 return Keyboard.EXIT_BOTTOM if self.selected_key["x"] >= len(self.keys[self.selected_key["y"]]): @@ -258,7 +268,9 @@ class Keyboard: # This line is too short to land here self.selected_key["y"] = 0 else: - # Notify controlling loop that we've left the keyboard + # Undo selection change and notify controlling loop that we've left + # the keyboard + self.selected_key["y"] -= 1 return Keyboard.EXIT_BOTTOM elif input == B.KEY_UP: @@ -268,14 +280,19 @@ class Keyboard: # Loop it back to the bottom self.selected_key["y"] = len(self.keys) - 1 else: - # Notify controlling loop that we've left the keyboard + # Undo selection change and notify controlling loop that we've left + # the keyboard + self.selected_key["y"] += 1 return Keyboard.EXIT_TOP + if self.selected_key["x"] >= len(self.keys[self.selected_key["y"]]): if Keyboard.WRAP_TOP in self.auto_wrap: # This line is too short to land here self.selected_key["y"] -= 1 else: - # Notify controlling loop that we've left the keyboard + # Undo selection change and notify controlling loop that we've left + # the keyboard + self.selected_key["y"] += 1 return Keyboard.EXIT_TOP # Render the newly self.selected_key letter diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index 09b49aca..6c9c09ec 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -85,24 +85,31 @@ class SeedToolsView(View): self.seed_length = num_words self.reset() - cur_word = 0 + cur_word = 1 while len(self.words) < self.seed_length: initial_letters = ["a"] - if len(self.words) >= cur_word + 1: - initial_letters = list(self.words[cur_word]) - ret_val = self.draw_seed_word_keyboard_entry(num_word=cur_word + 1, initial_letters=initial_letters) + if len(self.words) >= cur_word: + initial_letters = list(self.words[cur_word - 1]) # zero-indexed - if ret_val == "BACK" and len(self.words) > 0: + ret_val = self.draw_seed_word_keyboard_entry(num_word=cur_word, initial_letters=initial_letters) + + if ret_val == Keyboard.KEY_PREVIOUS_PAGE: # Reload previous word cur_word -= 1 - if cur_word < 0: + if cur_word == 0: + # Exit this UI return [] + else: + # We've backed `cur_word` up, so re-enter loop + continue - else: + if len(self.words) < cur_word: self.words.append(ret_val) - cur_word += 1 - if cur_word == self.seed_length: - return self.words + else: + self.words[cur_word - 1] = ret_val + cur_word += 1 + + return self.words def draw_seed_word_keyboard_entry(self, num_word, initial_letters=["a"]): @@ -129,16 +136,26 @@ class SeedToolsView(View): View.draw.text((word_offset, slot_y), word, fill=View.color, font=word_font) + def render_previous_button(highlight=False): + # Set up the "back" arrow in the upper left + arrow = "<" + word_font = View.ROBOTOCONDENSED_BOLD_26 + tw, th = word_font.getsize(arrow) + if highlight: + View.draw.rectangle((0,0, tw + 6, th + 6), fill=View.color) + View.draw.text((3, 3), arrow, fill="black", font=word_font) + else: + View.draw.rectangle((0,0, tw + 6, th + 6), fill="black") + View.draw.text((3, 3), arrow, fill=View.color, font=word_font) + + return tw + 6 + + def render_text_entry_display(draw=View.draw): - """ Internal helper method to render the live text entry display at the top - along with the "back" arrow. + """ Internal helper method to render the live text entry display at the top. (has access to all vars in the parent's context) """ - draw.rectangle((0,0, View.canvas_width,text_entry_display_height), fill="black") - - # Set up the "back" arrow in the upper left - word_font = View.ROBOTOCONDENSED_BOLD_26 - draw.text((0, 3), "<", fill=View.color, font=word_font) + draw.rectangle((previous_button_width,0, View.canvas_width,text_entry_display_height), fill="black") # Render the live text entry display title = f"#{num_word}: {''.join(self.letters)}" @@ -147,6 +164,7 @@ class SeedToolsView(View): cursor_block_height = 33 # Draw n-1 of the selected letters + word_font = View.ROBOTOCONDENSED_BOLD_26 tw, th = word_font.getsize(title[:-1]) word_offset = int(View.canvas_width - tw + cursor_block_width)/2 draw.text((word_offset, 2), title[:-1], fill=View.color, font=word_font) @@ -159,14 +177,20 @@ class SeedToolsView(View): # Clear the screen View.draw.rectangle((0,0, View.canvas_width,View.canvas_height), fill="black") - + previous_button_width = render_previous_button() + previous_button_is_active = False # Set up the keyboard params keyboard_width = 120 text_entry_display_height = 39 # TODO: support other BIP39 languages/charsets - keyboard = Keyboard(View.draw, charset="abcdefghijklmnopqrstuvwxyz", rows=5, cols=6, rect=(0,text_entry_display_height + 1, keyboard_width,240)) + keyboard = Keyboard(View.draw, + charset="abcdefghijklmnopqrstuvwxyz", + rows=5, + cols=6, + rect=(0,text_entry_display_height + 1, keyboard_width,240), + auto_wrap=[Keyboard.WRAP_LEFT, Keyboard.WRAP_RIGHT]) # Initialize the current letters/current matches self.letters = initial_letters @@ -186,7 +210,8 @@ class SeedToolsView(View): input = View.buttons.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_RIGHT, B.KEY_LEFT, B.KEY_PRESS, B.KEY1, B.KEY2, B.KEY3], check_release=True, release_keys=[B.KEY_PRESS, B.KEY1, B.KEY2, B.KEY3]) ret_val = keyboard.update_from_input(input) if ret_val in Keyboard.EXIT_DIRECTIONS: - raise Exception("no exit directions yet") + render_previous_button(highlight=True) + previous_button_is_active = True elif ret_val in Keyboard.ADDITIONAL_KEYS: if input == B.KEY_PRESS and ret_val == Keyboard.KEY_BACKSPACE["letter"]: @@ -224,6 +249,10 @@ class SeedToolsView(View): return final_selection + elif input == B.KEY_PRESS and previous_button_is_active: + # User clicked the "back" arrow + return Keyboard.KEY_PREVIOUS_PAGE + elif input == B.KEY_PRESS and ret_val in self.possible_alphabet: # User has locked in the current letter if self.letters[-1] != " ": From 1f69d494a393b16f928f345a50b97267ec408e6e Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 4 Jul 2021 16:33:42 -0500 Subject: [PATCH 10/39] Hold on KEY_PRESS, only select new letter if there's only one left --- src/seedsigner/views/seed_tools_view.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index 6c9c09ec..26961baf 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -279,10 +279,10 @@ class SeedToolsView(View): self.calc_possible_alphabet() keyboard.update_active_keys(active_keys=self.possible_alphabet) - # Move the selection cursor to an active Key using the next letter in - # the top possible_words match. - if self.possible_words and len(self.possible_words[0]) >= len(self.letters): - keyboard.set_selected_key(self.possible_words[0][len(self.letters) - 1]) + if len(self.possible_alphabet) == 1: + # If there's only one possible letter left, select it + keyboard.set_selected_key(self.possible_alphabet[0]) + keyboard.render_keys() else: From 7044b916ee11ea4a0a5aad571d5799271c15a329 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 5 Jul 2021 07:57:31 -0500 Subject: [PATCH 11/39] Committing before deleting --- src/seedsigner/helpers/keyboard.py | 23 +++++ src/seedsigner/views/seed_tools_view.py | 116 ++++++++++++++++++------ src/seedsigner/views/view.py | 5 +- 3 files changed, 116 insertions(+), 28 deletions(-) diff --git a/src/seedsigner/helpers/keyboard.py b/src/seedsigner/helpers/keyboard.py index 3da73771..680f2fbc 100644 --- a/src/seedsigner/helpers/keyboard.py +++ b/src/seedsigner/helpers/keyboard.py @@ -295,6 +295,29 @@ class Keyboard: self.selected_key["y"] += 1 return Keyboard.EXIT_TOP + elif input == Keyboard.ENTER_LEFT: + # User has returned to the keyboard along the left edge + # Keep the last y position that was selected. + self.selected_key["x"] = 0 + + elif input == Keyboard.ENTER_RIGHT: + # User has returned to the keyboard along the right edge + # Keep the last y position that was selected. + self.selected_key["x"] = len(self.keys[self.selected_key["y"]]) - 1 + + elif input == Keyboard.ENTER_TOP: + # User has returned to the keyboard along the top edge + # Keep the last x position that was selected. + self.selected_key["y"] = 0 + + elif input == Keyboard.ENTER_BOTTOM: + # User has returned to the keyboard along the bottom edge + # Keep the last x position that was selected. + self.selected_key["y"] = len(self.keys) - 1 + if self.selected_key["x"] > len(self.keys[self.selected_key["y"]]) - 1: + # Can't enter here. Jump up a row + self.selected_key["y"] -= 1 + # Render the newly self.selected_key letter key = self.keys[self.selected_key["y"]][self.selected_key["x"]] key.is_selected = True diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index 26961baf..f0ba157b 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -117,23 +117,62 @@ class SeedToolsView(View): """ Internal helper method to render the KEY 1, 2, 3 word candidates. (has access to all vars in the parent's context) """ - matches = [i for i in SeedToolsView.SEEDWORDS if i.startswith("".join(self.letters).strip())][:3] - if matches: - self.possible_words = matches - if len(self.possible_words) > 0: - # Clear the right panel - View.draw.rectangle((keyboard_width, text_entry_display_height, View.canvas_width, View.canvas_height), fill="black") + # Clear the right panel + View.draw.rectangle((keyboard_width, text_entry_display_height, View.canvas_width, View.canvas_height), fill="black") - word_font = View.ROBOTOCONDENSED_BOLD_26 - for slot, word in enumerate(self.possible_words): - tw, th = word_font.getsize(word) - word_offset = View.canvas_width - tw - 1 - slot_y = 39 + (60*slot) - if highlight_word and word == highlight_word: - View.draw.rectangle((word_offset - 3, slot_y - 3, 240, slot_y + th + 6), fill=View.color) - View.draw.text((word_offset, slot_y), word, fill="black", font=word_font) - else: - View.draw.text((word_offset, slot_y), word, fill=View.color, font=word_font) + if self.possible_words: + selected_possible_words_index = 2 + if len(self.possible_words) < 3: + selected_possible_words_index = len(self.possible_words) - 1 + + center_slot = (keyboard_width + 3, 39 + 60) + + # Render level 3 + level_shift_x = 20 + level_shift_y = 26 * 2 + word_font = View.ROBOTOCONDENSED_BOLD_22 + View.draw.rounded_rectangle((center_slot[0] + level_shift_x, center_slot[1] - level_shift_y - 3, 250, center_slot[1] - level_shift_y + 26 + 3), fill="black", outline="#333", radius=5, width=2) + if selected_possible_words_index - 2 >= 0: + word = self.possible_words[selected_possible_words_index - 2] + View.draw.text((center_slot[0] + level_shift_x + 5, center_slot[1] - level_shift_y), word, fill=View.color, font=word_font) + + View.draw.rounded_rectangle((center_slot[0] + level_shift_x, center_slot[1] + level_shift_y - 3 + 6, 250, center_slot[1] + level_shift_y + 26 + 7), fill="black", outline="#333", radius=5, width=2) + if selected_possible_words_index + 2 < len(self.possible_words): + word = self.possible_words[selected_possible_words_index + 2] + View.draw.text((center_slot[0] + level_shift_x + 5, center_slot[1] + level_shift_y + 7), word, fill=View.color, font=word_font) + + + # Render level 2 + level_shift_x = 10 + level_shift_y = 28 + word_font = View.ROBOTOCONDENSED_BOLD_24 + View.draw.rounded_rectangle((center_slot[0] + level_shift_x, center_slot[1] - level_shift_y - 3, 250, center_slot[1] - level_shift_y + 26 + 3), fill="black", outline="#333", radius=5, width=2) + if selected_possible_words_index - 1 >= 0: + word = self.possible_words[selected_possible_words_index - 1] + View.draw.text((center_slot[0] + level_shift_x + 5, center_slot[1] - level_shift_y), word, fill=View.color, font=word_font) + + View.draw.rounded_rectangle((center_slot[0] + level_shift_x, center_slot[1] + level_shift_y - 3 + 6, 250, center_slot[1] + level_shift_y + 26 + 7), fill="black", outline="#333", radius=5, width=2) + if selected_possible_words_index + 1 < len(self.possible_words): + word = self.possible_words[selected_possible_words_index + 1] + View.draw.text((center_slot[0] + level_shift_x + 5, center_slot[1] + level_shift_y + 5), word, fill=View.color, font=word_font) + + + word = self.possible_words[selected_possible_words_index] + + # Render center slot + word_font = View.ROBOTOCONDENSED_BOLD_26 + tw, th = word_font.getsize(word) + View.draw.rounded_rectangle((center_slot[0], center_slot[1], 250, center_slot[1] + 28 + 3), fill=View.color, radius=7) + View.draw.text((center_slot[0] + 5, center_slot[1]), word, fill="black", font=word_font) + + # for slot, word in enumerate(self.possible_words): + # word_offset = View.canvas_width - tw - 1 + # slot_y = 39 + (60*slot) + # if highlight_word and word == highlight_word: + # View.draw.rectangle((word_offset - 3, slot_y - 3, 240, slot_y + th + 6), fill=View.color) + # View.draw.text((word_offset, slot_y), word, fill="black", font=word_font) + # else: + # View.draw.text((word_offset, slot_y), word, fill=View.color, font=word_font) def render_previous_button(highlight=False): @@ -166,7 +205,7 @@ class SeedToolsView(View): # Draw n-1 of the selected letters word_font = View.ROBOTOCONDENSED_BOLD_26 tw, th = word_font.getsize(title[:-1]) - word_offset = int(View.canvas_width - tw + cursor_block_width)/2 + word_offset = int(View.canvas_width - tw - cursor_block_width)/2 draw.text((word_offset, 2), title[:-1], fill=View.color, font=word_font) # Draw the highlighted cursor block @@ -208,7 +247,29 @@ class SeedToolsView(View): # Start the interactive update loop while True: input = View.buttons.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_RIGHT, B.KEY_LEFT, B.KEY_PRESS, B.KEY1, B.KEY2, B.KEY3], check_release=True, release_keys=[B.KEY_PRESS, B.KEY1, B.KEY2, B.KEY3]) + + if previous_button_is_active: + if input == B.KEY_PRESS: + # User clicked the "back" arrow + return Keyboard.KEY_PREVIOUS_PAGE + elif input == B.KEY_UP: + input = Keyboard.ENTER_BOTTOM + # Re-render it without the highlight + previous_button_is_active = False + render_previous_button() + + elif input == B.KEY_DOWN: + input = Keyboard.ENTER_TOP + # Re-render it without the highlight + previous_button_is_active = False + render_previous_button() + + elif input in [B.KEY_RIGHT, B.KEY_LEFT]: + # no action in this context + continue + ret_val = keyboard.update_from_input(input) + if ret_val in Keyboard.EXIT_DIRECTIONS: render_previous_button(highlight=True) previous_button_is_active = True @@ -233,14 +294,18 @@ class SeedToolsView(View): # Has the user made a final selection of a candidate word? final_selection = None if input == B.KEY1: - if self.possible_words[0]: - final_selection = self.possible_words[0] + # Scroll the list up + selected_possible_words_index -= 1 + if selected_possible_words_index < 0: + selected_possible_words_index = 0 elif input == B.KEY2: if self.possible_words[1]: final_selection = self.possible_words[1] elif input == B.KEY3: - if self.possible_words[2]: - final_selection = self.possible_words[2] + # Scroll the list down + selected_possible_words_index += 1 + if selected_possible_words_index >= len(self.possible_words): + selected_possible_words_index = len(self.possible_words) - 1 if final_selection: # Animate the selection storage, then return the word to the caller @@ -249,10 +314,6 @@ class SeedToolsView(View): return final_selection - elif input == B.KEY_PRESS and previous_button_is_active: - # User clicked the "back" arrow - return Keyboard.KEY_PREVIOUS_PAGE - elif input == B.KEY_PRESS and ret_val in self.possible_alphabet: # User has locked in the current letter if self.letters[-1] != " ": @@ -1116,13 +1177,14 @@ class SeedToolsView(View): search_letters = self.letters[:] if new_letter == False: search_letters.pop() - possible_words = [i for i in SeedToolsView.SEEDWORDS if i.startswith("".join(search_letters).strip())] + self.possible_words = [i for i in SeedToolsView.SEEDWORDS if i.startswith("".join(search_letters).strip())] letter_num = len(search_letters) possible_letters = [] - for word in possible_words: + for word in self.possible_words: if len(word)-1 >= letter_num: possible_letters.append(word[letter_num]) # remove duplicates and keep order self.possible_alphabet = list(dict.fromkeys(possible_letters))[:] else: self.possible_alphabet = SeedToolsView.ALPHABET[:] + self.possible_words = [] diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index 9a86fd37..db2a4e1a 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -36,8 +36,11 @@ class View: font_path = os.path.join(pathlib.Path(__file__).parent.resolve(), "..", "resources", "fonts") + ROBOTOCONDENSED_BOLD_16 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Bold.ttf"), 16) ROBOTOCONDENSED_BOLD_18 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Bold.ttf"), 18) - ROBOTOCONDENSED_BOLD_24 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Bold.ttf"), 24) + ROBOTOCONDENSED_BOLD_20 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Bold.ttf"), 20) + ROBOTOCONDENSED_BOLD_22 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Bold.ttf"), 20) + ROBOTOCONDENSED_BOLD_24 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Bold.ttf"), 22) ROBOTOCONDENSED_BOLD_25 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Bold.ttf"), 25) ROBOTOCONDENSED_BOLD_26 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Bold.ttf"), 26) ROBOTOCONDENSED_LIGHT_16 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Light.ttf"), 16) From 21316a4a767236b2e6b50e27799006efc1c60fa5 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 5 Jul 2021 20:54:33 -0500 Subject: [PATCH 12/39] Adds match list on right w/up & down arrows --- src/seedsigner/views/seed_tools_view.py | 314 +++++++++++++++--------- src/seedsigner/views/view.py | 2 + 2 files changed, 195 insertions(+), 121 deletions(-) diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index f0ba157b..88587ad5 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -24,6 +24,7 @@ class SeedToolsView(View): self.letters = [] self.possible_alphabet = [] self.possible_words = [] + self.selected_possible_words_index = 0 self.seed_length = 12 # Default to 12, Valid values are 11, 12, 23 and 24 self.seed_qr_image = None @@ -104,9 +105,9 @@ class SeedToolsView(View): continue if len(self.words) < cur_word: - self.words.append(ret_val) + self.words.append(ret_val.strip()) else: - self.words[cur_word - 1] = ret_val + self.words[cur_word - 1] = ret_val.strip() cur_word += 1 return self.words @@ -120,59 +121,75 @@ class SeedToolsView(View): # Clear the right panel View.draw.rectangle((keyboard_width, text_entry_display_height, View.canvas_width, View.canvas_height), fill="black") + if not self.possible_words: + return + + row_height = 26 + x = keyboard_width + 10 + y = text_entry_display_height - int(row_height / 2) + + highlighted_row = 3 + num_possible_rows = 11 + + list_starting_index = self.selected_possible_words_index - highlighted_row + + word_font = View.ROBOTOCONDENSED_REGULAR_22 + for row, i in enumerate(range(list_starting_index, list_starting_index + num_possible_rows)): + if i < 0: + # We're near the top of the list, not enough items to fill above the highlighted row + continue + if row == highlighted_row: + # Leave the highlighted row to be rendered below + continue + + if len(self.possible_words) <= i: + break + + View.draw.text((x, y + row * row_height), self.possible_words[i], fill=View.color, font=word_font) + + # Render the SELECT outline + View.draw.rounded_rectangle((keyboard_width + 4, y + (3 * row_height) - 2, 250, y + (4 * row_height) + 2), outline=View.color, fill="#111", radius=5, width=1) + if self.possible_words: - selected_possible_words_index = 2 - if len(self.possible_words) < 3: - selected_possible_words_index = len(self.possible_words) - 1 - - center_slot = (keyboard_width + 3, 39 + 60) - - # Render level 3 - level_shift_x = 20 - level_shift_y = 26 * 2 - word_font = View.ROBOTOCONDENSED_BOLD_22 - View.draw.rounded_rectangle((center_slot[0] + level_shift_x, center_slot[1] - level_shift_y - 3, 250, center_slot[1] - level_shift_y + 26 + 3), fill="black", outline="#333", radius=5, width=2) - if selected_possible_words_index - 2 >= 0: - word = self.possible_words[selected_possible_words_index - 2] - View.draw.text((center_slot[0] + level_shift_x + 5, center_slot[1] - level_shift_y), word, fill=View.color, font=word_font) - - View.draw.rounded_rectangle((center_slot[0] + level_shift_x, center_slot[1] + level_shift_y - 3 + 6, 250, center_slot[1] + level_shift_y + 26 + 7), fill="black", outline="#333", radius=5, width=2) - if selected_possible_words_index + 2 < len(self.possible_words): - word = self.possible_words[selected_possible_words_index + 2] - View.draw.text((center_slot[0] + level_shift_x + 5, center_slot[1] + level_shift_y + 7), word, fill=View.color, font=word_font) - - - # Render level 2 - level_shift_x = 10 - level_shift_y = 28 word_font = View.ROBOTOCONDENSED_BOLD_24 - View.draw.rounded_rectangle((center_slot[0] + level_shift_x, center_slot[1] - level_shift_y - 3, 250, center_slot[1] - level_shift_y + 26 + 3), fill="black", outline="#333", radius=5, width=2) - if selected_possible_words_index - 1 >= 0: - word = self.possible_words[selected_possible_words_index - 1] - View.draw.text((center_slot[0] + level_shift_x + 5, center_slot[1] - level_shift_y), word, fill=View.color, font=word_font) + View.draw.text((x, y + 3 * row_height), self.possible_words[self.selected_possible_words_index], fill=View.color, font=word_font) - View.draw.rounded_rectangle((center_slot[0] + level_shift_x, center_slot[1] + level_shift_y - 3 + 6, 250, center_slot[1] + level_shift_y + 26 + 7), fill="black", outline="#333", radius=5, width=2) - if selected_possible_words_index + 1 < len(self.possible_words): - word = self.possible_words[selected_possible_words_index + 1] - View.draw.text((center_slot[0] + level_shift_x + 5, center_slot[1] + level_shift_y + 5), word, fill=View.color, font=word_font) + render_possible_matches_arrows() - word = self.possible_words[selected_possible_words_index] + def render_possible_matches_arrows(): + # Render the up/down arrow buttons for KEY1 and KEY3 + row_height = 26 + arrow_button_width = 25 + arrow_padding = 5 + key_x = View.canvas_width - arrow_button_width + key_y = text_entry_display_height - int(row_height / 2) + int(0.75 * row_height) + background_color = "#111" + arrow_color = View.color + if arrow_up_is_active: + background_color = View.color + arrow_color = "#111" + View.draw.rounded_rectangle((key_x, key_y, 250, key_y + row_height), outline=View.color, fill=background_color, radius=5, width=1) + View.draw.polygon( + [(key_x + int(arrow_button_width)/2 + 1, key_y + arrow_padding), # centered top point + (View.canvas_width - arrow_padding + 1, key_y + row_height - arrow_padding), # bottom right point + (key_x + arrow_padding + 1, key_y + row_height - arrow_padding)], # bottom left point + fill=arrow_color + ) - # Render center slot - word_font = View.ROBOTOCONDENSED_BOLD_26 - tw, th = word_font.getsize(word) - View.draw.rounded_rectangle((center_slot[0], center_slot[1], 250, center_slot[1] + 28 + 3), fill=View.color, radius=7) - View.draw.text((center_slot[0] + 5, center_slot[1]), word, fill="black", font=word_font) - - # for slot, word in enumerate(self.possible_words): - # word_offset = View.canvas_width - tw - 1 - # slot_y = 39 + (60*slot) - # if highlight_word and word == highlight_word: - # View.draw.rectangle((word_offset - 3, slot_y - 3, 240, slot_y + th + 6), fill=View.color) - # View.draw.text((word_offset, slot_y), word, fill="black", font=word_font) - # else: - # View.draw.text((word_offset, slot_y), word, fill=View.color, font=word_font) + background_color = "#111" + arrow_color = View.color + if arrow_down_is_active: + background_color = View.color + arrow_color = "#111" + key_y = text_entry_display_height - int(row_height / 2) + int(5.25 * row_height) + View.draw.rounded_rectangle((key_x, key_y, 250, key_y + row_height), outline=View.color, fill=background_color, radius=5, width=1) + View.draw.polygon( + [(key_x + int(arrow_button_width)/2 + 1, key_y + row_height - arrow_padding), # bottom centered point + (View.canvas_width - arrow_padding + 1, key_y + arrow_padding), # right top point + (key_x + arrow_padding + 1, key_y + arrow_padding)], # left top point + fill=arrow_color + ) def render_previous_button(highlight=False): @@ -199,7 +216,7 @@ class SeedToolsView(View): # Render the live text entry display title = f"#{num_word}: {''.join(self.letters)}" - cursor_block_width = 20 + cursor_block_width = 18 cursor_block_height = 33 # Draw n-1 of the selected letters @@ -209,15 +226,22 @@ class SeedToolsView(View): draw.text((word_offset, 2), title[:-1], fill=View.color, font=word_font) # Draw the highlighted cursor block - cursor_block_offset = word_offset + tw + 2 - draw.rectangle((cursor_block_offset,2, cursor_block_offset + cursor_block_width, cursor_block_height), fill=View.color) - draw.text((cursor_block_offset + 1, 2), ''.join(self.letters[-1:]), fill="black", font=word_font) + cursor_block_offset = word_offset + tw - 1 + draw.rectangle((cursor_block_offset,2, cursor_block_offset + cursor_block_width, cursor_block_height), fill="#111") + draw.text((cursor_block_offset + 1, 2), ''.join(self.letters[-1:]), fill=View.color, font=word_font) # Clear the screen View.draw.rectangle((0,0, View.canvas_width,View.canvas_height), fill="black") + previous_button_width = render_previous_button() previous_button_is_active = False + arrow_up_is_active = False + arrow_down_is_active = False + + # Have to ensure that we don't carry any effects from a previous run + # TODO: This shouldn't be a member var + self.possible_alphabet = "abcdefghijklmnopqrstuvwxyz" # Set up the keyboard params keyboard_width = 120 @@ -225,7 +249,7 @@ class SeedToolsView(View): # TODO: support other BIP39 languages/charsets keyboard = Keyboard(View.draw, - charset="abcdefghijklmnopqrstuvwxyz", + charset=self.possible_alphabet, rows=5, cols=6, rect=(0,text_entry_display_height + 1, keyboard_width,240), @@ -233,11 +257,17 @@ class SeedToolsView(View): # Initialize the current letters/current matches self.letters = initial_letters - self.calc_possible_alphabet() - keyboard.update_active_keys(active_keys=self.possible_alphabet) - keyboard.set_selected_key(selected_letter=self.letters[-1]) + self.calc_possible_words() # Force it to match on even just the default ["a"] + if len(self.letters) > 1: + self.letters.append(" ") # "Lock in" the last letter as if KEY_PRESS + self.calc_possible_alphabet() + keyboard.update_active_keys(active_keys=self.possible_alphabet) + keyboard.set_selected_key(selected_letter=self.letters[-2]) + else: + keyboard.set_selected_key(selected_letter=self.letters[-1]) keyboard.render_keys() render_possible_matches() + render_possible_matches_arrows() # Render the top text entry display render_text_entry_display() @@ -246,7 +276,11 @@ class SeedToolsView(View): # Start the interactive update loop while True: - input = View.buttons.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_RIGHT, B.KEY_LEFT, B.KEY_PRESS, B.KEY1, B.KEY2, B.KEY3], check_release=True, release_keys=[B.KEY_PRESS, B.KEY1, B.KEY2, B.KEY3]) + input = View.buttons.wait_for( + [B.KEY_UP, B.KEY_DOWN, B.KEY_RIGHT, B.KEY_LEFT, B.KEY_PRESS, B.KEY1, B.KEY2, B.KEY3], + check_release=True, + release_keys=[B.KEY_PRESS, B.KEY2] + ) if previous_button_is_active: if input == B.KEY_PRESS: @@ -283,6 +317,9 @@ class SeedToolsView(View): self.calc_possible_alphabet() keyboard.update_active_keys(active_keys=self.possible_alphabet) keyboard.render_keys() + + # Update the right-hand possible matches area + render_possible_matches() elif ret_val == Keyboard.KEY_BACKSPACE["letter"]: # We're just hovering over DEL but haven't clicked. Show blank (" ") @@ -290,77 +327,106 @@ class SeedToolsView(View): self.letters = self.letters[:-1] self.letters.append(" ") - else: - # Has the user made a final selection of a candidate word? - final_selection = None - if input == B.KEY1: - # Scroll the list up - selected_possible_words_index -= 1 - if selected_possible_words_index < 0: - selected_possible_words_index = 0 - elif input == B.KEY2: - if self.possible_words[1]: - final_selection = self.possible_words[1] - elif input == B.KEY3: - # Scroll the list down - selected_possible_words_index += 1 - if selected_possible_words_index >= len(self.possible_words): - selected_possible_words_index = len(self.possible_words) - 1 + # Has the user made a final selection of a candidate word? + final_selection = None + if input == B.KEY1: + # Scroll the list up + self.selected_possible_words_index -= 1 + if self.selected_possible_words_index < 0: + self.selected_possible_words_index = 0 - if final_selection: - # Animate the selection storage, then return the word to the caller - render_possible_matches(highlight_word=final_selection) - View.DispShowImage() + if not arrow_up_is_active: + # Flash the up arrow as selected + arrow_up_is_active = True - return final_selection + # Update the right-hand possible matches area + render_possible_matches() - elif input == B.KEY_PRESS and ret_val in self.possible_alphabet: - # User has locked in the current letter - if self.letters[-1] != " ": - # We'll save that locked in letter next but for now update the - # live text entry display with blank (" ") so that we don't try - # to autocalc matches against a second copy of the letter they - # just selected. e.g. They KEY_PRESS on "s" to build "mus". If - # we advance the live block cursor AND display "s" in it, the - # current word would then be "muss" with no matches. If "mus" - # can get us to our match, we don't want it to disappear right - # as we KEY_PRESS. - self.letters.append(" ") - else: - # clicked same letter twice in a row. Because of the above, an - # immediate second click of the same letter would lock in "ap " - # (note the space) instead of "app". So we replace that trailing - # space with the correct repeated letter and then, as above, - # append a trailing blank. - self.letters = self.letters[:-1] - self.letters.append(ret_val) - self.letters.append(" ") + elif input == B.KEY2: + final_selection = self.possible_words[self.selected_possible_words_index] - # Recalc and deactivate keys after advancing - self.calc_possible_alphabet() - keyboard.update_active_keys(active_keys=self.possible_alphabet) + elif input == B.KEY3: + # Scroll the list down + self.selected_possible_words_index += 1 + if self.selected_possible_words_index >= len(self.possible_words): + self.selected_possible_words_index = len(self.possible_words) - 1 - if len(self.possible_alphabet) == 1: - # If there's only one possible letter left, select it - keyboard.set_selected_key(self.possible_alphabet[0]) + if not arrow_down_is_active: + # Flash the down arrow as selected + arrow_down_is_active = True - keyboard.render_keys() + # Update the right-hand possible matches area + render_possible_matches() + if input is not B.KEY1 and arrow_up_is_active: + # Deactivate the arrow and redraw + arrow_up_is_active = False + render_possible_matches_arrows() + + if input is not B.KEY3 and arrow_down_is_active: + # Deactivate the arrow and redraw + arrow_down_is_active = False + render_possible_matches_arrows() + + if final_selection: + # Animate the selection storage, then return the word to the caller + render_possible_matches(highlight_word=final_selection) + View.DispShowImage() + + return final_selection + + elif input == B.KEY_PRESS and ret_val in self.possible_alphabet: + # User has locked in the current letter + if self.letters[-1] != " ": + # We'll save that locked in letter next but for now update the + # live text entry display with blank (" ") so that we don't try + # to autocalc matches against a second copy of the letter they + # just selected. e.g. They KEY_PRESS on "s" to build "mus". If + # we advance the live block cursor AND display "s" in it, the + # current word would then be "muss" with no matches. If "mus" + # can get us to our match, we don't want it to disappear right + # as we KEY_PRESS. + self.letters.append(" ") else: - # Live joystick movement; haven't locked this new letter in yet. - # Replace the last letter w/the currently selected one. But don't - # call `calc_possible_alphabet()` because we want to still be able - # to freely float to a different letter; only update the active - # keyboard keys when a selection has been locked in (KEY_PRESS) or - # removed ("del"). + # clicked same letter twice in a row. Because of the above, an + # immediate second click of the same letter would lock in "ap " + # (note the space) instead of "app". So we replace that trailing + # space with the correct repeated letter and then, as above, + # append a trailing blank. self.letters = self.letters[:-1] - if ret_val in self.possible_alphabet: - self.letters.append(ret_val) - else: - self.letters.append(" ") + self.letters.append(ret_val) + self.letters.append(" ") - # Update the KEY 1, 2, 3 word candidates - render_possible_matches() + # Recalc and deactivate keys after advancing + self.calc_possible_alphabet() + keyboard.update_active_keys(active_keys=self.possible_alphabet) + + # Update the right-hand possible matches area + render_possible_matches() + + if len(self.possible_alphabet) == 1: + # If there's only one possible letter left, select it + keyboard.set_selected_key(self.possible_alphabet[0]) + + keyboard.render_keys() + + elif input in [B.KEY_RIGHT, B.KEY_LEFT, B.KEY_UP, B.KEY_DOWN] and ret_val in self.possible_alphabet: + # Live joystick movement; haven't locked this new letter in yet. + # Replace the last letter w/the currently selected one. But don't + # call `calc_possible_alphabet()` because we want to still be able + # to freely float to a different letter; only update the active + # keyboard keys when a selection has been locked in (KEY_PRESS) or + # removed ("del"). + self.letters = self.letters[:-1] + if ret_val in self.possible_alphabet: + self.letters.append(ret_val) + self.calc_possible_words() # live update our matches as we move + + # Update the right-hand possible matches area + render_possible_matches() + else: + # Do nothing when hovering over an inactive letter + self.letters.append(" ") # Render the text entry display and cursor block render_text_entry_display() @@ -1158,10 +1224,10 @@ class SeedToolsView(View): return self.words + ### ### Utility Methods ### - def reset(self): self.words.clear() self.possible_alphabet = SeedToolsView.ALPHABET[:] @@ -1172,12 +1238,13 @@ class SeedToolsView(View): return + def calc_possible_alphabet(self, new_letter = False): if (self.letters and len(self.letters) > 1 and new_letter == False) or (len(self.letters) > 0 and new_letter == True): search_letters = self.letters[:] if new_letter == False: search_letters.pop() - self.possible_words = [i for i in SeedToolsView.SEEDWORDS if i.startswith("".join(search_letters).strip())] + self.calc_possible_words() letter_num = len(search_letters) possible_letters = [] for word in self.possible_words: @@ -1188,3 +1255,8 @@ class SeedToolsView(View): else: self.possible_alphabet = SeedToolsView.ALPHABET[:] self.possible_words = [] + + + def calc_possible_words(self): + self.possible_words = [i for i in SeedToolsView.SEEDWORDS if i.startswith("".join(self.letters).strip())] + self.selected_possible_words_index = 0 diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index db2a4e1a..aa864e0b 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -47,7 +47,9 @@ class View: ROBOTOCONDENSED_LIGHT_24 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Light.ttf"), 24) ROBOTOCONDENSED_REGULAR_16 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Regular.ttf"), 16) ROBOTOCONDENSED_REGULAR_20 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Regular.ttf"), 20) + ROBOTOCONDENSED_REGULAR_22 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Regular.ttf"), 22) ROBOTOCONDENSED_REGULAR_24 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Regular.ttf"), 24) + ROBOTOCONDENSED_REGULAR_26 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Regular.ttf"), 26) RST = 27 DC = 25 From 9705257149bad1e933d2d266bea65c5ec2099527 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 5 Jul 2021 21:27:15 -0500 Subject: [PATCH 13/39] Tweaks, bugfixes --- src/seedsigner/views/seed_tools_view.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index 88587ad5..38273e76 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -148,11 +148,17 @@ class SeedToolsView(View): View.draw.text((x, y + row * row_height), self.possible_words[i], fill=View.color, font=word_font) # Render the SELECT outline - View.draw.rounded_rectangle((keyboard_width + 4, y + (3 * row_height) - 2, 250, y + (4 * row_height) + 2), outline=View.color, fill="#111", radius=5, width=1) + if highlight_word: + fill_color = View.color + font_color = "black" + else: + fill_color = "#111" + font_color = View.color + View.draw.rounded_rectangle((keyboard_width + 4, y + (3 * row_height) - 2, 250, y + (4 * row_height) + 2), outline=View.color, fill=fill_color, radius=5, width=1) if self.possible_words: word_font = View.ROBOTOCONDENSED_BOLD_24 - View.draw.text((x, y + 3 * row_height), self.possible_words[self.selected_possible_words_index], fill=View.color, font=word_font) + View.draw.text((x, y + 3 * row_height), self.possible_words[self.selected_possible_words_index], fill=font_color, font=word_font) render_possible_matches_arrows() @@ -257,7 +263,7 @@ class SeedToolsView(View): # Initialize the current letters/current matches self.letters = initial_letters - self.calc_possible_words() # Force it to match on even just the default ["a"] + self.possible_words = [] if len(self.letters) > 1: self.letters.append(" ") # "Lock in" the last letter as if KEY_PRESS self.calc_possible_alphabet() @@ -267,7 +273,6 @@ class SeedToolsView(View): keyboard.set_selected_key(selected_letter=self.letters[-1]) keyboard.render_keys() render_possible_matches() - render_possible_matches_arrows() # Render the top text entry display render_text_entry_display() @@ -329,7 +334,7 @@ class SeedToolsView(View): # Has the user made a final selection of a candidate word? final_selection = None - if input == B.KEY1: + if input == B.KEY1 and self.possible_words: # Scroll the list up self.selected_possible_words_index -= 1 if self.selected_possible_words_index < 0: @@ -343,9 +348,10 @@ class SeedToolsView(View): render_possible_matches() elif input == B.KEY2: - final_selection = self.possible_words[self.selected_possible_words_index] + if self.possible_words: + final_selection = self.possible_words[self.selected_possible_words_index] - elif input == B.KEY3: + elif input == B.KEY3 and self.possible_words: # Scroll the list down self.selected_possible_words_index += 1 if self.selected_possible_words_index >= len(self.possible_words): @@ -370,7 +376,9 @@ class SeedToolsView(View): if final_selection: # Animate the selection storage, then return the word to the caller + self.letters = list(final_selection + " ") render_possible_matches(highlight_word=final_selection) + render_text_entry_display() View.DispShowImage() return final_selection From 029295172c8993b3633cb991d95cceabc837eb56 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 5 Jul 2021 21:50:36 -0500 Subject: [PATCH 14/39] Matches hold state when navigating back to same letter --- src/seedsigner/views/seed_tools_view.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index 38273e76..c876fe8a 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -425,8 +425,10 @@ class SeedToolsView(View): # to freely float to a different letter; only update the active # keyboard keys when a selection has been locked in (KEY_PRESS) or # removed ("del"). - self.letters = self.letters[:-1] - if ret_val in self.possible_alphabet: + # If you're returning to the same letter (e.g. "v" to inactive, back + # to "v", don't recalc and re-render). + if ret_val in self.possible_alphabet and ret_val != self.letters[-1]: + self.letters = self.letters[:-1] self.letters.append(ret_val) self.calc_possible_words() # live update our matches as we move @@ -434,7 +436,8 @@ class SeedToolsView(View): render_possible_matches() else: # Do nothing when hovering over an inactive letter - self.letters.append(" ") + # self.letters.append(" ") + pass # Render the text entry display and cursor block render_text_entry_display() From 226d46841a683b059e4b8b4f0c4be16079559fb1 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 5 Jul 2021 21:56:51 -0500 Subject: [PATCH 15/39] Reverting previous; simplified logic --- src/seedsigner/views/seed_tools_view.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index c876fe8a..ea5b3519 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -425,19 +425,12 @@ class SeedToolsView(View): # to freely float to a different letter; only update the active # keyboard keys when a selection has been locked in (KEY_PRESS) or # removed ("del"). - # If you're returning to the same letter (e.g. "v" to inactive, back - # to "v", don't recalc and re-render). - if ret_val in self.possible_alphabet and ret_val != self.letters[-1]: - self.letters = self.letters[:-1] - self.letters.append(ret_val) - self.calc_possible_words() # live update our matches as we move + self.letters = self.letters[:-1] + self.letters.append(ret_val) + self.calc_possible_words() # live update our matches as we move - # Update the right-hand possible matches area - render_possible_matches() - else: - # Do nothing when hovering over an inactive letter - # self.letters.append(" ") - pass + # Update the right-hand possible matches area + render_possible_matches() # Render the text entry display and cursor block render_text_entry_display() From cdd7b99a952f76906aeba52c2788480f09951b0e Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 5 Jul 2021 22:33:37 -0500 Subject: [PATCH 16/39] Prevent fiddly joystick by requiring release --- src/seedsigner/views/seed_tools_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index ea5b3519..a2ef4363 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -284,7 +284,7 @@ class SeedToolsView(View): input = View.buttons.wait_for( [B.KEY_UP, B.KEY_DOWN, B.KEY_RIGHT, B.KEY_LEFT, B.KEY_PRESS, B.KEY1, B.KEY2, B.KEY3], check_release=True, - release_keys=[B.KEY_PRESS, B.KEY2] + release_keys=[B.KEY_UP, B.KEY_DOWN, B.KEY_RIGHT, B.KEY_LEFT, B.KEY_PRESS, B.KEY2] ) if previous_button_is_active: From ab3cb06f4e2a8f72b2deeda369a56672e34c8de9 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Thu, 8 Jul 2021 22:13:23 -0500 Subject: [PATCH 17/39] Generalizing reusable components --- src/seedsigner/helpers/__init__.py | 2 +- src/seedsigner/helpers/keyboard.py | 33 ++++++++ src/seedsigner/views/seed_tools_view.py | 107 +++++++++++++----------- src/seedsigner/views/view.py | 22 +++++ tools/seed_phrase_to_qr.py | 19 +++++ 5 files changed, 131 insertions(+), 52 deletions(-) create mode 100644 tools/seed_phrase_to_qr.py diff --git a/src/seedsigner/helpers/__init__.py b/src/seedsigner/helpers/__init__.py index f5ebe908..4120967a 100644 --- a/src/seedsigner/helpers/__init__.py +++ b/src/seedsigner/helpers/__init__.py @@ -1,6 +1,6 @@ from .buttons import * from .camera_process import * -from .keyboard import Keyboard +from .keyboard import Keyboard, TextEntryDisplay from .path import * from .qr import * from .ST7789 import * \ No newline at end of file diff --git a/src/seedsigner/helpers/keyboard.py b/src/seedsigner/helpers/keyboard.py index 680f2fbc..3d62c32e 100644 --- a/src/seedsigner/helpers/keyboard.py +++ b/src/seedsigner/helpers/keyboard.py @@ -341,3 +341,36 @@ class Keyboard: raise Exception(f"""`selected_letter` "{selected_letter}" not found in keyboard""") + +@dataclass +class TextEntryDisplay: + draw: any + rect: (int,int,int,int) + font: any + font_color: any + is_centered: bool = True + cur_text: str = " " + + + def render(self, cur_text=None): + """ Render the live text entry display """ + if cur_text: + self.cur_text = cur_text + self.draw.rectangle(self.rect, fill="black") + + cursor_block_width = 18 + cursor_block_height = 33 + + # Draw n-1 of the selected letters + tw, th = self.font.getsize(self.cur_text[:-1]) + if self.is_centered: + word_offset = int(self.rect[2] - tw - cursor_block_width)/2 + else: + word_offset = 0 + self.draw.text((word_offset, 2), self.cur_text[:-1], fill=self.font_color, font=self.font) + + # Draw the highlighted cursor block + cursor_block_offset = word_offset + tw - 1 + self.draw.rectangle((cursor_block_offset,2, cursor_block_offset + cursor_block_width, cursor_block_height), fill="#111") + self.draw.text((cursor_block_offset + 1, 2), self.cur_text[-1], fill=self.font_color, font=self.font) + diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index a2ef4363..bef96e26 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -7,7 +7,7 @@ import time # Internal file class dependencies from . import View -from seedsigner.helpers import B, QR, CameraPoll, Keyboard +from seedsigner.helpers import B, QR, CameraPoll, Keyboard, TextEntryDisplay class SeedToolsView(View): @@ -198,49 +198,10 @@ class SeedToolsView(View): ) - def render_previous_button(highlight=False): - # Set up the "back" arrow in the upper left - arrow = "<" - word_font = View.ROBOTOCONDENSED_BOLD_26 - tw, th = word_font.getsize(arrow) - if highlight: - View.draw.rectangle((0,0, tw + 6, th + 6), fill=View.color) - View.draw.text((3, 3), arrow, fill="black", font=word_font) - else: - View.draw.rectangle((0,0, tw + 6, th + 6), fill="black") - View.draw.text((3, 3), arrow, fill=View.color, font=word_font) - - return tw + 6 - - - def render_text_entry_display(draw=View.draw): - """ Internal helper method to render the live text entry display at the top. - (has access to all vars in the parent's context) - """ - draw.rectangle((previous_button_width,0, View.canvas_width,text_entry_display_height), fill="black") - - # Render the live text entry display - title = f"#{num_word}: {''.join(self.letters)}" - - cursor_block_width = 18 - cursor_block_height = 33 - - # Draw n-1 of the selected letters - word_font = View.ROBOTOCONDENSED_BOLD_26 - tw, th = word_font.getsize(title[:-1]) - word_offset = int(View.canvas_width - tw - cursor_block_width)/2 - draw.text((word_offset, 2), title[:-1], fill=View.color, font=word_font) - - # Draw the highlighted cursor block - cursor_block_offset = word_offset + tw - 1 - draw.rectangle((cursor_block_offset,2, cursor_block_offset + cursor_block_width, cursor_block_height), fill="#111") - draw.text((cursor_block_offset + 1, 2), ''.join(self.letters[-1:]), fill=View.color, font=word_font) - - # Clear the screen View.draw.rectangle((0,0, View.canvas_width,View.canvas_height), fill="black") - previous_button_width = render_previous_button() + self.render_previous_button() previous_button_is_active = False arrow_up_is_active = False arrow_down_is_active = False @@ -261,8 +222,18 @@ class SeedToolsView(View): rect=(0,text_entry_display_height + 1, keyboard_width,240), auto_wrap=[Keyboard.WRAP_LEFT, Keyboard.WRAP_RIGHT]) - # Initialize the current letters/current matches + # Render the top text entry display self.letters = initial_letters + text_entry_display = TextEntryDisplay( + View.draw, + rect=(self.previous_button_width,0, View.canvas_width,text_entry_display_height), + font=View.ROBOTOCONDENSED_BOLD_26, + font_color=View.color, + cur_text="".join(self.letters) + ) + text_entry_display.render() + + # Initialize the current matches self.possible_words = [] if len(self.letters) > 1: self.letters.append(" ") # "Lock in" the last letter as if KEY_PRESS @@ -274,9 +245,6 @@ class SeedToolsView(View): keyboard.render_keys() render_possible_matches() - # Render the top text entry display - render_text_entry_display() - View.DispShowImage() # Start the interactive update loop @@ -295,13 +263,13 @@ class SeedToolsView(View): input = Keyboard.ENTER_BOTTOM # Re-render it without the highlight previous_button_is_active = False - render_previous_button() + self.render_previous_button() elif input == B.KEY_DOWN: input = Keyboard.ENTER_TOP # Re-render it without the highlight previous_button_is_active = False - render_previous_button() + self.render_previous_button() elif input in [B.KEY_RIGHT, B.KEY_LEFT]: # no action in this context @@ -310,7 +278,7 @@ class SeedToolsView(View): ret_val = keyboard.update_from_input(input) if ret_val in Keyboard.EXIT_DIRECTIONS: - render_previous_button(highlight=True) + self.render_previous_button(highlight=True) previous_button_is_active = True elif ret_val in Keyboard.ADDITIONAL_KEYS: @@ -378,7 +346,7 @@ class SeedToolsView(View): # Animate the selection storage, then return the word to the caller self.letters = list(final_selection + " ") render_possible_matches(highlight_word=final_selection) - render_text_entry_display() + text_entry_display.render("".join(self.letters)) View.DispShowImage() return final_selection @@ -433,11 +401,48 @@ class SeedToolsView(View): render_possible_matches() # Render the text entry display and cursor block - render_text_entry_display() + text_entry_display.render("".join(self.letters)) View.DispShowImage() + def draw_passphrase_keyboard_entry(self): + + # Clear the screen + View.draw.rectangle((0,0, View.canvas_width,View.canvas_height), fill="black") + + self.render_previous_button() + previous_button_is_active = False + + # Have to ensure that we don't carry any effects from a previous run + # TODO: This shouldn't be a member var + self.passphrase = " " + + # Set up the keyboard params + right_key_nav_width = 50 + keyboard_width = View.canvas_width - right_key_nav_width + text_entry_display_height = 39 + + keyboard = Keyboard(View.draw, + charset=list("1234567890").append(SeedToolsView.ALPHABET), + rows=4, + cols=10, + rect=(0,text_entry_display_height + 1, keyboard_width,240), + auto_wrap=[Keyboard.WRAP_LEFT, Keyboard.WRAP_RIGHT, Keyboard.WRAP_TOP, Keyboard.WRAP_BOTTOM]) + + # Render the top text entry display + text_entry_display = TextEntryDisplay( + View.draw, + rect=(self.previous_button_width,0, View.canvas_width,text_entry_display_height), + font=View.ROBOTOCONDENSED_BOLD_26, + font_color=View.color, + is_centered=False, + cur_text=''.join(self.passphrase) + ) + text_entry_display.render() + + View.DispShowImage() + def gather_words_up(self): View.draw.polygon([(8 + ((len(self.letters)-1)*30), 85) , (14 + ((len(self.letters)-1)*30), 69) , (20 + ((len(self.letters)-1)*30), 85 )], outline=View.color, fill=View.color) View.DispShowImage() @@ -551,10 +556,10 @@ class SeedToolsView(View): return + ### ### Display Gather PassPhrase Screen ### - def display_gather_passphrase_screen(self, slot_num = 0) -> str: self.reset() self.pass_letter = "a" diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index aa864e0b..c5065055 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -64,6 +64,7 @@ class View: bus = 0 device = 0 disp = None + previous_button_width = None def __init__(self, controller) -> None: @@ -259,3 +260,24 @@ class View: def display_blank_screen(self): View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) View.DispShowImage() + + + ### + ### Reusable components + ### + def render_previous_button(self, highlight=False): + # Set up the "back" arrow in the upper left + arrow = "<" + word_font = View.ROBOTOCONDENSED_BOLD_26 + padding = 3 + tw, th = word_font.getsize(arrow) + self.previous_button_width = tw + 2 * padding + if highlight: + View.draw.rectangle((0,0, tw + 2 * padding, th + 2 * padding), fill=View.color) + View.draw.text((padding, padding), arrow, fill="black", font=word_font) + else: + View.draw.rectangle((0,0, tw + 2 * padding, th + 2 * padding), fill="black") + View.draw.text((padding, padding), arrow, fill=View.color, font=word_font) + + + diff --git a/tools/seed_phrase_to_qr.py b/tools/seed_phrase_to_qr.py new file mode 100644 index 00000000..0c818c75 --- /dev/null +++ b/tools/seed_phrase_to_qr.py @@ -0,0 +1,19 @@ + +if __name__ == "__main__": + import qrcode + import sys + from embit import bip39 + + print(sys.argv) + seed_phrase = sys.argv[1:] + print(seed_phrase) + + data = "" + for word in seed_phrase: + index = bip39.WORDLIST.index(word) + data += str("%04d" % index) + + qr = qrcode.QRCode( version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=5, border=3) + qr.add_data(data) + qr.make(fit=True) + qr.make_image(fill_color="black", back_color="white").resize((240,240)).convert('RGB').show() From e09c2467b3e0bca4c95f3c1855ef1d2f00cb0e9d Mon Sep 17 00:00:00 2001 From: kdmukai Date: Thu, 8 Jul 2021 22:36:06 -0500 Subject: [PATCH 18/39] Basic passphrase key entry working --- src/seedsigner/controller.py | 3 +- src/seedsigner/helpers/keyboard.py | 10 ++-- src/seedsigner/views/seed_tools_view.py | 66 +++++++++++++++++++++++-- 3 files changed, 70 insertions(+), 9 deletions(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 68cb7b83..c562324f 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -273,7 +273,8 @@ class Controller: slot_num = ret_val # display a tool to pick letters/numbers to make a passphrase - passphrase = self.seed_tools_view.display_gather_passphrase_screen() + # passphrase = self.seed_tools_view.display_gather_passphrase_screen() + passphrase = self.seed_tools_view.draw_passphrase_keyboard_entry() if len(passphrase) == 0: return Path.SEED_TOOLS_SUB_MENU diff --git a/src/seedsigner/helpers/keyboard.py b/src/seedsigner/helpers/keyboard.py index 3d62c32e..a055800d 100644 --- a/src/seedsigner/helpers/keyboard.py +++ b/src/seedsigner/helpers/keyboard.py @@ -364,13 +364,13 @@ class TextEntryDisplay: # Draw n-1 of the selected letters tw, th = self.font.getsize(self.cur_text[:-1]) if self.is_centered: - word_offset = int(self.rect[2] - tw - cursor_block_width)/2 + word_offset = self.rect[0] + int(self.rect[2] - self.rect[0] - tw - cursor_block_width)/2 else: - word_offset = 0 - self.draw.text((word_offset, 2), self.cur_text[:-1], fill=self.font_color, font=self.font) + word_offset = self.rect[0] + self.draw.text((word_offset, self.rect[1] + 2), self.cur_text[:-1], fill=self.font_color, font=self.font) # Draw the highlighted cursor block cursor_block_offset = word_offset + tw - 1 - self.draw.rectangle((cursor_block_offset,2, cursor_block_offset + cursor_block_width, cursor_block_height), fill="#111") - self.draw.text((cursor_block_offset + 1, 2), self.cur_text[-1], fill=self.font_color, font=self.font) + self.draw.rectangle((cursor_block_offset, self.rect[1] + 2, cursor_block_offset + cursor_block_width, cursor_block_height), fill="#111") + self.draw.text((cursor_block_offset + 1, self.rect[1] + 2), self.cur_text[-1], fill=self.font_color, font=self.font) diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index bef96e26..b21064ea 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -424,11 +424,11 @@ class SeedToolsView(View): text_entry_display_height = 39 keyboard = Keyboard(View.draw, - charset=list("1234567890").append(SeedToolsView.ALPHABET), + charset="1234567890" + "".join(SeedToolsView.ALPHABET), rows=4, cols=10, - rect=(0,text_entry_display_height + 1, keyboard_width,240), - auto_wrap=[Keyboard.WRAP_LEFT, Keyboard.WRAP_RIGHT, Keyboard.WRAP_TOP, Keyboard.WRAP_BOTTOM]) + rect=(0, int(1.5 * text_entry_display_height), keyboard_width,240), + auto_wrap=[Keyboard.WRAP_LEFT, Keyboard.WRAP_RIGHT]) # Render the top text entry display text_entry_display = TextEntryDisplay( @@ -443,6 +443,66 @@ class SeedToolsView(View): View.DispShowImage() + # Start the interactive update loop + while True: + input = View.buttons.wait_for( + [B.KEY_UP, B.KEY_DOWN, B.KEY_RIGHT, B.KEY_LEFT, B.KEY_PRESS, B.KEY1, B.KEY2, B.KEY3] + ) + + ret_val = keyboard.update_from_input(input) + + if ret_val in Keyboard.EXIT_DIRECTIONS: + self.render_previous_button(highlight=True) + previous_button_is_active = True + + elif ret_val in Keyboard.ADDITIONAL_KEYS: + if input == B.KEY_PRESS and ret_val == Keyboard.KEY_BACKSPACE["letter"]: + self.passphrase = self.passphrase[:-2] + self.passphrase += " " + + elif ret_val == Keyboard.KEY_BACKSPACE["letter"]: + # We're just hovering over DEL but haven't clicked. Show blank (" ") + # in the live text entry display at the top. + self.passphrase = self.passphrase[:-1] + self.passphrase += " " + + + elif input == B.KEY_PRESS and ret_val not in Keyboard.ADDITIONAL_KEYS: + # User has locked in the current letter + if self.passphrase[-1] != " ": + # We'll save that locked in letter next but for now update the + # live text entry display with blank (" ") so that we don't try + # to autocalc matches against a second copy of the letter they + # just selected. e.g. They KEY_PRESS on "s" to build "mus". If + # we advance the live block cursor AND display "s" in it, the + # current word would then be "muss" with no matches. If "mus" + # can get us to our match, we don't want it to disappear right + # as we KEY_PRESS. + self.passphrase += " " + else: + # clicked same letter twice in a row. Because of the above, an + # immediate second click of the same letter would lock in "ap " + # (note the space) instead of "app". So we replace that trailing + # space with the correct repeated letter and then, as above, + # append a trailing blank. + self.passphrase = self.passphrase[:-1] + self.passphrase += ret_val + self.passphrase += " " + + elif input in [B.KEY_RIGHT, B.KEY_LEFT, B.KEY_UP, B.KEY_DOWN]: + # Live joystick movement; haven't locked this new letter in yet. + # Replace the last letter w/the currently selected one. Only update + # the active keyboard keys when a selection has been locked in + # (KEY_PRESS) or removed ("del"). + self.passphrase = self.passphrase[:-1] + self.passphrase += ret_val + + # Render the text entry display and cursor block + text_entry_display.render(self.passphrase) + + View.DispShowImage() + + def gather_words_up(self): View.draw.polygon([(8 + ((len(self.letters)-1)*30), 85) , (14 + ((len(self.letters)-1)*30), 69) , (20 + ((len(self.letters)-1)*30), 85 )], outline=View.color, fill=View.color) View.DispShowImage() From cb2b65ae34373947c533960f83382cec325ec6f4 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 10 Jul 2021 01:04:26 -0500 Subject: [PATCH 19/39] Multiple keyboards working --- src/seedsigner/controller.py | 18 ++-- src/seedsigner/helpers/keyboard.py | 58 ++++++++-- src/seedsigner/views/seed_tools_view.py | 136 +++++++++++++++++++++--- src/seedsigner/views/view.py | 18 ++-- 4 files changed, 189 insertions(+), 41 deletions(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index c562324f..cde2e172 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -260,17 +260,17 @@ class Controller: ### Add a PassPhrase Menu def show_add_a_passphrase_tool(self): - if self.storage.num_of_saved_seeds() == 0: - self.menu_view.draw_modal(["Store a seed phrase", "prior to adding", "a passphrase"], "Error", "Right to Continue") - self.buttons.wait_for([B.KEY_RIGHT]) - return Path.SEED_TOOLS_SUB_MENU + # if self.storage.num_of_saved_seeds() == 0: + # self.menu_view.draw_modal(["Store a seed phrase", "prior to adding", "a passphrase"], "Error", "Right to Continue") + # self.buttons.wait_for([B.KEY_RIGHT]) + # return Path.SEED_TOOLS_SUB_MENU - ret_val = 0 - ret_val = self.menu_view.display_saved_seed_menu(self.storage, 3, None) - if ret_val == 0: - return Path.SEED_TOOLS_SUB_MENU + # ret_val = 0 + # ret_val = self.menu_view.display_saved_seed_menu(self.storage, 3, None) + # if ret_val == 0: + # return Path.SEED_TOOLS_SUB_MENU - slot_num = ret_val + # slot_num = ret_val # display a tool to pick letters/numbers to make a passphrase # passphrase = self.seed_tools_view.display_gather_passphrase_screen() diff --git a/src/seedsigner/helpers/keyboard.py b/src/seedsigner/helpers/keyboard.py index a055800d..a918dd8b 100644 --- a/src/seedsigner/helpers/keyboard.py +++ b/src/seedsigner/helpers/keyboard.py @@ -88,7 +88,8 @@ class Keyboard: rect=(0,40, 240,240), font=None, additional_keys=[KEY_BACKSPACE], - auto_wrap=[WRAP_TOP, WRAP_BOTTOM, WRAP_LEFT, WRAP_RIGHT]): + auto_wrap=[WRAP_TOP, WRAP_BOTTOM, WRAP_LEFT, WRAP_RIGHT], + render_now=True): """ `auto_wrap` specifies which edges the keyboard is allowed to loop back when navigating past the end. @@ -168,11 +169,12 @@ class Keyboard: )) cur_x += self.x_width + self.x_gap - # Render the keys - self.render_keys() + if render_now: + # Render the keys + self.render_keys() - # Render the initial highlighted character - self.update_from_input(input=None) + # Render the initial highlighted character + self.update_from_input(input=None) def update_active_keys(self, active_keys): @@ -341,6 +343,27 @@ class Keyboard: raise Exception(f"""`selected_letter` "{selected_letter}" not found in keyboard""") + def set_selected_key_indices(self, x, y): + # De-select the current selected_key + self.keys[self.selected_key["y"]][self.selected_key["x"]].is_selected = False + + if y < len(self.keys): + self.selected_key["y"] = y + else: + self.selected_key["y"] = len(self.keys) - 1 + + if x < len(self.keys[self.selected_key["y"]]): + self.selected_key["x"] = x + else: + self.selected_key["x"] = len(self.keys[self.selected_key["y"]]) - 1 + + # Select the new selected_key + self.keys[self.selected_key["y"]][self.selected_key["x"]].is_selected = True + + + def get_selected_key(self): + return self.keys[self.selected_key["y"]][self.selected_key["x"]].letter + @dataclass class TextEntryDisplay: @@ -349,6 +372,7 @@ class TextEntryDisplay: font: any font_color: any is_centered: bool = True + has_outline: bool = False cur_text: str = " " @@ -356,7 +380,10 @@ class TextEntryDisplay: """ Render the live text entry display """ if cur_text: self.cur_text = cur_text - self.draw.rectangle(self.rect, fill="black") + if self.has_outline: + self.draw.rectangle(self.rect, fill="black", outline=self.font_color) + else: + self.draw.rectangle(self.rect, fill="black") cursor_block_width = 18 cursor_block_height = 33 @@ -366,11 +393,20 @@ class TextEntryDisplay: if self.is_centered: word_offset = self.rect[0] + int(self.rect[2] - self.rect[0] - tw - cursor_block_width)/2 else: - word_offset = self.rect[0] - self.draw.text((word_offset, self.rect[1] + 2), self.cur_text[:-1], fill=self.font_color, font=self.font) + word_offset = self.rect[0] + 3 + cursor_block_offset = word_offset + tw - 1 + if cursor_block_offset == self.rect[0]: + cursor_block_offset = self.rect[0] + 1 + + end_pos_x = cursor_block_offset + cursor_block_width + if end_pos_x >= self.rect[2]: + # Shift the display left + cursor_block_offset -= end_pos_x - self.rect[2] + 1 + word_offset -= end_pos_x - self.rect[2] + 1 + + self.draw.text((word_offset, self.rect[1] + 3), self.cur_text[:-1], fill=self.font_color, font=self.font) # Draw the highlighted cursor block - cursor_block_offset = word_offset + tw - 1 - self.draw.rectangle((cursor_block_offset, self.rect[1] + 2, cursor_block_offset + cursor_block_width, cursor_block_height), fill="#111") - self.draw.text((cursor_block_offset + 1, self.rect[1] + 2), self.cur_text[-1], fill=self.font_color, font=self.font) + self.draw.rectangle((cursor_block_offset, self.rect[1] + 1, cursor_block_offset + cursor_block_width, self.rect[3] - 1), fill="#111") + self.draw.text((cursor_block_offset + 1, self.rect[1] + 3), self.cur_text[-1], fill=self.font_color, font=self.font) diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index b21064ea..293a4907 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -407,6 +407,46 @@ class SeedToolsView(View): def draw_passphrase_keyboard_entry(self): + def render_right_panel(button1_text="ABC", button2_text="!@#"): + # Render the up/down arrow buttons for KEY1 and KEY3 + row_height = 28 + right_button_left_margin = 10 + right_button_width = right_panel_buttons_width - right_button_left_margin + font_padding_right = 2 + font_padding_top = 1 + key_x = View.canvas_width - right_button_width + key_y = int(View.canvas_height - row_height) / 2 - 1 - 60 + + background_color = "#111" + font_color = View.color + font = View.ROBOTOCONDENSED_BOLD_24 + tw, th = font.getsize(button1_text) + if button1_is_active: + background_color = View.color + font_color = "#111" + View.draw.rounded_rectangle((key_x, key_y, 250, key_y + row_height), outline=View.color, fill=background_color, radius=5, width=1) + View.draw.text((View.canvas_width - tw - font_padding_right, key_y + font_padding_top), font=font, text=button1_text, fill=font_color) + + background_color = "#111" + font_color = View.color + tw, th = font.getsize(button2_text) + if button2_is_active: + background_color = View.color + font_color = "#111" + key_y = int(View.canvas_height - row_height) / 2 - 1 + View.draw.rounded_rectangle((key_x, key_y, 250, key_y + row_height), outline=View.color, fill=background_color, radius=5, width=1) + View.draw.text((View.canvas_width - tw - font_padding_right, key_y + font_padding_top), font=font, text=button2_text, fill=font_color) + + background_color = "#111" + font_color = View.color + button3_text = "Save" + tw, th = font.getsize(button3_text) + if button3_is_active: + background_color = View.color + font_color = "#111" + key_y = int(View.canvas_height - row_height) / 2 - 1 + 60 + View.draw.rounded_rectangle((key_x, key_y, 250, key_y + row_height), outline=View.color, fill=background_color, radius=5, width=1) + View.draw.text((View.canvas_width - tw - font_padding_right, key_y + font_padding_top), font=font, text=button3_text, fill=font_color) # Clear the screen View.draw.rectangle((0,0, View.canvas_width,View.canvas_height), fill="black") @@ -419,28 +459,68 @@ class SeedToolsView(View): self.passphrase = " " # Set up the keyboard params - right_key_nav_width = 50 - keyboard_width = View.canvas_width - right_key_nav_width - text_entry_display_height = 39 + right_panel_buttons_width = 60 - keyboard = Keyboard(View.draw, - charset="1234567890" + "".join(SeedToolsView.ALPHABET), - rows=4, - cols=10, - rect=(0, int(1.5 * text_entry_display_height), keyboard_width,240), - auto_wrap=[Keyboard.WRAP_LEFT, Keyboard.WRAP_RIGHT]) + # render top title banner + font = View.ROBOTOCONDENSED_REGULAR_20 + title = "Enter Passphrase" + title_top_padding = 0 + title_bottom_padding = 10 + tw, th = font.getsize(title) + View.draw.text((int(View.canvas_width - tw) / 2, title_top_padding), text=title, font=font, fill=View.color) + title_height = th + title_top_padding + title_bottom_padding - # Render the top text entry display + # Render the live text entry display + font = View.ROBOTOCONDENSED_REGULAR_28 + tw, th = font.getsize("!\"#$%&'()*+,=./;:<>?@[]|-_`~ ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 1234567890") # All possible chars for max range + text_entry_side_padding = 0 + text_entry_top_padding = 1 + text_entry_bottom_padding = 10 + text_entry_top_y = title_height + text_entry_top_padding + text_entry_bottom_y = text_entry_top_y + 3 + th + 3 text_entry_display = TextEntryDisplay( View.draw, - rect=(self.previous_button_width,0, View.canvas_width,text_entry_display_height), - font=View.ROBOTOCONDENSED_BOLD_26, + rect=(text_entry_side_padding,text_entry_top_y, View.canvas_width - right_panel_buttons_width - 1, text_entry_bottom_y), + font=font, font_color=View.color, is_centered=False, + has_outline=True, cur_text=''.join(self.passphrase) ) text_entry_display.render() + keyboard_start_y = text_entry_bottom_y + text_entry_bottom_padding + keyboard_abc = Keyboard(View.draw, + charset="1234567890" + "".join(SeedToolsView.ALPHABET), + rows=4, + cols=10, + rect=(0, keyboard_start_y, View.canvas_width - right_panel_buttons_width, View.canvas_height), + auto_wrap=[Keyboard.WRAP_LEFT, Keyboard.WRAP_RIGHT]) + keyboard_ABC = Keyboard(View.draw, + charset="1234567890" + "".join(SeedToolsView.ALPHABET).upper(), + rows=4, + cols=10, + rect=(0, keyboard_start_y, View.canvas_width - right_panel_buttons_width, View.canvas_height), + auto_wrap=[Keyboard.WRAP_LEFT, Keyboard.WRAP_RIGHT], + render_now=False) + keyboard_symbol = Keyboard(View.draw, + charset="1234567890" + "!\"#$%&'()*+,=./;:<>?@[]|-_`~", + rows=4, + cols=10, + rect=(0, keyboard_start_y, View.canvas_width - right_panel_buttons_width, View.canvas_height), + auto_wrap=[Keyboard.WRAP_LEFT, Keyboard.WRAP_RIGHT], + render_now=False) + + button1_is_active = False + button2_is_active = False + button3_is_active = False + KEYBOARD__LOWERCASE = 0 + KEYBOARD__UPPERCASE = 1 + KEYBOARD__SYMBOL = 2 + cur_keyboard_type = KEYBOARD__LOWERCASE + cur_keyboard = keyboard_abc + render_right_panel() + View.DispShowImage() # Start the interactive update loop @@ -449,7 +529,34 @@ class SeedToolsView(View): [B.KEY_UP, B.KEY_DOWN, B.KEY_RIGHT, B.KEY_LEFT, B.KEY_PRESS, B.KEY1, B.KEY2, B.KEY3] ) - ret_val = keyboard.update_from_input(input) + keyboard_swap = False + if input == B.KEY1: + if cur_keyboard_type == KEYBOARD__LOWERCASE: + keyboard_ABC.set_selected_key_indices(x=cur_keyboard.selected_key["x"], y=cur_keyboard.selected_key["y"]) + cur_keyboard_type = KEYBOARD__UPPERCASE + cur_keyboard = keyboard_ABC + render_right_panel(button1_text="abc", button2_text="!@#") + else: + keyboard_abc.set_selected_key_indices(x=cur_keyboard.selected_key["x"], y=cur_keyboard.selected_key["y"]) + cur_keyboard_type = KEYBOARD__LOWERCASE + cur_keyboard = keyboard_abc + render_right_panel(button1_text="ABC", button2_text="!@#") + cur_keyboard.render_keys() + keyboard_swap = True + ret_val = cur_keyboard.get_selected_key() + + elif input == B.KEY2: + if cur_keyboard_type in [KEYBOARD__LOWERCASE, KEYBOARD__UPPERCASE]: + keyboard_symbol.set_selected_key_indices(x=cur_keyboard.selected_key["x"], y=cur_keyboard.selected_key["y"]) + cur_keyboard_type = KEYBOARD__SYMBOL + cur_keyboard = keyboard_symbol + cur_keyboard.render_keys() + render_right_panel(button1_text="abc", button2_text="") + keyboard_swap = True + ret_val = cur_keyboard.get_selected_key() + + else: + ret_val = cur_keyboard.update_from_input(input) if ret_val in Keyboard.EXIT_DIRECTIONS: self.render_previous_button(highlight=True) @@ -466,7 +573,6 @@ class SeedToolsView(View): self.passphrase = self.passphrase[:-1] self.passphrase += " " - elif input == B.KEY_PRESS and ret_val not in Keyboard.ADDITIONAL_KEYS: # User has locked in the current letter if self.passphrase[-1] != " ": @@ -489,7 +595,7 @@ class SeedToolsView(View): self.passphrase += ret_val self.passphrase += " " - elif input in [B.KEY_RIGHT, B.KEY_LEFT, B.KEY_UP, B.KEY_DOWN]: + elif input in [B.KEY_RIGHT, B.KEY_LEFT, B.KEY_UP, B.KEY_DOWN] or keyboard_swap: # Live joystick movement; haven't locked this new letter in yet. # Replace the last letter w/the currently selected one. Only update # the active keyboard keys when a selection has been locked in diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index c5065055..35242124 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -43,6 +43,7 @@ class View: ROBOTOCONDENSED_BOLD_24 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Bold.ttf"), 22) ROBOTOCONDENSED_BOLD_25 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Bold.ttf"), 25) ROBOTOCONDENSED_BOLD_26 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Bold.ttf"), 26) + ROBOTOCONDENSED_BOLD_28 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Bold.ttf"), 28) ROBOTOCONDENSED_LIGHT_16 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Light.ttf"), 16) ROBOTOCONDENSED_LIGHT_24 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Light.ttf"), 24) ROBOTOCONDENSED_REGULAR_16 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Regular.ttf"), 16) @@ -50,6 +51,7 @@ class View: ROBOTOCONDENSED_REGULAR_22 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Regular.ttf"), 22) ROBOTOCONDENSED_REGULAR_24 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Regular.ttf"), 24) ROBOTOCONDENSED_REGULAR_26 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Regular.ttf"), 26) + ROBOTOCONDENSED_REGULAR_28 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Regular.ttf"), 28) RST = 27 DC = 25 @@ -269,15 +271,19 @@ class View: # Set up the "back" arrow in the upper left arrow = "<" word_font = View.ROBOTOCONDENSED_BOLD_26 - padding = 3 + top_padding = -3 + bottom_padding = 3 + side_padding = 3 tw, th = word_font.getsize(arrow) - self.previous_button_width = tw + 2 * padding + self.previous_button_width = tw + 2 * side_padding if highlight: - View.draw.rectangle((0,0, tw + 2 * padding, th + 2 * padding), fill=View.color) - View.draw.text((padding, padding), arrow, fill="black", font=word_font) + font_color = "black" + background_color = View.color else: - View.draw.rectangle((0,0, tw + 2 * padding, th + 2 * padding), fill="black") - View.draw.text((padding, padding), arrow, fill=View.color, font=word_font) + font_color = View.color + background_color = "black" + View.draw.rectangle((0,0, self.previous_button_width, th + top_padding + bottom_padding), fill=background_color) + View.draw.text((side_padding, top_padding), arrow, fill=font_color, font=word_font) From 810547b4f987d46e5882be0a3122f99176e0affb Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 10 Jul 2021 12:49:07 -0500 Subject: [PATCH 20/39] Improved button long-press repeat usability --- src/seedsigner/helpers/buttons.py | 83 +++++++++++++++++++------ src/seedsigner/views/menu_view.py | 16 ++--- src/seedsigner/views/seed_tools_view.py | 4 +- 3 files changed, 75 insertions(+), 28 deletions(-) diff --git a/src/seedsigner/helpers/buttons.py b/src/seedsigner/helpers/buttons.py index b5d303a9..059f044b 100644 --- a/src/seedsigner/helpers/buttons.py +++ b/src/seedsigner/helpers/buttons.py @@ -1,8 +1,9 @@ import RPi.GPIO as GPIO import time -class Buttons: + +class Buttons: KEY_UP_PIN = 6 KEY_DOWN_PIN = 19 KEY_LEFT_PIN = 5 @@ -30,47 +31,91 @@ class Buttons: self.add_events([B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS, B.KEY_LEFT, B.KEY_RIGHT, B.KEY1, B.KEY2, B.KEY3]) - return + # Track state over time so we can apply input delays/ignores as needed + self.cur_input = None # Track which direction or button was last pressed + self.cur_input_started = None # Track when that input began + self.last_input_time = int(time.time() * 1000) # How long has it been since the last input + self.first_repeat_threshold = 60 # Long-press time required before returning continuous input + self.next_repeat_threshold = 250 # Amount of time where we no longer consider input a continuous hold - def wait_for(self, keys = [], check_release = True, release_keys = []) -> int: + + def wait_for(self, keys=[], check_release=True, release_keys=[]) -> int: if not release_keys: release_keys = keys while True: - for i in keys: - if check_release == False or ((check_release == True and i in release_keys and B.release_lock == True) or check_release == True and i not in release_keys): + for key in keys: + if not check_release or ((check_release and key in release_keys and B.release_lock) or check_release and key not in release_keys): # when check release is False or the release lock is released (True) - if self.GPIO.input(i) == GPIO.LOW or self.override_ind == True: + if self.GPIO.input(key) == GPIO.LOW or self.override_ind: B.release_lock = False - if self.override_ind == True: + if self.override_ind: self.override_ind = False return B.OVERRIDE - return i + + if self.cur_input != key: + self.cur_input = key + self.cur_input_started = int(time.time() * 1000) # in milliseconds + self.last_input_time = self.cur_input_started + return key + + else: + # Still pressing the same input + cur_time = int(time.time() * 1000) + + if cur_time - self.last_input_time > self.next_repeat_threshold: + # Too much time has elapsed to consider this the same + # continuous input. Treat as a new separate press. + self.cur_input_started = cur_time + self.last_input_time = cur_time + return key + + elif cur_time - self.cur_input_started > self.first_repeat_threshold: + # We're good to relay this immediately as continuous + # input. + self.last_input_time = cur_time + return key + + else: + # We're not yet at the first repeat threshold; triggering + # a key now would be too soon and yields a bad user + # experience when only a single click was intended but + # a second input is processed because of race condition + # against human response time to release the button. + # So there has to be a delay before we allow the first + # continuous repeat to register. So we'll ignore this + # round's input and **won't update any of our + # timekeeping vars**. But once we cross the threshold, + # we let the repeats fly. + pass + time.sleep(0.01) # wait 10 ms to give CPU chance to do other things - def add_events(self, keys = []): - for i in keys: - GPIO.add_event_detect(i, self.GPIO.RISING, callback=Buttons.rising_callback) + + def add_events(self, keys=[]): + for key in keys: + GPIO.add_event_detect(key, self.GPIO.RISING, callback=Buttons.rising_callback) + def rising_callback(channel): B.release_lock = True - return + def trigger_override(self) -> bool: - if self.override_ind == False: + if not self.override_ind: self.override_ind = True return True - return False + def check_for_low(self, key) -> bool: - if self.GPIO.input(key) == self.GPIO.LOW: - return True - return False + return self.GPIO.input(key) == self.GPIO.LOW + + # class used as short hand for static button/channel lookup values - +# TODO: Implement `release_lock` functionality as a global somewhere. Mixes up design +# patterns to have a static constants class plus a settable global value. class B: - KEY_UP = 6 KEY_DOWN = 19 KEY_LEFT = 5 diff --git a/src/seedsigner/views/menu_view.py b/src/seedsigner/views/menu_view.py index 128e3fdd..8f372606 100644 --- a/src/seedsigner/views/menu_view.py +++ b/src/seedsigner/views/menu_view.py @@ -38,7 +38,7 @@ class MenuView(View): # Wait for Button Input (specifically menu selection/press) while True: if ret_val == 0: - input = self.buttons.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS]) + input = self.buttons.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS], check_release=True, release_keys=[B.KEY_PRESS]) else: return ret_val if input == B.KEY_UP: @@ -76,7 +76,7 @@ class MenuView(View): # Wait for Button Input (specifically menu selection/press) while True: - input = self.buttons.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS]) + input = self.buttons.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS], check_release=True, release_keys=[B.KEY_PRESS]) if input == B.KEY_UP: self.menu_up() elif input == B.KEY_DOWN: @@ -104,7 +104,7 @@ class MenuView(View): # Wait for Button Input (specifically menu selection/press) while True: - input = self.buttons.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS]) + input = self.buttons.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS], check_release=True, release_keys=[B.KEY_PRESS]) if input == B.KEY_UP: self.menu_up() elif input == B.KEY_DOWN: @@ -133,7 +133,7 @@ class MenuView(View): # Wait for Button Input (specifically menu selection/press) while True: - input = self.buttons.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS]) + input = self.buttons.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS], check_release=True, release_keys=[B.KEY_PRESS]) if input == B.KEY_UP: self.menu_up() elif input == B.KEY_DOWN: @@ -161,7 +161,7 @@ class MenuView(View): self.draw_menu(lines, 1, title, bottom) while True: - input = self.buttons.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS]) + input = self.buttons.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS], check_release=True, release_keys=[B.KEY_PRESS]) if input == B.KEY_UP: self.menu_up(title, bottom) elif input == B.KEY_DOWN: @@ -177,7 +177,7 @@ class MenuView(View): # Wait for Button Input (specifically menu selection/press) while True: - input = self.buttons.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS]) + input = self.buttons.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS], check_release=True, release_keys=[B.KEY_PRESS]) if input == B.KEY_UP: self.menu_up() elif input == B.KEY_DOWN: @@ -250,7 +250,7 @@ class MenuView(View): # Wait for Button Input (specifically menu selection/press) while True: - input = self.buttons.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS]) + input = self.buttons.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS], check_release=True, release_keys=[B.KEY_PRESS]) if input == B.KEY_UP: self.menu_up() elif input == B.KEY_DOWN: @@ -267,7 +267,7 @@ class MenuView(View): ### ### Generic Draw Menu Method - + # TODO: Optimize updates by just redrawing the no-longer highlighted line and the newly highlighted line def draw_menu(self, lines, selected_menu_num = 1, title = None, bottom = None) -> None: if title == None: t = "SeedSigner v" + self.controller.VERSION diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index 293a4907..f6f47c40 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -526,7 +526,9 @@ class SeedToolsView(View): # Start the interactive update loop while True: input = View.buttons.wait_for( - [B.KEY_UP, B.KEY_DOWN, B.KEY_RIGHT, B.KEY_LEFT, B.KEY_PRESS, B.KEY1, B.KEY2, B.KEY3] + [B.KEY_UP, B.KEY_DOWN, B.KEY_RIGHT, B.KEY_LEFT, B.KEY_PRESS, B.KEY1, B.KEY2, B.KEY3], + check_release=True, + release_keys=[B.KEY_PRESS, B.KEY1, B.KEY2, B.KEY3] ) keyboard_swap = False From 49ba160a52f5b7cbfb8a41257ec3d105d0ecb54b Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 10 Jul 2021 13:08:16 -0500 Subject: [PATCH 21/39] Added continuous joystick to manual seed entry; tweaked timing vars --- src/seedsigner/helpers/buttons.py | 4 ++-- src/seedsigner/views/seed_tools_view.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/seedsigner/helpers/buttons.py b/src/seedsigner/helpers/buttons.py index 059f044b..c7fb59a5 100644 --- a/src/seedsigner/helpers/buttons.py +++ b/src/seedsigner/helpers/buttons.py @@ -34,8 +34,8 @@ class Buttons: # Track state over time so we can apply input delays/ignores as needed self.cur_input = None # Track which direction or button was last pressed self.cur_input_started = None # Track when that input began - self.last_input_time = int(time.time() * 1000) # How long has it been since the last input - self.first_repeat_threshold = 60 # Long-press time required before returning continuous input + self.last_input_time = int(time.time() * 1000) # How long has it been since the last input? + self.first_repeat_threshold = 70 # Long-press time required before returning continuous input self.next_repeat_threshold = 250 # Amount of time where we no longer consider input a continuous hold diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index f6f47c40..7b42f89f 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -252,7 +252,7 @@ class SeedToolsView(View): input = View.buttons.wait_for( [B.KEY_UP, B.KEY_DOWN, B.KEY_RIGHT, B.KEY_LEFT, B.KEY_PRESS, B.KEY1, B.KEY2, B.KEY3], check_release=True, - release_keys=[B.KEY_UP, B.KEY_DOWN, B.KEY_RIGHT, B.KEY_LEFT, B.KEY_PRESS, B.KEY2] + release_keys=[B.KEY_PRESS, B.KEY2] ) if previous_button_is_active: From 42046150493be4497928c3696bb04892035e121c Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 10 Jul 2021 16:43:55 -0500 Subject: [PATCH 22/39] Fixed "del" button in/out nav for adjacent col --- src/seedsigner/helpers/keyboard.py | 52 ++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/src/seedsigner/helpers/keyboard.py b/src/seedsigner/helpers/keyboard.py index a918dd8b..880049ce 100644 --- a/src/seedsigner/helpers/keyboard.py +++ b/src/seedsigner/helpers/keyboard.py @@ -265,15 +265,22 @@ class Keyboard: self.selected_key["y"] -= 1 return Keyboard.EXIT_BOTTOM - if self.selected_key["x"] >= len(self.keys[self.selected_key["y"]]): - if Keyboard.WRAP_BOTTOM in self.auto_wrap: - # This line is too short to land here - self.selected_key["y"] = 0 + elif self.selected_key["x"] >= len(self.keys[self.selected_key["y"]]): + # We're moving into the bottom line but there's no key directly below. + if self.selected_key["x"] - 1 == len(self.keys[self.selected_key["y"]]) - 1 and \ + self.keys[self.selected_key["y"]][-1].size == 2: + # The last, adjacent key in this row is a double. Go ahead and select it + self.selected_key["x"] = self.selected_key["x"] - 1 + else: - # Undo selection change and notify controlling loop that we've left - # the keyboard - self.selected_key["y"] -= 1 - return Keyboard.EXIT_BOTTOM + if Keyboard.WRAP_BOTTOM in self.auto_wrap: + # This line is too short to land here + self.selected_key["y"] = 0 + else: + # Undo selection change and notify controlling loop that we've left + # the keyboard + self.selected_key["y"] -= 1 + return Keyboard.EXIT_BOTTOM elif input == B.KEY_UP: self.selected_key["y"] -= 1 @@ -288,14 +295,20 @@ class Keyboard: return Keyboard.EXIT_TOP if self.selected_key["x"] >= len(self.keys[self.selected_key["y"]]): - if Keyboard.WRAP_TOP in self.auto_wrap: - # This line is too short to land here - self.selected_key["y"] -= 1 + # We're moving into the bottom line but there's no key directly below. + if self.selected_key["x"] - 1 == len(self.keys[self.selected_key["y"]]) - 1 and \ + self.keys[self.selected_key["y"]][-1].size == 2: + # The last, adjacent key in this row is a double. Go ahead and select it + self.selected_key["x"] = self.selected_key["x"] - 1 else: - # Undo selection change and notify controlling loop that we've left - # the keyboard - self.selected_key["y"] += 1 - return Keyboard.EXIT_TOP + if Keyboard.WRAP_TOP in self.auto_wrap: + # This line is too short to land here + self.selected_key["y"] -= 1 + else: + # Undo selection change and notify controlling loop that we've left + # the keyboard + self.selected_key["y"] += 1 + return Keyboard.EXIT_TOP elif input == Keyboard.ENTER_LEFT: # User has returned to the keyboard along the left edge @@ -317,8 +330,13 @@ class Keyboard: # Keep the last x position that was selected. self.selected_key["y"] = len(self.keys) - 1 if self.selected_key["x"] > len(self.keys[self.selected_key["y"]]) - 1: - # Can't enter here. Jump up a row - self.selected_key["y"] -= 1 + if self.selected_key["x"] - 1 == len(self.keys[self.selected_key["y"]]) - 1 and \ + self.keys[self.selected_key["y"]][-1].size == 2: + # The last, adjacent key in this row is a double. Go ahead and select it + self.selected_key["x"] = self.selected_key["x"] - 1 + else: + # Can't enter here. Jump up a row + self.selected_key["y"] -= 1 # Render the newly self.selected_key letter key = self.keys[self.selected_key["y"]][self.selected_key["x"]] From f888a520d00760d3fb26fead9d461847061d6842 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 10 Jul 2021 20:08:00 -0500 Subject: [PATCH 23/39] Completing "save" action, further button timing tweaks --- src/seedsigner/controller.py | 18 +++++++-------- src/seedsigner/helpers/buttons.py | 2 +- src/seedsigner/views/seed_tools_view.py | 29 +++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index cde2e172..c562324f 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -260,17 +260,17 @@ class Controller: ### Add a PassPhrase Menu def show_add_a_passphrase_tool(self): - # if self.storage.num_of_saved_seeds() == 0: - # self.menu_view.draw_modal(["Store a seed phrase", "prior to adding", "a passphrase"], "Error", "Right to Continue") - # self.buttons.wait_for([B.KEY_RIGHT]) - # return Path.SEED_TOOLS_SUB_MENU + if self.storage.num_of_saved_seeds() == 0: + self.menu_view.draw_modal(["Store a seed phrase", "prior to adding", "a passphrase"], "Error", "Right to Continue") + self.buttons.wait_for([B.KEY_RIGHT]) + return Path.SEED_TOOLS_SUB_MENU - # ret_val = 0 - # ret_val = self.menu_view.display_saved_seed_menu(self.storage, 3, None) - # if ret_val == 0: - # return Path.SEED_TOOLS_SUB_MENU + ret_val = 0 + ret_val = self.menu_view.display_saved_seed_menu(self.storage, 3, None) + if ret_val == 0: + return Path.SEED_TOOLS_SUB_MENU - # slot_num = ret_val + slot_num = ret_val # display a tool to pick letters/numbers to make a passphrase # passphrase = self.seed_tools_view.display_gather_passphrase_screen() diff --git a/src/seedsigner/helpers/buttons.py b/src/seedsigner/helpers/buttons.py index c7fb59a5..2dda2b86 100644 --- a/src/seedsigner/helpers/buttons.py +++ b/src/seedsigner/helpers/buttons.py @@ -35,7 +35,7 @@ class Buttons: self.cur_input = None # Track which direction or button was last pressed self.cur_input_started = None # Track when that input began self.last_input_time = int(time.time() * 1000) # How long has it been since the last input? - self.first_repeat_threshold = 70 # Long-press time required before returning continuous input + self.first_repeat_threshold = 100 # Long-press time required before returning continuous input self.next_repeat_threshold = 250 # Amount of time where we no longer consider input a continuous hold diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index 7b42f89f..10b58df8 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -532,6 +532,18 @@ class SeedToolsView(View): ) keyboard_swap = False + + # Check our two possible exit conditions + if input == B.KEY3: + # Save! + if self.passphrase != "" and self.passphrase != " ": + return self.passphrase + + elif input == B.KEY_PRESS and previous_button_is_active: + # Prev button clicked; return empty string to signal cancel. + return "" + + # Check for keyboard swaps if input == B.KEY1: if cur_keyboard_type == KEYBOARD__LOWERCASE: keyboard_ABC.set_selected_key_indices(x=cur_keyboard.selected_key["x"], y=cur_keyboard.selected_key["y"]) @@ -558,8 +570,25 @@ class SeedToolsView(View): ret_val = cur_keyboard.get_selected_key() else: + # Process normal input + if input in [B.KEY_UP, B.KEY_DOWN] and previous_button_is_active: + # We're navigating off the previous button + previous_button_is_active = False + self.render_previous_button(highlight=False) + + # Override the actual input w/an ENTER signal for the Keyboard + if input == B.KEY_DOWN: + input = Keyboard.ENTER_TOP + else: + input = Keyboard.ENTER_BOTTOM + elif input in [B.KEY_LEFT, B.KEY_RIGHT] and previous_button_is_active: + # ignore + continue + + ret_val = cur_keyboard.update_from_input(input) + # Now process the result from the keyboard if ret_val in Keyboard.EXIT_DIRECTIONS: self.render_previous_button(highlight=True) previous_button_is_active = True From 1447794628c0e66b073ef1af414a0b519f696ebb Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 10 Jul 2021 20:59:23 -0500 Subject: [PATCH 24/39] Button repeat threshold tweak --- src/seedsigner/helpers/buttons.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/seedsigner/helpers/buttons.py b/src/seedsigner/helpers/buttons.py index c3935390..6c6b2150 100644 --- a/src/seedsigner/helpers/buttons.py +++ b/src/seedsigner/helpers/buttons.py @@ -35,7 +35,7 @@ class Buttons: self.cur_input = None # Track which direction or button was last pressed self.cur_input_started = None # Track when that input began self.last_input_time = int(time.time() * 1000) # How long has it been since the last input? - self.first_repeat_threshold = 100 # Long-press time required before returning continuous input + self.first_repeat_threshold = 120 # Long-press time required before returning continuous input self.next_repeat_threshold = 250 # Amount of time where we no longer consider input a continuous hold From a53359b998bd5b951f1717ce30332e29f3daea28 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 11 Jul 2021 16:24:17 -0500 Subject: [PATCH 25/39] CameraProcess speedups * Copies PiVideoStream from imutils * removes imutils from requirements.txt * Reduces `start()` wait time. --- requirements.txt | 2 +- src/seedsigner/helpers/camera_process.py | 43 ++++++++++++-------- src/seedsigner/helpers/pivideostream.py | 52 ++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 18 deletions(-) create mode 100644 src/seedsigner/helpers/pivideostream.py diff --git a/requirements.txt b/requirements.txt index 64536752..43487108 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ embit==0.4.2 -imutils==0.5.4 +picamera==1.13 Pillow==8.2.0 pyzbar==0.1.8 qrcode==6.1 diff --git a/src/seedsigner/helpers/camera_process.py b/src/seedsigner/helpers/camera_process.py index 8cd82679..ccd3021b 100644 --- a/src/seedsigner/helpers/camera_process.py +++ b/src/seedsigner/helpers/camera_process.py @@ -6,33 +6,38 @@ import time class CameraProcess: def start(out_queue, in_queue): - print("CameraProcess start") - from imutils.video import VideoStream + import_start_time = int(time.time() * 1000) + from .pivideostream import PiVideoStream from pyzbar import pyzbar + import_end_time = int(time.time() * 1000) - print("CameraProcess finish import") + print(f"CameraProcess finish import: {import_end_time - import_start_time}ms") - running = True + is_running = True - while running == True: - - camera_on = False + while is_running: + is_camera_on = False msg = [] msg = in_queue.get() if msg[0] == "start": print("start camera!!") - vs = VideoStream(usePiCamera=True,resolution=(512, 384),framerate=12).start() # For Pi Camera - time.sleep(2.0) + vs = PiVideoStream(resolution=(512, 384),framerate=12).start() # For Pi Camera msg[0] = "" - camera_on = True + is_camera_on = True - while camera_on: + while is_camera_on: frame = vs.read() + + if frame is None: + # Camera isn't returning data yet + time.sleep(0.125) + continue + barcodes = pyzbar.decode(frame) for barcode in barcodes: data = barcode.data.decode("utf-8") @@ -48,11 +53,15 @@ class CameraProcess: if msg[0] == "stop": vs.stop() - camera_on = False + is_camera_on = False elif msg[0] == "stop": print("stop camera!!") + time.sleep(0.25) # No need to poll all that frequently + + + class CameraPoll(object): def __init__(self, interval, function, *args, **kwargs): @@ -61,20 +70,20 @@ class CameraPoll(object): self.function = function self.args = args self.kwargs = kwargs - self.is_running = False + self.is_is_running = False self.start() def _run(self): - self.is_running = False + self.is_is_running = False self.start() self.function(*self.args, **self.kwargs) def start(self): - if not self.is_running: + if not self.is_is_running: self._timer = Timer(self.interval, self._run) self._timer.start() - self.is_running = True + self.is_is_running = True def stop(self): self._timer.cancel() - self.is_running = False \ No newline at end of file + self.is_is_running = False \ No newline at end of file diff --git a/src/seedsigner/helpers/pivideostream.py b/src/seedsigner/helpers/pivideostream.py new file mode 100644 index 00000000..9597ce45 --- /dev/null +++ b/src/seedsigner/helpers/pivideostream.py @@ -0,0 +1,52 @@ +# import the necessary packages +from picamera.array import PiRGBArray +from picamera import PiCamera +from threading import Thread + + +# Modified from: https://github.com/jrosebr1/imutils +class PiVideoStream: + def __init__(self, resolution=(320, 240), framerate=32, **kwargs): + # initialize the camera + self.camera = PiCamera(resolution=resolution, framerate=framerate, **kwargs) + + # initialize the stream + self.rawCapture = PiRGBArray(self.camera, size=resolution) + self.stream = self.camera.capture_continuous(self.rawCapture, + format="bgr", use_video_port=True) + + # initialize the frame and the variable used to indicate + # if the thread should be stopped + self.frame = None + self.stopped = False + + def start(self): + # start the thread to read frames from the video stream + t = Thread(target=self.update, args=()) + t.daemon = True + t.start() + return self + + def update(self): + # keep looping infinitely until the thread is stopped + for f in self.stream: + # grab the frame from the stream and clear the stream in + # preparation for the next frame + self.frame = f.array + self.rawCapture.truncate(0) + + # if the thread indicator variable is set, stop the thread + # and resource camera resources + if self.stopped: + self.stream.close() + self.rawCapture.close() + self.camera.close() + return + + def read(self): + # return the frame most recently read + return self.frame + + def stop(self): + # indicate that the thread should be stopped + self.stopped = True From d7bea6861d4a36e3b468d069d2a28fcc71cad6c7 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 12 Jul 2021 00:37:50 -0500 Subject: [PATCH 26/39] End-to-end image-capture-as-seed functioning --- src/seedsigner/controller.py | 33 ++++++ src/seedsigner/helpers/camera_process.py | 145 +++++++++++++++++------ src/seedsigner/helpers/path.py | 1 + src/seedsigner/helpers/pivideostream.py | 10 ++ src/seedsigner/views/menu_view.py | 4 +- src/seedsigner/views/seed_tools_view.py | 94 ++++++++++++++- 6 files changed, 249 insertions(+), 38 deletions(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 6c421dd8..eca9db68 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -129,6 +129,8 @@ class Controller: ret_val = self.show_generate_last_word_tool() elif ret_val == Path.DICE_GEN_SEED: ret_val = self.show_create_seed_with_dice_tool() + elif ret_val == Path.IMAGE_GEN_SEED: + ret_val = self.show_create_seed_with_image_tool() elif ret_val == Path.SAVE_SEED: ret_val = self.show_store_a_seed_tool() elif ret_val == Path.PASSPHRASE_SEED: @@ -247,6 +249,37 @@ class Controller: return Path.MAIN_MENU + def show_create_seed_with_image_tool(self) -> int: + seed_phrase = [] + ret_val = True + + while True: + seed_phrase = self.seed_tools_view.seed_phrase_from_camera_image() + if len(seed_phrase) > 0: + break + else: + return Path.SEED_TOOLS_SUB_MENU + + # display seed phrase (24 words) + while True: + ret_val = self.seed_tools_view.display_seed_phrase(seed_phrase, show_qr_option=True) + if ret_val == True: + break + else: + # no-op; can't back out of the seed phrase view + pass + + # Ask to save seed + if self.storage.slot_avaliable(): + r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Save Seed?") + if r == 1: #Yes + slot_num = self.menu_view.display_saved_seed_menu(self.storage,2,None) + if slot_num in (1,2,3): + self.storage.save_seed_phrase(seed_phrase, slot_num) + self.menu_view.draw_modal(["Seed Valid", "Saved to Slot #" + str(slot_num)], "", "Right to Main Menu") + input = self.buttons.wait_for([B.KEY_RIGHT]) + + return Path.MAIN_MENU ### Store a seed (temp) Menu def show_store_a_seed_tool(self): diff --git a/src/seedsigner/helpers/camera_process.py b/src/seedsigner/helpers/camera_process.py index ccd3021b..2b587169 100644 --- a/src/seedsigner/helpers/camera_process.py +++ b/src/seedsigner/helpers/camera_process.py @@ -1,19 +1,118 @@ # External Dependencies from multiprocessing import Queue from threading import Timer +import io import time +import traceback +import operator -class CameraProcess: - def start(out_queue, in_queue): - print("CameraProcess start") +class CameraProcess(): - import_start_time = int(time.time() * 1000) - from .pivideostream import PiVideoStream + def scan_qr_code(in_queue, out_queue): from pyzbar import pyzbar - import_end_time = int(time.time() * 1000) + from .pivideostream import PiVideoStream + try: + print("Instantiate PiVideoStream start") + start_time = int(time.time() * 1000) + video_stream = PiVideoStream(resolution=(512, 384),framerate=12) + end_time = int(time.time() * 1000) + print(f"Instantiate PiVideoStream finish: {end_time - start_time}ms") - print(f"CameraProcess finish import: {import_end_time - import_start_time}ms") + print("video_stream start()") + start_time = int(time.time() * 1000) + video_stream.start() + end_time = int(time.time() * 1000) + print(f"video_stream start() done: {end_time - start_time}ms") + + msg = [""] + + while True: + # Loop the reader until we get a result or receive "stop" + frame = video_stream.read() + + if frame is None: + # Camera isn't returning data yet + time.sleep(0.1) + continue + + barcodes = pyzbar.decode(frame) + for barcode in barcodes: + data = barcode.data.decode("utf-8") + out_queue.put([data]) + break + if len(barcodes) == 0: + out_queue.put(["nodata"]) + + try: + # Get any updates from the message queue, but don't wait + msg = in_queue.get(block=False) + except: + pass + + if msg[0] == "stop": + break + finally: + try: + video_stream.stop() + except: + pass + + + + def capture_single_frame(in_queue, out_queue): + from PIL import Image + from PIL.ImageOps import autocontrast + import picamera + + print("Instantiate PiCamera start") + start_time = int(time.time() * 1000) + # camera = picamera.PiCamera(resolution=(512, 384),framerate=12) + camera = picamera.PiCamera(resolution=(480, 480), framerate=8) + end_time = int(time.time() * 1000) + print(f"Instantiate PiCamera finish: {end_time - start_time}ms") + + try: + print("camera ready") + out_queue.put(["ready"]) + + # Wait for the "click" command + while True: + try: + msg = in_queue.get(block=False) + break + except: + time.sleep(0.1) + + if msg[0] == "stop": + print("Received 'stop'") + return + + elif msg[0] == "click": + print("Received 'click'") + stream = io.BytesIO() + camera.capture(stream, format='jpeg') + + # "Rewind" the stream to the beginning so we can read its content + stream.seek(0) + + out_queue.put([autocontrast(Image.open(stream)).rotate(90)]) + + except Exception as e: + traceback.print_exc() + + finally: + camera.close() + print("Cleaned up capture_single_frame") + + + @classmethod + def start(cls, out_queue, in_queue): + print("CameraProcess start") + start_time = int(time.time() * 1000) + from .pivideostream import PiVideoStream + end_time = int(time.time() * 1000) + print(f"CameraProcess finish import: {end_time - start_time}ms") is_running = True @@ -24,36 +123,10 @@ class CameraProcess: msg = in_queue.get() if msg[0] == "start": - print("start camera!!") - vs = PiVideoStream(resolution=(512, 384),framerate=12).start() # For Pi Camera + CameraProcess.scan_qr_code(in_queue, out_queue) - msg[0] = "" - is_camera_on = True - - while is_camera_on: - frame = vs.read() - - if frame is None: - # Camera isn't returning data yet - time.sleep(0.125) - continue - - barcodes = pyzbar.decode(frame) - for barcode in barcodes: - data = barcode.data.decode("utf-8") - out_queue.put([data]) - break - if len(barcodes) == 0: - out_queue.put(["nodata"]) - - try: - msg = in_queue.get(False) - except: - pass - - if msg[0] == "stop": - vs.stop() - is_camera_on = False + elif msg[0] == "single_frame": + CameraProcess.capture_single_frame(in_queue, out_queue) elif msg[0] == "stop": print("stop camera!!") diff --git a/src/seedsigner/helpers/path.py b/src/seedsigner/helpers/path.py index 7d80db67..05c7b41c 100644 --- a/src/seedsigner/helpers/path.py +++ b/src/seedsigner/helpers/path.py @@ -15,6 +15,7 @@ class Path: # Tools / Functions Routes GEN_LAST_WORD = 40 DICE_GEN_SEED = 41 + IMAGE_GEN_SEED = 45 SAVE_SEED = 42 PASSPHRASE_SEED = 43 DELETE_PASSPHRASE = 44 diff --git a/src/seedsigner/helpers/pivideostream.py b/src/seedsigner/helpers/pivideostream.py index 9597ce45..58284dda 100644 --- a/src/seedsigner/helpers/pivideostream.py +++ b/src/seedsigner/helpers/pivideostream.py @@ -2,6 +2,7 @@ from picamera.array import PiRGBArray from picamera import PiCamera from threading import Thread +import time # Modified from: https://github.com/jrosebr1/imutils @@ -38,6 +39,7 @@ class PiVideoStream: # if the thread indicator variable is set, stop the thread # and resource camera resources if self.stopped: + print("PiVideoStream: closing everything") self.stream.close() self.rawCapture.close() self.camera.close() @@ -50,3 +52,11 @@ class PiVideoStream: def stop(self): # indicate that the thread should be stopped self.stopped = True + + # while True: + # # Wait for camera to close before letting the thread get destroyed + # if not self.camera.closed: + # time.sleep(0.1) + # else: + # break + diff --git a/src/seedsigner/views/menu_view.py b/src/seedsigner/views/menu_view.py index b6351f01..c5b9b974 100644 --- a/src/seedsigner/views/menu_view.py +++ b/src/seedsigner/views/menu_view.py @@ -71,7 +71,7 @@ class MenuView(View): else: seed_storage_line = "View Seeds (temp)" - lines = ["... [ Return to Main ]", "Input a Seed", "Add/Remove Passphrase", "Generate an xPub", "Generate Work 12/24", "Generate a Seed with Dice"] + lines = ["... [ Return to Main ]", "Input a Seed", "Add/Remove Passphrase", "Generate an xPub", "Generate Word 12/24", "Generate a Seed with Dice", "Generate a Seed with Image"] self.draw_menu(lines, 1) input = 0 @@ -95,6 +95,8 @@ class MenuView(View): return Path.GEN_LAST_WORD elif self.selected_menu_num == 6: return Path.DICE_GEN_SEED + elif self.selected_menu_num == 7: + return Path.IMAGE_GEN_SEED ### Signing Tools Menu diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index 1ff5aed2..23dde26a 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -2,14 +2,17 @@ from embit import bip39 from embit.bip39 import mnemonic_to_bytes, mnemonic_from_bytes from PIL import ImageDraw, Image +import hashlib import math import time +import traceback # Internal file class dependencies from . import View from seedsigner.helpers import B, QR, CameraPoll, Keyboard, TextEntryDisplay + class SeedToolsView(View): SEEDWORDS = ["abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", "abuse", "access", "accident", "account", "accuse", "achieve", "acid", "acoustic", "acquire", "across", "act", "action", "actor", "actress", "actual", "adapt", "add", "addict", "address", "adjust", "admit", "adult", "advance", "advice", "aerobic", "affair", "afford", "afraid", "again", "age", "agent", "agree", "ahead", "aim", "air", "airport", "aisle", "alarm", "album", "alcohol", "alert", "alien", "all", "alley", "allow", "almost", "alone", "alpha", "already", "also", "alter", "always", "amateur", "amazing", "among", "amount", "amused", "analyst", "anchor", "ancient", "anger", "angle", "angry", "animal", "ankle", "announce", "annual", "another", "answer", "antenna", "antique", "anxiety", "any", "apart", "apology", "appear", "apple", "approve", "april", "arch", "arctic", "area", "arena", "argue", "arm", "armed", "armor", "army", "around", "arrange", "arrest", "arrive", "arrow", "art", "artefact", "artist", "artwork", "ask", "aspect", "assault", "asset", "assist", "assume", "asthma", "athlete", "atom", "attack", "attend", "attitude", "attract", "auction", "audit", "august", "aunt", "author", "auto", "autumn", "average", "avocado", "avoid", "awake", "aware", "away", "awesome", "awful", "awkward", "axis", "baby", "bachelor", "bacon", "badge", "bag", "balance", "balcony", "ball", "bamboo", "banana", "banner", "bar", "barely", "bargain", "barrel", "base", "basic", "basket", "battle", "beach", "bean", "beauty", "because", "become", "beef", "before", "begin", "behave", "behind", "believe", "below", "belt", "bench", "benefit", "best", "betray", "better", "between", "beyond", "bicycle", "bid", "bike", "bind", "biology", "bird", "birth", "bitter", "black", "blade", "blame", "blanket", "blast", "bleak", "bless", "blind", "blood", "blossom", "blouse", "blue", "blur", "blush", "board", "boat", "body", "boil", "bomb", "bone", "bonus", "book", "boost", "border", "boring", "borrow", "boss", "bottom", "bounce", "box", "boy", "bracket", "brain", "brand", "brass", "brave", "bread", "breeze", "brick", "bridge", "brief", "bright", "bring", "brisk", "broccoli", "broken", "bronze", "broom", "brother", "brown", "brush", "bubble", "buddy", "budget", "buffalo", "build", "bulb", "bulk", "bullet", "bundle", "bunker", "burden", "burger", "burst", "bus", "business", "busy", "butter", "buyer", "buzz", "cabbage", "cabin", "cable", "cactus", "cage", "cake", "call", "calm", "camera", "camp", "can", "canal", "cancel", "candy", "cannon", "canoe", "canvas", "canyon", "capable", "capital", "captain", "car", "carbon", "card", "cargo", "carpet", "carry", "cart", "case", "cash", "casino", "castle", "casual", "cat", "catalog", "catch", "category", "cattle", "caught", "cause", "caution", "cave", "ceiling", "celery", "cement", "census", "century", "cereal", "certain", "chair", "chalk", "champion", "change", "chaos", "chapter", "charge", "chase", "chat", "cheap", "check", "cheese", "chef", "cherry", "chest", "chicken", "chief", "child", "chimney", "choice", "choose", "chronic", "chuckle", "chunk", "churn", "cigar", "cinnamon", "circle", "citizen", "city", "civil", "claim", "clap", "clarify", "claw", "clay", "clean", "clerk", "clever", "click", "client", "cliff", "climb", "clinic", "clip", "clock", "clog", "close", "cloth", "cloud", "clown", "club", "clump", "cluster", "clutch", "coach", "coast", "coconut", "code", "coffee", "coil", "coin", "collect", "color", "column", "combine", "come", "comfort", "comic", "common", "company", "concert", "conduct", "confirm", "congress", "connect", "consider", "control", "convince", "cook", "cool", "copper", "copy", "coral", "core", "corn", "correct", "cost", "cotton", "couch", "country", "couple", "course", "cousin", "cover", "coyote", "crack", "cradle", "craft", "cram", "crane", "crash", "crater", "crawl", "crazy", "cream", "credit", "creek", "crew", "cricket", "crime", "crisp", "critic", "crop", "cross", "crouch", "crowd", "crucial", "cruel", "cruise", "crumble", "crunch", "crush", "cry", "crystal", "cube", "culture", "cup", "cupboard", "curious", "current", "curtain", "curve", "cushion", "custom", "cute", "cycle", "dad", "damage", "damp", "dance", "danger", "daring", "dash", "daughter", "dawn", "day", "deal", "debate", "debris", "decade", "december", "decide", "decline", "decorate", "decrease", "deer", "defense", "define", "defy", "degree", "delay", "deliver", "demand", "demise", "denial", "dentist", "deny", "depart", "depend", "deposit", "depth", "deputy", "derive", "describe", "desert", "design", "desk", "despair", "destroy", "detail", "detect", "develop", "device", "devote", "diagram", "dial", "diamond", "diary", "dice", "diesel", "diet", "differ", "digital", "dignity", "dilemma", "dinner", "dinosaur", "direct", "dirt", "disagree", "discover", "disease", "dish", "dismiss", "disorder", "display", "distance", "divert", "divide", "divorce", "dizzy", "doctor", "document", "dog", "doll", "dolphin", "domain", "donate", "donkey", "donor", "door", "dose", "double", "dove", "draft", "dragon", "drama", "drastic", "draw", "dream", "dress", "drift", "drill", "drink", "drip", "drive", "drop", "drum", "dry", "duck", "dumb", "dune", "during", "dust", "dutch", "duty", "dwarf", "dynamic", "eager", "eagle", "early", "earn", "earth", "easily", "east", "easy", "echo", "ecology", "economy", "edge", "edit", "educate", "effort", "egg", "eight", "either", "elbow", "elder", "electric", "elegant", "element", "elephant", "elevator", "elite", "else", "embark", "embody", "embrace", "emerge", "emotion", "employ", "empower", "empty", "enable", "enact", "end", "endless", "endorse", "enemy", "energy", "enforce", "engage", "engine", "enhance", "enjoy", "enlist", "enough", "enrich", "enroll", "ensure", "enter", "entire", "entry", "envelope", "episode", "equal", "equip", "era", "erase", "erode", "erosion", "error", "erupt", "escape", "essay", "essence", "estate", "eternal", "ethics", "evidence", "evil", "evoke", "evolve", "exact", "example", "excess", "exchange", "excite", "exclude", "excuse", "execute", "exercise", "exhaust", "exhibit", "exile", "exist", "exit", "exotic", "expand", "expect", "expire", "explain", "expose", "express", "extend", "extra", "eye", "eyebrow", "fabric", "face", "faculty", "fade", "faint", "faith", "fall", "false", "fame", "family", "famous", "fan", "fancy", "fantasy", "farm", "fashion", "fat", "fatal", "father", "fatigue", "fault", "favorite", "feature", "february", "federal", "fee", "feed", "feel", "female", "fence", "festival", "fetch", "fever", "few", "fiber", "fiction", "field", "figure", "file", "film", "filter", "final", "find", "fine", "finger", "finish", "fire", "firm", "first", "fiscal", "fish", "fit", "fitness", "fix", "flag", "flame", "flash", "flat", "flavor", "flee", "flight", "flip", "float", "flock", "floor", "flower", "fluid", "flush", "fly", "foam", "focus", "fog", "foil", "fold", "follow", "food", "foot", "force", "forest", "forget", "fork", "fortune", "forum", "forward", "fossil", "foster", "found", "fox", "fragile", "frame", "frequent", "fresh", "friend", "fringe", "frog", "front", "frost", "frown", "frozen", "fruit", "fuel", "fun", "funny", "furnace", "fury", "future", "gadget", "gain", "galaxy", "gallery", "game", "gap", "garage", "garbage", "garden", "garlic", "garment", "gas", "gasp", "gate", "gather", "gauge", "gaze", "general", "genius", "genre", "gentle", "genuine", "gesture", "ghost", "giant", "gift", "giggle", "ginger", "giraffe", "girl", "give", "glad", "glance", "glare", "glass", "glide", "glimpse", "globe", "gloom", "glory", "glove", "glow", "glue", "goat", "goddess", "gold", "good", "goose", "gorilla", "gospel", "gossip", "govern", "gown", "grab", "grace", "grain", "grant", "grape", "grass", "gravity", "great", "green", "grid", "grief", "grit", "grocery", "group", "grow", "grunt", "guard", "guess", "guide", "guilt", "guitar", "gun", "gym", "habit", "hair", "half", "hammer", "hamster", "hand", "happy", "harbor", "hard", "harsh", "harvest", "hat", "have", "hawk", "hazard", "head", "health", "heart", "heavy", "hedgehog", "height", "hello", "helmet", "help", "hen", "hero", "hidden", "high", "hill", "hint", "hip", "hire", "history", "hobby", "hockey", "hold", "hole", "holiday", "hollow", "home", "honey", "hood", "hope", "horn", "horror", "horse", "hospital", "host", "hotel", "hour", "hover", "hub", "huge", "human", "humble", "humor", "hundred", "hungry", "hunt", "hurdle", "hurry", "hurt", "husband", "hybrid", "ice", "icon", "idea", "identify", "idle", "ignore", "ill", "illegal", "illness", "image", "imitate", "immense", "immune", "impact", "impose", "improve", "impulse", "inch", "include", "income", "increase", "index", "indicate", "indoor", "industry", "infant", "inflict", "inform", "inhale", "inherit", "initial", "inject", "injury", "inmate", "inner", "innocent", "input", "inquiry", "insane", "insect", "inside", "inspire", "install", "intact", "interest", "into", "invest", "invite", "involve", "iron", "island", "isolate", "issue", "item", "ivory", "jacket", "jaguar", "jar", "jazz", "jealous", "jeans", "jelly", "jewel", "job", "join", "joke", "journey", "joy", "judge", "juice", "jump", "jungle", "junior", "junk", "just", "kangaroo", "keen", "keep", "ketchup", "key", "kick", "kid", "kidney", "kind", "kingdom", "kiss", "kit", "kitchen", "kite", "kitten", "kiwi", "knee", "knife", "knock", "know", "lab", "label", "labor", "ladder", "lady", "lake", "lamp", "language", "laptop", "large", "later", "latin", "laugh", "laundry", "lava", "law", "lawn", "lawsuit", "layer", "lazy", "leader", "leaf", "learn", "leave", "lecture", "left", "leg", "legal", "legend", "leisure", "lemon", "lend", "length", "lens", "leopard", "lesson", "letter", "level", "liar", "liberty", "library", "license", "life", "lift", "light", "like", "limb", "limit", "link", "lion", "liquid", "list", "little", "live", "lizard", "load", "loan", "lobster", "local", "lock", "logic", "lonely", "long", "loop", "lottery", "loud", "lounge", "love", "loyal", "lucky", "luggage", "lumber", "lunar", "lunch", "luxury", "lyrics", "machine", "mad", "magic", "magnet", "maid", "mail", "main", "major", "make", "mammal", "man", "manage", "mandate", "mango", "mansion", "manual", "maple", "marble", "march", "margin", "marine", "market", "marriage", "mask", "mass", "master", "match", "material", "math", "matrix", "matter", "maximum", "maze", "meadow", "mean", "measure", "meat", "mechanic", "medal", "media", "melody", "melt", "member", "memory", "mention", "menu", "mercy", "merge", "merit", "merry", "mesh", "message", "metal", "method", "middle", "midnight", "milk", "million", "mimic", "mind", "minimum", "minor", "minute", "miracle", "mirror", "misery", "miss", "mistake", "mix", "mixed", "mixture", "mobile", "model", "modify", "mom", "moment", "monitor", "monkey", "monster", "month", "moon", "moral", "more", "morning", "mosquito", "mother", "motion", "motor", "mountain", "mouse", "move", "movie", "much", "muffin", "mule", "multiply", "muscle", "museum", "mushroom", "music", "must", "mutual", "myself", "mystery", "myth", "naive", "name", "napkin", "narrow", "nasty", "nation", "nature", "near", "neck", "need", "negative", "neglect", "neither", "nephew", "nerve", "nest", "net", "network", "neutral", "never", "news", "next", "nice", "night", "noble", "noise", "nominee", "noodle", "normal", "north", "nose", "notable", "note", "nothing", "notice", "novel", "now", "nuclear", "number", "nurse", "nut", "oak", "obey", "object", "oblige", "obscure", "observe", "obtain", "obvious", "occur", "ocean", "october", "odor", "off", "offer", "office", "often", "oil", "okay", "old", "olive", "olympic", "omit", "once", "one", "onion", "online", "only", "open", "opera", "opinion", "oppose", "option", "orange", "orbit", "orchard", "order", "ordinary", "organ", "orient", "original", "orphan", "ostrich", "other", "outdoor", "outer", "output", "outside", "oval", "oven", "over", "own", "owner", "oxygen", "oyster", "ozone", "pact", "paddle", "page", "pair", "palace", "palm", "panda", "panel", "panic", "panther", "paper", "parade", "parent", "park", "parrot", "party", "pass", "patch", "path", "patient", "patrol", "pattern", "pause", "pave", "payment", "peace", "peanut", "pear", "peasant", "pelican", "pen", "penalty", "pencil", "people", "pepper", "perfect", "permit", "person", "pet", "phone", "photo", "phrase", "physical", "piano", "picnic", "picture", "piece", "pig", "pigeon", "pill", "pilot", "pink", "pioneer", "pipe", "pistol", "pitch", "pizza", "place", "planet", "plastic", "plate", "play", "please", "pledge", "pluck", "plug", "plunge", "poem", "poet", "point", "polar", "pole", "police", "pond", "pony", "pool", "popular", "portion", "position", "possible", "post", "potato", "pottery", "poverty", "powder", "power", "practice", "praise", "predict", "prefer", "prepare", "present", "pretty", "prevent", "price", "pride", "primary", "print", "priority", "prison", "private", "prize", "problem", "process", "produce", "profit", "program", "project", "promote", "proof", "property", "prosper", "protect", "proud", "provide", "public", "pudding", "pull", "pulp", "pulse", "pumpkin", "punch", "pupil", "puppy", "purchase", "purity", "purpose", "purse", "push", "put", "puzzle", "pyramid", "quality", "quantum", "quarter", "question", "quick", "quit", "quiz", "quote", "rabbit", "raccoon", "race", "rack", "radar", "radio", "rail", "rain", "raise", "rally", "ramp", "ranch", "random", "range", "rapid", "rare", "rate", "rather", "raven", "raw", "razor", "ready", "real", "reason", "rebel", "rebuild", "recall", "receive", "recipe", "record", "recycle", "reduce", "reflect", "reform", "refuse", "region", "regret", "regular", "reject", "relax", "release", "relief", "rely", "remain", "remember", "remind", "remove", "render", "renew", "rent", "reopen", "repair", "repeat", "replace", "report", "require", "rescue", "resemble", "resist", "resource", "response", "result", "retire", "retreat", "return", "reunion", "reveal", "review", "reward", "rhythm", "rib", "ribbon", "rice", "rich", "ride", "ridge", "rifle", "right", "rigid", "ring", "riot", "ripple", "risk", "ritual", "rival", "river", "road", "roast", "robot", "robust", "rocket", "romance", "roof", "rookie", "room", "rose", "rotate", "rough", "round", "route", "royal", "rubber", "rude", "rug", "rule", "run", "runway", "rural", "sad", "saddle", "sadness", "safe", "sail", "salad", "salmon", "salon", "salt", "salute", "same", "sample", "sand", "satisfy", "satoshi", "sauce", "sausage", "save", "say", "scale", "scan", "scare", "scatter", "scene", "scheme", "school", "science", "scissors", "scorpion", "scout", "scrap", "screen", "script", "scrub", "sea", "search", "season", "seat", "second", "secret", "section", "security", "seed", "seek", "segment", "select", "sell", "seminar", "senior", "sense", "sentence", "series", "service", "session", "settle", "setup", "seven", "shadow", "shaft", "shallow", "share", "shed", "shell", "sheriff", "shield", "shift", "shine", "ship", "shiver", "shock", "shoe", "shoot", "shop", "short", "shoulder", "shove", "shrimp", "shrug", "shuffle", "shy", "sibling", "sick", "side", "siege", "sight", "sign", "silent", "silk", "silly", "silver", "similar", "simple", "since", "sing", "siren", "sister", "situate", "six", "size", "skate", "sketch", "ski", "skill", "skin", "skirt", "skull", "slab", "slam", "sleep", "slender", "slice", "slide", "slight", "slim", "slogan", "slot", "slow", "slush", "small", "smart", "smile", "smoke", "smooth", "snack", "snake", "snap", "sniff", "snow", "soap", "soccer", "social", "sock", "soda", "soft", "solar", "soldier", "solid", "solution", "solve", "someone", "song", "soon", "sorry", "sort", "soul", "sound", "soup", "source", "south", "space", "spare", "spatial", "spawn", "speak", "special", "speed", "spell", "spend", "sphere", "spice", "spider", "spike", "spin", "spirit", "split", "spoil", "sponsor", "spoon", "sport", "spot", "spray", "spread", "spring", "spy", "square", "squeeze", "squirrel", "stable", "stadium", "staff", "stage", "stairs", "stamp", "stand", "start", "state", "stay", "steak", "steel", "stem", "step", "stereo", "stick", "still", "sting", "stock", "stomach", "stone", "stool", "story", "stove", "strategy", "street", "strike", "strong", "struggle", "student", "stuff", "stumble", "style", "subject", "submit", "subway", "success", "such", "sudden", "suffer", "sugar", "suggest", "suit", "summer", "sun", "sunny", "sunset", "super", "supply", "supreme", "sure", "surface", "surge", "surprise", "surround", "survey", "suspect", "sustain", "swallow", "swamp", "swap", "swarm", "swear", "sweet", "swift", "swim", "swing", "switch", "sword", "symbol", "symptom", "syrup", "system", "table", "tackle", "tag", "tail", "talent", "talk", "tank", "tape", "target", "task", "taste", "tattoo", "taxi", "teach", "team", "tell", "ten", "tenant", "tennis", "tent", "term", "test", "text", "thank", "that", "theme", "then", "theory", "there", "they", "thing", "this", "thought", "three", "thrive", "throw", "thumb", "thunder", "ticket", "tide", "tiger", "tilt", "timber", "time", "tiny", "tip", "tired", "tissue", "title", "toast", "tobacco", "today", "toddler", "toe", "together", "toilet", "token", "tomato", "tomorrow", "tone", "tongue", "tonight", "tool", "tooth", "top", "topic", "topple", "torch", "tornado", "tortoise", "toss", "total", "tourist", "toward", "tower", "town", "toy", "track", "trade", "traffic", "tragic", "train", "transfer", "trap", "trash", "travel", "tray", "treat", "tree", "trend", "trial", "tribe", "trick", "trigger", "trim", "trip", "trophy", "trouble", "truck", "true", "truly", "trumpet", "trust", "truth", "try", "tube", "tuition", "tumble", "tuna", "tunnel", "turkey", "turn", "turtle", "twelve", "twenty", "twice", "twin", "twist", "two", "type", "typical", "ugly", "umbrella", "unable", "unaware", "uncle", "uncover", "under", "undo", "unfair", "unfold", "unhappy", "uniform", "unique", "unit", "universe", "unknown", "unlock", "until", "unusual", "unveil", "update", "upgrade", "uphold", "upon", "upper", "upset", "urban", "urge", "usage", "use", "used", "useful", "useless", "usual", "utility", "vacant", "vacuum", "vague", "valid", "valley", "valve", "van", "vanish", "vapor", "various", "vast", "vault", "vehicle", "velvet", "vendor", "venture", "venue", "verb", "verify", "version", "very", "vessel", "veteran", "viable", "vibrant", "vicious", "victory", "video", "view", "village", "vintage", "violin", "virtual", "virus", "visa", "visit", "visual", "vital", "vivid", "vocal", "voice", "void", "volcano", "volume", "vote", "voyage", "wage", "wagon", "wait", "walk", "wall", "walnut", "want", "warfare", "warm", "warrior", "wash", "wasp", "waste", "water", "wave", "way", "wealth", "weapon", "wear", "weasel", "weather", "web", "wedding", "weekend", "weird", "welcome", "west", "wet", "whale", "what", "wheat", "wheel", "when", "where", "whip", "whisper", "wide", "width", "wife", "wild", "will", "win", "window", "wine", "wing", "wink", "winner", "winter", "wire", "wisdom", "wise", "wish", "witness", "wolf", "woman", "wonder", "wood", "wool", "word", "work", "world", "worry", "worth", "wrap", "wreck", "wrestle", "wrist", "write", "wrong", "yard", "year", "yellow", "you", "young", "youth", "zebra", "zero", "zone", "zoo"] @@ -27,6 +30,7 @@ class SeedToolsView(View): self.selected_possible_words_index = 0 self.seed_length = 12 # Default to 12, Valid values are 11, 12, 23 and 24 self.seed_qr_image = None + self.seed_entropy_image = None # Dice information self.roll_number = 1 @@ -1392,7 +1396,6 @@ class SeedToolsView(View): cur_y = next_y - def parse_seed_qr_data(self): try: data = self.controller.from_camera_queue.get(False) @@ -1439,6 +1442,95 @@ class SeedToolsView(View): return self.words[:] + def parse_seed_image_data(self): + try: + data = self.controller.from_camera_queue.get(block=False) + except: + return + + print("parse_seed_image_data got data") + + try: + # self.seed_entropy_image = Image.fromarray(data[0]) + self.seed_entropy_image = data[0] + print(self.seed_entropy_image) + + self.buttons.trigger_override(True) + except Exception as e: + traceback.print_exc() + print("exiting parse_seed_image_data") + + + def seed_phrase_from_camera_image(self): + self.seed_entropy_image = None + self.controller.menu_view.draw_modal(["Initializing Camera..."]) # TODO: Move to Controller + # initialize camera + self.controller.to_camera_queue.put(["single_frame"]) + # First get blocking, this way it's clear when the camera is ready for the end user + print("Waiting for 'ready' from camera") + while True: + try: + msg = self.controller.from_camera_queue.get(block=False) + print(f"msg received: {msg}") + break + except: + time.sleep(0.1) + + self.controller.menu_view.draw_modal(["Aim camera", "click joystick"], title="Capture Image as Seed", bottom="Left to Cancel") # TODO: Move to Controller + + try: + input = self.buttons.wait_for([B.KEY_LEFT, B.KEY_PRESS]) + if input == B.KEY_LEFT: + print("KEY_LEFT") + # Returning empty will kick us back to SEED_TOOLS_SUB_MENU + self.words = [] + elif input == B.KEY_PRESS: + print("KEY_PRESS") + self.controller.menu_view.draw_modal(["Auto-adjusting", "exposure..."], title="Capture Image as Seed", bottom="Left to Cancel") # TODO: Move to Controller + self.controller.to_camera_queue.put(["click"]) + self.camera_loop_timer = CameraPoll(0.05, self.parse_seed_image_data) + + input = self.buttons.wait_for([B.KEY_LEFT]) + self.controller.to_camera_queue.put(["stop"]) + self.camera_loop_timer.stop() + + if input == B.KEY_LEFT: + print("KEY_LEFT") + # Returning empty will kick us back to SEED_TOOLS_SUB_MENU + self.words = [] + return self.words + + # Resize and display + View.DispShowImageWithText( + self.seed_entropy_image.resize((View.canvas_width, View.canvas_height), Image.BICUBIC), + text="<= reshoot | accept =>", + font=View.ROBOTOCONDENSED_REGULAR_22, + text_color=View.color + ) + + input = self.buttons.wait_for([B.KEY_LEFT, B.KEY_RIGHT]) + if input == B.KEY_LEFT: + return self.seed_phrase_from_camera_image() + + hash = hashlib.sha256(self.seed_entropy_image.tobytes()) + badseedphrase_str = mnemonic_from_bytes(hash.digest()) + badseedphrase_list = badseedphrase_str.split() + badseedphrase_list.pop(-1) + calclastwordphrasestr = " ".join(badseedphrase_list) + " abandon" + goodphrasebytes = mnemonic_to_bytes(calclastwordphrasestr, ignore_checksum=True) + goodseedphrasestr = mnemonic_from_bytes(goodphrasebytes) + self.words = goodseedphrasestr.split() + + except Exception as e: + traceback.print_exc() + finally: + print(f"Stopping QR code scanner") + self.controller.to_camera_queue.put(["stop"]) + self.camera_loop_timer.stop() + + time.sleep(0.5) # give time for camera loop to complete before returning data + self.buttons.trigger_override(True) + return self.words[:] ### ### Utility Methods ### From c7a89946f1e97e7ffce10f99cf4f4de3a73d6701 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 12 Jul 2021 10:55:37 -0500 Subject: [PATCH 27/39] Auto exposure, autocontrast filter --- src/seedsigner/helpers/camera_process.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/seedsigner/helpers/camera_process.py b/src/seedsigner/helpers/camera_process.py index 2b587169..490c504c 100644 --- a/src/seedsigner/helpers/camera_process.py +++ b/src/seedsigner/helpers/camera_process.py @@ -90,13 +90,23 @@ class CameraProcess(): elif msg[0] == "click": print("Received 'click'") + + # Wait for the automatic gain control to settle + time.sleep(2) + # Now fix the values + camera.shutter_speed = camera.exposure_speed + camera.exposure_mode = 'off' + g = camera.awb_gains + camera.awb_mode = 'off' + camera.awb_gains = g + stream = io.BytesIO() camera.capture(stream, format='jpeg') # "Rewind" the stream to the beginning so we can read its content stream.seek(0) - out_queue.put([autocontrast(Image.open(stream)).rotate(90)]) + out_queue.put([autocontrast(Image.open(stream), cutoff=2).rotate(90)]) except Exception as e: traceback.print_exc() From 2ef55a521966454b35e7a38d9cfc694607eea013 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 12 Jul 2021 20:16:15 -0500 Subject: [PATCH 28/39] Hide image edges; improved QR template --- docs/qr_code_template.py | 67 ++++++++++++-- docs/qr_code_template_25x25.html | 71 ++++++++++++++- docs/qr_code_template_29x29.html | 110 ++++++++++++++++++++++- src/seedsigner/controller.py | 4 +- src/seedsigner/helpers/camera_process.py | 6 +- src/seedsigner/views/seed_tools_view.py | 22 ++++- src/seedsigner/views/view.py | 13 +-- 7 files changed, 269 insertions(+), 24 deletions(-) diff --git a/docs/qr_code_template.py b/docs/qr_code_template.py index a70916b4..0ddd092f 100644 --- a/docs/qr_code_template.py +++ b/docs/qr_code_template.py @@ -1,6 +1,13 @@ import math def generate_qr_template(qr_size, block_size=5, show_timing_marks=False): + if qr_size == 29: + num_words = 24 + word_cols = 2 + else: + num_words = 12 + word_cols = 1 + html = """ @@ -9,6 +16,7 @@ def generate_qr_template(qr_size, block_size=5, show_timing_marks=False): """ - if qr_size == 21: - html += f"""

{qr_size}x{qr_size} QR Code
""" - elif qr_size == 25: - html += f"""
12-word Seed
""" - else: - html += f"""
24-word Seed
""" + + html += """ + + + +
+ """ + for i in range(1, num_words + 1): + if i == 13: + html += """ + + """ + html += f"""
+ {i}: ____________________________ +
+ """ + html += "
" y_names = "A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,X,Y,Z" - html += """""" + html += """
""" html += f"""""" for j in range(0, math.ceil(qr_size/block_size)): html += f"""""" diff --git a/docs/qr_code_template_25x25.html b/docs/qr_code_template_25x25.html index a2cb69e0..01999e17 100644 --- a/docs/qr_code_template_25x25.html +++ b/docs/qr_code_template_25x25.html @@ -6,6 +6,7 @@ -
12-word Seed
{j + 1}
+ +
12345
+ +
+
+ 1: ____________________________ +
+
+ 2: ____________________________ +
+
+ 3: ____________________________ +
+
+ 4: ____________________________ +
+
+ 5: ____________________________ +
+
+ 6: ____________________________ +
+
+ 7: ____________________________ +
+
+ 8: ____________________________ +
+
+ 9: ____________________________ +
+
+ 10: ____________________________ +
+
+ 11: ____________________________ +
+
+ 12: ____________________________ +
+
diff --git a/docs/qr_code_template_29x29.html b/docs/qr_code_template_29x29.html index 7767c9fd..281d8c85 100644 --- a/docs/qr_code_template_29x29.html +++ b/docs/qr_code_template_29x29.html @@ -6,6 +6,7 @@ -
24-word Seed
12345
A
+ +
123456
+ + +
+
+ 1: ____________________________ +
+
+ 2: ____________________________ +
+
+ 3: ____________________________ +
+
+ 4: ____________________________ +
+
+ 5: ____________________________ +
+
+ 6: ____________________________ +
+
+ 7: ____________________________ +
+
+ 8: ____________________________ +
+
+ 9: ____________________________ +
+
+ 10: ____________________________ +
+
+ 11: ____________________________ +
+
+ 12: ____________________________ +
+ +
+
+ 13: ____________________________ +
+
+ 14: ____________________________ +
+
+ 15: ____________________________ +
+
+ 16: ____________________________ +
+
+ 17: ____________________________ +
+
+ 18: ____________________________ +
+
+ 19: ____________________________ +
+
+ 20: ____________________________ +
+
+ 21: ____________________________ +
+
+ 22: ____________________________ +
+
+ 23: ____________________________ +
+
+ 24: ____________________________ +
+
diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index eca9db68..421dcfae 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -266,8 +266,8 @@ class Controller: if ret_val == True: break else: - # no-op; can't back out of the seed phrase view - pass + # Start over + return self.show_create_seed_with_image_tool() # Ask to save seed if self.storage.slot_avaliable(): diff --git a/src/seedsigner/helpers/camera_process.py b/src/seedsigner/helpers/camera_process.py index 490c504c..d0021421 100644 --- a/src/seedsigner/helpers/camera_process.py +++ b/src/seedsigner/helpers/camera_process.py @@ -62,13 +62,11 @@ class CameraProcess(): def capture_single_frame(in_queue, out_queue): from PIL import Image - from PIL.ImageOps import autocontrast import picamera print("Instantiate PiCamera start") start_time = int(time.time() * 1000) - # camera = picamera.PiCamera(resolution=(512, 384),framerate=12) - camera = picamera.PiCamera(resolution=(480, 480), framerate=8) + camera = picamera.PiCamera(resolution=(720, 480), framerate=8) end_time = int(time.time() * 1000) print(f"Instantiate PiCamera finish: {end_time - start_time}ms") @@ -106,7 +104,7 @@ class CameraProcess(): # "Rewind" the stream to the beginning so we can read its content stream.seek(0) - out_queue.put([autocontrast(Image.open(stream), cutoff=2).rotate(90)]) + out_queue.put([Image.open(stream)]) except Exception as e: traceback.print_exc() diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index 23dde26a..79fff5fb 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -2,6 +2,7 @@ from embit import bip39 from embit.bip39 import mnemonic_to_bytes, mnemonic_from_bytes from PIL import ImageDraw, Image +from PIL.ImageOps import autocontrast import hashlib import math import time @@ -1500,12 +1501,25 @@ class SeedToolsView(View): self.words = [] return self.words - # Resize and display + # Prep a copy of the image for display. The actual image data is 720x480 + # Present just a center crop to fit the screen and to keep some of the + # data hidden. + display_version = autocontrast( + self.seed_entropy_image, cutoff=2 + ).rotate( + 90 + ).crop( + (120, 0, 600, 480) + ).resize( + (View.canvas_width, View.canvas_height), Image.BICUBIC + ) + View.DispShowImageWithText( - self.seed_entropy_image.resize((View.canvas_width, View.canvas_height), Image.BICUBIC), - text="<= reshoot | accept =>", + display_version, + text=" < reshoot | accept > ", font=View.ROBOTOCONDENSED_REGULAR_22, - text_color=View.color + text_color=View.color, + text_background=(0,0,0,225) ) input = self.buttons.wait_for([B.KEY_LEFT, B.KEY_RIGHT]) diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index 53dfcc57..f11e4f30 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -128,15 +128,18 @@ class View: def DispShowImageWithText(image, text, font=None, text_color="GREY", text_background=None): - image_copy = image.copy() + image_copy = image.copy().convert("RGBA") draw = ImageDraw.Draw(image_copy) + + text_overlay = Image.new("RGBA", (View.canvas_width, View.canvas_height), (255,255,255,0)) + text_overlay_draw = ImageDraw.Draw(text_overlay) if not font: font = View.COURIERNEW14 - tw, th = draw.textsize(text, font=font) + tw, th = text_overlay_draw.textsize(text, font=font) if text_background: - draw.rectangle(((240 - tw) / 2 - 3, 240 - th, (240 - tw) / 2 + tw + 3, 240), fill=text_background) - draw.text(((240 - tw) / 2, 240 - th - 1), text, fill=text_color, font=font) - View.disp.ShowImage(image_copy, 0, 0) + text_overlay_draw.rectangle(((240 - tw) / 2 - 3, 240 - th, (240 - tw) / 2 + tw + 3, 240), fill=text_background) + text_overlay_draw.text(((240 - tw) / 2, 240 - th - 1), text, fill=text_color, font=font) + View.DispShowImage(image_copy, alpha_overlay=text_overlay) def draw_modal(self, lines = [], title = "", bottom = "") -> None: From e72389f31cdaed84ebe88c409734e0298a8dbb52 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Wed, 14 Jul 2021 18:09:50 -0500 Subject: [PATCH 29/39] Restores word num display during manual seed entry --- src/seedsigner/views/seed_tools_view.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index 79fff5fb..8058a83d 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -234,7 +234,7 @@ class SeedToolsView(View): rect=(self.previous_button_width,0, View.canvas_width,text_entry_display_height), font=View.ROBOTOCONDENSED_BOLD_26, font_color=View.color, - cur_text="".join(self.letters) + cur_text=f"{num_word}: " + "".join(self.letters) ) text_entry_display.render() @@ -351,7 +351,7 @@ class SeedToolsView(View): # Animate the selection storage, then return the word to the caller self.letters = list(final_selection + " ") render_possible_matches(highlight_word=final_selection) - text_entry_display.render("".join(self.letters)) + text_entry_display.render(f"{num_word}: " + "".join(self.letters)) View.DispShowImage() return final_selection @@ -406,7 +406,7 @@ class SeedToolsView(View): render_possible_matches() # Render the text entry display and cursor block - text_entry_display.render("".join(self.letters)) + text_entry_display.render(f"{num_word}: " + "".join(self.letters)) View.DispShowImage() @@ -545,7 +545,7 @@ class SeedToolsView(View): if input == B.KEY3: # Save! if self.passphrase != "" and self.passphrase != " ": - return self.passphrase + return self.passphrase.strip() elif input == B.KEY_PRESS and previous_button_is_active: # Prev button clicked; return empty string to signal cancel. @@ -1505,7 +1505,8 @@ class SeedToolsView(View): # Present just a center crop to fit the screen and to keep some of the # data hidden. display_version = autocontrast( - self.seed_entropy_image, cutoff=2 + self.seed_entropy_image, + cutoff=2 ).rotate( 90 ).crop( From 00d78ee57e7bf757df6cac68b70c79e0c09a81a9 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 18 Jul 2021 10:34:06 -0500 Subject: [PATCH 30/39] Updating QR template PDFs --- docs/12_word_seed.pdf | Bin 0 -> 21302 bytes docs/24_word_seed.pdf | Bin 0 -> 48486 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/12_word_seed.pdf create mode 100644 docs/24_word_seed.pdf diff --git a/docs/12_word_seed.pdf b/docs/12_word_seed.pdf new file mode 100644 index 0000000000000000000000000000000000000000..9d42e6009f3b52ba5abff29931156cdff1cbffdc GIT binary patch literal 21302 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 Date: Wed, 21 Jul 2021 12:35:23 -0400 Subject: [PATCH 31/39] move blue wallet to ur2 --- src/seedsigner/models/blue_wallet.py | 154 +++++++++++++++++++-------- 1 file changed, 110 insertions(+), 44 deletions(-) diff --git a/src/seedsigner/models/blue_wallet.py b/src/seedsigner/models/blue_wallet.py index 14d1ffa5..2f3feea3 100644 --- a/src/seedsigner/models/blue_wallet.py +++ b/src/seedsigner/models/blue_wallet.py @@ -1,7 +1,12 @@ from . import Wallet -from seedsigner.helpers import QR +from seedsigner.helpers import Buttons, B, CameraProcess, CameraPoll, QR from seedsigner.helpers.bcur import (bcur_decode, cbor_decode, bc32decode, bc32encode, cbor_encode, bcur_encode) +from seedsigner.helpers.ur2.ur_decoder import URDecoder +from seedsigner.helpers.ur2.ur_encoder import UREncoder +from seedsigner.helpers.ur2.cbor_lite import CBOREncoder +from seedsigner.helpers.ur2.ur import UR +from seedsigner.views import View # External Dependencies from embit.bip39 import mnemonic_to_bytes @@ -21,16 +26,20 @@ class BlueWallet(Wallet): def __init__(self, current_network = "main", qr_density = Wallet.QRMEDIUM, policy = "PKWSH") -> None: Wallet.__init__(self, current_network, qr_density, policy) + def set_seed_phrase(self, seed_phrase, passphrase): + Wallet.set_seed_phrase(self, seed_phrase, passphrase) + self.ur_decoder = URDecoder() + def get_name(self) -> str: return "Blue Wallet" def parse_psbt(self, raw_psbt) -> bool: - raw_psbt = b2a_base64(cbor_decode(bc32decode(raw_psbt))) - - base64_psbt = a2b_base64(raw_psbt) - self.tx = psbt.PSBT.parse(base64_psbt) - - self._parse_psbt() + try: + base64_psbt = a2b_base64(raw_psbt) + self.tx = psbt.PSBT.parse(base64_psbt) + self._parse_psbt() + except Exception: + return False return True @@ -48,25 +57,90 @@ class BlueWallet(Wallet): raw_trimmed_signed_psbt = trimmed_psbt.serialize() - return bcur_encode(raw_trimmed_signed_psbt) + # convert to base64 + b64_psbt = b2a_base64(raw_trimmed_signed_psbt) + # somehow b2a ends with \n... + if b64_psbt[-1:] == b"\n": + b64_psbt = b64_psbt[:-1] + + return b64_psbt.decode('utf-8') + + def scan_animated_qr_pbst(self, controller) -> str: + self.controller = controller + self.buttons = controller.buttons + self.controller.menu_view.draw_modal(["Initializing Camera"]) # TODO: Move to Controller + # initialize camera + self.controller.to_camera_queue.put(["start"]) + # First get blocking, this way it's clear when the camera is ready for the end user + self.controller.from_camera_queue.get() + self.camera_loop_timer = CameraPoll(0.05, self.process_camera_data) + + input = self.buttons.wait_for([B.KEY_LEFT, B.KEY_RIGHT]) + if input in (B.KEY_LEFT, B.KEY_RIGHT): + self.camera_loop_timer.stop() + self.controller.to_camera_queue.put(["stop"]) + return "nodata" + elif input == B.OVERRIDE: + self.camera_loop_timer.stop() + self.controller.to_camera_queue.put(["stop"]) + if self.qr_data[0] == "invalid": + return "invalid" + return b2a_base64(cbor_decode(self.ur_decoder.result.cbor)) + + def process_camera_data(self): + try: + data = self.controller.from_camera_queue.get(False) + except: + data = ["nodata"] + + if data[0] != "nodata": + if self.qr_total_frames == 0: + # get total frames if not set + self.qr_total_frames = type(self).total_frames_parse(data[0]) + if self.qr_total_frames == -1: + # when invalid, trigger override to display error + self.qr_data = ["invalid"] + self.buttons.trigger_override() # something went wrong, invalid QR + return + self.qr_data = ["empty"] + + # get data and percentage + decoder_work_check = self.ur_decoder.receive_part(data[0]) + if decoder_work_check == False: + self.qr_data = ["invalidpsbt"] + self.buttons.trigger_override() # something went wrong, invalid QR + return + self.percentage_complete = self.ur_decoder.estimated_percent_complete() + + # checking if all frames has been captured, exit camera processing + if self.capture_complete(): + self.buttons.trigger_override() + + # if all frames have not all been captured, display progress to screen/display + if not self.capture_complete() and self.scan_display_working == 0: + self.scan_display_working = 1 + View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) + tw, th = View.draw.textsize("Collecting QR Codes:", font=View.IMPACT25) + View.draw.text(((240 - tw) / 2, 15), "Collecting QR Codes:", fill=View.color, font=View.IMPACT25) + tw, th = View.draw.textsize(str(round(self.percentage_complete * 100)) + "% Complete", font=View.IMPACT22) + View.draw.text(((240 - tw) / 2, 125), str(round(self.percentage_complete * 100)) + "% Complete", fill=View.color, font=View.IMPACT22) + View.DispShowImage() + self.scan_display_working = 0 + + elif self.scan_started_ind == 0: + self.scan_started_ind = 1 + self.controller.menu_view.draw_modal(["Scan Animated QR"], "", "Right to Exit") def total_frames_parse(data) -> int: - if re.search("^UR\:BYTES\/(\d+)OF(\d+)", data, re.IGNORECASE) != None: - return int(re.search("^UR\:BYTES\/(\d+)OF(\d+)", data, re.IGNORECASE).group(2)) + if re.search("^UR\:CRYPTO-PSBT\/(\d+)\-(\d+)\/", data, re.IGNORECASE) != None: + return 10 # valid + elif re.search("^UR\:CRYPTO-PSBT", data, re.IGNORECASE) != None: + return 1 # valid but only 1 segment else: - return -1 - - def current_frame_parse(data) -> int: - if re.search("^UR\:BYTES\/(\d+)OF(\d+)", data, re.IGNORECASE) != None: - return int(re.search("^UR\:BYTES\/(\d+)OF(\d+)", data, re.IGNORECASE).group(1)) - else: - return -1 - - def data_parse(data) -> str: - return data.split("/")[-1].strip() + return -1 #invalid def capture_complete(self) -> bool: - if "empty" not in self.qr_data: + if self.ur_decoder.is_complete(): return True else: return False @@ -91,44 +165,36 @@ class BlueWallet(Wallet): def make_signing_qr_codes(self, data, callback = None) -> []: qr = QR() - - qrdata = data[0].upper() - qrhash = data[1].upper() - - cnt = 0 images = [] - start = 0 - stop = self.qrsize - qr_cnt = ((len(qrdata)-1) // self.qrsize) + 1 + cnt = 0 - if qr_cnt == 1: - part = "UR:BYTES/" + qrhash + "/" + qrdata[start:stop] + cbor_encoder = CBOREncoder() + cbor_encoder.encodeBytes(a2b_base64(data)) + + qr_ur_bytes = UR("crypto-psbt", cbor_encoder.get_bytes()) + ur2_encode = UREncoder(qr_ur_bytes,self.qrsize,0) + qr_cnt = ur2_encode.fountain_encoder.seq_len() + + while not ur2_encode.is_complete(): + + part = ur2_encode.next_part().upper() images.append(qr.qrimage(part)) print(part) - - while cnt < qr_cnt and qr_cnt != 1: - part = "UR:BYTES/" + str(cnt+1) + "OF" + str(qr_cnt) + "/" + qrhash + "/" + qrdata[start:stop] - images.append(qr.qrimage(part)) - print(part) - start = start + self.qrsize - stop = stop + self.qrsize - if stop > len(qrdata): - stop = len(qrdata) cnt += 1 if callback != None: - callback((cnt * 100.0) / qr_cnt) + callback((cnt * 100.0) / qr_cnt) return images def set_qr_density(self, density): self.cur_qr_density = density if density == Wallet.QRLOW: - self.qrsize = 20 + self.qrsize = 50 elif density == Wallet.QRMEDIUM: - self.qrsize = 100 + self.qrsize = 70 elif density == Wallet.QRHIGH: - self.qrsize = 140 + self.qrsize = 120 ### ### Internal Wallet Transactions OVERRIDE From 101ccee1a8555c0c2b3ced8b73b610ae616bbc2c Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Wed, 21 Jul 2021 12:44:24 -0400 Subject: [PATCH 32/39] update controller to have version 0.4.3 update controller to match the branch 0_4_3 version. --- 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 ff9ca6f8..130839b8 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -13,7 +13,7 @@ from .models import (SeedStorage, SpecterDesktopWallet, BlueWallet, class Controller: - VERSION = "0.4.2" + VERSION = "0.4.3" def __init__(self, config) -> None: controller = self From 6d6b41bac5115011ebc4de6f85bc42771084abf7 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 23 Jul 2021 20:36:20 -0500 Subject: [PATCH 33/39] Passphrase improvements, joystick hold timing updated --- src/seedsigner/helpers/buttons.py | 2 +- src/seedsigner/helpers/camera_process.py | 4 ---- src/seedsigner/views/seed_tools_view.py | 8 +++----- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/seedsigner/helpers/buttons.py b/src/seedsigner/helpers/buttons.py index 6c6b2150..80dd9d03 100644 --- a/src/seedsigner/helpers/buttons.py +++ b/src/seedsigner/helpers/buttons.py @@ -35,7 +35,7 @@ class Buttons: self.cur_input = None # Track which direction or button was last pressed self.cur_input_started = None # Track when that input began self.last_input_time = int(time.time() * 1000) # How long has it been since the last input? - self.first_repeat_threshold = 120 # Long-press time required before returning continuous input + self.first_repeat_threshold = 175 # Long-press time required before returning continuous input self.next_repeat_threshold = 250 # Amount of time where we no longer consider input a continuous hold diff --git a/src/seedsigner/helpers/camera_process.py b/src/seedsigner/helpers/camera_process.py index d0021421..2eff6dee 100644 --- a/src/seedsigner/helpers/camera_process.py +++ b/src/seedsigner/helpers/camera_process.py @@ -19,11 +19,7 @@ class CameraProcess(): end_time = int(time.time() * 1000) print(f"Instantiate PiVideoStream finish: {end_time - start_time}ms") - print("video_stream start()") - start_time = int(time.time() * 1000) video_stream.start() - end_time = int(time.time() * 1000) - print(f"video_stream start() done: {end_time - start_time}ms") msg = [""] diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index 9471912a..0a8cae80 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -636,11 +636,9 @@ class SeedToolsView(View): elif input in [B.KEY_RIGHT, B.KEY_LEFT, B.KEY_UP, B.KEY_DOWN] or keyboard_swap: # Live joystick movement; haven't locked this new letter in yet. - # Replace the last letter w/the currently selected one. Only update - # the active keyboard keys when a selection has been locked in - # (KEY_PRESS) or removed ("del"). - self.passphrase = self.passphrase[:-1] - self.passphrase += ret_val + # Leave current spot blank for now. Only update the active keyboard keys + # when a selection has been locked in (KEY_PRESS) or removed ("del"). + pass # Render the text entry display and cursor block text_entry_display.render(self.passphrase) From 464afcf5455cecec1d01adbc4d326bb5ed481ea1 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 23 Jul 2021 22:35:12 -0500 Subject: [PATCH 34/39] Image seed fixes; reduced auto-exposure time --- src/seedsigner/controller.py | 6 +- src/seedsigner/helpers/camera_process.py | 2 +- src/seedsigner/views/seed_tools_view.py | 79 ++++++++++-------------- 3 files changed, 40 insertions(+), 47 deletions(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index d43c78c0..bc5534d3 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -254,7 +254,11 @@ class Controller: ret_val = True while True: - seed_phrase = self.seed_tools_view.seed_phrase_from_camera_image() + (reshoot, seed_phrase) = self.seed_tools_view.seed_phrase_from_camera_image() + if reshoot: + # Relaunch into another image capture cycle + continue + if len(seed_phrase) > 0: break else: diff --git a/src/seedsigner/helpers/camera_process.py b/src/seedsigner/helpers/camera_process.py index 2eff6dee..dee13332 100644 --- a/src/seedsigner/helpers/camera_process.py +++ b/src/seedsigner/helpers/camera_process.py @@ -86,7 +86,7 @@ class CameraProcess(): print("Received 'click'") # Wait for the automatic gain control to settle - time.sleep(2) + time.sleep(0.25) # Now fix the values camera.shutter_speed = camera.exposure_speed camera.exposure_mode = 'off' diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index 0a8cae80..a2a18ce4 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -1443,23 +1443,17 @@ class SeedToolsView(View): def parse_seed_image_data(self): try: data = self.controller.from_camera_queue.get(block=False) + print(f"got queue data: {data}") except: return - print("parse_seed_image_data got data") - - try: - # self.seed_entropy_image = Image.fromarray(data[0]) - self.seed_entropy_image = data[0] - print(self.seed_entropy_image) - - self.buttons.trigger_override(True) - except Exception as e: - traceback.print_exc() + self.seed_entropy_image = data[0] print("exiting parse_seed_image_data") def seed_phrase_from_camera_image(self): + reshoot = False + self.seed_entropy_image = None self.controller.menu_view.draw_modal(["Initializing Camera..."]) # TODO: Move to Controller # initialize camera @@ -1476,27 +1470,26 @@ class SeedToolsView(View): self.controller.menu_view.draw_modal(["Aim camera", "click joystick"], title="Capture Image as Seed", bottom="Left to Cancel") # TODO: Move to Controller - try: - input = self.buttons.wait_for([B.KEY_LEFT, B.KEY_PRESS]) - if input == B.KEY_LEFT: - print("KEY_LEFT") - # Returning empty will kick us back to SEED_TOOLS_SUB_MENU - self.words = [] - elif input == B.KEY_PRESS: + input = self.buttons.wait_for([B.KEY_LEFT, B.KEY_PRESS]) + if input == B.KEY_LEFT: + self.words = [] + self.controller.to_camera_queue.put(["stop"]) + + elif input == B.KEY_PRESS: + try: print("KEY_PRESS") - self.controller.menu_view.draw_modal(["Auto-adjusting", "exposure..."], title="Capture Image as Seed", bottom="Left to Cancel") # TODO: Move to Controller + self.controller.menu_view.draw_modal(["Auto-adjusting", "exposure..."], title="Capture Image as Seed") # TODO: Move to Controller self.controller.to_camera_queue.put(["click"]) self.camera_loop_timer = CameraPoll(0.05, self.parse_seed_image_data) - input = self.buttons.wait_for([B.KEY_LEFT]) - self.controller.to_camera_queue.put(["stop"]) - self.camera_loop_timer.stop() - - if input == B.KEY_LEFT: - print("KEY_LEFT") - # Returning empty will kick us back to SEED_TOOLS_SUB_MENU - self.words = [] - return self.words + # Wait for the camera process to complete + while self.seed_entropy_image is None: + time.sleep(0.2) + except Exception as e: + traceback.print_exc() + finally: + self.controller.to_camera_queue.put(["stop"]) + self.camera_loop_timer.stop() # Prep a copy of the image for display. The actual image data is 720x480 # Present just a center crop to fit the screen and to keep some of the @@ -1522,27 +1515,23 @@ class SeedToolsView(View): input = self.buttons.wait_for([B.KEY_LEFT, B.KEY_RIGHT]) if input == B.KEY_LEFT: - return self.seed_phrase_from_camera_image() + reshoot = True - hash = hashlib.sha256(self.seed_entropy_image.tobytes()) - badseedphrase_str = mnemonic_from_bytes(hash.digest()) - badseedphrase_list = badseedphrase_str.split() - badseedphrase_list.pop(-1) - calclastwordphrasestr = " ".join(badseedphrase_list) + " abandon" - goodphrasebytes = mnemonic_to_bytes(calclastwordphrasestr, ignore_checksum=True) - goodseedphrasestr = mnemonic_from_bytes(goodphrasebytes) - self.words = goodseedphrasestr.split() + else: + # TODO: Pull this out into its own method so we can write tests against it + hash = hashlib.sha256(self.seed_entropy_image.tobytes()) + badseedphrase_str = mnemonic_from_bytes(hash.digest()) + badseedphrase_list = badseedphrase_str.split() + badseedphrase_list.pop(-1) + calclastwordphrasestr = " ".join(badseedphrase_list) + " abandon" + goodphrasebytes = mnemonic_to_bytes(calclastwordphrasestr, ignore_checksum=True) + goodseedphrasestr = mnemonic_from_bytes(goodphrasebytes) + self.words = goodseedphrasestr.split() + + # self.buttons.trigger_override(True) + return (reshoot, self.words) - except Exception as e: - traceback.print_exc() - finally: - print(f"Stopping QR code scanner") - self.controller.to_camera_queue.put(["stop"]) - self.camera_loop_timer.stop() - time.sleep(0.5) # give time for camera loop to complete before returning data - self.buttons.trigger_override(True) - return self.words[:] ### ### Utility Methods ### From d81fe9ebe7a9834e3414264a6c8edcc7c40f31bb Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 23 Jul 2021 22:43:04 -0500 Subject: [PATCH 35/39] search and replace bugfix --- src/seedsigner/helpers/camera_process.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/seedsigner/helpers/camera_process.py b/src/seedsigner/helpers/camera_process.py index dee13332..932162d2 100644 --- a/src/seedsigner/helpers/camera_process.py +++ b/src/seedsigner/helpers/camera_process.py @@ -147,20 +147,20 @@ class CameraPoll(object): self.function = function self.args = args self.kwargs = kwargs - self.is_is_running = False + self.is_running = False self.start() def _run(self): - self.is_is_running = False + self.is_running = False self.start() self.function(*self.args, **self.kwargs) def start(self): - if not self.is_is_running: + if not self.is_running: self._timer = Timer(self.interval, self._run) self._timer.start() - self.is_is_running = True + self.is_running = True def stop(self): self._timer.cancel() - self.is_is_running = False \ No newline at end of file + self.is_running = False \ No newline at end of file From 758f8e5a470fbde296b80bf53d4bab6831cfcc1f Mon Sep 17 00:00:00 2001 From: newtonick Date: Mon, 26 Jul 2021 14:22:07 -0400 Subject: [PATCH 36/39] bw fix for invalid psbt check --- src/seedsigner/models/blue_wallet.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/seedsigner/models/blue_wallet.py b/src/seedsigner/models/blue_wallet.py index 2f3feea3..dad039b2 100644 --- a/src/seedsigner/models/blue_wallet.py +++ b/src/seedsigner/models/blue_wallet.py @@ -85,6 +85,8 @@ class BlueWallet(Wallet): self.controller.to_camera_queue.put(["stop"]) if self.qr_data[0] == "invalid": return "invalid" + elif self.qr_data[0] == "invalidpsbt": + return "invalid" return b2a_base64(cbor_decode(self.ur_decoder.result.cbor)) def process_camera_data(self): From 0295b1a61f3e3ff7d25d6018dba18e0c0ba2187e Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Tue, 27 Jul 2021 17:29:50 +0100 Subject: [PATCH 37/39] exclude src/seedsigner.egg-info/ from repo --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9f2e8485..e5df4958 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .DS_Store __pycache__/ +src/seedsigner.egg-info/ From 9daaaec242c8e86b1c9581fa68d9715fb24cea74 Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Tue, 27 Jul 2021 17:45:58 +0100 Subject: [PATCH 38/39] disable ur2 decode checksum until ur2 lib is fixed --- src/seedsigner/helpers/ur2/bytewords.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/seedsigner/helpers/ur2/bytewords.py b/src/seedsigner/helpers/ur2/bytewords.py index 8c917898..b7e8432a 100644 --- a/src/seedsigner/helpers/ur2/bytewords.py +++ b/src/seedsigner/helpers/ur2/bytewords.py @@ -107,8 +107,8 @@ def decode(s, separator, word_len): body = buf[0:-4] body_checksum = buf[-4:] checksum = crc32_bytes(body) - if checksum != body_checksum: - raise ValueError('Invalid Bytewords.') + # if checksum != body_checksum: + # raise ValueError('Invalid Bytewords.') return body From ea85d93024c0948a1f33b3c382629bfcd31417fa Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Wed, 28 Jul 2021 21:59:20 +0100 Subject: [PATCH 39/39] fix seed from picture bug where after qr or sign scan it crashed --- src/seedsigner/views/seed_tools_view.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index a2a18ce4..33fba4b4 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -1464,7 +1464,14 @@ class SeedToolsView(View): try: msg = self.controller.from_camera_queue.get(block=False) print(f"msg received: {msg}") - break + + # check 'ready', sometimes queue contains nodata or data that needs to be cleared + is_ready = False + for m in msg: + if m == "ready": + is_ready = True + if is_ready: + break except: time.sleep(0.1)
123456
A