-
-
Notifications
You must be signed in to change notification settings - Fork 343
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
Comments
Are you going to be able to update to 3.5? I hope so! |
@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 :-) |
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');
});
} |
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 |
@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');
});
}
|
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;
} |
Yes I also think you need to add a new method "_encrypt35" and "_decrypt35" 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 |
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> |
@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 |
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 |
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. |
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? |
@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 |
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! |
Thx |
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 |
Cooool. From where the log comes in the last line? "data:..."? |
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 |
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. |
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. |
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;
}
Looks like packet.sequenceN may be an issue coming from the device? |
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 |
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. |
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) |
Where would I do that so this._resolvers is correct? I'm including my latest debug with my console logs.
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? |
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 |
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. |
maybe try to add some logging in the method that handles that received data ... because hard to see whats the reason here |
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. |
left old, right new, red removed, green added/changed |
if xou like save your file, download the full file (in the ... in the diff view "View file" and try it |
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 |
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" |
ok, do I need to do anything? I did the review (hopefully lol) |
@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) |
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! |
Thank you for all your support, really valuable to bring 3.5 protocol to the library |
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: |
@Apollon77 Created PR |
@Apollon77 Is there anything needed from me for finishing up these changes? |
@nospam2k I'm back from vacation and checking everything. I poked @codetheweb again to merge my PR |
@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 |
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:
From my code, I get:
Something in the changes you have made in index.js aren't getting the proper return from a set. Here is a log:
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. |
So, sorry for the late response ... "time" is currently my ultimate end enemy :-( I will check and adjust now ... I poke again when done |
@nospam2k This diff somehow makes no sense to me ... can you please post your full index.js again as file? |
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 |
Ok, I found it ... please try again with master version |
@nospam2k When master version works please provide info and I will release |
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. |
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. |
@nospam2k All the changes left now compared to your file are fixes for other topics and cleanups ... so unrelated to 3.4 ... so please use the master version and it should work too. If not I need information. Ok, so we need something more for discovery. ok, but what exactly? |
I'm out of town until next friday. When I get back I'll work on the discovery and try testing the master repo. I think it's just in the index.js. The code I attached it a working example of a discovery. |
No hurry and thank you for all your support. |
Hi, |
Sorry for the delay. I've been out of town a few times and just got back. I'm going to be busy over the holidays so I'm short on time. I'll try to get something together for discovery. I did attach a test discovery code if anyone is interested in picking it up. Again, apologies for the delay. |
@nospam2k No need for apologies ... RL always wins ... I also got tied up in other topics, so did not had a chance to install a device that in theory should be 3.5 |
@Apollon77 I love the open source community! Thanks for the encouraging word. ;) |
There seems to be new devices with a new format at least on discovery.
The text was updated successfully, but these errors were encountered: