Skip to content

Commit 76f3212

Browse files
committed
Add support for sections
1 parent 1028f27 commit 76f3212

7 files changed

Lines changed: 295 additions & 10 deletions

File tree

.gitignore

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
/.idea
2-
*.xml
1+
.idea/
2+
.tox/
3+
animatedledstrip_client.egg-info/
4+
build/
5+
dist/
6+
venv/
37
*.iml
4-
/venv
5-
test.py
6-
/animatedledstrip_client.egg-info
7-
/dist
8-
/.tox
8+
*.xml
99
.coverage
10-
build/
10+
.mutmut-cache
11+
test.py

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ sender.start()
3232
```
3333

3434
## Stopping the `AnimationSender`
35-
An `AnimationSender` is started by calling the `end()` method on the instance.
35+
An `AnimationSender` is stopped by calling the `end()` method on the instance.
3636

3737
```python
3838
sender.end()

animatedledstrip/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@
77
from .global_vars import ANIMATION_DATA_PREFIX, ANIMATION_INFO_PREFIX, DELIMITER, \
88
END_ANIMATION_PREFIX, SECTION_PREFIX, STRICT_TYPE_CHECKING, STRIP_INFO_PREFIX
99
from .param_usage import ParamUsage
10+
from .section import Section
1011
from .strip_info import StripInfo

animatedledstrip/animation_sender.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from .animation_info import AnimationInfo
2828
from .end_animation import EndAnimation
2929
from .global_vars import *
30+
from .section import Section
3031
from .strip_info import StripInfo
3132

3233

@@ -40,13 +41,15 @@ def __init__(self, ip_address: str, port_num: int):
4041
self.connected: bool = False
4142
self.recv_thread: Optional['Thread'] = None
4243
self.running_animations: Dict[str, 'AnimationData'] = {}
44+
self.sections: Dict[str, 'Section'] = {}
4345
self.stripInfo: Optional['StripInfo'] = None
4446
self.supported_animations: List['AnimationInfo'] = []
4547

4648
self.receiveCallback: Optional[Callable[[bytes], Any]] = None
4749
self.newAnimationDataCallback: Optional[Callable[['AnimationData'], Any]] = None
4850
self.newAnimationInfoCallback: Optional[Callable[['AnimationInfo'], Any]] = None
4951
self.newEndAnimationCallback: Optional[Callable[['EndAnimation'], Any]] = None
52+
self.newSectionCallback: Optional[Callable[['Section'], Any]] = None
5053
self.newStripInfoCallback: Optional[Callable[['StripInfo'], Any]] = None
5154

5255
def start(self) -> 'AnimationSender':
@@ -141,7 +144,15 @@ def parse_data(self):
141144
self.newEndAnimationCallback(data)
142145

143146
elif split_input.startswith(SECTION_PREFIX):
144-
pass # TODO
147+
# Create the EndAnimation instance
148+
sect = Section.from_json(split_input)
149+
150+
# Add new section to the sections dict
151+
self.sections[sect.name] = sect
152+
153+
# Call callback
154+
if self.newSectionCallback:
155+
self.newSectionCallback(sect)
145156

146157
elif split_input.startswith(STRIP_INFO_PREFIX):
147158
# Create the StripInfo instance

