diff --git a/requirements.txt b/requirements.txt index 7055026a..1fb2f7b7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ embit==0.4.10 numpy==1.21.1 picamera==1.13 Pillow==8.2.0 --e git+https://github.com/kdmukai/pyzbar.git@c3c237821c6a20b17953efe59b90df0b514a1c03#egg=pyzbar +-e git+https://github.com/seedsigner/pyzbar.git@c3c237821c6a20b17953efe59b90df0b514a1c03#egg=pyzbar qrcode==7.3.1 RPi.GPIO==0.7.0 six==1.16.0 diff --git a/src/seedsigner/gui/components.py b/src/seedsigner/gui/components.py index 18bff593..ef1c6815 100644 --- a/src/seedsigner/gui/components.py +++ b/src/seedsigner/gui/components.py @@ -257,7 +257,7 @@ class TextArea(BaseComponent): screen_x: int = 0 screen_y: int = 0 min_text_x: int = None - background_color: str = "black" + background_color: str = GUIConstants.BACKGROUND_COLOR font_name: str = GUIConstants.BODY_FONT_NAME font_size: int = GUIConstants.BODY_FONT_SIZE font_color: str = GUIConstants.BODY_FONT_COLOR @@ -1168,6 +1168,7 @@ class IconButton(Button): icon_size: int = GUIConstants.ICON_INLINE_FONT_SIZE text: str = None is_icon_inline: bool = False + is_text_centered: bool = True diff --git a/src/seedsigner/gui/renderer.py b/src/seedsigner/gui/renderer.py index cd3d031c..1191eeec 100644 --- a/src/seedsigner/gui/renderer.py +++ b/src/seedsigner/gui/renderer.py @@ -11,8 +11,8 @@ class Renderer(ConfigurableSingleton): buttons = None canvas_width = 0 canvas_height = 0 - canvas: Image = None - draw: ImageDraw = None + canvas: Image.Image = None + draw: ImageDraw.ImageDraw = None disp = None lock = Lock() diff --git a/src/seedsigner/gui/screens/scan_screens.py b/src/seedsigner/gui/screens/scan_screens.py index 1677b49f..1b29af27 100644 --- a/src/seedsigner/gui/screens/scan_screens.py +++ b/src/seedsigner/gui/screens/scan_screens.py @@ -21,7 +21,8 @@ class ScanScreen(BaseScreen): instructions_text: str = "Scan a QR code" resolution: Tuple[int,int] = (480, 480) framerate: int = 12 - auto_deactivate_buttons: bool = False # Used by the I/O test screen + render_rect: Tuple[int,int,int,int] = None + def __post_init__(self): from seedsigner.hardware.camera import Camera @@ -36,25 +37,35 @@ class ScanScreen(BaseScreen): decoder=self.decoder, renderer=self.renderer, instructions_text=self.instructions_text, - components=self.components, - auto_deactivate_buttons=self.auto_deactivate_buttons, + render_rect=self.render_rect, )) class LivePreviewThread(BaseThread): - def __init__(self, camera: Camera, decoder: DecodeQR, renderer: renderer, instructions_text: str, components: List[BaseComponent], auto_deactivate_buttons: bool): + def __init__(self, camera: Camera, decoder: DecodeQR, renderer: renderer.Renderer, instructions_text: str, render_rect: Tuple[int,int,int,int]): self.camera = camera self.decoder = decoder self.renderer = renderer self.instructions_text = instructions_text - self.components = components - self.auto_deactivate_buttons = auto_deactivate_buttons + if render_rect: + self.render_rect = render_rect + else: + self.render_rect = (0, 0, self.renderer.canvas_width, self.renderer.canvas_height) + self.render_width = self.render_rect[2] - self.render_rect[0] + self.render_height = self.render_rect[3] - self.render_rect[1] + + print(f"render_width: {self.render_width}") + print(f"render_height: {self.render_height}") + super().__init__() def run(self): + from timeit import default_timer as timer + instructions_font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, GUIConstants.BUTTON_FONT_SIZE) while self.keep_running: + start = timer() frame = self.camera.read_video_stream(as_image=True) if frame is not None: scan_text = self.instructions_text @@ -62,40 +73,35 @@ class ScanScreen(BaseScreen): scan_text = str(self.decoder.get_percent_complete()) + "% Complete" with self.renderer.lock: - if frame.width > self.renderer.canvas_width or frame.height > self.renderer.canvas_height: + if frame.width > self.render_width or frame.height > self.render_height: frame = frame.resize( - (self.renderer.canvas_width, self.renderer.canvas_height) + (self.render_width, self.render_height) ) self.renderer.canvas.paste( frame, - (0, 0) + (self.render_rect[0], self.render_rect[1]) ) - self.renderer.draw.text( - xy=( - int(self.renderer.canvas_width/2), - self.renderer.canvas_height - GUIConstants.EDGE_PADDING - ), - text=scan_text, - fill=GUIConstants.BODY_FONT_COLOR, - font=instructions_font, - stroke_width=4, - stroke_fill=GUIConstants.BACKGROUND_COLOR, - anchor="ms" - ) - - # Need to re-render all onscreen UI components since we just overwrote - # the screen. - for component in self.components: - component.render() - if self.auto_deactivate_buttons and type(component) in [Button, IconButton]: - if component.is_selected: - # deactivate for the next round - component.is_selected = False + if scan_text: + self.renderer.draw.text( + xy=( + int(self.renderer.canvas_width/2), + self.renderer.canvas_height - GUIConstants.EDGE_PADDING + ), + text=scan_text, + fill=GUIConstants.BODY_FONT_COLOR, + font=instructions_font, + stroke_width=4, + stroke_fill=GUIConstants.BACKGROUND_COLOR, + anchor="ms" + ) self.renderer.show_image() - time.sleep(0.1) # turn this up or down to tune performance while decoding psbt + end = timer() + # print(f"{1.0/(end - start)} fps") # Time in seconds, e.g. 5.38091952400282 + + time.sleep(0.05) # turn this up or down to tune performance while decoding psbt if self.camera._video_stream is None: break diff --git a/src/seedsigner/gui/screens/settings_screens.py b/src/seedsigner/gui/screens/settings_screens.py index 7bfc729c..c09f1e27 100644 --- a/src/seedsigner/gui/screens/settings_screens.py +++ b/src/seedsigner/gui/screens/settings_screens.py @@ -1,12 +1,14 @@ import time from dataclasses import dataclass +from PIL.ImageOps import autocontrast from typing import List -from seedsigner.gui.components import Button, CheckboxButton, CheckedSelectionButton, FontAwesomeIconConstants, GUIConstants, IconButton, TextArea +from seedsigner.gui.components import Button, CheckboxButton, CheckedSelectionButton, FontAwesomeIconConstants, Fonts, GUIConstants, Icon, IconButton, IconTextLine, TextArea from seedsigner.gui.screens.scan_screens import ScanScreen -from seedsigner.gui.screens.screen import BaseTopNavScreen, ButtonListScreen +from seedsigner.gui.screens.screen import BaseScreen, BaseTopNavScreen, ButtonListScreen from seedsigner.hardware.buttons import HardwareButtonsConstants +from seedsigner.hardware.camera import Camera from seedsigner.models.settings import SettingsConstants @@ -48,14 +50,18 @@ class SettingsEntryUpdateSelectionScreen(ButtonListScreen): @dataclass -class IOTestScreen(ScanScreen): +class IOTestScreen(BaseTopNavScreen): def __post_init__(self): + self.title = "I/O Test" + self.show_back_button = False + self.resolution = (96, 96) + self.framerate = 10 + self.instructions_text = None super().__post_init__() # D-pad pictogram - input_button_width = GUIConstants.BUTTON_HEIGHT - input_button_height = input_button_width - + input_button_width = GUIConstants.BUTTON_HEIGHT + 2 + input_button_height = input_button_width + 2 dpad_center_x = GUIConstants.EDGE_PADDING + input_button_width + GUIConstants.COMPONENT_PADDING dpad_center_y = int((self.canvas_height - input_button_height)/2) @@ -73,7 +79,6 @@ class IOTestScreen(ScanScreen): self.joystick_up_button = IconButton( icon_name=FontAwesomeIconConstants.ANGLE_UP, icon_size=GUIConstants.ICON_INLINE_FONT_SIZE, - is_text_centered=True, width=input_button_width, height=input_button_height, screen_x=dpad_center_x, @@ -116,22 +121,30 @@ class IOTestScreen(ScanScreen): ) self.components.append(self.joystick_right_button) - key_button_width = int(1.75*GUIConstants.BUTTON_HEIGHT) - key_button_height = int(0.85*GUIConstants.BUTTON_HEIGHT) + # Hardware keys UI + font = Fonts.get_font(GUIConstants.BUTTON_FONT_NAME, GUIConstants.BUTTON_FONT_SIZE) + (left, top, text_width, bottom) = font.getbbox(text="Clear", anchor="ls") + icon = Icon( + icon_name=FontAwesomeIconConstants.CAMERA, + icon_size=GUIConstants.ICON_INLINE_FONT_SIZE, + ) + key_button_width = text_width + 2*GUIConstants.COMPONENT_PADDING + GUIConstants.EDGE_PADDING + key_button_height = icon.height + int(1.5*GUIConstants.COMPONENT_PADDING) key2_y = int(self.canvas_height/2) - int(key_button_height/2) self.key2_button = Button( - text=" ", + text="Clear", # Initialize with text to set vertical centering width=key_button_width, height=key_button_height, screen_x=self.canvas_width - key_button_width + GUIConstants.EDGE_PADDING, screen_y=key2_y, outline_color=GUIConstants.ACCENT_COLOR, ) + self.key2_button.text = " " # but default state is empty self.components.append(self.key2_button) - self.key1_button = Button( - text=" ", + self.key1_button = IconButton( + icon_name=FontAwesomeIconConstants.CAMERA, width=key_button_width, height=key_button_height, screen_x=self.canvas_width - key_button_width + GUIConstants.EDGE_PADDING, @@ -153,10 +166,90 @@ class IOTestScreen(ScanScreen): def _run(self): cur_selected_button = self.key1_button + msg_height = GUIConstants.ICON_LARGE_BUTTON_SIZE + 2*GUIConstants.COMPONENT_PADDING + camera_message = TextArea( + text="Capturing image...", + font_size=GUIConstants.TOP_NAV_TITLE_FONT_SIZE, + is_text_centered=True, + height=msg_height, + screen_y=int((self.canvas_height - msg_height)/ 2), + ) while True: input = self.hw_inputs.wait_for(keys=HardwareButtonsConstants.ALL_KEYS, check_release=False) - if input == HardwareButtonsConstants.KEY_PRESS: + if input == HardwareButtonsConstants.KEY1: + cur_selected_button = self.key1_button + + with self.renderer.lock: + cur_selected_button.is_selected = True + cur_selected_button.render() + camera_message.render() + # Render edges around message box + self.image_draw.rectangle( + ( + -1, int((self.canvas_height - msg_height)/ 2) - 1, + self.canvas_width + 1, int((self.canvas_height + msg_height)/ 2) + 1 + ), + outline=GUIConstants.ACCENT_COLOR, + width=1, + ) + self.renderer.show_image() + + # Snap a pic, render it as the background, re-render all onscreen elements + camera = Camera.get_instance() + try: + camera.start_single_frame_mode(resolution=(self.canvas_width, self.canvas_height)) + + # Reset the button state + with self.renderer.lock: + cur_selected_button.is_selected = False + cur_selected_button.render() + self.renderer.show_image() + + time.sleep(0.25) + background_frame = camera.capture_frame() + display_version = autocontrast( + background_frame, + cutoff=2 + ) + with self.renderer.lock: + self.canvas.paste(display_version, (0, self.top_nav.height)) + self.key2_button.text = "Clear" + for component in self.components: + component.render() + self.renderer.show_image() + finally: + camera.stop_single_frame_mode() + + continue + + elif input == HardwareButtonsConstants.KEY2: + cur_selected_button = self.key2_button + + # Clear the background + with self.renderer.lock: + cur_selected_button.is_selected = True + self._render() + self.renderer.show_image() + + # And then re-render Key2 in its initial state + self.key2_button.text = " " + cur_selected_button.is_selected = False + cur_selected_button.render() + self.renderer.show_image() + + continue + + elif input == HardwareButtonsConstants.KEY3: + # Exit + cur_selected_button = self.key3_button + cur_selected_button.is_selected = True + with self.renderer.lock: + cur_selected_button.render() + self.renderer.show_image() + return + + elif input == HardwareButtonsConstants.KEY_PRESS: cur_selected_button = self.joystick_click_button elif input == HardwareButtonsConstants.KEY_UP: @@ -171,23 +264,15 @@ class IOTestScreen(ScanScreen): elif input == HardwareButtonsConstants.KEY_RIGHT: cur_selected_button = self.joystick_right_button - elif input == HardwareButtonsConstants.KEY1: - cur_selected_button = self.key1_button - - elif input == HardwareButtonsConstants.KEY2: - cur_selected_button = self.key2_button - - elif input == HardwareButtonsConstants.KEY3: - # Exit - self.camera.stop_video_stream_mode() - cur_selected_button = self.key3_button + with self.renderer.lock: cur_selected_button.is_selected = True - with self.renderer.lock: - cur_selected_button.render() - self.renderer.show_image() - return + cur_selected_button.render() + self.renderer.show_image() - cur_selected_button.is_selected = True + with self.renderer.lock: + cur_selected_button.is_selected = False + cur_selected_button.render() + self.renderer.show_image() time.sleep(0.1) diff --git a/src/seedsigner/views/settings_views.py b/src/seedsigner/views/settings_views.py index 5a705e08..9aeb0dcb 100644 --- a/src/seedsigner/views/settings_views.py +++ b/src/seedsigner/views/settings_views.py @@ -178,12 +178,7 @@ class SettingsEntryUpdateSelectionView(View): ****************************************************************************""" class IOTestView(View): def run(self): - settings_screens.IOTestScreen( - instructions_text="Live camera feed", - resolution=(240,240), - framerate=24, - auto_deactivate_buttons=True, - ).display() + settings_screens.IOTestScreen().display() return Destination(SettingsMenuView)