Skip to content

Commit

Permalink
Merge pull request #17 from ManiacalLabs/v1.2
Browse files Browse the repository at this point in the history
V1.2
  • Loading branch information
adammhaile committed May 4, 2015
2 parents 1a43a4f + 91cace2 commit c2de87d
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 17 deletions.
6 changes: 6 additions & 0 deletions CHANGELIST.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
### v1.2.0
Added LEDPOV, LEDCircle Classes
Added Serial Device Version Support
Added threaded animation support
Improved Serial Device detection

### v1.1.7
Added check for pyserial version
Forced case insenitive grep for USB ID
Expand Down
2 changes: 1 addition & 1 deletion bibliopixel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
import log
import colors

VERSION = '1.1.7'
VERSION = '1.2.0'
70 changes: 65 additions & 5 deletions bibliopixel/animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,37 @@

from led import LEDMatrix
from led import LEDStrip
from led import LEDCircle
import colors

import threading

class animThread(threading.Thread):

def __init__(self, anim, args):
super(animThread, self).__init__()
self.setDaemon(True)
self._anim = anim
self._args = args

# def stopped(self):
# return self._anim._stopThread

def run(self):
log.logger.debug("Starting thread...")
self._anim._run(**self._args)
log.logger.debug("Thread Complete")

class BaseAnimation(object):
def __init__(self, led):
self._led = led
self.animComplete = False
self._step = 0
self._timeRef = 0
self._internalDelay = None
self._threaded = False
self._stopThread = False
self._thread = None

def _msTime(self):
return time.time() * 1000.0
Expand All @@ -22,7 +44,19 @@ def preRun(self):
def step(self, amt = 1):
raise RuntimeError("Base class step() called. This shouldn't happen")

def run(self, amt = 1, fps=None, sleep=None, max_steps = 0, untilComplete = False, max_cycles = 0):
def stopThread(self, wait = False):
if self._thread:
self._stopThread = True
if wait:
self._thread.join()

def stopped(self):
if self._thread:
return not self._thread.isAlive()
else:
return True

