diff --git a/sdlvloed.py b/sdlvloed.py new file mode 100755 index 0000000..7c2efb0 --- /dev/null +++ b/sdlvloed.py @@ -0,0 +1,348 @@ +#!/usr/bin/python +"""This is a udp / binary version of PixelFlut + +Inspired by the PixelFlut projector on eth0:winter 2016 and +code from https://github.com/defnull/pixelflut/ + +This version runs without PyGame but uses SDL instead +""" + +__version__ = 0.4 +__author__ = "Jan Klopper " + +# import gevent monkeypatching and perform patch_all before anything else to +# avoid nasty eception on python closing time +from gevent import spawn, monkey +monkey.patch_all() + +import sdl2.ext +from pygame import locals as pygamelocals +import struct +import time +import socket + +from gevent.server import DatagramServer +from gevent.queue import Queue + +UDP_IP = "127.0.0.1" +UDP_PORT = 5005 +DISCOVER_PORT = 5006 +PROTOCOL_VERSION = 1 +MAX_PROTOCOL_VERSION = 1 +PROTOCOL_PREAMBLE = "pixelvloed" +MAX_PIXELS = 140 +DEFAULT_WIDTH = 786 +DEFAULT_HEIGHT = 1366 + +class Canvas(object): + """PixelVloed display class""" + + def __init__(self, queue, options): + """Init the pixelVloed server""" + self.debug = options.debug if options.debug else False + self.pixeloffset = 2 + self.fps = 30 + self.screen = None + self.udp_ip = UDP_IP + self.udp_port = UDP_PORT + self.factor = options.factor if options.factor else 1 + self.width = options.width if options.width else DEFAULT_WIDTH + self.height = options.height if options.height else DEFAULT_HEIGHT + self.canvas() + + self.queue = queue + self.limit = options.maxpixels if options.maxpixels else MAX_PIXELS + self.pixels = None + self.broadcastsocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.broadcastsocket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + self.broadcastsocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + @staticmethod + def set_title(text=None): + """Sets the window title""" + title = 'PixelVloed %0.02f' % __version__ + if text: + title += ' ' + text + return title + + def canvas(self): + """Init the pygame canvas""" + sdl2.ext.init() + self.screen = sdl2.ext.Window(self.set_title(), + size=(self.width, self.height)) + self.screen.show() + self.surface = self.screen.get_surface() + + def Pixel(self, x, y, r, g, b, a=255): # pylint: disable=C0103 + """Print a pixel to the screen""" + try: + if a == 255: + color = (r*256*256) + (g*256) + b + if self.factor>1: + for w in xrange(0, self.factor): + for h in xrange(0, self.factor): + self.pixels[(x*self.factor) + w][(y*self.factor) + h] = color + else: + self.pixels[x][y] = color + else: + old = self.pixels[x][y] + oldr = old >> 16 + oldg = (old & 0x00ff00) / 256 + oldb = old & 0x0000ff + red = (r * a) + (oldr * (1.0 - a)) + green = (g * a) + (oldg * (1.0 - a)) + blue = (b * a) + (oldb * (1.0 - a)) + self.pixels[x][y] = (red*256*256) + (green*256) + blue + except IndexError: + pass + + def CanvasUpdate(self): + """Updates the screen according to self.fps""" + lasttime = lastbroadcast = time.time() + changed = False + while True: + changed = self.Draw() or changed + #events = sdl2.ext.get_events() + #for event in events: + # if event.type == sdl2.SDL_QUIT: + # sys.exit() + # break + if time.time() - lastbroadcast > 2: + lastbroadcast = time.time() + self.SendDiscoveryPacket() + + if time.time() - lasttime >= 1.0 / self.fps and changed: + self.pixels = None # release the lock on these pixels so we can flip + self.screen.refresh() + changed = False + lasttime = time.time() + else: + time.sleep(1.0 / self.fps) + + def Draw(self): + """Draws pixels specified in the received packages in the queue""" + if self.queue.empty(): + # indicate that nothing was done, and we can skip flipping the screen + return False + #access the pixel array and lock it + self.pixels = sdl2.ext.pixels2d(self.surface) + returntime = time.time() + (1.0 / self.fps) + # while we have stuff in the queue, and its not our next time to draw a + # frame, lets process packets from the queue + while time.time() < returntime and not self.queue.empty(): + try: + data = self.queue.get() + preamble = struct.unpack_from("', DISCOVER_PORT)) + if self.debug: + print 'sending discovery packet' + except Exception as error: + if self.debug: + print error + + def __del__(self): + """Clean up any sockets we created""" + self.broadcastsocket.close() + +class PixelVloedServer(DatagramServer): + """PixelVloed server class""" + + def __init__(self, *args, **kwargs): + """Set up some vars for this instance""" + self.queue = Queue() + pixelcanvas = Canvas(self.queue, kwargs['options']) + __request_processing_greenlet = spawn(pixelcanvas.CanvasUpdate) + del (kwargs['options']) + DatagramServer.__init__(self, *args, **kwargs) + + def handle(self, data, _address): + """Is called by the DataGramServer whenever an udp package is received""" + self.queue.put(data) + +class PixelVloedClient(object): + """Sets up a client + + Arguments: + firstserver: (bool) False, select the first server immediately + debug: (bool) False + ip: (str) None + port: (int) None + width: (int) 640 + height: (int) 480 + + Listens for servers if no ip is given + """ + + def __init__(self, firstserver=False, debug=False, + ip=None, port=None, + width=640, height=480): + self.sleep = 0.01 + self.debug = debug + if not ip: + servers = False + while servers == False: + servers = self.DiscoverServers(firstserver) + self.ipaddress = servers[0]['ip'] + self.port = servers[0]['port'] + self.width = servers[0]['width'] + self.height = servers[0]['height'] + + if self.debug: + print ('displaying on %(ip)s:%(port)d, %(width)d*%(height)dpx' % + servers[0]) + else: + self.ipaddress = ip + self.port = port if port else UDP_PORT + self.width = width + self.height = height + if self.debug: + print ('displaying on %(ip)s:%(port)d, %(width)d*%(height)dpx' % + self) + self.sock = socket.socket(socket.AF_INET, # Internet + socket.SOCK_DGRAM) # UDP + + def Sleep(self, duration=None): + """Sleeps the designated amount of time""" + time.sleep(duration if duration else self.sleep) + + def SendPacket(self, message, sleep=True): + """Sends the message to the udp server + + Arguments: + message: (str, 140) + sleep: (bool) True, should the client sleep for a while? + """ + self.sock.sendto(message, (self.ipaddress, self.port)) + if sleep: + self.Sleep() + + @staticmethod + def DiscoverServers(returnfirst=False, timeout=5): + """Discover servers that send out the pixelvloed preample""" + discoverysock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + discoverysock.bind(('', DISCOVER_PORT)) + starttime = time.time() + servers = [] + foundhash = {} + while (time.time() - timeout) < starttime: + data, _addr = discoverysock.recvfrom(1024) + try: + if data.startswith(PROTOCOL_PREAMBLE): + dataset = data.split(' ') + if float(dataset[0].split(':')[1]) <= MAX_PROTOCOL_VERSION: + ipaddress = dataset[1].split(':')[0] + port = int(dataset[1].split(':')[1]) + width = int(dataset[2].split('*')[0]) + height = int(dataset[2].split('*')[1]) + if data not in foundhash: + newserver = {'ip': ipaddress, + 'port': port, + 'width': width, + 'height': height} + foundhash[data] = True + print 'New pixelvloed screen found: %r' % newserver + servers.append(newserver) + if returnfirst: + return servers + except: + pass + if servers: + return servers + return False + +def NewMessage(): + """Creates a new message with the correct max size, rgb mode and version""" + message = MaxSizeList(MAX_PIXELS+2) + message.append(SetRGBAMode(False)) + message.append(SetVersionBit()) + return message + +def RGBPixel(x, y, r, g, b, a=None): # pylint: disable=C0103 + """Generates the packed data for a pixel""" + if a is not None: + return struct.pack("<2H4B", x, y, r, g, b, a) + return struct.pack("<2H3B", x, y, r, g, b) + +def SetRGBAMode(mode): + """Generate the rgb/rgba bit""" + return struct.pack("