Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: tinue/apa102-pi
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: main
Choose a base ref
...
head repository: LoganK/APA102_Pi
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: master
Choose a head ref
Can’t automatically merge. Don’t worry, you can still create the pull request.

Commits on Jan 30, 2018

  1. Copy the full SHA
    34a2626 View commit details

Commits on Feb 2, 2018

  1. Copy the full SHA
    d62d547 View commit details
  2. Copy the full SHA
    7a1ebb3 View commit details
  3. Copy the full SHA
    4bcfdfc View commit details
  4. Copy the full SHA
    5b76b87 View commit details
  5. Copy the full SHA
    fa3d0f4 View commit details
  6. Copy the full SHA
    0ad1d80 View commit details
  7. Copy the full SHA
    60e161e View commit details
  8. Copy the full SHA
    082c4f9 View commit details

Commits on Feb 3, 2018

  1. Copy the full SHA
    0012298 View commit details
  2. Add Pixel.BLACK

    LoganK committed Feb 3, 2018
    Copy the full SHA
    11e5d50 View commit details
  3. Copy the full SHA
    361b1f6 View commit details
  4. Copy the full SHA
    9c8ebf0 View commit details
  5. Copy the full SHA
    41cb63c View commit details
  6. Support 'with' for APA102

    LoganK committed Feb 3, 2018
    Copy the full SHA
    2d69975 View commit details
  7. Copy the full SHA
    255f8ac View commit details
  8. Copy the full SHA
    eedadb4 View commit details
  9. Copy the full SHA
    0791181 View commit details
  10. Template: More accurate timer cadence

    Also, by moving the pause between calculation and show we drastically reduce the
    risk of st-st-st-stutter.
    LoganK committed Feb 3, 2018
    Copy the full SHA
    97edd36 View commit details
  11. Copy the full SHA
    22a4736 View commit details
  12. Reduce syscalls during write

    I'm hoping that one side effect of this will be improved clocking of data at the
    end of the strip as the end_frame delay will no longer exist.
    
    Also fix the 1024 LED limitation while I'm in here.
    LoganK committed Feb 3, 2018
    Copy the full SHA
    9536ddd View commit details
  13. Copy the full SHA
    41d91db View commit details
  14. Copy the full SHA
    1d76b0e View commit details
  15. Copy the full SHA
    23f8170 View commit details
  16. Copy the full SHA
    cdaf50f View commit details
  17. Obligatory Morse messaging

    LoganK committed Feb 3, 2018
    Copy the full SHA
    b890199 View commit details
  18. Copy the full SHA
    e6073bc View commit details
  19. Copy the full SHA
    6cb4f98 View commit details
  20. Copy the full SHA
    2102ea0 View commit details
  21. Copy the full SHA
    b244e1c View commit details

Commits on Feb 4, 2018

  1. Copy the full SHA
    4a8978b View commit details
  2. Copy the full SHA
    1ea83e3 View commit details
  3. Performance improvements

    36% faster for core library, 60% faster when using the debug output on Pattern 5
    LoganK committed Feb 4, 2018
    Copy the full SHA
    605454d View commit details

Commits on Mar 4, 2018

  1. Copy the full SHA
    2d2924f View commit details
  2. fixed stability issue on color calculation when using longer strand l…

    …engths, by specifying the max brightness
    krux702 committed Mar 4, 2018
    Copy the full SHA
    4109043 View commit details
  3. Merge pull request #1 from krux702/master

    Fix when using rainbow color scheme on longer strands
    LoganK authored Mar 4, 2018
    Copy the full SHA
    560802c View commit details
Showing with 703 additions and 132 deletions.
  1. +198 −61 apa102.py
  2. +48 −14 colorcycletemplate.py
  3. +259 −18 colorschemes.py
  4. +37 −0 debug.py
  5. +59 −0 examples_larson.py
  6. +94 −33 runcolorcycle.py
  7. +8 −6 sample.py
259 changes: 198 additions & 61 deletions apa102.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,86 @@
"""This is the main driver module for APA102 LEDs"""
import Adafruit_GPIO as GPIO
import Adafruit_GPIO.SPI as SPI
from math import ceil
from collections import namedtuple
import functools
import itertools
from types import MethodType

