-
Notifications
You must be signed in to change notification settings - Fork 47
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for sending realtime UDP frames #67
Conversation
Ideally this should be complemented by a high level function that can extract the number of leds and bytes per led, and perhaps provide some kind of iterator pattern for sending frames. This needs some preparatory work, like better modelling of different led layouts (RGB, RGBW, AWW, more..?). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think of control.py
as application logic that doesn't use network at all. If it needs to send something it uses external module. Originally I got tests on my mind.
I'm thinking of slightly different design - code in the control would prepare the device for sending data and initialize a new object with an interface that accepts data. And separately there would be a code that interacts only with network. Maybe in or alongside of udp_client.py
.
What do you think?
I see your point, but I also think it's easy to overcomplicate things. It's after all just a single method less than a page long. But sure, I can move it to a separate class. The current udp_client.py does something completely different so it'd have to be a new file then. |
I think this actually got better, if not for anything else so for the fact that you only need to setup num_leds and bytes_per_led once. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like separate class better. You have managed to identify some simplifications where an object can store separate state.
On the other hand I believe the API is rather cumbersome to use - we start but never stop (only timeout). Send function wraps packet construction and cannot be tested separately.
Edit: one more note - this is first code that would deal with multiple generations and/or firmware versions. No other code deals with that yet. I think that existing code might need to be gradually refactored to allow various features available in different models. High level interfaces should be able to construct correct class based on the device that they are talking to. I was thinking about different classes with inheritance but I'm not sure how well this would map to existing code.
Please see inline comments and let me know what do you think.
@@ -411,7 +411,7 @@ def set_timer(self, time_on, time_off, time_now=None): | |||
assert all(key in app_response.keys() for key in required_keys) | |||
|
|||
|
|||
class HighControlInterface(ControlInterface): | |||
xclass HighControlInterface(ControlInterface): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
x
here is an typo.
data_size = self.leds_number*self.bytes_per_led | ||
assert len(data) == data_size | ||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: | ||
if data_size < 900 and self.leds_number < 256: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I have found recently (see https://github.com/scrool/xled-docs/pull/13 ) very first packet doesn't depend on the number of the LEDs but rather on the firmware version. I have Generation II device with 250 LEDs that initially used protocol version 2 and only later switched to protocol version three (like bellow).
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: | ||
if data_size < 900 and self.leds_number < 256: | ||
# Send single frame | ||
packet = bytearray(b'\x01') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would like to see construction of the packet in network-independent code. Ideally also together with tests.
With this approach, in a single function, we would unecesarily need to mock networking just to test out building of a packet.
sock.sendto(packet, (self.control.host, REALTIME_UDP_PORT_NUMBER)) | ||
else: | ||
# Send multi frame | ||
packet_size = 900//self.bytes_per_led |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same reasoning as above with protocol version and separation of the packet construction.
self.leds_number = leds_number | ||
self.bytes_per_led = bytes_per_led | ||
|
||
def start_realtime(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure about this interface / API. I understand that it might be useful to have it here.
On the other hand if we have start here we would need a stop here - otherwise we end up with a device that stops getting new data and displays just last frame for couple of seconds until it timeouts.
And how would the stop look?
a) it could be either 'movie', 'effect' or 'playlist' depending on the desirable state which would be decided on a caller
b) or it could store state before realtime start end switch to original one at the end.
In the case of a) one this would just duplicate corresponding methods from control one. Or leak underlying API. In the end - do we need start_realtime()
if the caller could manage to do that on their own?
b) might be interface that is easier for a caller. But in the end it would need much more logic for a class that is indended for RealtimeChannel. So I guess in that case it might be better to have all the logic in higher level interface where one would not call start nor stop and only send data. Maybe even define Context manager (with with
keyword).
|
||
|
||
class HighControlInterface(ControlInterface): | ||
xclass HighControlInterface(ControlInterface): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto!!!
xled/control.py
Outdated
@@ -410,6 +416,36 @@ def set_timer(self, time_on, time_off, time_now=None): | |||
required_keys = [u"code"] | |||
assert all(key in app_response.keys() for key in required_keys) | |||
|
|||
def send_realtime_frame(self, leds_number, bytes_per_led, data): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The performance of this is surprisingly good, but it could be improved even more by not opening a new socket connection each time but having a long-lasting socket connection served by a daemon thread and a Queue.
I don't want to delay this pull request but I could knock this out pretty fast once this is committed.
xled/control.py
Outdated
# Send multi frame | ||
packet_size = 900//bytes_per_led | ||
for i in range(0, math.ceil(data_size/packet_size)): | ||
packet_data = data[:(900//bytes_per_led)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops! The packet data it sends is always the same initial segment. This expression needs to depend on i
.
I can't test this case unfortunately because I have 250 x RGB lights.
Also, there's another issue with this where it does all these bytearray concatenations on every frame. Most of the packet is the same every time it's sent, so an optimization would be to have a fixed sized packet and simply copy in the data.
(Sorry for the single comments one at a time, I'm writing code as I go. :-)
EDIT: OOPS! I was wrong about this - there's surgery on that list, cutting off the initial portions.
Hi @magicus , |
@Anders-Holst Great! Thank you for your hard work. |
Adds "rt" mode to set_mode() and send_realtime_frame() which takes a byte array with a raw frame and sends it using UDP.
This function is tested for the single packet UDP case, but not for the multipacket case. Testing help is needed!
You can use it like this: (Example from my GRBW 210 led Twinkly):