From 2117f8f4e2ff43fb6d29c66feb2393f0d26795e5 Mon Sep 17 00:00:00 2001 From: Christian Bartz Date: Sun, 30 Jul 2017 23:57:07 +0200 Subject: [PATCH] add screensaver functionality to display server --- assets/fonts/dotmat.ttf | Bin 0 -> 30212 bytes display_server.py | 99 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 assets/fonts/dotmat.ttf diff --git a/assets/fonts/dotmat.ttf b/assets/fonts/dotmat.ttf new file mode 100644 index 0000000000000000000000000000000000000000..dd60a2296577e89c23d035d6aa791892364da1cf GIT binary patch literal 30212 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 literal 0 HcmV?d00001 diff --git a/display_server.py b/display_server.py index bfcab08..c5948da 100644 --- a/display_server.py +++ b/display_server.py @@ -1,8 +1,22 @@ +import time +import math + +from collections import namedtuple + +from PIL import Image +from PIL import ImageDraw +from PIL import ImageFont +from datetime import datetime, timedelta + import socket import struct +import threading import zlib +Size = namedtuple('Size', ['height', 'width']) + + class DataListener: def __init__(self, controller, ip='', port=1337): self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) @@ -36,10 +50,82 @@ def __iter__(self): # continue elif len(data) != self.frame_size: self.log('Error receiving UDP frame: Invalid frame size: {}'.format(len(data))) - continue + continue0 yield 'udp:'+addr, data +class PauseFiller(threading.Thread): + + def __init__(self, motd, controller): + super().__init__() + self.motd = motd + self.controller = controller + self.stop = False + self.font = ImageFont.truetype('assets/fonts/dotmat.ttf', 10) + + def get_pad_data(self, image, image_data, window_size): + height, width, channels = image_data.shape + + height_padding = window_size.height - height + padding_top = height_padding // 2 + padding_top_data = np.zeros((padding_top, image.width, channels)) + padding_bottom = math.ceil(height_padding / 2) + padding_bottom_data = np.zeros((padding_bottom, image.width, channels)) + + return padding_top_data, padding_bottom_data + + def display_text(self, image, slide_image=False): + image_data = np.array(image) + window_size = Size(height=self.controller.image_height, width=self.controller.image_width) + window_position = 0 + + padding_top, padding_bottom = self.get_pad_data(image, image_data, window_size) + image_data = np.vstack((padding_top, image_data, padding_bottom)) + + while True: + if self.stop: + break + + data = image_data.copy()[:, window_position:min(window_position + window_size[1], image.width), ...] + height, width, channels = data.shape + + width_padding = np.zeros((window_size.height, window_size.width - width, channels)) + data = np.hstack((data, width_padding)) + + self.controller.display(data) + if slide_image: + window_position += 1 + if window_position >= image.width: + window_position = 0 + time.sleep(1000 / 20 / 1000) + + def run(self): + text_image = Image.new('RGB', self.font.getsize(self.motd)) + draw = ImageDraw.Draw(text_image) + draw.fontmode = "1" + draw.text((0, 0), self.motd, font=self.font, fill=(255, 255, 255)) + + self.display_text(text_image, slide_image=True) + + +class IdleWatcher(threading.Thread): + + def __init__(self, idle_time): + super().__init__() + self.idle_time = idle_time + + def run(self): + global pause_filler + while True: + time.sleep(timedelta(seconds=self.idle_time).total_seconds()) + time_now = datetime.utcnow() + time_delta = (time_now - last_frame).total_seconds() + + if time_delta > self.idle_time and not pause_filler.is_alive(): + pause_filler = PauseFiller(args.motd, controller) + pause_filler.start() + + if __name__ == "__main__": import argparse import numpy as np @@ -48,14 +134,25 @@ def __iter__(self): parser = argparse.ArgumentParser(description="Remote Display Server that shows already rendered images") parser.add_argument('config', help='path to config file for matelight') parser.add_argument('-p', '--port', type=int, default=1337, help='port to listen on') + parser.add_argument('--idle-time', type=int, default=20, help='max. number of seconds matelight shall idle') + parser.add_argument('--motd', default='Contribute on code.ilexlux.xyz!') args = parser.parse_args() controller = LEDController(args.config) server = DataListener(controller, port=args.port) + last_frame = datetime.utcnow() + + pause_filler = PauseFiller(args.motd, controller) + pause_filler.start() + + idle_watcher = IdleWatcher(args.idle_time) + idle_watcher.start() try: for _, frame in server: + last_frame = datetime.utcnow() + pause_filler.stop = True frame = np.fromstring(frame, dtype=np.uint8).reshape(controller.image_height, controller.image_width, 3) controller.display(frame)