diff --git a/brain.py b/brain.py new file mode 100644 index 0000000..f986984 --- /dev/null +++ b/brain.py @@ -0,0 +1,108 @@ +import pixelflut +import os +import time + +def guess_IP(): + import socket + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + s.connect(("google.com", 80)) + return s.getsockname()[0] + finally: + s.close() + +IP = guess_IP() + +port = 1234 +text = 'P1XELFLUT! v%s (%d)\n' % ( + pixelflut.__version__, + os.stat(__file__).st_mtime) +text += 'Connect to %s:%d\n\n' % (IP, port) +text += '>>> HELP\n' +text += '>>> SIZE\n' +text += '>>> TEXT x y text\n' +text += '>>> PX x y [RRGGBB (hex)]\n' +text += '... and more ...\n\n' +text += 'H A C K O N\n' + +@on('LOAD') +def callback(c): + c.load_font('./font.png') + c.set_title('@ %s:%d' % (IP, port)) + +@on('UNLOAD') +def callback(canvas): + canvas.text(5, 5, 'Reloading ...') + +@on('RESIZE') +def on_resize(c): + c.text(5, 5, 'Screen Size: %dx%d' % c.get_size()) + +@on('CONNECT') +def on_connect(c, client): + pass + #print c.clients.keys() + +@on('KEYDOWN-c') +def on_key_c(c): + c.clear() + pixelflut.async(c.text, 5, 5, text, delay=0.1) + +@on('KEYDOWN-s') +def on_key_s(c): + import os + i, mask = 0, 'screen%05d.png' + while os.path.exists(mask%i): i += 1 + c.save_as(mask%i) + c.text(5,5, 'Saved as %s' % mask % i) + + + +@on('COMMAND-HELP') +def on_help(canvas, client): + client.send(text) + +@on('COMMAND-TEXT') +def on_text(canvas, client, x, y, *words): + x, y = int(x), int(y) + canvas.text(x, y, ' '.join(words), delay=0.1) + +@on('COMMAND-SIZE') +def on_size(canvas, client): + client.send('SIZE %d %d' % canvas.get_size()) + +@on('COMMAND-QUIT') +def on_quit(canvas, client): + client.disconnect() + +@on('COMMAND-PX') +def on_px(canvas, client, x, y, color=None): + client.last_pixel = time.time() + x, y = int(x), int(y) + if color: + c = int(color, 16) + if c <= 16777215: + r = (c & 0xff0000) >> 16 + g = (c & 0x00ff00) >> 8 + b = c & 0x0000ff + a = 0xff + else: + r = (c & 0xff000000) >> 24 + g = (c & 0x00ff0000) >> 16 + b = (c & 0x0000ff00) >> 8 + a = c & 0x000000ff + canvas.set_pixel(x, y, r, g, b, a) + else: + r,g,b,a = canvas.get_pixel(x,y) + client.send('PX %d %d %02x%02x%02x%02x' % (x,y,r,g,b,a)) + + + +last_save = 0 +@on('TICK') +def on_tick(canvas, dt): + global last_save + if time.time() > last_save: + last_save = time.time() + 5 + canvas.save_as('save/mov_%d.png' % last_save) + canvas.text(5, 5, text, delay=0) diff --git a/pixelflut.py b/pixelflut.py index a232944..227ba8d 100755 --- a/pixelflut.py +++ b/pixelflut.py @@ -1,6 +1,6 @@ #coding: utf8 -__version__ = '0.4' +__version__ = '0.5' import time from gevent import spawn, sleep as gsleep @@ -8,75 +8,67 @@ from gevent.server import StreamServer from gevent.coros import Semaphore from gevent.queue import Queue from collections import deque +import pygame +import cairo +import math +import random +import array +import os +import os.path + +import logging +log = logging.getLogger('pixelflut') async = spawn class Client(object): - px_per_tick = 10 + px_per_tick = 100 - def __init__(self, canvas, socket, address): + def __init__(self, canvas): self.canvas = canvas - self.socket = socket - self.address = address + self.socket = None self.connect_ts = time.time() # This buffer discards all but the newest 1024 messages self.sendbuffer = deque([], 1024) # And this is used to limit clients to X messages per tick # We start at 0 (instead of x) to add a reconnect-penalty. self.limit = Semaphore(0) - print 'CONNECT', address def send(self, line): self.sendbuffer.append(line.strip() + '\n') - def disconnect(self): - print 'DISCONNECT', self.address - self.socket.close() - del self.canvas.clients[self.address] + def nospam(self, line): + if not self.sendbuffer: + self.sendbuffer.append(line.strip() + '\n') - def serve(self): + def disconnect(self): + if self.socket: + self.socket.close() + self.socket = None + + def serve(self, socket): + self.socket = socket sendall = self.socket.sendall readline = self.socket.makefile().readline try: while True: + self.limit.acquire() # Idea: Send first, receive later. If the client is to # slow to get the send-buffer empty, he cannot send. while self.sendbuffer: sendall(self.sendbuffer.popleft()) - line = readline() + line = readline().strip() if not line: break arguments = line.split() command = arguments.pop(0) - if command == 'PX': - self.on_PX(arguments) - elif command == 'SIZE': - self.on_SIZE(arguments) - else: - self.canvas.fire('COMMAND-%s' % command.upper, self, *arguments) + try: + self.canvas.fire('COMMAND-%s' % command.upper(), self, *arguments) + except TypeError, e: + self.nospam('ERROR %r :(' % e) finally: self.disconnect() - def on_SIZE(self, args): - self.send('SIZE %d %d' % self.canvas.get_size()) - - def on_PX(self, args): - self.limit.acquire() - x,y,color = args - x,y = int(x), int(y) - c = int(color, 16) - if c <= 16777215: - r = (c & 0xff0000) >> 16 - g = (c & 0x00ff00) >> 8 - b = c & 0x0000ff - a = 0xff - else: - r = (c & 0xff000000) >> 24 - g = (c & 0x00ff0000) >> 16 - b = (c & 0x0000ff00) >> 8 - a = c & 0x000000ff - self.canvas.set_pixel(x, y, r, g, b, a) - def tick(self): while self.limit.counter <= self.px_per_tick: self.limit.release() @@ -84,11 +76,7 @@ class Client(object): -import pygame -import cairo -import math -import random -import array + class Canvas(object): size = 640,480 @@ -97,47 +85,54 @@ class Canvas(object): def __init__(self): pygame.init() pygame.mixer.quit() - pygame.display.set_caption('P1XELFLUT') + self.set_title() self.screen = pygame.display.set_mode(self.size, self.flags) self.ticks = 0 self.width = self.screen.get_width() self.height = self.screen.get_height() self.clients = {} self.events = {} - + self.font = pygame.font.Font(None, 17) + def serve(self, host, port): self.server = StreamServer((host, port), self.make_client) self.server.start() return spawn(self._loop) def make_client(self, socket, address): - if address in self.clients: - self.clients[address].disconnect() - self.clients[address] = client = Client(self, socket, address) - self.fire('CONNECT', client) - client.serve() # This blocks until ready - self.fire('DISCONNECT', client) + ip = address[0] + client = self.clients.get(ip) + if client: + client.disconnect() + else: + self.clients[ip] = client = Client(self) + + try: + self.fire('CONNECT', client) + client.serve(socket) # This blocks until ready + self.fire('DISCONNECT', client) + finally: + client.disconnect() def _loop(self): - self.fire('START') while True: gsleep(0.01) # Required to allow other tasks to run - if not self.ticks % 10: - for e in pygame.event.get(): - if e.type == pygame.VIDEORESIZE: - old = self.screen.copy() - self.screen = pygame.display.set_mode(e.size, self.flags) - self.screen.blit(old, (0,0)) - self.width = self.screen.get_width() - self.height = self.screen.get_height() - self.fire('RESIZE') - elif e.type == pygame.QUIT: - self.fire('QUIT') - return - elif e.type == pygame.KEYDOWN: - self.fire('KEYDOWN-' + e.unicode) + for e in pygame.event.get(): + if e.type == pygame.VIDEORESIZE: + old = self.screen.copy() + self.screen = pygame.display.set_mode(e.size, self.flags) + self.screen.blit(old, (0,0)) + self.width, self.height = e.size + self.fire('RESIZE') + elif e.type == pygame.QUIT: + self.fire('QUIT') + return + elif e.type == pygame.KEYDOWN: + self.fire('KEYDOWN-' + e.unicode) self.ticks += 1 self.fire('TICK', self.ticks) + for c in self.clients.values(): + c.tick() pygame.display.flip() def on(self, name): @@ -150,7 +145,10 @@ class Canvas(object): def fire(self, name, *a, **ka): ''' Fire an event. ''' if name in self.events: - self.events[name](self, *a, **ka) + try: + self.events[name](self, *a, **ka) + except: + log.exception('Error in callback for %r', name) def get_size(self): ''' Get the current screen dimension as a (width, height) tuple.''' @@ -164,28 +162,39 @@ class Canvas(object): ''' Change the colour of a pixel. If an alpha value is given, the new colour is mixed with the old colour accordingly. ''' if a == 0: return + screen = self.screen if a == 0xff: - self.screen.set_at((x,y), (r,g,b)) - else: - r2,g2,b2,a2 = self.screen.get_at((x, y)) + screen.set_at((x, y), (r,g,b)) + elif 0 <= x < self.width and 0 <= y < self.height: + r2, g2, b2, a2 = screen.get_at((x, y)) r = (r2*(0xff-a)+(r*a)) / 0xff g = (g2*(0xff-a)+(g*a)) / 0xff b = (b2*(0xff-a)+(b*a)) / 0xff - self.screen.set_at((x, y), (r,g,b)) + screen.set_at((x, y), (r,g,b)) - def clear(self, r=0, g=0, b=0): + def clear(self, r=0, g=0, b=0, a=255): ''' Fill the entire screen with a solid colour (default: black)''' - self.screen.fill((r,g,b)) + self.screen.fill((r, g, b)) def save_as(self, filename): ''' Save screen to disk. ''' pygame.image.save(self.screen, filename) + def load_from(self, filename): + img = pygame.image.load(filename).convert() + self.screen.blit(img, (0,0)) + def load_font(self, fname): ''' Load a font image with 16x16 sprites. ''' self.font_img = pygame.image.load(fname).convert() self.font_res = int(self.font_img.get_width())/16 + def set_title(self, text=None): + title = 'P1XELFLUT' + if text: + title += ' ' + text + pygame.display.set_caption(title) + def putc(self, x, y, c): if not self.font_img: self.load_font('font.png') @@ -204,3 +213,39 @@ class Canvas(object): +if __name__ == '__main__': + logging.basicConfig() + + import optparse + parser = optparse.OptionParser("usage: %prog [options] brain_script") + parser.add_option("-H", "--host", dest="hostname", + default="0.0.0.0", type="string", + help="specify hostname to run on") + parser.add_option("-p", "--port", dest="portnum", default=1234, + type="int", help="port number to run on") + + (options, args) = parser.parse_args() + if len(args) != 1: + parser.error("incorrect number of arguments") + + canvas = Canvas() + task = canvas.serve(options.hostname, options.portnum) + + brainfile = args[0] + mtime = 0 + + while True: + gsleep(1) + if mtime < os.stat(brainfile).st_mtime: + canvas.fire('UNLOAD') + canvas.events.clear() + try: + execfile(brainfile, {'on':canvas.on, '__file__': brainfile}) + except: + log.exception('Brain failed') + continue + canvas.fire('LOAD') + mtime = os.stat(brainfile).st_mtime + + task.join() + diff --git a/run.py b/run.py deleted file mode 100644 index 01be0b4..0000000 --- a/run.py +++ /dev/null @@ -1,54 +0,0 @@ -import pixelflut - - -def guess_IP(): - import socket - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - try: - s.connect(("google.com", 80)) - return s.getsockname()[0] - finally: - s.close() - - -port = 2342 -text = 'P1XELFLUT! v%s\n' % pixelflut.__version__ -text += 'Connect to %s:%d\n\n' % (guess_IP(), port) -text += '>>> SIZE\n' -text += '>>> PX x y hex-color\n' -text += '... and more ...\n\n' -text += 'H A C K O N\n' - -canvas = pixelflut.Canvas() - -@canvas.on('START') -def callback(c): - c.load_font('./font.png') - pixelflut.async(c.text, 5, 5, text, delay=0.1) - -@canvas.on('RESIZE') -def callback(c): - c.text(5, 5, 'Screen Size: %dx%d' % c.get_size()) - -@canvas.on('CONNECT') -def callback(c, client): - c.text(5, 5, 'Connect: %s' % client.address[0]) - -@canvas.on('KEYDOWN-c') -def callback(c): - c.clear() - -@canvas.on('KEYDOWN-s') -def callback(c): - import os - i, mask = 0, 'screen%05d.png' - while os.path.exists(mask%i): i += 1 - c.save_as(mask%i) - c.text(5,5, 'Saved as %s' % mask % i) - -@canvas.on('COMMAND-CLEAR') -def callback(canvas, client, *args): - canvas.clear() - -task = canvas.serve('0.0.0.0', port) -task.join()