scrool / xled

🎄Unofficial control for Twinkly - Smart Decoration LED lights.
MIT License
206 stars 47 forks source link

What should I put in octet-stream in uploading movie? #81

Closed bigboss97 closed 2 years ago

bigboss97 commented 3 years ago

Finally, I managed to get my script working. Now I can turn my Twinkly on/off and read information via HTTP requests. I want to try uploading movie: https://xled-docs.readthedocs.io/en/latest/rest_api.html#upload-new-movie-to-list-of-movies but have no ideas what's the movie format and what should I put in octet-stream. Has anyone tried that before or does anyone know an information source?

Thanks

Anders-Holst commented 3 years ago

Are you using the python interface or talking directly via http? In the python interface it is a "file like object", or specifically an io.BytesIO object. You can check the functions write_static_movie and set_static_color in control.py how such a stream can be created. Then it is uploaded with set_led_movie_full. Do you have the led profile RGB or RGBW? The RGB the stream consists of thee bytes per led and frame (the red, green, and blue strength each between 0 and 255). If you have RGBW there are four bytes ber led and the White byte comes first.

Anders

bigboss97 commented 3 years ago

Are you using the python interface or talking directly via http?

http directly (using python script)

In the python interface it is a "file like object", or specifically an io.BytesIO object. You can check the functions write_static_movie and set_static_color in control.py how such a stream can be created. Then it is uploaded with set_led_movie_full.

That's very helpful. I'll dig into that.

Do you have the led profile RGB or RGBW? The RGB the stream consists of thee bytes per led and frame (the red, green, and blue strength each between 0 and 255)

"led_profile":"RGB"

Thank you for the quick response

Phuoc

bigboss97 commented 3 years ago

Now I can use HTTP request "led/movie/full" to upload binary data from file and can control some LEDs. Thanks for your help. I've got some questions: 1) Does the upload mean writing to the same section of memory every time I run my script or it's adding more and more data and I will run out of memory after many tests? 2) I've mapped my LEDs to a rectangle. How does the 2D coordinate map to the sequential binary data?

Anders-Holst commented 3 years ago

