firmware/stm32/bootloader/assets/convert.py
2018-12-11 10:21:19 -05:00

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')