Starting to generalize support for the different displays

This commit is contained in:
kdmukai
2025-04-28 14:06:32 -05:00
parent 1e6dd1babd
commit e33fac8f2c
12 changed files with 196 additions and 221 deletions
+32 -14
View File
@@ -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)
-169
View File
@@ -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
@@ -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):
+4 -1
View File
@@ -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,
+1 -1
View File
@@ -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
+18 -7
View File
@@ -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
+1
View File
@@ -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
+22 -2
View File
@@ -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,