1: The old movie is overwritten every time, with the led/movie/full call. When creating playlists I suppose the idea is that several movies can co-exist, but I have not tried it yet. The current control.py do not support movie lists, but I plan to make a stab at it, eventually (after I've done nr 2 below).

2: Layout mapping is not supported either by the current control.py. I am working on it now though. If you have placed the lights fairly regularly by hand, you might be able to write a simple function to map between the led index on the string and the led's x,y-coordinates. Or else (assuming you mapped the lights with the phone) you can try to get via HTTP "led/layout/full", and there you will find the coordinates for each led in order.

bigboss97 commented 3 years ago

I'm using "led/movie/full" http request to send RGB information via "Content-Type"="application/octet-stream". I post the data via a file of 1200 bytes (for 400 LEDs). The RGB colors come through. But somehow there are ~50 LEDs at one end untouched when I post the same color to all LED's. Do I have to offset some bytes (~50) in my octet-stream?

Anders-Holst commented 3 years ago

Strange. There should be no offset in the stream.

I myself have only 250 leds. I know that for real-time frames there is a difference in protocol when you have more than 300 leds, but was under the impression that the movie protocol is the same regardless of number of leds.

Just to make sure, if you create a bytes-stream in python (io.BytesIO), remember to do seek(0) before uploading, so you get all of the stream from the beginning. You can also check what happens if you upload a movie of several frames. Change the color slightly between frames, so you see if the frames go into each other (i.e the last few leds belonging to the next frame). I understand that you have written your python script yourself, instead of using control.py. But what happens if you use control.py, specifically the set_static_color call?

bigboss97 commented 3 years ago

I understand that you have written your python script yourself, instead of using control.py. But what happens if you use control.py, specifically the set_static_color call?

I started from here. But I couldn't get it work. It simply didn't want to connect. Then I found a very simple Python script example using HTTP request to turn the lights on/off. I got that working. Then I came back to here to get to understand the other operations. Meanwhile, I've converted my script to Processing:

  void showFrame() {
    PostRequest postMovieFull= new PostRequest( urlBase+"led/movie/full");
    postMovieFull.addHeader("X-Auth-Token", authToken);  
    postMovieFull.addHeader("Content-Type","application/octet-stream");  
    postMovieFull.addFile("movie","/work/Processing/Twinkly/movie.dat");
    postMovieFull.send();    

    PostRequest postMovieCfg= new PostRequest( urlBase+"led/movie/config");
    postMovieCfg.addHeader("X-Auth-Token", authToken);  
    postMovieCfg.addHeader("Content-Type","application/json");  
    postMovieCfg.addData("{\"frame_delay\":1}");  
    postMovieCfg.addData("{\"frames_number\":1}");  
    postMovieCfg.addData("{\"leds_number\":"+ ledNum+"}");  
    postMovieCfg.send();    
  }

movie.dat is a file of 1200 bytes. When I tried single color: 20210923_090814 When I tried pixel art it looks like CRT TV out of sync 😄

bigboss97 commented 3 years ago

There's one more weird thing happening... My script basically turns on the light (set mode=movie), uploads data and runs movie config. If I runs the script again and again it circles through RBG colors 😄 Obviously, there's one byte shifted each time.

Anders-Holst commented 3 years ago

From the way the two strings enter from the left, I see that the "noise" in the pattern is at the beginning, not at the end. So there are some data sent at the start of the application/octet-stream that should not be there. If you instead of making the postMovieFull.send could print out what exactly it is going to send, you might see what it is. Ie, is it in the movie.dat file, or garbage added by the call. And repeat it and see where the extra byte comes from - the printout from both calls should of course be identical when it is correct. Anyhow, it is likely a problem with your implementation.

But I am more interested when you say that when trying the xled code, it "simply didn't connect". Why? What was the problem? Was it the discovery, or creating the control interface, or authentication? It might be good to know to be able to fix the xled package. (And I eventually hoped to ask you to test one of my pieces of code, which I can not test myself on >300 leds. Byt then the basis has to start first of course.)

/ Anders

bigboss97 commented 3 years ago

So there are some data sent at the start of the application/octet-stream that should not be there.

You're right. I must be some kind of HTTP header stuff.

But I am more interested when you say that when trying the xled code, it "simply didn't connect". Why?

I followed your instructions to setup the environment and I tried the on/off command of the example. The command simply didn't return, no time out, no error messages. I had no problem to ping the IP. I tried to look at the codes. I had no ideas where it got stuck :-)

I'm using miniConda on Windows 10.

Anders-Holst commented 3 years ago

I see. My best guess then is that it is the discovery that fails when you try to use xled. I saw that there seem to be some issues with it, but I have not looked at that part of the code myself.

Fortunately, you only need the discovery to find out the IP-adress of your device. So if you already know the IP you can create the ControlInterface with it directly, and skip the discovery step (until it is fixed at least).

Partially off this topic, but partially in: Would you like to help me check some code on your 400 led device? In-topic, because we will then see if the python code in the xled package will work for you (and without the artifact in the beginning that you experience with the processing code). Off-topic, because what I struggle with is quite something else: to understand the real-time mode interface. There are 4 different protocols depending on communication protocol, family and firmware. I have put together all four of them, and want to see how they behave on your device. Especially the last version, which behaves special when there are more than 300 leds (and I have only 250, so can't check it myself).

If so, here is the code:

import sys
import time
import base64
import socket
import io
import struct
from requests.compat import urljoin

from xled.control import ControlInterface

REALTIME_UDP_PORT_NUMBER = 7777

def make_solid_movie(ctr, r, g, b):
    num_leds = ctr.get_device_info()['number_of_led']
    pat = [struct.pack(">BBB", r, g, b)] * num_leds
    movie = io.BytesIO()
    movie.write(b''.join(pat))
    movie.seek(0)
    return movie

def set_mode_rt(ctr):
    json_payload = {"mode": "rt"}
    url = urljoin(ctr.base_url, "led/mode")
    response = ctr.session.post(url, json=json_payload)
    return response

def set_rt_frame(ctr, frame):
    """
    Uploads a frame in rt-mode
    """
    print("REST version")
    url = urljoin(ctr.base_url, "led/rt/frame")
    head = {"Content-Type": "application/octet-stream"}
    response = ctr.session.post(url, headers=head, data=frame)
    return response

def set_rt_frame_socket(ctr, frame, version, leds_number=255):
    """
    Uploads a frame in rt-mode, over an UDP socket
    """
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    if version == 1:
        # Send single frame
        print("Version 1 RT")
        packet = bytearray(b'\x01')
        packet.extend(base64.b64decode(ctr.session.access_token))
        packet.extend(struct.pack(">B", leds_number))
        packet.extend(frame.read())
        sock.sendto(packet, (ctr.host, REALTIME_UDP_PORT_NUMBER))
    elif version == 2:
        # Send single frame
        print("Version 2 RT")
        packet = bytearray(b'\x02')
        packet.extend(base64.b64decode(ctr.session.access_token))
        packet.extend(b'\x00')
        packet.extend(frame.read())
        sock.sendto(packet, (ctr.host, REALTIME_UDP_PORT_NUMBER))
    else:
        # Send multi frame
        print("Version 3 RT")
        packet_size = 900
        data_packet = frame.read(packet_size)
        i = 0
        while data_packet:
            packet = bytearray(b'\x03')
            packet.extend(base64.b64decode(ctr.session.access_token))
            packet.extend(b'\x00\x00')
            packet.extend(struct.pack(">B", i))
            packet.extend(data_packet)
            sock.sendto(packet, (ctr.host, REALTIME_UDP_PORT_NUMBER))
            data_packet = frame.read(packet_size)
            i += 1

host = sys.argv[1]
print("Host:", host)

ctr = ControlInterface(host)

set_mode_rt(ctr)

print("Green")
set_rt_frame(ctr, make_solid_movie(ctr, 0, 255, 0))
time.sleep(5)

print("Warm yellow")
set_rt_frame_socket(ctr, make_solid_movie(ctr, 230, 170, 0), 1)
time.sleep(5)

print("Lime")
set_rt_frame_socket(ctr, make_solid_movie(ctr, 100, 255, 0), 2)
time.sleep(5)

print("Orange")
set_rt_frame_socket(ctr, make_solid_movie(ctr, 230, 85, 0), 3)
time.sleep(5)

If you put it in a file, for example "rttest.py", then you should be able to run it from the command line with: python -m rttest your-twinkly-ip-here It is supposed to first make your leds first all green, then yellow, then lime (i.e. green-yellow) and finally orange. Does all these colors appear? Especially the last one, does it cover all leds or are some still the previous color?

Best Regards Anders

bigboss97 commented 3 years ago

It is supposed to first make your leds first all green, then yellow, then lime (i.e. green-yellow) and finally orange. Does all these colors appear? Especially the last one, does it cover all leds or are some still the previous color?

I ran the script in my miniConda venv and it works perfectly, well done! There are only few dead "pixels". Some LEDs just can't decide their color. They are different each time depending on the switching color. After the script has terminated for ~1min, the lights switch back to their initial colors.

bigboss97 commented 3 years ago

What does ">BBB" actually mean?


pat = [struct.pack(">BBB", r, g, b)] * num_leds
Anders-Holst commented 3 years ago

Great! So we know at least that it is possible to get the xled code to work for you, and that the problem was in the discovery. (The command line interface should be fixed of course, to avoid the discovery phase when the IP-address is already provided.)

What does ">BBB" actually mean?

Like so often I happily steal code from others without always understanding the details. I understand that struct.pack is one way that works the same on python-2 and python-3 to pack integers in the range 0 - 255 into a bytes string. Now looking it up, ">BBB" is the format, where B stands for byte and > specifies that the "standard format" should be used (in case some obscure architecture wants would have something else than 8 bits in a byte I suppose).

There are only few dead "pixels"

Now, that is the interesting part. Exactly which of the four variants experienced dead pixels, and where were they located? Supposedly the first or the last led, and in case of the last test possibly somewhere around led no 300 (ie halfway into the second string). Did all colors come out right? As you experienced yourself, one byte offset would switch the colors, so I avoided the blue pixel on purpose to easier see this: Were there no blue, green-blue, or purple hues? And just to make sure, which firmware version do you have? Hint: print(ctr.firmware_version()['version']) I.e, is it the original firmware or have you updated it since you bought them?

Thanks for the help! Anders

bigboss97 commented 3 years ago

Now, that is the interesting part. Exactly which of the four variants experienced dead pixels, and where were they located?

They are in the middle. Don't worry about them. I've seen them in using Twinkly app. It seems certain LED has difficulty in displaying certain color/value. When color is changed the dead pixels move.

Did all colors come out right? As you experienced yourself, one byte offset would switch the colors,

Yes, even the "dead" pixels appear to try to get to the right color. It's not really dead. It's in an unstable state. The offset issue which I'm having... I found it in the codes of Progressing library. The random colors seem to be the URI hahaha

And just to make sure, which firmware version do you have? Hint: print(ctr.firmware_version()['version']) I.e, is it the original firmware or have you updated it since you bought them?

I bought them last Xmas and have never updated. Do you know a link for updating firmware? I'll find out the version.

Thanks for the help!

No worries

bigboss97 commented 3 years ago

Off topic: Do you know any good tool which can convert Python to Android app (apk)?

bigboss97 commented 3 years ago

How does the "movie" with multiple frames work? Do you have an example?

Anders-Holst commented 3 years ago

Must be annoying with those unstable leds of yours! One of my two lights have one led which never gets black but always has a dim red light on. With bright colors you don't notice, but dim colors have this extra red on them. I noticed it too late after buying so probably too late to complain now. I plan to put some black sticky tape on it. Unfortunate for you to have several different leds that flicker. They seem to have some quality issues in the production...

However, great then: All four protocol versions appear to work on your hardware, just as they do on mine. Then I can submit this code for inclusion in xled.

Do you know any good tool which can convert Python to Android app (apk)?

No, sorry. I too would like such a tool. I have no experience of creating apps myself. A full fledged python interpreter on the phone would be nice.

How does the "movie" with multiple frames work? Do you have an example?

Its easy, you just concatenate a number of frames in the same BytesIO stream, and send it in with "/led/movie/full" as discussed before, providing the right number of frames with "/led/movie/config". There are several examples of how to do that in the version of control.py which is included in my pull request "Thorough enhancement..." Which I am now going to enhance further with this real-time functionality.

bigboss97 commented 3 years ago

Must be annoying with those unstable leds of yours!

Not sure whether it's a hardware issue. But fact is, certain LEDs only have trouble with certain colors.

However, great then: All four protocol versions appear to work on your hardware, just as they do on mine. Then I can submit this code for inclusion in xled.

Great job!

I too would like such a tool. I have no experience of creating apps myself. A full fledged python interpreter on the phone would be nice.

That was the main reason that I'm trying to port Python to Processing (not a Java fan though) because you can export it to apk easily. But I still haven't been able to fix the unwanted data in the octet-stream. Are you familiar with Java? Processing is basically java.

The good thing of Processing, you don't need complicate Android simulator on PC. You can even write simple script directly on your phone.

Anders-Holst commented 3 years ago

No, Java is not my language. (I'm originally a C++ guy, who really hate python, its such an unstructured mishmash of everything, and so much bugs in various packages - but everyone uses python nowadays so all collaborative software developments I do at my work is python...)

Just googled, so surely you already know about it: but what about "kivy"?

bigboss97 commented 3 years ago

yes, but only since yesterday when I started googling it 😄 There are so many kivy related apps on Play Store. No ideas how to begin.

I'm originally a BASIC (C64) man/boy. Recently I even managed to use RFO-BASIC to control LEDs via bluetooth. RFO is also very nice and very easy to export apk. It can also handle HTTP stuff.

scrool commented 2 years ago

I see that conversation here is stale and from what I have seen on Facebook, @bigboss97 managed to create an Android app :) . I'm going to close this one.