#!/usr/bin/python """ order queue display Copyright (C) 2007 James Cameron (quozl@us.netrek.org) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -- A shop or event display node with multicast capability. Basic Usage: a node with a keyboard, and any number of nodes without keyboard, connected via network. The operator types a message, which appears on their node during composition, then when the commit key (TAB) is pressed the message appears on all other nodes. Additional features: - messages are automatically sized to fit the available screen dimensions, - multi-line messages are possible, type Enter between lines, - editable history of previous messages, using the up and down arrow keys, in the style of a shell, - any node can be a master, all it takes is a keyboard, - nodes that are alive are listed on a master node, and if a node goes missing, it will fail to appear, - displays can be mounted upside down, inversion is by command line option or by keyboard control on the display node, - special control keys for reboot, poweroff, runlevel change, re-executing, and executing python commands. """ import os, sys, time, socket, select, pygame, code license = [ "Order Queue Display", "Copyright (C) 2007 James Cameron ", " ", "This program comes with ABSOLUTELY NO WARRANTY;", "for details see source.", " ", "This is free software, and you are welcome to ", "redistribute it under certain conditions; see ", "source for details.", " " ] class StdoutBuffer: def __init__(self): self.erase() def write(self, data): self.buffer = self.buffer + data def read(self): return self.buffer def erase(self): self.buffer = '' class FontCache: def __init__(self): self.cache = {} def read(self, name, size): if name == None: return pygame.font.Font(None, size) path = '/usr/share/fonts/truetype/ttf-dejavu/' try: return pygame.font.Font(path + name, size) except: return pygame.font.Font(None, size) def get(self, name, size): key = (name, size) if key not in self.cache: self.cache[key] = self.read(name, size) return self.cache[key] class UDP: def __init__(self, callback=None): self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: self.socket.bind(('224.0.0.1', opt.port)) except: print 'no route to multicast?' self.callback = callback self.servers = {} def send(self, text): if opt.verbose: print 'send', text self.socket.sendto(text, ('224.0.0.1', opt.port)) def recv(self): while 1: is_readable = [self.socket] is_writable = [] is_error = [] r, w, e = select.select(is_readable, is_writable, is_error, 1.0/opt.updates) if not r: return None try: (data, address) = self.socket.recvfrom(2048) break except: return None return (data, address) def text_to_image(text, colour, size): """ convert text to a centered image """ # build a list of surfaces containing the rendered lines surfaces = [] lines = text.split('\n') for line in lines: fn = fc.get('DejaVuSans-Bold.ttf', size) ts = fn.render(line, 1, colour) if ts.get_width() > width: return None surfaces.append(ts) # calculate maximum width and total height when stacked my = 0 mx = 0 for surface in surfaces: sx = surface.get_width() sy = surface.get_height() if sx > mx: mx = sx my = my + sy if mx > width or my > height: return None # make an image to hold it image = pygame.Surface((mx, my)) # lay out the smaller images on the master image y = 0 for surface in surfaces: rect = surface.get_rect(centerx = mx/2, top = y); image.blit(surface, rect) y = y + rect.height return image def emit(text, colour=(255, 255, 255)): global size ts = text_to_image(text, colour, size) while ts == None: size = int(size * 0.75) if size < 10: break ts = text_to_image(text, colour, size) if opt.invert: ts = pygame.transform.flip(ts, True, True) screen.blit(background, (0, 0)) tr = ts.get_rect(center=(width/2, height/2)) screen.blit(ts, tr) pygame.display.flip() def show_license(): fn = fc.get('DejaVuSansMono.ttf', 35) x = 20 y = 20 for line in license: ts = fn.render(line, 1, (255, 255, 255)) tr = ts.get_rect(left=x, top=y) y = tr.bottom screen.blit(ts, tr) pygame.display.flip() pygame.time.wait(500) others = {} others_next = 0 def show_alive(host): global others, others_next (a, b, c, d) = host.split('.') if not others.has_key(host): others_next = others_next + 1 others[host] = others_next fn = fc.get(None, 40) x = others[host] * 100 ts = fn.render(d, 1, (0, 255, 0)) tr = ts.get_rect(centerx=x, bottom=height) screen.blit(ts, tr) pygame.display.flip() def show_history(): fn = fc.get('DejaVuSansMono.ttf', 45) x = 20 y = 20 for n, line in history.iteritems(): colour = (127, 127, 127) if n == cursor: colour = (255, 255, 255) (text, size) = line ts = fn.render("%02d %s" % (n, text), 1, colour) tr = ts.get_rect(left=x, top=y) y = tr.bottom screen.blit(ts, tr) pygame.display.flip() def op_shell(): global sys, text realstdout = sys.stdout sys.stdout = mf ii.runsource(text) sys.stdout = realstdout text = mf.read() mf.erase() # TODO: strip last newline size = opt.size try: emit(text) except: emit('result\ncannot be\nrendered') def op_reboot(): os.system('reboot') def op_poweroff(): os.system('poweroff') def op_sugar(): os.system('telinit 5') def op_exec(): os.execv('/usr/local/bin/oqd.py', ['oqd.py']) def op_invert(): global opt opt.invert = True opt.master = False def op_normal(): global opt opt.invert = False opt.master = False def op_quit(): sys.exit() def ed_clear(): """ clear text entry """ global size, text size = opt.size text = '' emit(text) def ed_up(): """ move back through history """ global history, cursor, text, size history[cursor] = (text, size) if cursor != 0: cursor = cursor - 1 (text, size) = history[cursor] emit(text, (255, 0, 255)) show_history() def ed_down(): """ move forward through history """ global history, cursor, text, size history[cursor] = (text, size) if cursor != current: cursor = cursor + 1 (text, size) = history[cursor] emit(text, (255, 0, 255)) show_history() def ed_backspace(): """ undo typing """ global size, text size = opt.size text = text[:-1] emit(text, (255, 255, 0)) def ed_newline(): """ insert line break """ global text text = text + '\n' emit(text, (255, 255, 0)) def ed_insert(event): """ insert entered text """ global text text = text + event.unicode emit(text, (255, 255, 0)) def op_send(): """ send to slaves """ global cursor, current, history, text, size if cursor == current: history[current] = (text, size) current = current + 1 cursor = current udp.send('2 text ' + str(size) + ' ' + text) text = '' size = opt.size else: history[cursor] = (text, size) udp.send('2 text ' + str(size) + ' ' + text) # control keyboard table, relates keys to functions kb_table_control = { pygame.K_EQUALS: op_shell, pygame.K_r: op_reboot, pygame.K_o: op_poweroff, pygame.K_s: op_sugar, pygame.K_e: op_exec, pygame.K_i: op_invert, pygame.K_n: op_normal, pygame.K_d: op_quit, pygame.K_w: ed_clear } # normal keyboard table, relates keys to functions kb_table_normal = { pygame.K_UP: ed_up, pygame.K_DOWN: ed_down, pygame.K_BACKSPACE: ed_backspace, pygame.K_RETURN: ed_newline, pygame.K_TAB: op_send } def kb(event): """ handle keyboard events caused by user """ # if we are started as a slave, ignore keyboard if opt.slave: return # assume we are a master if there is any ambiguity opt.master = True # ignore the shift and control keys if event.key == pygame.K_LSHIFT or event.key == pygame.K_RSHIFT: return if event.key == pygame.K_LCTRL or event.key == pygame.K_RCTRL: return # check for control key sequences pressed if (event.mod == pygame.KMOD_CTRL or event.mod == pygame.KMOD_LCTRL or event.mod == pygame.KMOD_RCTRL): if kb_table_control.has_key(event.key): handler = kb_table_control[event.key] handler() return # check for normal keys pressed if kb_table_normal.has_key(event.key): handler = kb_table_normal[event.key] handler() return # all other keys that are printable, insert them on screen if event.key > 31 and event.key < 255: ed_insert(event) return print 'unhandled key', event.key def inbound(data, address): global size if opt.verbose: print 'recv', data, address if data[:6] == '2 text': (a, b) = data[7:].split(' ', 1) size = int(a) udp.send('3 accept ' + str(size) + ' ' + b) emit(b) size = opt.size elif data[:7] == '3 accept': (host, port) = address if opt.master: show_alive(host) elif data[:7] == '1 alive': (host, port) = address if opt.master: show_alive(host) from optparse import OptionParser parser = OptionParser(usage="usage: %prog [options] message", version="%prog 0.2") parser.add_option("--port", type="int", dest="port", default="8521", help="UDP port number for communication") parser.add_option("--updates", type="int", dest="updates", default="10", help="updates per second, default 10") parser.add_option("--size", type="int", dest="size", default="900", help="initial font size, default 900") parser.add_option("--verbose", action="store_true", dest="verbose", default=False, help="generate verbose output") parser.add_option("--master", action="store_true", dest="master", default=False, help="this is a master station") parser.add_option("--slave", action="store_true", dest="slave", default=False, help="this is a slave station, disable input") parser.add_option("--invert", action="store_true", dest="invert", default=False, help="display is mounted upside down") parser.add_option("--no-license", action="store_true", dest="no_license", default=False, help="do not display license") (opt, args) = parser.parse_args() if not opt.no_license: for line in license: print line pygame.init() width, height = 1200, 900 screen = pygame.display.set_mode((width, height)) pygame.mouse.set_visible(False) background = screen.copy() background.fill((0, 0, 0)) fc = FontCache() if not opt.no_license: show_license() mf = StdoutBuffer() ii = code.InteractiveInterpreter() udp = UDP() udp.send('0 start') alive = 0 text = '' size = opt.size history = {} current = cursor = 0 while True: # handle any quit or keyboard events from pygame interface for event in pygame.event.get(): if event.type == pygame.QUIT: sys.exit() elif event.type == pygame.KEYDOWN: kb(event) # handle any network packets from peers pair = udp.recv() if pair != None: (data, address) = pair inbound(data, address) # occasionally inform peers we are alive alive = alive + 1 if alive > 99: udp.send('1 alive') alive = 0