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

Implement 3.5 protocol #623

Open
Apollon77 opened this issue Feb 28, 2023 · 81 comments
Open

Implement 3.5 protocol #623

Apollon77 opened this issue Feb 28, 2023 · 81 comments

Comments

@Apollon77
Copy link
Collaborator

There seems to be new devices with a new format at least on discovery.

@nospam2k
Copy link
Contributor

Are you going to be able to update to 3.5? I hope so!

@Apollon77
Copy link
Collaborator Author

@nospam2k I have no such device, sooo right now not really on my list ... (and even if time is another topic). But happy to get a PR :-)

@nospam2k
Copy link
Contributor

nospam2k commented Jul 31, 2024

From looking at Protocol notes from tinytuya as Apollon77 has posted, I have been able to convert the test python code to node.js and return a packet from the 3.5 device, I am going to work on figuring out how to implement it into tuyapi but I'm not sure exactly what I'm doing as I'm not familiar with the tuyapi code. Any help would be appreciated.

const net = require('net');
const crypto = require('crypto');

const ip = '192.168.x.xxx'; // Add the correct IP address
const key = Buffer.from('xxxxxxxxxxxxxx', 'utf-8'); // Add local key
var stime;

for (let i = 0; i < 2; i++) {
    const client = new net.Socket();

    client.setTimeout(5000);

    client.connect(6668, ip, () => {
        console.log('connected!');

        stime = Date.now();
        const localNonce = Buffer.from('0123456789abcdef', 'utf-8'); // not-so-random random key
        const localIV = localNonce.slice(0, 12); // not-so-random random iv

        const pkt = Buffer.alloc(18);
        pkt.writeUInt32BE(0x6699, 0);
        pkt.writeUInt16BE(0, 4);
        pkt.writeUInt32BE(1, 6);
        pkt.writeUInt32BE(3, 10);
        pkt.writeUInt32BE(localNonce.length + localIV.length + 16, 14);

        const cipher = crypto.createCipheriv('aes-128-gcm', key, localIV);
        cipher.setAAD(pkt.slice(4));
        const encrypted = Buffer.concat([cipher.update(localNonce), cipher.final()]);
        const tag = cipher.getAuthTag();

        const message = Buffer.concat([pkt, localIV, encrypted, tag, Buffer.from([0x00, 0x00, 0x99, 0x66])]);

        client.write(message);
    });

    client.on('data', (data) => {
        console.log('data:', data, 'in', (Date.now() - stime) / 1000);
        client.destroy();
    });

    client.on('timeout', () => {
        console.log('socket timeout');
        client.destroy();
    });

    client.on('error', (err) => {
        console.log('socket error:', err.message);
        client.destroy();
    });

    client.on('close', () => {
        console.log('connection closed');
    });
}

@Apollon77
Copy link
Collaborator Author

Ok, so this in your connect callback is kind if the handshake? And then data arrive? are the data then plain text or also encrypted?

So the code you have here should go into https://github.com/codetheweb/tuyapi/blob/master/index.js#L668 ... so like another "if" with 3.5 after the 3.4 if

@nospam2k
Copy link
Contributor

nospam2k commented Aug 2, 2024

@Apollon77 I really don't know what I'm doing but I THINK I've worked out an encrypt and decrypt from digging through tinytuya. I'll include my return at the end. If you have time to put in some more info I'll be glad to test things. I'm not sure about implementation in your above reply because I really don't know anything about the protocol. Right now, I've just been trying to isolate how the message is sent and received. Thanks for any help you can give. Here is the updated code:

const net = require('net');
const crypto = require('crypto');

const ip = '<ip address>'; // Add the correct IP address
const key = Buffer.from('<local key>', 'utf-8');
var stime;