import debug

RGB_MAP = { 'rgb': [3, 2, 1], 'rbg': [3, 1, 2], 'grb': [2, 3, 1],
'gbr': [2, 1, 3], 'brg': [1, 3, 2], 'bgr': [1, 2, 3] }

# Red, Green, Blue are supposed to be in the range 0--255. Brightness is in the
# range 0--100.
Pixel = namedtuple('Pixel', 'red green blue brightness')
Pixel.RED = Pixel(255, 0, 0, 100)
Pixel.YELLOW = Pixel(255, 255, 0, 100)
Pixel.GREEN = Pixel(0, 255, 0, 100)
Pixel.CYAN = Pixel(0, 255, 255, 100)
Pixel.BLUE = Pixel(0, 0, 255, 100)
Pixel.MAGENTA = Pixel(255, 0, 255, 100)
Pixel.BLACK = Pixel(0, 0, 0, 0)
Pixel.WHITE = Pixel(255, 255, 255, 100)

def clamp(val, min_val, max_val):
"""Return the value clamped within the range [min_val, max_val]."""
if val < min_val:
val = min_val
elif val > max_val:
val = max_val
return val

class APA102Cmd:
"""Helper class to convert Pixel instance to an APA102 command.
"""
LED_START = 0b11100000 # Three "1" bits, followed by 5 brightness bits
BRIGHTNESS = 0b00011111

@staticmethod
def bright_percent(pct):
""" Scales the given percent to a value within the hardware range. """
ret = clamp(ceil(pct * APA102Cmd.BRIGHTNESS / 100.0), 0, APA102Cmd.BRIGHTNESS)
return ret

def bright_cmd(self, pixel):
return Pixel(pixel.red, pixel.green, pixel.blue,
APA102Cmd.bright_percent(round(pixel.brightness * self.max_brightness / 100)))

def bright_color(self, pixel):
scale = pixel.brightness * self.max_brightness / 100 / 100
return Pixel(*[round(n*scale) for n in pixel[0:3]], brightness=APA102Cmd.BRIGHTNESS)

def __init__(self, rgb_map, max_brightness, bright_rgb=True):
"""Set some global options for our LED type.
Params:
bright_rgb - If True, scale the brightness by adjusting the RGB values
instead of the brightness command. This may result in less visible
flicker (19.2kHz vs 582Hz). See
https://cpldcpu.wordpress.com/2014/08/27/apa102/
"""
self.rgb_map = rgb_map
self.max_brightness = max_brightness
self.bright = MethodType(APA102Cmd.bright_color if bright_rgb else APA102Cmd.bright_cmd, self)

@functools.lru_cache(maxsize=1024)
def to_cmd(self, pixel):
## if not isinstance(pixel, Pixel):
## raise TypeError("expected Pixel")

pixel = self.bright(pixel)
# LED startframe is three "1" bits, followed by 5 brightness bits
ledstart = pixel.brightness | self.LED_START

cmd = [ledstart, 0, 0, 0]

# Note that rgb_map is 1-indexed for historial reasons, but is convenient here.
cmd[self.rgb_map[0]] = clamp(pixel.red, 0, 255)
cmd[self.rgb_map[1]] = clamp(pixel.green, 0, 255)
cmd[self.rgb_map[2]] = clamp(pixel.blue, 0, 255)

return cmd


class APA102:
"""
Driver for APA102 LEDS (aka "DotStar").
@@ -65,40 +140,62 @@ class APA102:
information is still with person 2. Essentially the driver sends additional
zeroes to LED 1 as long as it takes for the last color frame to make it
down the line to the last LED.
"""
# Constants
MAX_BRIGHTNESS = 31 # Safeguard: Max. brightness that can be selected.
LED_START = 0b11100000 # Three "1" bits, followed by 5 brightness bits

def __init__(self, num_led, global_brightness=MAX_BRIGHTNESS,
order='rgb', mosi=10, sclk=11, max_speed_hz=8000000):
"""Initializes the library.
"""
self.num_led = num_led # The number of LEDs in the Strip
order = order.lower()
self.rgb = RGB_MAP.get(order, RGB_MAP['rgb'])
# Limit the brightness to the maximum if it's set higher
if global_brightness > self.MAX_BRIGHTNESS:
self.global_brightness = self.MAX_BRIGHTNESS
else:
self.global_brightness = global_brightness
self.leds = [self.LED_START,0,0,0] * self.num_led # Pixel buffer

# MOSI 10 and SCLK 11 is hardware SPI, which needs to be set-up differently
if mosi == 10 and sclk == 11:
self.spi = SPI.SpiDev(0, 0, max_speed_hz) # Bus 0, chip select 0
Params:
led_order - If set, allow the use of a logical order that doesn't match
the physical strip given as a sequence of (first, last) ranges. As a
compex example, if your strips were connected as:
5-6-7-8-0-1-2-3-12-11-10-9
then you could set led_order=((5, 8), (0, 3), (12, 9))
Tip: runcolorcycle.py can be useful to verify you have these values correct.
"""
def __init__(self,
num_led,
global_brightness=100,
order='rgb',
mosi=10,
sclk=11,
bus=0,
device=0,
max_speed_hz=8000000,
led_order=None):
"""Initializes the library."""

rgb_map = RGB_MAP[order.lower()]
self.pixel_cmd = APA102Cmd(rgb_map, global_brightness)
self.leds = [Pixel.BLACK] * num_led
self.led_order = led_order
self._assert_led_order()
self.BRIGHTNESS = APA102Cmd.BRIGHTNESS

if mosi is None or mosi < 0: # Debug output
# Reset leds_seq so the terminal output makes sense.
self.led_order = None
self.spi = debug.DummySPI(rgb_map)
else:
self.spi = SPI.BitBang(GPIO.get_platform_gpio(), sclk, mosi)
import Adafruit_GPIO.SPI as SPI
# MOSI 10 and SCLK 11 is hardware SPI, which needs to be set-up differently
if mosi == 10 and sclk == 11:
self.spi = SPI.SpiDev(bus, device, max_speed_hz)
else:
import Adafruit_GPIO as GPIO
self.spi = SPI.BitBang(GPIO.get_platform_gpio(), sclk, mosi)

def _assert_led_order(self):
"""Raise a ValueError if the given led_order isn't correct."""

found = set(self.order_iter())
need = set(range(len(self.leds)))
if found != need:
raise ValueError('led_order has gap and/or extra: {}'.format(need.symmetric_difference(found)))

def clock_start_frame(self):
"""Sends a start frame to the LED strip.
This method clocks out a start frame, telling the receiving LED
that it must update its own color now.
"""
self.spi.write([0] * 4) # Start frame, 32 zero bits
return [0] * 4 # Start frame, 32 zero bits


def clock_end_frame(self):
@@ -129,45 +226,56 @@ def clock_end_frame(self):
been sent as part of "clockEndFrame".
"""
# Round up num_led/2 bits (or num_led/16 bytes)
for _ in range((self.num_led + 15) // 16):
self.spi.write([0x00])
return [0x00] * ceil(self.num_led / 16)


def blank(self):
""" Turns off the strip. """
self.leds = [Pixel.BLACK] * self.num_led


def clear_strip(self):
""" Turns off the strip and shows the result right away."""

for led in range(self.num_led):
self.set_pixel(led, 0, 0, 0)
self.blank()
self.show()


@property
def num_led(self):
return len(self.leds)


def __getitem__(self, key):
return self.leds[key]


def __setitem__(self, key, item):
""" Allows for setting lights using array syntax:
strip[12] = Pixel(255, 255, 0, 50) # 50% Brightness yellow
or
strip[12] = (255, 255, 0) # 100% Brightness yellow
"""
if isinstance(item, Pixel):
self.leds[key] = item
elif len(item) == 4:
self.leds[key] = Pixel(*item)
elif len(item) == 3:
self.leds[key] = Pixel(*item, brightness=100)
else:
raise ValueError('unknown type for Pixel: {}'.format(item))


def set_pixel(self, led_num, red, green, blue, bright_percent=100):
"""Sets the color of one pixel in the LED stripe.
The changed pixel is not shown yet on the Stripe, it is only
written to the pixel buffer. Colors are passed individually.
If brightness is not set the global brightness setting is used.
"""
if led_num < 0:
return # Pixel is invisible, so ignore
if led_num >= self.num_led:
return # again, invisible

# Calculate pixel brightness as a percentage of the
# defined global_brightness. Round up to nearest integer
# as we expect some brightness unless set to 0
brightness = ceil(bright_percent*self.global_brightness/100.0)
brightness = int(brightness)

# LED startframe is three "1" bits, followed by 5 brightness bits
ledstart = (brightness & 0b00011111) | self.LED_START

start_index = 4 * led_num
self.leds[start_index] = ledstart
self.leds[start_index + self.rgb[0]] = red
self.leds[start_index + self.rgb[1]] = green
self.leds[start_index + self.rgb[2]] = blue
if led_num < 0 or led_num >= self.num_led:
raise ValueError('attempt to set invalid LED: {}'.format(led_num))

self.leds[led_num] = Pixel(red, green, blue, bright_percent)

def set_pixel_rgb(self, led_num, rgb_color, bright_percent=100):
"""Sets the color of one pixel in the LED stripe.
@@ -177,6 +285,11 @@ def set_pixel_rgb(self, led_num, rgb_color, bright_percent=100):
Colors are passed combined (3 bytes concatenated)
If brightness is not set the global brightness setting is used.
"""
if isinstance(rgb_color, str):
ln = len(rgb_color)
return self.set_pixel(led_num,
*(int(rgb_color[i:i + ln // 3], 16) for i in range(0, ln, ln // 3)),
bright_percent=bright_percent)
self.set_pixel(led_num, (rgb_color & 0xFF0000) >> 16,
(rgb_color & 0x00FF00) >> 8, rgb_color & 0x0000FF,
bright_percent)
@@ -189,21 +302,45 @@ def rotate(self, positions=1):
the specified number of positions. The number could be negative,
which means rotating in the opposite direction.
"""
cutoff = 4 * (positions % self.num_led)
cutoff = positions % self.num_led
self.leds = self.leds[cutoff:] + self.leds[:cutoff]


def order_iter(self):
"""Convert a user sequence of led_order tuples to a linear order."""

if self.led_order is None:
return range(self.num_led)

order = []
for s in led_order:
if s[0] < s[1]:
order.append(range(s[0], s[1] + 1, 1))
else:
order.append(range(s[1], s[0] - 1, -1))

return itertools.chain(*order)

def show(self):
"""Sends the content of the pixel buffer to the strip.
"""Sends the content of the pixel buffer to the strip."""

Todo: More than 1024 LEDs requires more than one xfer operation.
"""
self.clock_start_frame()
# xfer2 kills the list, unfortunately. So it must be copied first
cmds = self.clock_start_frame()
# SPI takes up to 4096 Integers. So we are fine for up to 1024 LEDs.
self.spi.write(list(self.leds))
self.clock_end_frame()
for led_i in self.order_iter():
cmds.extend(self.pixel_cmd.to_cmd(self.leds[led_i]))
cmds.extend(self.clock_end_frame())

for i in range(0, len(cmds), 4096):
sub_cmd = cmds[i:i+4096]
self.spi.write(sub_cmd)


def __enter__(self):
return self

def __exit__(self, exception_type, exception_value, traceback):
self.clear_strip()
self.cleanup()

def cleanup(self):
"""Release the SPI device; Call this method at the end"""
@@ -222,9 +359,9 @@ def wheel(self, wheel_pos):

if wheel_pos > 255:
wheel_pos = 255 # Safeguard
if wheel_pos < 85: # Green -> Red
if wheel_pos <= 85: # Green -> Red
return self.combine_color(wheel_pos * 3, 255 - wheel_pos * 3, 0)
if wheel_pos < 170: # Red -> Blue
if wheel_pos <= 170: # Red -> Blue
wheel_pos -= 85
return self.combine_color(255 - wheel_pos * 3, 0, wheel_pos * 3)
# Blue -> Green
Loading