Source code for drivers

# 102shows Drivers
# (c) 2016-2017 Simon Leiner
# licensed under the GNU Public License, version 2

"""This module contains the drivers for the LED strips"""

import logging
from abc import ABCMeta, abstractmethod
from multiprocessing import Array as SyncedArray

__all__ = ['apa102', 'dummy', 'LEDStrip']

logger = logging.getLogger('102shows.drivers')


[docs]class LEDStrip(metaclass=ABCMeta): """\ This class provides the general interface for LED drivers that the lightshows use. All LED drivers for 102shows should inherit this class. Mind the following: - Pixel order is ``r,g,b`` - Pixel resolution (number of dim-steps per color component) is 8-bit, so minimum brightness is ``0`` and maximum brightness is ``255`` The constructor stores the given parameters and initializes the color and brightness buffers. Drivers can and should extend this method. :param num_leds: number of LEDs in the strip :param max_clock_speed_hz: maximum clock speed (Hz) of the bus """ def __init__(self, num_leds: int, max_clock_speed_hz: int = 4000000, max_global_brightness: float = 1.0): # store the given parameters self.num_leds = num_leds self.max_clock_speed_hz = max_clock_speed_hz # private variables self.__is_frozen = False self._global_brightness = 1.0 #: global brightness multiplicator (0-1) self.__max_global_brightness = max_global_brightness # buffers self.color_buffer = [(0.0, 0.0, 0.0)] * self.num_leds self.brightness_buffer = [1] * self.num_leds #: the individual dim factors for each LED (0-1), EXCLUDING the global dim factor self.synced_red_buffer = SyncedArray('f', [0.0] * self.num_leds) self.synced_green_buffer = SyncedArray('f', [0.0] * self.num_leds) self.synced_blue_buffer = SyncedArray('f', [0.0] * self.num_leds) self.synced_brightness_buffer = SyncedArray('i', self.brightness_buffer) def __del__(self): """Invokes :py:func': `close` and deletes all the buffers.""" self.close() del self.color_buffer, self.synced_red_buffer, self.synced_green_buffer, self.synced_blue_buffer del self.brightness_buffer, self.synced_brightness_buffer logger.info("Driver successfully closed") @property def __frozen(self): """\ determines if the strip state can be altered using the :py:func:`set_pixel` function or the brightness setters """ return self.__is_frozen @__frozen.setter def __frozen(self, value): self.__is_frozen = value if self.__is_frozen: logger.debug("Strip is FREEZED") else: logger.debug("Strip is NOT FREEZED") max_refresh_time_sec = 1 """\ The maximum time (in *seconds*) that a call of :func:`show` needs to execute. Currently only used in :func:`lightshows.templates.base.sleep` """ @abstractmethod
[docs] def close(self) -> None: """\ **An abstract method to be overwritten by the drivers.** It should close the bus connection and clean up any remains. """ pass
[docs] def freeze(self) -> None: """\ Freezes the strip. All state-changing methods (:func:`on_color_change` and :func:`on_brightness_change`) must not do anything anymore and leave the buffer unchanged. """ self.__frozen = True
[docs] def unfreeze(self) -> None: """Revokes all effects of :func:`freeze`""" self.__frozen = False
[docs] def get_pixel(self, led_num: int) -> tuple: """\ Returns the pixel at index ``led_num`` :param led_num: the index of the pixel you want to get :return: ``(red, green, blue)`` as tuple """ return self.color_buffer[led_num]
# do not overwrite this method:
[docs] def set_pixel(self, led_num: int, red: float, green: float, blue: float) -> None: """\ The buffer value of pixel ``led_num`` is set to ``(red, green, blue)`` :param led_num: index of the pixel to be set :param red: red component of the pixel (``0.0 - 255.0``) :param green: green component of the pixel (``0.0 - 255.0``) :param blue: blue component of the pixel (``0.0 - 255.0``) """ if led_num < 0: return # Pixel is invisible, so ignore if led_num >= self.num_leds: return # again, invisible if not self.__frozen: self.color_buffer[led_num] = (red, green, blue) self.on_color_change(led_num, red, green, blue)
@abstractmethod
[docs] def on_color_change(self, led_num, red: float, green: float, blue: float) -> None: """\ Changes the message buffer after a pixel was changed in the global color buffer. To send the buffer to the strip and show the changes, you must invoke :func:`show` :param led_num: index of the pixel to be set :param red: red component of the pixel (``0.0 - 255.0``) :param green: green component of the pixel (``0.0 - 255.0``) :param blue: blue component of the pixel (``0.0 - 255.0``) """
[docs] def set_pixel_bytes(self, led_num: int, rgb_color: int) -> None: """\ Changes the pixel ``led_num`` to the given color **in the buffer**. To send the buffer to the strip and show the changes, invoke :func:`show` *If you do not know, how the 3-byte* ``rgb_color`` *works, just use* :func:`set_pixel` *.* :param led_num: index of the pixel to be set :param rgb_color: a 3-byte RGB color value represented as a base-10 integer """ red, green, blue = self.color_bytes_to_tuple(rgb_color) self.set_pixel(led_num, red, green, blue)
@staticmethod
[docs] def color_tuple_to_bytes(red: float, green: float, blue: float) -> int: """\ Converts an RGB color tuple (like ``(255, 0, 26)``) into a 3-byte color value (like ``FF001A``) :param red: red component of the tuple (``0.0 - 255.0``) :param green: green component of the tuple (``0.0 - 255.0``) :param blue: blue component of the tuple (``0.0 - 255.0``) :return: the tuple components joined into a 3-byte value with each byte representing a color component """ # round to integers red = round(red) green = round(green) blue = round(blue) return (red << 16) + (green << 8) + blue
@staticmethod
[docs] def color_bytes_to_tuple(rgb_color: int) -> tuple: """\ Converts a 3-byte color value (like ``FF001A``) into an RGB color tuple (like ``(255, 0, 26)``). :param rgb_color: a 3-byte RGB color value represented as a base-10 integer :return: color tuple ``(red, green, blue)`` """ r = (rgb_color & 0xFF0000) >> 16 g = (rgb_color & 0x00FF00) >> 8 b = rgb_color & 0x0000FF return r, g, b
@abstractmethod
[docs] def show(self) -> None: """\ **Subclasses should overwrite this method** This method should show the buffered pixels on the strip, e.g. write the message buffer to the port on which the strip is connected. """ pass
[docs] def rotate(self, positions: int = 1) -> None: """\ Treating the internal leds array as a circular buffer, rotate it by the specified number of positions. The number can be negative, which means rotating in the opposite direction. :param positions: the number of steps to rotate """ self.color_buffer = self.color_buffer[positions:] + self.color_buffer[:positions] for led_num in range(self.num_leds): r, g, b = self.get_pixel(led_num) self.on_color_change(led_num, r, g, b)
[docs] def set_brightness(self, led_num: int, brightness: float) -> None: """\ Sets the brightness for a single LED in the strip. A global multiplier is applied. :param led_num: the target LED index :param brightness: the desired brightness (``0.0 - 1.0``) """ if self.__frozen: # skip if show is frozen return # limit brightness to the area between (and including) 0.0 and 1.0 if brightness < 0.0: brightness = 0.0 elif brightness > 1.0: brightness = 1.0 self.brightness_buffer[led_num] = brightness self.on_brightness_change(led_num)
@abstractmethod
[docs] def on_brightness_change(self, led_num: int) -> None: """\ Reacts to a brightness change at ``led_num`` by modifying the message buffer :param led_num: number of the LED whose brightness was modified """ pass
[docs] def set_global_brightness(self, brightness: float) -> None: """\ Sets a global brightness multiplicator which applies to every single LED's brightness. :param brightness: the global brightness (``0.0 - 1.0``) multiplicator to be set """ if brightness < 0.0: self._global_brightness = 0.0 elif brightness > self.__max_global_brightness: self._global_brightness = self.__max_global_brightness else: self._global_brightness = brightness for led_num in range(self.num_leds): self.on_brightness_change(led_num)
[docs] def set_global_brightness_percent(self, brightness: float) -> None: """\ Just like :func:`set_global_brightness`, but with a 0-100 percent value. :param brightness: the global brightness (``0.0 - 100.0``) multiplicator to be set """ self.set_global_brightness(100 * brightness)
[docs] def clear_buffer(self) -> None: """Resets all pixels in the color buffer to ``(0,0,0)``.""" for led_num in range(self.num_leds): self.set_pixel(led_num, 0, 0, 0)
[docs] def clear_strip(self) -> None: """Clears the color buffer, then invokes a blackout on the strip by calling :py:func:`show`""" self.clear_buffer() self.show()
[docs] def sync_up(self) -> None: """\ Copies the local color and brightness buffers to the shared buffer so other processes can see the current strip state. """ logger.info("sync-up") for led_num, (red, green, blue) in enumerate(self.color_buffer): # colors self.synced_red_buffer[led_num] = red self.synced_green_buffer[led_num] = green self.synced_blue_buffer[led_num] = blue # brightness self.synced_brightness_buffer[led_num] = self.brightness_buffer[led_num]
[docs] def sync_down(self) -> None: """Reads the shared color and brightness buffers and copies them to the local buffers""" logger.info("sync-down") for led_num, _ in enumerate(self.color_buffer): # colors red = self.synced_red_buffer[led_num] green = self.synced_green_buffer[led_num] blue = self.synced_blue_buffer[led_num] self.color_buffer[led_num] = (red, green, blue) self.on_color_change(led_num, red, green, blue) # brightness self.brightness_buffer[led_num] = self.synced_brightness_buffer[led_num] self.on_brightness_change(led_num)