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 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10, 11, 11, 11, 12, 12, 13, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 24, 24, 25, 25, 26, 27, 27, 28, 29, 29, 30, 31, 32, 32, 33, 34, 35, 35, 36, 37, 38, 39, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 50, 51, 52, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 66, 67, 68, 69, 70, 72, 73, 74, 75, 77, 78, 79, 81, 82, 83, 85, 86, 87, 89, 90, 92, 93, 95, 96, 98, 99,101,102,104,105,107,109,110,112,114, 115,117,119,120,122,124,126,127,129,131,133,135,137,138,140,142, 144,146,148,150,152,154,156,158,160,162,164,167,169,171,173,175, 177,180,182,184,186,189,191,193,196,198,200,203,205,208,210,213, 215,218,220,223,225,228,231,233,236,239,241,244,247,249,252,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.uint32).ravel() message = _GAMMA_TABLE[message].astype(np.uint8).tostring() self._sock.sendto(message, (self._ip, self._port)) class PxMatrix(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 based PxMatrix Controller 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 flatten_and_clip(self, pixels): print(pixels) pixels = np.reshape(pixels, len(pixels)*4, order='F') pixels = pixels[len(pixels)//4:] pixels = numpy.reshape(pixels, (3, len(pixels)//3)).T return pixels def RGB565_array(self, array): output = np.empty(len(array), dtype=np.uint16) for i in range(len(array)): output[i] = (((array[i][0] & 0b11111000) << 8) | ((array[i][1] & 0b11111100) << 3) | (array[i][2] >> 3)) return output def show(self, pixels): """Sends UDP packets to ESP8266 to update PxMatrix The ESP8266 will receive an image to display on the matrix. """ #message = self.flatten_and_clip(pixels) message = self.RGB565_array(pixels.T.astype(np.uint16)) # print(len(message)) # print(message[0]) message = message[:700].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.setPixelColor(i, neopixel.Color(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() class sACNClient(LEDController): def __init__(self, start_universe=1, start_channel=1, channel_count=3, universe_size=512, fps=30, ip='192.168.1.180'): """Initialize object for communicating with as E1.31 capable device. Parameters ---------- start_universe: int, optional The start universe for the strip start_channel: int, optional The state channel offset within the universe channel_count: int, optional The total number of channels for the device. Typically this will be 3 * N_PIXELS as you need a sperate channel for the red, green, and blue components universe_size: int, optional The number of channels in each universe. This should pretty much never be changed unless using a controller that only support 510 channels. ip: str, optional The IP address of a E1.31 capable device on the network. fps: int, optional The frequency in which to flush the DMX buffer out. This does not need to align with the actually processing rate. Running the processing at 60fps and flusing at 30fps provides a pretty good balance. """ import sacn self._ip = ip self._start_universe = start_universe self._start_channel = start_channel self._channel_count = channel_count self._universe_size = universe_size self._sender = sacn.sACNsender() self._sender._fps = fps self._sender.start() self._stop_universe = start_universe + round(0.5 + ((start_channel + channel_count) / universe_size)) for universe in range(self._start_universe, self._stop_universe): self._sender.activate_output(universe) self._sender[universe].destination = ip self._sender[universe].multicast = False def __del__(self): self._sender.stop() def show(self, pixels): """ Calculate the mapping for pixels to universe and channel and populate the dmx buffer for each universe. The dmx buffer will be flushed by a background thread created by the sACNSender class. """ message = pixels.T.clip(0, config.settings["configuration"]["MAX_BRIGHTNESS"]).astype(np.uint8).ravel().tostring() for universe in range(self._start_universe, self._stop_universe): pixel_start = (universe - self._start_universe) * self._universe_size pixel_end = min(pixel_start + self._universe_size, len(message)) self._sender[universe].dmx_data = message[pixel_start:pixel_end] class Stripless(LEDController): def __init__(self): pass def show(self, pixels): pass