Skip to content
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

Closed
wants to merge 3 commits into from

Conversation

magicus
Copy link
Contributor

@magicus magicus commented Jan 2, 2021

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):

    discovered_device = xled.discover.discover()
    ip = discovered_device.ip_address
    hw = discovered_device.hw_address

    control = xled.ControlInterface(ip, hw)
    control.set_mode('rt')

    for i in range(0, 255):
        with io.BytesIO() as output:
            bytes_str = struct.pack(">BBBB", 0, 0, i, 0)
            for position in xrange(210):
                output.write(bytes_str)
            output.seek(0)
            ba = output.read()
            control.send_realtime_frame(210, 4, ba)
            time.sleep(0.1)

@magicus
Copy link
Contributor Author

magicus commented Jan 2, 2021

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..?).

Copy link
Owner

@scrool scrool left a 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?

xled/control.py Outdated Show resolved Hide resolved
@magicus
Copy link
Contributor Author

magicus commented Jan 2, 2021

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.

@magicus
Copy link
Contributor Author

magicus commented Jan 2, 2021

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.

Copy link
Owner

@scrool scrool left a 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):
Copy link
Owner

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:
Copy link
Owner

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')
Copy link
Owner

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
Copy link
Owner

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):
Copy link
Owner

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):
Copy link
Contributor

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):
Copy link
Contributor

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)]
Copy link
Contributor

@rec rec Apr 17, 2021

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.

@Anders-Holst
Copy link
Contributor

Hi @magicus ,
Now the UDP realtime support is in place too! So now this pull request of yours (from which I used your code as a starting point) is obsolete and can be safely closed.
The function is set_rt_frame_socket in ControlInterface.

@magicus
Copy link
Contributor Author

magicus commented Nov 16, 2021

@Anders-Holst Great! Thank you for your hard work.

@magicus magicus closed this Nov 16, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants