Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DRAFT] [Refactor] Threading cleanup & standardization #535

Draft
wants to merge 2 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions src/seedsigner/gui/screens/psbt_screens.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ def draw_line_segment(curves, i, j, color):
)

prev_color = reset_color
while self.keep_running:
while not self.event.wait(timeout=0.02): # No need to CPU limit when running in its own thread?
with self.renderer.lock:
# Only generate one new pulse at a time; trailing "reset_color" pulse
# erases the most recent pulse.
Expand Down Expand Up @@ -441,10 +441,6 @@ def draw_line_segment(curves, i, j, color):

self.renderer.show_image()

# No need to CPU limit when running in its own thread?
time.sleep(0.02)



@dataclass
class PSBTMathScreen(ButtonListScreen):
Expand Down
22 changes: 16 additions & 6 deletions src/seedsigner/gui/screens/scan_screens.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,16 +85,19 @@ def __init__(self, camera: Camera, decoder: DecodeQR, renderer: renderer.Rendere


def run(self):
from timeit import default_timer as timer

instructions_font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, GUIConstants.BUTTON_FONT_SIZE)

start_time = time.time()
num_frames = 0
show_framerate = False # enable for debugging / testing
while self.keep_running:
while not self.event.is_set():
frame = self.camera.read_video_stream(as_image=True)
if frame is not None:
if frame is None:
# give the camera a moment to get started
time.sleep(0.1)
continue

else:
num_frames += 1
cur_time = time.time()
cur_fps = num_frames / (cur_time - start_time)
Expand Down Expand Up @@ -167,10 +170,17 @@ def _run(self):
self.threads[0].decoder_fps = decoder_fps

if status in (DecodeQRStatus.COMPLETE, DecodeQRStatus.INVALID):
self.camera.stop_video_stream_mode()
break

if self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_RIGHT) or self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_LEFT):
self.camera.stop_video_stream_mode()
break

self.camera.stop_video_stream_mode()

# Stop the LivePreviewThread and...
self.threads[-1].stop()

# ...WAIT for it to exit so that it doesn't compete to render one last preview
# frame while we start rendering the next screen.
self.threads[-1].join()

32 changes: 13 additions & 19 deletions src/seedsigner/gui/screens/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay
from seedsigner.gui.renderer import Renderer
from seedsigner.hardware.buttons import HardwareButtonsConstants, HardwareButtons
from seedsigner.models.encode_qr import EncodeQR
from seedsigner.models.settings import SettingsConstants
from seedsigner.models.threads import BaseThread, ThreadsafeCounter
from seedsigner.models.threads import BaseThread, ThreadsafeCounter, ThreadsafeVar


# Must be huge numbers to avoid conflicting with the selected_button returned by the
Expand Down Expand Up @@ -150,7 +151,7 @@ def run(self):
screen_y=int((renderer.canvas_height - bounding_box[3])/2),
).render()