animatedledstrip/section.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Copyright (c) 2019-2020 AnimatedLEDStrip
2+
#
3+
# Permission is hereby granted, free of charge, to any person obtaining a copy
4+
# of this software and associated documentation files (the "Software"), to deal
5+
# in the Software without restriction, including without limitation the rights
6+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
# copies of the Software, and to permit persons to whom the Software is
8+
# furnished to do so, subject to the following conditions:
9+
#
10+
# The above copyright notice and this permission notice shall be included in
11+
# all copies or substantial portions of the Software.
12+
#
13+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
# THE SOFTWARE.
20+
21+
import json
22+
from typing import AnyStr
23+
24+
from animatedledstrip.utils import check_data_type
25+
26+
27+
class Section(object):
28+
"""Stores information about a section of the LED strip"""
29+
30+
def __init__(self):
31+
self.name: str = ''
32+
self.start_pixel: int = -1
33+
self.end_pixel: int = -1
34+
self.physical_start: int = -1
35+
self.num_leds: int = 0
36+
37+
def json(self) -> str:
38+
"""Create a JSON representation of this instance"""
39+
if self.check_data_types() is False:
40+
# If something has a bad data type (and STRICT_TYPE_CHECKING is False), return an empty string
41+
return ''
42+
else:
43+
# Otherwise, create the JSON representation and return it
44+
return ''.join(['SECT:{',
45+
','.join([
46+
'"name":"{}"'.format(self.name),
47+
'"startPixel":{}'.format(self.start_pixel),
48+
'"endPixel":{}'.format(self.end_pixel)]),
49+
'}'])
50+
51+
@classmethod
52+
def from_json(cls, input_str: AnyStr) -> 'Section':
53+
"""Create a Section instance from a json representation"""
54+
# Parse the JSON
55+
input_json = json.loads(input_str[5:])
56+
57+
# Create a new StripInfo instance
58+
new_instance = cls()
59+
60+
new_instance.name = input_json.get('name', new_instance.name)
61+
new_instance.start_pixel = input_json.get('startPixel', new_instance.start_pixel)
62+
new_instance.end_pixel = input_json.get('endPixel', new_instance.end_pixel)
63+
new_instance.physical_start = input_json.get('physicalStart', new_instance.physical_start)
64+
new_instance.num_leds = input_json.get('numLEDs', new_instance.num_leds)
65+
66+
# Double check that everything has the right data type
67+
new_instance.check_data_types()
68+
69+
return new_instance
70+
71+
def check_data_types(self) -> bool:
72+
"""Check that all parameter types are correct"""
73+
good_types = True
74+
75+
good_types = good_types and check_data_type('name', self.name, str)
76+
good_types = good_types and check_data_type('start_pixel', self.start_pixel, int)
77+
good_types = good_types and check_data_type('end_pixel', self.end_pixel, int)
78+
good_types = good_types and check_data_type('physical_start', self.physical_start, int)
79+
good_types = good_types and check_data_type('num_leds', self.num_leds, int)
80+
81+
return good_types

test/animatedledstrip/test_animation_sender.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ def test_constructor():
3737
assert sender.newAnimationDataCallback is None
3838
assert sender.newAnimationInfoCallback is None
3939
assert sender.newEndAnimationCallback is None
40+
assert sender.newSectionCallback is None
4041
assert sender.newStripInfoCallback is None
4142

4243

@@ -179,6 +180,31 @@ def test_parse_data_end_animation():
179180
assert mock_callback.called
180181

181182

