Skip to content

Commit 554ed97

Browse files
committed
Add support for Message, Command, and processing partial data
1 parent 76f3212 commit 554ed97

7 files changed

Lines changed: 183 additions & 44 deletions

File tree

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ Runs before `newAnimationDataCallback`, `newAnimationInfoCallback`, `newEndAnima
7474
def receiveData(data: bytes):
7575
# Your code here
7676

77-
sender.receiveCallback = receiveData
77+
sender.on_receive_callback = receiveData
7878
```
7979

8080
### NewAnimationDataCallback
@@ -86,7 +86,7 @@ Runs after the `receiveCallback`.
8686
def processAnimationData(data: 'AnimationData'):
8787
# Your code here
8888

89-
sender.newAnimationDataCallback = processAnimationData
89+
sender.on_new_animation_data_callback = processAnimationData
9090
```
9191

9292
### NewAnimationInfoCallback
@@ -98,7 +98,7 @@ Runs after the `receiveCallback`.
9898
def handleNewAnimationInfo(info: 'AnimationInfo'):
9999
# Your code here
100100

101-
sender.newAnimationInfoCallback = handleNewAnimationInfo
101+
sender.on_new_animation_info_callback = handleNewAnimationInfo
102102
```
103103

104104
### NewEndAnimationCallback
@@ -110,7 +110,7 @@ Runs after the `receiveCallback`.
110110
def handleEndAnimation(anim: 'EndAnimation'):
111111
# Your code here
112112

113-
sender.newEndAnimationCallback = handleEndAnimation
113+
sender.on_new_end_animation_callback = handleEndAnimation
114114
```
115115

116116
### NewSectionCallback *(Coming soon)*
@@ -122,7 +122,7 @@ Runs after the `receiveCallback`.
122122
def newSectionHandler(sect: 'Section'):
123123
# Your code here
124124

125-
sender.newSectionCallback = newSectionHandler
125+
sender.on_new_section_callback = newSectionHandler
126126
```
127127

128128
### NewStripInfoCallback *(Coming soon)*
@@ -134,5 +134,5 @@ Runs after the `receiveCallback`.
134134
def processStripInfo(info: 'StripInfo'):
135135
# Your code here
136136

