mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 15:08:25 +00:00
Merge pull request #58 from SeedSigner/0_4_3
Merge 0.4.3 changes into main branch
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
.DS_Store
|
||||
__pycache__/
|
||||
src/seedsigner.egg-info/
|
||||
|
||||
@@ -169,3 +169,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)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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 = """
|
||||
<html>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
@@ -9,6 +16,7 @@ def generate_qr_template(qr_size, block_size=5, show_timing_marks=False):
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin-top: 5em;
|
||||
text-align: center;
|
||||
font-family: "Open Sans", sans-serif;
|
||||
-webkit-print-color-adjust: exact;
|
||||
@@ -29,6 +37,11 @@ def generate_qr_template(qr_size, block_size=5, show_timing_marks=False):
|
||||
width: 11px;
|
||||
height: 12px;
|
||||
}
|
||||
.qr_table {
|
||||
margin-top: 5em;
|
||||
margin-right: 4em;
|
||||
float: right;
|
||||
}
|
||||
.filled {
|
||||
background-color: black;
|
||||
border: 1px solid black;
|
||||
@@ -55,19 +68,59 @@ def generate_qr_template(qr_size, block_size=5, show_timing_marks=False):
|
||||
.no_border {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.word_list_table {
|
||||
"""
|
||||
|
||||
if num_words == 12:
|
||||
html += "margin-left: 10em;\n"
|
||||
else:
|
||||
html += "margin-left: 0;\n"
|
||||
|
||||
html += """
|
||||
float: left;
|
||||
}
|
||||
.word_list {
|
||||
text-align: right;
|
||||
padding-top: 2em;
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
}
|
||||
.word_row {
|
||||
height: 2.5em;
|
||||
}
|
||||
.word_num {
|
||||
width: 1em;
|
||||
text-align: right;
|
||||
color: #999;
|
||||
}
|
||||
.word_blank {
|
||||
color: #ccc;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
"""
|
||||
if qr_size == 21:
|
||||
html += f"""<div class="title">{qr_size}x{qr_size} QR Code</div>"""
|
||||
elif qr_size == 25:
|
||||
html += f"""<div class="title">12-word Seed</div>"""
|
||||
else:
|
||||
html += f"""<div class="title">24-word Seed</div>"""
|
||||
|
||||
html += """
|
||||
<table align="center" class="word_list_table">
|
||||
<tr>
|
||||
<td class="word_list">
|
||||
"""
|
||||
for i in range(1, num_words + 1):
|
||||
if i == 13:
|
||||
html += """
|
||||
</td>
|
||||
<td class="word_list">
|
||||
"""
|
||||
html += f"""<div class="word_row">
|
||||
<span class="word_num">{i}: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
"""
|
||||
html += "</td></tr></table>"
|
||||
|
||||
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 += """<table align="center">"""
|
||||
html += """<table align="center" class="qr_table">"""
|
||||
html += f"""<tr rowspan="{block_size}"><td class="col_block_divider row_block_divider"></td>"""
|
||||
for j in range(0, math.ceil(qr_size/block_size)):
|
||||
html += f"""<td colspan="{block_size}" class="col_name col_block_divider">{j + 1}</td>"""
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin-top: 5em;
|
||||
text-align: center;
|
||||
font-family: "Open Sans", sans-serif;
|
||||
-webkit-print-color-adjust: exact;
|
||||
@@ -26,6 +27,11 @@
|
||||
width: 11px;
|
||||
height: 12px;
|
||||
}
|
||||
.qr_table {
|
||||
margin-top: 5em;
|
||||
margin-right: 4em;
|
||||
float: right;
|
||||
}
|
||||
.filled {
|
||||
background-color: black;
|
||||
border: 1px solid black;
|
||||
@@ -52,9 +58,72 @@
|
||||
.no_border {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.word_list_table {
|
||||
margin-left: 10em;
|
||||
|
||||
float: left;
|
||||
}
|
||||
.word_list {
|
||||
text-align: right;
|
||||
padding-top: 2em;
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
}
|
||||
.word_row {
|
||||
height: 2.5em;
|
||||
}
|
||||
.word_num {
|
||||
width: 1em;
|
||||
text-align: right;
|
||||
color: #999;
|
||||
}
|
||||
.word_blank {
|
||||
color: #ccc;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<div class="title">12-word Seed</div><table align="center"><tr rowspan="5"><td class="col_block_divider row_block_divider"></td><td colspan="5" class="col_name col_block_divider">1</td><td colspan="5" class="col_name col_block_divider">2</td><td colspan="5" class="col_name col_block_divider">3</td><td colspan="5" class="col_name col_block_divider">4</td><td colspan="5" class="col_name col_block_divider">5</td></tr><tr>
|
||||
|
||||
<table align="center" class="word_list_table">
|
||||
<tr>
|
||||
<td class="word_list">
|
||||
<div class="word_row">
|
||||
<span class="word_num">1: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
<div class="word_row">
|
||||
<span class="word_num">2: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
<div class="word_row">
|
||||
<span class="word_num">3: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
<div class="word_row">
|
||||
<span class="word_num">4: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
<div class="word_row">
|
||||
<span class="word_num">5: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
<div class="word_row">
|
||||
<span class="word_num">6: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
<div class="word_row">
|
||||
<span class="word_num">7: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
<div class="word_row">
|
||||
<span class="word_num">8: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
<div class="word_row">
|
||||
<span class="word_num">9: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
<div class="word_row">
|
||||
<span class="word_num">10: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
<div class="word_row">
|
||||
<span class="word_num">11: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
<div class="word_row">
|
||||
<span class="word_num">12: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
</td></tr></table><table align="center" class="qr_table"><tr rowspan="5"><td class="col_block_divider row_block_divider"></td><td colspan="5" class="col_name col_block_divider">1</td><td colspan="5" class="col_name col_block_divider">2</td><td colspan="5" class="col_name col_block_divider">3</td><td colspan="5" class="col_name col_block_divider">4</td><td colspan="5" class="col_name col_block_divider">5</td></tr><tr>
|
||||
<td rowspan="5" class="row_name row_block_divider">A</td><td id="0_0"></td><td id="0_1"></td><td id="0_2"></td><td id="0_3"></td><td id="0_4"></td><td id="0_5"></td><td id="0_6"></td><td id="0_7"></td><td id="0_8"></td><td id="0_9"></td><td id="0_10"></td><td id="0_11"></td><td id="0_12"></td><td id="0_13"></td><td id="0_14"></td><td id="0_15"></td><td id="0_16"></td><td id="0_17"></td><td id="0_18"></td><td id="0_19"></td><td id="0_20"></td><td id="0_21"></td><td id="0_22"></td><td id="0_23"></td><td id="0_24"></td></tr>
|
||||
<tr>
|
||||
<td id="1_0"></td><td id="1_1"></td><td id="1_2"></td><td id="1_3"></td><td id="1_4"></td><td id="1_5"></td><td id="1_6"></td><td id="1_7"></td><td id="1_8"></td><td id="1_9"></td><td id="1_10"></td><td id="1_11"></td><td id="1_12"></td><td id="1_13"></td><td id="1_14"></td><td id="1_15"></td><td id="1_16"></td><td id="1_17"></td><td id="1_18"></td><td id="1_19"></td><td id="1_20"></td><td id="1_21"></td><td id="1_22"></td><td id="1_23"></td><td id="1_24"></td></tr>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin-top: 5em;
|
||||
text-align: center;
|
||||
font-family: "Open Sans", sans-serif;
|
||||
-webkit-print-color-adjust: exact;
|
||||
@@ -26,6 +27,11 @@
|
||||
width: 11px;
|
||||
height: 12px;
|
||||
}
|
||||
.qr_table {
|
||||
margin-top: 5em;
|
||||
margin-right: 4em;
|
||||
float: right;
|
||||
}
|
||||
.filled {
|
||||
background-color: black;
|
||||
border: 1px solid black;
|
||||
@@ -52,9 +58,111 @@
|
||||
.no_border {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.word_list_table {
|
||||
margin-left: 0;
|
||||
|
||||
float: left;
|
||||
}
|
||||
.word_list {
|
||||
text-align: right;
|
||||
padding-top: 2em;
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
}
|
||||
.word_row {
|
||||
height: 2.5em;
|
||||
}
|
||||
.word_num {
|
||||
width: 1em;
|
||||
text-align: right;
|
||||
color: #999;
|
||||
}
|
||||
.word_blank {
|
||||
color: #ccc;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<div class="title">24-word Seed</div><table align="center"><tr rowspan="5"><td class="col_block_divider row_block_divider"></td><td colspan="5" class="col_name col_block_divider">1</td><td colspan="5" class="col_name col_block_divider">2</td><td colspan="5" class="col_name col_block_divider">3</td><td colspan="5" class="col_name col_block_divider">4</td><td colspan="5" class="col_name col_block_divider">5</td><td colspan="5" class="col_name col_block_divider">6</td></tr><tr>
|
||||
|
||||
<table align="center" class="word_list_table">
|
||||
<tr>
|
||||
<td class="word_list">
|
||||
<div class="word_row">
|
||||
<span class="word_num">1: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
<div class="word_row">
|
||||
<span class="word_num">2: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
<div class="word_row">
|
||||
<span class="word_num">3: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
<div class="word_row">
|
||||
<span class="word_num">4: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
<div class="word_row">
|
||||
<span class="word_num">5: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
<div class="word_row">
|
||||
<span class="word_num">6: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
<div class="word_row">
|
||||
<span class="word_num">7: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
<div class="word_row">
|
||||
<span class="word_num">8: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
<div class="word_row">
|
||||
<span class="word_num">9: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
<div class="word_row">
|
||||
<span class="word_num">10: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
<div class="word_row">
|
||||
<span class="word_num">11: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
<div class="word_row">
|
||||
<span class="word_num">12: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
|
||||
</td>
|
||||
<td class="word_list">
|
||||
<div class="word_row">
|
||||
<span class="word_num">13: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
<div class="word_row">
|
||||
<span class="word_num">14: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
<div class="word_row">
|
||||
<span class="word_num">15: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
<div class="word_row">
|
||||
<span class="word_num">16: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
<div class="word_row">
|
||||
<span class="word_num">17: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
<div class="word_row">
|
||||
<span class="word_num">18: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
<div class="word_row">
|
||||
<span class="word_num">19: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
<div class="word_row">
|
||||
<span class="word_num">20: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
<div class="word_row">
|
||||
<span class="word_num">21: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
<div class="word_row">
|
||||
<span class="word_num">22: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
<div class="word_row">
|
||||
<span class="word_num">23: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
<div class="word_row">
|
||||
<span class="word_num">24: </span><span class="word_blank">____________________________</span>
|
||||
</div>
|
||||
</td></tr></table><table align="center" class="qr_table"><tr rowspan="5"><td class="col_block_divider row_block_divider"></td><td colspan="5" class="col_name col_block_divider">1</td><td colspan="5" class="col_name col_block_divider">2</td><td colspan="5" class="col_name col_block_divider">3</td><td colspan="5" class="col_name col_block_divider">4</td><td colspan="5" class="col_name col_block_divider">5</td><td colspan="5" class="col_name col_block_divider">6</td></tr><tr>
|
||||
<td rowspan="5" class="row_name row_block_divider">A</td><td id="0_0"></td><td id="0_1"></td><td id="0_2"></td><td id="0_3"></td><td id="0_4"></td><td id="0_5"></td><td id="0_6"></td><td id="0_7"></td><td id="0_8"></td><td id="0_9"></td><td id="0_10"></td><td id="0_11"></td><td id="0_12"></td><td id="0_13"></td><td id="0_14"></td><td id="0_15"></td><td id="0_16"></td><td id="0_17"></td><td id="0_18"></td><td id="0_19"></td><td id="0_20"></td><td id="0_21"></td><td id="0_22"></td><td id="0_23"></td><td id="0_24"></td><td id="0_25"></td><td id="0_26"></td><td id="0_27"></td><td id="0_28"></td></tr>
|
||||
<tr>
|
||||
<td id="1_0"></td><td id="1_1"></td><td id="1_2"></td><td id="1_3"></td><td id="1_4"></td><td id="1_5"></td><td id="1_6"></td><td id="1_7"></td><td id="1_8"></td><td id="1_9"></td><td id="1_10"></td><td id="1_11"></td><td id="1_12"></td><td id="1_13"></td><td id="1_14"></td><td id="1_15"></td><td id="1_16"></td><td id="1_17"></td><td id="1_18"></td><td id="1_19"></td><td id="1_20"></td><td id="1_21"></td><td id="1_22"></td><td id="1_23"></td><td id="1_24"></td><td id="1_25"></td><td id="1_26"></td><td id="1_27"></td><td id="1_28"></td></tr>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
[pytest]
|
||||
testpaths = tests
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
embit==0.4.3
|
||||
imutils==0.5.4
|
||||
picamera==1.13
|
||||
Pillow==8.2.0
|
||||
pyzbar==0.1.8
|
||||
qrcode==6.1
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
+5
-1
@@ -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()
|
||||
|
||||
+108
-30
@@ -11,37 +11,78 @@ from .models import (SeedStorage, SpecterDesktopWallet, BlueWallet,
|
||||
SparrowWallet, GenericUR2Wallet, Wallet)
|
||||
|
||||
|
||||
class Controller:
|
||||
|
||||
VERSION = "0.4.2"
|
||||
|
||||
def __init__(self, config) -> None:
|
||||
controller = self
|
||||
|
||||
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()
|
||||
```
|
||||
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.3"
|
||||
|
||||
_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):
|
||||
# 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")
|
||||
|
||||
|
||||
@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")
|
||||
self.color = config["display"]["TEXT_COLOR"]
|
||||
controller.DEBUG = config.getboolean("system", "DEBUG")
|
||||
controller.color = config["display"]["TEXT_COLOR"]
|
||||
|
||||
# Input Buttons
|
||||
self.buttons = Buttons()
|
||||
controller.buttons = Buttons()
|
||||
|
||||
# models
|
||||
self.storage = SeedStorage()
|
||||
self.wallet_klass = globals()["SpecterDesktopWallet"]
|
||||
self.wallet = self.wallet_klass()
|
||||
controller.storage = SeedStorage()
|
||||
controller.wallet_klass = globals()["SpecterDesktopWallet"]
|
||||
controller.wallet = controller.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)
|
||||
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()
|
||||
|
||||
|
||||
@@ -88,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:
|
||||
@@ -141,9 +184,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
|
||||
|
||||
@@ -206,6 +249,41 @@ class Controller:
|
||||
|
||||
return Path.MAIN_MENU
|
||||
|
||||
def show_create_seed_with_image_tool(self) -> int:
|
||||
seed_phrase = []
|
||||
ret_val = True
|
||||
|
||||
while True:
|
||||
(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:
|
||||
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:
|
||||
# Start over
|
||||
return self.show_create_seed_with_image_tool()
|
||||
|
||||
# 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):
|
||||
@@ -234,9 +312,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:
|
||||
@@ -321,7 +399,7 @@ 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(self.storage.get_passphrase(slot_num))
|
||||
passphrase = self.seed_tools_view.draw_passphrase_keyboard_entry(existing_passphrase=self.storage.get_passphrase(slot_num))
|
||||
if len(passphrase) == 0:
|
||||
return Path.SEED_TOOLS_SUB_MENU
|
||||
|
||||
@@ -359,9 +437,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:
|
||||
seed_phrase = self.seed_tools_view.read_seed_phrase_qr()
|
||||
else:
|
||||
@@ -381,7 +459,7 @@ class Controller:
|
||||
r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Optional Passphrase?")
|
||||
if r == 1:
|
||||
# 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.draw_passphrase_keyboard_entry()
|
||||
if len(passphrase) == 0 or passphrase == "-1":
|
||||
passphrase = ""
|
||||
self.menu_view.draw_modal(["No passphrase added", "to seed words"], "", "Left to Exit, Right to Continue")
|
||||
@@ -430,9 +508,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 Main Menu ]")
|
||||
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:
|
||||
@@ -452,7 +530,7 @@ class Controller:
|
||||
r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Optional Passphrase?")
|
||||
if r == 1:
|
||||
# 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.draw_passphrase_keyboard_entry()
|
||||
if len(passphrase) == 0 or passphrase == "-1":
|
||||
passphrase = ""
|
||||
self.menu_view.draw_modal(["No passphrase added", "to seed words"], "", "Left to Exit, Right to Continue")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .buttons import *
|
||||
from .camera_process import *
|
||||
from .keyboard import Keyboard, TextEntryDisplay
|
||||
from .path import *
|
||||
from .qr import *
|
||||
from .ST7789 import *
|
||||
@@ -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,40 +31,83 @@ 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 = 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
|
||||
|
||||
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
|
||||
self.override_ind = False
|
||||
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, force_release = False) -> bool:
|
||||
if force_release == True:
|
||||
if force_release:
|
||||
B.release_lock = True
|
||||
|
||||
if self.override_ind == False:
|
||||
if not self.override_ind:
|
||||
self.override_ind = True
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def force_release(self) -> bool:
|
||||
@@ -71,14 +115,14 @@ class Buttons:
|
||||
return True
|
||||
|
||||
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
|
||||
|
||||
@@ -1,58 +1,144 @@
|
||||
# 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):
|
||||
class CameraProcess():
|
||||
|
||||
print("CameraProcess start")
|
||||
|
||||
from imutils.video import VideoStream
|
||||
def scan_qr_code(in_queue, out_queue):
|
||||
from pyzbar import pyzbar
|
||||
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("CameraProcess finish import")
|
||||
video_stream.start()
|
||||
|
||||
running = True
|
||||
msg = [""]
|
||||
|
||||
while running == True:
|
||||
while True:
|
||||
# Loop the reader until we get a result or receive "stop"
|
||||
frame = video_stream.read()
|
||||
|
||||
camera_on = False
|
||||
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
|
||||
import picamera
|
||||
|
||||
print("Instantiate PiCamera start")
|
||||
start_time = int(time.time() * 1000)
|
||||
camera = picamera.PiCamera(resolution=(720, 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'")
|
||||
|
||||
# Wait for the automatic gain control to settle
|
||||
time.sleep(0.25)
|
||||
# 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([Image.open(stream)])
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
CameraProcess.scan_qr_code(in_queue, out_queue)
|
||||
|
||||
msg[0] = ""
|
||||
camera_on = True
|
||||
|
||||
while camera_on:
|
||||
frame = vs.read()
|
||||
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()
|
||||
camera_on = False
|
||||
elif msg[0] == "single_frame":
|
||||
CameraProcess.capture_single_frame(in_queue, out_queue)
|
||||
|
||||
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):
|
||||
|
||||
@@ -0,0 +1,430 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
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"
|
||||
|
||||
REGULAR_KEY_FONT = "regular"
|
||||
COMPACT_KEY_FONT = "compact"
|
||||
|
||||
KEY_BACKSPACE = {
|
||||
"letter": "del",
|
||||
"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
|
||||
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],
|
||||
render_now=True):
|
||||
"""
|
||||
`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
|
||||
|
||||
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
|
||||
self.auto_wrap = auto_wrap
|
||||
self.background_color = "black"
|
||||
self.highlight_color = View.color
|
||||
|
||||
# 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}")
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
if render_now:
|
||||
# Render the keys
|
||||
self.render_keys()
|
||||
|
||||
# Render the initial highlighted character
|
||||
self.update_from_input(input=None)
|
||||
|
||||
|
||||
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).
|
||||
|
||||
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.
|
||||
|
||||
`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.
|
||||
"""
|
||||
key = self.keys[self.selected_key["y"]][self.selected_key["x"]]
|
||||
|
||||
# Before we update, undo our previously self.selected_key key
|
||||
key.is_selected = False
|
||||
key.render_key()
|
||||
|
||||
if input == B.KEY_RIGHT:
|
||||
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_key["x"] = 0
|
||||
else:
|
||||
# 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:
|
||||
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_key["x"] = len(self.keys[self.selected_key["y"]]) - 1
|
||||
else:
|
||||
# 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:
|
||||
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_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 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:
|
||||
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
|
||||
if self.selected_key["y"] < 0:
|
||||
if Keyboard.WRAP_TOP in self.auto_wrap:
|
||||
# Loop it back to the bottom
|
||||
self.selected_key["y"] = len(self.keys) - 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 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:
|
||||
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
|
||||
# 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:
|
||||
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"]]
|
||||
key.is_selected = True
|
||||
key.render_key()
|
||||
|
||||
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""")
|
||||
|
||||
|
||||
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:
|
||||
draw: any
|
||||
rect: (int,int,int,int)
|
||||
font: any
|
||||
font_color: any
|
||||
is_centered: bool = True
|
||||
has_outline: bool = False
|
||||
cur_text: str = " "
|
||||
|
||||
|
||||
def render(self, cur_text=None):
|
||||
""" Render the live text entry display """
|
||||
if cur_text:
|
||||
self.cur_text = cur_text
|
||||
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
|
||||
|
||||
# Draw n-1 of the selected letters
|
||||
tw, th = self.font.getsize(self.cur_text[:-1])
|
||||
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] + 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
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# import the necessary packages
|
||||
from picamera.array import PiRGBArray
|
||||
from picamera import PiCamera
|
||||
from threading import Thread
|
||||
import time
|
||||
|
||||
|
||||
# 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:
|
||||
print("PiVideoStream: closing everything")
|
||||
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
|
||||
|
||||
# while True:
|
||||
# # Wait for camera to close before letting the thread get destroyed
|
||||
# if not self.camera.closed:
|
||||
# time.sleep(0.1)
|
||||
# else:
|
||||
# break
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,92 @@ 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"
|
||||
elif self.qr_data[0] == "invalidpsbt":
|
||||
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 +167,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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -9,8 +9,8 @@ from seedsigner.helpers import B, CameraPoll
|
||||
|
||||
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
|
||||
|
||||
@@ -7,10 +7,11 @@ from seedsigner.models import SeedStorage
|
||||
import time
|
||||
import re
|
||||
|
||||
|
||||
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
|
||||
@@ -38,7 +39,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:
|
||||
@@ -70,13 +71,13 @@ 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
|
||||
|
||||
# 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:
|
||||
@@ -94,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
|
||||
|
||||
@@ -104,7 +107,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:
|
||||
@@ -135,7 +138,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:
|
||||
@@ -166,7 +169,7 @@ class MenuView(View):
|
||||
self.draw_menu(lines, 1, title, bottom, True)
|
||||
|
||||
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:
|
||||
@@ -182,7 +185,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:
|
||||
@@ -255,7 +258,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:
|
||||
@@ -272,7 +275,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, force_redraw = False) -> None:
|
||||
if title == None:
|
||||
t = "SeedSigner v" + self.controller.VERSION
|
||||
|
||||
@@ -2,12 +2,16 @@
|
||||
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
|
||||
import traceback
|
||||
|
||||
# Internal file class dependencies
|
||||
from . import View
|
||||
from seedsigner.helpers import B, QR, CameraPoll
|
||||
from seedsigner.helpers import B, QR, CameraPoll, Keyboard, TextEntryDisplay
|
||||
|
||||
|
||||
|
||||
class SeedToolsView(View):
|
||||
@@ -16,16 +20,18 @@ 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 = []
|
||||
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
|
||||
self.seed_entropy_image = None
|
||||
|
||||
# Dice information
|
||||
self.roll_number = 1
|
||||
@@ -80,6 +86,566 @@ class SeedToolsView(View):
|
||||
|
||||
self.draw_gather_words()
|
||||
|
||||
|
||||
def display_manual_seed_entry(self, num_words):
|
||||
self.seed_length = num_words
|
||||
self.reset()
|
||||
|
||||
cur_word = 1
|
||||
while len(self.words) < self.seed_length:
|
||||
initial_letters = ["a"]
|
||||
if len(self.words) >= cur_word:
|
||||
initial_letters = list(self.words[cur_word - 1]) # zero-indexed
|
||||
|
||||
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:
|
||||
# Exit this UI
|
||||
return []
|
||||
else:
|
||||
# We've backed `cur_word` up, so re-enter loop
|
||||
continue
|
||||
|
||||
if len(self.words) < cur_word:
|
||||
self.words.append(ret_val.strip())
|
||||
else:
|
||||
self.words[cur_word - 1] = ret_val.strip()
|
||||
cur_word += 1
|
||||
|
||||
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)
|
||||
"""
|
||||
# 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
|
||||
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=font_color, font=word_font)
|
||||
|
||||
render_possible_matches_arrows()
|
||||
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
# 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
|
||||
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
|
||||
text_entry_display_height = 39
|
||||
|
||||
# TODO: support other BIP39 languages/charsets
|
||||
keyboard = Keyboard(View.draw,
|
||||
charset=self.possible_alphabet,
|
||||
rows=5,
|
||||
cols=6,
|
||||
rect=(0,text_entry_display_height + 1, keyboard_width,240),
|
||||
auto_wrap=[Keyboard.WRAP_LEFT, Keyboard.WRAP_RIGHT])
|
||||
|
||||
# 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=f"{num_word}: " + "".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
|
||||
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()
|
||||
|
||||
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.KEY2]
|
||||
)
|
||||
|
||||
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
|
||||
self.render_previous_button()
|
||||
|
||||
elif input == B.KEY_DOWN:
|
||||
input = Keyboard.ENTER_TOP
|
||||
# Re-render it without the highlight
|
||||
previous_button_is_active = False
|
||||
self.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:
|
||||
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.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()
|
||||
|
||||
# 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 (" ")
|
||||
# in the live text entry display at the top.
|
||||
self.letters = self.letters[:-1]
|
||||
self.letters.append(" ")
|
||||
|
||||
# Has the user made a final selection of a candidate word?
|
||||
final_selection = None
|
||||
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:
|
||||
self.selected_possible_words_index = 0
|
||||
|
||||
if not arrow_up_is_active:
|
||||
# Flash the up arrow as selected
|
||||
arrow_up_is_active = True
|
||||
|
||||
# Update the right-hand possible matches area
|
||||
render_possible_matches()
|
||||
|
||||
elif input == B.KEY2:
|
||||
if self.possible_words:
|
||||
final_selection = self.possible_words[self.selected_possible_words_index]
|
||||
|
||||
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):
|
||||
self.selected_possible_words_index = len(self.possible_words) - 1
|
||||
|
||||
if not arrow_down_is_active:
|
||||
# Flash the down arrow as selected
|
||||
arrow_down_is_active = True
|
||||
|
||||
# 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
|
||||
self.letters = list(final_selection + " ")
|
||||
render_possible_matches(highlight_word=final_selection)
|
||||
text_entry_display.render(f"{num_word}: " + "".join(self.letters))
|
||||
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)
|
||||
|
||||
# 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]
|
||||
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()
|
||||
|
||||
# Render the text entry display and cursor block
|
||||
text_entry_display.render(f"{num_word}: " + "".join(self.letters))
|
||||
|
||||
View.DispShowImage()
|
||||
|
||||
|
||||
def draw_passphrase_keyboard_entry(self, existing_passphrase = ""):
|
||||
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")
|
||||
|
||||
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
|
||||
if existing_passphrase:
|
||||
self.passphrase = existing_passphrase
|
||||
else:
|
||||
self.passphrase = " "
|
||||
|
||||
# Set up the keyboard params
|
||||
right_panel_buttons_width = 60
|
||||
|
||||
# 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 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=(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
|
||||
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]
|
||||
)
|
||||
|
||||
keyboard_swap = False
|
||||
|
||||
# Check our two possible exit conditions
|
||||
if input == B.KEY3:
|
||||
# Save!
|
||||
if self.passphrase != "" and 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.
|
||||
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"])
|
||||
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:
|
||||
# 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
|
||||
|
||||
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] or keyboard_swap:
|
||||
# Live joystick movement; haven't locked this new letter in yet.
|
||||
# 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)
|
||||
|
||||
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()
|
||||
@@ -193,10 +759,10 @@ class SeedToolsView(View):
|
||||
|
||||
return
|
||||
|
||||
|
||||
###
|
||||
### Display Gather PassPhrase Screen
|
||||
###
|
||||
|
||||
def display_gather_passphrase_screen(self, existing_passphrase = "") -> str:
|
||||
self.reset()
|
||||
self.pass_letter = "a"
|
||||
@@ -828,7 +1394,6 @@ class SeedToolsView(View):
|
||||
cur_y = next_y
|
||||
|
||||
|
||||
|
||||
def parse_seed_qr_data(self):
|
||||
try:
|
||||
data = self.controller.from_camera_queue.get(False)
|
||||
@@ -874,10 +1439,109 @@ class SeedToolsView(View):
|
||||
self.buttons.trigger_override(True)
|
||||
return self.words[:]
|
||||
|
||||
|
||||
def parse_seed_image_data(self):
|
||||
try:
|
||||
data = self.controller.from_camera_queue.get(block=False)
|
||||
print(f"got queue data: {data}")
|
||||
except:
|
||||
return
|
||||
|
||||
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
|
||||
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}")
|
||||
|
||||
# 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)
|
||||
|
||||
self.controller.menu_view.draw_modal(["Aim camera", "click joystick"], title="Capture Image as Seed", bottom="Left to Cancel") # TODO: Move to Controller
|
||||
|
||||
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") # TODO: Move to Controller
|
||||
self.controller.to_camera_queue.put(["click"])
|
||||
self.camera_loop_timer = CameraPoll(0.05, self.parse_seed_image_data)
|
||||
|
||||
# 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
|
||||
# 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(
|
||||
display_version,
|
||||
text=" < reshoot | accept > ",
|
||||
font=View.ROBOTOCONDENSED_REGULAR_22,
|
||||
text_color=View.color,
|
||||
text_background=(0,0,0,225)
|
||||
)
|
||||
|
||||
input = self.buttons.wait_for([B.KEY_LEFT, B.KEY_RIGHT])
|
||||
if input == B.KEY_LEFT:
|
||||
reshoot = True
|
||||
|
||||
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)
|
||||
|
||||
|
||||
###
|
||||
### Utility Methods
|
||||
###
|
||||
|
||||
def reset(self):
|
||||
self.words.clear()
|
||||
self.possible_alphabet = SeedToolsView.ALPHABET[:]
|
||||
@@ -888,18 +1552,25 @@ 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))]
|
||||
self.calc_possible_words()
|
||||
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 = []
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -3,10 +3,11 @@ from . import View
|
||||
from seedsigner.helpers import B, QR
|
||||
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -8,8 +8,8 @@ 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
|
||||
|
||||
###
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
# External Dependencies
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import os
|
||||
import pathlib
|
||||
import spidev as SPI
|
||||
import time
|
||||
from multiprocessing import Queue
|
||||
from seedsigner.helpers import B, ST7789
|
||||
|
||||
from seedsigner.helpers import ST7789
|
||||
|
||||
### Generic View Class to Instatiate Display
|
||||
### Static Class variables are used for display
|
||||
@@ -31,11 +34,29 @@ class View:
|
||||
COURIERNEW30 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/courbd.ttf', 30)
|
||||
COURIERNEW20 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/courbd.ttf', 20)
|
||||
|
||||
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_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_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)
|
||||
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)
|
||||
ROBOTOCONDENSED_REGULAR_28 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Regular.ttf"), 28)
|
||||
|
||||
RST = 27
|
||||
DC = 25
|
||||
BL = 24
|
||||
|
||||
controller = None
|
||||
buttons = None
|
||||
canvas_width = 0
|
||||
canvas_height = 0
|
||||
@@ -44,13 +65,15 @@ class View:
|
||||
bus = 0
|
||||
device = 0
|
||||
disp = None
|
||||
previous_button_width = None
|
||||
|
||||
def __init__(self, controller) -> None:
|
||||
def __init__(self) -> None:
|
||||
# Import here to avoid circular imports
|
||||
from seedsigner.controller import Controller
|
||||
self.controller = Controller.get_instance()
|
||||
|
||||
# Global Singleton
|
||||
View.controller = controller
|
||||
View.buttons = View.controller.buttons
|
||||
View.color = View.controller.color
|
||||
View.buttons = self.controller.buttons
|
||||
View.color = self.controller.color
|
||||
|
||||
View.canvas_width = View.WIDTH
|
||||
View.canvas_height = View.HEIGHT
|
||||
@@ -85,11 +108,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
|
||||
|
||||
@@ -103,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:
|
||||
@@ -208,6 +236,7 @@ class View:
|
||||
View.DispShowImage()
|
||||
|
||||
return
|
||||
|
||||
|
||||
###
|
||||
### Power Off Screen
|
||||
@@ -236,3 +265,28 @@ 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
|
||||
top_padding = -3
|
||||
bottom_padding = 3
|
||||
side_padding = 3
|
||||
tw, th = word_font.getsize(arrow)
|
||||
self.previous_button_width = tw + 2 * side_padding
|
||||
if highlight:
|
||||
font_color = "black"
|
||||
background_color = View.color
|
||||
else:
|
||||
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)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -0,0 +1,2 @@
|
||||
pytest==6.2.4
|
||||
mock==4.0.3
|
||||
@@ -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"
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user