aboutsummaryrefslogtreecommitdiff
path: root/NeoRuntime/Runtime/luxcena_neo
diff options
context:
space:
mode:
Diffstat (limited to 'NeoRuntime/Runtime/luxcena_neo')
-rw-r--r--NeoRuntime/Runtime/luxcena_neo/__init__.py2
-rw-r--r--NeoRuntime/Runtime/luxcena_neo/color_utils.py81
-rw-r--r--NeoRuntime/Runtime/luxcena_neo/matrix.py73
-rw-r--r--NeoRuntime/Runtime/luxcena_neo/neo_behaviour.py266
-rw-r--r--NeoRuntime/Runtime/luxcena_neo/power_calc.py6
-rw-r--r--NeoRuntime/Runtime/luxcena_neo/strip.py187
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.")