for (let i = 0; i < 2; i++) {
    const client = new net.Socket();

    client.setTimeout(5000);

    client.connect(6668, ip, () => {
        console.log('connected!');

        stime = Date.now();
        const localNonce = Buffer.from('0123456789abcdef', 'utf-8'); // not-so-random random key
        const localIV = localNonce.slice(0, 12); // not-so-random random iv

        const pkt = Buffer.alloc(18);
        pkt.writeUInt32BE(0x6699, 0);
        pkt.writeUInt16BE(0, 4);
        pkt.writeUInt32BE(1, 6);
        pkt.writeUInt32BE(3, 10);
        pkt.writeUInt32BE(localNonce.length + localIV.length + 16, 14);

        const cipher = crypto.createCipheriv('aes-128-gcm', key, localIV);
        cipher.setAAD(pkt.slice(4));
        const encrypted = Buffer.concat([cipher.update(localNonce), cipher.final()]);
        const tag = cipher.getAuthTag();

        const message = Buffer.concat([pkt, localIV, encrypted, tag, Buffer.from([0x00, 0x00, 0x99, 0x66])]);

        client.write(message);
    });

    client.on('data', (data) => {
        //console.log('data:', data.toString('hex'));
        //console.log('prefix:', data.slice(0, 4).toString('hex'));
        //console.log('unknown:', data.slice(4, 6).toString('hex'));
        //console.log('sequence:', data.slice(6, 10).toString('hex'));
        //console.log('command:', data.slice(10, 14).toString('hex'));
        //console.log('length:', data.slice(14, 18).toString('hex'));
        //console.log('iv:', data.slice(18, 30).toString('hex'));
        //console.log('payload:', data.slice(18 + 12, plen + 18 - 16).toString('hex'));
        //console.log('tag:', data.slice(plen + 18 - 16, plen + 18 - 16 + 16).toString('hex'));
        //console.log('footer:', data.slice(plen + 18).toString('hex'));
        const header = data.slice(4, 18)
        const prefix = data.slice(0, 4);
        const unknown = data.slice(4, 6);
        const sequence = data.slice(6, 10);
        const command = data.slice(10, 14);
        const length = data.slice(14, 18);
        const iv = data.slice(18, 30);
        const plen = parseInt(data.slice(14, 18).toString('hex'), 16);
        const payload = data.slice(18 + 12, plen + 2); // 18 - 16
        const tag = data.slice(plen + 2, plen + 18); // 18 - 16
        const footer = data.slice(plen + 18);

        client.destroy();

        const decipher = crypto.createDecipheriv('aes-256-gcm', key.toString('hex'), iv.toString('hex'));
        decipher.setAAD(header);
        decipher.setAuthTag(tag);
        let raw = decipher.update(payload, 'binary', 'utf-8');
        console.log(raw);
    });

    client.on('timeout', () => {
        console.log('socket timeout');
        client.destroy();
    });

    client.on('error', (err) => {
        console.log('socket error:', err.message);
        client.destroy();
    });

    client.on('close', () => {
        console.log('connection closed');
    });
}
connected!
connected!
�����(\<}�5W(�M�'��W�y�� ��}���z�_�
                                   �d��v{ey�f
connection closed

�?�3&3݄~>�O8����놡�Ý*����7�:�����:��J��{4
connection closed

@nospam2k
Copy link
Contributor

nospam2k commented Aug 2, 2024

Looking more, it looks like I need to update these, but I'm not sure exactly how to get the missing parameters. Options seems to be the payload???:

cipher.js
  /**
   * Encrypt data for protocol 3.4
   * @param {Object} options Options for encryption
   * @param {String} options.data data to encrypt
   * @param {Boolean} [options.base64=true] `true` to return result in Base64
   * @returns {Buffer|String} returns Buffer unless options.base64 is true
   */
  _encrypt34(options) {
    const cipher = crypto.createCipheriv('aes-128-ecb', this.getKey(), null);
    cipher.setAutoPadding(false);
    const encrypted = cipher.update(options.data);
    cipher.final();

    // Default base64 enable TODO: check if this is needed?
    // if (options.base64 === false) {
    //   return Buffer.from(encrypted, 'base64');
    // }

    return encrypted;
  }

  /**
   * Decrypts data for protocol 3.4
   * @param {String|Buffer} data to decrypt
   * @returns {Object|String}
   * returns object if data is JSON, else returns string
   */
  _decrypt34(data) {
    let result;
    try {
      const decipher = crypto.createDecipheriv('aes-128-ecb', this.getKey(), null);
      decipher.setAutoPadding(false);
      result = decipher.update(data);
      decipher.final();
      // Remove padding
      result = result.slice(0, (result.length - result[result.length - 1]));
    } catch (_) {
      throw new Error('Decrypt failed');
    }

    // Try to parse data as JSON,
    // otherwise return as string.
    // 3.4 protocol
    // {"protocol":4,"t":1632405905,"data":{"dps":{"101":true},"cid":"00123456789abcde"}}
    try {
      if (result.indexOf(this.version) === 0) {
        result = result.slice(15);
      }

      const res = JSON.parse(result);
      if ('data' in res) {
        const resData = res.data;
        resData.t = res.t;
        return resData; // Or res.data // for compatibility with tuya-mqtt
      }

      return res;
    } catch (_) {
      return result;
    }
  }

message-parser.js
_encode34(options) {
    let payload = options.data;

    if (options.commandByte !== CommandType.DP_QUERY &&
        options.commandByte !== CommandType.HEART_BEAT &&
        options.commandByte !== CommandType.DP_QUERY_NEW &&
        options.commandByte !== CommandType.SESS_KEY_NEG_START &&
        options.commandByte !== CommandType.SESS_KEY_NEG_FINISH &&
        options.commandByte !== CommandType.DP_REFRESH) {
      // Add 3.4 header
      // check this: mqc_very_pcmcd_mcd(int a1, unsigned int a2)
      const buffer = Buffer.alloc(payload.length + 15);
      Buffer.from('3.4').copy(buffer, 0);
      payload.copy(buffer, 15);
      payload = buffer;
    }

    // ? if (payload.length > 0) { // is null messages need padding - PING work without
    const padding = 0x10 - (payload.length & 0xF);
    const buf34 = Buffer.alloc((payload.length + padding), padding);
    payload.copy(buf34);
    payload = buf34;
    // }

    payload = this.cipher.encrypt({
      data: payload
    });

    payload = Buffer.from(payload);

    // Allocate buffer with room for payload + 24 bytes for
    // prefix, sequence, command, length, crc, and suffix
    const buffer = Buffer.alloc(payload.length + 52);

    // Add prefix, command, and length
    buffer.writeUInt32BE(0x000055AA, 0);
    buffer.writeUInt32BE(options.commandByte, 8);
    buffer.writeUInt32BE(payload.length + 0x24, 12);

    if (options.sequenceN) {
      buffer.writeUInt32BE(options.sequenceN, 4);
    }

    // Add payload, crc, and suffix
    payload.copy(buffer, 16);
    const calculatedCrc = this.cipher.hmac(buffer.slice(0, payload.length + 16));// & 0xFFFFFFFF;
    calculatedCrc.copy(buffer, payload.length + 16);

    buffer.writeUInt32BE(0x0000AA55, payload.length + 48);
    return buffer;
  }

@Apollon77
Copy link
Collaborator Author

Yes I also think you need to add a new method "_encrypt35" and "_decrypt35"
options.data and pot options.base64 is the values on encrypt and on decrypt also comparable.

In fact try to also log your result after decryption form what you get as data in as hex too then we might see details

@nospam2k
Copy link
Contributor

nospam2k commented Aug 4, 2024

Sorry for the delay

This is console.log(raw.toString('hex'): <Buffer 4b 51 45 29 69 60 67 47 4f 56 75 39 47 5d 6f 41>

@nospam2k
Copy link
Contributor

nospam2k commented Aug 8, 2024

@Apollon77 Ok, I've gotten a long way on this but I'm stuck. It seems like I've got the negotiation of the key working but the packet for getting query doesn't seem to be coming back. I set up a test.js to make sure the packet is encrypting and decrypting with the new session key and it is.

Decrypted text: Id":"xxxxxxxxxxxxxxxxxxxxxxxx","devId":"xxxxxxxxxxxxxxxxxxxxxxxx","t":"1723094767","dps":{},"uid":"xxxxxxxxxxxxxxxxxxxxxxxx"}

Here is my getdev.js file:

const TuyAPI = require('tuyapi');

const device = new TuyAPI({
  id: 'xxxxxxxxxxxxxxxxxx',
  key: 'xxxxxxxxxxxxxxxxxx',
  ip: '192.168.2.187',
  version: '3.5',
  issueGetOnConnect: false});

(async () => {
  await device.find();

  await device.connect();

  let status = await device.get();

  console.log(`Current status: ${status}.`);

  await device.set({set: !status});

  status = await device.get();

  console.log(`New status: ${status}.`);

  device.disconnect();
})();

Here is a debug:

 TuyAPI IP and ID are already both resolved. +0ms
  TuyAPI Connecting to 192.168.2.187... +2ms
  TuyAPI Socket connected. +94ms
  TuyAPI Protocol 3.4, 3.5: Negotiate Session Key - Send Msg 0x03 +2ms
  TuyAPI Received data: 000066990000000047010000000400000050a66ff15d05b84428b5ccf97be39affa4acc15d15929f11120fea57f332702a2b5fb0c8368d2eef6dfab8da055e2084ce8da5774fa91d3de7dc8189679801b2daa04fcf8f05e5694256cb08f97da2197b00009966 +110ms
  TuyAPI Parsed: +2ms
  TuyAPI {
  TuyAPI   payload: <Buffer 35 38 39 39 32 34 35 63 37 38 64 30 63 37 34 32 3a bf d5 25 b5 63 5d e6 2e c9 57 35 b0 89 d2 f1 e4 22 f9 6e e8 8e 04 9b 13 21 05 3b bf 3a 31 a2>,
  TuyAPI   leftover: false,
  TuyAPI   commandByte: 4,
  TuyAPI   sequenceN: <Buffer 00 00 47 01>
  TuyAPI } +0ms
  TuyAPI Protocol 3.5: Local Random Key: a46f0c381283c50d35f5d5bc971f19b5 +2ms
  TuyAPI Protocol 3.5: Remote Random Key: 34323abfd525b5635de62ec9 +0ms
  TuyAPI Protocol 3.4, 3.5: Session Key: 793a236b01ced4deb36d4ba9011a34c1 +1ms
  TuyAPI Protocol 3.4, 3.5: Initialization done +0ms
  TuyAPI GET Payload: +1ms
  TuyAPI {
  TuyAPI   gwId: 'xxxxxxxxxxxxxxxxxxxxxxxx',
  TuyAPI   devId: 'xxxxxxxxxxxxxxxxxxxxxxxxx',
  TuyAPI   t: '1723095589',
  TuyAPI   dps: {},
  TuyAPI   uid: 'xxxxxxxxxxxxxxxxxxxx'
  TuyAPI } +0ms
  TuyAPI Socket closed: 192.168.2.187 +98ms
  TuyAPI Disconnect +0ms

@Apollon77
Copy link
Collaborator Author

Where exactly happens this part? Because I can not see your other log messages? or does it mean it hangs on connect? or on the first device get? Did you tried to get with a list of datapoint ids instead of "all"?

But hm ... the log also seems incomplete ... because should after the "get payload" not also it log the binary data from the encoded packet that is sent? Instead connection gets closed

@nospam2k
Copy link
Contributor

nospam2k commented Aug 8, 2024

index.js

 async get(options = {}) {
    const payload = {
      gwId: this.device.gwID,
      devId: this.device.id,
      t: Math.round(new Date().getTime() / 1000).toString(),
      dps: {},
      uid: this.device.id
    };

    if (options.cid) {
      payload.cid = options.cid;
    }

    const commandByte = this.device.version === '3.4' || this.device.version === '3.5' ? CommandType.DP_QUERY_NEW : CommandType.DP_QUERY;

    // Create byte buffer
    const buffer = this.device.parser.encode({
      data: payload,
      commandByte,
      sequenceN: ++this._currentSequenceN
    });

    let data;
    // Send request to read data - should work in most cases beside Protocol 3.2
    if (this.device.version !== '3.2') {
      debug('GET Payload:');
      debug(payload);

      data = await this._send(buffer); // <- this never returns.

@Apollon77
Copy link
Collaborator Author

Okmthen this means that the devcie closes the connection ... try to add 3.5 to that "exception list so that you go into https://github.com/codetheweb/tuyapi/blob/master/index.js#L168 case ... does that work?

@nospam2k
Copy link
Contributor

nospam2k commented Aug 8, 2024

@Apollon77 Ok, I messed with how the session key is chopped out of the packet and now it looks like it's better. It isn't returning the query but I'm getting some communication and it never exits. I'm getting this in debug:

TuyAPI IP and ID are already both resolved. +0ms
  TuyAPI Connecting to 192.168.2.187... +2ms
  TuyAPI Socket connected. +43ms
  TuyAPI Protocol 3.4, 3.5: Negotiate Session Key - Send Msg 0x03 +1ms
  TuyAPI Received data: 00006699000000004a290000000400000050edea2e4207e6c194190cbf50b2fd1a443b25812fcced1ce5dfd4933d873228b896a4846fef0d5399dc0eb0d9c461a86c673a8715fd2f72b7004fc5d304ac01582b2e3ee5e61ee3ae0a6d180ce120eba100009966 +122ms
  TuyAPI Parsed: +1ms
  TuyAPI {
  TuyAPI   payload: <Buffer 63 62 31 61 34 31 33 65 66 65 31 61 61 37 35 39 e8 d7 8d a4 92 87 e2 fa f8 21 b6 d2 5f 25 60 60 c7 57 9a 6b bc 5d 45 70 1d 3d 87 b7 02 07 f6 f8>,
  TuyAPI   leftover: false,
  TuyAPI   commandByte: 4,
  TuyAPI   sequenceN: <Buffer 00 00 4a 29>
  TuyAPI } +0ms
  TuyAPI Protocol 3.4, 3.5: Local Random Key: d1c72259f0997d66628f824263ce74cf +3ms
  TuyAPI Protocol 3.4, 3.5: Remote Random Key: 63623161343133656665316161373539 +0ms
  TuyAPI Protocol 3.4, 3.5: Session Key: 5bc206d402c0eab3df9403ff94fc6c82 +1ms
  TuyAPI Protocol 3.4, 3.5: Initialization done +0ms
  TuyAPI GET Payload: +1ms
  TuyAPI {
  TuyAPI   gwId: 'eb840f68d95e7cbc92ivmj',
  TuyAPI   devId: 'eb840f68d95e7cbc92ivmj',
  TuyAPI   t: '1723131283',
  TuyAPI   dps: {},
  TuyAPI   uid: 'eb840f68d95e7cbc92ivmj'
  TuyAPI } +0ms
  TuyAPI Pinging 192.168.2.187 +10s
  TuyAPI Received data: 00006699000000004a2a000000090000002056b841f17444ab11ee2a1ca28b184b079c16aef0f66b65604714b02b62a3c81e00009966 +118ms
  TuyAPI Parsed: +1ms
  TuyAPI {
  TuyAPI   payload: '\x00\x00\x00\x00J*\x00\x00\x00\t\x00\x00\x00 V�A�tD�\x11�*\x1C��\x18K\x07�\x16���ke`G\x14�+b��\x1E',
  TuyAPI   leftover: false,
  TuyAPI   commandByte: 9,
  TuyAPI   sequenceN: <Buffer 00 00 4a 2a>
  TuyAPI } +0ms
  TuyAPI Pong from 192.168.2.187 +1ms
  TuyAPI Pinging 192.168.2.187 +10s
  TuyAPI Received data: 00006699000000004a2b0000000900000020c424fe4285fe82a4a22d1cf527b260ff84971ac2ea145564589b63a998a242e600009966 +51ms
  TuyAPI Parsed: +0ms
  TuyAPI {
  TuyAPI   payload: "\x00\x00\x00\x00J+\x00\x00\x00\t\x00\x00\x00 �$�B�����-\x1C�'�`���\x1A��\x14UdX�c���B�",
  TuyAPI   leftover: false,
  TuyAPI   commandByte: 9,
  TuyAPI   sequenceN: <Buffer 00 00 4a 2b>
  TuyAPI } +0ms
  TuyAPI Pong from 192.168.2.187 +0ms
  TuyAPI Pinging 192.168.2.187 +10s
  TuyAPI Received data: 00006699000000004a2c0000000900000020ba6e2036c6b03a4c2819b91392e892406e36a3631477d1b64ec10720e2ecedac00009966 +85ms
  TuyAPI Parsed: +1ms
  TuyAPI {
  TuyAPI   payload: '\x00\x00\x00\x00J,\x00\x00\x00\t\x00\x00\x00 �n 6ư:L(\x19�\x13��@n6�c\x14wѶN�\x07 ����',
  TuyAPI   leftover: false,
  TuyAPI   commandByte: 9,
  TuyAPI   sequenceN: <Buffer 00 00 4a 2c>
  TuyAPI } +0ms
  TuyAPI Pong from 192.168.2.187 +0ms

@Apollon77
Copy link
Collaborator Author

Apollon77 commented Aug 8, 2024

But ok that binary response payloads look like that something is now still wrong with decryption ... but great progress, awesome.

PS: One info: I'm still here whole next week, but then on vacation 19.8.-28.8. without access to things ... so depending on when it would be ready it could then have a break because of me absent to get a published version (and maybe also makes sense to give it some days before I can not release fixes or such) :) But keep the great progress!

@nospam2k
Copy link
Contributor

nospam2k commented Aug 8, 2024

Thx

@nospam2k
Copy link
Contributor

nospam2k commented Aug 11, 2024

Ok, I'm nearly there (thanks to the help of @uzlonewolf !!) I'm not getting a return of the query but here is the debug which shows I received the packet. The program hangs at this point.

  TuyAPI Connecting to 192.168.2.187... +0ms
  TuyAPI Socket connected. +95ms
  TuyAPI Protocol 3.5: Negotiate Session Key - Send Msg 0x03 +2ms
  TuyAPI Received data: 0000669900000000ed650000000400000050ba56d857c587135e7495e13c72bca670be3f077cf6f52b4e303785c9ba798b7d20b19f5f3002081936dc0d28355d4f9cb2b5dcd08caa086e724477953e695ee01f2de9a4a0a56483f45fd578193e6d1f00009966 +216ms
  TuyAPI Parsed: +2ms
  TuyAPI {
  TuyAPI   payload: <Buffer 65 38 65 34 30 39 61 30 31 31 66 39 33 61 37 66 f8 3f 7e 76 97 f1 98 38 85 44 e8 ce 4c 40 a8 9e ab 43 06 eb 88 a1 b5 cf de 41 97 16 7e 11 61 d0>,
  TuyAPI   leftover: false,
  TuyAPI   commandByte: 4,
  TuyAPI   sequenceN: <Buffer 00 00 ed 65>
  TuyAPI } +0ms
  TuyAPI Protocol 3.4: Local Random Key: 3a2d998bb7abfc7e52d38e7908e3c97d +2ms
  TuyAPI Protocol 3.4: Remote Random Key: 65386534303961303131663933613766 +0ms
  TuyAPI Protocol 3.4: Session Key: a64d6c0852818d65fe93a3c60b45acb2 +1ms
  TuyAPI Protocol 3.4: Initialization done +0ms
  TuyAPI GET Payload: +1ms
  TuyAPI {
  TuyAPI   gwId: 'eb840f68d95e7cbc92ivmj',
  TuyAPI   devId: 'eb840f68d95e7cbc92ivmj',
  TuyAPI   t: '1723360392',
  TuyAPI   dps: {},
  TuyAPI   uid: 'eb840f68d95e7cbc92ivmj'
  TuyAPI } +0ms
  TuyAPI Received data: 0000669900000000ed66000000100000009c9a6db21d1c47d725b9f00d456f26e751aeb5842a561e57db8bc3181bafd9388a932ff964dcdd63d53e6abae0d2f82c097f181e74997874843721f5ef08ab25d78b6326ae7f355b56f98b069d8530a35c477cca006b73566d941f05073594bc688853dbdfcb8fa8597069bb47bff8e332119dc774205f3f9e0e3d70a930981ddea708971dee015bd01eff49dac6c1ea7fb0ff26c43b79b819525792b200009966 +403ms
  TuyAPI Parsed: +1ms
  TuyAPI {
  TuyAPI   payload: {
  TuyAPI     dps: {
  TuyAPI       '20': false,
  TuyAPI       '21': 'white',
  TuyAPI       '22': 1000,
  TuyAPI       '23': 0,
  TuyAPI       '24': '000003e803e8',
  TuyAPI       '25': '000e0d0000000000000000c80000',
  TuyAPI       '26': 0,
  TuyAPI       '34': false
  TuyAPI     }
  TuyAPI   },
  TuyAPI   leftover: false,
  TuyAPI   commandByte: 16,
  TuyAPI   sequenceN: <Buffer 00 00 ed 66>
  TuyAPI } +0ms
  TuyAPI Received DATA packet +0ms
  TuyAPI data: 16 : {"dps":{"20":false,"21":"white","22":1000,"23":0,"24":"000003e803e8","25":"000e0d0000000000000000c80000","26":0,"34":false}} +0ms

@Apollon77
Copy link
Collaborator Author

Cooool. From where the log comes in the last line? "data:..."?

@Apollon77
Copy link
Collaborator Author

Ps: could it be that now #634 joins the game? Can you try the change that was proposed there. If yes could you add it for 3.4 and 3.5? Then we have that in too

@nospam2k
Copy link
Contributor

nospam2k commented Aug 11, 2024

I don't thinks this applies. in _packetHandler, this._currentSequenceN = 1 already. The difficulty is I'm chasing promises and on.data emit etc. So where does emit('data') end up?

      this.emit('data', packet.payload, packet.commandByte, packet.sequenceN);

this is in index.js _packetHandler.

@nospam2k
Copy link
Contributor

nospam2k commented Aug 11, 2024

Ok, a little more playing and it seems that neither set nor get are returning. Here is some test code:

const TuyAPI = require('tuyapi');

const device = new TuyAPI({
  id: '<id>',
  key: '<key>',
  ip: '<ip>',
  version: '3.5'});

let stateHasChanged = false;

// Find device on network
//device.find().then(() => { <<<<< I haven't messed with find yet
// Connect to device
  device.connect();
//});

// Add event listeners
device.on('connected', () => {
  console.log('Connected to device!');
  //device.get();
  device.set({
      dps: 20,
      set: true
  });
});

device.on('disconnected', () => {
  console.log('Disconnected from device.');
});

device.on('error', error => {
  console.log('Error!', error);
});

device.on('data', data => {
  console.log('Data from device:', data);
});

// Disconnect after 10 seconds
setTimeout(() => { device.disconnect(); }, 10000);

The light turns on, but it never returns and then dies after setTimeout expires.
device.on('data') is returning data correctly.

@nospam2k
Copy link
Contributor

nospam2k commented Aug 11, 2024

More info from the end of _packetHandler with some console.logging:

    console.log('packet.sequenceN', packet.sequenceN);
    console.log('this._resolvers', this._resolvers);

    // Call data resolver for sequence number
    if (packet.sequenceN in this._resolvers) {
      this._resolvers[packet.sequenceN](packet.payload);

      // Remove resolver
      delete this._resolvers[packet.sequenceN];
      this._expectRefreshResponseForSequenceN = undefined;
    }
packet.sequenceN <Buffer 00 00 d8 ad>
this._resolvers { '5': [Function (anonymous)], '6': [Function (anonymous)] }
Data from device: {
  dps: {
    '20': false,
    '21': 'white',
    '22': 1000,
    '23': 0,
    '24': '000003e803e8',
    '25': '000e0d0000000000000000c80000',
    '26': 0,
    '34': false
  }
}
packet.sequenceN <Buffer 00 00 d8 ae>
this._resolvers { '5': [Function (anonymous)], '6': [Function (anonymous)] }

Looks like packet.sequenceN may be an issue coming from the device?

@Apollon77
Copy link
Collaborator Author

Thats why I asked for the sequence thing. The responses are mapped via the expectedsequence Number to return when I remember correctly. When you see here sequence number coming back is 00 00 d8 ad which do not match to 5 or 6

@nospam2k
Copy link
Contributor

nospam2k commented Aug 12, 2024

Ok, so what I think is happening is here:

    // Call data resolver for sequence number
    if (packet.sequenceN in this._resolvers) {
      this._resolvers[packet.sequenceN](packet.payload);

      // Remove resolver
      delete this._resolvers[packet.sequenceN];
      this._expectRefreshResponseForSequenceN = undefined;
    }

I tried

this._currentSequenceN = packet.sequenceN - 1;

but it didn't work.

I only have 3.5 devices so I will need confirmation, but I believe <3.4 devices return a sequence the same as the query packet. 3.4 devices are sending +1 (why the sequence - 1 is necessary) but 3.5 devices are a totally different sequence. Either that or I don't understand the sequence handling at all. So if 3.5 device and client sequence numbers are not related, this._resolvers cannot work. Somehow the sequence number has to be from the device or a completely different way of resolving is necessary for 3.5. I willingly admit I now have enough information to be insanely wrong as my knowledge of Tuya protocol was 0 and my knowledge of node is not much above web based javascript.

UPDATE: I thoroughly read through #634 and it seem 3.4 and 3.5 are doing the same thing but I cannot understand how the recommended fix (above) worked as the resolve is already set so changing this._currentSequenceN only changes the pointer but the resolve array still contains the client sequence number.

@Apollon77
Copy link
Collaborator Author

Apollon77 commented Aug 12, 2024

That could be. When I check your logs then the sequence seems to be increasing ... What about just remembering the last received sequence and then use this +1 for the next expected package? (for 3.5 packages)

@nospam2k
Copy link
Contributor

nospam2k commented Aug 12, 2024

Where would I do that so this._resolvers is correct? I'm including my latest debug with my console logs.

david@bkup my_tuya % node tuyapi.js
  TuyAPI Connecting to 192.168.2.187... +0ms
  TuyAPI Socket connected. +125ms
  TuyAPI Protocol 3.5: Negotiate Session Key - Send Msg 0x03 +2ms
  TuyAPI Received data: 0000669900000000708100000004000000507e156992640142165ed6feb037a2d5da9b5fc1670f31fd8f81be2e8de530646fc1471edc2d56e404f854648016d0ff19450003bf46ad7b8f9b745cdacde58f35b08d926f753e839857f8c1f39e8f003500009966 +96ms
  TuyAPI Parsed: +1ms
  TuyAPI {
  TuyAPI   payload: <Buffer 63 31 66 61 36 33 66 30 39 62 63 31 31 65 36 34 b0 82 d6 98 09 c4 63 15 95 dd 65 b6 eb 12 74 da f2 92 c6 3a 57 d1 da e5 9f be 1b 31 05 74 65 59>,
  TuyAPI   leftover: false,
  TuyAPI   commandByte: 4,
  TuyAPI   sequenceN: <Buffer 00 00 70 81>
  TuyAPI } +0ms
  TuyAPI Protocol 3.4: Local Random Key: 60061fd06df3a396e1c245d50476b8ca +3ms
  TuyAPI Protocol 3.4: Remote Random Key: 63316661363366303962633131653634 +0ms
  TuyAPI Protocol 3.4, 3.5: Session Key: fe1b797875755c607b04bb5aab44bfd8 +1ms
  TuyAPI Protocol 3.4, 3.5: Initialization done +0ms
  TuyAPI GET Payload: +0ms
  TuyAPI {
  TuyAPI   gwId: 'eb840f68d95e7cbc92ivmj',
  TuyAPI   devId: 'eb840f68d95e7cbc92ivmj',
  TuyAPI   t: '1723477133',
  TuyAPI   dps: {},
  TuyAPI   uid: 'eb840f68d95e7cbc92ivmj'
  TuyAPI } +0ms
index 468 Resolving sequence number: 3
  TuyAPI Received data: 00006699000000007082000000100000009c9d3c3309d6323dd51e8c3ebfd51daafdc6ff780e31da0601eb971480c0c6c84687c74b278136a9cde85177e20b776bb660edd5102bda9297a8f2553674145aedb02701946b5aa8340d26fb93008b0aa6b43e7d783bae3004070150e0b6b37855d6278956d4fdcd1bbc7bc337c8f8cfcaf7fa62a1c07c814a219fa2ea39fe240f0e1bafc297af7e82b4b4e6688453772ac5d47107b15f71ba7718c6cb00009966 +195ms
  TuyAPI Parsed: +1ms
  TuyAPI {
  TuyAPI   payload: {
  TuyAPI     dps: {
  TuyAPI       '20': false,
  TuyAPI       '21': 'white',
  TuyAPI       '22': 1000,
  TuyAPI       '23': 0,
  TuyAPI       '24': '000003e803e8',
  TuyAPI       '25': '000e0d0000000000000000c80000',
  TuyAPI       '26': 0,
  TuyAPI       '34': false
  TuyAPI     }
  TuyAPI   },
  TuyAPI   leftover: false,
  TuyAPI   commandByte: 16,
  TuyAPI   sequenceN: <Buffer 00 00 70 82>
  TuyAPI } +0ms
  TuyAPI Received DATA packet +1ms
  TuyAPI data: 16 : {"dps":{"20":false,"21":"white","22":1000,"23":0,"24":"000003e803e8","25":"000e0d0000000000000000c80000","26":0,"34":false}} +0ms
index 910 packet.sequenceN <Buffer 00 00 70 82>
index 911 this._currentSequenceN 3
index 912 this._resolvers { '3': [Function (anonymous)] }

My main confusion is I don't really understand the data resolver. If I force:

      this._resolvers[packet.sequenceN](packet.payload);

      // Remove resolver
      delete this._resolvers[packet.sequenceN];
      this._expectRefreshResponseForSequenceN = undefined;

I get back: Current status: undefined.

What exactly should happen during _packetHandler for a get?

@Apollon77
Copy link
Collaborator Author

I would do a new class instance variable "previouslyReceivedCommandNo" and whenever you receive a package set this number there. then when you send simply use it when setting the number for the expected resolver instead of this "packet.sequenceN".

It seems that 3.5 uses separated counters where the older versions synced them

@uzlonewolf
Copy link

uzlonewolf commented Aug 12, 2024

What about just remembering the last received sequence and then use this +1 for the next expected package?

My 3.5 devices send 2 responses: an async STATUS (command 8) followed by the result for the sent command (CONTROL_NEW command 13). So my suggestion would be any sequence greater than the last plus match the command.

@Apollon77
Copy link
Collaborator Author

maybe try to add some logging in the method that handles that received data ... because hard to see whats the reason here

@Apollon77
Copy link
Collaborator Author

Thank you very much for all your efforts so far! Interesting question is how to continue ... Seems I need to try to get a 3.5 device myself to debug based on your awesome work - but this will not happen before beginning of september due to my vacation.

On how to do a PR: Simply way to go into GitHub and select e.g,. the index.ts file and hit the pencil icon upper right. Then edit the relevant content (or copy your version in) then scroll down and add details and the button. This creates a fork and branch with this one change. On next page klick create Pull Request.
Then in the top line you see tuyapi_master and the branch name of your branch ... second one as "link" ... klick it and you are on this branch ... edit the other files via pencil and these changes are all added to this one PR

@Apollon77
Copy link
Collaborator Author

Great, so kind of the place I proposed? in fact this also removes the other +1 place right?

@nospam2k
Copy link
Contributor

Yes the end of the _packetHandler can go and also the code in the get ack (not sure which is still there)

Apollon77 added a commit to Apollon77/tuyapi that referenced this issue Aug 16, 2024
@Apollon77
Copy link
Collaborator Author

so? 96af23b

@nospam2k
Copy link
Contributor

788 can go. only:

    if (
      (
        packet.commandByte === CommandType.CONTROL ||
        packet.commandByte === CommandType.CONTROL_NEW
      ) && packet.payload === false) {
        debug('Got SET ack.');
      return;
    }

@Apollon77
Copy link
Collaborator Author

Bildschirmfoto 2024-08-16 um 18 35 50

yes should be already

@nospam2k
Copy link
Contributor

nospam2k commented Aug 16, 2024

Ahh... the changes are on the left? Maybe I'm reading this wrong.

Screenshot 2024-08-16 at 9 43 18 AM

@Apollon77
Copy link
Collaborator Author

left old, right new, red removed, green added/changed

@Apollon77
Copy link
Collaborator Author

if xou like save your file, download the full file (in the ... in the diff view "View file" and try it

@Apollon77
Copy link
Collaborator Author

I think you are reading the diff wromng ... i try with your file - and yes I also needed to do some changes because you code was not compliant to the code formatting rules of the project :-( But all good ... we get this tackled

@Apollon77
Copy link
Collaborator Author

Ok I compared it and it is the same - beside formatting and one change that makes no difference from code flow and one protection gainst "double resolve/reject" I added. So index.js in the PR from me is "same as yours"

@nospam2k
Copy link
Contributor

nospam2k commented Aug 16, 2024

ok, do I need to do anything? I did the review (hopefully lol)

@Apollon77
Copy link
Collaborator Author

@nospam2k Nope you did a really great job and anything you can do right now ... I wrote @codetheweb an email to get his approval and I would continue as follows: If Max did not had time till I'm back from vacation then I will merge differently. So also release will happen when I'm back (sorry for this)

@nospam2k
Copy link
Contributor

No problem. I'm glad to help. I just really didn't know what I was doing lol. It was a steep learning curve to get my head around how it all worked. Not to mention, I've never done a PR. I can use the code as is in my project. Enjoy your vacation!

@Apollon77
Copy link
Collaborator Author

Thank you for all your support, really valuable to bring 3.5 protocol to the library

@nospam2k
Copy link
Contributor

nospam2k commented Aug 18, 2024

I might have missed something here but the current master branch is missing this diff in index.js. Just checking as I think you did say something about delaying the final changes of index.js but just to be safe. The reason I mention this is I git cloned the master branch and tested and it failed with my torture test:

416,417d415
<         if(this.device.version === '3.5')
<           this._currentSequenceN++;
758d755
< 
786c783,791
<         debug('Got SET ack.');
---
> 
>       if(this.device.version === '3.5')
>       {
>         // Move resolver to next sequence for incoming response after ack
>         this._resolvers[(parseInt(packet.sequenceN) + 1).toString()] = this._resolvers[packet.sequenceN.toString()];
>         delete this._resolvers[packet.sequenceN.toString()];
>       }
> 
>       debug('Got SET ack.');

Just for clarity, the top red should be added and the bottom green should be removed. My diff command was: diff (mine ->) node_modules/tuyapi/index.js (github ->) ../latest-tuyapi/tuyapi/index.js

@nospam2k
Copy link
Contributor

nospam2k commented Aug 19, 2024

@Apollon77 Created PR

@nospam2k
Copy link
Contributor

nospam2k commented Sep 6, 2024

@Apollon77 Is there anything needed from me for finishing up these changes?

@Apollon77
Copy link
Collaborator Author

@nospam2k I'm back from vacation and checking everything. I poked @codetheweb again to merge my PR

@Apollon77
Copy link
Collaborator Author

@nospam2k Can you have a look at the "master branch code" and check if all is now as it should be? Then I would try to publish a new version next

@nospam2k
Copy link
Contributor

nospam2k commented Sep 16, 2024

I ran the following test:

const TuyAPI = require('tuyapi');

// Configure the Tuya device
const device = new TuyAPI({
  id: '<id>',
  key: '<key>',
  ip: '<ip>',
  version: '3.5',
  issueGetOnConnect: false,
  issueRefreshOnConnect: false,
  issueRefreshOnPing: false
});

// Handle errors
device.on('error', err => {
  console.log('Error:', err);
});

// Handle disconnection
device.on('disconnected', () => {
  console.log('Device has been disconnected.');
});

(async () => {
  try {
    // Connect to the device
    await device.connect();
    console.log('Device connected.');

    // Loop to set device property
    for (let i = 0; i < 10; i++) {
      try {
        const result = await device.set({ dps: '20', set: false });
        console.log(`Iteration ${i}: ${result}`);
      } catch (setError) {
        console.log(`Error in setting dps in iteration ${i}:`, setError);
      }
    }
  } catch (connectError) {
    console.log('Error connecting to device:', connectError);
  }
})();

I get the following result with the master:

Device connected.
Iteration 0: undefined
Iteration 1: undefined
Iteration 2: undefined
Iteration 3: undefined
Iteration 4: undefined
Iteration 5: undefined
Iteration 6: undefined
Iteration 7: undefined
Iteration 8: undefined
Iteration 9: undefined

From my code, I get:

Device connected.
Iteration 0: [object Object]
Iteration 1: [object Object]
Iteration 2: [object Object]
Iteration 3: [object Object]
Iteration 4: [object Object]
Iteration 5: [object Object]
Iteration 6: [object Object]
Iteration 7: [object Object]
Iteration 8: [object Object]
Iteration 9: [object Object]

Something in the changes you have made in index.js aren't getting the proper return from a set. Here is a log:

  TuyAPI SET Payload: +3ms
  TuyAPI {
  TuyAPI   data: {
  TuyAPI     ctype: 0,
  TuyAPI     devId: 'eb0f60f5d50d00c3efacv6',
  TuyAPI     gwId: 'eb0f60f5d50d00c3efacv6',
  TuyAPI     uid: '',
  TuyAPI     dps: { '20': false }
  TuyAPI   },
  TuyAPI   protocol: 5,
  TuyAPI   t: 1726501251
  TuyAPI } +0ms
  TuyAPI Received data: 000066990000000073c70000000800000077d3b8feef0dd357178f1f449ddabb92ed2323fc008c2a6c8472e4ffd71105e8964502a7093efa55788eec22722d3036ad24a7dabefecae26aec203fde2875f4b0edf0d72f2a64a2a093ccc53476aa16238057efe6dd3162e83b37c838a6533fea0d4372ab195e9097df2a4a1d33025a7d2e30d4d11fa54600009966 +413ms
  TuyAPI Parsed: +1ms
  TuyAPI {
  TuyAPI   payload: { dps: { '20': false }, type: 'query', t: 1726501251 },
  TuyAPI   leftover: false,
  TuyAPI   commandByte: 8,
  TuyAPI   sequenceN: 29639
  TuyAPI } +0ms
  TuyAPI Received DP_REFRESH packet. +0ms
Iteration 0: undefined

Here is a diff:

413a414,415
>       // Make sure we only resolve or reject once
>       let resolvedOrRejected = false;
422c424,428
<         this._send(buffer);
---
>         this._send(buffer).catch(error => {
>           if (options.shouldWaitForResponse && !resolvedOrRejected) {
>             reject(error);
>           }
>         });
424c430,435
<           this._setResolver = resolve;
---
>           this._setResolver = () => {
>             if (!resolvedOrRejected) {
>               resolve();
>             }
>           };
> 
426a438
>           resolvedOrRejected = true;
429a442
>         resolvedOrRejected = true;
495,499c508,517
<     this._pingPongTimeout = setTimeout(() => {
<       if (this._lastPingAt < now) {
<         this.disconnect();
<       }
<     }, this._responseTimeout * 1000);
---
>     if (this._pingPongTimeout === null) {
>       // If we do not expect a pong from a former ping, we need to set a timeout
>       this._pingPongTimeout = setTimeout(() => {
>         if (this._lastPingAt < now) {
>           this.disconnect();
>         }
>       }, this._responseTimeout * 1000);
>     } else {
>       debug('There was no response to the last ping.');
>     }
710,712d727
<     // Response was received, so stop waiting
<     clearTimeout(this._sendTimeout);
< 
758c773
<       if(this.device.version === '3.4') {
---
>       if (this.device.version === '3.4') {
760c775
<       } else if(this.device.version === '3.5') {
---
>       } else if (this.device.version === '3.5') {
780a796,797
>       clearTimeout(this._pingPongTimeout);
>       this._pingPongTimeout = null;
807c824
<       } else {
---
>       } else if (packet.sequenceN in this._resolvers) {
809,811d825
<         if (packet.sequenceN in this._resolvers) {
<           debug('Received DP_REFRESH response packet - resolve');
<           this._resolvers[packet.sequenceN](packet.payload);
813,825c827,841
<           // Remove resolver
<           delete this._resolvers[packet.sequenceN];
<           this._expectRefreshResponseForSequenceN = undefined;
<         } else if (this._expectRefreshResponseForSequenceN && this._expectRefreshResponseForSequenceN in this._resolvers) {
<           debug('Received DP_REFRESH response packet without data - resolve');
<           this._resolvers[this._expectRefreshResponseForSequenceN](packet.payload);
< 
<           // Remove resolver
<           delete this._resolvers[this._expectRefreshResponseForSequenceN];
<           this._expectRefreshResponseForSequenceN = undefined;
<         } else {
<           debug('Received DP_REFRESH response packet - no resolver found for sequence number' + packet.sequenceN);
<         }
---
>         debug('Received DP_REFRESH response packet - resolve');
>         this._resolvers[packet.sequenceN](packet.payload);
> 
>         // Remove resolver
>         delete this._resolvers[packet.sequenceN];
>         this._expectRefreshResponseForSequenceN = undefined;
>       } else if (this._expectRefreshResponseForSequenceN && this._expectRefreshResponseForSequenceN in this._resolvers) {
>         debug('Received DP_REFRESH response packet without data - resolve');
>         this._resolvers[this._expectRefreshResponseForSequenceN](packet.payload);
> 
>         // Remove resolver
>         delete this._resolvers[this._expectRefreshResponseForSequenceN];
>         this._expectRefreshResponseForSequenceN = undefined;
>       } else {
>         debug('Received DP_REFRESH response packet - no resolver found for sequence number' + packet.sequenceN);
826a843
> 
913,915d929
<     clearTimeout(this._sendTimeout);
<     clearTimeout(this._connectTimeout);
<     clearTimeout(this._responseTimeout);

cipher.js and message_parser.js are good.

@Apollon77
Copy link
Collaborator Author

So, sorry for the late response ... "time" is currently my ultimate end enemy :-( I will check and adjust now ... I poke again when done

@Apollon77
Copy link
Collaborator Author

@nospam2k This diff somehow makes no sense to me ... can you please post your full index.js again as file?

@Apollon77
Copy link
Collaborator Author

Ahh ok the diff is in the wrong direction and you revert changes I did for completely different reasons ... ok I will check these again now

@Apollon77
Copy link
Collaborator Author

Ok, I found it ... please try again with master version

@Apollon77
Copy link
Collaborator Author

@nospam2k When master version works please provide info and I will release

@nospam2k
Copy link
Contributor

I'm ok with the changes in cyber.js and message-parser.js, but I'm not sure about the changes in index.js. Here is my file so you can compare them and if you're ok with the other changes then we should be ok.

index.js.zip

@nospam2k
Copy link
Contributor

I need to mention. I didn't update the 3.5 discovery code yet so this is probably not ready for release. Here is a test that works for discovery. I haven't had time to implement it.
tuya-3.5-device-discovery.js.zip

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

No branches or pull requests

3 participants