codetheweb / tuyapi

🌧 An easy-to-use API for devices that use Tuya's cloud services. Documentation: https://codetheweb.github.io/tuyapi.
MIT License
2.1k stars 342 forks source link

Registering a device #22

Closed Ericmas001 closed 6 years ago

Ericmas001 commented 6 years ago

Some discussion on #5 are about figuring out the registration process, I'm starting a thread here so we can focus on this particular issue.

I recorded with fiddler on my old phone the action of adding a new device to the SmartLife App. I also recorded with WireShark all the UDP packets sent by either the device I was registering or the phone I was using to register the device.

I will try to make something readable of all of this and post all the info here, i will just need a little time.

My conclusion from a quick glance a the data:

So my guess is:

That's how i imagine the registration works.

codetheweb commented 6 years ago

From my own packet sniffing I can confirm this is correct.

Here's an idea: what if we're able to send a custom server address in the configuration step?

Ericmas001 commented 6 years ago

That was my original Idea. I had plans to try: Different WIFI ssid, different WIFI password, different pairing app (There is at leat 3 or 4 that we can try. With those tests I think we would be available to determine what is what.

Ericmas001 commented 6 years ago

During registration, device makes

Ericmas001 commented 6 years ago

I've registered 3 times the same device, to the same network, and I compared the UDP packet. I did not make any conclusion yet, only some highlighting. Maybe someone can make some sense with this πŸ˜ƒ http://htmlpreview.github.io/?raw.githubusercontent.com/Ericmas001/Tuya-Api-Tools/master/SmartLife.htm

Ericmas001 commented 6 years ago

Ok I've played a little with the APK. The apps all uses with JNI a C++ .so library to Create and Send UDP packets. jniLibs.zip 2 methods are exposed to JNI

package com.tuya.smart.android.device;

public class TuyaSmartLink
{
  static
  {
    try
    {
      System.loadLibrary("TuyaSmartLink");
      return;
    }
    catch (Throwable localThrowable)
    {
      localThrowable.printStackTrace();
    }
  }

  public static native void sendStatusStop();

  public static native int smartLink(String paramString1, String paramString2, String paramString3, int paramInt1, int paramInt2, int paramInt3, int paramInt4, int paramInt5);
}

Just remember that some info are obtained by the App at the beginning of the device registration process, the app says "I want to register a device", and the server sends back:

"secret": "0NiE",
"token": "QyMLEFzw"

The App also asks for the WifiPassword of the Wifi network that the device is connected to. This password, and the name of the wifi network, will be also used in the process.

Finally, the App determine the current region. There seems to be 3 regions, EU = Europe, AZ = America and AY = Asia. The complete code about choosing the region is there: TyCommonUtil.zip

So for example, values could be:

"region": "AZ",
"wifiName": "Bikini.Bottom.Wifi.2.4",
"wifiPassword": "5p0ng3B0b"

The call to smartlink() is made like this

constructedToken = region + token + secret // AZQyMLEFzw0NiE
TuyaSmartLink.smartLink(wifiName, wifiPassword, constructedToken, 5, 2, 1000, 1, 1);

From there, I know that the TuyaSmartLink DLL will send a Header and a Message. The header is the first batch of udp packets (+1, +3, +6, +10), and the Message is the second Batch.

The Message will be encoded (Not quite figured out exactly the encoding fct, decompiled C++ DLL is not fun to read, not sure I want to go that far). It seems like the Message is an encoded byteArray that is constructed like this:

  // ABB...BBCDD...DDEE...EE
  // A: len of password
  // B: password
  // C: len of token
  // D: token
  // E: network name

So that's what I discovered so far πŸ˜„

Ericmas001 commented 6 years ago

Oh and I do have some kind of conclusion knowing all of this: the url of the API is not sent to the device. Meaning, the device cannot register differently if it comes from SmartLife, Tyua, Jinvoo or eFamilyCloud.

So the Apps are clearly all the same, since the device is clearly gonna call the same API with no distinction depending witch App you are using. So my theory is when you register into one of those app, a user is created in the Global Database, but this user is associated with the app that you use. When you register a device, you are given a token/secret associated to your user, and by association to the App you are using. The device will be added to the Global Database, but linked to your user, so also by association linked to the App you are using. That's why user and device don't crossover between apps.

That means @codetheweb that we cannot send a custom server address to the device 😞

The only thing I don't get is the need to have different APIs for the different Apps. They should call the same Tuya API with different clientId, and it should be enough.

clach04 commented 6 years ago

@Ericmas001 dumb question, does spoofing the API address work? E.g. setup DNS server on your network to point to your server?

Ericmas001 commented 6 years ago

@clach04 I did not try. I guess the device is calling the API via https and this will fail because of the certificate, but I may be wrong

BillSobel commented 6 years ago

@Ericmas001 Did you happen to get any farther on the registration packets? Looking at the header files for the wifi sdk I think you are seeing different registration steps as defined here:

typedef enum { SMARTLINK_STEP_HEAD = 0, SMARTLINK_STEP_MAGIC, SMARTLINK_STEP_PREFIX, SMARTLINK_STEP_DATA, SMARTLINK_STEP_FINISH, SMARTLINK_STEP_END, SMARTLINK_STEP_TLINK, } TYPE_SMARTLINK_STEP;

BillSobel commented 6 years ago

Notes on AP registration steps:

Packet is UDP broadcast packet sent on 6669

int prefix 0x000055aa int version 0x00000000 int command 0x00000001 (AP register?) int size (json data token, passed, ssid) json data (no null terminator) crc32 (calculated from packet start to packetsize-8 [e.g. to end of json data]) int suffix 0x0000aa55

Device accepts packet and if wifi credentials are ok will connect to that new network. I believe at that time it will validate the token, but I am off to confirm. I wanted to simply leave some notes for others that might be following this down.

BillSobel commented 6 years ago

Some more notes, registration (for AP mode to start) is fairly straight forward:

Need cloud access keys from the developer portal (took me a few days to get approved) Create a device token (tuya.m.device.token.create) Turn on access point mode on device (LED flashes slowly) Connect to device access point Send UDP packet defined above Connect to normal access point If the normal access point has internet access, the device will connect and register itself with the cloud. The calls to get a new UID and to activate a device are called by devices (apparently) and NOT the gui manager (so you can ignore these calls)

Call list token until the device you registered is returned as one of the items (or you hit a timeout you define)

That result will give you the deviceId. You can call tuya.m.device.get with that deviceId to get the local encryption key.

Also once connected to the normal wifi the device will beacon every few seconds on UDP 6666. This packet contains the deviceId (but not the local key).

Thats it, device is now controllable.

tjfontaine commented 6 years ago

coincidentally I came to similar conclusions regarding the message layout, though not all of it entirely aligns with yours https://github.com/tjfontaine/tuyapi/commit/7f98bf05cff31fb704786a742a7d7f03ba21ffca

BillSobel commented 6 years ago

@tjfontaine I have to go review, I think what I marked as 'version' may actually be the packet id (other people saw this increment by 1 or a few numbers on each new packet). Was there other differences? If so can you mention what you saw so we can compare?

sebkajeka commented 6 years ago

Would it be possible to use a custom app to register a device? This app could easily capture and display required information to use the API.

codetheweb commented 6 years ago

@sebkajeka yes, I'd like to. The initial version would be a command line program, though, not an app for your phone.

zuik commented 6 years ago

@sebkajeka It seem to require the app key and app secret according to the Tuya API. However, I haven't been successful in reproducing their example

sebkajeka commented 6 years ago

Simplifying the device registration/localKey process is important. Am I correct in thinking it's possible to collect this information via API provided the device is registered with the Tuya App and we have our own app API credentials?

codetheweb commented 6 years ago

@sebkajeka no, devices are tied up to the keys they were registered with. If you want to access the local key you need to register it yourself with your own API keys.

BillSobel commented 6 years ago

@sebkajeka As CodeTheWeb said, each developer set of keys is it's own walled garden in their backend. So my@email.com with keyset1 can not see devices from the same my@email.com with keyset2. So once you have a keyset all apps would have to use it (which would preclude using the smart life app) When you register keys they will build you custom copies of those apps, it runs about $1600 all said and done (so I have not done that yet with them, not sure if I will bother as most people using devices via HomeSeer will be using ImperialHome or some other dashboard apps which should alleviate the need for their app)

@zuik I've got registration working, what part are you stuck at?

zuik commented 6 years ago

@BillSobel I tried following their example app at: https://github.com/TuyaInc/tuyasmart_android_sdk but it doesn't seem to compile properly in Android Studio. I don't know any Chinese, so navigating their website is a bit ... Did you register as a developer to get the app key and app secret to put in your TuyaSDK.init() method?

BillSobel commented 6 years ago

@zuik Aww, sorry didn't realize you were using the SDK. I wrote the code to do registration completely separate from the SDK (get the token, send the udp packet, wait for device to register itself). Im in c# writing a HomeSeer plugin, so couldn't use any of the SDK. The said, I did dig into (wrote some xposed hooks to look at some of their process). I did register with them and get official keys from the, took about a week. Also on the site, change the /cn/ in the URL to /en/ many of the pages are translated...

zuik commented 6 years ago

@BillSobel I see. I was reading through the decompiled java code and the biggest part that I was stuck on is how they do the "Fast Connect Mode" (The other one not the AP mode). In this thread, we know that they send out a bunch of UDP broadcast packets but most of these packets are zeros.

I was wondering how they encode these into UDP packets? Did you use the AP mode or the "Fast Connect Mode"? How did you encode the WiFi credential into UDP packets?

BillSobel commented 6 years ago

@zuik I have not understood 'Fast Connect Mode' yet. When a new device comes into an environment to be registered it needs the ssid and the password. I have not been able to understand how the fast connect mode works given that. Ive considered they may be using bluetooth, I know iOS has a special wifi setup mode that seems to open a second ssid concurrent with the primary one (assuming Android has something similar), that fast connect mode only works for a network the device had been on prior (so if you reset it, it still attempts to connect to the last network), that the device attempts to connect to the strongest open radio it sees, and lastly that the devices have an internal mesh so once one is up, the mesh can send the data to others. So far I have not found anything conclusive on this so have only implemented the AP registration mode. That mode is actually VERY simple to write (albeit it requires keys).

You do a cloud token request which gives you a token and a secret. You setup a packet (just like a command packet, same structure) with the token field out as a string of the region+token+secret, the passwd field (wifi password) and the ssid field (ssid name). You then broadcast that on port 6669 (not the same port as TCP). The device does not respond. If simply validates the packet (so the CRC needs to be correct) and extracts the data and connects to the AP. If it connects and has open internet access it registers with the cloud. You then poll the cloud on the token you gave the device and get back a list of devices which have registered with it (the token is good for 10 minutes so you can reuse it during that time, albeit probably easier to just get a new one for each registration you are doing, since this is AP mode you can only do one device at a time).

While we would all prefer this was 100% local, once that is in place you really don't need to talk to the cloud again, you have the local key and can control the device easily.

sebkajeka commented 6 years ago

Afaik, Fast Connect, EZ setup, SimpleConfig etc is just a variation of "SmartConfig" which is available on ESP8266. There's a few names for it but effectively what it does is encode the data in the length of a UDP packet. This way non-connected devices can listen to all nearby traffic for the appropriate change in packet lengths and start listening for configuration data. http://www.stijnvandrunen.nl/2015/06/esp8266-mqtt-smartconfig/

How do devices like Alexa communicate with this walled garden keyset mechanism? My Tuya plug states Alexa compatibility. I don't have an echo device so I can't see what's going on with this.

I'm really interested in getting a simplified device registration process up and running. The ability to control generic plugs via Octoprint for 3D printing would be incredible useful :)

Ericmas001 commented 6 years ago

That's also the conclusion I had that the packet length was the data itself. I had decompiled the dll used by the app to try to understand how it was encoded, but reading decompiled c++ code is... Γ€ different kind of fun... :) your link is really helpful :)

On Wed, May 2, 2018, 5:59 AM Seb Horsewell, notifications@github.com wrote:

Afaik, Fast Connect, EZ setup, SimpleConfig etc is just a variation of "SmartConfig" which is available on ESP8266. There's a few names for it but effectively what it does is encode the data in the length of a UDP packet. This way non-connected devices can listen to all nearby traffic for the appropriate change in packet lengths and start listening for configuration data. http://www.stijnvandrunen.nl/2015/06/esp8266-mqtt-smartconfig/

β€” You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/codetheweb/tuyapi/issues/22#issuecomment-385926742, or mute the thread https://github.com/notifications/unsubscribe-auth/AK6vF7w3dQ6tQzwNJ4scjdrq9rKM-1pOks5tuYN8gaJpZM4RwkTC .

BillSobel commented 6 years ago

@sebkajeka Thank you. Packet length is something that never occurred to me. I implemented a version of the sender from the project linked above. Just need to determine the length encoding algorithm (hoping it is the same) and the data packet structure. I tried a test using the same data as goes with an AP registration (json formatted) and no luck.

If anyone has pointers on the data format or data to length algorithm please let me know, looks like Wireshark is going to be my friend for a few days!

BillSobel commented 6 years ago

@sebkajeka @Ericmas001 I have figured out most of the data sent during the SmartLink process.

The start is just a loop of packets of lengths 1, 3, 6, and 10.

As discussed the data is encoded into the length. It appears there is an unknown 4 packet header (I am trying to figure out what these 4 lengths represent) followed by a sequence of 6 packets (this sequence repeats the same basic structure until the end of the data)

In the sequence of 6 packets, first packet is crc8 of the sequence number (next packet) and 4 data packets. The crc is done before the data is or'd to rebase it at 128 or 256. The crc and the sequence number are modded by 128 and then based at 128 (finalcrc = (crc % 128) | 128.

the data packets are simply the data or'd with 256 (so character 45 becomes 301). Data is padded with 0 (which becomes 256) if the last group of packets is short (last group would be short 1-3 packets if the data length is not evenly divisible by 4).

The data itself is a length byte, the password bytes, length byte, token string (need to verify but suspect it is the region + token + secret string same as in other use cases), length byte, ssid name bytes.

So, hopefully just those header bytes to figure out, and this might actually work!

codetheweb commented 6 years ago

@BillSobel from what you've seen so far, do you think it would be possible to generate a random local key and just send that to the device? Or does it need a key registered with Tuya's API to work properly?

BillSobel commented 6 years ago

@codetheweb Definitely requires the cloud to configure the key. You send the device a token the cloud generates which you then both use to 'find' each other, no options to send it a key or other material to use. Once configured you can run fully locally, but until then no....

BillSobel commented 6 years ago

An update. The header is just the length of all the strings (+2) encoded 4 different ways. The CRC8 calculation doesn't match a normal CRC8 (or any of the known ones I can find documented). But the code from the SmartLink android project mentioned above does implement the same CRC8 and works for this.

// // Fill in first 4 bytes // of header here // var stringLength = (wifiNameBytes.Length + regionTokenSecretBytes.Length + wifiNameBytes.Length + 2) % 256; ; var stringLengthBuffer = new byte[1]; stringLengthBuffer[0] = (byte)(stringLength); var stringLengthCrc = CalculateTuyaCrc8(stringLengthBuffer, 1);

        // Length encoded into the first two bytes based at 16 and then 32
        encodedData[0] = (stringLength / 16) | 16;
        encodedData[1] = (stringLength % 16) | 32;
        // Length CRC encoded into the next two bytes based at 46 and 64
        encodedData[2] = (stringLengthCrc / 16) | 48;
        encodedData[3] = (stringLengthCrc % 16) | 64;
BillSobel commented 6 years ago

It works! I got an outlet device to register to the cloud by doing the SmartLink protocol to send the config data over. So now we know we can do both Smartlink and AP registration modes (albeit both need the cloud involvement)

sebkajeka commented 6 years ago

Well done! So to confirm, we need a Cloud API key from developer.tuya. With the cloud API we can now register devices and control them via the same API?

Are you publishing this code somewhere @BillSobel?

BillSobel commented 6 years ago

@sebkajeka The actual code is part of a closed source project, so I cant post it all, but can definitely share the code around registration. I wouldn't have gotten here without this group, and more than happy to ensure others can do the same thing.

codetheweb commented 6 years ago

@BillSobel could you please update this list with corrections? I had a hard time understanding some of what you said.

Beginning Pattern (loops for 50+ iterations)

  1. Packet of length 1
  2. Packet of length 3
  3. Packet of length 6
  4. Packet of length 10

Data Pattern (loops until all data has been transmitted)

BillSobel commented 6 years ago

@codetheweb The initial loop is 144 times (albeit, not clear that is required). Also those packets are sent without delay between them but them a variable 33-41ms delay between groups. I got that from the code above, have not confirmed it is fully required. Thats it for the beginning pattern.

On the data pattern:

The token string is the region, the token, and the secret concatted into one 14 character string. The token and secret come from a cloud request.
The length of SSID byte is NOT there. I thought it was (silly its not) but they actually put in the password length (as you stated) and the token length, but then apparently deprive the ssid length from the header and subtracting these two values.

So PwLengthPasswordBytesTokenLenTokenBytesSSDIBytes

The sequence number is just a counter starting at 0 and incremented for each group of 5 packets (each group consists of the CRC packet, this sequence packet, and 4 data packets)

CRC and the sequence number are %128 | 128 (so range 128-255)..

So 4 header packets sent (length determined by header[x] value above) Then groups of these 5 packets: UDP packet with length as CRC %128 | 128 UDP packet with length as sequence %128 | 128 UDP packet with data | 256 (4 times)

If you are short on data packets (for the last group) then 'empty' packets with a length of 256 are used.

BillSobel commented 6 years ago
    public async Task<bool> RegisterSmartLink(string region, string token, string secret, string wifiName, string wifiPassword, CancellationToken cancellationToken)
    {
        if (String.IsNullOrWhiteSpace(region) || region.Length != 2) throw new ArgumentException("Invalid region");
        if (String.IsNullOrWhiteSpace(token) || token.Length != 8) throw new ArgumentException("Invalid token");
        if (String.IsNullOrWhiteSpace(secret) || secret.Length != 4) throw new ArgumentException("Invalid secret");
        if (String.IsNullOrWhiteSpace(wifiName) || wifiName.Length > 32) throw new ArgumentException("Invalid wifiName");
        if (String.IsNullOrWhiteSpace(wifiPassword) || wifiPassword.Length > 64) throw new ArgumentException("Invalid wifiPassword");

        var encodedBuffer = SmartLinkEncode(region, token, secret, wifiName, wifiPassword);

        try
        {
            using (var udpClient = new UdpClient())
            {
                udpClient.ExclusiveAddressUse = false;
                udpClient.EnableBroadcast = true;
                udpClient.Client.SendTimeout = 2 * 1000;
                udpClient.Client.ReceiveTimeout = 2 * 1000;
                udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, UdpSmartLinkConfigurationPort));
                udpClient.Connect(IPAddress.Parse("255.255.255.255"), 30011);

                int delayMs = 0;
                int timeoutSeconds = 30;

                DateTime startTime = DateTime.UtcNow;
                while (!cancellationToken.IsCancellationRequested && Math.Abs((DateTime.UtcNow - startTime).TotalSeconds) < timeoutSeconds)
                {
                    Console.WriteLine("Sending SmartLink initializtion packets");
                    // Send empty packets of lengths 1, 3, 6, and 10 in a loop
                    await SendSmartLinkStart(udpClient, cancellationToken);

                    Console.WriteLine("Sending SmartLink information");
                    for (int x = 0; x < 30 && !cancellationToken.IsCancellationRequested; x++)
                    {
                        if (delayMs > 26) delayMs = 6;
                        foreach (int encodedByte in encodedBuffer)
                        {
                            await udpClient.SendAsync(ZERO_BUFFER, (int)encodedByte).ConfigureAwait(false);
                            await Task.Delay(delayMs).ConfigureAwait(false);
                        }
                        if(!cancellationToken.IsCancellationRequested) await Task.Delay(200).ConfigureAwait(false);
                        delayMs += 3;
                    }
                }
            }
            return !cancellationToken.IsCancellationRequested;
        }
        catch(Exception ex)
        {
            // Broadcast sending, we do not need a response.
            Console.WriteLine("RegisterSmartLink exception: " + ex.ToString());
            return false;
        }
    }

    private static async Task SendSmartLinkStart(UdpClient udpClient, CancellationToken cancellationToken)
    {
        for (int x = 0; x < 144 && !cancellationToken.IsCancellationRequested; x++)
        {
            await udpClient.SendAsync(ZERO_BUFFER, 1).ConfigureAwait(false);
            await udpClient.SendAsync(ZERO_BUFFER, 3).ConfigureAwait(false);
            await udpClient.SendAsync(ZERO_BUFFER, 6).ConfigureAwait(false);
            await udpClient.SendAsync(ZERO_BUFFER, 10).ConfigureAwait(false);
            await Task.Delay((x % 8) + 33, cancellationToken).ConfigureAwait(false);
        }    
    }

    public static byte CalculateTuyaCrc8(byte[] p, int len)
    {
        byte crc = 0;
        int i = 0;
        while (i < len)
        {
            crc = (byte)calcrc_1byte(crc ^ p[i]);
            int j = crc & 0xff;
            i++;
        }
        return crc;
    }

    private static int calcrc_1byte(int abyte)
    {
        int i, crc_1byte;
        crc_1byte = 0;
        for (i = 0; i < 8; i++)
        {
            if (((crc_1byte ^ abyte) & 0x01) > 0)
            {
                crc_1byte ^= 0x18;
                crc_1byte >>= 1;
                crc_1byte |= 0x80;
            }
            else
            {
                crc_1byte >>= 1;
            }
            abyte >>= 1;
        }
        return crc_1byte;
    }

    public static int[] SmartLinkEncode(string region, string token, string secret, string wifiName, string wifiPassword)
    {
        var wifiPasswordBytes = Encoding.UTF8.GetBytes(wifiPassword);
        var regionTokenSecretBytes = Encoding.UTF8.GetBytes(region + token + secret);
        var wifiNameBytes = Encoding.UTF8.GetBytes(wifiName);

        var rawByteArray = new byte[1 + wifiPasswordBytes.Length + 1 + regionTokenSecretBytes.Length + wifiNameBytes.Length];
        int rawByteArrayIndex = 0;

        rawByteArray[rawByteArrayIndex++] = (byte)wifiPassword.Length;
        Buffer.BlockCopy(wifiPasswordBytes, 0, rawByteArray, rawByteArrayIndex, wifiPasswordBytes.Length);
        rawByteArrayIndex += wifiPasswordBytes.Length;

        rawByteArray[rawByteArrayIndex++] = (byte)regionTokenSecretBytes.Length;
        Buffer.BlockCopy(regionTokenSecretBytes, 0, rawByteArray, rawByteArrayIndex, regionTokenSecretBytes.Length);
        rawByteArrayIndex += regionTokenSecretBytes.Length;

        Buffer.BlockCopy(wifiNameBytes, 0, rawByteArray, rawByteArrayIndex, wifiNameBytes.Length);
        rawByteArrayIndex += wifiNameBytes.Length;

        if (rawByteArrayIndex != rawByteArray.Length) return null;

        // Packet broadcast (using length for data)
        int rawDataLengthRoundedUp = (int)Rounder(rawByteArray.Length, 4);
        int finalDataArrayLength = 4 + rawDataLengthRoundedUp + ((rawDataLengthRoundedUp / 4) * 2);
        var encodedData = new int[finalDataArrayLength];

        //
        // Fill in first 4 bytes 
        // of header here
        //
        var stringLength = (wifiPasswordBytes.Length + regionTokenSecretBytes.Length + wifiNameBytes.Length + 2) % 256; ;
        var stringLengthBuffer = new byte[1];
        stringLengthBuffer[0] = (byte)(stringLength);
        var stringLengthCrc = CalculateTuyaCrc8(stringLengthBuffer, 1);

        // Length encoded into the first two bytes based at 16 and then 32
        encodedData[0] = (stringLength / 16) | 16;
        encodedData[1] = (stringLength % 16) | 32;
        // Length CRC encoded into the next two bytes based at 46 and 64
        encodedData[2] = (stringLengthCrc / 16) | 48;
        encodedData[3] = (stringLengthCrc % 16) | 64;

        int encodedDataIndex = 4;
        int sequenceCounter = 0;
        for (int x = 0; x < rawDataLengthRoundedUp; x += 4)
        {
            // Build CRC buffer, pulling in data or 0 values if ran past the end.
            var crcData = new byte[5];
            crcData[0] = (byte)sequenceCounter++;
            crcData[1] = x + 0 < rawByteArray.Length ? rawByteArray[x + 0] : (byte)0;
            crcData[2] = x + 1 < rawByteArray.Length ? rawByteArray[x + 1] : (byte)0;
            crcData[3] = x + 2 < rawByteArray.Length ? rawByteArray[x + 2] : (byte)0;
            crcData[4] = x + 3 < rawByteArray.Length ? rawByteArray[x + 3] : (byte)0;

            // Calculate the CRC
            var crc = CalculateTuyaCrc8(crcData, 5);

            // Move data into encodedData array...

            // CRC8
            encodedData[encodedDataIndex++] = (crc % 128) | 128;
            // Sequence number
            encodedData[encodedDataIndex++] = (crcData[0] % 128) | 128;
            // Data
            encodedData[encodedDataIndex++] = (crcData[1] % 256) | 256;
            encodedData[encodedDataIndex++] = (crcData[2] % 256) | 256;
            encodedData[encodedDataIndex++] = (crcData[3] % 256) | 256;
            encodedData[encodedDataIndex++] = (crcData[4] % 256) | 256;
        }

        return encodedData;
    }    

    private static double Rounder(double x, double g = 4)
    {
        return Math.Floor((x + g / 2) / g) * g;
    }
codetheweb commented 6 years ago

Ok, thanks for the code @BillSobel.

Ericmas001 commented 6 years ago

So that would mean every device is registered in the same database, even if 2 device are registered with 2 different app? They just have a "ID app" attached to it and when you call to get the token with you app key, it will be linked to the app because off that?

If not, I don't understand how the device can call the right api since this is not part of the thing we send in the udp packet

On Fri, May 4, 2018, 3:51 PM Max Isom, notifications@github.com wrote:

Ok, thanks for the code @BillSobel https://github.com/BillSobel.

β€” You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/codetheweb/tuyapi/issues/22#issuecomment-386714417, or mute the thread https://github.com/notifications/unsubscribe-auth/AK6vFxKi_PEcIW7k6MsiDUglIGzvfEm_ks5tvLFZgaJpZM4RwkTC .

BillSobel commented 6 years ago

@Ericmas001 Sort of, you can think of it logically as one big data base per region but the actual implementation is probably different. Region is the first main key, that actually drives you to one of three amazon data centers (europe, us, or asia). Within that data center logically all devices are registered but the same device registered with different keys will basically be treated as completely different devices.

When you get a token, the token is valid within your namespace on their backend, so when the device registers with it, they know whom to assign it to.

codetheweb commented 6 years ago

Hey @BillSobel, would you mind looking at my code here? I translated your protocol implementation from C# to Javascript, but it's not working for some reason, even though captured packets from both my program and the app look identical.

