Skip to content
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
2 changes: 2 additions & 0 deletions locales/locales.csv
Original file line number Diff line number Diff line change
Expand Up @@ -236,3 +236,5 @@ settings-font-outline-color-settings-subtitle;Standardfarbe der Schriftumrandung
settings-font-outline-width-settings-header;Dicke der Schriftumrandung;Width of the outline;Taille du contour;Ancho del contorno;Largura do contorno
settings-font-outline-width-settings-subtitle;Standarddicke der Schriftumrandung;Default width of the outline;Taille par défaut du contour;Ancho predeterminado del contorno;Largura padrão do contorno
support;Unterstützen;Donate;Faites un don;Donar;Doar
simple-screen.clock;Uhr;Clock;Horloge;Relogio;Relogio
simple-screen.clock.expander.subtitle;Takt konfigurieren;Configure screen clock;Configurer l'horloge;Configurar relogio;Configurar relogio
2 changes: 1 addition & 1 deletion main.py
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,7 @@ def main():
app_settings = gl.settings_manager.get_app_settings()
auto_start = app_settings.get("system", {}).get("autostart", True)
setup_autostart(auto_start)

create_cache_folder()
threading.Thread(target=update_assets, name="update_assets").start()
load()
Expand Down
164 changes: 158 additions & 6 deletions src/backend/DeckManagement/DeckController.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,35 @@ def run(self):

gl.deck_manager.connect_new_decks()

@dataclass
class MediaPlayerSetScreenImageTask:
deck_controller: "DeckController"
page: Page
native_image: bytes

n_failed_in_row: ClassVar[dict] = {}

def run(self):
try:
self.deck_controller.deck.set_screen_image(self.native_image)
self.native_image = None
del self.native_image
except StreamDeck.TransportError as e:
log.error(f"Failed to set deck screen image. Error: {e}")

beta_resume = gl.settings_manager.get_app_settings().get("system", {}).get("beta-resume-mode", True)
if beta_resume:
return

MediaPlayerSetScreenImageTask.n_failed_in_row[self.deck_controller.serial_number()] += 1
if MediaPlayerSetScreenImageTask.n_failed_in_row[self.deck_controller.serial_number()] > 5:
log.debug(f"Failed to set screen image for 5 times in a row for deck {self.deck_controller.serial_number()}. Removing controller")

self.deck_controller.deck.close()
self.deck_controller.media_player.running = False # Set stop flag - otherwise remove_controller will wait until this task is done, which it never will because it waits
gl.deck_manager.remove_controller(self.deck_controller)

gl.deck_manager.connect_new_decks()

class MediaPlayerThread(threading.Thread):
def __init__(self, deck_controller: "DeckController"):
Expand All @@ -157,6 +186,7 @@ def __init__(self, deck_controller: "DeckController"):
self.tasks: list[MediaPlayerTask] = []
self.image_tasks = {}
self.touchscreen_task = None
self.screen_task = None
self._wake_event = threading.Event()

self.fps: list[float] = []
Expand Down Expand Up @@ -294,6 +324,13 @@ def add_touchscreen_task(self, native_image: bytes):
)
self._wake_event.set()

def add_screen_task(self, native_image: bytes):
self.screen_task = MediaPlayerSetScreenImageTask(
deck_controller=self.deck_controller,
page=self.deck_controller.active_page,
native_image=native_image
)

def add_image_task(self, key_index: int, native_image: bytes):
self.image_tasks[key_index] = MediaPlayerSetImageTask(
deck_controller=self.deck_controller,
Expand Down Expand Up @@ -324,6 +361,12 @@ def perform_media_player_tasks(self):
self.touchscreen_task.run()
del self.touchscreen_task
self.touchscreen_task = None

if self.screen_task is not None:
self.screen_task.run()
del self.screen_task
self.screen_task = None

def check_connection(self):
try:
self.deck_controller.deck.get_firmware_version()
Expand Down Expand Up @@ -356,9 +399,9 @@ def __init__(self, deck_manager: "DeckManager", deck: StreamDeck.StreamDeck):
log.error(f"Failed to clear deck, maybe it's already connected to another instance? Skipping... Error: {e}")
del self
return

self.hold_time: float = gl.settings_manager.get_app_settings().get("general", {}).get("hold-time", 0.5)

self.own_deck_stack_child: "DeckStackChild" = None
self.own_key_grid: "KeyGridChild" = None

Expand Down Expand Up @@ -514,6 +557,15 @@ def get_touchscreen_image_size(self) -> tuple[int]:
size = max(size[0], 800), max(size[1], 100)
return size

@lru_cache(maxsize=None)
def get_screen_image_size(self) -> tuple[int]:
if not self.get_alive(): return
size = self.deck.screen_image_format()["size"]
if size is None:
return (800, 100)
return size

# ------------ #
# ------------ #
# Page Loading #
# ------------ #
Expand Down Expand Up @@ -870,7 +922,7 @@ def get_own_deck_stack_child(self) -> "DeckStackChild":

self.own_deck_stack_child = deck_stack_child
return deck_stack_child

def clear(self):
if not self.is_visual():
return
Expand All @@ -886,6 +938,13 @@ def clear(self):

self.deck.set_touchscreen_image(native_image, x_pos=0, y_pos=0, width=touchscreen_size[0], height=touchscreen_size[1])

if self.deck.is_visual():
screen_size = self.get_screen_image_size()
empty = Image.new("RGB", screen_size, (0, 0, 0))
native_image = PILHelper.to_native_screen_format(self.deck, empty)

self.deck.set_screen_image(native_image)

def get_own_key_grid(self) -> KeyGrid:
# Why not just lru_cache this? Because this would also cache the None that gets returned while the ui is still loading
if self.own_key_grid is not None:
Expand Down Expand Up @@ -943,6 +1002,8 @@ def __init__(self, deck_controller: DeckController):

self.tiles: list[Image.Image] = [None] * deck_controller.deck.key_count()

self.screen = None

def set_image(self, image: "BackgroundImage", update: bool = True) -> None:
self.image = image
if self.video is not None:
Expand All @@ -951,8 +1012,11 @@ def set_image(self, image: "BackgroundImage", update: bool = True) -> None:
gc.collect()

self.update_tiles()
self.update_screen()

if update:
self.deck_controller.update_all_inputs()
self.screen = self.image.get_screen_image()

def set_video(self, video: "BackgroundVideo", update: bool = True) -> None:
if self.video is not None:
Expand All @@ -970,9 +1034,11 @@ def set_from_path(self, path: str, fps: int = 30, loop: bool = True, update: boo
path = None
if path is None:
self.image = None
self.screen = None
# self.video = None
self.set_video(None, update=False)
self.update_tiles()
self.update_screen()
if update:
self.deck_controller.update_all_inputs()
elif is_video(path):
Expand Down Expand Up @@ -1007,6 +1073,10 @@ def update_tiles(self) -> None:
del tile
del old_tiles

def update_screen(self) -> None:
if self.image is not None:
self.screen = self.image.get_screen_image()

class BackgroundImage:
def __init__(self, deck_controller: DeckController, image: Image) -> None:
self.deck_controller = deck_controller
Expand All @@ -1029,10 +1099,17 @@ def create_full_deck_sized_image(self) -> Image:
# obscured pixels.
full_deck_image_size = (key_width + spacing_x, key_height + spacing_y)

# If it has a screen, then extend the image to account for it
if self.deck_controller.deck.is_visual():
extra_screen_size = self.deck_controller.get_screen_image_size()

# Adds extra screen space, plus the vertical spacing
full_deck_image_size = tuple(map(sum, zip(full_deck_image_size, extra_screen_size, (0, spacing_y))))

# Convert to RGBA first to preserve transparency, then resize
img_rgba = self.image.convert("RGBA")
return ImageOps.fit(img_rgba, full_deck_image_size, Image.LANCZOS)

def crop_key_image_from_deck_sized_image(self, image: Image.Image, key):
deck = self.deck_controller.deck

Expand All @@ -1057,7 +1134,28 @@ def crop_key_image_from_deck_sized_image(self, image: Image.Image, key):

# Return the segment directly, converting to RGBA to preserve transparency
return segment.convert("RGBA")


def crop_screen_image_from_deck_sized_image(self, image: Image.Image):
deck = self.deck_controller.deck

key_rows, key_cols = deck.key_layout()
key_width, key_height = deck.key_image_format()['size']
screen_width, screen_height = self.deck_controller.get_screen_image_size()
spacing_x, spacing_y = self.deck_controller.key_spacing

# Compute the starting X and Y offsets into the full size image that the
# requested key should display.
start_x = key_width + spacing_x
start_y = key_rows * (key_height + spacing_y)

# Compute the region of the larger deck image that is occupied by the given
# key, and crop out that segment of the full image.
region = (start_x, start_y, start_x + screen_width, start_y + screen_height)
segment = image.crop(region)

# Return the segment directly, converting to RGBA to preserve transparency
return segment.convert("RGBA")

def get_tiles(self) -> list[Image.Image]:
full_deck_sized_image = self.create_full_deck_sized_image()

Expand All @@ -1068,6 +1166,11 @@ def get_tiles(self) -> list[Image.Image]:

return tiles

def get_screen_image(self):
full_deck_sized_image = self.create_full_deck_sized_image()

return self.crop_screen_image_from_deck_sized_image(full_deck_sized_image)

class BackgroundVideo(BackgroundVideoCache):
def __init__(self, deck_controller: DeckController, video_path: str, loop: bool = True, fps: int = 30) -> None:
self.deck_controller = deck_controller
Expand Down Expand Up @@ -2401,6 +2504,55 @@ def get_own_ui_key(self) -> KeyButton:
def get_image_size(self) -> tuple[int, int]:
return self.deck_controller.get_key_image_size()

class ControllerScreen(ControllerInput):
def __init__(self, deck_controller: DeckController, ident: InputIdentifier):
super().__init__(deck_controller, ControllerTouchScreenState, ident)

self.enable_states = False

@staticmethod
def Available_Identifiers(deck):
if deck.is_visual():
return ["sd-neo"]
return []

def update(self) -> None:
image = self.get_current_image()

if image is None:
return

rgb_image = image.convert("RGB")
native_image = PILHelper.to_native_screen_format(self.deck_controller.deck, rgb_image)
rgb_image.close()
self.deck_controller.media_player.add_screen_task(native_image)

del rgb_image
self.set_ui_image(image)

def generate_empty_image(self) -> Image.Image:
return Image.new("RGBA", self.get_screen_dimensions(), (0, 0, 0, 0))

def set_ui_image(self, image: Image.Image) -> None:
if recursive_hasattr(self, "deck_controller.own_deck_stack_child.page_settings.deck_config.screenbar.image") and gl.app.main_win.get_mapped():
screenbar = self.deck_controller.own_deck_stack_child.page_settings.deck_config.screenbar
screenbar.image.set_image(image)
else:
self.deck_controller.ui_image_changes_while_hidden[self.identifier] = image

def get_current_image(self) -> Image.Image:
return self.deck_controller.background.screen

def get_screen_dimensions(self) -> tuple[int, int]:
return self.deck_controller.get_screen_image_size()

def clear(self):
self.set_current_image(self.controller_touch.generate_empty_image())

def set_current_image(self) -> None:
self.deck_controller.deck.set_screen_image(native_image)


class ControllerTouchScreen(ControllerInput):
def __init__(self, deck_controller: DeckController, ident: InputIdentifier):
super().__init__(deck_controller, ControllerTouchScreenState, ident)
Expand Down Expand Up @@ -2902,4 +3054,4 @@ def clear(self):
self.key_video = None
self.label_manager.clear_labels()
self.layout_manager.clear()
self.background_manager.set_page_color(None)
self.background_manager.set_page_color(None)
28 changes: 20 additions & 8 deletions src/backend/DeckManagement/InputIdentifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,13 @@ def Coords_To_Index(deck_controller: "DeckController", coords):# -> Any:
x, y = map(int, coords)
rows, cols = deck_controller.deck.key_layout()
return y * cols + x

def get_page_coords(self):
return self.Coords_To_PageCoords(self.coords)

def get_index(self, deck_controller: "DeckController"):
return self.Coords_To_Index(deck_controller, self.coords)

class Dial(InputIdentifier):
input_type = "dials"
controller_class_name = "ControllerDial"
Expand All @@ -105,7 +105,7 @@ class Events(InputEvent):
TURN_CCW = "Dial Turn CCW"
SHORT_TOUCH_PRESS = "Dial Touchscreen Short Press"
LONG_TOUCH_PRESS = "Dial Touchscreen Long Press"

def __init__(self, json_identifier: str):
self.index = int(json_identifier)
super().__init__(self.input_type, json_identifier, self.controller_class_name)
Expand All @@ -123,20 +123,32 @@ def __init__(self, json_identifier: str):
self.index = str(json_identifier)
super().__init__(self.input_type, json_identifier, self.controller_class_name)

All = (Key, Dial, Touchscreen)
class Screen(InputIdentifier):
input_type = "screens"
controller_class_name = "ControllerScreen"

class Events(InputEvent):
UPDATE = "Screen Update"

def __init__(self, json_identifier: str):
self.index = str(json_identifier)
super().__init__(self.input_type, json_identifier, self.controller_class_name)

All = (Key, Dial, Touchscreen, Screen)
KeyTypes = [key_type.input_type for key_type in All]

@staticmethod
def FromTypeIdentifier(input_type: str, json_identifier: str):
input_map = {
"keys": Input.Key,
"dials": Input.Dial,
"touchscreens": Input.Touchscreen
"touchscreens": Input.Touchscreen,
"screens": Input.Screen
}
if input_type in input_map:
return input_map[input_type](json_identifier)
raise ValueError(f"Unknown input type {input_type}")

@staticmethod
def AllEvents() -> list[InputEvent]:
events: list[InputEvent] = []
Expand Down
Loading