diff --git a/index.js b/index.js index 5618d73..bffa265 100644 --- a/index.js +++ b/index.js @@ -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) { diff --git a/test/sequence.js b/test/sequence.js new file mode 100644 index 0000000..ae12aa2 --- /dev/null +++ b/test/sequence.js @@ -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); +});