Skip to content
Open
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
31 changes: 30 additions & 1 deletion src/seedsigner/gui/keyboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,36 @@ def update_from_input(self, input, enter_from=None):
return key.code


def get_key_at_screen_coords(self, screen_x: int, screen_y: int):
"""
Find which key (if any) is at the given screen coordinates.

Args:
screen_x, screen_y: Screen coordinates in native space (240x240)

Returns:
Key object if found and active, None otherwise
"""
# Check if within keyboard rect
if not (self.rect[0] <= screen_x <= self.rect[2] and
self.rect[1] <= screen_y <= self.rect[3]):
return None

# Find the key at these coordinates
for row_keys in self.keys:
for key in row_keys:
key_right = key.screen_x + self.key_width * key.size
key_bottom = key.screen_y + self.key_height
if (key.screen_x <= screen_x <= key_right and
key.screen_y <= screen_y <= key_bottom):
# Only return active keys - greyed out keys should be ignored
if key.is_active:
return key
return None

return None


def set_selected_key(self, selected_letter):
# De-select the current selected_key
self.get_selected_key().is_selected = False
Expand Down Expand Up @@ -646,4 +676,3 @@ def render(self, cur_text=None, cursor_position=None):

# Paste the display onto the main canvas
self.canvas.paste(image, (self.rect[0], self.rect[1]))

115 changes: 113 additions & 2 deletions src/seedsigner/gui/renderer.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import os
from typing import Optional
from PIL import Image, ImageDraw
from threading import Lock

Expand All @@ -8,13 +10,92 @@
DISPLAY_TYPE__ILI9486,
DISPLAY_TYPE__ST7789,
DISPLAY_TYPE__DESKTOP,
DISPLAY_TYPE__DPI28,
DisplayDriver,
)
from seedsigner.models.settings import Settings
from seedsigner.models.settings_definition import SettingsConstants
from seedsigner.models.singleton import ConfigurableSingleton


def _detect_display_type() -> Optional[str]:
"""
Auto-detect display type based on hardware.

Returns 'dpi28' if Waveshare 2.8" DPI LCD is detected.
Can be overridden by SEEDSIGNER_DISPLAY environment variable.
"""
# Allow manual override
env_display = os.environ.get('SEEDSIGNER_DISPLAY')
if env_display:
return env_display

# Auto-detect DPI LCD by checking framebuffer
try:
# Check if framebuffer exists with expected size for DPI LCD (480x640)
if os.path.exists('/dev/fb0'):
with open('/sys/class/graphics/fb0/virtual_size', 'r') as f:
size = f.read().strip()
if size == '480,640':
print("[Display] Auto-detected DPI28 framebuffer (480x640)")
return 'dpi28'
except (IOError, FileNotFoundError):
pass

return None # Let Settings decide


def _detect_touch_mode() -> bool:
"""
Auto-detect if touch input is available.

Returns True if capacitive touch device is found, False otherwise.
Can be overridden by SEEDSIGNER_TOUCH environment variable.
"""
# Allow manual override
env_touch = os.environ.get("SEEDSIGNER_TOUCH")
if env_touch:
return env_touch == "1"

# Auto-detect touch input device via sysfs (no evdev needed)
try:
keywords = ["touch", "goodix", "ft5", "edt-ft5", "ft5406", "touchscreen", "stmpe", "raspberry pi"]
for i in range(10):
name_path = f"/sys/class/input/event{i}/device/name"
try:
with open(name_path, "r") as f:
name = f.read().strip()
# Look for common touch device names
if any(keyword in name.lower() for keyword in keywords):
print(f"[Touch] Auto-detected touch device: {name}")
return True
except (IOError, FileNotFoundError):
continue
except Exception:
pass

# If DPI28 display is detected, assume touch is available
# (the DPI28 Waveshare display includes integrated touch)
if _detect_display_type() == "dpi28":
print("[Touch] DPI28 display detected - enabling touch mode")
return True

return False


# Auto-detect touch mode
TOUCH_MODE = _detect_touch_mode()

# Set environment variable so other modules can check it
if TOUCH_MODE and 'SEEDSIGNER_TOUCH' not in os.environ:
os.environ['SEEDSIGNER_TOUCH'] = '1'

# Check for auto-detected DPI28 display
_auto_detected_display = _detect_display_type()
if _auto_detected_display == 'dpi28' and 'SEEDSIGNER_DISPLAY' not in os.environ:
os.environ['SEEDSIGNER_DISPLAY'] = 'dpi28'



class Renderer(ConfigurableSingleton):
buttons = None
Expand All @@ -25,6 +106,10 @@ class Renderer(ConfigurableSingleton):
disp = None
lock = Lock()

@property
def is_screenshot_generator(self) -> bool:
return False


@classmethod
def configure_instance(cls):
Expand All @@ -40,18 +125,33 @@ def initialize_display(self):
# 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)
# Check for auto-detected display first
env_display = os.environ.get("SEEDSIGNER_DISPLAY")
if env_display == "dpi28":
display_config = "dpi28_240x240"
print("[Display] Using auto-detected DPI28 display")
else:
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")

# Fallback to DPI28 if SPI hardware is missing but a DPI framebuffer is present.
if self.display_type == DISPLAY_TYPE__ST7789:
spi_missing = not os.path.exists("/dev/spidev0.0")
if spi_missing and _detect_display_type() == "dpi28":
display_config = "dpi28_240x240"
self.display_type = DISPLAY_TYPE__DPI28
width, height = "240", "240"
print("[Display] SPI device missing; using DPI28 display instead")
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 in [DISPLAY_TYPE__ST7789, DISPLAY_TYPE__DESKTOP]:
if self.display_type in [DISPLAY_TYPE__ST7789, DISPLAY_TYPE__DESKTOP, DISPLAY_TYPE__DPI28]:
self.canvas_width = self.disp.width
self.canvas_height = self.disp.height

Expand Down Expand Up @@ -122,3 +222,14 @@ def show_image_pan(self, image, start_x, start_y, end_x, end_y, rate, alpha_over
def display_blank_screen(self):
self.draw.rectangle((0, 0, self.canvas_width, self.canvas_height), outline=0, fill=0)
self.show_image()


def set_touch_bar_labels(self, labels: tuple):
"""
Set the touch bar labels for DPI28 display.

Args:
labels: Tuple from DPI28 touch bar presets (e.g., TOUCH_BAR_DEFAULT, TOUCH_BAR_KEYBOARD)
"""
if self.display_type == DISPLAY_TYPE__DPI28 and hasattr(self.disp.display, 'set_touch_bar_labels'):
self.disp.display.set_touch_bar_labels(labels)
Loading
Loading