Skip to content

Commit

Permalink
1.0.1:
Browse files Browse the repository at this point in the history
- Fixed an issue where consecutive fades would not start from the correct value
  • Loading branch information
spacemanspiff2007 committed Feb 20, 2023
1 parent 27c6f2d commit 5f0d0dc
Show file tree
Hide file tree
Showing 11 changed files with 149 additions and 106 deletions.
4 changes: 4 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/pyartnet/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '1.0.0'
__version__ = '1.0.1'
47 changes: 25 additions & 22 deletions src/pyartnet/base/channel.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -137,15 +136,25 @@ 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
:param values: Target values for the fade
: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()
Expand All @@ -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)
Expand All @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion src/pyartnet/base/channel_fade.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
14 changes: 3 additions & 11 deletions src/pyartnet/fades/fade_base.py
Original file line number Diff line number Diff line change
@@ -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()
24 changes: 14 additions & 10 deletions src/pyartnet/fades/fade_linear.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
84 changes: 49 additions & 35 deletions tests/channel/test_boundaries.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down Expand Up @@ -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!'
15 changes: 15 additions & 0 deletions tests/channel/test_channel.py
Original file line number Diff line number Diff line change
@@ -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'
Loading

0 comments on commit 5f0d0dc

Please sign in to comment.