The only thing I can think of is that the timing between packets and packet groups might be off (is that important?).

BillSobel commented 6 years ago

@codetheweb The code referenced earlier which got me started on this had those options to change up the packet delay, but you copied them so you should be good. Have you verified data is on the wire in the expected format? The code looks correct, but I have one (possibly naive) question on when you calculate the CRC:

let stringLengthCRC = this.tuyaCRC8([stringLength], 1);

I'm assuming that stringLength is an int. Are you sure the crc function is getting the right byte of the int (the low byte of the low word) in this calculation? It appears to be either indeed getting it right or potentially getting the high byte of the high word. The second thing is, while the CRC implimentation looks right, do you want to crc a few example strings and do the same (or I can if your not setup) in the c# version to ensure its indeed calculating the same results.

codetheweb commented 6 years ago

Yeah, my code is currently pretty messy @BillSobel πŸ˜›. But it seems to work. Here's a workbook for testing your C# code. It produces the same output as my CRC8 function. The JS version looks like this:

let device = new tuya();

var testBuffer = Buffer.from('codetheweb@icloud.com');

console.log(device.tuyaCRC8(testBuffer, testBuffer.length));
BillSobel commented 6 years ago

How about the crc of the int, did you verify the right byte is getting the crc calc applied?

codetheweb commented 6 years ago

I'm not really sure what you mean by the 'right byte' @BillSobel. I could be wrong, but from my (limited) knowledge of high/low bytes and the way JS treats variables, I believe stringLength is only stored as one byte. I could be wrong. However, Buffer.from([stringLength]).readInt8(0) === stringLength.

BillSobel commented 6 years ago

@codetheweb That sounds correct, so Im unfortunately not seeing any obvious errors in the port. Any chance you can wireshark the conversation?

codetheweb commented 6 years ago

Here's a capture with these parameters @BillSobel:

Thanks for all your help.

BillSobel commented 6 years ago

@codetheweb Just get a 404 on the link...

codetheweb commented 6 years ago

Sorry about that, try this @BillSobel.

BillSobel commented 6 years ago

The first 4 bytes of your header (from the capture) are: 17,40,79.53 While I don't see an issue yet in the code, 79 doesn't appear valid. Since the CRC is a single byte, it its range is 0-255. /16 give us a range 0-15. Its then based at 48 giving a range of 48-63. I don't see how the 79 is getting in there unless the CRC function is returning a value > 256.

BillSobel commented 6 years ago

Also the rest of the data is misordered. The packet sequence should appear every 6 bytes (CRC, seq, 4 data bytes). The packet capture shows the seq # 128, 129, 130, etc but they are jumbled through the set for some reason. Im going to guess that the system is not actually waiting for each packet to be completely sent before sending the next and data is getting interwoven out of order.