Skip to content

Commit a9dc7f4

Browse files
authored
Merge pull request #709 from ostjen/fix-ir-large-payload
Raise parse_header payload ceiling so large IR learn frames decode
2 parents f111b98 + b6f692a commit a9dc7f4

5 files changed

Lines changed: 38 additions & 4 deletions

File tree

RELEASE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# RELEASE NOTES
22

3+
## v1.18.1 - IR Learn Frame Fix
4+
5+
* core: Added `MAX_PAYLOAD_LENGTH` constant (default 1440 bytes) in `tinytuya/core/const.py` to replace the hardcoded 1000-byte ceiling in `parse_header()`. Enables local IR learn frame capture from devices with larger payloads such as AC IR blasters. Fixes [#708](https://github.com/jasonacox/tinytuya/issues/708) via [#709](https://github.com/jasonacox/tinytuya/pull/709) by @ostjen.
6+
37
## v1.18.0 - Format Handling and UX Improvements
48

59
* `devices.json` format: All loading paths (library, CLI, scanner, wizard, API server) now support both a flat `[{...}]` list and the `{"devices": [{...}]}` wrapped-dict format via a new centralized `load_devicefile()` helper. Fixes [#532](https://github.com/jasonacox/tinytuya/issues/532) via [#700](https://github.com/jasonacox/tinytuya/pull/700) by @uzlonewolf and @jasonacox.

tests.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
import tinytuya
2222
from tinytuya.Contrib.RFRemoteControlDevice import RFRemoteControlDevice
23+
from tinytuya.core import message_helper as mh
24+
from tinytuya.core.exceptions import DecodeError
2325

2426
LOCAL_KEY = '0123456789abcdef'
2527

@@ -384,5 +386,26 @@ def test_special_chars_in_key(self):
384386
self.assertEqual(result[0]['key'], ":|S'vf<MT6xhr{1~")
385387

386388

389+
class TestParseHeader(unittest.TestCase):
390+
"""parse_header must accept large IR/AC learn frames (>1000 bytes) while
391+
still rejecting absurd sizes from a corrupt or desynced stream."""
392+
393+
def _header_6699(self, payload_len):
394+
return struct.pack(
395+
mh.H.MESSAGE_HEADER_FMT_6699,
396+
mh.H.PREFIX_6699_VALUE, 0, 1, 8, payload_len,
397+
)
398+
399+
def test_large_ir_payload_accepted(self):
400+
# a 1038-byte frame is a real air-conditioner learn report; it must parse
401+
header = mh.parse_header(self._header_6699(1038) + b'\x00' * 40)
402+
self.assertEqual(header.length, 1038)
403+
404+
def test_oversized_payload_rejected(self):
405+
oversized = self._header_6699(mh.MAX_PAYLOAD_LENGTH + 1)
406+
with self.assertRaises(DecodeError):
407+
mh.parse_header(oversized + b'\x00' * 40)
408+
409+
387410
if __name__ == '__main__':
388411
unittest.main()

tinytuya/core/const.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@
1212
TCPTIMEOUT = 0.4 # Seconds to wait for socket open for scanning
1313
DEFAULT_NETWORK = '192.168.0.0/24'
1414

15+
# Heuristic ceiling used to reject corrupt/desynced streams. Most devices only
16+
# have ~256 KiB of RAM and need 2x-3x the payload size for buffers, plus packets
17+
# over ~1440 bytes tend to fragment, so keep the default conservative. Override
18+
# at runtime if a particular device needs larger frames.
19+
MAX_PAYLOAD_LENGTH = 1440
20+
1521
# Configuration Files
1622
CONFIGFILE = 'tinytuya.json'
1723
DEVICEFILE = 'devices.json'

tinytuya/core/core.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@
101101
if HAVE_COLORAMA:
102102
init()
103103

104-
version_tuple = (1, 18, 0) # Major, Minor, Patch
104+
version_tuple = (1, 18, 1) # Major, Minor, Patch
105105
version = __version__ = "%d.%d.%d" % version_tuple
106106
__author__ = "jasonacox"
107107

tinytuya/core/message_helper.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from .crypto_helper import AESCipher
1212
from .exceptions import DecodeError
13+
from .const import MAX_PAYLOAD_LENGTH
1314
from . import header as H
1415

1516
log = logging.getLogger(__name__)
@@ -91,9 +92,9 @@ def parse_header(data):
9192
#log.debug('Header prefix wrong! %08X != %08X', prefix, PREFIX_VALUE)
9293
raise DecodeError('Header prefix wrong! %08X is not %08X or %08X' % (prefix, H.PREFIX_55AA_VALUE, H.PREFIX_6699_VALUE))
9394

94-
# sanity check. currently the max payload length is somewhere around 300 bytes
95-
if payload_len > 1000:
96-
raise DecodeError('Header claims the packet size is over 1000 bytes! It is most likely corrupt. Claimed size: %d bytes. fmt:%s unpacked:%r' % (payload_len,fmt,unpacked))
95+
# sanity check to catch a corrupt/desynced stream (see MAX_PAYLOAD_LENGTH)
96+
if payload_len > MAX_PAYLOAD_LENGTH:
97+
raise DecodeError('Header claims the packet size is over %d bytes! It is most likely corrupt. Claimed size: %d bytes. fmt:%s unpacked:%r' % (MAX_PAYLOAD_LENGTH,payload_len,fmt,unpacked))
9798

9899
return TuyaHeader(prefix, seqno, cmd, payload_len, total_length)
99100

0 commit comments

Comments
 (0)