mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 15:08:25 +00:00
Starting to generalize support for the different displays
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
+12
-19
@@ -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):
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user