137-
sender.newStripInfoCallback = processStripInfo
137+
sender.on_new_strip_info_callback = processStripInfo
138138
```

animatedledstrip/animation_sender.py

Lines changed: 67 additions & 25 deletions
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 .message import Message
3031
from .section import Section
3132
from .strip_info import StripInfo
3233

@@ -39,21 +40,40 @@ def __init__(self, ip_address: str, port_num: int):
3940
self.port: int = port_num
4041
self.connection: 'socket.socket' = socket.socket()
4142
self.connected: bool = False
43+
self.started: bool = False
4244
self.recv_thread: Optional['Thread'] = None
45+
4346
self.running_animations: Dict[str, 'AnimationData'] = {}
4447
self.sections: Dict[str, 'Section'] = {}
45-
self.stripInfo: Optional['StripInfo'] = None
4648
self.supported_animations: List['AnimationInfo'] = []
49+
self.strip_info: Optional['StripInfo'] = None
50+
51+
self.on_connect_callback: Optional[Callable[[str, int], Any]] = None
52+
self.on_disconnect_callback: Optional[Callable[[str, int], Any]] = None
53+
self.on_unable_to_connect_callback: Optional[Callable[[str, int], Any]] = None
4754

48-
self.receiveCallback: Optional[Callable[[bytes], Any]] = None
49-
self.newAnimationDataCallback: Optional[Callable[['AnimationData'], Any]] = None
50-
self.newAnimationInfoCallback: Optional[Callable[['AnimationInfo'], Any]] = None
51-
self.newEndAnimationCallback: Optional[Callable[['EndAnimation'], Any]] = None
52-
self.newSectionCallback: Optional[Callable[['Section'], Any]] = None
53-
self.newStripInfoCallback: Optional[Callable[['StripInfo'], Any]] = None
55+
self.on_receive_callback: Optional[Callable[[bytes], Any]] = None
56+
self.on_new_animation_data_callback: Optional[Callable[['AnimationData'], Any]] = None
57+
self.on_new_animation_info_callback: Optional[Callable[['AnimationInfo'], Any]] = None
58+
self.on_new_end_animation_callback: Optional[Callable[['EndAnimation'], Any]] = None
59+
self.on_new_message_callback: Optional[Callable[['Message'], Any]] = None
60+
self.on_new_section_callback: Optional[Callable[['Section'], Any]] = None
61+
self.on_new_strip_info_callback: Optional[Callable[['StripInfo'], Any]] = None
62+
63+
self.partial_data: bytes = b''
5464

5565
def start(self) -> 'AnimationSender':
5666
"""Connect to the server"""
67+
if self.started:
68+
return self
69+
70+
self.running_animations.clear()
71+
self.sections.clear()
72+
self.supported_animations.clear()
73+
self.strip_info = None
74+
75+
self.started = True
76+
5777
# Attempt to connect to the server
5878
self.connection = socket.create_connection((self.address, self.port), timeout=2.0)
5979

@@ -74,7 +94,7 @@ def end(self) -> 'AnimationSender':
7494
# Connection has been closed, so set connected = False
7595
self.connected = False
7696

77-
self.stripInfo = None
97+
self.strip_info = None
7898

7999
# If the separate thread for receiving animations was started, join it with the main thread.
80100
# The loop should stop because the connection is closed and connected is False,
@@ -100,15 +120,26 @@ def parse_data(self):
100120
try:
101121
all_input: bytes = self.connection.recv(4096)
102122

123+
# Handle any partial data from previous communications
124+
complete_input: bytes = self.partial_data + all_input
125+
self.partial_data = b''
126+
103127
# Split up data (multiple may have come in the same message -
104128
# they are split up with triple semicolons)
105-
# TODO: Support partial data
106-
for split_input in all_input.split(DELIMITER):
129+
input_list = complete_input.split(DELIMITER)
130+
131+
# If last input is partial, save for later when the rest comes
132+
if not complete_input.endswith(DELIMITER):
133+
self.partial_data = input_list[-1]
134+
input_list = input_list[:-1]
135+
136+
# Process inputs
137+
for split_input in input_list:
107138
if len(split_input) == 0:
108139
continue
109140

110-
if self.receiveCallback:
111-
self.receiveCallback(split_input)
141+
if self.on_receive_callback:
142+
self.on_receive_callback(split_input)
112143

113144
if split_input.startswith(ANIMATION_DATA_PREFIX):
114145
# Create the AnimationData instance
@@ -118,8 +149,8 @@ def parse_data(self):
118149
self.running_animations[data.id] = data
119150

120151
# Call callback
121-
if self.newAnimationDataCallback:
122-
self.newAnimationDataCallback(data)
152+
if self.on_new_animation_data_callback:
153+
self.on_new_animation_data_callback(data)
123154

124155
elif split_input.startswith(ANIMATION_INFO_PREFIX):
125156
# Create the AnimationInfo instance
@@ -129,8 +160,11 @@ def parse_data(self):
129160
self.supported_animations.append(info)
130161

131162
# Call callback
132-
if self.newAnimationInfoCallback:
133-
self.newAnimationInfoCallback(info)
163+
if self.on_new_animation_info_callback:
164+
self.on_new_animation_info_callback(info)
165+
166+
elif split_input.startswith(COMMAND_PREFIX):
167+
logging.warning("Receiving Command is not supported by client")
134168

135169
elif split_input.startswith(END_ANIMATION_PREFIX):
136170
# Create the EndAnimation instance
@@ -140,32 +174,40 @@ def parse_data(self):
140174
del self.running_animations[data.id]
141175

142176
# Call callback
143-
if self.newEndAnimationCallback:
144-
self.newEndAnimationCallback(data)
177+
if self.on_new_end_animation_callback:
178+
self.on_new_end_animation_callback(data)
179+
180+
elif split_input.startswith(MESSAGE_PREFIX):
181+
# Create the Message instance
182+
msg = Message.from_json(split_input)
183+
184+
# Call callback
185+
if self.on_new_message_callback:
186+
self.on_new_message_callback(msg)
145187

146188
elif split_input.startswith(SECTION_PREFIX):
147-
# Create the EndAnimation instance
189+
# Create the Section instance
148190
sect = Section.from_json(split_input)
149191

150192
# Add new section to the sections dict
151193
self.sections[sect.name] = sect
152194

153195
# Call callback
154-
if self.newSectionCallback:
155-
self.newSectionCallback(sect)
196+
if self.on_new_section_callback:
197+
self.on_new_section_callback(sect)
156198

157199
elif split_input.startswith(STRIP_INFO_PREFIX):
158200
# Create the StripInfo instance
159201
info = StripInfo.from_json(split_input)
160202

161-
self.stripInfo = info
203+
self.strip_info = info
162204

163205
# Call callback
164-
if self.newStripInfoCallback:
165-
self.newStripInfoCallback(info)
206+
if self.on_new_strip_info_callback:
207+
self.on_new_strip_info_callback(info)
166208

167209
else:
168-
logging.warning('Unrecognized data type: {} ({})'.format(split_input[:4], split_input))
210+
logging.warning('Unrecognized data type: {}'.format(split_input[:4]))
169211

170212
except socket.timeout:
171213
pass

animatedledstrip/command.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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 .utils import check_data_type
22+
23+
24+
class Command(object):
25+
"""A command to send to the server"""
26+
27+
def __init__(self):
28+
self.command = ''
29+
30+
def json(self) -> str:
31+
"""Create a JSON representation of this instance"""
32+
if self.check_data_types() is False:
33+
# If something has a bad data type (and STRICT_TYPE_CHECKING is False), return an empty string
34+
return ''
35+
else:
36+
# Otherwise, create the JSON representation and return it
37+
return ''.join(['CMD :{',
38+
'"command":"{}"'.format(self.command),
39+
'}'])
40+
41+
def check_data_types(self) -> bool:
42+
"""Check that all parameter types are correct"""
43+
return check_data_type('message', self.command, str)

animatedledstrip/global_vars.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727

2828
ANIMATION_DATA_PREFIX: bytes = bytes('DATA:', 'utf-8')
2929
ANIMATION_INFO_PREFIX: bytes = bytes('AINF:', 'utf-8')
30+
COMMAND_PREFIX: bytes = bytes('CMD :', 'utf-8')
3031
END_ANIMATION_PREFIX: bytes = bytes('END :', 'utf-8')
32+
MESSAGE_PREFIX: bytes = bytes('MSG :', 'utf-8')
3133
SECTION_PREFIX: bytes = bytes('SECT:', 'utf-8')
3234
STRIP_INFO_PREFIX: bytes = bytes('SINF:', 'utf-8')

animatedledstrip/message.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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 .utils import check_data_type
25+
26+
27+
class Message(object):
28+
"""A message from the server"""
29+
30+
def __init__(self):
31+
self.message: str = ''
32+
33+
@classmethod
34+
def from_json(cls, input_str: AnyStr) -> 'Message':
35+
"""Create an AnimationData instance from a JSON representation"""
36+
# Parse the JSON
37+
input_json = json.loads(input_str[5:])
38+
39+
# Create a new Message instance
40+
new_instance = cls()
41+
42+
# Get each property from the JSON, reverting to defaults if it can't be found
43+
new_instance.message = input_json.get('message', new_instance.message)
44+
45+
# Double check that everything has the right data type
46+
new_instance.check_data_types()
47+
48+
return new_instance
49+
50+
def check_data_types(self) -> bool:
51+
"""Check that all parameter types are correct"""
52+
return check_data_type('message', self.message, str)

get_raw.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
from animatedledstrip import AnimationSender
22

33

4-
sender = AnimationSender("10.0.0.55", 6)
4+
sender = AnimationSender("10.44.167.23", 6)
55

66

77
def receive(data: bytes):
88
print(str(data) + '\n')
99

1010

11-
sender.receiveCallback = receive
11+
sender.on_receive_callback = receive
1212

1313
sender.start()
1414

test/animatedledstrip/test_animation_sender.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,14 @@ def test_constructor():
3131
assert sender.connected is False
3232
assert sender.recv_thread is None
3333
assert sender.running_animations == {}
34-
assert sender.stripInfo is None
34+
assert sender.strip_info is None
3535
assert sender.supported_animations == []
36-
assert sender.receiveCallback is None
37-
assert sender.newAnimationDataCallback is None
38-
assert sender.newAnimationInfoCallback is None
39-
assert sender.newEndAnimationCallback is None
40-
assert sender.newSectionCallback is None
41-
assert sender.newStripInfoCallback is None
36+
assert sender.on_receive_callback is None
37+
assert sender.on_new_animation_data_callback is None
38+
assert sender.on_new_animation_info_callback is None
39+
assert sender.on_new_end_animation_callback is None
40+
assert sender.on_new_section_callback is None
41+
assert sender.on_new_strip_info_callback is None
4242

4343

4444
def test_send_animation():
@@ -216,9 +216,9 @@ def test_parse_data_strip_info():
216216

217217
sender.parse_data()
218218

219-
assert sender.stripInfo is not None
219+
assert sender.strip_info is not None
220220

221-
info = sender.stripInfo
221+
info = sender.strip_info
222222

223223
assert info.num_leds == 240
224224
assert info.pin == 12
@@ -236,9 +236,9 @@ def test_parse_data_strip_info():
236236

237237
sender.parse_data()
238238

239-
assert sender.stripInfo is not None
239+
assert sender.strip_info is not None
240240

241-
info2 = sender.stripInfo
241+
info2 = sender.strip_info
242242

243243
assert info2.num_leds == 20
244244
assert info2.pin is None

0 commit comments

Comments
 (0)