From 7f7993ef9642f5bad2ff29946451d5fa9fe46c68 Mon Sep 17 00:00:00 2001 From: LIN SE Date: Wed, 10 Jan 2024 15:14:40 +0100 Subject: [PATCH] added first control box - added python display daemon - on control boxes, the uplink is the leftmost plug by default --- display.py | 234 +++++++++++++++++++++++++++++ install.py | 83 +++++++--- servercfg/linse-box1_5b265c.cfg | 9 ++ to_system/etc/profile.d/welcome.sh | 6 +- 4 files changed, 309 insertions(+), 23 deletions(-) create mode 100644 display.py create mode 100644 servercfg/linse-box1_5b265c.cfg diff --git a/display.py b/display.py new file mode 100644 index 0000000..7aa4352 --- /dev/null +++ b/display.py @@ -0,0 +1,234 @@ +import sys +import os +import time +import serial +import socket +import threading + +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 + +IDENT = 0xf3 + +FONT_GEO = [(6, 8), (8, 16), (20, 40)] + +TOPGAP = 8 # upper part of display is not useable +HEIGHT = 120 +WIDTH = 480 + + +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 + thread = None + bar = None + blinkchar = ' ' + menu = None + + def __init__(self, dev='/dev/ttyS1', timeout=0.5): + self.event = threading.Event() + self.term = serial.Serial(dev, baudrate=115200, timeout=timeout) + self.gethost(False) + self.reset() + + def reset(self): + self.send(MODE_GRAPH) + self.font(2) + self.send(CLEAR, self.bg) + + def start(self): + if self.thread is None or not self.thread.is_alive(): + self.thread = threading.Thread(target=self.gethostthread) + self.thread.daemon = True + self.thread.start() + self.event.wait(1) # wait for thread to be started + print('refresh') + self.refresh() + + def color(self, fg=15, bg=0): + self.fg = fg + self.bg = bg + + def send(self, code, *args): + out = [code] + for arg in args: + if isinstance(arg, bytes): + out.extend(list(arg)) + elif isinstance(arg, str): + out.append(arg.encode('latin-1')) + else: + out.append(arg) + self.term.write(bytes([ESC,ESC,len(out)] + out)) + + def version(self): + self.term.write(bytes([ESC,ESC,1,0xf3])) + reply = self.term.read(4) + assert reply[0:2] == ESCESC + 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.send(SET_COLOR, self.bg, self.bg, self.fg, self.fg) + + 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 gethost(self, truehostname): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + s.connect(('8.8.8.8', 80)) # 8.8.8.8: google DNS + self.hostip = s.getsockname()[0] + except Exception as e: + self.hostip = '' + if self.hostip and truehostname: + self.hostname = socket.gethostbyaddr(self.hostip)[0] + else: + self.hostname = socket.gethostname() + + def gethostthread(self): + self.gethost(True) + self.event.set() + time.sleep(1) + while True: + self.event.clear() + self.gethost(True) + self.event.wait(1) + + 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] + print(x) + return x + print('skipped', data) + + def menu_reboot(self, x): + if x is None: + return + if x < 120: + self.show('reboot ...') + os.system('reboot now') + else: + self.std_display() + + def menu_shutdown(self, x): + if x is None: + return + if x < 120: + self.show('shutdown ...') + os.system('shutdown now') + else: + self.std_display() + + def menu_main(self, x): + if x is None: + return + print(x) + if x < 160: + self.std_display() + elif x < 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, x): + if x is None: + hostip = self.hostip or 'no network.' + self.text(hostip.replace('.', self.blinkchar, 1), 0, 0, 15) + self.text(self.hostname, 1) + self.event.set() + self.blinkchar = ' ' if self.blinkchar == '.' else '.' + else: + self.menu = self.menu_main + self.show('(cancel)(reboot)(shdown)') + + def refresh(self): + func = self.menu or self.net_display + func(self.gettouch()) + + def bootmode(self): + self.send(0xf0, 0xcb, 0xef, 0x20, 0x18) + + +d = Display() + +if len(sys.argv) == 1: + d.start() + while True: + d.refresh() + diff --git a/install.py b/install.py index 3ef02b1..99c056a 100755 --- a/install.py +++ b/install.py @@ -12,6 +12,7 @@ import sys import os import filecmp import shutil +import serial from subprocess import Popen, PIPE from glob import glob from ipaddress import IPv4Interface @@ -26,17 +27,28 @@ CFGPATH = '/root/aputools/servercfg/%s_%6.6x.cfg' COMMENT = "; please refer to README.md for help" -CONFIG_TEMPLATE = """[NETWORK] -%s +CONFIG_TEMPLATE = f"""[NETWORK] +{COMMENT} enp1s0=192.168.127.254 enp2s0=192.168.2.2 enp3s0=192.168.3.3 enp4s0=dhcp [ROUTER] -%s +{COMMENT} 3001=192.168.127.254:3001 -""" % (COMMENT, COMMENT) +""" + +CONTROLBOX_TEMPLATE = f"""[NETWORK] +{COMMENT} +enp1s0=dhcp +enp2s0=192.168.1.1 +enp3s0=192.168.2.2 +enp4s0=192.168.3.3 + +[DISPLAY] +{COMMENT} +""" DHCP_HEADER = """ default-lease-time 600; @@ -66,6 +78,19 @@ ExecStart = /usr/bin/python3 /home/l_samenv/frappy/bin/secop-server %s [Install] WantedBy = multi-user.target """ + +DISPLAY_TEMPLATE = """[Unit] +Description = status display +After = network.target + +[Service] +User = root +ExecStart = /usr/bin/python3 /root/aputools/display.py + +[Install] +WantedBy = multi-user.target +""" + pip_requirements = { 'l_samenv': {}, 'root': {} @@ -95,6 +120,10 @@ def router(**opts): return ROUTER_TEMPLATE +def display(**opts): + return DISPLAY_TEMPLATE + + def pip(): for user, requirements in pip_requirements.items(): if user == 'root': @@ -118,7 +147,7 @@ def pip(): show.dirty = True -SERVICES = dict(router=router, frappy=frappy) +SERVICES = dict(router=router, frappy=frappy, display=display) def unix_cmd(command, always=False, stdout=PIPE): @@ -308,36 +337,50 @@ class Do: os.remove(join(self.syspath, file)) -IFNAMES = ['enp%ds0' % i for i in range(1,5)] - +netaddr_dict = {} +for netif in os.scandir('/sys/class/net'): + if netif.name != 'lo': # do not consider loopback interface + with open(os.path.join(netif.path, 'address')) as f: + netaddr_dict[netif.name] = netaddr = f.read().strip().lower() def handle_config(): - netaddr_dict = {} - for ifname in IFNAMES: - with open('/sys/class/net/%s/address' % ifname) as f: - netaddr_dict[ifname] = f.read().strip().lower() - netaddr = netaddr_dict[IFNAMES[0]] - apuid = int(''.join(netaddr.split(':')[-3:]), 16) & 0xfffffc cfgfile = None cfgfiles = [] - for i in range(4): - # goodie: look for mac addresses of all 4 ports - cfgfiles += glob(CFGPATH % ('*', apuid + i)) + shortids = {} + for netif, addr in netaddr_dict.items(): + shortid = int(''.join(addr.split(':')[-3:]), 16) & 0xffffff + cfgfiles += glob(CFGPATH % ('*', shortid)) + shortids[netif] = shortid with open('/etc/hostname') as f: hostname = f.read().strip() if not cfgfiles: - print('no cfg file found for %s' % hostname) + disp = serial.Serial('/dev/ttyS1', baudrate=115200, timeout=1) + disp.write(b'\x1b\x1b\x01\xf3') + display_available = disp.read(8)[0:4] == b'\x1b\x1b\x05\xf3' + ids = sorted(shortids) + if display_available: + # leftmost interface labelled 'eth0' + apuid = shortids[ids[0]] + typ = 'controlbox' + template = CONTROLBOX_TEMPLATE + else: + # rightmost interface + apuid = shortid[idx[-1]] + typ = 'bare apu' + template = CONFIG_TEMPLATE + print('no cfg file found for this', typ, f'with id {apuid:%6.6x} (hostname={hostname})') newname = input('enter host name: ') if not newname: print('no hostname given') return False cfgfile = CFGPATH % (newname, apuid) with open(cfgfile, 'w') as f: - f.write(CONFIG_TEMPLATE) + f.write(template) elif len(cfgfiles) > 1: print('ERROR: ambiguous cfg files: %s' % ', '.join(cfgfiles)) else: cfgfile = cfgfiles[0] + apuid = int(cfgfile.split('_')[-1].split('.')[0], 16) if cfgfile != CFGPATH % (hostname, apuid): if doit: os.system('sh ../sethostname.sh') @@ -354,8 +397,8 @@ def handle_config(): try: parser.read(cfgfile) network = dict(parser['NETWORK']) - for ifname in IFNAMES: - content = create_if(ifname, network.pop(ifname, 'off'), netaddr_dict[ifname], dhcp_server_cfg) + for ifname, addr in netaddr_dict.items(): + content = create_if(ifname, network.pop(ifname, 'off'), addr, dhcp_server_cfg) content = '\n'.join('%s=%s' % kv for kv in content.items()) todo = write_when_new('/etc/sysconfig/network-scripts/ifcfg-%s' % ifname, content) if todo and more_info == 'NONE': diff --git a/servercfg/linse-box1_5b265c.cfg b/servercfg/linse-box1_5b265c.cfg new file mode 100644 index 0000000..6e62f83 --- /dev/null +++ b/servercfg/linse-box1_5b265c.cfg @@ -0,0 +1,9 @@ +[NETWORK] +; please refer to README.md for help +enp1s0=dhcp +enp2s0=192.168.1.1 +enp3s0=192.168.2.2 +enp4s0=192.168.3.3 + +[DISPLAY] +; please refer to README.md for help diff --git a/to_system/etc/profile.d/welcome.sh b/to_system/etc/profile.d/welcome.sh index 6d5eaf3..b4b10e3 100755 --- a/to_system/etc/profile.d/welcome.sh +++ b/to_system/etc/profile.d/welcome.sh @@ -1,11 +1,11 @@ echo "Welcome to $HOSTNAME $(hostname -I)" -echo "$(cat /sys/class/net/enp4s0/address)" +echo "$(cat /sys/class/net/enp1s0/address) .. $(cat /sys/class/net/enp4s0/address)" export EDITOR=nano function service_status () { for name in $@; do \ - echo ${name} $(systemctl is-active ${name}) $(systemctl is-enabled ${name}); \ + echo ${name} $(systemctl is-active ${name}) $(systemctl is-enabled ${name} 2> /dev/null); \ done | column -t | grep --color=always '\(disabled\|inactive\|$\)' } alias current='cat /root/aputools/current' -service_status router frappy +service_status router frappy display echo "> current # show currently installed state"