You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

427 lines
16 KiB

# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. <hello@foundationdevices.com>
# SPDX-License-Identifier: GPL-3.0-or-later
#
# SPDX-FileCopyrightText: 2018 Coinkite, Inc. <coldcardwallet.com>
# SPDX-License-Identifier: GPL-3.0-only
#
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
#
# display.py - Screen rendering and brightness control
#
import foundation
from foundation import LCD
from foundation import Backlight
from foundation import Powermon
import framebuf
import uzlib
from graphics import Graphics
from passport_fonts import FontLarge, FontSmall, FontTiny, lookup
from uasyncio import sleep_ms
class Display:
WIDTH = 230
# Note for the Sharp display the frame buffer width has to be 240 to draw properly
FB_WIDTH = 240
LINE_SIZE_BYTES = 30
HALF_WIDTH = WIDTH // 2
HEIGHT = 303
HALF_HEIGHT = HEIGHT // 2
HEADER_HEIGHT = 38
FOOTER_HEIGHT = 34
SCROLLBAR_WIDTH = 6
BATTERY_MAX = 3000
BATTERY_MIN = 2500
BYTES_PER_STRIP = WIDTH
BUF_SIZE = BYTES_PER_STRIP * ((HEIGHT + 7) // 8)
def __init__(self):
# Setup frame buffer, in show we will call scrn.update(self.dis) to show the buffer
self.scrn = LCD()
self.dis = framebuf.FrameBuffer(bytearray(
self.LINE_SIZE_BYTES * self.HEIGHT), self.FB_WIDTH, self.HEIGHT, framebuf.MONO_HLSB)
self.backlight = Backlight()
self.powermon = Powermon()
self.v_avg = [3000 for i in range(10)]
self.clear()
self.show()
def clear(self, invert=0):
self.dis.fill(invert)
def clear_rect(self, x, y, w, h):
self.dis.fill_rect(x, y, w, h, 0)
def show(self):
self.scrn.update(self.dis)
def hline(self, y, invert=1):
self.dis.line(0, y, self.WIDTH, y, invert)
def vline(self, x, invert=1):
self.dis.line(x, 0, x, self.HEIGHT, invert)
def hsegment(self, x1, x2, y, col):
self.dis.line(x1, y, x2, y, col)
def vsegment(self, x, y1, y2, col):
self.dis.line(x, y1, x, y2, col)
# Draw a filled rectangle with a border of a specified thickness
def draw_rect(self, x, y, w, h, border_w, fill_color=0, border_color=None):
if border_color == None:
border_color = 1 if fill_color == 0 else 0
# print("draw_rect() x={} y={} w={} h={} fill={} border={}".format(
# x, y, w, h, fill_color, border_color))
if border_w > 0:
self.dis.fill_rect(x, y, w, h, border_color)
self.dis.fill_rect(x + border_w, y + border_w,
w - border_w * 2, h - border_w * 2, fill_color)
def set_pixel(self, x, y, col):
self.dis.fill_rect(x, y, 1, 1, col)
def progress_bar(self, percent):
# Horizontal progress bar
# takes 0.0 .. 1.0 as fraction of doneness
percent = max(0, min(1.0, percent))
side_space = 10
bottom_space = 40
bar_height = 9
bw = 2
bw2 = bw * 2
width = self.WIDTH - (side_space * 2) - bw2
self.dis.fill_rect(side_space, self.HEIGHT - bottom_space, width, bar_height, 1)
self.dis.fill_rect(side_space + bw, self.HEIGHT - bottom_space + bw, width - bw2, bar_height - bw2, 0)
self.dis.fill_rect(side_space + bw + 1, self.HEIGHT - bottom_space + bw + 1, int((width - bw2 - 2) * percent), bar_height - bw2 - 2, 1)
def progress_bar_show(self, percent):
self.progress_bar(percent)
def set_brightness(self, val):
# normal = 128, max brightness=254, off <= 10
# This is be done with the backlight object (10 to 254 for val)
self.backlight.intensity(val)
def fullscreen(self, msg, percent=None, line2=None):
# show a simple message "fullscreen".
headingFont = FontSmall
subheadingFont = FontTiny
self.clear()
if line2:
y = self.HALF_HEIGHT - (headingFont.height // 2)
self.text(None, y, msg, font=headingFont)
y += headingFont.leading
self.text(None, y, line2, font=subheadingFont)
else:
y = self.HALF_HEIGHT - (headingFont.height // 2)
self.text(None, y, msg, font=headingFont)
if percent is not None:
self.progress_bar(percent)
self.show()
def splash(self):
# Display a splash screen with some version numbers
self.clear()
self.icon(None, self.HALF_HEIGHT - 80, 'splash')
# from version import get_mpy_version
# timestamp, label, *_ = get_mpy_version()
timestamp, label, *_ = ('11/21/2020', '0.1.0', None)
# Show version and timestamp info
y = self.HEIGHT - FontTiny.leading - 4
self.text(8, y, 'Version ' + label, font=FontTiny)
self.text(-8, y, timestamp, font=FontTiny)
self.show()
def width(self, msg, font):
return sum(lookup(font, ord(ch)).advance for ch in msg)
def icon_size(self, name):
# see graphics.py (auto generated file) for names
w, h, _bw, _wbits, _data = getattr(Graphics, name)
return (w, h)
def icon(self, x, y, name, invert=0):
# see graphics.py (auto generated file) for names
w, h, bw, wbits, data = getattr(Graphics, name)
if wbits:
data = uzlib.decompress(data, wbits)
if invert:
data = bytearray(i ^ 0xff for i in data)
gly = framebuf.FrameBuffer(bytearray(data), w, h, framebuf.MONO_HLSB)
if x is None:
x = self.HALF_WIDTH - (w // 2)
if y is None:
y = self.HALF_HEIGHT - (h // 2)
self.dis.blit(gly, x, y, invert)
return (w, h)
def image(self, x, y, w, h, img_data, invert=0):
gly = framebuf.FrameBuffer(
bytearray(img_data), w, h, framebuf.MONO_HLSB)
if x is None:
x = self.HALF_WIDTH - (w // 2)
if y is None:
y = self.HALF_HEIGHT - (h // 2)
self.dis.blit(gly, x, y, invert)
return (w, h)
def char_width(self, ch, font=FontSmall):
fn = lookup(font, ord(ch))
return fn.advance
def text_input(self, x, y, msg, font=FontSmall, invert=0, cursor_pos=None, visible_spaces=False, fixed_spacing=None, cursor_shape='line', max_chars_per_line=0):
if max_chars_per_line > 0:
# Split text into multiple lines and draw them separately
lines = [msg[i:i+max_chars_per_line]
for i in range(0, len(msg), max_chars_per_line)]
for line in lines:
self.text(x, y, line, font, invert, cursor_pos,
visible_spaces, fixed_spacing, cursor_shape)
y += font.leading
cursor_pos -= max_chars_per_line
else:
self.text(x, y, msg, font, invert, cursor_pos,
visible_spaces, fixed_spacing, cursor_shape)
def text(self, x, y, msg, font=FontSmall, invert=0, cursor_pos=None, visible_spaces=False, fixed_spacing=None, cursor_shape='line'):
# Draw at x,y (top left corner of first letter)
# using font. Use invert=1 to get reverse video
if x is None or x < 0:
# center/rjust
w = self.width(msg, font)
if x == None:
x = max(0, self.HALF_WIDTH - (w // 2))
else:
# measure from right edge (right justify)
x = max(0, self.WIDTH - w + 1 + x)
if y < 0:
# measure up from bottom edge
y = self.HEIGHT - font.leading + 1 + y
return x + (len(msg) * 8)
curr_pos = 0
for ch in msg:
if visible_spaces and ch == ' ':
ch = '_' # TODO: Replace this with a difference character code that is not an ASCII symbol
fn = lookup(font, ord(ch))
if fn is None:
# use last char in font as error char for junk we don't
# know how to render
fn = font.lookup(font.code_range.stop)
# TODO: This is always the same per font - can reuse this buffer if there are performance issues
bits = bytearray(fn.w * fn.h)
bits[0:len(fn.bits)] = fn.bits
if invert:
bits = bytearray(i ^ 0xff for i in bits)
gly = framebuf.FrameBuffer(bits, fn.w, fn.h, framebuf.MONO_HLSB)
advance = fn.advance
adjust = 0
if fixed_spacing != None:
# Adjust x to center the character within the fixed spacing
adjust = (fixed_spacing - fn.advance) // 2
x += adjust
advance = fixed_spacing - adjust
if cursor_pos != None and curr_pos == cursor_pos:
# Draw the block cursor
if cursor_shape == 'block':
# Invert the character under the block cursor if necessary
_invert = 0 if invert else 1
if _invert:
bits = bytearray(i ^ 0xff for i in bits)
gly = framebuf.FrameBuffer(
bits, fn.w, fn.h, framebuf.MONO_HLSB)
# Draw block
self.dis.fill_rect(
x - adjust, y, fixed_spacing, font.leading, _invert)
# Draw the character
self.dis.blit(gly, x + fn.x, y + font.ascent -
fn.h - fn.y, _invert)
else:
# Draw the line cursor
self.dis.fill_rect(x, y, 1, font.leading - 4, 1)
self.dis.blit(gly, x + fn.x, y +
font.ascent - fn.h - fn.y, invert)
else:
# Just draw the character normally
self.dis.blit(gly, x + fn.x, y +
font.ascent - fn.h - fn.y, invert)
x += advance
curr_pos += 1
if cursor_shape == 'line' and cursor_pos == len(msg):
# Draw the line cursor at the end if positioned at the end
self.dis.fill_rect(x, y, 1, font.leading, 1)
return x
def scrollbar(self, scroll_percent, content_to_height_ratio):
# Draw scrollbar only if the content doesn't fit on screen
if content_to_height_ratio < 1:
sb_width = 7
sb_left = self.WIDTH - sb_width
# Draw a rectangle background for the entire thing
# NOTE: We go up one pixel to cover the header divider (looks better)
self.dis.fill_rect(sb_left, self.HEADER_HEIGHT - 2,
sb_width, self.HEIGHT - self.HEADER_HEIGHT + 2, 1)
self.dis.fill_rect(sb_left+1, self.HEADER_HEIGHT - 2,
sb_width - 2, self.HEIGHT - self.HEADER_HEIGHT + 2, 0)
# Draw the scrollbar track
self.icon(sb_left + 1, self.HEADER_HEIGHT - 3, 'scrollbar')
# Draw the thumb in the right position
mm = self.HEIGHT - self.HEADER_HEIGHT - self.FOOTER_HEIGHT + 4
pos = min(int(mm * scroll_percent), mm) + self.HEADER_HEIGHT - 2
thumb_height = min(int(mm * content_to_height_ratio), mm)
thumb_width = sb_width - 2
thumb_left = sb_left + 1
self.dis.fill_rect(thumb_left, pos, thumb_width, thumb_height, 0)
# Round the thumb corners
self.set_pixel(thumb_left, pos, 1)
self.set_pixel(thumb_left + 4, pos, 1)
self.set_pixel(thumb_left, pos + thumb_height - 1, 1)
self.set_pixel(thumb_left + 4, pos + thumb_height - 1, 1)
# Draw separator lines above and below the thumb
if scroll_percent > 0:
self.hsegment(thumb_left, thumb_left + thumb_width, pos - 1, 1)
if scroll_percent < 1:
self.hsegment(thumb_left, thumb_left +
thumb_width, pos + thumb_height, 1)
# Draw a thumb pattern in the middle
notch_height = 3
notch_width = 3
# Reserve 3 pixels at the top and bottom (the 6 below)
num_notches = min((thumb_height - 6) // notch_height, 9)
notch_y = pos + (thumb_height // 2) - \
(((num_notches - 1) * notch_height) // 2) - 1
for i in range(num_notches):
self.hsegment(thumb_left + 1, thumb_left +
notch_width, notch_y, 1)
notch_y += notch_height
def draw_header(self, title='Passport', wordmark=False, left_text=None):
LEFT_MARGIN = 11
title_y = 8
# Fill background
self.dis.fill_rect(0, 0, self.WIDTH, self.HEADER_HEIGHT, 0)
self.hline(self.HEADER_HEIGHT - 4, 1)
self.hline(self.HEADER_HEIGHT - 3, 1)
self.hline(self.HEADER_HEIGHT - 2, 0)
self.hline(self.HEADER_HEIGHT - 1, 0)
# Title
self.text(None, title_y, title, font=FontSmall, invert=0)
# Left text
if left_text != None:
self.text(LEFT_MARGIN, title_y, left_text,
font=FontSmall, invert=0)
# Get battery level and shift into array
# for i in range(10):
# self.v_avg[i] = self.v_avg[i+1]
for i in range(9,0,-1):
self.v_avg[i] = self.v_avg[i-1]
(current, voltage) = self.powermon.read()
self.v_avg[0] = round(voltage * (44.7 + 22.1) / 44.7) # Voltage divider on PCB
# Calculate average of array
voltage_average = 0
for i in range(10):
voltage_average += self.v_avg[i]
# print('v_avg[{}] = {}'.format(i, self.v_avg[i]))
voltage_average = voltage_average / 10
# print('voltage_average = {}'.format(voltage_average))
# Normalize to battery operating range
batteryLife = 100 # round(100 * (voltage_average - self.BATTERY_MIN) / (self.BATTERY_MAX - self.BATTERY_MIN))
# print('batteryLife = {}'.format(batteryLife))
battery_icon = self.get_battery_icon(batteryLife)
batt_w, batt_h = self.icon_size(battery_icon)
self.icon(self.WIDTH - batt_w - 11, ((self.HEADER_HEIGHT - 4) //
2 - batt_h // 2) + 2, battery_icon, invert=0)
def draw_button(self, x, y, w, h, label, font=FontTiny, invert=0):
self.draw_rect(x, y, w, h, border_w=1,
fill_color=1 if invert else 0, border_color=1)
label_w = self.width(label, font)
x = x + (w // 2 - label_w // 2)
y = y + (h // 2 - font.ascent // 2)
self.text(x, y, label, font, invert)
def draw_footer(self, left_btn='', right_btn='', left_down=False, right_down=False):
btn_w = self.WIDTH // 2
# Ignore up/down state if there is no label
if left_btn == '':
left_down = False
if right_btn == '':
right_down = False
# Draw left button
self.draw_button(-1, self.HEIGHT - self.FOOTER_HEIGHT + 1, btn_w + 1,
self.FOOTER_HEIGHT, left_btn, invert=1 if left_down else 0)
# Draw right button
self.draw_button(btn_w - 1, self.HEIGHT - self.FOOTER_HEIGHT + 1,
btn_w + 2, self.FOOTER_HEIGHT, right_btn, invert=1 if right_down else 0)
def get_battery_icon(self, level):
if level > 90:
return 'battery_100'
elif level >= 70:
return 'battery_75'
elif level >= 50:
return 'battery_50'
elif level >= 30:
return 'battery_25'
elif level >= 10:
return 'battery_low'