jasonacox / tinytuya

Python API for Tuya WiFi smart devices using a direct local area network (LAN) connection or the cloud (TuyaCloud API).
MIT License
868 stars 157 forks source link

Fast scrubbing through colours #432

Open lmcd opened 6 months ago

lmcd commented 6 months ago

I've built a similar GUI application to the Tuya Smart Life app with a hue colour wheel to quickly scrub through values.

Setting a single colour via tinytuya transitions immediately on the bulb, but when I scrub through many values quickly, it starts to lag and doesn't transition gracefully. The Smart Life app on the other hand can handle super-fast scrubbing, and the transitions are very smooth. Is the Smart Life app doing something different to achieve a more graceful effect?

In my code I'm opening a persistent connection, then sending set_colour for each colour change.

lmcd commented 6 months ago

Ok I've managed to capture packets from the iOS app, and noticed that when scrubbing through colours with your finger held down on the wheel, it uses DP ID 28 - which I suspect is a more efficient path for sending lots of values in quick succession.

When you just tap a single colour, it instead sets the mode with DP 21 then then colour with DP 24

lmcd commented 6 months ago

Ok, can confirm DP 28 looks way better visually when quickly moving between colours. The interpolation/transition is different and looks visually a lot nicer.

With DP 28, the device doesn't send any response or acknowledgement. But there still seems to be a build of packets that are getting delayed in their delivery to the device. Is this something to do with the way TCP/IP works in Python? Is there some kind of bottleneck in the library?

I'm also throttling colour changes to at most 1 every 50ms, so as to not overload the device. This doesn't help with the delay.

lmcd commented 6 months ago

Ok - it seems that the Smart Life device is throttling every 150ms. This now seems to create a smooth result.

Here is the payload structure for DP 28:

Example: 1002803e803e800000000 Structure: THHHHSSSSVVVVWWWWBBBB

T = Transition (1 = Fade, 0 = Instant) H = Hue (0-1000) S = Saturation (0-1000) V = Value (0-1000) W = Warmth (0-1000) B = Brightness (0-1000)

I'll submit a PR when I have time.

WWWW and BBBB are all zero if controlling the white/warmth level. HHHH, SSSS, VVVV are all zero if controlling the colour.

Also, values set through DP 28 are not persisted to memory and aren't restored when the bulb is switched off and on again. To have a colour value 'saved', and broadcast out to the Smart Life app, DP 24 must be used.

28 in the readme is listed as 'Debugger'. This is not true at all, and is used often in the Smart Life app as a way of interactively controlling the light.

jasonacox commented 6 months ago

Hi @lmcd - thanks for this!

Can you post an example of your code?

Have you tried using the nowait=True settings on the d.set_colour() call?

import tinytuya

# Connect to Tuya BulbDevice
d = tinytuya.BulbDevice(DEVICEID, DEVICEIP, DEVICEKEY)
d.set_version(float(DEVICEVERS))
# Keep socket connection open between commands
d.set_socketPersistent(True)  

# Flip through colors of rainbow - set_colour(r, g, b):
print('\nColor Test - Cycle through rainbow')
rainbow = {"red": [255, 0, 0], "orange": [255, 127, 0], "yellow": [255, 200, 0],
           "green": [0, 255, 0], "blue": [0, 0, 255], "indigo": [46, 43, 95],
           "violet": [139, 0, 255]}
for x in range(10):
    for i in rainbow:
        r = rainbow[i][0]
        g = rainbow[i][1]
        b = rainbow[i][2]
        print('    %s (%d,%d,%d)' % (i, r, g, b))
        d.set_colour(r, g, b, nowait=True)
        time.sleep(0.05) # 50 ms
    print('')
lmcd commented 6 months ago

Yes I was already using that. All the lag has now been resolved, and my app is now doing exactly what the Smart Life app is doing:

With these changes I'm getting identical behaviour to that of the Smart Life iOS experience.

Here's roughly what the code is on the tinytuya side looks like. I'll clean it up as a PR soon:

def set_colour_fast(self, r, g, b):
        """
        Set colour of an rgb bulb.

        Args:
            r(int): Value for the colour Red as int from 0-255.
            g(int): Value for the colour Green as int from 0-255.
            b(int): Value for the colour Blue as int from 0-255.
        """
        if not self.has_colour:
            log.debug("set_colour: Device does not appear to support color.")
            # return error_json(ERR_FUNCTION, "set_colour: Device does not support color.")
        if not 0 <= r <= 255:
            return error_json(
                ERR_RANGE,
                "set_colour: The value for red needs to be between 0 and 255.",
            )
        if not 0 <= g <= 255:
            return error_json(
                ERR_RANGE,
                "set_colour: The value for green needs to be between 0 and 255.",
            )
        if not 0 <= b <= 255:
            return error_json(
                ERR_RANGE,
                "set_colour: The value for blue needs to be between 0 and 255.",
            )

        hexvalue = "1" + BulbDevice._rgb_to_hexvalue(r, g, b, self.bulb_type) + "00000000"

        payload = self.generate_payload(
            CONTROL,
            {
                self.DPS_INDEX_CONTROL[self.bulb_type]: hexvalue,
            },
        )
        self._send_receive_quick(payload, None)

I'm actually interacting with Python via Swift as I'm building a user interface for the Mac.

lmcd commented 6 months ago

Also, I figured this out by doing a packet capture on an actual iOS device whilst playing with the Smart Life app. You can do this on a physically tethered iPhone over USB using the instructions here: https://developer.apple.com/documentation/network/recording_a_packet_trace

Then open the pcap in Wireshark and resave again as pcap. Then you can parse the file with pcap_parse.py

jasonacox commented 6 months ago

This is brilliant @lmcd !! Great research!

Yes, please submit a PR. 🙏