mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 15:08:25 +00:00
Merge pull request #741 from kdmukai/ili9341_320x240_v2
[New Feature] Support for 320x240 displays (st7789, ili9341)
This commit is contained in:
@@ -1986,3 +1986,47 @@ def reflow_text_into_pages(text: str,
|
||||
pages.append("\n".join(lines[i:i+lines_per_page]))
|
||||
|
||||
return pages
|
||||
|
||||
|
||||
def resize_image_to_fill(img: Image, target_size_x: int, target_size_y: int, sampling_method=Image.Resampling.NEAREST) -> Image:
|
||||
"""
|
||||
Resizes the image to fill the target size, cropping the image if necessary.
|
||||
"""
|
||||
if img.width == target_size_x and img.height == target_size_y:
|
||||
# No need to resize
|
||||
return img
|
||||
|
||||
# if the image aspect ratio doesn't match the render area, we
|
||||
# need to provide an aspect ratio-aware crop box.
|
||||
render_aspect_ratio = target_size_x / target_size_y
|
||||
source_frame_aspect_ratio = img.width / img.height
|
||||
if render_aspect_ratio > source_frame_aspect_ratio:
|
||||
# Render surface is wider than the source frame; preserve
|
||||
# the width but crop the height
|
||||
cropped_height = (img.width * target_size_y / target_size_x)
|
||||
box = (
|
||||
0,
|
||||
int((img.height - cropped_height)/2),
|
||||
img.width,
|
||||
img.height - int((img.height - cropped_height)/2),
|
||||
)
|
||||
|
||||
elif render_aspect_ratio < source_frame_aspect_ratio:
|
||||
# Render surface is taller than the source frame; preserve
|
||||
# the height but crop the width
|
||||
box = (
|
||||
int((img.width - img.height * target_size_x / target_size_y) / 2),
|
||||
0,
|
||||
int(img.width - (img.width - img.height * target_size_x / target_size_y) / 2),
|
||||
img.height,
|
||||
)
|
||||
|
||||
else:
|
||||
# Render surface and source frame are the same aspect ratio
|
||||
box = None
|
||||
|
||||
return img.resize(
|
||||
(target_size_x, target_size_y),
|
||||
resample=sampling_method,
|
||||
box=box,
|
||||
)
|
||||
@@ -1,7 +1,11 @@
|
||||
from PIL import Image, ImageDraw
|
||||
from threading import Lock
|
||||
|
||||
from seedsigner.hardware.ST7789 import ST7789
|
||||
# from seedsigner.hardware.st7789_mpy import ST7789
|
||||
from seedsigner.hardware.displays.display_driver import ALL_DISPLAY_TYPES, DISPLAY_TYPE__ILI9341, DISPLAY_TYPE__ILI9486, DISPLAY_TYPE__ST7789, DisplayDriver
|
||||
from seedsigner.hardware.displays.ili9341 import ILI9341, ILI9341_TFTWIDTH, ILI9341_TFTHEIGHT
|
||||
from seedsigner.models.settings import Settings
|
||||
from seedsigner.models.settings_definition import SettingsConstants
|
||||
from seedsigner.models.singleton import ConfigurableSingleton
|
||||
|
||||
|
||||
@@ -22,19 +26,44 @@ class Renderer(ConfigurableSingleton):
|
||||
renderer = cls.__new__(cls)
|
||||
cls._instance = renderer
|
||||
|
||||
# Eventually we'll be able to plug in other display controllers
|
||||
renderer.disp = ST7789()
|
||||
renderer.canvas_width = renderer.disp.width
|
||||
renderer.canvas_height = renderer.disp.height
|
||||
renderer.initialize_display()
|
||||
|
||||
renderer.canvas = Image.new('RGB', (renderer.canvas_width, renderer.canvas_height))
|
||||
renderer.draw = ImageDraw.Draw(renderer.canvas)
|
||||
|
||||
def initialize_display(self):
|
||||
# May be called while already running with a previous display driver; must
|
||||
# prevent any other screen writes while we're changing the display driver.
|
||||
self.lock.acquire()
|
||||
|
||||
display_config = Settings.get_instance().get_value(SettingsConstants.SETTING__DISPLAY_CONFIGURATION, default_if_none=True)
|
||||
self.display_type = display_config.split("_")[0]
|
||||
if self.display_type not in ALL_DISPLAY_TYPES:
|
||||
raise Exception(f"Invalid display type: {self.display_type}")
|
||||
|
||||
width, height = display_config.split("_")[1].split("x")
|
||||
self.disp = DisplayDriver(self.display_type, width=int(width), height=int(height))
|
||||
|
||||
if Settings.get_instance().get_value(SettingsConstants.SETTING__DISPLAY_COLOR_INVERTED, default_if_none=True) == SettingsConstants.OPTION__ENABLED:
|
||||
self.disp.invert()
|
||||
|
||||
if self.display_type == DISPLAY_TYPE__ST7789:
|
||||
self.canvas_width = self.disp.width
|
||||
self.canvas_height = self.disp.height
|
||||
|
||||
elif self.display_type in [DISPLAY_TYPE__ILI9341, DISPLAY_TYPE__ILI9486]:
|
||||
# Swap for the natively portrait-oriented displays
|
||||
self.canvas_width = self.disp.height
|
||||
self.canvas_height = self.disp.width
|
||||
|
||||
self.canvas = Image.new('RGB', (self.canvas_width, self.canvas_height))
|
||||
self.draw = ImageDraw.Draw(self.canvas)
|
||||
|
||||
self.lock.release()
|
||||
|
||||
|
||||
def show_image(self, image=None, alpha_overlay=None, show_direct=False):
|
||||
if show_direct:
|
||||
# Use the incoming image as the canvas and immediately render
|
||||
self.disp.ShowImage(image, 0, 0)
|
||||
self.disp.show_image(image, 0, 0)
|
||||
return
|
||||
|
||||
if alpha_overlay:
|
||||
@@ -46,7 +75,7 @@ class Renderer(ConfigurableSingleton):
|
||||
# Always write to the current canvas, rather than trying to replace it
|
||||
self.canvas.paste(image)
|
||||
|
||||
self.disp.ShowImage(self.canvas, 0, 0)
|
||||
self.disp.show_image(self.canvas, 0, 0)
|
||||
|
||||
|
||||
def show_image_pan(self, image, start_x, start_y, end_x, end_y, rate, alpha_overlay=None):
|
||||
@@ -80,7 +109,7 @@ class Renderer(ConfigurableSingleton):
|
||||
# Always keep a copy of the current display in the canvas
|
||||
self.canvas.paste(crop)
|
||||
|
||||
self.disp.ShowImage(crop, 0, 0)
|
||||
self.disp.show_image(crop, 0, 0)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from gettext import gettext as _
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
from seedsigner.gui import renderer
|
||||
from seedsigner.gui.components import GUIConstants, Fonts
|
||||
from seedsigner.gui.components import GUIConstants, Fonts, resize_image_to_fill
|
||||
from seedsigner.models.decode_qr import DecodeQR
|
||||
from seedsigner.models.threads import BaseThread, ThreadsafeCounter
|
||||
|
||||
@@ -132,11 +132,8 @@ class ScanScreen(BaseScreen):
|
||||
scan_text += f" {cur_fps:0.2f} | {self.decoder_fps}"
|
||||
|
||||
with self.renderer.lock:
|
||||
if frame.width > self.render_width or frame.height > self.render_height:
|
||||
frame = frame.resize(
|
||||
(self.render_width, self.render_height),
|
||||
resample=Image.NEAREST # Use nearest neighbor for max speed
|
||||
)
|
||||
# Use nearest neighbor resizing for max speed
|
||||
frame = resize_image_to_fill(frame, self.render_width, self.render_height, sampling_method=Image.Resampling.NEAREST)
|
||||
|
||||
if scan_text:
|
||||
# Note: shadowed text (adding a 'stroke' outline) can
|
||||
|
||||
@@ -579,9 +579,11 @@ class LargeButtonScreen(BaseTopNavScreen):
|
||||
if len(self.button_data) not in [2, 4]:
|
||||
raise Exception("LargeButtonScreen only supports 2 or 4 buttons")
|
||||
|
||||
# Maximize 2-across width; calc height with a 4:3 aspect ratio
|
||||
# Maximize 2-across width
|
||||
button_width = int((self.canvas_width - (2 * GUIConstants.EDGE_PADDING) - GUIConstants.COMPONENT_PADDING) / 2)
|
||||
button_height = int(button_width * (3.0 / 4.0))
|
||||
|
||||
# Maximize 2-row height
|
||||
button_height = int((self.canvas_height - self.top_nav.height - (2 * GUIConstants.COMPONENT_PADDING) - GUIConstants.EDGE_PADDING) / 2)
|
||||
|
||||
# Vertically center the buttons
|
||||
if len(self.button_data) == 2:
|
||||
|
||||
@@ -31,8 +31,17 @@ class SeedMnemonicEntryScreen(BaseTopNavScreen):
|
||||
|
||||
self.possible_alphabet = "abcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
# Measure the width required to display the longest word in the English bip39
|
||||
# wordlist.
|
||||
# TODO: If we ever support other wordlist languages, adjust accordingly.
|
||||
matches_list_highlight_font_name = GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME
|
||||
matches_list_highlight_font_size = GUIConstants.get_button_font_size() + 4
|
||||
(left, top, right, bottom) = Fonts.get_font(matches_list_highlight_font_name, matches_list_highlight_font_size).getbbox("mushroom", anchor="ls")
|
||||
matches_list_max_text_width = right - left
|
||||
matches_list_button_width = matches_list_max_text_width + 2*GUIConstants.COMPONENT_PADDING
|
||||
|
||||
# Set up the keyboard params
|
||||
self.keyboard_width = 128
|
||||
self.keyboard_width = self.canvas_width - GUIConstants.EDGE_PADDING - matches_list_button_width
|
||||
text_entry_display_y = self.top_nav.height
|
||||
text_entry_display_height = 30
|
||||
|
||||
@@ -78,7 +87,7 @@ class SeedMnemonicEntryScreen(BaseTopNavScreen):
|
||||
else:
|
||||
self.keyboard.set_selected_key(selected_letter=self.letters[-1])
|
||||
|
||||
self.matches_list_x = GUIConstants.EDGE_PADDING + self.keyboard.width + GUIConstants.COMPONENT_PADDING
|
||||
self.matches_list_x = self.canvas_width - matches_list_button_width
|
||||
self.matches_list_y = self.top_nav.height
|
||||
self.highlighted_row_y = int((self.canvas_height - GUIConstants.BUTTON_HEIGHT)/2)
|
||||
|
||||
@@ -634,12 +643,17 @@ class SeedExportXpubDetailsScreen(WarningEdgesMixin, ButtonListScreen):
|
||||
)
|
||||
self.components.append(self.derivation_line)
|
||||
|
||||
font_name = GUIConstants.FIXED_WIDTH_FONT_NAME
|
||||
font_size = GUIConstants.get_body_font_size() + 2
|
||||
left, top, right, bottom = Fonts.get_font(font_name, font_size).getbbox("X")
|
||||
char_width = right - left
|
||||
num_chars = int((self.canvas_width - GUIConstants.ICON_FONT_SIZE - 2*GUIConstants.COMPONENT_PADDING) / char_width) - 3 # ellipsis
|
||||
|
||||
self.xpub_line = IconTextLine(
|
||||
icon_name=FontAwesomeIconConstants.X,
|
||||
icon_color=GUIConstants.INFO_COLOR,
|
||||
# TRANSLATOR_NOTE: Short for "BIP32 Extended Public Key"
|
||||
label_text=_("Xpub"),
|
||||
value_text=f"{self.xpub[:18]}...",
|
||||
value_text=f"{self.xpub[:num_chars]}...",
|
||||
font_name=GUIConstants.FIXED_WIDTH_FONT_NAME,
|
||||
font_size=GUIConstants.get_body_font_size() + 2,
|
||||
screen_x=GUIConstants.COMPONENT_PADDING,
|
||||
@@ -1181,68 +1195,81 @@ class SeedTranscribeSeedQRWholeQRScreen(WarningEdgesMixin, ButtonListScreen):
|
||||
|
||||
@dataclass
|
||||
class SeedTranscribeSeedQRZoomedInScreen(BaseScreen):
|
||||
"""
|
||||
QR codes are defined by the number of "modules" (squares), e.g. 21x21 modules.
|
||||
|
||||
Each module will be rendered as a square of pixels, e.g. 24x24 pixels.
|
||||
|
||||
In this Screen, a "zone" will mean a square module area, e.g. 5x5 modules, that
|
||||
corresponds to the SeedQR templates which include zone guidelines and labels
|
||||
(e.g. "B-3").
|
||||
"""
|
||||
qr_data: str = None
|
||||
num_modules: int = None
|
||||
initial_block_x: int = 0
|
||||
initial_block_y: int = 0
|
||||
initial_zone_x: int = 0
|
||||
initial_zone_y: int = 0
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
|
||||
# Render an oversized QR code that we can view up close
|
||||
self.pixels_per_block = 24
|
||||
self.pixels_per_module = 24
|
||||
|
||||
# Border must accommodate the 3 blocks outside the center 5x5 mask plus up to
|
||||
# 1 empty block inside the 5x5 mask (29x29 has a 4-block final col/row).
|
||||
self.qr_border = 4
|
||||
self.num_qr_border_modules = 4
|
||||
if self.num_modules == 21:
|
||||
# Optimize for 21x21
|
||||
self.qr_blocks_per_zoom = 7
|
||||
self.modules_per_zone = 7 # i.e. a 7x7 group of modules
|
||||
else:
|
||||
self.qr_blocks_per_zoom = 5
|
||||
self.modules_per_zone = 5
|
||||
|
||||
self.qr_width = (self.qr_border + self.num_modules + self.qr_border) * self.pixels_per_block
|
||||
self.height = self.qr_width
|
||||
total_qr_image_width = (self.num_qr_border_modules + self.num_modules + self.num_qr_border_modules) * self.pixels_per_module
|
||||
qr = QR()
|
||||
self.qr_image = qr.qrimage(
|
||||
self.qr_image: Image = qr.qrimage(
|
||||
self.qr_data,
|
||||
width=self.qr_width,
|
||||
height=self.height,
|
||||
border=self.qr_border,
|
||||
width=total_qr_image_width,
|
||||
height=total_qr_image_width, # QR image is always square
|
||||
border=self.num_qr_border_modules,
|
||||
style=QR.STYLE__ROUNDED
|
||||
).convert("RGBA")
|
||||
# ).convert("RGBA")
|
||||
)
|
||||
|
||||
# Render gridlines but leave the 1-block border as-is
|
||||
draw = ImageDraw.Draw(self.qr_image)
|
||||
for i in range(self.qr_border, math.floor(self.qr_width/self.pixels_per_block) - self.qr_border):
|
||||
draw.line((i * self.pixels_per_block, self.qr_border * self.pixels_per_block, i * self.pixels_per_block, self.height - self.qr_border * self.pixels_per_block), fill="#bbb")
|
||||
draw.line((self.qr_border * self.pixels_per_block, i * self.pixels_per_block, self.qr_width - self.qr_border * self.pixels_per_block, i * self.pixels_per_block), fill="#bbb")
|
||||
# Render gridlines over the QR code but don't draw on its external white border
|
||||
qr_image_draw = ImageDraw.Draw(self.qr_image)
|
||||
for i in range(self.num_qr_border_modules, math.floor(self.qr_image.width/self.pixels_per_module) - self.num_qr_border_modules):
|
||||
qr_image_draw.line((i * self.pixels_per_module, self.num_qr_border_modules * self.pixels_per_module, i * self.pixels_per_module, self.qr_image.height - self.num_qr_border_modules * self.pixels_per_module), fill="#bbb")
|
||||
qr_image_draw.line((self.num_qr_border_modules * self.pixels_per_module, i * self.pixels_per_module, self.qr_image.width - self.num_qr_border_modules * self.pixels_per_module, i * self.pixels_per_module), fill="#bbb")
|
||||
|
||||
# Prep the semi-transparent mask overlay
|
||||
# make a blank image for the overlay, initialized to transparent
|
||||
self.block_mask = Image.new("RGBA", (self.canvas_width, self.canvas_height), (255,255,255,0))
|
||||
draw = ImageDraw.Draw(self.block_mask)
|
||||
|
||||
self.mask_width = int((self.canvas_width - self.qr_blocks_per_zoom * self.pixels_per_block)/2)
|
||||
self.mask_height = int((self.canvas_height - self.qr_blocks_per_zoom * self.pixels_per_block)/2)
|
||||
# Make a blank semi-transparent image for the overlay, initially across the
|
||||
# entire canvas.
|
||||
mask_rgba = (0, 0, 0, 226)
|
||||
draw.rectangle((0, 0, self.canvas_width, self.mask_height), fill=mask_rgba)
|
||||
draw.rectangle((0, self.canvas_height - self.mask_height - 1, self.canvas_width, self.canvas_height), fill=mask_rgba)
|
||||
draw.rectangle((0, self.mask_height, self.mask_width, self.canvas_height - self.mask_height), fill=mask_rgba)
|
||||
draw.rectangle((self.canvas_width - self.mask_width - 1, self.mask_height, self.canvas_width, self.canvas_height - self.mask_height), fill=mask_rgba)
|
||||
self.zone_mask = Image.new("RGBA", (self.canvas_width, self.canvas_height), mask_rgba)
|
||||
zone_mask_draw = ImageDraw.Draw(self.zone_mask)
|
||||
|
||||
# Draw a box around the cutout portion of the mask for better visibility
|
||||
draw.line((self.mask_width, self.mask_height, self.mask_width, self.canvas_height - self.mask_height), fill=GUIConstants.ACCENT_COLOR)
|
||||
draw.line((self.canvas_width - self.mask_width, self.mask_height, self.canvas_width - self.mask_width, self.canvas_height - self.mask_height), fill=GUIConstants.ACCENT_COLOR)
|
||||
draw.line((self.mask_width, self.mask_height, self.canvas_width - self.mask_width, self.mask_height), fill=GUIConstants.ACCENT_COLOR)
|
||||
draw.line((self.mask_width, self.canvas_height - self.mask_height, self.canvas_width - self.mask_width, self.canvas_height - self.mask_height), fill=GUIConstants.ACCENT_COLOR)
|
||||
# Now punch a hole in the center of the mask to highlight the current zone with
|
||||
# an accent outline.
|
||||
# The `zone_mask_offset_*` vars are the top left xy coords of the mask.
|
||||
self.zone_mask_offset_x = int((self.canvas_width - (self.modules_per_zone * self.pixels_per_module))/2)
|
||||
self.zone_mask_offset_y = int((self.canvas_height - (self.modules_per_zone * self.pixels_per_module))/2)
|
||||
zone_mask_draw.rectangle(
|
||||
(
|
||||
self.zone_mask_offset_x,
|
||||
self.zone_mask_offset_y,
|
||||
self.canvas_width - self.zone_mask_offset_x,
|
||||
self.canvas_height - self.zone_mask_offset_y
|
||||
),
|
||||
fill=(255, 255, 255, 0), # fully transparent mask area
|
||||
outline=GUIConstants.ACCENT_COLOR,
|
||||
width=1
|
||||
)
|
||||
|
||||
msg = _("click to exit")
|
||||
font = Fonts.get_font(GUIConstants.get_body_font_name(), GUIConstants.get_body_font_size())
|
||||
(left, top, right, bottom) = font.getbbox(msg, anchor="ls")
|
||||
msg_height = -1 * top + GUIConstants.COMPONENT_PADDING
|
||||
msg_width = right + 2*GUIConstants.COMPONENT_PADDING
|
||||
draw.rectangle(
|
||||
zone_mask_draw.rectangle(
|
||||
(
|
||||
int((self.canvas_width - msg_width)/2),
|
||||
self.canvas_height - msg_height,
|
||||
@@ -1251,7 +1278,7 @@ class SeedTranscribeSeedQRZoomedInScreen(BaseScreen):
|
||||
),
|
||||
fill=GUIConstants.BACKGROUND_COLOR,
|
||||
)
|
||||
draw.text(
|
||||
zone_mask_draw.text(
|
||||
(int(self.canvas_width/2), self.canvas_height - int(GUIConstants.COMPONENT_PADDING/2)),
|
||||
msg,
|
||||
fill=GUIConstants.BODY_FONT_COLOR,
|
||||
@@ -1261,103 +1288,103 @@ class SeedTranscribeSeedQRZoomedInScreen(BaseScreen):
|
||||
|
||||
|
||||
|
||||
def draw_block_labels(self):
|
||||
# Create overlay for block labels (e.g. "D-5")
|
||||
block_labels_x = ["1", "2", "3", "4", "5", "6"]
|
||||
block_labels_y = ["A", "B", "C", "D", "E", "F"]
|
||||
def draw_zone_labels(self):
|
||||
# Create overlay for zone labels (e.g. "D-5")
|
||||
# TODO: Discuss w/translators if these zone labels need to be translated; would
|
||||
# trigger a secondary need to have translated SeedQR printable templates as well.
|
||||
zone_labels_x = ["1", "2", "3", "4", "5", "6"]
|
||||
zone_labels_y = ["A", "B", "C", "D", "E", "F"]
|
||||
|
||||
block_labels = Image.new("RGBA", (self.canvas_width, self.canvas_height), (255,255,255,0))
|
||||
draw = ImageDraw.Draw(block_labels)
|
||||
draw.rectangle((self.mask_width, 0, self.canvas_width - self.mask_width, self.pixels_per_block), fill=GUIConstants.ACCENT_COLOR)
|
||||
draw.rectangle((0, self.mask_height, self.pixels_per_block, self.canvas_height - self.mask_height), fill=GUIConstants.ACCENT_COLOR)
|
||||
zone_labels = Image.new("RGBA", (self.canvas_width, self.canvas_height), (255,255,255,0))
|
||||
zone_labels_draw = ImageDraw.Draw(zone_labels)
|
||||
zone_labels_draw.rectangle((self.zone_mask_offset_x, 0, self.canvas_width - self.zone_mask_offset_x, self.pixels_per_module), fill=GUIConstants.ACCENT_COLOR)
|
||||
zone_labels_draw.rectangle((0, self.zone_mask_offset_y, self.pixels_per_module, self.canvas_height - self.zone_mask_offset_y), fill=GUIConstants.ACCENT_COLOR)
|
||||
|
||||
label_font = Fonts.get_font(GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME, 28)
|
||||
x_label = block_labels_x[self.cur_block_x]
|
||||
x_label = zone_labels_x[self.cur_zone_x]
|
||||
(left, top, right, bottom) = label_font.getbbox(x_label, anchor="ls")
|
||||
x_label_height = -1 * top
|
||||
|
||||
draw.text(
|
||||
(int(self.canvas_width/2), self.pixels_per_block - int((self.pixels_per_block - x_label_height)/2)),
|
||||
zone_labels_draw.text(
|
||||
(int(self.canvas_width/2), self.pixels_per_module - int((self.pixels_per_module - x_label_height)/2)),
|
||||
text=x_label,
|
||||
fill=GUIConstants.BUTTON_SELECTED_FONT_COLOR,
|
||||
font=label_font,
|
||||
anchor="ms", # Middle, baSeline
|
||||
)
|
||||
|
||||
y_label = block_labels_y[self.cur_block_y]
|
||||
y_label = zone_labels_y[self.cur_zone_y]
|
||||
(left, top, right, bottom) = label_font.getbbox(y_label, anchor="ls")
|
||||
y_label_height = -1 * top
|
||||
draw.text(
|
||||
(int(self.pixels_per_block/2), int((self.canvas_height + y_label_height) / 2)),
|
||||
zone_labels_draw.text(
|
||||
(int(self.pixels_per_module/2), int((self.canvas_height + y_label_height) / 2)),
|
||||
text=y_label,
|
||||
fill=GUIConstants.BUTTON_SELECTED_FONT_COLOR,
|
||||
font=label_font,
|
||||
anchor="ms", # Middle, baSeline
|
||||
)
|
||||
|
||||
return block_labels
|
||||
return zone_labels
|
||||
|
||||
|
||||
def _render(self):
|
||||
# Track our current coordinates for the upper left corner of our view
|
||||
self.cur_block_x = self.initial_block_x
|
||||
self.cur_block_y = self.initial_block_y
|
||||
self.cur_x = (self.cur_block_x * self.qr_blocks_per_zoom * self.pixels_per_block) + self.qr_border * self.pixels_per_block - self.mask_width
|
||||
self.cur_y = (self.cur_block_y * self.qr_blocks_per_zoom * self.pixels_per_block) + self.qr_border * self.pixels_per_block - self.mask_height
|
||||
self.next_x = self.cur_x
|
||||
self.next_y = self.cur_y
|
||||
# Track our current zone-level (macro-module) position and our actual pixel
|
||||
# coordinates as we pan around across the QR code image.
|
||||
self.cur_zone_x = self.initial_zone_x
|
||||
self.cur_zone_y = self.initial_zone_y
|
||||
self.cur_pixel_x = (self.cur_zone_x * self.modules_per_zone * self.pixels_per_module) + self.num_qr_border_modules * self.pixels_per_module - self.zone_mask_offset_x
|
||||
self.cur_pixel_y = (self.cur_zone_y * self.modules_per_zone * self.pixels_per_module) + self.num_qr_border_modules * self.pixels_per_module - self.zone_mask_offset_y
|
||||
self.next_pixel_x = self.cur_pixel_x
|
||||
self.next_pixel_y = self.cur_pixel_y
|
||||
|
||||
block_labels = self.draw_block_labels()
|
||||
zone_labels = self.draw_zone_labels()
|
||||
|
||||
self.renderer.show_image(
|
||||
self.qr_image.crop((self.cur_x, self.cur_y, self.cur_x + self.canvas_width, self.cur_y + self.canvas_height)),
|
||||
alpha_overlay=Image.alpha_composite(self.block_mask, block_labels)
|
||||
self.qr_image.crop((self.cur_pixel_x, self.cur_pixel_y, self.cur_pixel_x + self.canvas_width, self.cur_pixel_y + self.canvas_height)),
|
||||
alpha_overlay=Image.alpha_composite(self.zone_mask, zone_labels)
|
||||
)
|
||||
|
||||
|
||||
def _run(self):
|
||||
while True:
|
||||
input = self.hw_inputs.wait_for(HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN + HardwareButtonsConstants.KEYS__ANYCLICK)
|
||||
if input == HardwareButtonsConstants.KEY_RIGHT:
|
||||
self.next_x = self.cur_x + self.qr_blocks_per_zoom * self.pixels_per_block
|
||||
self.cur_block_x += 1
|
||||
if self.next_x > self.qr_width - self.canvas_width:
|
||||
self.next_x = self.cur_x
|
||||
self.cur_block_x -= 1
|
||||
elif input == HardwareButtonsConstants.KEY_LEFT:
|
||||
self.next_x = self.cur_x - self.qr_blocks_per_zoom * self.pixels_per_block
|
||||
self.cur_block_x -= 1
|
||||
if self.next_x < 0:
|
||||
self.next_x = self.cur_x
|
||||
self.cur_block_x += 1
|
||||
elif input == HardwareButtonsConstants.KEY_DOWN:
|
||||
self.next_y = self.cur_y + self.qr_blocks_per_zoom * self.pixels_per_block
|
||||
self.cur_block_y += 1
|
||||
if self.next_y > self.height - self.canvas_height:
|
||||
self.next_y = self.cur_y
|
||||
self.cur_block_y -= 1
|
||||
elif input == HardwareButtonsConstants.KEY_UP:
|
||||
self.next_y = self.cur_y - self.qr_blocks_per_zoom * self.pixels_per_block
|
||||
self.cur_block_y -= 1
|
||||
if self.next_y < 0:
|
||||
self.next_y = self.cur_y
|
||||
self.cur_block_y += 1
|
||||
elif input in HardwareButtonsConstants.KEYS__ANYCLICK:
|
||||
|
||||
if input in HardwareButtonsConstants.KEYS__ANYCLICK:
|
||||
# User clicked to exit
|
||||
return
|
||||
|
||||
# Create overlay for block labels (e.g. "D-5")
|
||||
block_labels = self.draw_block_labels()
|
||||
elif input == HardwareButtonsConstants.KEY_RIGHT and self.cur_zone_x + 1 < math.ceil(self.num_modules/self.modules_per_zone):
|
||||
self.next_pixel_x = self.cur_pixel_x + self.modules_per_zone * self.pixels_per_module
|
||||
self.cur_zone_x += 1
|
||||
|
||||
if self.cur_x != self.next_x or self.cur_y != self.next_y:
|
||||
with self.renderer.lock:
|
||||
self.renderer.show_image_pan(
|
||||
self.qr_image,
|
||||
self.cur_x, self.cur_y, self.next_x, self.next_y,
|
||||
rate=self.pixels_per_block,
|
||||
alpha_overlay=Image.alpha_composite(self.block_mask, block_labels)
|
||||
)
|
||||
self.cur_x = self.next_x
|
||||
self.cur_y = self.next_y
|
||||
elif input == HardwareButtonsConstants.KEY_LEFT and self.cur_zone_x - 1 >= 0:
|
||||
self.next_pixel_x = self.cur_pixel_x - self.modules_per_zone * self.pixels_per_module
|
||||
self.cur_zone_x -= 1
|
||||
|
||||
elif input == HardwareButtonsConstants.KEY_DOWN and self.cur_zone_y + 1 < math.ceil(self.num_modules/self.modules_per_zone):
|
||||
self.next_pixel_y = self.cur_pixel_y + self.modules_per_zone * self.pixels_per_module
|
||||
self.cur_zone_y += 1
|
||||
|
||||
elif input == HardwareButtonsConstants.KEY_UP and self.cur_zone_y - 1 >= 0:
|
||||
self.next_pixel_y = self.cur_pixel_y - self.modules_per_zone * self.pixels_per_module
|
||||
self.cur_zone_y -= 1
|
||||
|
||||
else:
|
||||
# User selected a direction that we can't advance any further
|
||||
continue
|
||||
|
||||
# Create overlay for zone labels (e.g. "D-5")
|
||||
zone_labels = self.draw_zone_labels()
|
||||
|
||||
with self.renderer.lock:
|
||||
self.renderer.show_image_pan(
|
||||
self.qr_image,
|
||||
self.cur_pixel_x, self.cur_pixel_y, self.next_pixel_x, self.next_pixel_y,
|
||||
rate=self.pixels_per_module,
|
||||
alpha_overlay=Image.alpha_composite(self.zone_mask, zone_labels)
|
||||
)
|
||||
self.cur_pixel_x = self.next_pixel_x
|
||||
self.cur_pixel_y = self.next_pixel_y
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -4,10 +4,11 @@ from dataclasses import dataclass
|
||||
from gettext import gettext as _
|
||||
from typing import Any
|
||||
from PIL.Image import Image
|
||||
from seedsigner.gui.renderer import Renderer
|
||||
from seedsigner.hardware.camera import Camera
|
||||
from seedsigner.gui.components import FontAwesomeIconConstants, Fonts, GUIConstants, IconTextLine, SeedSignerIconConstants, TextArea
|
||||
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, BaseScreen, ButtonListScreen, KeyboardScreen
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, BaseScreen, ButtonListScreen, ButtonOption, KeyboardScreen
|
||||
from seedsigner.hardware.buttons import HardwareButtonsConstants
|
||||
from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition
|
||||
|
||||
@@ -19,7 +20,12 @@ class ToolsImageEntropyLivePreviewScreen(BaseScreen):
|
||||
super().__post_init__()
|
||||
|
||||
self.camera = Camera.get_instance()
|
||||
self.camera.start_video_stream_mode(resolution=(self.canvas_width, self.canvas_height), framerate=24, format="rgb")
|
||||
|
||||
# If the stream is set to 320x240, we get pillarboxed frames (black bars on the
|
||||
# sides). But passing in square dims gives us an edge-to-edge image.
|
||||
# TODO: Figure out why (camera expecting frame dims of multiples other than 16?)
|
||||
max_dimension = max(self.canvas_width, self.canvas_height)
|
||||
self.camera.start_video_stream_mode(resolution=(max_dimension, max_dimension), framerate=24, format="rgb")
|
||||
|
||||
|
||||
def _run(self):
|
||||
@@ -36,13 +42,38 @@ class ToolsImageEntropyLivePreviewScreen(BaseScreen):
|
||||
self.camera.stop_video_stream_mode()
|
||||
return RET_CODE__BACK_BUTTON
|
||||
|
||||
frame = self.camera.read_video_stream(as_image=True)
|
||||
frame: Image = self.camera.read_video_stream(as_image=True)
|
||||
|
||||
if frame is None:
|
||||
# Camera probably isn't ready yet
|
||||
time.sleep(0.01)
|
||||
continue
|
||||
|
||||
with self.renderer.lock:
|
||||
# Account for the possibly different aspect ratio of the camera frame
|
||||
# vs the display; crop any excess.
|
||||
# TODO: This cropping may be unnecessary if the above TODO about the
|
||||
# camera resolution is solved.
|
||||
box = None
|
||||
if self.canvas_width != frame.width:
|
||||
half_width_diff = int(abs(self.canvas_width - frame.width)/2)
|
||||
box = (
|
||||
half_width_diff,
|
||||
0,
|
||||
frame.width - half_width_diff,
|
||||
frame.height
|
||||
)
|
||||
elif self.canvas_height != frame.height:
|
||||
half_height_diff = int(abs(self.canvas_height - frame.height)/2)
|
||||
box = (
|
||||
0,
|
||||
half_height_diff,
|
||||
frame.width,
|
||||
frame.height - half_height_diff
|
||||
)
|
||||
|
||||
self.renderer.canvas.paste(frame.crop(box=box))
|
||||
|
||||
# Check for ANYCLICK to take final entropy image
|
||||
if self.hw_inputs.check_for_low(keys=HardwareButtonsConstants.KEYS__ANYCLICK):
|
||||
# Have to manually update last input time since we're not in a wait_for loop
|
||||
@@ -50,8 +81,6 @@ class ToolsImageEntropyLivePreviewScreen(BaseScreen):
|
||||
self.camera.stop_video_stream_mode()
|
||||
|
||||
with self.renderer.lock:
|
||||
self.renderer.canvas.paste(frame)
|
||||
|
||||
self.renderer.draw.text(
|
||||
xy=(
|
||||
int(self.renderer.canvas_width/2),
|
||||
@@ -70,8 +99,6 @@ class ToolsImageEntropyLivePreviewScreen(BaseScreen):
|
||||
|
||||
# If we're still here, it's just another preview frame loop
|
||||
with self.renderer.lock:
|
||||
self.renderer.canvas.paste(frame)
|
||||
|
||||
self.renderer.draw.text(
|
||||
xy=(
|
||||
int(self.renderer.canvas_width/2),
|
||||
@@ -462,3 +489,47 @@ class ToolsAddressExplorerAddressTypeScreen(ButtonListScreen):
|
||||
screen_x=GUIConstants.EDGE_PADDING,
|
||||
screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING,
|
||||
))
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolsAddressExplorerAddressListScreen(ButtonListScreen):
|
||||
start_index: int = 0
|
||||
addresses: list[str] = None
|
||||
|
||||
def __post_init__(self):
|
||||
self.button_font_name = GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME
|
||||
self.button_font_size = GUIConstants.get_button_font_size() + 4
|
||||
self.is_button_text_centered = False
|
||||
self.is_bottom_list = True
|
||||
|
||||
left, top, right, bottom = Fonts.get_font(self.button_font_name, self.button_font_size).getbbox("X")
|
||||
char_width = right - left
|
||||
|
||||
last_addr_index = self.start_index + len(self.addresses) - 1
|
||||
index_digits = len(str(last_addr_index))
|
||||
|
||||
# Calculate how many pixels we have available within each address button,
|
||||
# remembering to account for the index number that will be displayed.
|
||||
# Note: because we haven't called the parent's post_init yet, we don't have a
|
||||
# self.canvas_width set; have to use the Renderer singleton to get it.
|
||||
available_width = Renderer.get_instance().canvas_width - 2*GUIConstants.EDGE_PADDING - 2*GUIConstants.COMPONENT_PADDING - (index_digits + 1)*char_width
|
||||
displayable_chars = int(available_width / char_width) - 3 # ellipsis
|
||||
displayable_half = int(displayable_chars/2)
|
||||
|
||||
self.button_data = []
|
||||
for i, address in enumerate(self.addresses):
|
||||
cur_index = i + self.start_index
|
||||
|
||||
# TODO: Intentionally NOT marking these for translation, but we may need to in
|
||||
# the future.
|
||||
button_label = f"{cur_index}:{address[:displayable_half]}...{address[-1*displayable_half:]}"
|
||||
active_button_label = f"{cur_index}:{address}"
|
||||
|
||||
self.button_data.append(ButtonOption(button_label, active_button_label=active_button_label))
|
||||
|
||||
# TRANSLATOR_NOTE: Insert the number of addrs displayed per screen (e.g. "Next 10")
|
||||
button_label = _("Next {}").format(len(self.addresses))
|
||||
self.button_data.append(ButtonOption(button_label, right_icon_name=SeedSignerIconConstants.CHEVRON_RIGHT))
|
||||
|
||||
super().__post_init__()
|
||||
|
||||
@@ -1,169 +1,173 @@
|
||||
import spidev
|
||||
import RPi.GPIO as GPIO
|
||||
import time
|
||||
import array
|
||||
|
||||
|
||||
|
||||
class ST7789(object):
|
||||
"""class for ST7789 240*240 1.3inch OLED displays."""
|
||||
|
||||
def __init__(self):
|
||||
self.width = 240
|
||||
self.height = 240
|
||||
|
||||
#Initialize DC RST pin
|
||||
self._dc = 22
|
||||
self._rst = 13
|
||||
self._bl = 18
|
||||
|
||||
GPIO.setmode(GPIO.BOARD)
|
||||
GPIO.setwarnings(False)
|
||||
GPIO.setup(self._dc,GPIO.OUT)
|
||||
GPIO.setup(self._rst,GPIO.OUT)
|
||||
GPIO.setup(self._bl,GPIO.OUT)
|
||||
GPIO.output(self._bl, GPIO.HIGH)
|
||||
|
||||
#Initialize SPI
|
||||
self._spi = spidev.SpiDev(0, 0)
|
||||
self._spi.max_speed_hz = 40000000
|
||||
|
||||
self.init()
|
||||
|
||||
|
||||
""" Write register address and data """
|
||||
def command(self, cmd):
|
||||
GPIO.output(self._dc, GPIO.LOW)
|
||||
self._spi.writebytes([cmd])
|
||||
|
||||
def data(self, val):
|
||||
GPIO.output(self._dc, GPIO.HIGH)
|
||||
self._spi.writebytes([val])
|
||||
|
||||
def init(self):
|
||||
"""Initialize dispaly"""
|
||||
self.reset()
|
||||
|
||||
self.command(0x36)
|
||||
self.data(0x70) #self.data(0x00)
|
||||
|
||||
self.command(0x3A)
|
||||
self.data(0x05)
|
||||
|
||||
self.command(0xB2)
|
||||
self.data(0x0C)
|
||||
self.data(0x0C)
|
||||
self.data(0x00)
|
||||
self.data(0x33)
|
||||
self.data(0x33)
|
||||
|
||||
self.command(0xB7)
|
||||
self.data(0x35)
|
||||
|
||||
self.command(0xBB)
|
||||
self.data(0x19)
|
||||
|
||||
self.command(0xC0)
|
||||
self.data(0x2C)
|
||||
|
||||
self.command(0xC2)
|
||||
self.data(0x01)
|
||||
|
||||
self.command(0xC3)
|
||||
self.data(0x12)
|
||||
|
||||
self.command(0xC4)
|
||||
self.data(0x20)
|
||||
|
||||
self.command(0xC6)
|
||||
self.data(0x0F)
|
||||
|
||||
self.command(0xD0)
|
||||
self.data(0xA4)
|
||||
self.data(0xA1)
|
||||
|
||||
self.command(0xE0)
|
||||
self.data(0xD0)
|
||||
self.data(0x04)
|
||||
self.data(0x0D)
|
||||
self.data(0x11)
|
||||
self.data(0x13)
|
||||
self.data(0x2B)
|
||||
self.data(0x3F)
|
||||
self.data(0x54)
|
||||
self.data(0x4C)
|
||||
self.data(0x18)
|
||||
self.data(0x0D)
|
||||
self.data(0x0B)
|
||||
self.data(0x1F)
|
||||
self.data(0x23)
|
||||
|
||||
self.command(0xE1)
|
||||
self.data(0xD0)
|
||||
self.data(0x04)
|
||||
self.data(0x0C)
|
||||
self.data(0x11)
|
||||
self.data(0x13)
|
||||
self.data(0x2C)
|
||||
self.data(0x3F)
|
||||
self.data(0x44)
|
||||
self.data(0x51)
|
||||
self.data(0x2F)
|
||||
self.data(0x1F)
|
||||
self.data(0x1F)
|
||||
self.data(0x20)
|
||||
self.data(0x23)
|
||||
|
||||
self.command(0x21)
|
||||
|
||||
self.command(0x11)
|
||||
|
||||
self.command(0x29)
|
||||
|
||||
def reset(self):
|
||||
"""Reset the display"""
|
||||
GPIO.output(self._rst,GPIO.HIGH)
|
||||
time.sleep(0.01)
|
||||
GPIO.output(self._rst,GPIO.LOW)
|
||||
time.sleep(0.01)
|
||||
GPIO.output(self._rst,GPIO.HIGH)
|
||||
time.sleep(0.01)
|
||||
|
||||
def SetWindows(self, Xstart, Ystart, Xend, Yend):
|
||||
#set the X coordinates
|
||||
self.command(0x2A)
|
||||
self.data(0x00) #Set the horizontal starting point to the high octet
|
||||
self.data(Xstart & 0xff) #Set the horizontal starting point to the low octet
|
||||
self.data(0x00) #Set the horizontal end to the high octet
|
||||
self.data((Xend - 1) & 0xff) #Set the horizontal end to the low octet
|
||||
|
||||
#set the Y coordinates
|
||||
self.command(0x2B)
|
||||
self.data(0x00)
|
||||
self.data((Ystart & 0xff))
|
||||
self.data(0x00)
|
||||
self.data((Yend - 1) & 0xff )
|
||||
|
||||
self.command(0x2C)
|
||||
|
||||
def ShowImage(self,Image,Xstart,Ystart):
|
||||
"""Set buffer to value of Python Imaging Library image."""
|
||||
"""Write display buffer to physical display"""
|
||||
imwidth, imheight = Image.size
|
||||
if imwidth != self.width or imheight != self.height:
|
||||
raise ValueError('Image must be same dimensions as display \
|
||||
({0}x{1}).' .format(self.width, self.height))
|
||||
# convert 24-bit RGB-8:8:8 to gBRG-3:5:5:3; then per-pixel byteswap to 16-bit RGB-5:6:5
|
||||
arr = array.array("H", Image.convert("BGR;16").tobytes())
|
||||
arr.byteswap()
|
||||
pix = arr.tobytes()
|
||||
self.SetWindows ( 0, 0, self.width, self.height)
|
||||
GPIO.output(self._dc,GPIO.HIGH)
|
||||
self._spi.writebytes2(pix)
|
||||
|
||||
def clear(self):
|
||||
"""Clear contents of image buffer"""
|
||||
_buffer = [0xff]*(self.width * self.height * 2)
|
||||
self.SetWindows ( 0, 0, self.width, self.height)
|
||||
GPIO.output(self._dc,GPIO.HIGH)
|
||||
self._spi.writebytes2(_buffer)
|
||||
import spidev
|
||||
import RPi.GPIO as GPIO
|
||||
import time
|
||||
import array
|
||||
|
||||
|
||||
|
||||
class ST7789(object):
|
||||
"""class for ST7789 240*240 1.3inch OLED displays."""
|
||||
|
||||
def __init__(self):
|
||||
self.width = 240
|
||||
self.height = 240
|
||||
|
||||
#Initialize DC RST pin
|
||||
self._dc = 22
|
||||
self._rst = 13
|
||||
self._bl = 18
|
||||
|
||||
GPIO.setmode(GPIO.BOARD)
|
||||
GPIO.setwarnings(False)
|
||||
GPIO.setup(self._dc,GPIO.OUT)
|
||||
GPIO.setup(self._rst,GPIO.OUT)
|
||||
GPIO.setup(self._bl,GPIO.OUT)
|
||||
GPIO.output(self._bl, GPIO.HIGH)
|
||||
|
||||
#Initialize SPI
|
||||
self._spi = spidev.SpiDev(0, 0)
|
||||
self._spi.max_speed_hz = 40000000
|
||||
|
||||
self.init()
|
||||
|
||||
|
||||
""" Write register address and data """
|
||||
def command(self, cmd):
|
||||
GPIO.output(self._dc, GPIO.LOW)
|
||||
self._spi.writebytes([cmd])
|
||||
|
||||
def data(self, val):
|
||||
GPIO.output(self._dc, GPIO.HIGH)
|
||||
self._spi.writebytes([val])
|
||||
|
||||
def init(self):
|
||||
"""Initialize dispaly"""
|
||||
self.reset()
|
||||
|
||||
self.command(0x36)
|
||||
self.data(0x70) #self.data(0x00)
|
||||
|
||||
self.command(0x3A)
|
||||
self.data(0x05)
|
||||
|
||||
self.command(0xB2)
|
||||
self.data(0x0C)
|
||||
self.data(0x0C)
|
||||
self.data(0x00)
|
||||
self.data(0x33)
|
||||
self.data(0x33)
|
||||
|
||||
self.command(0xB7)
|
||||
self.data(0x35)
|
||||
|
||||
self.command(0xBB)
|
||||
self.data(0x19)
|
||||
|
||||
self.command(0xC0)
|
||||
self.data(0x2C)
|
||||
|
||||
self.command(0xC2)
|
||||
self.data(0x01)
|
||||
|
||||
self.command(0xC3)
|
||||
self.data(0x12)
|
||||
|
||||
self.command(0xC4)
|
||||
self.data(0x20)
|
||||
|
||||
self.command(0xC6)
|
||||
self.data(0x0F)
|
||||
|
||||
self.command(0xD0)
|
||||
self.data(0xA4)
|
||||
self.data(0xA1)
|
||||
|
||||
self.command(0xE0)
|
||||
self.data(0xD0)
|
||||
self.data(0x04)
|
||||
self.data(0x0D)
|
||||
self.data(0x11)
|
||||
self.data(0x13)
|
||||
self.data(0x2B)
|
||||
self.data(0x3F)
|
||||
self.data(0x54)
|
||||
self.data(0x4C)
|
||||
self.data(0x18)
|
||||
self.data(0x0D)
|
||||
self.data(0x0B)
|
||||
self.data(0x1F)
|
||||
self.data(0x23)
|
||||
|
||||
self.command(0xE1)
|
||||
self.data(0xD0)
|
||||
self.data(0x04)
|
||||
self.data(0x0C)
|
||||
self.data(0x11)
|
||||
self.data(0x13)
|
||||
self.data(0x2C)
|
||||
self.data(0x3F)
|
||||
self.data(0x44)
|
||||
self.data(0x51)
|
||||
self.data(0x2F)
|
||||
self.data(0x1F)
|
||||
self.data(0x1F)
|
||||
self.data(0x20)
|
||||
self.data(0x23)
|
||||
|
||||
self.command(0x21) # inversion ON; 0x20 = inversion OFF
|
||||
|
||||
self.command(0x11)
|
||||
|
||||
self.command(0x29)
|
||||
|
||||
def reset(self):
|
||||
"""Reset the display"""
|
||||
GPIO.output(self._rst,GPIO.HIGH)
|
||||
time.sleep(0.01)
|
||||
GPIO.output(self._rst,GPIO.LOW)
|
||||
time.sleep(0.01)
|
||||
GPIO.output(self._rst,GPIO.HIGH)
|
||||
time.sleep(0.01)
|
||||
|
||||
def SetWindows(self, Xstart, Ystart, Xend, Yend):
|
||||
#set the X coordinates
|
||||
self.command(0x2A)
|
||||
self.data(0x00) #Set the horizontal starting point to the high octet
|
||||
self.data(Xstart & 0xff) #Set the horizontal starting point to the low octet
|
||||
self.data(0x00) #Set the horizontal end to the high octet
|
||||
self.data((Xend - 1) & 0xff) #Set the horizontal end to the low octet
|
||||
|
||||
#set the Y coordinates
|
||||
self.command(0x2B)
|
||||
self.data(0x00)
|
||||
self.data((Ystart & 0xff))
|
||||
self.data(0x00)
|
||||
self.data((Yend - 1) & 0xff )
|
||||
|
||||
self.command(0x2C)
|
||||
|
||||
def show_image(self,Image,Xstart,Ystart):
|
||||
"""Set buffer to value of Python Imaging Library image."""
|
||||
"""Write display buffer to physical display"""
|
||||
imwidth, imheight = Image.size
|
||||
if imwidth != self.width or imheight != self.height:
|
||||
raise ValueError('Image must be same dimensions as display \
|
||||
({0}x{1}).' .format(self.width, self.height))
|
||||
# convert 24-bit RGB-8:8:8 to gBRG-3:5:5:3; then per-pixel byteswap to 16-bit RGB-5:6:5
|
||||
arr = array.array("H", Image.convert("BGR;16").tobytes())
|
||||
arr.byteswap()
|
||||
pix = arr.tobytes()
|
||||
self.SetWindows ( 0, 0, self.width, self.height)
|
||||
GPIO.output(self._dc,GPIO.HIGH)
|
||||
self._spi.writebytes2(pix)
|
||||
|
||||
def clear(self):
|
||||
"""Clear contents of image buffer"""
|
||||
_buffer = [0xff]*(self.width * self.height * 2)
|
||||
self.SetWindows ( 0, 0, self.width, self.height)
|
||||
GPIO.output(self._dc,GPIO.HIGH)
|
||||
self._spi.writebytes2(_buffer)
|
||||
|
||||
def invert(self, enabled: bool = True):
|
||||
"""Invert how the display interprets colors"""
|
||||
self.command(0x21 if enabled else 0x20)
|
||||
@@ -0,0 +1,60 @@
|
||||
DISPLAY_TYPE__ST7789 = "st7789"
|
||||
DISPLAY_TYPE__ILI9341 = "ili9341"
|
||||
DISPLAY_TYPE__ILI9486 = "ili9486"
|
||||
|
||||
ALL_DISPLAY_TYPES = [DISPLAY_TYPE__ST7789, DISPLAY_TYPE__ILI9341, DISPLAY_TYPE__ILI9486]
|
||||
|
||||
|
||||
class DisplayDriver:
|
||||
def __init__(self, display_type: str = DISPLAY_TYPE__ST7789, width: int = None, height: int = None):
|
||||
if display_type not in ALL_DISPLAY_TYPES:
|
||||
raise ValueError(f"Invalid display type: {display_type}")
|
||||
self.display_type = display_type
|
||||
|
||||
if self.display_type == DISPLAY_TYPE__ST7789:
|
||||
if width not in [240, 320] or height != 240:
|
||||
raise ValueError("ST7789 display only supports 240x240 or 320x240 resolutions")
|
||||
|
||||
if width == 240:
|
||||
# TODO: For now the original ST7789 driver has to be used for 240x240.
|
||||
# The mpy version below renders incorrectly (almost like each row of pixels
|
||||
# is one pixel short, so the entire screen exhibits a diagonal skew).
|
||||
from seedsigner.hardware.displays.ST7789 import ST7789
|
||||
self.display = ST7789()
|
||||
|
||||
elif width == 320:
|
||||
from seedsigner.hardware.displays.st7789_mpy import ST7789
|
||||
# Have to swap width and height; screen is natively 240x320
|
||||
self.display = ST7789(width=height, height=width)
|
||||
|
||||
elif self.display_type == DISPLAY_TYPE__ILI9341:
|
||||
from seedsigner.hardware.displays.ili9341 import ILI9341
|
||||
self.display = ILI9341()
|
||||
self.display.begin()
|
||||
|
||||
elif self.display_type == DISPLAY_TYPE__ILI9486:
|
||||
# TODO: improve performance of ili9486 driver
|
||||
raise Exception("ILI9486 display not implemented yet")
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"DisplayDriver(display_type={self.display_type}, width={self.width}, height={self.height})"
|
||||
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return self.display.width
|
||||
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
return self.display.height
|
||||
|
||||
|
||||
def invert(self, enabled: bool = True):
|
||||
"""Invert how the display interprets colors"""
|
||||
self.display.invert(enabled)
|
||||
|
||||
|
||||
def show_image(self, image, x_start: int = 0, y_start: int = 0):
|
||||
self.display.show_image(image, x_start, y_start)
|
||||
@@ -0,0 +1,376 @@
|
||||
# Copyright (c) 2014 Adafruit Industries
|
||||
# Author: Tony DiCola
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
"""
|
||||
Tested with a 320x240 IPS display (https://a.co/d/2Q9wDLo)
|
||||
* Framerate is excellent (~10fps)
|
||||
* Requires `invert()` and 90° rotation
|
||||
* Exhibits noticeable residual ghosting
|
||||
"""
|
||||
import numbers
|
||||
import time
|
||||
# import numpy as np
|
||||
import array
|
||||
|
||||
from PIL import Image
|
||||
from PIL import ImageDraw
|
||||
|
||||
import RPi.GPIO as GPIO
|
||||
from spidev import SpiDev
|
||||
|
||||
|
||||
# Constants for interacting with display registers.
|
||||
ILI9341_TFTWIDTH = 240
|
||||
ILI9341_TFTHEIGHT = 320
|
||||
|
||||
ILI9341_NOP = 0x00
|
||||
ILI9341_SWRESET = 0x01
|
||||
ILI9341_RDDID = 0x04
|
||||
ILI9341_RDDST = 0x09
|
||||
|
||||
ILI9341_SLPIN = 0x10
|
||||
ILI9341_SLPOUT = 0x11
|
||||
ILI9341_PTLON = 0x12
|
||||
ILI9341_NORON = 0x13
|
||||
|
||||
ILI9341_RDMODE = 0x0A
|
||||
ILI9341_RDMADCTL = 0x0B
|
||||
ILI9341_RDPIXFMT = 0x0C
|
||||
ILI9341_RDIMGFMT = 0x0A
|
||||
ILI9341_RDSELFDIAG = 0x0F
|
||||
|
||||
ILI9341_INVOFF = 0x20
|
||||
ILI9341_INVON = 0x21
|
||||
ILI9341_GAMMASET = 0x26
|
||||
ILI9341_DISPOFF = 0x28
|
||||
ILI9341_DISPON = 0x29
|
||||
|
||||
ILI9341_CASET = 0x2A
|
||||
ILI9341_PASET = 0x2B
|
||||
ILI9341_RAMWR = 0x2C
|
||||
ILI9341_RAMRD = 0x2E
|
||||
|
||||
ILI9341_PTLAR = 0x30
|
||||
ILI9341_MADCTL = 0x36
|
||||
ILI9341_PIXFMT = 0x3A
|
||||
|
||||
ILI9341_FRMCTR1 = 0xB1
|
||||
ILI9341_FRMCTR2 = 0xB2
|
||||
ILI9341_FRMCTR3 = 0xB3
|
||||
ILI9341_INVCTR = 0xB4
|
||||
ILI9341_DFUNCTR = 0xB6
|
||||
|
||||
ILI9341_PWCTR1 = 0xC0
|
||||
ILI9341_PWCTR2 = 0xC1
|
||||
ILI9341_PWCTR3 = 0xC2
|
||||
ILI9341_PWCTR4 = 0xC3
|
||||
ILI9341_PWCTR5 = 0xC4
|
||||
ILI9341_VMCTR1 = 0xC5
|
||||
ILI9341_VMCTR2 = 0xC7
|
||||
|
||||
ILI9341_RDID1 = 0xDA
|
||||
ILI9341_RDID2 = 0xDB
|
||||
ILI9341_RDID3 = 0xDC
|
||||
ILI9341_RDID4 = 0xDD
|
||||
|
||||
ILI9341_GMCTRP1 = 0xE0
|
||||
ILI9341_GMCTRN1 = 0xE1
|
||||
|
||||
ILI9341_PWCTR6 = 0xFC
|
||||
|
||||
ILI9341_BLACK = 0x0000
|
||||
ILI9341_BLUE = 0x001F
|
||||
ILI9341_RED = 0xF800
|
||||
ILI9341_GREEN = 0x07E0
|
||||
ILI9341_CYAN = 0x07FF
|
||||
ILI9341_MAGENTA = 0xF81F
|
||||
ILI9341_YELLOW = 0xFFE0
|
||||
ILI9341_WHITE = 0xFFFF
|
||||
|
||||
|
||||
def color565(r, g, b):
|
||||
"""Convert red, green, blue components to a 16-bit 565 RGB value. Components
|
||||
should be values 0 to 255.
|
||||
"""
|
||||
return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
|
||||
|
||||
def image_to_data(image):
|
||||
"""Generator function to convert a PIL image to 16-bit 565 RGB bytes."""
|
||||
#NumPy is much faster at doing this. NumPy code provided by:
|
||||
#Keith (https://www.blogger.com/profile/02555547344016007163)
|
||||
# pb = np.array(image.convert('RGB')).astype('uint16')
|
||||
# color = ((pb[:,:,0] & 0xF8) << 8) | ((pb[:,:,1] & 0xFC) << 3) | (pb[:,:,2] >> 3)
|
||||
# return np.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
|
||||
|
||||
# convert 24-bit RGB-8:8:8 to gBRG-3:5:5:3 ("BGR;16"):
|
||||
# 3 highest bits of green + 5 highest bits of blue in the first byte and
|
||||
# 5 highest bits of red + the next 3 highest bits of green (not yet expressed) in the second byte.
|
||||
# Then per-pixel byteswap to 16-bit RGB-5:6:5
|
||||
# This approach was measured to be ~3.4x faster than the numpy code above.
|
||||
arr = array.array("H", image.convert("BGR;16").tobytes())
|
||||
arr.byteswap()
|
||||
return arr.tobytes()
|
||||
|
||||
|
||||
class ILI9341(object):
|
||||
"""Representation of an ILI9341 TFT LCD."""
|
||||
|
||||
def __init__(self, dc=22, rst=13, led=12, width=ILI9341_TFTWIDTH,
|
||||
height=ILI9341_TFTHEIGHT, rotation=90):
|
||||
"""Create an instance of the display using SPI communication. Must
|
||||
provide the GPIO pin number for the D/C pin and the SPI driver. Can
|
||||
optionally provide the GPIO pin number for the reset pin as the rst
|
||||
parameter.
|
||||
"""
|
||||
spi = SpiDev(0, 0)
|
||||
# spi.mode = 0b10 # [CPOL|CPHA] -> polarity 1, phase 0
|
||||
spi.max_speed_hz = 64_000_000
|
||||
|
||||
self._dc = dc
|
||||
self._rst = rst
|
||||
self._spi = spi
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.rotation = rotation
|
||||
self.inverted = False
|
||||
# if self._gpio is None:
|
||||
# self._gpio = GPIO.get_platform_gpio()
|
||||
# Set DC as output.
|
||||
|
||||
GPIO.setmode(GPIO.BOARD) # Use physical pin nums, not gpio labels
|
||||
GPIO.setwarnings(False)
|
||||
GPIO.setup(self._dc, GPIO.OUT)
|
||||
GPIO.output(self._dc, GPIO.HIGH)
|
||||
GPIO.setup(led, GPIO.OUT)
|
||||
GPIO.output(led, GPIO.HIGH)
|
||||
if self._rst is not None:
|
||||
GPIO.setup(self._rst, GPIO.OUT)
|
||||
GPIO.output(self._rst, GPIO.HIGH)
|
||||
|
||||
# Create an image buffer.
|
||||
self.buffer = Image.new('RGB', (width, height))
|
||||
|
||||
# @property
|
||||
# def width(self):
|
||||
# return self.width
|
||||
|
||||
# @property
|
||||
# def height(self):
|
||||
# return self.height
|
||||
|
||||
|
||||
def send(self, data, is_data=True, chunk_size=4096):
|
||||
"""Write a byte or array of bytes to the display. Is_data parameter
|
||||
controls if byte should be interpreted as display data (True) or command
|
||||
data (False). Chunk_size is an optional size of bytes to write in a
|
||||
single SPI transaction, with a default of 4096.
|
||||
"""
|
||||
# Set DC low for command, high for data.
|
||||
GPIO.output(self._dc, is_data)
|
||||
# Convert scalar argument to list so either can be passed as parameter.
|
||||
if isinstance(data, numbers.Number):
|
||||
data = [data & 0xFF]
|
||||
# Write data a chunk at a time.
|
||||
# for start in range(0, len(data), chunk_size):
|
||||
# end = min(start+chunk_size, len(data))
|
||||
# self._spi.writebytes2(data[start:end])
|
||||
|
||||
self._spi.writebytes2(data)
|
||||
|
||||
def command(self, data):
|
||||
"""Write a byte or array of bytes to the display as command data."""
|
||||
self.send(data, False)
|
||||
|
||||
def data(self, data):
|
||||
"""Write a byte or array of bytes to the display as display data."""
|
||||
self.send(data, True)
|
||||
|
||||
def reset(self):
|
||||
"""Reset the display, if reset pin is connected."""
|
||||
if self._rst is not None:
|
||||
GPIO.output(self._rst, GPIO.HIGH)
|
||||
time.sleep(0.005)
|
||||
GPIO.output(self._rst, GPIO.LOW)
|
||||
time.sleep(0.02)
|
||||
GPIO.output(self._rst, GPIO.HIGH)
|
||||
time.sleep(0.150)
|
||||
|
||||
def _init(self):
|
||||
# Initialize the display. Broken out as a separate function so it can
|
||||
# be overridden by other displays in the future.
|
||||
self.command(0xEF)
|
||||
self.data(0x03)
|
||||
self.data(0x80)
|
||||
self.data(0x02)
|
||||
self.command(0xCF)
|
||||
self.data(0x00)
|
||||
self.data(0XC1)
|
||||
self.data(0X30)
|
||||
self.command(0xED)
|
||||
self.data(0x64)
|
||||
self.data(0x03)
|
||||
self.data(0X12)
|
||||
self.data(0X81)
|
||||
self.command(0xE8)
|
||||
self.data(0x85)
|
||||
self.data(0x00)
|
||||
self.data(0x78)
|
||||
self.command(0xCB)
|
||||
self.data(0x39)
|
||||
self.data(0x2C)
|
||||
self.data(0x00)
|
||||
self.data(0x34)
|
||||
self.data(0x02)
|
||||
self.command(0xF7)
|
||||
self.data(0x20)
|
||||
self.command(0xEA)
|
||||
self.data(0x00)
|
||||
self.data(0x00)
|
||||
self.command(ILI9341_PWCTR1) # Power control
|
||||
self.data(0x23) # VRH[5:0]
|
||||
self.command(ILI9341_PWCTR2) # Power control
|
||||
self.data(0x10) # SAP[2:0];BT[3:0]
|
||||
self.command(ILI9341_VMCTR1) # VCM control
|
||||
self.data(0x3e)
|
||||
self.data(0x28)
|
||||
self.command(ILI9341_VMCTR2) # VCM control2
|
||||
self.data(0x86) # --
|
||||
self.command(ILI9341_MADCTL) # Memory Access Control
|
||||
self.data(0x48)
|
||||
self.command(ILI9341_PIXFMT)
|
||||
self.data(0x55)
|
||||
self.command(ILI9341_FRMCTR1)
|
||||
self.data(0x00)
|
||||
self.data(0x18)
|
||||
self.command(ILI9341_DFUNCTR) # Display Function Control
|
||||
self.data(0x08)
|
||||
self.data(0x82)
|
||||
self.data(0x27)
|
||||
self.command(0xF2) # 3Gamma Function Disable
|
||||
self.data(0x00)
|
||||
self.command(ILI9341_GAMMASET) # Gamma curve selected
|
||||
self.data(0x01)
|
||||
self.command(ILI9341_GMCTRP1) # Set Gamma
|
||||
self.data(0x0F)
|
||||
self.data(0x31)
|
||||
self.data(0x2B)
|
||||
self.data(0x0C)
|
||||
self.data(0x0E)
|
||||
self.data(0x08)
|
||||
self.data(0x4E)
|
||||
self.data(0xF1)
|
||||
self.data(0x37)
|
||||
self.data(0x07)
|
||||
self.data(0x10)
|
||||
self.data(0x03)
|
||||
self.data(0x0E)
|
||||
self.data(0x09)
|
||||
self.data(0x00)
|
||||
self.command(ILI9341_GMCTRN1) # Set Gamma
|
||||
self.data(0x00)
|
||||
self.data(0x0E)
|
||||
self.data(0x14)
|
||||
self.data(0x03)
|
||||
self.data(0x11)
|
||||
self.data(0x07)
|
||||
self.data(0x31)
|
||||
self.data(0xC1)
|
||||
self.data(0x48)
|
||||
self.data(0x08)
|
||||
self.data(0x0F)
|
||||
self.data(0x0C)
|
||||
self.data(0x31)
|
||||
self.data(0x36)
|
||||
self.data(0x0F)
|
||||
self.command(ILI9341_SLPOUT) # Exit Sleep
|
||||
time.sleep(0.120)
|
||||
self.command(ILI9341_DISPON) # Display on
|
||||
|
||||
def begin(self):
|
||||
"""Initialize the display. Should be called once before other calls that
|
||||
interact with the display are called.
|
||||
"""
|
||||
self.reset()
|
||||
self._init()
|
||||
|
||||
def invert(self, state: bool = True):
|
||||
"""Sets display inversion to the specified state. If not provided, state
|
||||
is True, which inverts the display. If state is False, the display turns
|
||||
back into normal mode."""
|
||||
if state:
|
||||
self.command(ILI9341_INVON)
|
||||
else:
|
||||
self.command(ILI9341_INVOFF)
|
||||
self.inverted = state
|
||||
return self
|
||||
|
||||
def set_window(self, x0=0, y0=0, x1=None, y1=None):
|
||||
"""Set the pixel address window for proceeding drawing commands. x0 and
|
||||
x1 should define the minimum and maximum x pixel bounds. y0 and y1
|
||||
should define the minimum and maximum y pixel bound. If no parameters
|
||||
are specified the default will be to update the entire display from 0,0
|
||||
to 239,319.
|
||||
"""
|
||||
if x1 is None:
|
||||
x1 = self.width-1
|
||||
if y1 is None:
|
||||
y1 = self.height-1
|
||||
self.command(ILI9341_CASET) # Column addr set
|
||||
self.data(x0 >> 8)
|
||||
self.data(x0) # XSTART
|
||||
self.data(x1 >> 8)
|
||||
self.data(x1) # XEND
|
||||
self.command(ILI9341_PASET) # Row addr set
|
||||
self.data(y0 >> 8)
|
||||
self.data(y0) # YSTART
|
||||
self.data(y1 >> 8)
|
||||
self.data(y1) # YEND
|
||||
self.command(ILI9341_RAMWR) # write to RAM
|
||||
|
||||
def show_image(self, image=None, x_start: int = 0, y_start: int = 0):
|
||||
"""Write the display buffer or provided image to the hardware. If no
|
||||
image parameter is provided the display buffer will be written to the
|
||||
hardware. If an image is provided, it should be RGB format and the
|
||||
same dimensions as the display hardware.
|
||||
"""
|
||||
# By default write the internal buffer to the display.
|
||||
if image is None:
|
||||
image = self.buffer
|
||||
|
||||
output_image = image.rotate(self.rotation, expand=True)
|
||||
self.set_window(x_start, y_start, x_start + output_image.width - 1, y_start + output_image.height - 1)
|
||||
|
||||
# Convert image to array of 16bit 565 RGB data bytes.
|
||||
# Unfortunate that this copy has to occur, but the SPI byte writing
|
||||
# function needs to take an array of bytes and PIL doesn't natively
|
||||
# store images in 16-bit 565 RGB format.
|
||||
pixelbytes = image_to_data(output_image)
|
||||
|
||||
# Write data to hardware.
|
||||
self.data(pixelbytes)
|
||||
|
||||
def clear(self, color=(0,0,0)):
|
||||
"""Clear the image buffer to the specified RGB color (default black)."""
|
||||
width, height = self.buffer.size
|
||||
self.buffer.putdata([color]*(width*height))
|
||||
|
||||
def draw(self):
|
||||
"""Return a PIL ImageDraw instance for 2D drawing on the image buffer."""
|
||||
return ImageDraw.Draw(self.buffer)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,6 @@ import time
|
||||
|
||||
from seedsigner.models.singleton import Singleton
|
||||
from seedsigner.models.threads import BaseThread
|
||||
from seedsigner.models.settings import Settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -33,6 +32,8 @@ class MicroSD(Singleton, BaseThread):
|
||||
|
||||
@property
|
||||
def is_inserted(self):
|
||||
from seedsigner.models.settings import Settings # Import here to avoid circular import issues
|
||||
|
||||
if Settings.HOSTNAME == Settings.SEEDSIGNER_OS:
|
||||
return os.path.exists(MicroSD.MOUNT_POINT)
|
||||
else:
|
||||
@@ -47,6 +48,7 @@ class MicroSD(Singleton, BaseThread):
|
||||
def run(self):
|
||||
from seedsigner.controller import Controller
|
||||
from seedsigner.gui.toast import SDCardStateChangeToastManagerThread
|
||||
from seedsigner.models.settings import Settings # Import here to avoid circular import issues
|
||||
action = ""
|
||||
|
||||
# explicitly only microsd add/remove detection in seedsigner-os
|
||||
|
||||
@@ -123,7 +123,8 @@ class Settings(Singleton):
|
||||
|
||||
|
||||
def save(self):
|
||||
if self._data[SettingsConstants.SETTING__PERSISTENT_SETTINGS] == SettingsConstants.OPTION__ENABLED:
|
||||
from seedsigner.hardware.microsd import MicroSD
|
||||
if self._data[SettingsConstants.SETTING__PERSISTENT_SETTINGS] == SettingsConstants.OPTION__ENABLED and MicroSD.get_instance().is_inserted:
|
||||
with open(Settings.SETTINGS_FILENAME, 'w') as settings_file:
|
||||
json.dump(self._data, settings_file, indent=4)
|
||||
# SeedSignerOS makes removing the microsd possible, flush and then fsync forces persistent settings to disk
|
||||
@@ -191,13 +192,16 @@ class Settings(Singleton):
|
||||
self.load_locale()
|
||||
|
||||
|
||||
def get_value(self, attr_name: str):
|
||||
def get_value(self, attr_name: str, default_if_none: bool = None):
|
||||
"""
|
||||
Returns the attr's current value.
|
||||
|
||||
Note that for multiselect, the current value is a List.
|
||||
"""
|
||||
if attr_name not in self._data:
|
||||
if default_if_none:
|
||||
return SettingsDefinition.get_settings_entry(attr_name).default_value
|
||||
|
||||
raise Exception(f"Setting for {attr_name} not found")
|
||||
return self._data[attr_name]
|
||||
|
||||
|
||||
@@ -320,6 +320,9 @@ class SettingsConstants:
|
||||
SETTING__COORDINATORS = "coordinators"
|
||||
SETTING__BTC_DENOMINATION = "denomination"
|
||||
|
||||
SETTING__DISPLAY_CONFIGURATION = "display_config"
|
||||
SETTING__DISPLAY_COLOR_INVERTED = "color_inverted"
|
||||
|
||||
SETTING__NETWORK = "network"
|
||||
SETTING__QR_DENSITY = "qr_density"
|
||||
SETTING__XPUB_EXPORT = "xpub_export"
|
||||
@@ -339,6 +342,20 @@ class SettingsConstants:
|
||||
|
||||
SETTING__DEBUG = "debug"
|
||||
|
||||
|
||||
# Hardware config settings
|
||||
DISPLAY_CONFIGURATION__ST7789__240x240 = "st7789_240x240" # default; original Waveshare 1.3" display hat
|
||||
DISPLAY_CONFIGURATION__ST7789__320x240 = "st7789_320x240" # natively portrait dimensions; we apply a 90° rotation
|
||||
DISPLAY_CONFIGURATION__ILI9341__320x240 = "ili9341_320x240" # natively portrait dimensions; we apply a 90° rotation
|
||||
DISPLAY_CONFIGURATION__ILI9486__480x320 = "ili9486_480x320" # natively portrait dimensions; we apply a 90° rotation
|
||||
ALL_DISPLAY_CONFIGURATIONS = [
|
||||
(DISPLAY_CONFIGURATION__ST7789__240x240, "st7789 240x240"),
|
||||
(DISPLAY_CONFIGURATION__ST7789__320x240, "st7789 320x240"),
|
||||
(DISPLAY_CONFIGURATION__ILI9341__320x240, "ili9341 320x240 (beta)"),
|
||||
# (DISPLAY_CONFIGURATION__ILI9486__320x480, "ili9486 480x320"), # TODO: Enable when ili9486 driver performance is improved
|
||||
]
|
||||
|
||||
|
||||
# Hidden settings
|
||||
SETTING__QR_BRIGHTNESS = "qr_background_color"
|
||||
|
||||
@@ -352,6 +369,7 @@ class SettingsConstants:
|
||||
|
||||
VISIBILITY__GENERAL = "general"
|
||||
VISIBILITY__ADVANCED = "advanced"
|
||||
VISIBILITY__HARDWARE = "hardware"
|
||||
VISIBILITY__DEVELOPER = "developer"
|
||||
VISIBILITY__HIDDEN = "hidden" # For data-only (e.g. custom_derivation), not configurable by the user
|
||||
|
||||
@@ -681,6 +699,26 @@ class SettingsDefinition:
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
default_value=SettingsConstants.OPTION__ENABLED),
|
||||
|
||||
|
||||
# Hardware config
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM,
|
||||
attr_name=SettingsConstants.SETTING__DISPLAY_CONFIGURATION,
|
||||
abbreviated_name="disp_conf",
|
||||
display_name="Display type",
|
||||
type=SettingsConstants.TYPE__SELECT_1,
|
||||
visibility=SettingsConstants.VISIBILITY__HARDWARE,
|
||||
selection_options=SettingsConstants.ALL_DISPLAY_CONFIGURATIONS,
|
||||
default_value=SettingsConstants.DISPLAY_CONFIGURATION__ST7789__240x240),
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM,
|
||||
attr_name=SettingsConstants.SETTING__DISPLAY_COLOR_INVERTED,
|
||||
abbreviated_name="rgb_inv",
|
||||
display_name="Invert colors",
|
||||
type=SettingsConstants.TYPE__ENABLED_DISABLED,
|
||||
visibility=SettingsConstants.VISIBILITY__HARDWARE,
|
||||
default_value=SettingsConstants.OPTION__DISABLED),
|
||||
|
||||
|
||||
# Developer options
|
||||
# TODO: No real Developer options needed yet. Disable for now.
|
||||
# SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM,
|
||||
|
||||
@@ -77,6 +77,8 @@ class OpeningSplashScreen(LogoScreen):
|
||||
if self.force_partner_logos is not None:
|
||||
show_partner_logos = self.force_partner_logos
|
||||
|
||||
logo_offset_x = int((self.canvas_width - self.logo.width)/2)
|
||||
|
||||
if show_partner_logos:
|
||||
logo_offset_y = -56
|
||||
else:
|
||||
@@ -87,11 +89,14 @@ class OpeningSplashScreen(LogoScreen):
|
||||
# Fade in alpha
|
||||
for i in range(250, -1, -25):
|
||||
self.logo.putalpha(255 - i)
|
||||
self.renderer.canvas.paste(Image.alpha_composite(background, self.logo), (0, logo_offset_y))
|
||||
self.renderer.canvas.paste(
|
||||
Image.alpha_composite(background, self.logo),
|
||||
(logo_offset_x, logo_offset_y)
|
||||
)
|
||||
self.renderer.show_image()
|
||||
else:
|
||||
# Skip animation for the screenshot generator
|
||||
self.renderer.canvas.paste(self.logo, (0, logo_offset_y))
|
||||
self.renderer.canvas.paste(self.logo, (logo_offset_x, logo_offset_y))
|
||||
|
||||
# Display version num below SeedSigner logo
|
||||
font = Fonts.get_font(GUIConstants.get_body_font_name(), GUIConstants.get_top_nav_title_font_size())
|
||||
@@ -144,17 +149,24 @@ class ScreensaverScreen(LogoScreen):
|
||||
|
||||
self.buttons = buttons
|
||||
|
||||
# Paste the logo in a bigger image that is 2x the size of the logo
|
||||
self.image = Image.new("RGB", (2 * self.logo.size[0], 2 * self.logo.size[1]), (0,0,0))
|
||||
self.image.paste(self.logo, (int(self.logo.size[0] / 2), int(self.logo.size[1] / 2)))
|
||||
# Paste the logo in a bigger image that is the canvas + the logo dims (half the
|
||||
# logo will render off the canvas at each edge).
|
||||
self.image = Image.new("RGB", (self.renderer.canvas_width + self.logo.width, self.renderer.canvas_height + self.logo.height), (0,0,0))
|
||||
|
||||
# Place the logo centered on the larger image
|
||||
logo_x = int((self.image.width - self.logo.width) / 2)
|
||||
logo_y = int((self.image.height - self.logo.height) / 2)
|
||||
self.image.paste(self.logo, (logo_x, logo_y))
|
||||
|
||||
self.min_coords = (0, 0)
|
||||
self.max_coords = (self.logo.size[0], self.logo.size[1])
|
||||
self.max_coords = (self.renderer.canvas_width, self.renderer.canvas_height)
|
||||
|
||||
# Update our first rendering position so we're centered
|
||||
self.cur_x = int(self.logo.width / 2)
|
||||
self.cur_y = int(self.logo.height / 2)
|
||||
|
||||
self.increment_x = self.rand_increment()
|
||||
self.increment_y = self.rand_increment()
|
||||
self.cur_x = int(self.logo.size[0] / 2)
|
||||
self.cur_y = int(self.logo.size[1] / 2)
|
||||
|
||||
self._is_running = False
|
||||
self.last_screen = None
|
||||
@@ -197,7 +209,7 @@ class ScreensaverScreen(LogoScreen):
|
||||
crop = self.image.crop((
|
||||
self.cur_x, self.cur_y,
|
||||
self.cur_x + self.renderer.canvas_width, self.cur_y + self.renderer.canvas_height))
|
||||
self.renderer.disp.ShowImage(crop, 0, 0)
|
||||
self.renderer.disp.show_image(crop, 0, 0)
|
||||
|
||||
self.cur_x += self.increment_x
|
||||
self.cur_y += self.increment_y
|
||||
|
||||
@@ -1558,16 +1558,16 @@ class SeedTranscribeSeedQRWholeQRView(View):
|
||||
|
||||
class SeedTranscribeSeedQRZoomedInView(View):
|
||||
"""
|
||||
intial_block_x, initial_block_y: Used by the screenshot generator to shift the view
|
||||
intial_zone_x, initial_zone_y: Used by the screenshot generator to shift the view
|
||||
to a more interesting part of the QR code template.
|
||||
"""
|
||||
def __init__(self, seed_num: int, seedqr_format: str, initial_block_x: int = 0, initial_block_y: int = 0):
|
||||
def __init__(self, seed_num: int, seedqr_format: str, initial_zone_x: int = 0, initial_zone_y: int = 0):
|
||||
super().__init__()
|
||||
self.seed_num = seed_num
|
||||
self.seedqr_format = seedqr_format
|
||||
self.seed = self.controller.get_seed(seed_num)
|
||||
self.initial_block_x = initial_block_x
|
||||
self.initial_block_y = initial_block_y
|
||||
self.initial_zone_x = initial_zone_x
|
||||
self.initial_zone_y = initial_zone_y
|
||||
|
||||
|
||||
def run(self):
|
||||
@@ -1594,8 +1594,8 @@ class SeedTranscribeSeedQRZoomedInView(View):
|
||||
seed_screens.SeedTranscribeSeedQRZoomedInScreen(
|
||||
qr_data=data,
|
||||
num_modules=num_modules,
|
||||
initial_block_x=self.initial_block_x,
|
||||
initial_block_y=self.initial_block_y,
|
||||
initial_zone_x=self.initial_zone_x,
|
||||
initial_zone_y=self.initial_zone_y,
|
||||
).display()
|
||||
|
||||
return Destination(SeedTranscribeSeedQRConfirmQRPromptView, view_args={"seed_num": self.seed_num})
|
||||
|
||||
@@ -13,6 +13,8 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SettingsMenuView(View):
|
||||
ADVANCED = ButtonOption("Advanced", right_icon_name=SeedSignerIconConstants.CHEVRON_RIGHT)
|
||||
HARDWARE = ButtonOption("Hardware", right_icon_name=SeedSignerIconConstants.CHEVRON_RIGHT)
|
||||
IO_TEST = ButtonOption("I/O test")
|
||||
DONATE = ButtonOption("Donate")
|
||||
|
||||
@@ -29,7 +31,7 @@ class SettingsMenuView(View):
|
||||
settings_entries = SettingsDefinition.get_settings_entries(
|
||||
visibility=self.visibility
|
||||
)
|
||||
button_data=[ButtonOption(e.display_name) for e in settings_entries]
|
||||
button_data: list[ButtonOption] = [ButtonOption(e.display_name) for e in settings_entries]
|
||||
|
||||
selected_button = 0
|
||||
if self.selected_attr:
|
||||
@@ -42,7 +44,7 @@ class SettingsMenuView(View):
|
||||
title = _("Settings")
|
||||
|
||||
# Set up the next nested level of menuing
|
||||
button_data.append(ButtonOption("Advanced", right_icon_name=SeedSignerIconConstants.CHEVRON_RIGHT))
|
||||
button_data.append(self.ADVANCED)
|
||||
next_destination = Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__ADVANCED})
|
||||
|
||||
button_data.append(self.IO_TEST)
|
||||
@@ -51,11 +53,14 @@ class SettingsMenuView(View):
|
||||
elif self.visibility == SettingsConstants.VISIBILITY__ADVANCED:
|
||||
title = _("Advanced")
|
||||
|
||||
# So far there are no real Developer options; disabling for now
|
||||
# button_data.append(("Developer Options", None, None, None, SeedSignerIconConstants.CHEVRON_RIGHT))
|
||||
# next_destination = Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__DEVELOPER})
|
||||
# The hardware options nest below "Advanced"
|
||||
button_data.append(self.HARDWARE)
|
||||
next_destination = Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__HARDWARE})
|
||||
|
||||
elif self.visibility == SettingsConstants.VISIBILITY__HARDWARE:
|
||||
title = "Hardware"
|
||||
next_destination = None
|
||||
|
||||
|
||||
elif self.visibility == SettingsConstants.VISIBILITY__DEVELOPER:
|
||||
title = _("Dev Options")
|
||||
next_destination = None
|
||||
@@ -80,7 +85,10 @@ class SettingsMenuView(View):
|
||||
else:
|
||||
return Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__ADVANCED})
|
||||
|
||||
elif selected_menu_num == len(settings_entries):
|
||||
if button_data[selected_menu_num] == self.ADVANCED:
|
||||
return next_destination
|
||||
|
||||
elif button_data[selected_menu_num] == self.HARDWARE:
|
||||
return next_destination
|
||||
|
||||
elif button_data[selected_menu_num] == self.IO_TEST:
|
||||
@@ -222,6 +230,12 @@ class SettingsEntryUpdateSelectionView(View):
|
||||
value=updated_value
|
||||
)
|
||||
|
||||
if self.settings_entry.attr_name == SettingsConstants.SETTING__DISPLAY_CONFIGURATION:
|
||||
self.renderer.initialize_display()
|
||||
|
||||
elif self.settings_entry.attr_name == SettingsConstants.SETTING__DISPLAY_COLOR_INVERTED:
|
||||
self.renderer.disp.invert(enabled=updated_value == SettingsConstants.OPTION__ENABLED)
|
||||
|
||||
if destination:
|
||||
return destination
|
||||
|
||||
@@ -240,9 +254,16 @@ class SettingsIngestSettingsQRView(View):
|
||||
# May raise an Exception which will bubble up to the Controller to display to the
|
||||
# user.
|
||||
self.config_name, settings_update_dict = Settings.parse_settingsqr(data)
|
||||
|
||||
changes_display_driver = (
|
||||
SettingsConstants.SETTING__DISPLAY_CONFIGURATION in settings_update_dict and
|
||||
self.settings.get_value(SettingsConstants.SETTING__DISPLAY_CONFIGURATION) != settings_update_dict[SettingsConstants.SETTING__DISPLAY_CONFIGURATION])
|
||||
|
||||
self.settings.update(settings_update_dict)
|
||||
|
||||
if changes_display_driver:
|
||||
self.renderer.initialize_display()
|
||||
|
||||
if MicroSD.get_instance().is_inserted and self.settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__ENABLED:
|
||||
self.status_message = _("Persistent Settings enabled. Settings saved to SD card.")
|
||||
else:
|
||||
|
||||
@@ -5,7 +5,7 @@ import time
|
||||
|
||||
from gettext import gettext as _
|
||||
|
||||
from seedsigner.gui.components import FontAwesomeIconConstants, GUIConstants, SeedSignerIconConstants
|
||||
from seedsigner.gui.components import FontAwesomeIconConstants, GUIConstants, SeedSignerIconConstants, resize_image_to_fill
|
||||
from seedsigner.gui.screens import RET_CODE__BACK_BUTTON, ButtonListScreen
|
||||
from seedsigner.gui.screens.screen import ButtonOption
|
||||
from seedsigner.helpers import mnemonic_generation
|
||||
@@ -83,21 +83,25 @@ class ToolsImageEntropyFinalImageView(View):
|
||||
from seedsigner.hardware.camera import Camera
|
||||
# Take the final full-res image
|
||||
camera = Camera.get_instance()
|
||||
camera.start_single_frame_mode(resolution=(720, 480))
|
||||
max_dim = max(self.canvas_width, self.canvas_height)
|
||||
|
||||
# Final image will be at least 4x the number of pixels the screen can
|
||||
# actually display.
|
||||
camera.start_single_frame_mode(resolution=(2*max_dim, 2*max_dim))
|
||||
|
||||
time.sleep(0.25)
|
||||
self.controller.image_entropy_final_image = camera.capture_frame()
|
||||
camera.stop_single_frame_mode()
|
||||
|
||||
# Prep a copy of the image for display. The actual image data is 720x480
|
||||
# Present just a center crop and resize it to fit the screen and to keep some of
|
||||
# the data hidden.
|
||||
display_version = autocontrast(
|
||||
self.controller.image_entropy_final_image,
|
||||
cutoff=2
|
||||
).crop(
|
||||
(120, 0, 600, 480)
|
||||
).resize(
|
||||
(self.canvas_width, self.canvas_height), Image.Resampling.BICUBIC
|
||||
# Prep a copy of the image for display:
|
||||
# * Boost the contrast for better presentation (but preserve the original pixels)
|
||||
# * Resize it to fit the screen
|
||||
boosted_version = autocontrast(self.controller.image_entropy_final_image, cutoff=2)
|
||||
display_version = resize_image_to_fill(
|
||||
boosted_version,
|
||||
target_size_x=self.canvas_width,
|
||||
target_size_y=self.canvas_height,
|
||||
sampling_method=Image.Resampling.BICUBIC,
|
||||
)
|
||||
|
||||
ret = ToolsImageEntropyFinalImageScreen(
|
||||
@@ -619,6 +623,7 @@ class ToolsAddressExplorerAddressListView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.tools_screens import ToolsAddressExplorerAddressListScreen
|
||||
self.loading_screen = None
|
||||
|
||||
addresses = []
|
||||
@@ -672,31 +677,11 @@ class ToolsAddressExplorerAddressListView(View):
|
||||
# Everything is set. Stop the loading screen
|
||||
self.loading_screen.stop()
|
||||
|
||||
for i, address in enumerate(addresses):
|
||||
cur_index = i + self.start_index
|
||||
|
||||
# Adjust the trailing addr display length based on available room
|
||||
# (the index number will push it out on each order of magnitude)
|
||||
if cur_index < 10:
|
||||
end_digits = -6
|
||||
elif cur_index < 100:
|
||||
end_digits = -5
|
||||
else:
|
||||
end_digits = -4
|
||||
button_data.append(ButtonOption(f"{cur_index}:{address[:8]}...{address[end_digits:]}", active_button_label=f"{cur_index}:{address}"))
|
||||
|
||||
# TRANSLATOR_NOTE: Insert the number of addrs displayed per screen (e.g. "Next 10")
|
||||
button_label = _("Next {}").format(addrs_per_screen)
|
||||
button_data.append(ButtonOption(button_label, right_icon_name=SeedSignerIconConstants.CHEVRON_RIGHT))
|
||||
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
ToolsAddressExplorerAddressListScreen,
|
||||
title=_("Receive Addrs") if not self.is_change else _("Change Addrs"),
|
||||
button_data=button_data,
|
||||
button_font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME,
|
||||
button_font_size=GUIConstants.get_button_font_size() + 4,
|
||||
is_button_text_centered=False,
|
||||
is_bottom_list=True,
|
||||
start_index=self.start_index,
|
||||
addresses=addresses,
|
||||
selected_button=self.selected_button_index,
|
||||
scroll_y_initial_offset=self.initial_scroll,
|
||||
)
|
||||
|
||||
@@ -11,6 +11,8 @@ sys.modules['seedsigner.gui.toast'] = MagicMock()
|
||||
sys.modules['seedsigner.views.screensaver'] = MagicMock()
|
||||
sys.modules['seedsigner.hardware.buttons'] = MagicMock()
|
||||
sys.modules['seedsigner.hardware.camera'] = MagicMock()
|
||||
sys.modules['seedsigner.hardware.st7789_mpy'] = MagicMock()
|
||||
sys.modules['seedsigner.hardware.ili9341'] = MagicMock()
|
||||
|
||||
from seedsigner.controller import Controller, FlowBasedTestException, StopFlowBasedTest
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, RET_CODE__POWER_BUTTON, ButtonOption
|
||||
|
||||
@@ -13,7 +13,8 @@ from embit.script import Script
|
||||
|
||||
# Prevent importing modules w/Raspi hardware dependencies.
|
||||
# These must precede any SeedSigner imports.
|
||||
sys.modules['seedsigner.hardware.ST7789'] = MagicMock()
|
||||
sys.modules['seedsigner.hardware.displays.st7789_mpy'] = MagicMock()
|
||||
sys.modules['seedsigner.hardware.displays.ili9341'] = MagicMock()
|
||||
sys.modules['seedsigner.views.screensaver.ScreensaverScreen'] = MagicMock()
|
||||
sys.modules['RPi'] = MagicMock()
|
||||
sys.modules['RPi.GPIO'] = MagicMock()
|
||||
@@ -24,6 +25,7 @@ sys.modules['seedsigner.hardware.microsd'] = MagicMock()
|
||||
patch('PIL.ImageFont.core.HAVE_RAQM', False).start()
|
||||
|
||||
from seedsigner.controller import Controller
|
||||
from seedsigner.gui.components import GUIConstants
|
||||
from seedsigner.gui.renderer import Renderer
|
||||
from seedsigner.gui.screens.seed_screens import SeedAddPassphraseScreen
|
||||
from seedsigner.gui.toast import RemoveSDCardToastManagerThread, SDCardStateChangeToastManagerThread
|
||||
@@ -160,40 +162,57 @@ def generate_screenshots(locale):
|
||||
"addr_format": embit_utils.parse_derivation_path(derivation_path)
|
||||
}
|
||||
|
||||
# Automatically populate all Settings options Views
|
||||
settings_views_list = []
|
||||
settings_views_list.append(ScreenshotConfig(settings_views.SettingsMenuView))
|
||||
settings_views_list.append(
|
||||
ScreenshotConfig(
|
||||
settings_views.SettingsMenuView,
|
||||
dict(
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
selected_attr=SettingsConstants.SETTING__ELECTRUM_SEEDS,
|
||||
),
|
||||
screenshot_name="SettingsMenuView__Advanced"
|
||||
)
|
||||
)
|
||||
|
||||
# so we get a choice for transcribe seed qr format
|
||||
controller.settings.set_value(
|
||||
attr_name=SettingsConstants.SETTING__COMPACT_SEEDQR,
|
||||
value=SettingsConstants.OPTION__ENABLED
|
||||
)
|
||||
for settings_entry in SettingsDefinition.settings_entries:
|
||||
if settings_entry.visibility == SettingsConstants.VISIBILITY__HIDDEN:
|
||||
continue
|
||||
|
||||
if settings_entry.attr_name == SettingsConstants.SETTING__LOCALE:
|
||||
# Locale selection has its own dedicated View
|
||||
settings_views_list.append(ScreenshotConfig(settings_views.LocaleSelectionView))
|
||||
else:
|
||||
# Generic SettingsEntry selection View
|
||||
settings_views_list.append(ScreenshotConfig(settings_views.SettingsEntryUpdateSelectionView, dict(attr_name=settings_entry.attr_name), screenshot_name=f"SettingsEntryUpdateSelectionView_{settings_entry.attr_name}"))
|
||||
# Automatically populate all Settings options Views
|
||||
settings_views_list = []
|
||||
def add_settings_entries(visibility = SettingsConstants.VISIBILITY__GENERAL):
|
||||
for settings_entry in SettingsDefinition.settings_entries:
|
||||
if settings_entry.visibility != visibility:
|
||||
continue
|
||||
|
||||
if settings_entry.attr_name == SettingsConstants.SETTING__LOCALE:
|
||||
# Locale selection has its own dedicated View
|
||||
settings_views_list.append(ScreenshotConfig(settings_views.LocaleSelectionView))
|
||||
else:
|
||||
# Generic SettingsEntry selection View
|
||||
settings_views_list.append(ScreenshotConfig(settings_views.SettingsEntryUpdateSelectionView, dict(attr_name=settings_entry.attr_name), screenshot_name=f"SettingsEntryUpdateSelectionView_{settings_entry.attr_name}"))
|
||||
|
||||
# Add the top level "General" settings menu and entries
|
||||
settings_views_list.append(ScreenshotConfig(settings_views.SettingsMenuView))
|
||||
add_settings_entries(SettingsConstants.VISIBILITY__GENERAL)
|
||||
|
||||
# Add the "Advanced" menu...
|
||||
settings_views_list.append(
|
||||
ScreenshotConfig(
|
||||
settings_views.SettingsMenuView,
|
||||
dict(
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
),
|
||||
screenshot_name="SettingsMenuView__Advanced"
|
||||
)
|
||||
)
|
||||
|
||||
# ...and Advanced entries
|
||||
add_settings_entries(SettingsConstants.VISIBILITY__ADVANCED)
|
||||
|
||||
# Render the nested "Advanced" -> "Hardware" submenu
|
||||
settings_views_list.append(
|
||||
ScreenshotConfig(
|
||||
settings_views.SettingsMenuView,
|
||||
dict(visibility=SettingsConstants.VISIBILITY__HARDWARE),
|
||||
screenshot_name="SettingsMenuView__Hardware"
|
||||
)
|
||||
)
|
||||
add_settings_entries(SettingsConstants.VISIBILITY__HARDWARE)
|
||||
|
||||
settingsqr_data_persistent = f"settings::v1 name=English_noob_mode persistent=E coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E locale={locale}"
|
||||
settingsqr_data_not_persistent = f"settings::v1 name=Mode_Ephemeral persistent=D coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E locale={locale}"
|
||||
|
||||
|
||||
# Set up screenshot-specific callbacks to inject data before the View is run and
|
||||
# reset data after the View is run.
|
||||
def load_basic_psbt_cb():
|
||||
@@ -297,8 +316,8 @@ def generate_screenshots(locale):
|
||||
ScreenshotConfig(seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed_num=0, seedqr_format=QRType.SEED__SEEDQR, num_modules=25), screenshot_name="SeedTranscribeSeedQRWholeQRView_12_Standard"),
|
||||
ScreenshotConfig(seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed_num=2, seedqr_format=QRType.SEED__COMPACTSEEDQR, num_modules=25), screenshot_name="SeedTranscribeSeedQRWholeQRView_24_Compact"),
|
||||
ScreenshotConfig(seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed_num=2, seedqr_format=QRType.SEED__SEEDQR, num_modules=29), screenshot_name="SeedTranscribeSeedQRWholeQRView_24_Standard"),
|
||||
ScreenshotConfig(seed_views.SeedTranscribeSeedQRZoomedInView, dict(seed_num=0, seedqr_format=QRType.SEED__COMPACTSEEDQR, initial_block_x=1, initial_block_y=1), screenshot_name="SeedTranscribeSeedQRZoomedInView_12_Compact"),
|
||||
ScreenshotConfig(seed_views.SeedTranscribeSeedQRZoomedInView, dict(seed_num=0, seedqr_format=QRType.SEED__SEEDQR, initial_block_x=2, initial_block_y=2), screenshot_name="SeedTranscribeSeedQRZoomedInView_12_Standard"),
|
||||
ScreenshotConfig(seed_views.SeedTranscribeSeedQRZoomedInView, dict(seed_num=0, seedqr_format=QRType.SEED__COMPACTSEEDQR, initial_zone_x=1, initial_zone_y=1), screenshot_name="SeedTranscribeSeedQRZoomedInView_12_Compact"),
|
||||
ScreenshotConfig(seed_views.SeedTranscribeSeedQRZoomedInView, dict(seed_num=0, seedqr_format=QRType.SEED__SEEDQR, initial_zone_x=2, initial_zone_y=2), screenshot_name="SeedTranscribeSeedQRZoomedInView_12_Standard"),
|
||||
|
||||
ScreenshotConfig(seed_views.SeedTranscribeSeedQRConfirmQRPromptView, dict(seed_num=0)),
|
||||
ScreenshotConfig(seed_views.SeedTranscribeSeedQRConfirmWrongSeedView),
|
||||
|
||||
Reference in New Issue
Block a user