183+
def test_parse_data_section():
184+
sender = AnimationSender('10.0.0.254', 5)
185+
186+
with mock.patch.object(sender, 'connection') as mock_socket:
187+
with mock.patch.object(sender, 'newSectionCallback') as mock_callback:
188+
mock_socket.recv.return_value = bytes('SECT:{"physicalStart":0,"numLEDs":240,"name":"section",'
189+
'"startPixel":0,"endPixel":239}',
190+
'utf-8')
191+
192+
sender.parse_data()
193+
194+
assert 'section' in sender.sections
195+
196+
sect = sender.sections['section']
197+
198+
assert sect.name == 'section'
199+
assert sect.start_pixel == 0
200+
assert sect.end_pixel == 239
201+
assert sect.physical_start == 0
202+
assert sect.num_leds == 240
203+
204+
assert mock_callback.called
205+
assert mock_callback.called_with(sect)
206+
207+
182208
def test_parse_data_strip_info():
183209
sender = AnimationSender('10.0.0.254', 5)
184210

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# Copyright (c) 2019-2020 AnimatedLEDStrip
2+
#
3+
# Permission is hereby granted, free of charge, to any person obtaining a copy
4+
# of this software and associated documentation files (the "Software"), to deal
5+
# in the Software without restriction, including without limitation the rights
6+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
# copies of the Software, and to permit persons to whom the Software is
8+
# furnished to do so, subject to the following conditions:
9+
#
10+
# The above copyright notice and this permission notice shall be included in
11+
# all copies or substantial portions of the Software.
12+
#
13+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
# THE SOFTWARE.
20+
21+
from unittest import mock
22+
23+
from animatedledstrip import *
24+
25+
26+
def test_constructor():
27+
sect = Section()
28+
29+
assert sect.name == ''
30+
assert sect.start_pixel == -1
31+
assert sect.end_pixel == -1
32+
assert sect.physical_start == -1
33+
assert sect.num_leds == 0
34+
35+
36+
def test_name(caplog):
37+
sect = Section()
38+
39+
sect.name = 'Test'
40+
assert sect.check_data_types() is True
41+
42+
sect.name = 5
43+
with mock.patch('animatedledstrip.global_vars.STRICT_TYPE_CHECKING', False):
44+
assert sect.check_data_types() is False
45+
log_messages = {(log.msg, log.levelname) for log in caplog.records}
46+
assert log_messages == {("Bad data type for name: <class 'int'> (should be <class 'str'>)", 'ERROR')}
47+
48+
try:
49+
sect.check_data_types()
50+
raise AssertionError
51+
except TypeError:
52+
pass
53+
54+
55+
def test_start_pixel(caplog):
56+
sect = Section()
57+
58+
sect.start_pixel = 3
59+
assert sect.check_data_types() is True
60+
61+
sect.start_pixel = '5'
62+
with mock.patch('animatedledstrip.global_vars.STRICT_TYPE_CHECKING', False):
63+
assert sect.check_data_types() is False
64+
log_messages = {(log.msg, log.levelname) for log in caplog.records}
65+
assert log_messages == {("Bad data type for start_pixel: <class 'str'> (should be <class 'int'>)", 'ERROR')}
66+
67+
try:
68+
sect.check_data_types()
69+
raise AssertionError
70+
except TypeError:
71+
pass
72+
73+
74+
def test_end_pixel(caplog):
75+
sect = Section()
76+
77+
sect.end_pixel = 3
78+
assert sect.check_data_types() is True
79+
80+
sect.end_pixel = '5'
81+
with mock.patch('animatedledstrip.global_vars.STRICT_TYPE_CHECKING', False):
82+
assert sect.check_data_types() is False
83+
log_messages = {(log.msg, log.levelname) for log in caplog.records}
84+
assert log_messages == {("Bad data type for end_pixel: <class 'str'> (should be <class 'int'>)", 'ERROR')}
85+
86+
try:
87+
sect.check_data_types()
88+
raise AssertionError
89+
except TypeError:
90+
pass
91+
92+
93+
def test_physical_start(caplog):
94+
sect = Section()
95+
96+
sect.physical_start = 3
97+
assert sect.check_data_types() is True
98+
99+
sect.physical_start = '5'
100+
with mock.patch('animatedledstrip.global_vars.STRICT_TYPE_CHECKING', False):
101+
assert sect.check_data_types() is False
102+
log_messages = {(log.msg, log.levelname) for log in caplog.records}
103+
assert log_messages == {("Bad data type for physical_start: <class 'str'> (should be <class 'int'>)", 'ERROR')}
104+
105+
try:
106+
sect.check_data_types()
107+
raise AssertionError
108+
except TypeError:
109+
pass
110+
111+
112+
def test_num_leds(caplog):
113+
sect = Section()
114+
115+
sect.num_leds = 3
116+
assert sect.check_data_types() is True
117+
118+
sect.num_leds = '5'
119+
with mock.patch('animatedledstrip.global_vars.STRICT_TYPE_CHECKING', False):
120+
assert sect.check_data_types() is False
121+
log_messages = {(log.msg, log.levelname) for log in caplog.records}
122+
assert log_messages == {("Bad data type for num_leds: <class 'str'> (should be <class 'int'>)", 'ERROR')}
123+
124+
try:
125+
sect.check_data_types()
126+
raise AssertionError
127+
except TypeError:
128+
pass
129+
130+
131+
def test_json():
132+
sect = Section()
133+
134+
sect.name = 'TEST'
135+
sect.start_pixel = 40
136+
sect.end_pixel = 50
137+
138+
assert sect.json() == 'SECT:{"name":"TEST","startPixel":40,"endPixel":50}'
139+
140+
141+
def test_json_bad_type():
142+
sect = Section()
143+
sect.start_pixel = 'bad'
144+
145+
with mock.patch('animatedledstrip.global_vars.STRICT_TYPE_CHECKING', False):
146+
assert sect.json() == ''
147+
148+
try:
149+
sect.json()
150+
raise AssertionError
151+
except TypeError:
152+
pass
153+
154+
155+
def test_from_json():
156+
json_data = 'SECT:{"physicalStart":0,"numLEDs":240,"name":"section","startPixel":0,"endPixel":239}'
157+
158+
sect = Section.from_json(json_data)
159+
assert sect.name == 'section'
160+
assert sect.start_pixel == 0
161+
assert sect.end_pixel == 239
162+
assert sect.physical_start == 0
163+
assert sect.num_leds == 240
164+
165+
assert sect.json() == 'SECT:{"name":"section","startPixel":0,"endPixel":239}'

0 commit comments

Comments
 (0)