def _run(self, amt, fps, sleep, max_steps, untilComplete, max_cycles):
"""
untilComplete makes it run until the animation signals it has completed a cycle
max_cycles should be used with untilComplete to make it run for more than one cycle
Expand All @@ -33,15 +67,14 @@ def run(self, amt = 1, fps=None, sleep=None, max_steps = 0, untilComplete = Fals
if sleep == None and fps != None:
sleep = int(1000 / fps)


initSleep = sleep

self._step = 0
cur_step = 0
cycle_count = 0
self.animComplete = False

while (not untilComplete and (max_steps == 0 or cur_step < max_steps)) or (untilComplete and not self.animComplete):
while not self._stopThread and ((not untilComplete and (max_steps == 0 or cur_step < max_steps)) or (untilComplete and not self.animComplete)):
self._timeRef = self._msTime()

start = self._msTime()
Expand Down Expand Up @@ -72,8 +105,6 @@ def run(self, amt = 1, fps=None, sleep=None, max_steps = 0, untilComplete = Fals
updateTime = int(now - mid)
totalTime = stepTime + updateTime



if self._led._threadedUpdate:
log.logger.debug("Frame: {}ms / Update Max: {}ms".format(stepTime, updateTime))
else:
Expand All @@ -87,6 +118,23 @@ def run(self, amt = 1, fps=None, sleep=None, max_steps = 0, untilComplete = Fals
time.sleep(t)
cur_step += 1

def run(self, amt = 1, fps=None, sleep=None, max_steps = 0, untilComplete = False, max_cycles = 0, threaded = False, joinThread = False):

self._threaded = threaded
self._stopThread = False

if self._threaded:
args = locals()
args.pop('self', None)
args.pop('threaded', None)
args.pop('joinThread', None)
self._thread = animThread(self, args)
self._thread.start()
if joinThread:
self._thread.join()
else:
self._run(amt, fps, sleep, max_steps, untilComplete, max_cycles)

class BaseStripAnim(BaseAnimation):
def __init__(self, led, start = 0, end = -1):
super(BaseStripAnim, self).__init__(led)
Expand Down Expand Up @@ -122,6 +170,18 @@ def __init__(self, led, width=0, height=0, startX=0, startY=0):
self.startX = startX
self.startY = startY

class BaseCircleAnim(BaseAnimation):
def __init__(self, led):
super(BaseCircleAnim, self).__init__(led)

if not isinstance(led, LEDCircle):
raise RuntimeError("Must use LEDCircle with Circle Animations!")

self.rings = led.rings
self.ringCount = led.ringCount
self.lastRing = led.lastRing
self.ringSteps = led.ringSteps

class StripChannelTest(BaseStripAnim):
def __init__(self, led):
super(StripChannelTest, self).__init__(led)
Expand Down
3 changes: 2 additions & 1 deletion bibliopixel/drivers/network_receiver.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ def handle(self):
self.request.sendall(packet)

elif cmd == CMDTYPE.BRIGHTNESS:
bright = ord(self.request.recv(1))
res = self.request.recv(1)
bright = ord(res)
result = RETURN_CODES.ERROR_UNSUPPORTED
if self.server.setBrightness:
if self.server.setBrightness(bright):
Expand Down
61 changes: 51 additions & 10 deletions bibliopixel/drivers/serial_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class CMDTYPE:
BRIGHTNESS = 3 #data will be single 0-255 brightness value, length must be 0x00,0x01
GETID = 4
SETID = 5
GETVER = 6

class RETURN_CODES:
SUCCESS = 255 #All is well
Expand All @@ -33,6 +34,7 @@ class RETURN_CODES:
ERROR_SIZE = 1 #Data receieved does not match given command length
ERROR_UNSUPPORTED = 2 #Unsupported command
ERROR_PIXEL_COUNT = 3 #Too many pixels for device
ERROR_BAD_CMD = 4 #Unknown Command

class LEDTYPE:
GENERIC = 0 #Use if the serial device only supports one chipset
Expand Down Expand Up @@ -75,6 +77,7 @@ class DriverSerial(DriverBase):
"""Main driver for Serial based LED strips"""
foundDevices = []
deviceIDS = {}
deviceVers = []
def __init__(self, type, num, dev="", c_order = ChannelOrder.RGB, SPISpeed = 2, gamma = None, restart_timeout = 3, deviceID = None, hardwareID = "1D50:60AB"):
super(DriverSerial, self).__init__(num, c_order = c_order, gamma = gamma)

Expand All @@ -87,6 +90,7 @@ def __init__(self, type, num, dev="", c_order = ChannelOrder.RGB, SPISpeed = 2,
self._type = type
self._bufPad = 0
self.dev = dev
self.devVer = 0
self.deviceID = deviceID
if self.deviceID != None and (self.deviceID < 0 or self.deviceID > 255):
raise ValueError("deviceID must be between 0 and 255")
Expand Down Expand Up @@ -114,9 +118,12 @@ def findSerialDevices(hardwareID = "1D50:60AB"):
DriverSerial.foundDevices = []
DriverSerial.deviceIDS = {}
for port in serial.tools.list_ports.grep(hardwareID):
DriverSerial.foundDevices.append(port[0])
id = DriverSerial.getDeviceID(port[0])
DriverSerial.deviceIDS[id] = port[0]
ver = DriverSerial.getDeviceVer(port[0])
if id >= 0:
DriverSerial.deviceIDS[id] = port[0]
DriverSerial.foundDevices.append(port[0])
DriverSerial.deviceVers.append(ver)

return DriverSerial.foundDevices

Expand All @@ -129,8 +136,10 @@ def _printError(error):
msg = "Unsupported configuration attempted."
elif error == RETURN_CODES.ERROR_PIXEL_COUNT:
msg = "Too many pixels specified for device."
elif error == RETURN_CODES.ERROR_BAD_CMD:
msg = "Unsupported protocol command. Check your device version."

log.logger.error(msg)
log.logger.error("{}: {}".format(error, msg))
raise BiblioSerialError(msg)


Expand All @@ -148,20 +157,32 @@ def _connect(self):
if self.deviceID != None:
if self.deviceID in DriverSerial.deviceIDS:
self.dev = DriverSerial.deviceIDS[self.deviceID]
log.logger.info( "Using COM Port: {}, Device ID: {}".format(self.dev, self.deviceID))

self.devVer = 0
try:
i = DriverSerial.foundDevices.index(self.dev)
self.devVer = DriverSerial.deviceVers[i]
except:
pass
log.logger.info( "Using COM Port: {}, Device ID: {}, Device Ver: {}".format(self.dev, self.deviceID, self.devVer))

if self.dev == "":
error = "Unable to find device with ID: {}".format(self.deviceID)
log.logger.error(error)
raise ValueError(error)
elif len(DriverSerial.foundDevices) > 0:
self.dev = DriverSerial.foundDevices[0]
self.devVer = 0
try:
i = DriverSerial.foundDevices.index(self.dev)
self.devVer = DriverSerial.deviceVers[i]
except:
pass
devID = -1
for id in DriverSerial.deviceIDS:
if DriverSerial.deviceIDS[id] == self.dev:
devID = id

log.logger.info( "Using COM Port: {}, Device ID: {}".format(self.dev, devID))
log.logger.info( "Using COM Port: {}, Device ID: {}, Device Ver: {}".format(self.dev, devID, self.devVer))

try:
self._com = serial.Serial(self.dev, timeout=5)
Expand All @@ -177,8 +198,11 @@ def _connect(self):
packet.append(self._type) #set strip type
byteCount = self.bufByteCount
if self._type in BufferChipsets:
self._bufPad = BufferChipsets[self._type](self.numLEDs)*3
byteCount += self._bufPad
if self._type == LEDTYPE.APA102 and self.devVer >= 2:
pass
else:
self._bufPad = BufferChipsets[self._type](self.numLEDs)*3
byteCount += self._bufPad

packet.append(byteCount & 0xFF) #set 1st byte of byteCount
packet.append(byteCount >> 8) #set 2nd byte of byteCount
Expand Down Expand Up @@ -238,8 +262,25 @@ def getDeviceID(dev):
resp = ord(com.read(1))
return resp
except serial.SerialException as e:
#log.logger.error("Problem connecting to serial device.")
raise IOError("Problem connecting to serial device.")
log.logger.error("Problem connecting to serial device.")
return -1

@staticmethod
def getDeviceVer(dev):
packet = DriverSerial._generateHeader(CMDTYPE.GETVER, 0)
try:
com = serial.Serial(dev, timeout=0.5)
com.write(packet)
ver = 0
resp = com.read(1)
if len(resp) > 0:
resp = ord(resp)
if resp == RETURN_CODES.SUCCESS:
ver = ord(com.read(1))
return ver
except serial.SerialException as e:
log.logger.error("Problem connecting to serial device.")
return 0


def setMasterBrightness(self, brightness):
Expand Down
1 change: 1 addition & 0 deletions bibliopixel/gamma.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#From https://github.com/scottjgibson/PixelPi/blob/master/pixelpi.py
LPD8806 = [int(pow(float(i) / 255.0, 2.5) * 255.0 + 0.5) for i in range(256)]
APA102 = LPD8806
WS2801 = [int(pow(float(i) / 255.0, 2.5) * 255.0) for i in range(256)]
SM16716 = [int(pow(float(i) / 255.0, 2.5) * 255.0) for i in range(256)]
LPD6803 = [int(pow(float(i) / 255.0, 2.0) * 255.0 + 0.5) for i in range(256)]
Expand Down
93 changes: 93 additions & 0 deletions bibliopixel/led.py
Original file line number Diff line number Diff line change
Expand Up @@ -616,3 +616,96 @@ def drawText(self, text, x = 0, y = 0, color = colors.White, bg = colors.Off, si
if x >= self.width:
break

#Takes a matrix and displays it as individual columns over time
class LEDPOV(LEDMatrix):

def __init__(self, driver, povHeight, width, rotation = MatrixRotation.ROTATE_0, vert_flip = False):
self.numLEDs = povHeight * width

super(LEDPOV, self).__init__(driver, width, povHeight, None, rotation, vert_flip, False)

#This is the magic. Overriding the normal update() method
#It will automatically break up the frame into columns spread over frameTime (ms)
def update(self, frameTime = None):
if frameTime:
self._frameTotalTime = frameTime

sleep = None
if self._frameTotalTime:
sleep = (self._frameTotalTime - self._frameGenTime) / self.width

width = self.width
for h in range(width):
start = time.time() * 1000.0

buf = [item for sublist in [self.buffer[(width*i*3)+(h*3):(width*i*3)+(h*3)+(3)] for i in range(self.height)] for item in sublist]
self.driver.update(buf)
sendTime = (time.time() * 1000.0) - start
if sleep:
time.sleep(max(0, (sleep - sendTime) / 1000.0))


class LEDCircle(LEDBase):

def __init__(self, driver, rings, rotation = 0, threadedUpdate = False):
super(LEDCircle, self).__init__(driver, threadedUpdate)
self.rings = rings
self.ringCount = len(self.rings)
self.lastRing = self.ringCount - 1
self.ringSteps = []
num = 0
for r in self.rings:
count = (r[1] - r[0] + 1)
self.ringSteps.append(360.0/count)
num += count

self.rotation = rotation

if driver.numLEDs != num:
raise ValueError("Total ring LED count does not equal driver LED count!")

def angleToPixel(self, angle, ring):
if ring >= self.ringCount:
return -1

angle = (angle+self.rotation)%360
return self.rings[ring][0] + int(math.floor(angle/self.ringSteps[ring]))

#Set single pixel to Color value
def set(self, ring, angle, color):
"""Set pixel to RGB color tuple"""
pixel = self.angleToPixel(angle, ring)
self._set_base(pixel, color)

def get(self, ring, angle):
"""Get RGB color tuple of color at index pixel"""
pixel = self.angleToPixel(angle, ring)
return self._get_base(pixel)

def drawRadius(self, angle, color, startRing=0, endRing=-1):
if startRing < 0:
startRing = 0
if endRing < 0 or endRing > self.lastRing:
endRing = self.lastRing
for ring in range(startRing, endRing + 1):
self.set(ring, angle, color)

def fillRing(self, ring, color, startAngle=0, endAngle=None):
if endAngle == None:
endAngle = 359

if ring >= self.ringCount:
raise ValueError("Invalid ring!")

start = self.angleToPixel(startAngle, ring)
end = self.angleToPixel(endAngle, ring)
pixels = []
if start > end:
pixels = range(start, self.rings[ring][1]+1)
pixels.extend(range(self.rings[ring][0], end+1))
elif start == end:
pixels = range(self.rings[ring][0], self.rings[ring][1]+1)
else:
pixels = range(start, end+1);
for i in pixels:
self._set_base(i, color)

0 comments on commit c2de87d

Please sign in to comment.