diff options
Diffstat (limited to 'NeoRuntime/Runtime/luxcena_neo')
-rw-r--r-- | NeoRuntime/Runtime/luxcena_neo/__init__.py | 2 | ||||
-rw-r--r-- | NeoRuntime/Runtime/luxcena_neo/color_utils.py | 81 | ||||
-rw-r--r-- | NeoRuntime/Runtime/luxcena_neo/matrix.py | 73 | ||||
-rw-r--r-- | NeoRuntime/Runtime/luxcena_neo/neo_behaviour.py | 266 | ||||
-rw-r--r-- | NeoRuntime/Runtime/luxcena_neo/power_calc.py | 6 | ||||
-rw-r--r-- | NeoRuntime/Runtime/luxcena_neo/strip.py | 187 |
6 files changed, 615 insertions, 0 deletions
diff --git a/NeoRuntime/Runtime/luxcena_neo/__init__.py b/NeoRuntime/Runtime/luxcena_neo/__init__.py new file mode 100644 index 0000000..606f365 --- /dev/null +++ b/NeoRuntime/Runtime/luxcena_neo/__init__.py @@ -0,0 +1,2 @@ +from .neo_behaviour import NeoBehaviour, VariableType, ColorVariable, FloatVariable, IntegerVariable, BooleanVariable +import luxcena_neo.color_utils as utils diff --git a/NeoRuntime/Runtime/luxcena_neo/color_utils.py b/NeoRuntime/Runtime/luxcena_neo/color_utils.py new file mode 100644 index 0000000..3b7ece4 --- /dev/null +++ b/NeoRuntime/Runtime/luxcena_neo/color_utils.py @@ -0,0 +1,81 @@ +def hex_to_rgb(value: str) -> tuple: + """ Convert hex color to rgb tuple. """ + value = value.lstrip("#") + lv = len(value) + return tuple(int(value[i:i + lv // 3], 16) for i in range(0, lv, lv // 3)) + +def rgb_to_hex(rgb: tuple) -> str: + """ Convert rgb color to hex string. """ + return '#%02x%02x%02x' % rgb + +def rgb_from_twentyfour_bit(color: int) -> tuple: + """ Convert 24-bit color value into a tuple representing the rgb values. """ + # w = (color & 0xFF000000) >> 24 + r = (color & 0x00FF0000) >> 16 + g = (color & 0x0000FF00) >> 8 + b = (color & 0x000000FF) + return (r, g, b) + +def hex_from_twentyfour_bit(color: int) -> str: + """ Convert 24-bit color value into hex str. """ + rgb_to_hex(rgb_from_24bit(color)) + +def twentyfour_bit_from_rgb(red, green, blue, white=0): + """ + Convert the provided red, green, blue color to a 24-bit color value. + Each color component should be a value 0-255 + where 0 is the lowest intensity and 255 is the highest intensity. + """ + return (white << 24) | (red << 16) | (green << 8) | blue + + +def twentyfour_bit_from_hex(hex_color: str): + """ Convert the provided hex code to a 24-bit color value. """ + print(hex_color) + value = hex_color.lstrip('#') + return (int(value[0:2], 16) << 16) | (int(value[2:4], 16) << 8) | (int(value[4:6], 16)) + + +def detect_format_convert_color(*color) -> int: + """ + Detect format of a color and return its 24-bit color value. + + If parameter is only a str, it will be treated as a hex value. + If parameter is a tuple, the first three items in that tuple will be treated as a rgb value. + If parameter is a int, it will be treated as a 24-bit color value. + If there are 3 parameters, these will be treated as a rgb value. + """ + if (len(color) == 1) and (isinstance(color[0], str)): + return twentyfour_bit_from_hex(color[0]) + if (len(color) == 1) and (isinstance(color[0], tuple)): + return twentyfour_bit_from_rgb(*(color[0])) + if (len(color) == 1) and (isinstance(color[0], int)): + return color[0] + if (len(color) == 3): + return twentyfour_bit_from_rgb(*color) + raise ValueError("Invalid parameters provided, check documentation.") + + +class Color: + + def __init__(self, *color): + self.__color = detect_format_convert_color(*color) + + @property + def hex(self): return hex_from_twentyfour_bit(self.__color) + + @property + def rgb(self): return rgb_from_twentyfour_bit(self.__color) + + def __repr__(self): + return self.__color + + def __str__(self): + return str(repr(self)) + + def __int__(self): + return self.__color + + def __invert__(self): + rgb_color = self.rgb + return Color((255-rgb_color[0], 255-rgb_color[1], 255-rgb_color[2])) diff --git a/NeoRuntime/Runtime/luxcena_neo/matrix.py b/NeoRuntime/Runtime/luxcena_neo/matrix.py new file mode 100644 index 0000000..b842789 --- /dev/null +++ b/NeoRuntime/Runtime/luxcena_neo/matrix.py @@ -0,0 +1,73 @@ +def get_segment_range(segments, n): + """ Return a list of all the actual led-numbers in a segment """ + # Sum all the segments prior to the one we are looking for + i = 0 + start = 0 + while True: + if i >= n: break + start += segments[i] # Add number of leds in this segment to the start-index + i += 1 + + # Add all numbers in the segment we are looking for to a list + i = start + breakPoint = i + segments[n] + range = [] + while True: + range.append(i) + i += 1 + if i >= breakPoint: break + return range + + +class Matrix: + + def __init__(self, segments, matrix): + self.matrix = [] # Holds the matrix + self.x_len = 0 # The width of the matrix + self.y_len = len(matrix) # The heigth of the matrix + + for y_pos in range(len(matrix)): + y_val = matrix[y_pos] + + this_y = [] + for x_pos in range(len(y_val)): + # This gets the range of segment n + segment_range = get_segment_range(segments, matrix[y_pos][x_pos][0]) + + # This adds the range to the current row's list + # if in the config [<segment_num>, <reversed>] + # reversed == true, revese the list before adding it + this_y += reversed(segment_range) if matrix[y_pos][x_pos][1] else segment_range + + # This just finds the longest row in the matrix + if (len(this_y) > self.x_len): + self.x_len = len(this_y) + + self.matrix.append(this_y) + + def get(self, x, y): + """ Return the value of a place in the matrix given x and y coordinates """ + return self.matrix[y][x] + + def dump(self): + n_spacers = (self.x_len*6) // 2 - 6 + print( ("=" * n_spacers) + "Matrix dump" + ("=" * n_spacers) ) + + for y in self.matrix: + this_y_line = "" + for x in y: + this_y_line += ( ' ' * (5 - len(str(x))) ) + str(x) + ' ' + print(this_y_line) + + print("=" * (self.x_len*6)) + + +if __name__ == "__main__": + test_matrix = Matrix( + [2, 2, 2, 2, 2, 2, 2, 2, 2], + [ + [[0, False], [1, True], [2, False]], + [[3, True], [4, False], [5, True]], + [[6, False], [7, True], [8, False]] + ] + ) diff --git a/NeoRuntime/Runtime/luxcena_neo/neo_behaviour.py b/NeoRuntime/Runtime/luxcena_neo/neo_behaviour.py new file mode 100644 index 0000000..4da3093 --- /dev/null +++ b/NeoRuntime/Runtime/luxcena_neo/neo_behaviour.py @@ -0,0 +1,266 @@ +import json +from os import path +from enum import Enum +from .strip import detect_format_convert_color +from .color_utils import rgb_from_twentyfour_bit, hex_from_twentyfour_bit + +class NeoBehaviour: + """ + This is the base-class "main" should inherit from! + All methods are blocking :) This means that you could potentially loose a "tick" + For example, if "eachSecond" is taking up the thread, and the clock goes from 11:58 to 12:02, "eachHour", will not be called. + """ + + def __init__(self, package_path): + """ + THIS METHOD SHOULD NOT BE OVERIDDEN! Use onStart if you want a setup-method!!! + Contains basic setup + """ + self.var = Variables(package_path) + + self.declare = self.var.declare + self.declare_variables() + del self.declare + + def declare_variables(self): + """ This should be overridden, and ALL variables should be declared here. """ + return + + def on_start(self): + """ This method will be run right after __init__ """ + return + + def each_tick(self): + """ This method will be run every tick, that means every time the program has time """ + return + + def each_second(self): + """ This method is called every second (on the clock), given that the thread is open """ + return + + def each_minute(self): + """ This method is called every mintue (on the clock), given that the thread is open """ + return + + def each_hour(self): + """ This method is called every whole hour (on the clock), given that the thread is open """ + return + + def each_day(self): + """ This method is called every day at noon, given that the thread is open """ + return + +class VariableType(Enum): + TEXT = 1 + INT = 2 + FLOAT = 3 + COLOR = 4 + BOOL = 5 + +class Variables: + + def __init__(self, package_path): + self.__vars = {} + self.__vars_save_file = "{}/state.json".format(package_path) + self.__saved_variables = {} + self.read_saved_variables() + + def __getattr__(self, name): + if name in ["_Variables__vars", "_Variables__vars_save_file", "_Variables__saved_variables"]: + return super(Variables, self).__getattr__(name) + return self.__vars[name].value + + def __setattr__(self, name, value): + if name in ["_Variables__vars", "_Variables__vars_save_file", "_Variables__saved_variables"]: + super(Variables, self).__setattr__(name, value) + else: + self.__vars[name].value = value + + def __delattr__(self, name): + if name in ["_Variables__vars", "_Variables__vars_save_file", "_Variables__saved_variables"]: + super(Variables, self).__delattr__(name) + else: + del self.__vars[name] + + def __getitem__(self, name): + return self.__vars[name] + + def __setitem__(self, name, value): + self.__vars[name].value = value + + def __contains__(self, name): + return name in self.__vars + + def __repr__(self): + return json.dumps({name: var.value for name, var in self.__vars.items()}) + + def __str__(self): + return repr(self) + + def __dir__(self): + return super(Variables, self).__dir__() + [name for name in self.__vars.keys()] + + def __len__(self): + return len(self.__vars) + + def __iter__(self): + return iter(self.__vars.items()) + + def declare(self, variable): + """ Declare a new variable. """ + if variable.name in self.__vars: + raise Exception("Variable with name {} already defined.".format(variable.name)) + if variable.name in self.__saved_variables: + variable.value = self.__saved_variables[variable.name] + + variable.set_save_func(self.save_variables) + self.__vars[variable.name] = variable + + def read_saved_variables(self): + """ Read saved variable values from file. """ + if not path.exists(self.__vars_save_file): return + try: + with open(self.__vars_save_file, "r") as f: + self.__saved_variables = json.load(f) + except: + print("Could not load saved variables") + + def save_variables(self): + """ Save variable values to file. """ + self.__saved_variables = {name: var.value for name, var in self.__vars.items()} + with open(self.__vars_save_file, "w") as f: + f.write(json.dumps(self.__saved_variables)) + + def to_dict(self): + return {x.name: x.to_dict() for x in self.__vars.values()} + +class Variable: + + def __init__(self, name, default, var_type: VariableType, on_change = None): + self.__name = name + self.__value = default + self.__var_type = var_type + self.__on_change = on_change + self.__save_func = None + + @property + def name(self): return self.__name + + @property + def value(self): return self.__value + + @value.setter + def value(self, value): + self.__value = value + if self.__save_func is not None: + self.__save_func() + if self.__on_change is not None: + self.__on_change(self.value) + + @property + def var_type(self): return self.__var_type.name + + def to_dict(self): + return {"name": self.name, "value": self.value, "type": self.var_type} + + def __str__(self): + return "{}: {}".format(self.name, self.value) + + def set_save_func(self, save_func): + self.__save_func = save_func + +class ColorVariable(Variable): + + def __init__(self, name: str, *color, **kwargs): + if not self.verify_color(*color): + raise Exception("Invalid color {}".format(color)) + super().__init__(name, self.extract_interesting(*color), VariableType.COLOR, **kwargs) + + @Variable.value.setter + def value(self, *color): + if not self.verify_color(*color): + print("Attempting to set {} to invalid value {}".format(self.name, color)) + return + super(ColorVariable, type(self)).value.fset(self, self.extract_interesting(*color)) + + def verify_color(self, *color): + if (len(color) == 1) and (isinstance(color[0], str)): + return True + if (len(color) == 1) and (isinstance(color[0], tuple)): + if len(color[0]) != 3: return False + for c in color[0]: + if not (0 <= c <= 255): return False + return True + if (len(color) == 1) and (isinstance(color[0], int)): + return True + if (len(color) == 3): + for c in color: + if not isinstance(c, int): return False + if not (0 <= c <= 255): return False + return True + return False + + def extract_interesting(self, *color): + if (len(color) == 1): return color[0] + return color + +class IntegerVariable(Variable): + + def __init__(self, name: str, default: int = 0, min_val: int = 0, max_val: int = 255, **kwargs): + self.__min = min_val + self.__max = max_val + super().__init__(name, default, VariableType.INT) + + @Variable.value.setter + def value(self, value): + try: + value = int(value) + if (self.__min <= value <= self.__max): + super(IntegerVariable, type(self)).value.fset(self, value) + else: + print("Attempted to set {} to {} but range is [{},{}].".format(self.name, value, self.__min, self.__max)) + except ValueError: + print("Attempted to set {} to \"{}\", which is not a valid integer...".format(self.name, value)) + + def to_dict(self): + return {"name": self.name, "value": self.value, "type": self.var_type, "min": self.__min, "max": self.__max} + + +class FloatVariable(Variable): + + def __init__(self, name: str, default: float = 0.0, min_val: float = 0.0, max_val: float = 255.0, **kwargs): + self.__min = min_val + self.__max = max_val + super().__init__(name, default, VariableType.FLOAT) + + @Variable.value.setter + def value(self, value): + try: + value = float(value) + if (self.__min <= value <= self.__max): + super(FloatVariable, type(self)).value.fset(self, value) + else: + print("Attempted to set {} to {} but range is [{},{}].".format(self.name, value, self.__min, self.__max)) + except ValueError: + print("Attempted to set {} to \"{}\", which is not a valid float...".format(self.name, self.value)) + + def __str__(self): + return round(self.value, 2) + + def to_dict(self): + return {"name": self.name, "value": self.value, "type": self.var_type, "min": self.__min, "max": self.__max} + + + +class BooleanVariable(Variable): + + def __init__(self, name: str, default: bool, **kwargs): + super().__init__(name, default, VariableType.BOOL) + + @Variable.value.setter + def value(self, value): + try: + value = bool(value) + super(BooleanVariable, type(self)).value.fset(self, value) + except: + print("Attempted to set {} to \"{}\", which is not a valid bool...".format(self.name, value)) diff --git a/NeoRuntime/Runtime/luxcena_neo/power_calc.py b/NeoRuntime/Runtime/luxcena_neo/power_calc.py new file mode 100644 index 0000000..4f6a6d6 --- /dev/null +++ b/NeoRuntime/Runtime/luxcena_neo/power_calc.py @@ -0,0 +1,6 @@ + + +def calcCurrent(pixels): + current = 0 + for pixel in pixels: + break diff --git a/NeoRuntime/Runtime/luxcena_neo/strip.py b/NeoRuntime/Runtime/luxcena_neo/strip.py new file mode 100644 index 0000000..bc2e2be --- /dev/null +++ b/NeoRuntime/Runtime/luxcena_neo/strip.py @@ -0,0 +1,187 @@ +import json +from os import path +import rpi_ws281x as ws +from .matrix import Matrix, get_segment_range +from .power_calc import calcCurrent + + +class Strip: + + def __init__(self, strip_conf): + # Read in all config options + self.SEGMENTS = strip_conf["segments"] + self.LED_FREQ_HZ = int(strip_conf["led_freq_hz"]) # LED signal frequency in hertz (usually 800khz) + self.LED_CHANNEL = int(strip_conf["led_channel"]) # Set to '1' for GPIOs 13, 19, 41, 45, 53 + self.LED_INVERT = strip_conf["led_invert"] # True to invert the signal, (when using NPN transistor level shift) + self.LED_PIN = int(strip_conf["led_pin"]) # 18 uses PWM, 10 uses SPI /dev/spidev0.0 + self.LED_DMA = int(strip_conf["led_dma"]) # DMA channel for generating the signal, on the newer ones, try 10 + self.LED_COUNT = sum(self.SEGMENTS) # Number of LEDs in strip + + # Setup the color calibration array + if ("color_calibration" in strip_conf) and (strip_conf["color_calibration"] != ""): + self.COLOR_CALIBRATION = strip_conf["led_calibration"] + else: + self.COLOR_CALIBRATION = [0xffffffff for x in range(self.LED_COUNT)] + + # Setup some buffers we can use to keep track of what will be displayed + # and what is displayed (could use rpi_ws281x functions for this maybe) + self.TMPCOLORSTATE = [0 for x in range(self.LED_COUNT)] + self.COLORSTATE = [0 for x in range(self.LED_COUNT)] + + # Keeping the state of the strip power + self.__power_on = True + # Keeping what the brightness is set to + self.__set_brightness = 255 + # Keeping what the brightness actually is + self.__actual_brightness = self.__set_brightness + + # Setup the strip instance + self.strip = ws.Adafruit_NeoPixel( + self.LED_COUNT, + self.LED_PIN, + self.LED_FREQ_HZ, + self.LED_DMA, + self.LED_INVERT, + self.__set_brightness, + self.LED_CHANNEL, + strip_type=ws.WS2812_STRIP + ) + self.strip.begin() + + # Blank out all the LEDs + i = 0 + while True: + self.strip.setPixelColor(i, 0) + i += 1 + if (i > self.LED_COUNT): break + self.strip.show() + + # Setup matrix + print(" * Generating matrix") + # try: + self.pixelMatrix = Matrix(self.SEGMENTS, strip_conf["matrix"]) + self.pixelMatrix.dump() + # except: + # print("Something went wrong while setting up your self-defined matrix.") + + # Read in state file, so we can revoces the last state. + self.__globvars_path = path.join(path.split(path.dirname(path.abspath(__file__)))[0], "state.json") + if path.exists(self.__globvars_path): + try: + with open(self.__globvars_path, "r") as f: + globvars = json.load(f) + self.power_on = globvars["power_on"] + self.brightness = globvars["brightness"] + except: + print("Could not load saved globvars...") + + def save_globvars(self): + with open(self.__globvars_path, "w") as f: + f.write(json.dumps({ + "power_on": self.__power_on, + "brightness": self.__set_brightness + })) + + @property + def power_on(self): + return self.__power_on + + @power_on.setter + def power_on(self, value: bool): + self.__power_on = value + self._set_brightness(self.__set_brightness if self.power_on else 0) + self.save_globvars() + + @property + def brightness(self): + return self.__actual_brightness + + @brightness.setter + def brightness(self, value: int): + if 0 <= value <= 255: + self.__set_brightness = value + if (self.power_on): + self._set_brightness(value) + self.save_globvars() + else: + raise Exception("Value ({}) outside allowed range (0-255)".format(value)) + + def _set_brightness(self, value): + self.__actual_brightness = value + self.strip.setBrightness(value) + self.show() + + def show(self): + """Update the display with the data from the LED buffer.""" + self.COLORSTATE = self.TMPCOLORSTATE + self.strip.show() + + def set_pixel_color(self, n, *color): + """Set LED at position n to the provided 24-bit color value (in RGB order). + """ + c = detect_format_convert_color(*color) + self.TMPCOLORSTATE[n] = c + self.strip.setPixelColor(n, c) + + def set_pixel_color_XY(self, x, y, *color): + """Set LED at position n to the provided 24-bit color value (in RGB order). + """ + self.set_pixel_color(self.pixelMatrix.get(x, y), *color) + + def set_segment_color(self, segment, *color): + """Set a whole segment to the provided red, green and blue color. + Each color component should be a value from 0 to 255 (where 0 is the + lowest intensity and 255 is the highest intensity).""" + for n in get_segment_range(self.SEGMENTS, segment): + self.set_pixel_color(n, *color) + + def get_pixels(self): + """Return an object which allows access to the LED display data as if + it were a sequence of 24-bit RGB values. + """ + return self.strip.getPixels() + + def num_pixels(self): + """Return the number of pixels in the display.""" + return self.LED_COUNT + + def get_pixel_color(self, n): + """Get the 24-bit RGB color value for the LED at position n.""" + return self.strip.getPixelColor(n) + + +def color_from_rgb(red, green, blue, white=0): + """ + Convert the provided red, green, blue color to a 24-bit color value. + Each color component should be a value 0-255 + where 0 is the lowest intensity and 255 is the highest intensity. + """ + return (white << 24) | (red << 16) | (green << 8) | blue + + +def color_from_hex(hex_color: str): + """ Convert the provided hex code to a 24-bit color value. """ + value = hex_color.lstrip('#') + lv = len(value) + rgb = tuple(int(value[i:i+lv//3], 16) for i in range(0, lv, lv//3)) + return color_from_rgb(red=rgb[0], green=rgb[1], blue=rgb[2]) + + +def detect_format_convert_color(*color) -> int: + """ + Detect format of a color and return its 24-bit color value. + + If parameter is only a str, it will be treated as a hex value. + If parameter is a tuple, the first three items in that tuple will be treated as a rgb value. + If parameter is a int, it will be treated as a 24-bit color value. + If there are 3 parameters, these will be treated as a rgb value. + """ + if (len(color) == 1) and (isinstance(color[0], str)): + return color_from_hex(color[0]) + if (len(color) == 1) and (isinstance(color[0], tuple)): + return color_from_rgb(*(color[0])) + if (len(color) == 1) and (isinstance(color[0], int)): + return color[0] + if (len(color) == 3): + return color_from_rgb(*color) + raise ValueError("Invalid parameters provided, check documentation.") |