From 8f1b113646231982dfa6c6b1d45701f4d9278c94 Mon Sep 17 00:00:00 2001 From: Alexey Masolov Date: Sun, 24 May 2026 16:40:08 +1000 Subject: [PATCH 01/11] Add support for Broadlink RM Max (0xAF8B) Place in the rmpro class which uses --- README.md | 2 +- broadlink/__init__.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a8babf31..c52ef895 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ behaviour, pin the original distribution (`broadlink==0.19.0`) instead. The following devices are supported: -- **Universal remotes**: RM home, RM mini 3, RM plus, RM pro, RM pro+, RM4 mini, RM4 pro, RM4C mini, RM4S, RM4 TV mate +- **Universal remotes**: RM home, RM mini 3, RM plus, RM pro, RM pro+, RM4 mini, RM4 pro, RM4C mini, RM4S, RM4 TV mate, RM Max - **Smart plugs**: SP mini, SP mini 3, SP mini+, SP1, SP2, SP2-BR, SP2-CL, SP2-IN, SP2-UK, SP3, SP3-EU, SP3S-EU, SP3S-US, SP4L-AU, SP4L-EU, SP4L-UK, SP4M, SP4M-US, Ankuoo NEO, Ankuoo NEO PRO, Efergy Ego, BG AHC/U-01 - **Switches**: MCB1, SC1, SCB1E, SCB2 - **Outlets**: BG 800, BG 900 diff --git a/broadlink/__init__.py b/broadlink/__init__.py index bd77fa2a..9b14542b 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -112,6 +112,7 @@ 0x27A6: ("RM plus", "Broadlink"), 0x27A9: ("RM pro+", "Broadlink"), 0x27C3: ("RM pro+", "Broadlink"), + 0xAF8B: ("RM Max", "Broadlink"), }, rmminib: { 0x5F36: ("RM mini 3", "Broadlink"), From f051a262fa7707b6de02fd38c3bb9f4df9dcda51 Mon Sep 17 00:00:00 2001 From: Anil Daoud Date: Mon, 27 Oct 2025 22:24:41 +0800 Subject: [PATCH 02/11] Update __init__.py --- broadlink/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 9b14542b..ac5b2d3d 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -11,7 +11,7 @@ from .device import Device, _open_endpoint, ping, scan from .hub import s3 from .light import lb1, lb2 -from .remote import rm, rm4, rm4mini, rm4pro, rmmini, rmminib, rmpro +from .remote import rm, rm4, rm4mini, rm4pro, rm5plus, rmmini, rmminib, rmpro from .sensor import a1, a2 from .switch import bg1, ehc31, mp1, mp1s, sp1, sp2, sp2s, sp3, sp3s, sp4, sp4b @@ -148,6 +148,9 @@ 0x649B: ("RM4 pro", "Broadlink"), 0x653C: ("RM4 pro", "Broadlink"), }, + rm5plus: { + 0x5224: ("RM5 plus", "Broadlink"), + }, a1: { 0x2714: ("A1", "Broadlink"), }, From b920497394b81e3f18409b988d38140252705c90 Mon Sep 17 00:00:00 2001 From: Anil Daoud Date: Mon, 27 Oct 2025 22:26:15 +0800 Subject: [PATCH 03/11] Update remote.py --- broadlink/remote.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/broadlink/remote.py b/broadlink/remote.py index 64103882..64bebffc 100644 --- a/broadlink/remote.py +++ b/broadlink/remote.py @@ -171,3 +171,8 @@ class rm4(rm4pro): """For backwards compatibility.""" TYPE = "RM4" + +class rm5plus(rmminib): + """Controls a Broadlink RM5 Plus.""" + + TYPE = "RM5PLUS" From b8ac3178e768db54d1c2beb67be1cbc71fe45219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Noga=C5=9B?= Date: Thu, 27 Mar 2025 02:54:01 +0100 Subject: [PATCH 04/11] Add an OEM device code for RM mini 3 --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index ac5b2d3d..466ca46f 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -97,6 +97,7 @@ 0x27D3: ("RM mini 3", "Broadlink"), 0x27DC: ("RM mini 3", "Broadlink"), 0x27DE: ("RM mini 3", "Broadlink"), + 0xA544: ("RM mini 3", "Broadlink (OEM)"), }, rmpro: { 0x2712: ("RM pro/pro+", "Broadlink"), From 6654dcb023df3054315a9c054c49d4b3df7afe84 Mon Sep 17 00:00:00 2001 From: shuxin Date: Thu, 9 May 2024 03:17:48 +0800 Subject: [PATCH 05/11] support RM mini 3 cmcc version support RM mini 3 cmcc version --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 466ca46f..351e3240 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -90,6 +90,7 @@ 0x27B7: ("RM mini 3", "Broadlink"), 0x27C2: ("RM mini 3", "Broadlink"), 0x27C7: ("RM mini 3", "Broadlink"), + 0x27C8: ("RM mini 3", "Broadlink"), #cmcc version 0x27CC: ("RM mini 3", "Broadlink"), 0x27CD: ("RM mini 3", "Broadlink"), 0x27D0: ("RM mini 3", "Broadlink"), From c23a854859b9b051b599ac4e6d2821441c95f3b4 Mon Sep 17 00:00:00 2001 From: techitapart <70172453+techitapart@users.noreply.github.com> Date: Fri, 20 Sep 2024 20:48:52 -0400 Subject: [PATCH 06/11] Added support for another type of LB26 R1 --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 351e3240..de428928 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -184,6 +184,7 @@ 0xA4F4: ("LB27 R1", "Broadlink"), 0xA5F7: ("LB27 R1", "Broadlink"), 0xA6EF: ("EFCF60WSMT", "Luceco"), + 0xA517: ("LB26 R1", "Broadlink"), }, S1C: { 0x2722: ("S2KIT", "Broadlink"), From 775aa5fcbb075f278423123aaebf378a52d979ac Mon Sep 17 00:00:00 2001 From: bbcbbk <44605459+bbcbbk@users.noreply.github.com> Date: Sat, 15 Jun 2024 21:39:40 +0800 Subject: [PATCH 07/11] Update __init__.py add 0x7d15 SP mini 3-AL --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index de428928..517fdb7c 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -72,6 +72,7 @@ 0xA589: ("SP4L-UK", "Broadlink"), 0xA5D3: ("SP4L-EU", "Broadlink"), 0xA6F4: ("SP4D-US", "Broadlink"), + 0x7d15: ("SP mini 3-AL", "Broadlink (OEM)"), }, sp4b: { 0x5115: ("SCB1E", "Broadlink"), From c291b9e7651655a37cf24ea4089e104f19b2a844 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel Date: Wed, 17 Apr 2024 06:45:34 -0300 Subject: [PATCH 08/11] Add support for LEDVANCE SMART+ WIFI CEILING TW 24W (0x6498) --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 517fdb7c..fd7690b8 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -180,6 +180,7 @@ 0x644C: ("LB27 R1", "Broadlink"), 0x644E: ("LB26 R1", "Broadlink"), 0x6488: ("LB27 C1", "Broadlink"), + 0x6498: ("SMART+ WIFI CEILING TW 24W", "LEDVANCE"), }, lb2: { 0xA4F4: ("LB27 R1", "Broadlink"), From d732283bab3b06be12427f6476146f86ec0cc06c Mon Sep 17 00:00:00 2001 From: David <128871138+DAB-LABS@users.noreply.github.com> Date: Fri, 7 Aug 2026 00:53:53 -0700 Subject: [PATCH 09/11] Refactor tick parameter to use constant value --- broadlink/remote.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/broadlink/remote.py b/broadlink/remote.py index 64bebffc..7edc7f9a 100644 --- a/broadlink/remote.py +++ b/broadlink/remote.py @@ -6,7 +6,10 @@ from .device import Device -def pulses_to_data(pulses: List[int], tick: float = 32.84) -> bytes: +TICK = 8192 / 269 # 30.4535 us -- Broadlink's tick, per protocol.md (fixes #839) + + +def pulses_to_data(pulses: List[int], tick: float = TICK) -> bytes: """Convert a microsecond duration sequence into a Broadlink IR packet.""" result = bytearray(4) result[0x00] = 0x26 @@ -25,7 +28,7 @@ def pulses_to_data(pulses: List[int], tick: float = 32.84) -> bytes: return result -def data_to_pulses(data: bytes, tick: float = 32.84) -> List[int]: +def data_to_pulses(data: bytes, tick: float = TICK) -> List[int]: """Parse a Broadlink packet into a microsecond duration sequence.""" result = [] index = 4 From b490e5a6eb8e249cc3ff67c142ca7abbd3d113bf Mon Sep 17 00:00:00 2001 From: David <128871138+DAB-LABS@users.noreply.github.com> Date: Fri, 7 Aug 2026 00:55:43 -0700 Subject: [PATCH 10/11] Add tests for TICK constant in remote module Add unit tests for the TICK constant in remote module to ensure accuracy against protocol.md examples and validate behavior with real hardware. --- tests/test_remote.py | 63 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 tests/test_remote.py diff --git a/tests/test_remote.py b/tests/test_remote.py new file mode 100644 index 00000000..c0f7f06a --- /dev/null +++ b/tests/test_remote.py @@ -0,0 +1,63 @@ +"""Tests for the tick constant used by pulses_to_data / data_to_pulses (GH #839).""" +import unittest + +from broadlink.remote import TICK, data_to_pulses, pulses_to_data + +OLD_TICK = 32.84 # the constant this PR replaces + + +class TestTickConstant(unittest.TestCase): + """TICK must match protocol.md's worked examples (its "us * 269 / 8192" formula).""" + + def test_tick_matches_protocol_md(self): + # protocol.md's literal, verified-by-example formula. + self.assertAlmostEqual(TICK, 8192 / 269, places=4) + + def test_protocol_md_worked_examples(self): + # protocol.md's own worked examples, applying its own formula + # (us * 269 / 8192) literally: 8920 us -> 0x124 (292 ticks), + # 4450 us -> 0x92 (146 ticks). TICK = 8192 / 269 reproduces both + # exactly; the alternative reading "2^-15 s" (1e6 / 2**15, + # 30.5176 us) is 0.2% different and lands one tick short on the + # second example under floor division. See PR discussion for why + # 8192/269 is the better-evidenced choice pending hardware bench. + for us, expected_ticks in ((8920, 292), (4450, 146)): + got = int(us // TICK) + self.assertLessEqual(abs(got - expected_ticks), 1) + old_got = int(us // OLD_TICK) + self.assertGreater(abs(old_got - expected_ticks), 5) + + def test_round_trip(self): + # Learn-then-send is unaffected by which tick is used, as long as + # both directions agree -- this must hold for TICK just as it held + # for the old constant. + pulses = [9000, 4500, 560, 1690, 560, 560] + packet = pulses_to_data(pulses) + decoded = data_to_pulses(packet) + for original, result in zip(pulses, decoded): + self.assertAlmostEqual(result, original, delta=TICK) + + def test_true_microsecond_nec_leader_is_now_correct(self): + # A real NEC leader (9000/4500 us) built from TRUE microseconds + # (e.g. Home Assistant's infrared platform, not a Broadlink round + # trip) must decode back to ~9000/4500, not ~7% short. + packet = pulses_to_data([9000, 4500]) + decoded = data_to_pulses(packet) + self.assertAlmostEqual(decoded[0], 9000, delta=50) + self.assertAlmostEqual(decoded[1], 4500, delta=50) + + def test_old_constant_was_seven_percent_short_on_real_hardware(self): + # The silicon's timebase is fixed regardless of what the software + # assumed, so what the old code actually put on the wire for a + # true-microsecond input is tick_count * TICK, not + # tick_count * OLD_TICK. This reproduces the ~7% figure from the + # issue's hardware bench (8362us/8437us measured vs ~9000/9116us + # true, same ballpark once packet framing rounding is folded in). + buggy_packet = pulses_to_data([9000, 4500], tick=OLD_TICK) + actually_transmitted = data_to_pulses(buggy_packet, tick=TICK) + self.assertLess(actually_transmitted[0], 9000 - 500) + self.assertLess(actually_transmitted[1], 4500 - 250) + + +if __name__ == "__main__": + unittest.main() From 1c48b03b8ebcecf9795f86a2fd7d173dc142495f Mon Sep 17 00:00:00 2001 From: DAB-LABS <128871138+DAB-LABS@users.noreply.github.com> Date: Fri, 4 Sep 2026 20:25:15 +0000 Subject: [PATCH 11/11] Round pulses to the nearest tick, add issue-reported device IDs, update tests and changelog Follow-ups to the carried-over commits: - pulses_to_data rounds instead of truncating, so a duration that is 0.9 of a tick no longer becomes zero ticks. - TICK gets a docstring explaining the 32768 Hz timebase and the history of the 32.84 value; the import block is sorted for ruff. - 0x4EDA MP1-1K3S2U (#816) and 0xA57A SP4 (#758) added from issues, by family; 0x7D15 and 0x27C8 entries tidied into hex order and house style. - cryptography floor raised to 43, the first release with 3.13 wheels (supersedes #749). - tests/test_helpers.py re-pinned to the new tick and rounding; the old 32.84 pins are gone. - README device list and CHANGELOG updated. --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ README.md | 6 +++--- broadlink/__init__.py | 6 ++++-- broadlink/remote.py | 15 ++++++++++++--- pyproject.toml | 2 +- tests/test_helpers.py | 38 +++++++++++++++++++++++++++----------- 6 files changed, 74 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b44e43a..fb74a4b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,8 +39,35 @@ history below starts at that fork point. 3.14, and builds the sdist and wheel on every pull request. Releases are published to PyPI from version tags using trusted publishing. +### Fixed + +- The IR tick constant used by `pulses_to_data` and `data_to_pulses` is now + `TICK = 8192 / 269` (about 30.45 us), matching the device's 32768 Hz + timebase as documented in `protocol.md`. The previous value, 32.84, was + the inverse ratio applied the wrong way round and compressed IR codes + built from true microsecond timings by about 7 percent. Codes learned and + replayed through the same device were unaffected. Verified on an RM4 Pro + against an independent receiver in both directions. + (mjg59/python-broadlink#839, #841) +- `pulses_to_data` rounds each duration to the nearest tick instead of + truncating, which removes up to one tick of systematic shortening per + pulse. + ### Added +- Devices, carried over from pull requests against the original repository + with their authors' commits intact: RM Max 0xAF8B (#838, Alexey Masolov); + RM5 plus 0x5224 with a new `rm5plus` class (#831, Anil Daoud); RM mini 3 + OEM 0xA544 (#823, Bartłomiej Nogaś); RM mini 3 CMCC 0x27C8 (#802, + shuxin); LB26 R1 0xA517 (#812, techitapart); SP mini 3-AL 0x7D15 (#805, + bbcbbk); LEDVANCE SMART+ WIFI CEILING TW 24W 0x6498 (#799, Felipe Martins + Diel). +- Devices reported in issues against the original repository, added by + model name to the existing class for that family and not yet confirmed on + hardware: MP1-1K3S2U 0x4EDA (#816) and SP4 0xA57A (#758). Please open an + issue if either does not behave. +- `cryptography` 43 or newer is required, the first release with wheels for + Python 3.13 (supersedes mjg59/python-broadlink#749). - A test suite. The `tests/oracle` package records the exact request bytes every public method of every device class sends, and the results it decodes from canned responses, so that later changes to the transport diff --git a/README.md b/README.md index c52ef895..f3fe226f 100644 --- a/README.md +++ b/README.md @@ -40,14 +40,14 @@ behaviour, pin the original distribution (`broadlink==0.19.0`) instead. The following devices are supported: -- **Universal remotes**: RM home, RM mini 3, RM plus, RM pro, RM pro+, RM4 mini, RM4 pro, RM4C mini, RM4S, RM4 TV mate, RM Max -- **Smart plugs**: SP mini, SP mini 3, SP mini+, SP1, SP2, SP2-BR, SP2-CL, SP2-IN, SP2-UK, SP3, SP3-EU, SP3S-EU, SP3S-US, SP4L-AU, SP4L-EU, SP4L-UK, SP4M, SP4M-US, Ankuoo NEO, Ankuoo NEO PRO, Efergy Ego, BG AHC/U-01 +- **Universal remotes**: RM home, RM mini 3, RM plus, RM pro, RM pro+, RM4 mini, RM4 pro, RM4C mini, RM4S, RM4 TV mate, RM Max, RM5 plus +- **Smart plugs**: SP mini, SP mini 3, SP mini+, SP1, SP2, SP2-BR, SP2-CL, SP2-IN, SP2-UK, SP3, SP3-EU, SP3S-EU, SP3S-US, SP4L-AU, SP4L-EU, SP4L-UK, SP4M, SP4M-US, SP mini 3-AL, Ankuoo NEO, Ankuoo NEO PRO, Efergy Ego, BG AHC/U-01 - **Switches**: MCB1, SC1, SCB1E, SCB2 - **Outlets**: BG 800, BG 900 - **Power strips**: MP1-1K3S2U, MP1-1K4S, MP2 - **Environment sensors**: A1 - **Alarm kits**: S1C, S2KIT -- **Light bulbs**: LB1, LB26 R1, LB27 R1, SB800TD +- **Light bulbs**: LB1, LB26 R1, LB27 R1, SB800TD, LEDVANCE SMART+ WIFI CEILING TW 24W - **Curtain motors**: Dooya DT360E-45/20 - **Thermostats**: Hysen HY02B05H - **Hubs**: S3 diff --git a/broadlink/__init__.py b/broadlink/__init__.py index fd7690b8..eab43e72 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -63,6 +63,7 @@ 0x7583: ("SP mini 3", "Broadlink"), 0x7587: ("SP4L-UK", "Broadlink"), 0x7D11: ("SP mini 3", "Broadlink"), + 0x7D15: ("SP mini 3-AL", "Broadlink (OEM)"), 0xA4F9: ("WS4", "Broadlink (OEM)"), 0xA569: ("SP4L-UK", "Broadlink"), 0xA56A: ("MCB1", "Broadlink"), @@ -71,8 +72,8 @@ 0xA576: ("SP4L-AU", "Broadlink"), 0xA589: ("SP4L-UK", "Broadlink"), 0xA5D3: ("SP4L-EU", "Broadlink"), + 0xA57A: ("SP4", "Broadlink"), 0xA6F4: ("SP4D-US", "Broadlink"), - 0x7d15: ("SP mini 3-AL", "Broadlink (OEM)"), }, sp4b: { 0x5115: ("SCB1E", "Broadlink"), @@ -91,7 +92,7 @@ 0x27B7: ("RM mini 3", "Broadlink"), 0x27C2: ("RM mini 3", "Broadlink"), 0x27C7: ("RM mini 3", "Broadlink"), - 0x27C8: ("RM mini 3", "Broadlink"), #cmcc version + 0x27C8: ("RM mini 3", "Broadlink"), # CMCC version 0x27CC: ("RM mini 3", "Broadlink"), 0x27CD: ("RM mini 3", "Broadlink"), 0x27D0: ("RM mini 3", "Broadlink"), @@ -162,6 +163,7 @@ }, mp1: { 0x4EB5: ("MP1-1K4S", "Broadlink"), + 0x4EDA: ("MP1-1K3S2U", "Broadlink"), 0x4F1B: ("MP1-1K3S2U", "Broadlink (OEM)"), 0x4F65: ("MP1-1K3S2U", "Broadlink"), }, diff --git a/broadlink/remote.py b/broadlink/remote.py index 7edc7f9a..2aa3c464 100644 --- a/broadlink/remote.py +++ b/broadlink/remote.py @@ -1,12 +1,20 @@ """Support for universal remotes.""" + import struct from typing import List, Optional, Tuple from . import exceptions as e from .device import Device +TICK = 8192 / 269 +"""Duration of one Broadlink timing unit in microseconds (about 30.45 us). -TICK = 8192 / 269 # 30.4535 us -- Broadlink's tick, per protocol.md (fixes #839) +The RM firmware counts pulses on a 32768 Hz clock (protocol.md: us * 269 / 8192). +Earlier releases used 32.84, the inverse of the right ratio applied the wrong +way round, which compressed externally sourced IR codes by about 7 percent +(mjg59/python-broadlink#839). Codes learned and replayed through the same +device were unaffected because both directions shared the constant. +""" def pulses_to_data(pulses: List[int], tick: float = TICK) -> bytes: @@ -15,7 +23,7 @@ def pulses_to_data(pulses: List[int], tick: float = TICK) -> bytes: result[0x00] = 0x26 for pulse in pulses: - div, mod = divmod(int(pulse // tick), 256) + div, mod = divmod(round(pulse / tick), 256) if div: result.append(0) result.append(div) @@ -175,7 +183,8 @@ class rm4(rm4pro): TYPE = "RM4" + class rm5plus(rmminib): """Controls a Broadlink RM5 Plus.""" - + TYPE = "RM5PLUS" diff --git a/pyproject.toml b/pyproject.toml index 7c223b33..52abb677 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ classifiers = [ "Topic :: Home Automation", ] dependencies = [ - "cryptography>=3.2", + "cryptography>=43", ] [project.optional-dependencies] diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 04629daf..27db245f 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -8,36 +8,52 @@ from broadlink.helpers import CRC16 from broadlink.protocol import Datetime -from broadlink.remote import data_to_pulses, pulses_to_data +from broadlink.remote import TICK, data_to_pulses, pulses_to_data -# NOTE: these pin the 0.19.0 behavior of the pulse helpers, including the -# 32.84 tick that upstream issue #839 identifies as wrong. They are expected -# to change, deliberately and in the same pull request, when the tick fix -# lands; until then they document what shipped. + +def test_tick_constant(): + # 32768 Hz timebase: protocol.md's "us * 269 / 8192". + assert TICK == pytest.approx(8192 / 269) + assert TICK == pytest.approx(30.4535, abs=1e-4) def test_pulses_to_data_header_and_short_pulses(): - data = pulses_to_data([328, 656], tick=32.84) + data = pulses_to_data([328, 656]) assert data[0] == 0x26 assert data[1] == 0x00 assert int.from_bytes(data[2:4], "little") == 2 - assert data[4:] == bytes([9, 19]) # floor(328/32.84)=9, floor(656/32.84)=19 + # round(328/30.4535)=11, round(656/30.4535)=22 + assert data[4:] == bytes([11, 22]) + + +def test_pulses_to_data_rounds_to_nearest_tick(): + # 0.6 of a tick rounds up; 0.4 rounds down. The old code floored both. + assert pulses_to_data([TICK * 10.6])[4] == 11 + assert pulses_to_data([TICK * 10.4])[4] == 10 def test_pulses_to_data_long_pulse_uses_three_byte_form(): - data = pulses_to_data([10000], tick=32.84) - ticks = int(10000 // 32.84) # 304 + data = pulses_to_data([10000]) + ticks = round(10000 / TICK) # 328 + assert ticks > 255 assert data[4:] == bytes([0, ticks >> 8, ticks & 0xFF]) assert int.from_bytes(data[2:4], "little") == 3 +def test_explicit_tick_argument_still_honored(): + # Callers may still pass their own tick. + assert pulses_to_data([328, 656], tick=32.84)[4:] == bytes([10, 20]) + assert data_to_pulses(bytes([0x26, 0, 1, 0, 10]), tick=32.84) == [328] + + def test_data_to_pulses_round_trip_at_same_tick(): pulses = [9000, 4500, 560, 560, 560, 1690, 40000] data = pulses_to_data(pulses) back = data_to_pulses(data) - # Both directions use the same tick, so the round trip lands within a tick. + # Rounding on the way in (and int() on the way out) keeps the round + # trip within half a tick plus one microsecond. for a, b in zip(pulses, back, strict=True): - assert abs(a - b) <= 33 + assert abs(a - b) <= TICK / 2 + 1 def test_data_to_pulses_honors_declared_length():