Merge pull request #741 from kdmukai/ili9341_320x240_v2

[New Feature] Support for 320x240 displays (st7789, ili9341)
This commit is contained in:
Nick Klockenga
2025-04-30 22:32:58 -04:00
committed by GitHub
20 changed files with 2109 additions and 383 deletions
+44
View File
@@ -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,
)
+39 -10
View File
@@ -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)
+3 -6
View File
@@ -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
+4 -2
View File
@@ -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:
+129 -102
View File
@@ -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
+78 -7
View File
@@ -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)
+376
View File
@@ -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
+3 -1
View File
@@ -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
+6 -2
View File
@@ -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,
+21 -9
View File
@@ -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
+6 -6
View File
@@ -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})
+28 -7
View File
@@ -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:
+20 -35
View File
@@ -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,
)
+2
View File
@@ -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
+46 -27
View File
@@ -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),