import sys import os import time import serial import socket import threading import re import queue from utils import MainIf from configparser import ConfigParser # display tty device tty = '/dev/ttyS1' TOOLS = '/home/l_samenv/boxtools' STARTUP_TEXT = f'{TOOLS}/startup_display.txt' ESC = 0x1b ESCESC = bytes((ESC, ESC)) MODE_GRAPH = 0x20 MODE_CONSOLE = 0x21 SET_POS = 0x30 SET_FONT = 0x31 SET_COLOR = 0x32 CLEAR = 0x40 LINES = 0x41 RECT = 0x42 ICON = 0x43 TEXT = 0x44 COPYRECT = 0x45 PLOT = 0x46 TOUCH = 0x50 TOUCH_MODE = 0x51 SAVE_ATTRS = 0xa0 SEL_ATTRS = 0xc0 SET_STARTUP = 0xf2 IDENT = 0xf3 FONT_GEO = [(6, 8), (8, 16), (20, 40), (38,64), (16, 16), (12, 24)] TOPGAP = 8 # upper part of display is not useable HEIGHT = 120 WIDTH = 480 MAGIC = b'\xcb\xef\x20\x18' def xy(x, y): x = min(480, int(x)) return bytes([(min(127, y + TOPGAP) << 1) + (x >> 8), x % 256]) class Display: fg = 15 bg = 0 touch = 0 bar = None blink = False menu = None storage = None def __init__(self, dev, timeout=0.5, daemon=False): # self.event = threading.Event() self.term = serial.Serial(dev, baudrate=115200, timeout=timeout) self.storage = bytearray() self.hostname = socket.gethostname() if daemon: todo_file = STARTUP_TEXT + '.todo' try: with open(todo_file) as f: text = f.read() print('new startup text:') print(text) except FileNotFoundError: text = None if text: self.reset() self.show(*text.split('\n')[0:3]) self.set_startup() os.rename(todo_file, STARTUP_TEXT) else: self.storage = None else: os.system('systemctl stop display') self.reset() if daemon: self.net_display(None) def reset(self): self.send(MODE_GRAPH) self.font(2) self.send(CLEAR, self.bg) def color(self, fg=15, bg=0): self.fg = fg self.bg = bg self.send(SET_COLOR, bg, bg, fg, fg) def send(self, code, *args): out = bytearray([code]) for arg in args: if isinstance(arg, str): out.extend(arg.encode('latin-1')) elif hasattr(arg, '__len__'): out.extend(arg) else: out.append(arg) cmd = bytearray([ESC,ESC,len(out)]) + out if self.storage is not None: self.storage.extend(cmd) self.term.write(cmd) def set_startup(self): if self.storage is None: print('storage off') return data = self.storage self.storage = None self.send(SET_STARTUP, data) def version(self): self.term.write(bytes([ESC,ESC,1,0xf3])) reply = self.term.read(4) if reply[0:2] != ESCESC: print('make sure the display is not in test mode - touch through it first!') raise ValueError(f'reply is not starting with ESCESC: {reply!r}') return self.term.read(reply[2]-1) def font(self, size): self.fontsize = size self.send(SET_FONT, size) self.colwid, self.rowhei = FONT_GEO[self.fontsize] self.nrows = HEIGHT // self.rowhei self.ncols = WIDTH // self.colwid self.textbuffer = [" " * self.ncols] * self.nrows self.color() def text(self, text, row=0, left=0, right=None, font=2): if font != self.fontsize: self.font(font) if right is None: right = self.ncols if right < 0: right += self.ncols if left < 0: left += self.ncols for line in text.split('\n'): padded = line + " " * (right - left - len(line)) self.send(SET_POS, xy(left * self.colwid, TOPGAP + row * self.rowhei)) self.send(TEXT, padded.encode('latin-1')) def show(self, *args): self.send(CLEAR, self.bg) if len(args) == 1: self.text(args[0], 1) else: for row, line in enumerate(args): if row < 3: self.text(line, row) def set_touch(self, on): self.send(TOUCH_MODE, on) self.touch = on def gettouch(self): if not self.touch: self.set_touch(1) while True: ch2 = self.term.read(2) while ch2 != ESCESC: if len(ch2) < 2: return ch2 = ch2[1:2] + self.term.read(1) length = self.term.read(1) if not length: return data = self.term.read(length[0]) if len(data) < length[0]: return if data[0] == TOUCH and len(data) == 3: x = (data[1] % 2) * 256 + data[2] return x print('skipped', data) def menu_reboot(self, xtouch): if xtouch is None: return if xtouch < 120: self.show('reboot ...') os.system('reboot now') else: self.std_display() def menu_shutdown(self, xtouch): if xtouch is None: return if xtouch < 120: self.show('shutdown ...') os.system('shutdown now') else: self.std_display() def menu_main(self, xtouch): if xtouch is None: return if xtouch < 160: self.std_display() elif xtouch < 320: self.menu = self.menu_reboot self.show('reboot?', '( OK ) (cancel)') else: self.menu = self.menu_shutdown self.show('shutdown?', '( OK ) (cancel)') def std_display(self): self.menu = self.net_display self.net_display(None) def net_display(self, xtouch): if xtouch is None: hostip = mainif.ip or 'no network' dotpos = hostip.find('.') if dotpos < 0: dotpos = len(hostip) self.text(hostip.replace('.', ' ', 1), 0, 0, 15) self.send(SET_COLOR, 0, 0, 0, 11 + self.blink) # yellow / blue self.text('.', 0, dotpos, dotpos+1) self.color() self.text(mainif.hostname() or self.hostname, 1) # self.event.set() self.blink = not self.blink else: self.menu = self.menu_main self.show('(cancel)(reboot)(shdown)') def refresh(self): mainif.poll() func = self.menu or self.net_display func(self.gettouch()) def bootmode(self): self.send(0xf0, 0xcb, 0xef, 0x20, 0x18) def pretty_version(v): return '%c v%d.%d%s' % (v[0], v[2], v[3], ' test' if v[1] else '') daemon = False firmware = None for arg in sys.argv[1:]: if arg == '-d': daemon = True elif arg.endswith('.bin'): firmware = arg elif tty.startswith('/dev/'): tty = arg else: raise ValueError(f'do not know how to treat argument: {arg}') if not firmware: mainif = MainIf() d = Display(tty, daemon=daemon and not firmware) if firmware: with open(firmware, 'rb') as f: tag = f.read()[-8:] if tag[:4] != MAGIC: raise ValueError(f'{firmware} is not a valid firmware file') hwversion = d.version() if tag[4:] != hwversion: print('display version:', pretty_version(hwversion)) print('binfile version:', pretty_version(tag[4:])) result = input('flash this (takes 1 min)? ').lower() if result in ('y', 'yes'): print('\ndo NOT interrupt') d.bootmode() d.term.close() time.sleep(1.) os.system(f'../stm32flash-0.7/stm32flash -R -v -b 115200 -w {firmware} {tty}') if daemon: while True: d.refresh()