You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

444 lines
13 KiB

"""
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