while self.keep_running:
while not self.event.is_set():
with renderer.lock:
# Render leading arc
renderer.draw.arc(
Expand Down Expand Up @@ -657,11 +658,10 @@ def swap_selected_button(new_selected_button: int):

@dataclass
class QRDisplayScreen(BaseScreen):
qr_encoder: 'EncodeQR' = None
qr_encoder: EncodeQR = None

class QRDisplayThread(BaseThread):
def __init__(self, qr_encoder: 'EncodeQR', qr_brightness: ThreadsafeCounter, renderer: Renderer,
tips_start_time: ThreadsafeCounter):
def __init__(self, qr_encoder: EncodeQR, qr_brightness: ThreadsafeVar[int], renderer: Renderer, tips_start_time: ThreadsafeCounter):
super().__init__()
self.qr_encoder = qr_encoder
self.qr_brightness = qr_brightness
Expand Down Expand Up @@ -748,30 +748,27 @@ def run(self):

# Loop whether the QR is a single frame or animated; each loop might adjust
# brightness setting.
while self.keep_running:
while not self.event.wait(timeout=(5/30.0)): # Target n held frames per second before rendering next QR image
# convert the self.qr_brightness integer (31-255) into hex triplets
hex_color = (hex(self.qr_brightness.cur_count).split('x')[1]) * 3
hex_color = (hex(self.qr_brightness.cur_value).split('x')[1]) * 3
image = self.qr_encoder.next_part_image(240, 240, border=2, background_color=hex_color)

# Display the brightness tips toast
duration = 10 ** 9 * 1.2 # 1.2 seconds
if show_brightness_tips and time.time_ns() - self.tips_start_time.cur_count < duration:
if show_brightness_tips and time.time_ns() - self.tips_start_time.cur_value < duration:
self.add_brightness_tips(image)

with self.renderer.lock:
self.renderer.show_image(image)

# Target n held frames per second before rendering next QR image
time.sleep(5 / 30.0)


def __post_init__(self):
from seedsigner.models.settings import Settings
super().__post_init__()

# Shared coordination var so the display thread can detect success
settings = Settings.get_instance()
self.qr_brightness = ThreadsafeCounter(
self.qr_brightness = ThreadsafeVar[int](
initial_value=settings.get_value(SettingsConstants.SETTING__QR_BRIGHTNESS))
self.tips_start_time = ThreadsafeCounter(initial_value=time.time_ns())

Expand Down Expand Up @@ -799,12 +796,12 @@ def _run(self):
)
if user_input == HardwareButtonsConstants.KEY_DOWN:
# Reduce QR code background brightness
self.qr_brightness.set_value(max(31, self.qr_brightness.cur_count - 31))
self.qr_brightness.set_value(max(31, self.qr_brightness.cur_value - 31))
self.tips_start_time.set_value(time.time_ns())

elif user_input == HardwareButtonsConstants.KEY_UP:
# Incrase QR code background brightness
self.qr_brightness.set_value(min(self.qr_brightness.cur_count + 31, 255))
self.qr_brightness.set_value(min(self.qr_brightness.cur_value + 31, 255))
self.tips_start_time.set_value(time.time_ns())

else:
Expand All @@ -814,7 +811,7 @@ def _run(self):
time.sleep(0.01)
break

Settings.get_instance().set_value(SettingsConstants.SETTING__QR_BRIGHTNESS, self.qr_brightness.cur_count)
Settings.get_instance().set_value(SettingsConstants.SETTING__QR_BRIGHTNESS, self.qr_brightness.cur_value)



Expand Down Expand Up @@ -894,7 +891,7 @@ def render_border(color, width):
)

try:
while self.keep_running:
while not self.event.wait(timeout=0.05): # Target ~10fps
with screen.renderer.lock:
# Ramp the edges from a darker version out to full color
inhale_scalar = inhale_factor * int(255/inhale_max)
Expand Down Expand Up @@ -924,9 +921,6 @@ def render_border(color, width):
inhale_factor = 1
inhale_factor += inhale_step

# Target ~10fps
time.sleep(0.05)

except KeyboardInterrupt as e:
self.stop()
raise e
Expand Down
29 changes: 13 additions & 16 deletions src/seedsigner/gui/screens/seed_screens.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from PIL import Image, ImageDraw, ImageFilter
from seedsigner.gui.renderer import Renderer
from seedsigner.helpers.qr import QR
from seedsigner.models.threads import BaseThread, ThreadsafeCounter
from seedsigner.models.threads import BaseThread, ThreadsafeCounter, ThreadsafeVar

