From 5f0d0dcf3544b047870944bbcfb5c92d06982f3a Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 Date: Mon, 20 Feb 2023 05:54:10 +0100 Subject: [PATCH] 1.0.1: - Fixed an issue where consecutive fades would not start from the correct value --- readme.md | 4 ++ src/pyartnet/__version__.py | 2 +- src/pyartnet/base/channel.py | 47 +++++++++-------- src/pyartnet/base/channel_fade.py | 2 +- src/pyartnet/fades/fade_base.py | 14 ++---- src/pyartnet/fades/fade_linear.py | 24 +++++---- tests/channel/test_boundaries.py | 84 ++++++++++++++++++------------- tests/channel/test_channel.py | 15 ++++++ tests/channel/test_fade.py | 55 ++++++++++++-------- tests/test_base_node.py | 6 +-- tests/test_impl/test_impl.py | 2 +- 11 files changed, 149 insertions(+), 106 deletions(-) create mode 100644 tests/channel/test_channel.py diff --git a/readme.md b/readme.md index f445ad2..41628da 100644 --- a/readme.md +++ b/readme.md @@ -15,6 +15,10 @@ Docs and examples can be found [here](https://pyartnet.readthedocs.io/en/latest/ # Changelog +#### 1.0.1 (2023-02-20) +- Fixed an issue where consecutive fades would not start from the correct value +- renamed `channel.add_fade` to `channel.set_fade` (`channel.add_fade` will issue a `DeprecationWarning`) + #### 1.0.0 (2023-02-08) - Complete rework of library (breaking change) - Add support for sACN and KiNet diff --git a/src/pyartnet/__version__.py b/src/pyartnet/__version__.py index 1f356cc..cd7ca49 100644 --- a/src/pyartnet/__version__.py +++ b/src/pyartnet/__version__.py @@ -1 +1 @@ -__version__ = '1.0.0' +__version__ = '1.0.1' diff --git a/src/pyartnet/base/channel.py b/src/pyartnet/base/channel.py index f1e3116..49aa05b 100644 --- a/src/pyartnet/base/channel.py +++ b/src/pyartnet/base/channel.py @@ -1,8 +1,9 @@ import logging +import warnings from array import array from logging import DEBUG as LVL_DEBUG from math import ceil -from typing import Any, Callable, Final, Iterable, List, Literal, Optional, Type, Union +from typing import Any, Callable, Collection, Final, List, Literal, Optional, Type, Union from pyartnet.errors import ChannelOutOfUniverseError, ChannelValueOutOfBoundsError, \ ChannelWidthError, ValueCountDoesNotMatchChannelWidthError @@ -96,17 +97,20 @@ def get_values(self) -> List[int]: """ return self._values_raw.tolist() - def set_values(self, values: Iterable[Union[int, float]]): + def set_values(self, values: Collection[Union[int, float]]): """Set values for a channel without a fade :param values: Iterable of values with the same size as the channel width """ # get output correction function + if len(values) != self._width: + raise ValueCountDoesNotMatchChannelWidthError( + f'Not enough fade values specified, expected {self._width} but got {len(values)}!') + correction = self._correction_current value_max = self._value_max changed = False - i: int = -1 for i, val in enumerate(values): raw_new = round(val) if not 0 <= raw_new <= value_max: @@ -118,11 +122,6 @@ def set_values(self, values: Iterable[Union[int, float]]): changed = True self._values_act[i] = act_new - # check that we passed all values - if i + 1 != self._width: - raise ValueCountDoesNotMatchChannelWidthError( - f'Not enough fade values specified, expected {self._width} but got {i + 1}!') - if changed: self._parent_universe.channel_changed(self) return self @@ -137,8 +136,14 @@ def to_buffer(self, buf: bytearray): start += byte_size return self + def add_fade(self, values: Collection[Union[int, FadeBase]], duration_ms: int, + fade_class: Type[FadeBase] = LinearFade): + warnings.warn( + f"{self.set_fade.__name__:s} is deprecated, use {self.set_fade.__name__:s} instead", DeprecationWarning) + return self.set_fade(values, duration_ms, fade_class) + # noinspection PyProtectedMember - def add_fade(self, values: Iterable[Union[int, FadeBase]], duration_ms: int, + def set_fade(self, values: Collection[Union[int, FadeBase]], duration_ms: int, fade_class: Type[FadeBase] = LinearFade): """Add and schedule a new fade for the channel @@ -146,6 +151,10 @@ def add_fade(self, values: Iterable[Union[int, FadeBase]], duration_ms: int, :param duration_ms: Duration for the fade in ms :param fade_class: What kind of fade """ + # check that we passed all values + if len(values) != self._width: + raise ValueCountDoesNotMatchChannelWidthError( + f'Not enough fade values specified, expected {self._width} but got {len(values)}!') if self._current_fade is not None: self._current_fade.cancel() @@ -158,22 +167,16 @@ def add_fade(self, values: Iterable[Union[int, FadeBase]], duration_ms: int, # build fades fades: List[FadeBase] = [] - i: int = -1 - for i, val in enumerate(values): # noqa: B007 + for i, target in enumerate(values): # default is linear - k = fade_class(val) if not isinstance(val, FadeBase) else val + k = fade_class() if not isinstance(target, FadeBase) else target fades.append(k) - if not 0 <= k.val_target <= self._value_max: + if not 0 <= target <= self._value_max: raise ChannelValueOutOfBoundsError( - f'Target value out of bounds! 0 <= {k.val_target} <= {self._value_max}') + f'Target value out of bounds! 0 <= {target} <= {self._value_max}') - k.initialize(fade_steps) - - # check that we passed all values - if i + 1 != self._width: - raise ValueCountDoesNotMatchChannelWidthError( - f'Not enough fade values specified, expected {self._width} but got {i + 1}!') + k.initialize(self._values_raw[i], target, fade_steps) # Add to scheduling self._current_fade = ChannelBoundFade(self, fades) @@ -182,11 +185,11 @@ def add_fade(self, values: Iterable[Union[int, FadeBase]], duration_ms: int, # start fade/refresh task if necessary self._parent_node._process_task.start() + # todo: this on the ChannelBoundFade if log.isEnabledFor(LVL_DEBUG): log.debug(f'Added fade with {fade_steps} steps:') for i, fade in enumerate(fades): - log.debug(f'CH {self._start + i}: {self._values_raw[i]:03d} -> {fade.val_target:03d}' - f' | {fade.debug_initialize():s}') + log.debug(f'CH {self._start + i}: {fade.debug_initialize():s}') return self def __await__(self): diff --git a/src/pyartnet/base/channel_fade.py b/src/pyartnet/base/channel_fade.py index 09fbfab..87c0c32 100644 --- a/src/pyartnet/base/channel_fade.py +++ b/src/pyartnet/base/channel_fade.py @@ -16,7 +16,7 @@ def __init__(self, channel: 'pyartnet.base.Channel', fades: Iterable['pyartnet.f self.channel: 'pyartnet.base.Channel' = channel self.fades: Tuple['pyartnet.fades.FadeBase', ...] = tuple(fades) - self.values: List[float] = [f.val_current for f in fades] + self.values: List[float] = [0 for _ in fades] self.is_done = False self.event: Final = Event() diff --git a/src/pyartnet/fades/fade_base.py b/src/pyartnet/fades/fade_base.py index 5678b1f..bb3e6e6 100644 --- a/src/pyartnet/fades/fade_base.py +++ b/src/pyartnet/fades/fade_base.py @@ -1,23 +1,15 @@ -import abc +class FadeBase: -class FadeBase(metaclass=abc.ABCMeta): - - def __init__(self, target: int): - self.val_target : int = int(target) # Target Value - self.val_start : int = 0 # Start Value - self.val_current : float = 0.0 # Current Value - + def __init__(self): self.is_done = False - @abc.abstractmethod - def initialize(self, steps : int): + def initialize(self, current: int, target: int, steps: int): raise NotImplementedError() def debug_initialize(self) -> str: """return debug string of the calculated values in initialize fade""" return "" - @abc.abstractmethod def calc_next_value(self) -> float: raise NotImplementedError() diff --git a/src/pyartnet/fades/fade_linear.py b/src/pyartnet/fades/fade_linear.py index a4527e8..bb6e0f4 100644 --- a/src/pyartnet/fades/fade_linear.py +++ b/src/pyartnet/fades/fade_linear.py @@ -3,26 +3,30 @@ class LinearFade(FadeBase): - def __init__(self, target: int): - super().__init__(target) + def __init__(self): + super().__init__() + self.target: int = 0 # Target Value + self.current: float = 0.0 # Current Value self.factor: float = 1.0 def debug_initialize(self) -> str: - return f"step: {self.factor:+5.1f}" + return f"{self.current:03.0f} -> {self.target:03d} | step: {self.factor:+5.1f}" - def initialize(self, steps: int): - self.factor = (self.val_target - self.val_start) / steps + def initialize(self, start: int, target: int, steps: int): + self.current = start + self.target = target + self.factor = (self.target - start) / steps def calc_next_value(self) -> float: - self.val_current += self.factor + self.current += self.factor # is_done status - curr = round(self.val_current) + curr = round(self.current) if self.factor <= 0: - if curr <= self.val_target: + if curr <= self.target: self.is_done = True else: - if curr >= self.val_target: + if curr >= self.target: self.is_done = True - return curr + return self.current diff --git a/tests/channel/test_boundaries.py b/tests/channel/test_boundaries.py index f0c7b79..efff26d 100644 --- a/tests/channel/test_boundaries.py +++ b/tests/channel/test_boundaries.py @@ -2,9 +2,9 @@ import pytest -from pyartnet.base import BaseUniverse from pyartnet.base.channel import Channel -from pyartnet.errors import ChannelOutOfUniverseError, ChannelValueOutOfBoundsError +from pyartnet.errors import ChannelOutOfUniverseError, \ + ChannelValueOutOfBoundsError, ValueCountDoesNotMatchChannelWidthError def test_channel_boundaries(): @@ -32,44 +32,58 @@ def test_channel_boundaries(): Channel(univ, 511, 1, byte_size=2) -def test_set_value_invalid(): +def get_node_universe_mock(): + node = Mock() + node._process_every = 0.001 + universe = Mock() + universe._node = node universe.output_correction = None + return node, universe - b = Channel(universe, 1, 1) - with pytest.raises(ChannelValueOutOfBoundsError) as e: - b.set_values([256]) - assert str(e.value) == 'Channel value out of bounds! 0 <= 256 <= 255' - b.set_values([255]) - b = Channel(universe, 1, 1, byte_size=2) - with pytest.raises(ChannelValueOutOfBoundsError) as e: - b.set_values([65536]) - assert str(e.value) == 'Channel value out of bounds! 0 <= 65536 <= 65535' - b.set_values([65535]) +@pytest.mark.parametrize( + ('width', 'byte_size', 'invalid', 'valid'), + ((1, 1, -1, 255), (1, 1, 256, 255), (3, 1, 256, 255), + (1, 2, -1, 65535), (1, 2, 65536, 65535), (3, 2, 65536, 65535), )) +def test_set_invalid(width, byte_size, invalid, valid): + node, universe = get_node_universe_mock() - b = Channel(universe, 3, 3) + invalid_values = [0] * (width - 1) + [invalid] + valid_values = [0] * (width - 1) + [valid] + + # test set_values + c = Channel(universe, 1, width, byte_size=byte_size) with pytest.raises(ChannelValueOutOfBoundsError) as e: - b.set_values([0, 0, 256]) - assert str(e.value) == 'Channel value out of bounds! 0 <= 256 <= 255' - b.set_values([0, 0, 255]) + c.set_values(invalid_values) + assert str(e.value) == f'Channel value out of bounds! 0 <= {invalid:d} <= {valid}' + c.set_values(valid_values) - b = Channel(universe, 3, 3, byte_size=2) + # test set_fade + c = Channel(universe, 1, width, byte_size=byte_size) with pytest.raises(ChannelValueOutOfBoundsError) as e: - b.set_values([0, 0, 65536]) - assert str(e.value) == 'Channel value out of bounds! 0 <= 65536 <= 65535' - b.set_values([0, 0, 65535]) - - -def test_values_add_channel(universe: BaseUniverse): - u = universe.add_channel(1, 2, byte_size=3, byte_order='big') - assert u._start == 1 - assert u._width == 2 - assert u._byte_size == 3 - assert u._byte_order == 'big' - - u = universe.add_channel(10, 5, byte_size=2, byte_order='little') - assert u._start == 10 - assert u._width == 5 - assert u._byte_size == 2 - assert u._byte_order == 'little' + c.set_fade(invalid_values, 100) + assert str(e.value) == f'Target value out of bounds! 0 <= {invalid:d} <= {valid}' + c.set_fade(valid_values, 100) + + +async def test_set_missing(): + node, universe = get_node_universe_mock() + + c = Channel(universe, 1, 1) + with pytest.raises(ValueCountDoesNotMatchChannelWidthError) as e: + c.set_values([0, 0, 255]) + assert str(e.value) == 'Not enough fade values specified, expected 1 but got 3!' + + with pytest.raises(ValueCountDoesNotMatchChannelWidthError) as e: + c.set_fade([0, 0, 255], 0) + assert str(e.value) == 'Not enough fade values specified, expected 1 but got 3!' + + c = Channel(universe, 1, 3) + with pytest.raises(ValueCountDoesNotMatchChannelWidthError) as e: + c.set_values([0, 255]) + assert str(e.value) == 'Not enough fade values specified, expected 3 but got 2!' + + with pytest.raises(ValueCountDoesNotMatchChannelWidthError) as e: + c.set_fade([0, 255], 0) + assert str(e.value) == 'Not enough fade values specified, expected 3 but got 2!' diff --git a/tests/channel/test_channel.py b/tests/channel/test_channel.py new file mode 100644 index 0000000..73c0e57 --- /dev/null +++ b/tests/channel/test_channel.py @@ -0,0 +1,15 @@ +from pyartnet.base import BaseUniverse + + +def test_values_add_channel(universe: BaseUniverse): + u = universe.add_channel(1, 2, byte_size=3, byte_order='big') + assert u._start == 1 + assert u._width == 2 + assert u._byte_size == 3 + assert u._byte_order == 'big' + + u = universe.add_channel(10, 5, byte_size=2, byte_order='little') + assert u._start == 10 + assert u._width == 5 + assert u._byte_size == 2 + assert u._byte_order == 'little' diff --git a/tests/channel/test_fade.py b/tests/channel/test_fade.py index 27b2c71..a66273a 100644 --- a/tests/channel/test_fade.py +++ b/tests/channel/test_fade.py @@ -1,11 +1,8 @@ import asyncio from time import monotonic -import pytest - from pyartnet.base import BaseUniverse from pyartnet.base.channel import Channel -from pyartnet.errors import ChannelValueOutOfBoundsError, ValueCountDoesNotMatchChannelWidthError from tests.conftest import STEP_MS, TestingNode @@ -13,7 +10,7 @@ async def test_channel_await(node: TestingNode, universe: BaseUniverse, caplog): a = Channel(universe, 1, 1) assert a.get_values() == [0] - a.add_fade([255], 200) + a.set_fade([255], 200) start = monotonic() await asyncio.wait_for(a, 1) @@ -28,7 +25,7 @@ async def test_single_step(node: TestingNode, universe: BaseUniverse, caplog): a = Channel(universe, 1, 1) assert a.get_values() == [0] - a.add_fade([255], 0) + a.set_fade([255], 0) assert a.get_values() == [0] assert list(caplog.messages) == [ @@ -50,7 +47,7 @@ async def test_single_fade(node: TestingNode, universe: BaseUniverse, caplog): a = Channel(universe, 1, 1) assert a.get_values() == [0] - a.add_fade([2], 2 * STEP_MS) + a.set_fade([2], 2 * STEP_MS) assert a.get_values() == [0] assert list(caplog.messages) == [ @@ -72,7 +69,7 @@ async def test_tripple_fade(node: TestingNode, universe: BaseUniverse, caplog): a = Channel(universe, 1, 3) assert a.get_values() == [0, 0, 0] - a.add_fade([3, 6, 9], 3 * STEP_MS) + a.set_fade([3, 6, 9], 3 * STEP_MS) assert a.get_values() == [0, 0, 0] assert list(caplog.messages) == [ @@ -90,18 +87,6 @@ async def test_tripple_fade(node: TestingNode, universe: BaseUniverse, caplog): assert node.data == ['010203', '020406', '030609'] -async def test_fade_errors(node: TestingNode, universe: BaseUniverse): - c = universe.add_channel(1, 1) - - with pytest.raises(ChannelValueOutOfBoundsError) as e: - c.add_fade([0, 0, 256], 0) - assert str(e.value) == 'Target value out of bounds! 0 <= 256 <= 255' - - with pytest.raises(ValueCountDoesNotMatchChannelWidthError) as e: - c.add_fade([0, 0, 255], 0) - assert str(e.value) == 'Not enough fade values specified, expected 1 but got 3!' - - async def test_fade_await(node: TestingNode, universe: BaseUniverse, caplog): caplog.set_level(0) @@ -116,7 +101,7 @@ async def check_no_wait_time_when_no_fade(): await check_no_wait_time_when_no_fade() - channel.add_fade([2], 2 * STEP_MS) + channel.set_fade([2], 2 * STEP_MS) assert channel.get_values() == [0] assert list(caplog.messages) == [ @@ -132,12 +117,38 @@ async def check_no_wait_time_when_no_fade(): await check_no_wait_time_when_no_fade() - channel.add_fade([10], 2 * STEP_MS) + channel.set_fade([10], 2 * STEP_MS) assert channel._current_fade is not None await channel assert channel._current_fade is None - assert node.data == ['01', '02', '05', '0a'] + assert node.data == ['01', '02', '06', '0a'] await check_no_wait_time_when_no_fade() await node.wait_for_task_finish() + + +async def test_up_down_fade(node: TestingNode, universe: BaseUniverse, caplog): + caplog.set_level(0) + + a = Channel(universe, 1, 1) + for _ in range(5): + node.data.clear() + assert a.get_values() == [0] + + a.set_fade([255], 2 * STEP_MS) + assert a.get_values() == [0] + + await a + + assert a.get_values() == [255] + assert node.data == ['80', 'ff'] + + # Fade down + a.set_fade([0], 2 * STEP_MS) + assert a.get_values() == [255] + + await a + + assert a.get_values() == [0] + assert node.data == ['80', 'ff', '80', '00'] diff --git a/tests/test_base_node.py b/tests/test_base_node.py index c727faf..72f6ca1 100644 --- a/tests/test_base_node.py +++ b/tests/test_base_node.py @@ -53,7 +53,7 @@ async def check_wait_time_when_fade(steps: int): await check_no_wait_time_when_no_fade() - channel.add_fade([2], 2 * STEP_MS) + channel.set_fade([2], 2 * STEP_MS) assert channel.get_values() == [0] assert list(caplog.messages) == [ @@ -69,12 +69,12 @@ async def check_wait_time_when_fade(steps: int): await check_no_wait_time_when_no_fade() - channel.add_fade([10], 2 * STEP_MS) + channel.set_fade([10], 2 * STEP_MS) assert channel._current_fade is not None await check_wait_time_when_fade(2) assert channel._current_fade is None - assert node.data == ['01', '02', '05', '0a'] + assert node.data == ['01', '02', '06', '0a'] await check_no_wait_time_when_no_fade() await node.wait_for_task_finish() diff --git a/tests/test_impl/test_impl.py b/tests/test_impl/test_impl.py index 7c976f8..d2bfa4e 100644 --- a/tests/test_impl/test_impl.py +++ b/tests/test_impl/test_impl.py @@ -30,5 +30,5 @@ async def test_set_funcs(node: TestingNode, caplog, cls): c.set_values([5]) await sleep(0.1) - c.add_fade([250], 700) + c.set_fade([250], 700) await c