289 lines
12 KiB
Python
289 lines
12 KiB
Python
|
import time
|
||
|
import numpy as np
|
||
|
import lib.config as config
|
||
|
|
||
|
_GAMMA_TABLE = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1,
|
||
|
1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5,
|
||
|
5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 11,
|
||
|
11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17,
|
||
|
18, 18, 19, 19, 20, 20, 21, 21, 22, 23, 23, 24, 24, 25,
|
||
|
26, 26, 27, 28, 28, 29, 30, 30, 31, 32, 32, 33, 34, 35,
|
||
|
35, 36, 37, 38, 38, 39, 40, 41, 42, 42, 43, 44, 45, 46,
|
||
|
47, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 56, 57, 58,
|
||
|
59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 73,
|
||
|
74, 75, 76, 77, 78, 79, 80, 81, 82, 84, 85, 86, 87, 88,
|
||
|
89, 91, 92, 93, 94, 95, 97, 98, 99, 100, 102, 103, 104,
|
||
|
105, 107, 108, 109, 111, 112, 113, 115, 116, 117, 119,
|
||
|
120, 121, 123, 124, 126, 127, 128, 130, 131, 133, 134,
|
||
|
136, 137, 139, 140, 142, 143, 145, 146, 148, 149, 151,
|
||
|
152, 154, 155, 157, 158, 160, 162, 163, 165, 166, 168,
|
||
|
170, 171, 173, 175, 176, 178, 180, 181, 183, 185, 186,
|
||
|
188, 190, 192, 193, 195, 197, 199, 200, 202, 204, 206,
|
||
|
207, 209, 211, 213, 215, 217, 218, 220, 222, 224, 226,
|
||
|
228, 230, 232, 233, 235, 237, 239, 241, 243, 245, 247,
|
||
|
249, 251, 253, 255]
|
||
|
_GAMMA_TABLE = np.array(_GAMMA_TABLE)
|
||
|
|
||
|
class LEDController:
|
||
|
"""Base class for interfacing with hardware LED strip controllers
|
||
|
To add support for another hardware device, simply inherit this class
|
||
|
and implement the show() method.
|
||
|
Example usage:
|
||
|
import numpy as np
|
||
|
N_pixels = 60
|
||
|
pixels = np.random.random(size=(3, N_pixels))
|
||
|
device = LEDController()
|
||
|
device.show(pixels)
|
||
|
"""
|
||
|
|
||
|
def __init__(self):
|
||
|
pass
|
||
|
|
||
|
def show(self, pixels):
|
||
|
"""Set LED pixels to the values given in the array
|
||
|
This function accepts an array of RGB pixel values (pixels)
|
||
|
and displays them on the LEDs. To add support for another
|
||
|
hardware device, you should create a class that inherits from
|
||
|
this class, and then implement this method.
|
||
|
Parameters
|
||
|
----------
|
||
|
pixels: numpy.ndarray
|
||
|
2D array containing RGB pixel values for each of the LEDs.
|
||
|
The shape of the array is (3, n_pixels), where n_pixels is the
|
||
|
number of LEDs that the device has.
|
||
|
The array is formatted as shown below. There are three rows
|
||
|
(axis 0) which represent the red, green, and blue color channels.
|
||
|
Each column (axis 1) contains the red, green, and blue color values
|
||
|
for a single pixel:
|
||
|
np.array([ [r0, ..., rN], [g0, ..., gN], [b0, ..., bN]])
|
||
|
Each value brightness value is an integer between 0 and 255.
|
||
|
Returns
|
||
|
-------
|
||
|
None
|
||
|
"""
|
||
|
raise NotImplementedError('Show() was not implemented')
|
||
|
|
||
|
def test(self, n_pixels):
|
||
|
pixels = np.zeros((3, n_pixels))
|
||
|
pixels[0][0] = 255
|
||
|
pixels[1][1] = 255
|
||
|
pixels[2][2] = 255
|
||
|
print('Starting LED strip test.')
|
||
|
print('Press CTRL+C to stop the test at any time.')
|
||
|
print('You should see a scrolling red, green, and blue pixel.')
|
||
|
while True:
|
||
|
self.show(pixels)
|
||
|
pixels = np.roll(pixels, 1, axis=1)
|
||
|
time.sleep(0.2)
|
||
|
|
||
|
|
||
|
class ESP8266(LEDController):
|
||
|
def __init__(self, auto_detect=False,
|
||
|
mac_addr="aa-bb-cc-dd-ee-ff",
|
||
|
ip='192.168.0.150',
|
||
|
port=7778):
|
||
|
"""Initialize object for communicating with as ESP8266
|
||
|
Parameters
|
||
|
----------
|
||
|
auto_detect: bool, optional
|
||
|
Automatically search for and find devices on windows hotspot
|
||
|
with given mac addresses. Windows hotspot resets the IP
|
||
|
addresses of any devices on reset, meaning the IP of the
|
||
|
ESP8266 changes every time you turn on the hotspot. This
|
||
|
will find the IP address of the devices for you.
|
||
|
mac_addr: str, optional
|
||
|
The MAC address of the ESP8266 on the network. Only used if
|
||
|
auto-detect is used
|
||
|
ip: str, optional
|
||
|
The IP address of the ESP8266 on the network. This must exactly
|
||
|
match the IP address of your ESP8266 device, unless using
|
||
|
the auto-detect feature.
|
||
|
port: int, optional
|
||
|
The port number to use when sending data to the ESP8266. This
|
||
|
must exactly match the port number in the ESP8266's firmware.
|
||
|
"""
|
||
|
import socket
|
||
|
self._mac_addr = mac_addr
|
||
|
self._ip = ip
|
||
|
self._port = port
|
||
|
self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||
|
if auto_detect:
|
||
|
self.detect()
|
||
|
|
||
|
def detect(self):
|
||
|
from subprocess import check_output
|
||
|
from time import sleep
|
||
|
""" Uses "arp -a" to find esp8266 on windows hotspot"""
|
||
|
# Find the audio strip automagically
|
||
|
ip_addr = False
|
||
|
while not ip_addr:
|
||
|
arp_out = check_output(['arp', '-a']).splitlines()
|
||
|
for i in arp_out:
|
||
|
if self._mac_addr in str(i):
|
||
|
ip_addr = i.split()[0].decode("utf-8")
|
||
|
break
|
||
|
else:
|
||
|
print("Device not found at physical address {}, retrying in 1s".format(self._mac_addr))
|
||
|
sleep(1)
|
||
|
print("Found device {}, with IP address {}".format(self._mac_addr, ip_addr))
|
||
|
self._ip = ip_addr
|
||
|
|
||
|
def show(self, pixels):
|
||
|
"""Sends UDP packets to ESP8266 to update LED strip values
|
||
|
The ESP8266 will receive and decode the packets to determine what values
|
||
|
to display on the LED strip. The communication protocol supports LED strips
|
||
|
with a maximum of 256 LEDs.
|
||
|
The packet encoding scheme is:
|
||
|
|i|r|g|b|
|
||
|
where
|
||
|
i (0 to 255): Index of LED to change (zero-based)
|
||
|
r (0 to 255): Red value of LED
|
||
|
g (0 to 255): Green value of LED
|
||
|
b (0 to 255): Blue value of LED
|
||
|
"""
|
||
|
message = pixels.T.clip(0, config.settings["configuration"]["MAX_BRIGHTNESS"]).astype(np.uint8).ravel().tostring()
|
||
|
self._sock.sendto(message, (self._ip, self._port))
|
||
|
|
||
|
|
||
|
class FadeCandy(LEDController):
|
||
|
def __init__(self, server='localhost:7890'):
|
||
|
"""Initializes object for communicating with a FadeCandy device
|
||
|
Parameters
|
||
|
----------
|
||
|
server: str, optional
|
||
|
FadeCandy server used to communicate with the FadeCandy device.
|
||
|
"""
|
||
|
try:
|
||
|
import audioled.opc
|
||
|
except ImportError as e:
|
||
|
print('Unable to import audioled.opc library')
|
||
|
print('You can install this library with `pip install opc`')
|
||
|
raise e
|
||
|
self.client = audioled.opc.Client(server)
|
||
|
if self.client.can_connect():
|
||
|
print('Successfully connected to FadeCandy server.')
|
||
|
else:
|
||
|
print('Could not connect to FadeCandy server.')
|
||
|
print('Ensure that fcserver is running and try again.')
|
||
|
|
||
|
def show(self, pixels):
|
||
|
self.client.put_pixels(pixels.T.clip(0, 255).astype(int).tolist())
|
||
|
|
||
|
|
||
|
class BlinkStick(LEDController):
|
||
|
def __init__(self):
|
||
|
"""Initializes a BlinkStick controller"""
|
||
|
try:
|
||
|
from blinkstick import blinkstick
|
||
|
except ImportError as e:
|
||
|
print('Unable to import the blinkstick library')
|
||
|
print('You can install this library with `pip install blinkstick`')
|
||
|
raise e
|
||
|
self.stick = blinkstick.find_first()
|
||
|
|
||
|
def show(self, pixels):
|
||
|
"""Writes new LED values to the Blinkstick.
|
||
|
This function updates the LED strip with new values.
|
||
|
"""
|
||
|
# Truncate values and cast to integer
|
||
|
n_pixels = pixels.shape[1]
|
||
|
pixels = pixels.clip(0, 255).astype(int)
|
||
|
pixels = _GAMMA_TABLE[pixels]
|
||
|
# Read the rgb values
|
||
|
r = pixels[0][:].astype(int)
|
||
|
g = pixels[1][:].astype(int)
|
||
|
b = pixels[2][:].astype(int)
|
||
|
|
||
|
# Create array in which we will store the led states
|
||
|
newstrip = [None] * (n_pixels * 3)
|
||
|
|
||
|
for i in range(n_pixels):
|
||
|
# Blinkstick uses GRB format
|
||
|
newstrip[i * 3] = g[i]
|
||
|
newstrip[i * 3 + 1] = r[i]
|
||
|
newstrip[i * 3 + 2] = b[i]
|
||
|
# Send the data to the blinkstick
|
||
|
self.stick.set_led_data(0, newstrip)
|
||
|
|
||
|
|
||
|
class RaspberryPi(LEDController):
|
||
|
def __init__(self, n_pixels, pin=18, invert_logic=False,
|
||
|
freq=800000, dma=5):
|
||
|
"""Creates a Raspberry Pi output device
|
||
|
Parameters
|
||
|
----------
|
||
|
n_pixels: int
|
||
|
Number of LED strip pixels
|
||
|
pin: int, optional
|
||
|
GPIO pin used to drive the LED strip (must be a PWM pin).
|
||
|
Pin 18 can be used on the Raspberry Pi 2.
|
||
|
invert_logic: bool, optional
|
||
|
Whether or not to invert the driving logic.
|
||
|
Set this to True if you are using an inverting logic level
|
||
|
converter, otherwise set to False.
|
||
|
freq: int, optional
|
||
|
LED strip protocol frequency (Hz). For ws2812 this is 800000.
|
||
|
dma: int, optional
|
||
|
DMA (direct memory access) channel used to drive PWM signals.
|
||
|
If you aren't sure, try 5.
|
||
|
"""
|
||
|
try:
|
||
|
import neopixel
|
||
|
except ImportError as e:
|
||
|
url = 'learn.adafruit.com/neopixels-on-raspberry-pi/software'
|
||
|
print('Could not import the neopixel library')
|
||
|
print('For installation instructions, see {}'.format(url))
|
||
|
raise e
|
||
|
self.strip = neopixel.Adafruit_NeoPixel(n_pixels, pin, freq, dma,
|
||
|
invert_logic, 255)
|
||
|
self.strip.begin()
|
||
|
|
||
|
def show(self, pixels):
|
||
|
"""Writes new LED values to the Raspberry Pi's LED strip
|
||
|
Raspberry Pi uses the rpi_ws281x to control the LED strip directly.
|
||
|
This function updates the LED strip with new values.
|
||
|
"""
|
||
|
# Truncate values and cast to integer
|
||
|
n_pixels = pixels.shape[1]
|
||
|
pixels = pixels.clip(0, 255).astype(int)
|
||
|
# Optional gamma correction
|
||
|
pixels = _GAMMA_TABLE[pixels]
|
||
|
# Encode 24-bit LED values in 32 bit integers
|
||
|
r = np.left_shift(pixels[0][:].astype(int), 8)
|
||
|
g = np.left_shift(pixels[1][:].astype(int), 16)
|
||
|
b = pixels[2][:].astype(int)
|
||
|
rgb = np.bitwise_or(np.bitwise_or(r, g), b)
|
||
|
# Update the pixels
|
||
|
for i in range(n_pixels):
|
||
|
self.strip._led_data[i] = rgb[i]
|
||
|
self.strip.show()
|
||
|
|
||
|
|
||
|
class DotStar(LEDController):
|
||
|
def __init__(self, pixels, brightness=31):
|
||
|
"""Creates an APA102-based output device
|
||
|
Parameters
|
||
|
----------
|
||
|
pixels: int
|
||
|
Number of LED strip pixels
|
||
|
brightness: int, optional
|
||
|
Global brightness
|
||
|
"""
|
||
|
try:
|
||
|
import apa102
|
||
|
except ImportError as e:
|
||
|
url = 'https://github.com/tinue/APA102_Pi'
|
||
|
print('Could not import the apa102 library')
|
||
|
print('For installation instructions, see {}'.format(url))
|
||
|
raise e
|
||
|
self.strip = apa102.APA102(numLEDs=pixels, globalBrightness=brightness) # Initialize the strip
|
||
|
led_data = np.array(self.strip.leds, dtype=np.uint8)
|
||
|
# memoryview preserving the first 8 bits of LED frames (w/ global brightness)
|
||
|
self.strip.leds = led_data.data
|
||
|
# 2D view of led_data
|
||
|
self.led_data = led_data.reshape((pixels, 4)) # or (-1, 4)
|
||
|
|
||
|
def show(self, pixels):
|
||
|
bgr = [2,1,0]
|
||
|
self.led_data[0:,1:4] = pixels[bgr].T.clip(0,255)
|
||
|
self.strip.show()
|