from .screen import RET_CODE__BACK_BUTTON, BaseScreen, BaseTopNavScreen, ButtonListScreen, KeyboardScreen, WarningEdgesMixin
from ..components import (Button, FontAwesomeIconConstants, Fonts, FormattedAddress, IconButton,
Expand Down Expand Up @@ -1370,8 +1370,8 @@ class SeedAddressVerificationScreen(ButtonListScreen):
sig_type: str = None
network: str = None
is_mainnet: bool = None
threadsafe_counter: ThreadsafeCounter = None
verified_index: ThreadsafeCounter = None
cur_addr_index: ThreadsafeCounter = None
verified_index: ThreadsafeVar[int] = None


def __post_init__(self):
Expand Down Expand Up @@ -1404,34 +1404,33 @@ def __post_init__(self):
self.threads.append(SeedAddressVerificationScreen.ProgressThread(
renderer=self.renderer,
screen_y=self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING,
threadsafe_counter=self.threadsafe_counter,
cur_addr_index=self.cur_addr_index,
verified_index=self.verified_index,
))


def _run_callback(self):
# Exit the screen on success via a non-None value
print(f"verified_index: {self.verified_index.cur_count}")
if self.verified_index.cur_count is not None:
print("Screen callback returning success!")
if self.verified_index.cur_value is not None:
self.threads[-1].stop()
while self.threads[-1].is_alive():
time.sleep(0.01)

# Wait for the thread to exit
self.threads[-1].join()
return 1


class ProgressThread(BaseThread):
def __init__(self, renderer: Renderer, screen_y: int, threadsafe_counter: ThreadsafeCounter, verified_index: ThreadsafeCounter):
def __init__(self, renderer: Renderer, screen_y: int, cur_addr_index: ThreadsafeCounter, verified_index: ThreadsafeVar[int]):
self.renderer = renderer
self.screen_y = screen_y
self.threadsafe_counter = threadsafe_counter
self.cur_addr_index = cur_addr_index
self.verified_index = verified_index
super().__init__()


def run(self):
while self.keep_running:
if self.verified_index.cur_count is not None:
while not self.event.wait(timeout=0.1):
if self.verified_index.cur_value is not None:
# This thread will detect the success state while its parent Screen
# holds in its `wait_for`. Have to trigger a hw_input event to break
# the Screen._run out of the `wait_for` state. The Screen will then
Expand All @@ -1440,7 +1439,7 @@ def run(self):
return

textarea = TextArea(
text=f"Checking address {self.threadsafe_counter.cur_count}",
text=f"Checking address {self.cur_addr_index.cur_value}",
font_name=GUIConstants.BODY_FONT_NAME,
font_size=GUIConstants.BODY_FONT_SIZE,
screen_y=self.screen_y
Expand All @@ -1450,8 +1449,6 @@ def run(self):
textarea.render()
self.renderer.show_image()

time.sleep(0.1)



@dataclass
Expand Down
24 changes: 7 additions & 17 deletions src/seedsigner/gui/toast.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import time
from dataclasses import dataclass
from seedsigner.gui.components import BaseComponent, GUIConstants, Icon, SeedSignerIconConstants, TextArea
from seedsigner.hardware.microsd import MicroSD
from seedsigner.models.threads import BaseThread


Expand Down Expand Up @@ -70,7 +71,7 @@ class BaseToastOverlayManagerThread(BaseThread):
manager thread that the Controller will use to coordinate handing off resources
between competing toasts, the screensaver, and the current underlying Screen.

Controller should set BaseThread.keep_running = False to terminate the toast when it
Controller should call the thread's stop() to terminate the toast when it
needs to be removed or replaced.

Controller should set toggle_renderer_lock = True to make the toast temporarily
Expand Down Expand Up @@ -109,11 +110,6 @@ def instantiate_toast(self) -> ToastOverlay:
raise Exception("Must be implemented by subclass")


def should_keep_running(self) -> bool:
""" Placeholder for custom exit conditions """
return True


def toggle_renderer_lock(self):
self._toggle_renderer_lock = True

Expand All @@ -137,8 +133,11 @@ def run(self):

has_rendered = False
previous_screen_state = None
while self.keep_running and self.should_keep_running():
if self.hw_inputs.has_any_input():
while not self.event.wait(timeout=0.1):
if not MicroSD.get_instance().is_inserted:
break

elif self.hw_inputs.has_any_input():
# User has pressed a button, hide the toast
print(f"{self.__class__.__name__}: Exiting due to user input")
break
Expand Down Expand Up @@ -169,9 +168,6 @@ def run(self):
print(f"{self.__class__.__name__}: Hiding toast")
break

# Free up cpu resources for main thread
time.sleep(0.1)

finally:
print(f"{self.__class__.__name__}: exiting")
if has_rendered and self.renderer.lock.locked():
Expand Down Expand Up @@ -203,12 +199,6 @@ def instantiate_toast(self) -> ToastOverlay:
)


def should_keep_running(self) -> bool:
""" Custom exit condition: keep running until the SD card is removed """
from seedsigner.hardware.microsd import MicroSD
return MicroSD.get_instance().is_inserted



class SDCardStateChangeToastManagerThread(BaseToastOverlayManagerThread):
def __init__(self, action: str, *args, **kwargs):
Expand Down
4 changes: 1 addition & 3 deletions src/seedsigner/hardware/microsd.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,10 @@ def run(self):

os.mkfifo(self.FIFO_PATH, self.FIFO_MODE)

while self.keep_running:
while not self.event.wait(timeout=0.1):
with open(self.FIFO_PATH) as fifo:
action = fifo.read()
print(f"fifo message: {action}")

Settings.handle_microsd_state_change(action=action)
Controller.get_instance().activate_toast(SDCardStateChangeToastManagerThread(action=action))

time.sleep(0.1)
Loading
Loading