diff --git a/animator/__init__.py b/animator/__init__.py index c6b248c..dd4e7eb 100644 --- a/animator/__init__.py +++ b/animator/__init__.py @@ -4,9 +4,14 @@ import random import time from dataclasses import dataclass, field -from typing import Iterable +import logging -import neopixel +try: + import neopixel +except NotImplementedError as e: + logging.error(f"Error importing NeoPixel driver {repr(e)}. If you are using the emulator, you can ignore this message.") + +import neopixel_emu from animator import light_funcs @@ -22,7 +27,7 @@ @dataclass class AnimationState: - "State of animations and neopixels" + """State of animations and neopixels""" state: str = "OFF" color: tuple = (255, 255, 255) effect: str = "SingleColor" @@ -31,24 +36,24 @@ class AnimationState: @dataclass class SingleColorArgs: - "Single Color mode options" + """Single Color mode options""" color: tuple = (255, 0, 0) @dataclass class GlitterRainbowArgs: - "Glitter Rainbow Animation options" + """Glitter Rainbow Animation options""" glitter_ratio: float = 0.05 @dataclass class FadeArgs: - "Fade Animation options" + """Fade Animation options""" colora: tuple = (255, 0, 0) colorb: tuple = (0, 0, 0) @dataclass class FlashArgs: - "Flash Animation options" + """Flash Animation options""" colora: tuple = (255, 0, 0) colorb: tuple = (0, 0, 0) speed: float = 25 @@ -56,7 +61,7 @@ class FlashArgs: @dataclass class WipeArgs: - "Wipe Animation options" + """Wipe Animation options""" colora: tuple = (255, 0, 0) colorb: tuple = (0, 0, 255) leds_iter: int = 1 @@ -64,7 +69,7 @@ class WipeArgs: @dataclass class AnimationArgs: - "Options for animations" + """Options for animations""" single_color: SingleColorArgs = field(default_factory=SingleColorArgs) glitter_rainbow: GlitterRainbowArgs = field(default_factory=GlitterRainbowArgs) fade: FadeArgs = field(default_factory=FadeArgs) @@ -110,7 +115,7 @@ class Animator: """NeoPixel Animation class""" def __init__( self, - pixels: neopixel.NeoPixel, + pixels: 'neopixel.NeoPixel | neopixel_emu.NeoPixel', num_pixels: int, animation_state: AnimationState, animation_args: AnimationArgs, diff --git a/animator/light_funcs.py b/animator/light_funcs.py index 54ebf94..90dbe01 100644 --- a/animator/light_funcs.py +++ b/animator/light_funcs.py @@ -1,3 +1,5 @@ +from typing import Any + "Useful functions for animator" @@ -47,7 +49,7 @@ def lerp(begin, end, t): # linear interpolation return (lerp(r1, r2, t), lerp(g1, g2, t), lerp(b1, b2, t)) -def map_range(x: int, in_min: int, in_max: int, out_min: int, out_max: int): +def map_range(x: float, in_min: int, in_max: int, out_min: int, out_max: int): """Map bounds of input to bounds of output Args: @@ -111,7 +113,7 @@ def square_wave(t, period, amplitude): return -amplitude -def rindex(lst: list, value: any) -> int: +def rindex(lst: list, value: Any) -> int | None: """Get last occurrence of object in list Args: diff --git a/config.yaml b/config.yaml index a1b1974..b05acee 100644 --- a/config.yaml +++ b/config.yaml @@ -22,6 +22,7 @@ mqtt: max_reconnect_delay: 60 driver: - num_pixels: 50 + virtual: false + num_pixels: 200 pin: "D18" order: "RGB" \ No newline at end of file diff --git a/mqtt_animator.py b/mqtt_animator.py index bc75f1f..558634a 100644 --- a/mqtt_animator.py +++ b/mqtt_animator.py @@ -9,13 +9,18 @@ import traceback import dataclasses -import board -import neopixel +try: + import board + import neopixel +except NotImplementedError as e: + logging.error(f"Error importing NeoPixel driver {repr(e)}. If you are using the emulator, you can ignore this message.") + import yaml from paho.mqtt import client as mqtt_client import animator from animator import AnimationArgs +import neopixel_emu # Import yaml config with open("config.yaml", encoding="utf-8") as stream: @@ -62,22 +67,31 @@ # NeoPixel driver config driver_config: dict = configuration.get("driver", {}) +virtual: bool = driver_config.get("virtual", False) num_pixels: int = driver_config.get("num_pixels", 100) # strip length -pixel_pin = getattr(board, driver_config.get("pin", "D18")) # rpi gpio pin +if not virtual: + pixel_pin = getattr(board, driver_config.get("pin", "D18")) # rpi gpio pin +else: + pixel_pin = None pixel_order = driver_config.get("order", "RGB") # Color order global animation_args animation_args = animator.AnimationArgs() -animation_args.single_color.color = [0, 255, 0] +animation_args.single_color.color = (0, 255, 0) animation_state = animator.AnimationState() animation_state.brightness = 100 # Create NeoPixel object -pixels = neopixel.NeoPixel( - pixel_pin, num_pixels, brightness=1.0, auto_write=False, pixel_order=pixel_order -) +if virtual: + pixels = neopixel_emu.NeoPixel( + pixel_pin, num_pixels, brightness=1.0, auto_write=False, pixel_order=pixel_order + ) +else: + pixels = neopixel.NeoPixel( + pixel_pin, num_pixels, brightness=1.0, auto_write=False, pixel_order=pixel_order # type: ignore + ) animator = animator.Animator(pixels, num_pixels, animation_state, animation_args) def validate_arg_import(json_data, dataclass_type): @@ -135,7 +149,7 @@ def on_message(cli: mqtt_client.Client, __, msg): global animation_args "Callback for mqtt message recieved" - print(f"Received `{msg.payload.decode()}` from `{msg.topic}` topic") + logging.debug(f"Received `{msg.payload.decode()}` from `{msg.topic}` topic") if msg.topic == data_request_topic: publish_state(cli) diff --git a/neopixel_emu.py b/neopixel_emu.py new file mode 100644 index 0000000..f24d436 --- /dev/null +++ b/neopixel_emu.py @@ -0,0 +1,52 @@ +import tcolorpy +import adafruit_pixelbuf + +__version__ = "0.1.0" + +RGB = "RGB" +GRB = "GRB" +RGBW = "RGBW" +GRBW = "GRBW" + + +class NeoPixel(adafruit_pixelbuf.PixelBuf): + """NeoPixel Emulator + Semi-compatible with the Adafruit CircuitPython Neopixel Module + """ + + def __init__( + self, + pin, + n: int, + *, + bpp: int = 3, + brightness: float = 1.0, + auto_write: bool = True, + pixel_order: str = "RGB" + ): + super().__init__(n, byteorder=pixel_order, brightness=brightness, auto_write=auto_write) + + def deinit(self) -> None: + pass + + def __enter__(self): + return self + + def __repr__(self): + return "[" + ", ".join([str(x) for x in self]) + "]" + + @property + def n(self) -> int: + """ Get the number of pixels """ + return len(self) + + def write(self) -> None: + """ Same as .show(), deprecated """ + self.show() + + def _transmit(self, _: bytearray) -> None: + termout = "" + for pixel in self: + termout += tcolorpy.tcolor("█", '#{:02x}{:02x}{:02x}'.format(*[int(i * self.brightness) for i in pixel])) + print(termout) + diff --git a/requirements.txt b/requirements.txt index 1840d90..9a0cc82 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ adafruit-circuitpython-neopixel~=6.3.11 -paho-mqtt~=2.0.0 -pyyaml~=6.0.1 \ No newline at end of file +paho-mqtt<2.0.0 +pyyaml~=6.0.1 +tcolorpy~=0.1.6 \ No newline at end of file