""" This module is part of the 'blup' package and provides several output modules to send frames as well as a mechanism to create them from a string specification. """ import re import serial import socket from blup import BLP # # ---- exceptions ---- # class IllegalOutputSpecificationError(Exception): """ Will be thrown when a string is given to getOutput (see below) that is not matched by any output modules regex. """ def __str__(self): return __class__.__name__ # # ---- generic output class to be subclassed ---- # class Output(object): def sendFrame(self, frame): raise Exception('not implemented - just a dummy class') # # ---- MCUFOutput (MicroController Unit Frame) ---- # class MCUFOutput(Output): """ This output module allows to send frames to blinkendevices over a serial line using the MCUF protocol. """ moduleRegexDesc = 'mcuf:DEVICE:BAUDRATE[:DEPTH]' moduleRegex = '^mcuf:([^:]+):(\d+)(:(\d+))?$' def __init__(self, port, baudrate, depth=0): """ Initialize the module with given port and baudrate. If depth is specified, every frame will be transformed, if it not already has the correct depth. """ self.__port = port self.__baudrate = baudrate self.__ser = serial.Serial(port=port, baudrate=baudrate) self.__depth = depth @classmethod def fromRegexMatch(cls, regexMatch): """ Create an instance from a RegexMatch (matched with moduleRegex) """ port = regexMatch.group(1) baudrate = int(regexMatch.group(2)) if regexMatch.group(4): depth = int(regexMatch.group(4)) else: depth = 0 return cls(port, baudrate, depth) def sendFrame(self, frame): """ Pass a frame to the module. """ (w, h) = frame.size if self.__depth != 0: if frame.depth != self.__depth: frame = frame.transform(self.__depth) maxval = frame.depth - 1 framedata = '' for y in range(h): for x in range(w): framedata += chr(frame.getPixel(x, y)) packet = '\x23\x54\x26\x66' packet += '%s%s%s%s' % (chr(h>>8), chr(h & 0xff), chr(w>>8), chr(w & 0xff)) packet += '\x00\x01\x00%s' % (chr(maxval)) packet += framedata self.__ser.write(packet) return True # # ---- SerialBlupOutput ---- # class SerialBlupOutput(Output): """ This output module allows sending frames to 18x8 blinkendevices with depth 2 over a serial line, by sending every column as a byte. """ moduleRegexDesc = 'serialblup:DEVICE:BAUDRATE' moduleRegex = '^serialblup:([^:]+):(\d+)$' def __init__(self, port, baudrate): """ Initialize the output module with the given port. """ self.__port = port self.__baudrate = baudrate self.__ser = serial.Serial(port=port, baudrate=baudrate) self.__stx2 = 'a' self.__etx2 = 'b' self.__stx8 = 'q' self.__etx8 = 'b' self.__stxRGB = chr(0xca) self.__etxRGB = chr(0xfe) @classmethod def fromRegexMatch(cls, regexMatch): """ Create an instance from a RegexMatch (matched with moduleRegex) """ port = regexMatch.group(1) baudrate = int(regexMatch.group(2)) return cls(port, baudrate) def sendFrame(self, frame): """ Pass a frame to the module. """ if frame.depth not in [2, 16] and frame.channels == 1: raise ValueError('%s only supports frames of depth 2 or 16' % (self.__class__.__name__)) if frame.channels not in [1, 3]: raise ValueError('%s only supports frames with 1 or 3 channels' % (self.__class__.__name__)) if frame.size != (18, 8): raise ValueError('%s currently only supports 18x8 frames' % (self.__class__.__name__)) if frame.channels == 1 and frame.depth == 2: bytes = [ 0 ] * 18 for x in range(18): for y in range(8): if frame.getPixel(x, y) == 1: bytes[x] += 2**(7 - y) packet = self.__stx2 + ''.join(map(lambda b: chr(b), bytes)) + self.__etx2 elif frame.channels == 1 and frame.depth == 16: bytes = [ 0 ] * (18*4) for x in range(18): for y in range(8): pixelval = frame.getPixel(x, y) pixelnum = y*18 + x bytes[pixelnum/2] += (pixelval << ((pixelnum % 2) * 4)) packet = self.__stx8 + ''.join(map(lambda b: chr(b), bytes)) + self.__etx8 elif frame.channels == 3: bytes = [ 0 ] * (18*8*3) for y in range(8): for x in range(18): (r,g,b) = frame.getPixel(x, y) pixelnum = y*18 + x bytes[3*pixelnum] = r bytes[3*pixelnum + 1] = g bytes[3*pixelnum + 2] = b packet = self.__stxRGB + ''.join(map(lambda b: chr(b), bytes)) + self.__etxRGB self.__ser.write(packet) return True # # ---- ShellOuput ---- # class ShellOutput(Output): """ This output module prints frames with depth 2 on the terminal. """ moduleRegexDesc = 'shell[:ON_CHAR:OFF_CHAR]' moduleRegex = '^shell(:(.):(.))?$' def __init__(self, onChar='#', offChar=' '): """ Initialize the output. """ self.__onChar = onChar self.__offChar = offChar self.__initialized = False @classmethod def fromRegexMatch(cls, regexMatch): """ Create an instance from a RegexMatch (matched with moduleRegex) """ if regexMatch.group(2) is not None: return cls(regexMatch.group(2), regexMatch.group(3)) else: return cls() def sendFrame(self, frame): """ Pass a frame to the module. """ if frame.depth != 2: raise ValueError('%s only supports frames of depth 2' % (self.__class__.__name__)) (width, height) = frame.size if not self.__initialized: self.__initialized = True print('\n' * height) print('\x1b[%dA' % (height + 2)) for y in range(height): line = '' for x in range(width): if frame.getPixel(x, y) == 1: line += self.__onChar else: line += self.__offChar print(line) print('') return True # # ---- ColorfulShellOuput ---- # class ColorfulShellOutput(Output): """ This output module prints frames on the terminal, using colors. """ moduleRegexDesc = 'colorfulshell' moduleRegex = '^colorfulshell?$' def __init__(self): """ Initialize the output. """ self.__initialized = False @classmethod def fromRegexMatch(cls, regexMatch): """ Create an instance from a RegexMatch (matched with moduleRegex) """ return cls() def sendFrame(self, frame): """ Pass a frame to the module. """ if frame.channels not in [1, 3]: raise ValueError('%s only supports frames with 1 or 3 channels' % (self.__class__.__name__)) (width, height) = frame.size if not self.__initialized: self.__initialized = True print('\n' * height) print('\x1b[%dA' % (height + 2)) for y in range(height): line = '' for x in range(width): if frame.channels == 3: (r,g,b) = frame.getPixel(x, y) if frame.depth != 5: factor = 5 / ((frame.depth - 1) * 1.0) r = round(r*factor) g = round(g*factor) b = round(b*factor) colorval = 36*r + 6*g + b + 16 line += '\033[48;05;%dm ' % (colorval) elif frame.channels == 1: colorval = frame.getPixel(x, y) if frame.depth != 24: factor = (23 / ((frame.depth - 1) * 1.0)) colorval = 232 + colorval * factor line += '\033[48;05;%dm ' % (colorval) print(line) print('') return True # # ---- BLPOutput ---- # class BLPOutput(Output): """ This output module sends frames over the network using the UDP-based BLP protocol. """ moduleRegexDesc = 'blp[:HOST:PORT]' moduleRegex = '^(blp|eblp|e3blp)(:(.+):(\d+))?$' def __init__(self, host='127.0.0.1', port=4242, protocol=BLP.PROTO_BLP): """ Initialize the output. Frames are sent to host:port. """ self.__host = host self.__port = port self.__sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.__protocol = protocol @classmethod def fromRegexMatch(cls, regexMatch): """ Create an instance from a RegexMatch (matched with moduleRegex) """ if regexMatch.group(1) == 'blp': protocol = BLP.PROTO_BLP elif regexMatch.group(1) == 'eblp': protocol = BLP.PROTO_EBLP elif regexMatch.group(1) == 'e3blp': protocol = BLP.PROTO_E3BLP if regexMatch.group(3) and regexMatch.group(4): host = regexMatch.group(3) port = int(regexMatch.group(4)) return cls(host, port, protocol) else: return cls(protocol=protocol) def sendFrame(self, frame): """ Pass a frame to the module. """ packet = BLP.frame2blp(frame, protocol=self.__protocol) self.__sock.sendto(packet, (self.__host, self.__port)) return True # # ---- BlinkenbuntHDOutput ---- # class BlinkenbuntHDOutput(Output): moduleRegexDesc = 'bbunthd' moduleRegex = '^bbunthd$' def __init__(self): import neopixel self.w = 22 self.h = 16 LED_COUNT = self.w * self.h LED_PIN = 13 LED_FREQ_HZ = 800000 LED_DMA = 5 LED_BRIGHTNESS = 50 LED_INVERT = False LED_PWM = 1 self.strip = neopixel.Adafruit_NeoPixel( LED_COUNT, LED_PIN, LED_FREQ_HZ, LED_DMA, LED_INVERT, LED_BRIGHTNESS, LED_PWM ) self.strip.begin() self._color_cls = neopixel.Color @classmethod def fromRegexMatch(cls, regexMatch): return cls() def sendFrame(self, frame): for y in range(self.h): for x in range(self.w): if x%2 == 0: lednum = x*self.h + y else: lednum = x*self.h - y + 15 pix = frame.getPixel(x, y) print(pix) self.strip.setPixelColor(lednum, self._color_cls(*pix)) self.strip.show() # # ---- PygameOutput ---- # class PygameOutput(Output): moduleRegexDesc = 'pygame' moduleRegex = '^pygame$' def __init__(self, width=22, height=16, scalex=8, scaley=8): self.width = width self.height = height self.scalex = scalex self.scaley = scaley import pygame self.__pygame = pygame self.screen = pygame.display.set_mode((width*scalex, height*scaley)) pygame.display.update() @classmethod def fromRegexMatch(cls, regexMatch): return cls() def sendFrame(self, frame): pygame = self.__pygame for y in range(self.height): for x in range(self.width): rect = (x*self.scalex, y*self.scaley, self.scalex, self.scaley) pygame.draw.rect(self.screen, frame.getPixel(x, y), rect, 0) pygame.display.update() #import time #time.sleep(0.1) # # ---- output creator ;) ---- # __outputModules = { SerialBlupOutput.moduleRegex: SerialBlupOutput, MCUFOutput.moduleRegex: MCUFOutput, ShellOutput.moduleRegex: ShellOutput, ColorfulShellOutput.moduleRegex: ColorfulShellOutput, BLPOutput.moduleRegex: BLPOutput, BlinkenbuntHDOutput.moduleRegex: BlinkenbuntHDOutput, PygameOutput.moduleRegex: PygameOutput, } def getOutput(outputSpec): """ Return an output module instance described by the given specification. """ for regex in __outputModules.keys(): m = re.match(regex, outputSpec) if m: return __outputModules[regex].fromRegexMatch(regexMatch=m) raise IllegalOutputSpecificationError() def getOutputDescriptions(): """ Return a string containing the moduleRegexDesc of every available output module. """ s = "" for outputModule in __outputModules.values(): s += " %s\n" % (outputModule.moduleRegexDesc) return s