diff --git a/CHANGELIST.md b/CHANGELIST.md index 958cfae47..13f348c76 100644 --- a/CHANGELIST.md +++ b/CHANGELIST.md @@ -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 diff --git a/bibliopixel/__init__.py b/bibliopixel/__init__.py index bda72d79e..3b2370477 100644 --- a/bibliopixel/__init__.py +++ b/bibliopixel/__init__.py @@ -2,4 +2,4 @@ import log import colors -VERSION = '1.1.7' \ No newline at end of file +VERSION = '1.2.0' \ No newline at end of file diff --git a/bibliopixel/animation.py b/bibliopixel/animation.py index 8cfc524a5..43e86124d 100644 --- a/bibliopixel/animation.py +++ b/bibliopixel/animation.py @@ -3,8 +3,27 @@ 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 @@ -12,6 +31,9 @@ def __init__(self, led): 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 @@ -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 @@ -33,7 +67,6 @@ 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 @@ -41,7 +74,7 @@ def run(self, amt = 1, fps=None, sleep=None, max_steps = 0, untilComplete = Fals 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() @@ -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: @@ -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) @@ -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) diff --git a/bibliopixel/drivers/network_receiver.py b/bibliopixel/drivers/network_receiver.py index 99b8b6fde..b9f104672 100644 --- a/bibliopixel/drivers/network_receiver.py +++ b/bibliopixel/drivers/network_receiver.py @@ -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): diff --git a/bibliopixel/drivers/serial_driver.py b/bibliopixel/drivers/serial_driver.py index 4df71134c..5b8ba9341 100644 --- a/bibliopixel/drivers/serial_driver.py +++ b/bibliopixel/drivers/serial_driver.py @@ -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 @@ -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 @@ -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) @@ -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") @@ -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 @@ -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) @@ -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) @@ -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 @@ -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): diff --git a/bibliopixel/gamma.py b/bibliopixel/gamma.py index d7c10c932..477f89d14 100644 --- a/bibliopixel/gamma.py +++ b/bibliopixel/gamma.py @@ -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)] diff --git a/bibliopixel/led.py b/bibliopixel/led.py index 3e0610410..5e879c36d 100644 --- a/bibliopixel/led.py +++ b/bibliopixel/led.py @@ -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) \ No newline at end of file