diff --git a/docs/configuration.md b/docs/configuration.md index df99be0..1c26524 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1103,7 +1103,9 @@ explanation of each parameter: default is False. ### `[wled]` -Enables control of an WLED strip. +Enables control of a [WLED](https://kno.wled.ge/) strip. Moonraker always +supports 4 color channel strips - the color order is defined within WLED +itself. ```ini # moonraker.conf @@ -1131,8 +1133,8 @@ initial_white: chain_count: # Number of addressable neopixels for use (default: 1) color_order: +# *** DEPRECATED - Color order is defined per GPIO in WLED directly *** # Color order for WLED strip, RGB or RGBW (default: RGB) - ``` Below are some examples: ```ini @@ -1166,7 +1168,7 @@ one or more macros, such as: # printer.cfg [gcode_macro WLED_ON] -description: Turn WLED strip on using optional preset +description: Turn WLED strip on using optional preset and resets led colors gcode: {% set strip = params.STRIP|string %} {% set preset = params.PRESET|default(-1)|int %} @@ -1186,7 +1188,7 @@ gcode: state=False)} [gcode_macro SET_WLED] -description: SET_LED like functionlity for WLED +description: SET_LED like functionlity for WLED, applies to all active segments gcode: {% set strip = params.STRIP|string %} {% set red = params.RED|default(0)|float %} diff --git a/docs/user_changes.md b/docs/user_changes.md index 95fbc54..d135ce1 100644 --- a/docs/user_changes.md +++ b/docs/user_changes.md @@ -2,6 +2,11 @@ This file will track changes that require user intervention, such as a configuration change or a reinstallation. +### January 22th 2022 +- The `color_order` option in the `[wled]` section has been deprecated. + This is configured in wled directly. This is not a breaking change, + the setting will simply be ignored not affecting functionality. + ### December 24th 2021 - The `password_file` option in the `[mqtt]` section has been deprecated. Use the `password` option instead. This option may be a template, thus diff --git a/moonraker/components/wled.py b/moonraker/components/wled.py index dcca5e6..ac8c753 100644 --- a/moonraker/components/wled.py +++ b/moonraker/components/wled.py @@ -33,29 +33,20 @@ if TYPE_CHECKING: from . import klippy_apis APIComp = klippy_apis.KlippyAPI -class ColorOrder(str, Enum): - RGB: str = "RGB" - RGBW: str = "RGBW" - - def Elem_Size(self): - if self is ColorOrder.RGB: - return 3 - return 4 - class OnOff(str, Enum): on: str = "on" off: str = "off" class Strip(): + _COLORSIZE: int = 4 + def __init__(self: Strip, name: str, - color_order: ColorOrder, cfg: ConfigHelper): self.server = cfg.get_server() self.request_mutex = asyncio.Lock() self.name = name - self.color_order = color_order self.initial_preset: int = cfg.getint("initial_preset", -1) self.initial_red: float = cfg.getfloat("initial_red", 0.5) @@ -64,7 +55,9 @@ class Strip(): self.initial_white: float = cfg.getfloat("initial_white", 0.5) self.chain_count: int = cfg.getint("chain_count", 1) - self._chain_data = bytearray(self.chain_count * color_order.Elem_Size()) + # Supports rgbw always + self._chain_data = bytearray( + self.chain_count * self._COLORSIZE) self.onoff = OnOff.off self.preset = self.initial_preset @@ -75,7 +68,6 @@ class Strip(): "status": self.onoff, "chain_count": self.chain_count, "preset": self.preset, - "color_order": self.color_order, "error": self.error_state } @@ -105,10 +97,7 @@ class Strip(): blue = int(blue * 255. + .5) green = int(green * 255. + .5) white = int(white * 255. + .5) - if self.color_order is ColorOrder.RGB: - led_data = [red, green, blue] - else: - led_data = [red, green, blue, white] + led_data = [red, green, blue, white] if index is None: self._chain_data[:] = led_data * self.chain_count @@ -149,8 +138,18 @@ class Strip(): async def wled_off(self: Strip) -> None: logging.debug(f"WLED: {self.name} off") self.onoff = OnOff.off + # Without this calling SET_WLED for a single pixel after WLED_OFF + # would send just that pixel + self.send_full_chain_data = True await self._send_wled_command({"on": False}) + def _wled_pixel(self: Strip, index: int) -> List[int]: + led_color_data: List[int] = [] + for p in self._chain_data[(index-1)*self._COLORSIZE: + (index)*self._COLORSIZE]: + led_color_data.append(p) + return led_color_data + async def set_wled(self: Strip, red: float, green: float, blue: float, white: float, index: Optional[int], transmit: bool) -> None: @@ -159,40 +158,40 @@ class Strip(): f"INDEX={index} TRANSMIT={transmit}") self._update_color_data(red, green, blue, white, index) if transmit: - elem_size = self.color_order.Elem_Size() - if self.onoff == OnOff.off: - # Without a separate On call individual led control doesn"t - # turn the led strip back on - self.onoff = OnOff.on - await self._send_wled_command({"on": True}) + + # Base command for setting an led (for all active segments) + # See https://kno.wled.ge/interfaces/json-api/ + state: Dict[str, Any] = {"on": True, + "tt": 0, + "bri": 255, + "seg": {"bri": 255, "i": []}} if index is None: - # All pixels same color only send range command - elem = [] - for p in self._chain_data[0:elem_size]: - elem.append(p) + # All pixels same color only send range command of first color self.send_full_chain_data = False - await self._send_wled_command( - {"seg": {"i": [0, self.chain_count-1, elem]}}) + state["seg"]["i"] = [0, self.chain_count, self._wled_pixel(1)] elif self.send_full_chain_data: # Send a full set of color data (e.g. previous preset) - state: Dict[str, Any] = {"seg": {"i": []}} + self.send_full_chain_data = False cdata = [] for i in range(self.chain_count): - idx = i * elem_size - elem = [] - for p in self._chain_data[idx: idx+elem_size]: - elem.append(p) - cdata.append(elem) + cdata.append(self._wled_pixel(i+1)) state["seg"]["i"] = cdata - self.send_full_chain_data = False - await self._send_wled_command(state) else: - # Only one pixel has changed so send just that one - elem = [] - for p in self._chain_data[(index-1)*elem_size: - (index-1)*elem_size+elem_size]: - elem.append(p) - await self._send_wled_command({"seg": {"i": [index, elem]}}) + # Only one pixel has changed since last full data sent + # so send just that one + state["seg"]["i"] = [index-1, self._wled_pixel(index)] + + # Send wled control command + await self._send_wled_command(state) + + if self.onoff == OnOff.off: + # Without a repeated call individual led control doesn't + # turn the led strip back on or doesn't set brightness + # correctly from off + # Confirmed as a bug: + # https://discord.com/channels/473448917040758787/757254961640898622/934135556370202645 + self.onoff = OnOff.on + await self._send_wled_command(state) else: # If not transmitting this time easiest just to send all data when # next transmitting @@ -201,9 +200,8 @@ class Strip(): class StripHttp(Strip): def __init__(self: StripHttp, name: str, - color_order: ColorOrder, cfg: ConfigHelper): - super().__init__(name, color_order, cfg) + super().__init__(name, cfg) # Read the uri information addr: str = cfg.get("address") @@ -235,9 +233,8 @@ class StripHttp(Strip): class StripSerial(Strip): def __init__(self: StripSerial, name: str, - color_order: ColorOrder, cfg: ConfigHelper): - super().__init__(name, color_order, cfg) + super().__init__(name, cfg) # Read the serial information (requires wled 0.13 2108250 or greater) self.serialport: str = cfg.get("serial") @@ -261,25 +258,22 @@ class StripSerial(Strip): class WLED: def __init__(self: WLED, config: ConfigHelper) -> None: - try: - # root_logger = logging.getLogger() - # root_logger.setLevel(logging.DEBUG) + # root_logger = logging.getLogger() + # root_logger.setLevel(logging.DEBUG) - self.server = config.get_server() - prefix_sections = config.get_prefix_sections("wled") - logging.info(f"WLED component loading strips: {prefix_sections}") - color_orders = { - "RGB": ColorOrder.RGB, - "RGBW": ColorOrder.RGBW - } - strip_types = { - "http": StripHttp, - "serial": StripSerial - } - self.strips = {} - for section in prefix_sections: - cfg = config[section] + self.server = config.get_server() + prefix_sections = config.get_prefix_sections("wled") + logging.info(f"WLED component loading strips: {prefix_sections}") + strip_types = { + "HTTP": StripHttp, + "SERIAL": StripSerial + } + self.strips = {} + for section in prefix_sections: + cfg = config[section] + + try: name_parts = cfg.get_name().split(maxsplit=1) if len(name_parts) != 2: raise cfg.error( @@ -288,52 +282,46 @@ class WLED: logging.info(f"WLED strip: {name}") - color_order_cfg: str = cfg.get("color_order", "RGB") - color_order = color_orders.get(color_order_cfg) - if color_order is None: - raise cfg.error( - f"Color order not supported: {color_order_cfg}") + # Discard old color_order setting, always support 4 color strips + _ = cfg.get("color_order", "", deprecate=True) strip_type: str = cfg.get("type", "http") strip_class: Optional[Type[Strip]] - strip_class = strip_types.get(strip_type) + strip_class = strip_types.get(strip_type.upper()) if strip_class is None: - raise cfg.error(f"Unsupported Strip Type: {strip_type}") - try: - strip = strip_class(name, color_order, cfg) - except Exception as e: - msg = f"Failed to initialise strip [{cfg.get_name()}]\n{e}" - self.server.add_warning(msg) - continue + raise config.error(f"Unsupported Strip Type: {strip_type}") - self.strips[name] = strip + self.strips[name] = strip_class(name, cfg) - # Register two remote methods for GCODE - self.server.register_remote_method( - "set_wled_state", self.set_wled_state) - self.server.register_remote_method( - "set_wled", self.set_wled) + except Exception as e: + # Ensures errors such as "Color not supported" are visible + msg = f"Failed to initialise strip [{cfg.get_name()}]\n{e}" + self.server.add_warning(msg) + continue - # As moonraker is about making things a web api, let's try it - # Yes, this is largely a cut-n-paste from power.py - self.server.register_endpoint( - "/machine/wled/strips", ["GET"], - self._handle_list_strips) - self.server.register_endpoint( - "/machine/wled/status", ["GET"], - self._handle_batch_wled_request) - self.server.register_endpoint( - "/machine/wled/on", ["POST"], - self._handle_batch_wled_request) - self.server.register_endpoint( - "/machine/wled/off", ["POST"], - self._handle_batch_wled_request) - self.server.register_endpoint( - "/machine/wled/strip", ["GET", "POST"], - self._handle_single_wled_request) + # Register two remote methods for GCODE + self.server.register_remote_method( + "set_wled_state", self.set_wled_state) + self.server.register_remote_method( + "set_wled", self.set_wled) - except Exception as e: - logging.exception(e) + # As moonraker is about making things a web api, let's try it + # Yes, this is largely a cut-n-paste from power.py + self.server.register_endpoint( + "/machine/wled/strips", ["GET"], + self._handle_list_strips) + self.server.register_endpoint( + "/machine/wled/status", ["GET"], + self._handle_batch_wled_request) + self.server.register_endpoint( + "/machine/wled/on", ["POST"], + self._handle_batch_wled_request) + self.server.register_endpoint( + "/machine/wled/off", ["POST"], + self._handle_batch_wled_request) + self.server.register_endpoint( + "/machine/wled/strip", ["GET", "POST"], + self._handle_single_wled_request) async def component_init(self) -> None: try: