222 lines
6.6 KiB
Python
222 lines
6.6 KiB
Python
#!/usr/bin/env python3
|
|
#
|
|
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
|
|
# and is covered by GPLv3 license found in COPYING.
|
|
#
|
|
# Plan:
|
|
# - take a fixed background image and compose a number of info screens ontop
|
|
# - RLE compress that and create binary object to go in flash
|
|
# - simple logo/border plus a large symbol in middle
|
|
# - one line of small text for english speakers
|
|
#
|
|
# pip3 install Pillow
|
|
#
|
|
import os
|
|
from PIL import Image, ImageOps, ImageFont, ImageDraw
|
|
from itertools import groupby
|
|
|
|
# screen size
|
|
WH = (128, 64)
|
|
|
|
ICON_SIZE = 24 # was 40 ... anything less than 24 is ugly.
|
|
|
|
# PROBLEM: this file costs money... altho free version looks okay too
|
|
if os.path.exists('FontAwesome5Pro-Light-300.otf'):
|
|
awesome = ImageFont.truetype('FontAwesome5Pro-Light-300.otf', ICON_SIZE)
|
|
else:
|
|
awesome = ImageFont.truetype('FontAwesome407.otf', ICON_SIZE)
|
|
|
|
sm_font = ImageFont.load('zevv-peep-iso8859-15-07x14.pil')
|
|
lg_font = ImageFont.load('zevv-peep-iso8859-15-10x20.pil')
|
|
|
|
# I loaded FontAwesome as a system font my desktop, and cut-n-pasted the symbols from FontBook.
|
|
# You can also copy the Unicode code point from the FA website.
|
|
icons = {
|
|
'lemon': '',
|
|
'clock': '',
|
|
'usb': '\uf287',
|
|
'download': '',
|
|
'history': '',
|
|
'bug': '',
|
|
'x-circle': '',
|
|
#'bomb-spook': ' ', # bomb / half-space / sunglasses guy
|
|
'bomb-spook': ' ', # bomb / spaces / sunglasses guy
|
|
'spook': '', # sunglasses guy
|
|
'recycle': '\uf1b8',
|
|
'trash': '\uf2ed',
|
|
'thumbs-down': '',
|
|
'thumbs-up': '',
|
|
'graph-up': '',
|
|
'logout': '',
|
|
'ticket': '',
|
|
'dots': '',
|
|
}
|
|
|
|
def make_background():
|
|
img = Image.open('background.png').convert('1', dither=0)
|
|
assert img.size == WH
|
|
|
|
if 0:
|
|
# LATER: this costs too many bytes!
|
|
# add a logo
|
|
logo = Image.open('chip-logo.png').convert('1', dither=0)
|
|
img.paste(logo, (4,4))
|
|
|
|
# version # .. but little time to see it, and consumes too much ROM
|
|
#d = ImageDraw.Draw(img)
|
|
#d.text( (128-20, 5), 'v1', font=sm_font, fill=1)
|
|
|
|
return img
|
|
|
|
def make_frame(img, txt, icon_name, text_pos=None, icon_pos=3, icon_xpos=0):
|
|
rv = img.copy()
|
|
d = ImageDraw.Draw(rv)
|
|
|
|
if icon_name:
|
|
w,h = d.textsize(icons[icon_name], font=awesome)
|
|
icon_pos += (40 - ICON_SIZE)/2
|
|
d.text( (64-(w/2)+icon_xpos, icon_pos), icons[icon_name], font=awesome, fill=1)
|
|
text_pos = text_pos or 56
|
|
else:
|
|
text_pos = text_pos or 40
|
|
|
|
w,h = d.textsize(txt, font=sm_font)
|
|
assert w <= 128, "Message too wide: " + repr(txt)
|
|
d.text( (64-(w/2), text_pos-h), txt, font=sm_font, fill=1)
|
|
|
|
return rv
|
|
|
|
def rev(n):
|
|
return int('{:08b}'.format(n)[::-1], 2)
|
|
|
|
def rle_compress(orig):
|
|
# simple RLE:
|
|
# - first byte is flag + length (flag mask=0x80)
|
|
# - next byte is repeated length times if flag=0
|
|
# - or folloing length bytes are sent verbatim
|
|
# - must add up to full screen contents.
|
|
# - add zero at end
|
|
|
|
def flush(misc):
|
|
rv = b''
|
|
while misc:
|
|
here = min(len(misc), 127)
|
|
rv += bytes([0x80 + here]) + misc[0:here]
|
|
misc = misc[here:]
|
|
return rv
|
|
|
|
out = b''
|
|
misc = b''
|
|
|
|
for ch, group in groupby(orig):
|
|
dups = len(list(group))
|
|
if dups <= 2:
|
|
misc += bytes([ch] * dups)
|
|
continue
|
|
|
|
out += flush(misc)
|
|
misc = b''
|
|
|
|
while dups:
|
|
here = min(dups, 127)
|
|
out += bytes([0x00 + here, ch])
|
|
dups -= here
|
|
|
|
out += flush(misc)
|
|
|
|
return out + b'\0'
|
|
|
|
|
|
def serialize(img, label, fp):
|
|
|
|
img = ImageOps.mirror(img)
|
|
#img = ImageOps.flip(img)
|
|
|
|
assert img.size == WH, "wrong size: " + repr(img.size)
|
|
|
|
img = img.rotate(-90, expand=1)
|
|
assert img.size == (64, 128)
|
|
|
|
img = ImageOps.mirror(img)
|
|
img = ImageOps.flip(img)
|
|
|
|
raw = img.tobytes()
|
|
|
|
# OLED layout (dependant on settings during it's config)
|
|
# - FF81818100.. will draw a C shape in top left corner
|
|
# raw = b'\xff\x81\x81\x81' + (b'\0'*1020)
|
|
# - and each byte is reversed, etc.
|
|
|
|
assert len(raw) == 64*128//8, "Wrong size?"
|
|
|
|
reorg = bytearray(1024)
|
|
j = 0
|
|
for x in range(8):
|
|
for y in range(128):
|
|
reorg[j] = rev(raw[(y*8)+x])
|
|
j += 1
|
|
|
|
final = rle_compress(reorg)
|
|
|
|
fp.write('const unsigned char %s[%d] = {\n' % (label, len(final)))
|
|
fp.write(', '.join('0x%02x'%i for i in final))
|
|
fp.write('\n};\n\n')
|
|
|
|
return len(final)
|
|
|
|
# Actual screens and their contents.
|
|
#
|
|
# - getting the exact hight/position of the text aligned w/ 8-bits helps alot sometimes
|
|
# - rare screens don't need to be pretty
|
|
#
|
|
results = [
|
|
( 'verify', 'Verifying', 'clock', {} ),
|
|
( 'blankish', '. . .', None, dict(text_pos=36) ), # shown while we boot micropython (momentary)
|
|
( 'fatal', '#fwf', None, dict(text_pos=38) ), # don't waste space on rarely-seen screens
|
|
( 'mitm', '-/-', None, {} ), # don't waste space on rarely-seen screens
|
|
( 'brick', '', 'ticket', dict(icon_pos=12) ), # was: icon=Trash / I am brick.
|
|
#( 'dfu', 'Send Upgrade', 'download', {} ), # was beautiful, but won't be seen with RDP=2
|
|
( 'dfu', 'DFU', None, dict(text_pos=37) ),
|
|
( 'downgrade', 'Downgrade?', 'history', {} ),
|
|
( 'corrupt', 'Firmware?', 'lemon', {} ),
|
|
( 'logout', 'Logout Done', 'logout', {}),
|
|
( 'devmode', 'Danger! Caution!', 'bomb-spook', dict(icon_xpos=0)), # was 2
|
|
( 'upgrading', 'Upgrading', 'graph-up', {}),
|
|
( 'replug', 'Replug', None, {}), # visible in factory only
|
|
]
|
|
|
|
if __name__ == '__main__':
|
|
prefix = 'screen_';
|
|
out = open("screens.c", 'wt')
|
|
out.write("// autogenerated by assets/convert.py\n\n")
|
|
|
|
bg = make_background()
|
|
sampler = Image.new('1', (128+8, len(results) * (64+8)), 1)
|
|
|
|
y = 6
|
|
total = 0
|
|
for label, txt, icon, args in results:
|
|
if 0:
|
|
icon = None # XXX saves a lot of memory!
|
|
elif 1:
|
|
icon = None if label not in ('verify', 'devmode', 'logout', 'brick') else icon
|
|
|
|
img = make_frame(bg, txt, icon, **args)
|
|
sampler.paste(img, (4, y))
|
|
y += 64+4
|
|
|
|
total += serialize(img, prefix+label, out)
|
|
|
|
out.close()
|
|
|
|
out = open("screens.h", 'wt')
|
|
out.write("// autogenerated by assets/convert.py\n\n")
|
|
|
|
for label, txt, icon, _ in results:
|
|
out.write('\nextern const unsigned char %s[];\n\n' % (prefix+label))
|
|
|
|
print("Files created! %d bytes ROM used. See 'sampler.png'" % total)
|
|
#sampler.show()
|
|
sampler.save('sampler.png')
|
|
|