From 330e1eb20ec751256ea1cee7a6b100e1cb2d3a72 Mon Sep 17 00:00:00 2001 From: jaseg Date: Fri, 1 Jan 2016 15:55:52 +0100 Subject: Host: Add console CRAP client --- host/bdf.py | 56 ++++++++++++++ host/config.py | 44 +++++++---- host/crap.py | 68 +++++++++++++++++ host/matelight.py | 16 +--- host/server.py | 219 ++++++++++++++---------------------------------------- host/viewer.py | 30 ++++++++ 6 files changed, 239 insertions(+), 194 deletions(-) create mode 100644 host/bdf.py create mode 100644 host/crap.py create mode 100755 host/viewer.py diff --git a/host/bdf.py b/host/bdf.py new file mode 100644 index 0000000..b386426 --- /dev/null +++ b/host/bdf.py @@ -0,0 +1,56 @@ + +import config +import threading +import numpy +from ctypes import * + +class COLOR(Structure): + _fields_ = [('r', c_uint8), ('g', c_uint8), ('b', c_uint8), ('a', c_uint8)] + +class FRAMEBUFFER(Structure): + _fields_ = [('data', POINTER(COLOR)), ('w', c_size_t), ('h', c_size_t)] + +lib = CDLL('./libbdf.so') +lib.read_bdf_file.restype = c_void_p +lib.framebuffer_render_text.restype = POINTER(FRAMEBUFFER) +lib.framebuffer_render_text.argtypes= [c_char_p, c_void_p, c_void_p, c_size_t, c_size_t, c_size_t] + +dbuf = numpy.zeros(config.frame_size*4, dtype=numpy.uint8) +printlock = threading.Lock() +def printframe(fb): + with printlock: + print('\0337\033[H') + rgba = len(fb) == config.frame_size*4 + ip = numpy.frombuffer(fb, dtype=numpy.uint8) + numpy.copyto(dbuf[0::4], ip[0::3+rgba]) + numpy.copyto(dbuf[1::4], ip[1::3+rgba]) + numpy.copyto(dbuf[2::4], ip[2::3+rgba]) + lib.console_render_buffer(dbuf.ctypes.data_as(POINTER(c_uint8)), config.display_width, config.display_height) + + +class Font: + def __init__(self, fontfile='unifont.bdf'): + self.font = lib.read_bdf_file(fontfile) + assert self.font + # hack to prevent unlocalized memory leak arising from ctypes/numpy/cpython interaction + self.cbuf = create_string_buffer(config.frame_size*sizeof(COLOR)) + self.cbuflock = threading.Lock() + + def compute_text_bounds(text): + textbytes = text.encode() + textw, texth = c_size_t(0), c_size_t(0) + res = lib.framebuffer_get_text_bounds(textbytes, unifont, byref(textw), byref(texth)) + if res: + raise ValueError('Invalid text') + return textw.value, texth.value + + def render_text(text, offset): + with cbuflock: + textbytes = bytes(str(text), 'UTF-8') + res = lib.framebuffer_render_text(textbytes, self.font, self.cbuf, + config.display_width, config.display_height, offset) + if res: + raise ValueError('Invalid text') + return self.cbuf + + diff --git a/host/config.py b/host/config.py index b68bee5..c6bd42e 100644 --- a/host/config.py +++ b/host/config.py @@ -1,13 +1,4 @@ -# Hard timeout in seconds after which (approximately) the rendering of a single item will be cut off -RENDERER_TIMEOUT = 20.0 -# How long to show an image by default -DEFAULT_IMAGE_DURATION = 10.0 -# Default scrolling speed in pixels/second -DEFAULT_SCROLL_SPEED = 4 -# Pixels to leave blank between two letters -LETTER_SPACING = 0 - # Display geometry # ┌─────────┐ ┌───┬───┬ ⋯ ┬───┬───┐ # │1 o o o 5│ │ 1 │ │ │ │ 8│ @@ -19,13 +10,34 @@ LETTER_SPACING = 0 # │25 │ │ │ │32 │ # └───┴───┴ ⋯ ┴───┴───┘ -CRATE_WIDTH = 5 -CRATE_HEIGHT = 4 -CRATES_X = 8 -CRATES_Y = 4 +# Physical display dimensions +crate_width = 5 +crate_height = 4 +crates_x = 8 +crates_y = 4 # Computed values -DISPLAY_WIDTH = CRATES_X * CRATE_WIDTH -DISPLAY_HEIGHT = CRATES_Y * CRATE_HEIGHT -FRAME_SIZE = DISPLAY_WIDTH*DISPLAY_HEIGHT*3 +display_width = crates_x * crate_width +display_height = crates_y * crate_height +crate_size = crate_width*crate_height +frame_size = display_width*display_height + +# Display gamma factor +gamma = 2.5 + +# Brightness of the display. 0 to 1.0 +brightness = 1.0 + +# Frame timeout for UDP clients +udp_timeout = 3.0 + +# Interval for rotation of multiple concurrent UDP clients +udp_switch_interval = 30.0 + +# Listening addr/port for UDP and TCP servers +udp_addr = tcp_addr = '' +udp_port = tcp_port = 1337 + +# Forward addr/port +crap_fw_addr, crap_fw_port = '127.0.0.1', 1338 diff --git a/host/crap.py b/host/crap.py new file mode 100644 index 0000000..db9f7ee --- /dev/null +++ b/host/crap.py @@ -0,0 +1,68 @@ + +import socket +import struct +import zlib +import io +from time import time + +import config + +class CRAPClient: + def __init__(self, ip='127.0.0.1', port=1337): + self.ip, self.port = ip, port + self.sock = socket.Socket(socket.AF_INET, socket.SOCK_DGRAM) + self.close = self.sock.close + + def sendframe(self, frame): + self.sock.sendto(frame, (self.ip, self.port)) + + +def _timestamped_recv(sock): + while True: + try: + data, addr = sock.recvfrom(config.frame_size*3+4) + except io.BlockingIOError as e: + raise StopIteration() + else: + yield time(), data, addr + + +class CRAPServer: + def __init__(self, ip='', port=1337, blocking=False, log=print): + self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.sock.setblocking(blocking) + self.sock.bind((ip, port)) + + self.current_client = None + self.last_timestamp = 0 + self.begin_timestamp = 0 + self.log = log + + def close(self): + self.sock.close() + + def __iter__(self): + for timestamp, data, (addr, sport) in _timestamped_recv(self.sock): + if data is None: + yield None + + if timestamp - self.last_timestamp > config.udp_timeout\ + or timestamp - self.begin_timestamp > config.udp_switch_interval: + self.current_client = addr + self.begin_timestamp = timestamp + self.log('\x1B[91mAccepting UDP data from\x1B[0m', addr) + + if addr == self.current_client: + if len(data) == config.frame_size*3+4: + (crc1,), crc2 = struct.unpack('!I', data[-4:]), zlib.crc32(data, 0), + data = data[:-4] # crop CRC + if crc1 and crc1 != crc2: # crc1 zero-check for backward-compatibility + self.log('Error receiving UDP frame: Invalid frame CRC checksum: Expected {}, got {}'.format(crc2, crc1)) + continue + elif len(data) != config.frame_size*3: + self.log('Error receiving UDP frame: Invalid frame size: {}'.format(len(data))) + self.last_timestamp = timestamp + yield data + + diff --git a/host/matelight.py b/host/matelight.py index 1bafe66..9966aee 100644 --- a/host/matelight.py +++ b/host/matelight.py @@ -4,21 +4,7 @@ from ctypes import c_size_t, c_uint8, c_void_p, c_float, CDLL, Structure, POINTE import numpy as np import time -CRATE_WIDTH = 5 -CRATE_HEIGHT = 4 -CRATES_X = 8 -CRATES_Y = 4 - -DISPLAY_WIDTH = CRATES_X*CRATE_WIDTH -DISPLAY_HEIGHT = CRATES_Y*CRATE_HEIGHT -CRATE_SIZE = CRATE_WIDTH*CRATE_HEIGHT*3 -FRAME_SIZE = DISPLAY_WIDTH*DISPLAY_HEIGHT - -# Gamma factor -GAMMA = 2.5 - -# Brightness of the LEDs in percent. 1.0 means 100%. -BRIGHTNESS = 1.0 +from config import * ml = CDLL('./libml.so') ml.matelight_open.restype = c_void_p diff --git a/host/server.py b/host/server.py index 072b8ba..3ce1bdc 100755 --- a/host/server.py +++ b/host/server.py @@ -1,191 +1,84 @@ #!/usr/bin/env python -from socketserver import * import socket -import struct -import zlib -from time import time, strftime, sleep -from collections import namedtuple, deque +from time import strftime import itertools -import threading -import random -import os import sys +from contextlib import suppress -from ctypes import * +from config import * -from matelight import sendframe, DISPLAY_WIDTH, DISPLAY_HEIGHT, FRAME_SIZE +import matelight +import bdf +import crap -UDP_TIMEOUT = 3.0 -UDP_SWITCH_INTERVAL = 30.0 - -class COLOR(Structure): - _fields_ = [('r', c_uint8), ('g', c_uint8), ('b', c_uint8), ('a', c_uint8)] - -class FRAMEBUFFER(Structure): - _fields_ = [('data', POINTER(COLOR)), ('w', c_size_t), ('h', c_size_t)] - -bdf = CDLL('./libbdf.so') -bdf.read_bdf_file.restype = c_void_p -bdf.framebuffer_render_text.restype = POINTER(FRAMEBUFFER) -bdf.framebuffer_render_text.argtypes= [c_char_p, c_void_p, c_void_p, c_size_t, c_size_t, c_size_t] - -unifont = bdf.read_bdf_file('unifont.bdf') - -def compute_text_bounds(text): - assert unifont - textbytes = bytes(str(text), 'UTF-8') - textw, texth = c_size_t(0), c_size_t(0) - res = bdf.framebuffer_get_text_bounds(textbytes, unifont, byref(textw), byref(texth)) - if res: - raise ValueError('Invalid text') - return textw.value, texth.value - -cbuf = create_string_buffer(FRAME_SIZE*sizeof(COLOR)) -cbuflock = threading.Lock() -def render_text(text, offset): - global cbuf - cbuflock.acquire() - textbytes = bytes(str(text), 'UTF-8') - res = bdf.framebuffer_render_text(textbytes, unifont, cbuf, DISPLAY_WIDTH, DISPLAY_HEIGHT, offset) - if res: - raise ValueError('Invalid text') - cbuflock.release() - return cbuf - -printlock = threading.Lock() - -def printframe(fb): - printlock.acquire() - print('\0337\033[H', end='') - print('Rendering frame @{}'.format(time())) - bdf.console_render_buffer(fb, DISPLAY_WIDTH, DISPLAY_HEIGHT) - #print('\033[0m\033[KCurrently rendering', current_entry.entrytype, 'from', current_entry.remote, ':', current_entry.text, '\0338', end='') - printlock.release() def log(*args): - printlock.acquire() print(strftime('\x1B[93m[%m-%d %H:%M:%S]\x1B[0m'), ' '.join(str(arg) for arg in args), '\x1B[0m') sys.stdout.flush() - printlock.release() class TextRenderer: def __init__(self, text): self.text = text - self.width, _ = compute_text_bounds(text) + self.width, _ = unifont.compute_text_bounds(text) def __iter__(self): for i in range(-DISPLAY_WIDTH, self.width): - #print('Rendering text @ pos {}'.format(i)) yield render_text(self.text, i) -class MateLightUDPServer: - def __init__(self, port=1337, ip=''): - self.current_client = None - self.last_timestamp = 0 - self.begin_timestamp = 0 - self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.socket.bind((ip, port)) - self.thread = threading.Thread(target = self.udp_receive) - self.thread.daemon = True - self.start = self.thread.start - self.frame_condition = threading.Condition() - self.frame = None - - def frame_da(self): - return self.frame is not None +class MatelightTCPServer: + def __init__(self, port, ip): + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.sock.setblocking(blocking) + self.sock.bind((ip, port)) + self.conns = set() + self.renderqueue = [] def __iter__(self): - while True: - with self.frame_condition: - if not self.frame_condition.wait_for(self.frame_da, timeout=UDP_TIMEOUT): - raise StopIteration() - frame, self.frame = self.frame, None + q, self.renderqueue = self.renderqueue, [] + for frame in itertools.chain(*q): yield frame - def udp_receive(self): - while True: + def handle_connections(self): + for conn in self.conns: try: - data, (addr, sport) = self.socket.recvfrom(FRAME_SIZE*3+4) - timestamp = time() - if timestamp - self.last_timestamp > UDP_TIMEOUT \ - or timestamp - self.begin_timestamp > UDP_SWITCH_INTERVAL: - self.current_client = addr - self.begin_timestamp = timestamp - log('\x1B[91mAccepting UDP data from\x1B[0m', addr) - if addr == self.current_client: - if len(data) == FRAME_SIZE*3+4: - frame = data[:-4] - crc1, = struct.unpack('!I', data[-4:]) - if crc1: - crc2, = zlib.crc32(frame, 0), - if crc1 != crc2: - raise ValueError('Invalid frame CRC checksum: Expected {}, got {}'.format(crc2, crc1)) - elif len(data) == FRAME_SIZE*3: - frame = data - else: - raise ValueError('Invalid frame size: {}'.format(len(data))) - self.last_timestamp = timestamp - with self.frame_condition: - self.frame = frame - self.frame_condition.notify() - except Exception as e: - log('Error receiving UDP frame:', e) - -renderqueue = deque() - -class MateLightTCPTextHandler(BaseRequestHandler): - def handle(self): - global render_deque - data = str(self.request.recv(1024).strip(), 'UTF-8') - addr = self.client_address[0] - if len(data) > 140: - self.request.sendall(b'TOO MUCH INFORMATION!\n') - return - log('\x1B[95mText from\x1B[0m {}: {}\x1B[0m'.format(addr, data)) - renderqueue.append(TextRenderer(data)) - self.request.sendall(b'KTHXBYE!\n') - -TCPServer.allow_reuse_address = True -tserver = TCPServer(('', 1337), MateLightTCPTextHandler) -t = threading.Thread(target=tserver.serve_forever) -t.daemon = True -t.start() - -userver = MateLightUDPServer() -userver.start() - -defaultlines = [ TextRenderer(l[:-1].replace('\\x1B', '\x1B')) for l in open('default.lines').readlines() ] -#random.shuffle(defaultlines) -defaulttexts = itertools.chain(*defaultlines) + line = conn.recv(1024).decode('UTF-8').strip() + if len(data) > 140: # Unicode string length, *not* byte length of encoded UTF-8 + conn.sendall(b'TOO MUCH INFORMATION!\n') + else: + log('\x1B[95mText from\x1B[0m {}: {}\x1B[0m'.format(addr, data)) + renderqueue.append(TextRenderer(data)) + conn.sendall(b'KTHXBYE!\n') + except socket.error, e: + if err == errno.EAGAIN or err == errno.EWOULDBLOCK: + continue + with suppress(socket.error): + conn.close() + self.conns.remove(conn) + +def _fallbackiter(it, fallback): + for fel in fallback: + for el in it: + yield el + yield fel if __name__ == '__main__': - print('\033[?1049h'+'\n'*9) - while True: - if renderqueue: - renderer = renderqueue.popleft() - elif userver.frame_da(): - renderer = userver - else: - static_noise = time() % 300 < 60 - if False: - foo = os.urandom(640) - frame = bytes([v for c in zip(list(foo), list(foo), list(foo)) for v in c ]) - sleep(0.05) - else: - try: - frame = next(defaulttexts) - except StopIteration: - defaultlines = [ TextRenderer(l[:-1].replace('\\x1B', '\x1B')) for l in open('default.lines').readlines() ] - #random.shuffle(defaultlines) - defaulttexts = itertools.chain(*defaultlines) - sendframe(frame) -# printframe(frame) - continue -# sleep(0.1) - for frame in renderer: - sendframe(frame) -# printframe(frame) -# sleep(0.1) - + tcp_server = MatelightTCPServer(config.tcp_addr, config.tcp_port) + udp_server = crap.CRAPServer(config.udp_addr, config.udp_port) + forwarder = crap.CRAPClient(config.crap_fw_addr, config.crap_fw_port) if config.crap_fw_addr is not None else None + + def defaulttexts(filename='default.lines'): + with open(filename) as f: + return itertools.chain.from_iterable(( TextRenderer(l[:-1].replace('\\x1B', '\x1B')) for l in f.readlines() )) + + with suppress(KeyboardInterrupt): + for renderer in _fallbackiter(tcp_server, defaulttexts()): + for frame in _fallbackiter(udp_server, renderer): + matelight.sendframe(frame) + if forwarder: + forwarder.sendframe(frame) + + tcp_server.close() + udp_server.close() + forwarder.close() diff --git a/host/viewer.py b/host/viewer.py new file mode 100755 index 0000000..176c6d4 --- /dev/null +++ b/host/viewer.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python + +import socket +from time import time +import sys +from contextlib import suppress +import argparse +import atexit + +import config + +import bdf +import crap + +atexit.register(print, '\033[?1049l') # Restore normal screen buffer at exit + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('addr', default='127.0.0.1', nargs='?') + parser.add_argument('port', type=int, default=1337, nargs='?') + args = parser.parse_args() + + print('\033[?1049h'+'\n'*9) + udp_server = crap.CRAPServer(args.addr, args.port, blocking=True, log=lambda *_a: None) + + with suppress(KeyboardInterrupt): + for frame in udp_server: + bdf.printframe(frame) + + udp_server.close() -- cgit