aboutsummaryrefslogtreecommitdiff
path: root/host
diff options
context:
space:
mode:
authorjaseg <code@jaseg.net>2016-01-01 15:55:52 +0100
committerjaseg <code@jaseg.net>2016-01-02 01:28:58 +0100
commit330e1eb20ec751256ea1cee7a6b100e1cb2d3a72 (patch)
tree60b87fadebd58788b059b9eadc9e2ac3a6a9c746 /host
parent245fbf3d6b5bb5d9cd75882a2fda3f5604335f25 (diff)
downloadmatelight-330e1eb20ec751256ea1cee7a6b100e1cb2d3a72.tar.gz
matelight-330e1eb20ec751256ea1cee7a6b100e1cb2d3a72.tar.bz2
matelight-330e1eb20ec751256ea1cee7a6b100e1cb2d3a72.zip
Host: Add console CRAP client
Diffstat (limited to 'host')
-rw-r--r--host/bdf.py56
-rw-r--r--host/config.py44
-rw-r--r--host/crap.py68
-rw-r--r--host/matelight.py16
-rwxr-xr-xhost/server.py219
-rwxr-xr-xhost/viewer.py30
6 files changed, 239 insertions, 194 deletions
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()