From e33fac8f2cfbad6c58f08a5c9214f4331a6d2f9e Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 25 Aug 2024 09:35:14 -0500 Subject: [PATCH] Starting to generalize support for the different displays --- src/seedsigner/gui/renderer.py | 46 +++-- src/seedsigner/hardware/ST7789.py | 169 ------------------ src/seedsigner/hardware/displays/__init__.py | 0 .../hardware/displays/display_driver.py | 46 +++++ .../hardware/{ => displays}/ili9341.py | 32 +++- .../hardware/{ => displays}/st7789_mpy.py | 31 ++-- src/seedsigner/models/settings.py | 5 +- src/seedsigner/models/settings_definition.py | 36 ++++ src/seedsigner/views/screensaver.py | 2 +- src/seedsigner/views/settings_views.py | 25 ++- tests/base.py | 1 + tests/screenshot_generator/generator.py | 24 ++- 12 files changed, 196 insertions(+), 221 deletions(-) delete mode 100644 src/seedsigner/hardware/ST7789.py create mode 100644 src/seedsigner/hardware/displays/__init__.py create mode 100644 src/seedsigner/hardware/displays/display_driver.py rename src/seedsigner/hardware/{ => displays}/ili9341.py (92%) rename src/seedsigner/hardware/{ => displays}/st7789_mpy.py (98%) diff --git a/src/seedsigner/gui/renderer.py b/src/seedsigner/gui/renderer.py index e78d8368..1a854d6a 100644 --- a/src/seedsigner/gui/renderer.py +++ b/src/seedsigner/gui/renderer.py @@ -2,7 +2,10 @@ from PIL import Image, ImageDraw from threading import Lock # from seedsigner.hardware.st7789_mpy import ST7789 -from seedsigner.hardware.ili9341 import ILI9341, ILI9341_TFTWIDTH, ILI9341_TFTHEIGHT +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 @@ -23,24 +26,39 @@ class Renderer(ConfigurableSingleton): renderer = cls.__new__(cls) cls._instance = renderer - # Eventually we'll be able to plug in other display controllers - renderer.disp = ILI9341() - renderer.disp.begin() - renderer.disp.invert() + renderer.initialize_display() - renderer.canvas_width = ILI9341_TFTHEIGHT - renderer.canvas_height = ILI9341_TFTWIDTH - # renderer.canvas_width = renderer.disp.width - # renderer.canvas_height = renderer.disp.height - renderer.canvas = Image.new('RGB', (renderer.canvas_width, renderer.canvas_height)) - renderer.draw = ImageDraw.Draw(renderer.canvas) + def initialize_display(self): + # TODO: How to handle unspecified settings + non-default display hardware? + 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) 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: @@ -52,7 +70,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): @@ -86,7 +104,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) diff --git a/src/seedsigner/hardware/ST7789.py b/src/seedsigner/hardware/ST7789.py deleted file mode 100644 index ebd4952b..00000000 --- a/src/seedsigner/hardware/ST7789.py +++ /dev/null @@ -1,169 +0,0 @@ -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) diff --git a/src/seedsigner/hardware/displays/__init__.py b/src/seedsigner/hardware/displays/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/seedsigner/hardware/displays/display_driver.py b/src/seedsigner/hardware/displays/display_driver.py new file mode 100644 index 00000000..8aee00be --- /dev/null +++ b/src/seedsigner/hardware/displays/display_driver.py @@ -0,0 +1,46 @@ +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: + from seedsigner.hardware.displays.st7789_mpy import ST7789 + if height != 240 or width not in [240, 320]: + raise ValueError("ST7789 display only supports 240x240 or 320x240 resolutions") + self.display = ST7789(width=width, height=height) + + 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") + + + @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) \ No newline at end of file diff --git a/src/seedsigner/hardware/ili9341.py b/src/seedsigner/hardware/displays/ili9341.py similarity index 92% rename from src/seedsigner/hardware/ili9341.py rename to src/seedsigner/hardware/displays/ili9341.py index 8eab9852..1330aa7c 100644 --- a/src/seedsigner/hardware/ili9341.py +++ b/src/seedsigner/hardware/displays/ili9341.py @@ -20,6 +20,9 @@ # 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 @@ -116,7 +119,11 @@ def image_to_data(image): # 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; then per-pixel byteswap to 16-bit RGB-5:6:5 + # 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() @@ -160,6 +167,15 @@ class ILI9341(object): # 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 @@ -328,7 +344,7 @@ class ILI9341(object): self.data(y1) # YEND self.command(ILI9341_RAMWR) # write to RAM - def display(self, image=None): + 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 @@ -337,19 +353,19 @@ class ILI9341(object): # By default write the internal buffer to the display. if image is None: image = self.buffer - # Set address bounds to entire display. - self.set_window() + + 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(image.rotate(self.rotation, expand=True)) + pixelbytes = image_to_data(output_image) + # Write data to hardware. self.data(pixelbytes) - def ShowImage(self, image, x, y): - self.display(image) - def clear(self, color=(0,0,0)): """Clear the image buffer to the specified RGB color (default black).""" width, height = self.buffer.size diff --git a/src/seedsigner/hardware/st7789_mpy.py b/src/seedsigner/hardware/displays/st7789_mpy.py similarity index 98% rename from src/seedsigner/hardware/st7789_mpy.py rename to src/seedsigner/hardware/displays/st7789_mpy.py index 609ebfae..b31593e3 100644 --- a/src/seedsigner/hardware/st7789_mpy.py +++ b/src/seedsigner/hardware/displays/st7789_mpy.py @@ -341,7 +341,10 @@ class ST7789: self._write(command, data) sleep_ms(delay) - def ShowImage(self,image,Xstart,Ystart): + def invert(self, enabled: bool = True): + raise Exception("Invert not implemented") + + def show_image(self, image, x_start: int = 0, y_start: int = 0): """Set buffer to value of Python Imaging Library image.""" """Write display buffer to physical display""" @@ -351,52 +354,46 @@ class ST7789: 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) - self._write(data=pix) + self._set_window(x_start, y_start, self.width, self.height) + GPIO.output(self.dc,GPIO.HIGH) + self._write(data=pix) def _write(self, command=None, data=None): """SPI write to the device: commands and data.""" if self.cs: - self.cs.off() + GPIO.output(self.cs, GPIO.LOW) if command is not None: GPIO.output(self.dc, GPIO.LOW) - # self.dc.off() self.spi.writebytes2(command) if data is not None: GPIO.output(self.dc,GPIO.HIGH) - # self.dc.on() self.spi.writebytes2(data) if self.cs: - self.cs.on() + GPIO.output(self.cs,GPIO.HIGH) def hard_reset(self): """ Hard reset display. """ if self.cs: - self.cs.off() + GPIO.output(self.cs, GPIO.LOW) if self.reset: GPIO.output(self.reset, GPIO.HIGH) - # self.reset.on() sleep_ms(10) if self.reset: GPIO.output(self.reset, GPIO.LOW) - # self.reset.off() sleep_ms(10) if self.reset: GPIO.output(self.reset, GPIO.HIGH) - # self.reset.on() sleep_ms(120) if self.cs: - GPIO.output(self.reset, GPIO.HIGH) - # self.cs.on() + GPIO.output(self.cs, GPIO.HIGH) def soft_reset(self): """ @@ -462,9 +459,6 @@ class ST7789: self._write(_ST7789_MADCTL, bytes([madctl])) - def SetWindows(self, x0, y0, x1, y1): - self._set_window(x0, y0, x1, y1) - def _set_window(self, x0, y0, x1, y1): """ Set window to column and row address. @@ -574,7 +568,6 @@ class ST7789: _ENCODE_PIXEL_SWAPPED if self.needs_swap else _ENCODE_PIXEL, color ) GPIO.output(self.dc,GPIO.HIGH) - # self.dc.on() if chunks: data = pixel * _BUFFER_SIZE for _ in range(chunks): diff --git a/src/seedsigner/models/settings.py b/src/seedsigner/models/settings.py index 3c26d797..112fb41c 100644 --- a/src/seedsigner/models/settings.py +++ b/src/seedsigner/models/settings.py @@ -191,13 +191,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] diff --git a/src/seedsigner/models/settings_definition.py b/src/seedsigner/models/settings_definition.py index f554b930..82f1957c 100644 --- a/src/seedsigner/models/settings_definition.py +++ b/src/seedsigner/models/settings_definition.py @@ -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" + DISPLAY_CONFIGURATION__ILI9341__240x320 = "ili9341_240x320" # natively portrait dimensions; we apply a 90° rotation + DISPLAY_CONFIGURATION__ILI9486__480x320 = "ili9486_320x480" # 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__240x320, "ili9341 240x320"), + # (DISPLAY_CONFIGURATION__ILI9486__480x320, "ili9486 320x480"), # TODO: Enable when ili9486 driver performance is improved + ] + + # Hidden settings SETTING__QR_BRIGHTNESS = "qr_background_color" @@ -351,6 +368,7 @@ class SettingsConstants: CATEGORY__FEATURES = "features" VISIBILITY__GENERAL = "general" + VISIBILITY__HARDWARE = "hardware" VISIBILITY__ADVANCED = "advanced" VISIBILITY__DEVELOPER = "developer" VISIBILITY__HIDDEN = "hidden" # For data-only (e.g. custom_derivation), not configurable by the user @@ -681,6 +699,24 @@ class SettingsDefinition: visibility=SettingsConstants.VISIBILITY__ADVANCED, default_value=SettingsConstants.OPTION__ENABLED), + + # Hardware config + SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM, + attr_name=SettingsConstants.SETTING__DISPLAY_CONFIGURATION, + 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, + 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, diff --git a/src/seedsigner/views/screensaver.py b/src/seedsigner/views/screensaver.py index 38254b72..03e66686 100644 --- a/src/seedsigner/views/screensaver.py +++ b/src/seedsigner/views/screensaver.py @@ -204,7 +204,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 diff --git a/src/seedsigner/views/settings_views.py b/src/seedsigner/views/settings_views.py index 17db700e..6a4f6955 100644 --- a/src/seedsigner/views/settings_views.py +++ b/src/seedsigner/views/settings_views.py @@ -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,9 @@ class SettingsEntryUpdateSelectionView(View): value=updated_value ) + if self.settings_entry.attr_name == SettingsConstants.SETTING__DISPLAY_COLOR_INVERTED: + self.renderer.disp.invert(enabled=updated_value == SettingsConstants.OPTION__ENABLED) + if destination: return destination diff --git a/tests/base.py b/tests/base.py index ffd960f4..e89174ca 100644 --- a/tests/base.py +++ b/tests/base.py @@ -12,6 +12,7 @@ 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 diff --git a/tests/screenshot_generator/generator.py b/tests/screenshot_generator/generator.py index 629a539b..c3142b25 100644 --- a/tests/screenshot_generator/generator.py +++ b/tests/screenshot_generator/generator.py @@ -11,10 +11,12 @@ from embit import compact from embit.psbt import PSBT, OutputScope from embit.script import Script +from seedsigner.gui.components import GUIConstants + # Prevent importing modules w/Raspi hardware dependencies. # These must precede any SeedSigner imports. -sys.modules['seedsigner.hardware.ST7789'] = MagicMock() -sys.modules['seedsigner.hardware.st7789_mpy'] = MagicMock() +sys.modules['seedsigner.hardware.displays.st7789_mpy'] = MagicMock() +sys.modules['seedsigner.hardware.displays.ili9341'] = MagicMock() sys.modules['seedsigner.gui.screens.screensaver'] = MagicMock() sys.modules['seedsigner.views.screensaver'] = MagicMock() sys.modules['RPi'] = MagicMock() @@ -176,6 +178,24 @@ def generate_screenshots(locale): ) ) + # Render the nested "Hardware" submenu option at the end of "Advanced" + num_advanced_settings = len(SettingsDefinition.get_settings_entries(visibility=SettingsConstants.VISIBILITY__ADVANCED)) - 5 # hard-coded for 240px height: the first 5 settings options are already visible + settings_views_list.append(( + settings_views.SettingsMenuView, + dict( + visibility=SettingsConstants.VISIBILITY__ADVANCED, + selected_attr=SettingsConstants.SETTING__PARTNER_LOGOS, + initial_scroll=num_advanced_settings*GUIConstants.BUTTON_HEIGHT + (num_advanced_settings-1)*GUIConstants.COMPONENT_PADDING, # Force menu to scroll to the bottom + ), + "SettingsMenuView__Advanced_Hardware" + )) + + settings_views_list.append(( + settings_views.SettingsMenuView, + dict(visibility=SettingsConstants.VISIBILITY__HARDWARE), + "SettingsMenuView__Hardware" + )) + # so we get a choice for transcribe seed qr format controller.settings.set_value( attr_name=SettingsConstants.SETTING__COMPACT_SEEDQR,