Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,18 @@ class TuyaDevice extends EventEmitter {
}

_packetHandler(packet) {
// Keep our outgoing sequence counter in sync with the device's.
// The device advances its own sequence number for every packet it sends,
// including unsolicited ones (e.g. the STATUS/DP_REFRESH push it emits right
// after a SET). If we don't account for those, our next request reuses a
// sequence number the device has already consumed, so its reply no longer
// matches the resolver we registered and the pending get()/set() hangs until
// the socket is recycled. Re-syncing here makes request/resolver matching
// survive device-initiated packets.
if (packet.sequenceN > this._currentSequenceN) {
this._currentSequenceN = packet.sequenceN;
}

// Protocol 3.4, 3.5 - Response to Msg 0x03
if (packet.commandByte === CommandType.SESS_KEY_NEG_RES) {
if (!this.connectPromise) {
Expand Down
64 changes: 64 additions & 0 deletions test/sequence.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import test from 'ava';

const TuyAPI = require('..');

// A Tuya device advances its own sequence number for every packet it sends,
// including unsolicited ones (e.g. the STATUS/DP_REFRESH push emitted right
// after a SET). TuyAPI must re-sync its outgoing counter from received packets:
// otherwise the next request reuses a sequence number the device has already
// consumed, the device answers with a different number, and the resolver we
// registered is never called — so the pending get()/set() hangs until the socket
// is recycled.

function makeDevice() {
const device = new TuyAPI({id: '22325186db4a2217dc8e',
key: '4226aa407d5c1e2b',
ip: '127.0.0.1',
version: '3.4'});
// Pretend a session is already established and swallow socket writes.
device._connected = true;
device.client = {write() {}};
return device;
}

test('re-syncs the sequence counter from an unsolicited device packet', t => {
const device = makeDevice();
device._currentSequenceN = 98;

// The device pushes a STATUS packet using sequence 100; our counter is behind.
device._packetHandler({commandByte: 8, sequenceN: 100, payload: {dps: {3: 15}}});

t.is(device._currentSequenceN, 100);
});

test('does not rewind the sequence counter on an older packet', t => {
const device = makeDevice();
device._currentSequenceN = 100;

device._packetHandler({commandByte: 8, sequenceN: 90, payload: {dps: {3: 15}}});

t.is(device._currentSequenceN, 100);
});

test('a pending request still resolves after an unsolicited push', t => {
const device = makeDevice();
let deviceSequenceN = 200; // the last sequence number the device has used

// The device pushes an unsolicited packet, advancing its own counter.
deviceSequenceN++;
device._packetHandler({commandByte: 8, sequenceN: deviceSequenceN, payload: {dps: {3: 15}}});

// Issue a read the way get()/_send() do: claim the next sequence number and
// register a resolver for it.
const requestSequenceN = ++device._currentSequenceN;
let received;
device._resolvers[requestSequenceN] = payload => {
received = payload;
};

// The device answers on the sequence number that follows the one it last used.
deviceSequenceN++;
device._packetHandler({commandByte: 16, sequenceN: deviceSequenceN, payload: {dps: {1: true}}});

t.is(received.dps['1'], true);
});