Skip to content

Add capture() and capture_rf() - #842

Closed
DAB-LABS wants to merge 4 commits into
mjg59:masterfrom
DAB-LABS:capture
Closed

Add capture() and capture_rf()#842
DAB-LABS wants to merge 4 commits into
mjg59:masterfrom
DAB-LABS:capture

Conversation

@DAB-LABS

@DAB-LABS DAB-LABS commented Sep 5, 2026

Copy link
Copy Markdown

A learning session on these remotes is arm, poll, wait, re-arm, one code at a time, on a device that leaves learning mode without saying so and whose own transmissions end the session. capture() and capture_rf() own that loop and hand back clean signals, so a consumer subscribes and reads instead of reimplementing the dance the remote platform and the receiver PR each did separately.

capture(window, stop_after_first, poll_interval, rearm_interval) is an async generator yielding CapturedSignal. It re-arms on a timer (15 s, under the 25 to 40 s the device was measured to hold a session) and after any send_data, since a send ends the session. One window per device. capture_rf(window, frequency, ...) on the Pro models takes the carrier directly, or sweeps for it when not given; the sweep is unreliable on some firmware, so the known-frequency path is primary. CapturedSignal carries the device packet, the pulses at the corrected tick, the kind, the repeat count, and for RF the carrier the packet does not itself record.

Packet helpers: pulses_to_data gains kind and repeat, parse_packet is the inverse, SignalKind names the bands. A returned RF packet does not always use the canonical type byte (an RM4 Pro answers a 433 MHz capture with 0xB1, not 0xB2), so the kind is read by band and a capture is tagged from what it armed rather than the byte.

Benched live on an RM4 Pro: IR single and multi-capture confirmed, the 25 to 40 s session limit and the send-ends-session behaviour measured, the 0xB1 RF byte and the flaky sweep observed. 242 tests on 3.13 and 3.14; the transport oracle fixtures are unchanged.

DAB-LABS and others added 4 commits September 4, 2026 11:39
Move packaging to pyproject.toml as the python-broadlink distribution
(import name unchanged), require Python 3.13 or newer, and replace the
flake8 workflow with ruff and pytest on 3.13 and 3.14 plus an sdist and
wheel build. Add a trusted-publishing workflow for version tags.

Add tests/oracle: a harness that records the exact request bytes every
public method of every device class sends and the result it decodes
from canned responses, frozen in fixtures.json (155 cases), plus
transport tests for send_packet framing, checksums, auth, discovery,
gendevice and setup, and tests for the pure helpers. No library
behavior changes; three import blocks were reordered for ruff.

Add a README note explaining the fork and a CHANGELOG.
Every method that reaches a device is now a coroutine, with the same
names, arguments and return values as before. Discovery, hello and
setup are coroutines and xdiscover is an async generator. The packet,
CRC and datetime helpers stay synchronous. There is no synchronous
compatibility layer.

Transport: each device keeps one UDP endpoint (asyncio
DatagramProtocol) for its lifetime and serializes requests on it with
an asyncio.Lock; the previous code opened a socket per call and
declared a lock it never acquired. Retry and timeout behaviour is
unchanged. An expired session key is re-authenticated once and the
request repeated. async with / aclose() release the endpoint.

Device classes are a mechanical port (async def and await); the
oracle suite recorded in the previous change passes unchanged, so
every method sends the same bytes and decodes the same results as
0.19.0. Transport tests use a fake endpoint and gain cases for lock
serialization, endpoint reuse, stale-reply draining and re-auth.
The CLI runs under asyncio.run. README and CHANGELOG describe the
break.

Live-checked against an RM4 Pro: discovery, hello, auth, sensors,
concurrent calls, learning primitives, send, and the timeout path.
* Add support for Broadlink RM Max (0xAF8B)

Place in the rmpro class which uses <I framing for IR learn/send
commands, matching the device's actual protocol. The rm4pro class
uses <HI framing which causes "device is locked" errors on this
hardware.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Update __init__.py

* Update remote.py

* Add an OEM device code for RM mini 3

* support RM mini 3 cmcc version

support RM mini 3 cmcc version

* Added support for another type of LB26 R1

* Update __init__.py

add 0x7d15 SP mini 3-AL

* Add support for LEDVANCE SMART+ WIFI CEILING TW 24W (0x6498)

* Refactor tick parameter to use constant value

* 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.

* 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 (mjg59#816) and 0xA57A SP4 (mjg59#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 mjg59#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.

---------

Co-authored-by: Alexey Masolov <amasolov@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Anil Daoud <anil.daoud@protonmail.com>
Co-authored-by: Bartłomiej Nogaś <bart.nogas@gmail.com>
Co-authored-by: shuxin <shuxin@users.noreply.github.com>
Co-authored-by: techitapart <70172453+techitapart@users.noreply.github.com>
Co-authored-by: bbcbbk <44605459+bbcbbk@users.noreply.github.com>
Co-authored-by: Felipe Martins Diel <felipe-diel@hotmail.com>
A learning session is arm, poll, wait, re-arm, one code at a time, with a
device that leaves learning mode without saying so and a send that ends the
session. capture() and capture_rf() own that loop and hand back clean
signals, so a consumer subscribes and reads instead of reimplementing the
dance (as the remote platform, the receiver PR and others each did).

- capture(window, stop_after_first, poll_interval, rearm_interval): async
  generator yielding CapturedSignal. Re-arms on a timer (default 15 s, under
  the 25-40 s the device was measured to hold a session) and after any
  send_data (a send ends the session; both from the bench). One window per
  device; a second raises CaptureInProgressError.
- capture_rf(window, frequency, ...) on the Pro classes: takes the carrier
  directly, or sweeps for it when not given. The sweep is unreliable on some
  firmware, so the known-frequency path is primary.
- CapturedSignal: device packet, pulses at the corrected tick, kind, repeat,
  and the RF carrier the packet does not itself record.
- pulses_to_data gains kind and repeat; parse_packet is the inverse;
  SignalKind names the bands. A returned RF packet does not always carry the
  canonical type byte (an RM4 Pro sends 0xB1 for 433 MHz), so kind is read by
  band and a capture is tagged from what it armed, never dropped on the byte.
- One shared front-end lock and a transmit generation counter already live on
  the device; capture reads the counter so a concurrent send re-arms the
  window.

Tests drive the loops against a scripted device that models the bench
findings; the transport oracle fixtures are unchanged.
@DAB-LABS DAB-LABS closed this Sep 5, 2026
@DAB-LABS

DAB-LABS commented Sep 5, 2026

Copy link
Copy Markdown
Author

Opened against the wrong repository by mistake; this work lives in the DAB-LABS fork.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant