diff --git a/AGENTS.md b/AGENTS.md index 5fd245e..7457409 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -77,7 +77,7 @@ pytest tests/test_commands.py::test_nec_command_get_raw_timings_standard ``` infrared_protocols/ # Library source (only this directory is linted/type-checked) __init__.py # Public API: defines __all__ and re-exports - commands.py # All domain logic: Command ABC, NECCommand, Timing + commands.py # All domain logic: Command ABC, NECCommand loader.py # Flipper `.ir` parser + get_codes / CommandCollection codes/ # Bundled Flipper `.ir` code files (packaged as data) lg/tv.ir @@ -125,7 +125,7 @@ Python changes required. |---|---|---| | Files / modules | `snake_case` | `commands.py` | | Packages | `snake_case` | `infrared_protocols` | -| Classes | `PascalCase` | `NECCommand`, `Timing` | +| Classes | `PascalCase` | `NECCommand`, `CommandCollection` | | Functions / methods | `snake_case` | `get_raw_timings` | | Variables / attributes | `snake_case` | `high_us`, `repeat_count` | | Local numeric constants | `snake_case` (not `UPPER_CASE`) | `leader_high = 9000` | @@ -139,11 +139,11 @@ Python changes required. - Type checker: `basedpyright` with `typeCheckingMode = "standard"`. - **All** function parameters and return types must be annotated. - `-> None` must be explicit on `__init__` and void methods. -- Use PEP 585 lowercase generics: `list[Timing]`, not `List[Timing]`. +- Use PEP 585 lowercase generics: `list[int]`, not `List[int]`. - Use PEP 604 union syntax: `T | None`, not `Optional[T]`. - Use `@override` (from `typing`, Python 3.12+) on every overridden method. - No `Any`; avoid `cast`; prefer real type narrowing. -- Inline variable annotations where needed: `timings: list[Timing] = []`. +- Inline variable annotations where needed: `timings: list[int] = []`. ### Classes - Abstract base classes use `abc.ABC` and `@abc.abstractmethod`. @@ -158,7 +158,7 @@ Python changes required. parameter sections unless complexity demands it. ```python -def get_raw_timings(self) -> list[Timing]: +def get_raw_timings(self) -> list[int]: """Get raw timings for the NEC command. NEC protocol timing (in microseconds): @@ -173,23 +173,25 @@ def get_raw_timings(self) -> list[Timing]: ### Adding a New Protocol 1. Subclass `Command` (ABC) in `infrared_protocols/commands.py`. -2. Implement `get_raw_timings(self) -> list[Timing]`. +2. Implement `get_raw_timings(self) -> list[int]` (signed: +mark / -space µs). 3. Decorate the override with `@override`. 4. Define timing constants as local `snake_case` variables inside the method. 5. Re-export the new class from `infrared_protocols/__init__.py` and add it to `__all__`. ### Key Abstractions -- **`Timing(high_us, low_us)`** — frozen dataclass representing one pulse+space pair - (microseconds). Immutable, comparable by value. +- **Timings** — a flat `list[int]` of signed microseconds. Positive values are + marks (carrier on), negative values are spaces (carrier off). Lists alternate + mark/space starting with a mark; an unpaired trailing mark is allowed (and + conventional for an end pulse). - **`Command` (ABC)** — base class for all IR protocol encoders. Holds `modulation` and `repeat_count`. - **`NECCommand(Command)`** — encodes the NEC protocol; reference implementation. ### Patterns to Follow -- Build timing lists by starting with a base list, appending in a loop, then using - `extend()` for repeat frames. -- Repeat-code frame gaps replace the last timing's `low_us` (see `NECCommand`). +- Build timing lists by appending signed ints (positive mark, then negative space). +- Repeat-code frame gaps are appended as a single negative int that turns the + previous bare end-pulse mark into a mark+space pair (see `NECCommand`). - Bit manipulation uses masks like `data & 1`, `data >>= 1`, `(~x) & 0xFF`. --- @@ -197,7 +199,7 @@ def get_raw_timings(self) -> list[Timing]: ## Testing - No mocking. Tests use pure value comparison against manually constructed - `list[Timing]` fixtures. + `list[int]` fixtures. - One assertion per logical case; reuse expected values with list unpacking (`[*expected[:-1], ...]`) rather than duplicating fixtures. - Tests live in `tests/test_commands.py`; keep them in one file unless the suite diff --git a/infrared_protocols/__init__.py b/infrared_protocols/__init__.py index aa62615..a197595 100644 --- a/infrared_protocols/__init__.py +++ b/infrared_protocols/__init__.py @@ -1,13 +1,12 @@ """Library to decode and encode infrared signals.""" -from .commands import Command, NECCommand, Timing +from .commands import Command, NECCommand from .loader import CommandCollection, get_codes, parse_ir __all__ = [ "Command", "CommandCollection", "NECCommand", - "Timing", "get_codes", "parse_ir", ] diff --git a/infrared_protocols/commands.py b/infrared_protocols/commands.py index 9d9dfc0..576f09b 100644 --- a/infrared_protocols/commands.py +++ b/infrared_protocols/commands.py @@ -1,18 +1,9 @@ """Common IR command definitions.""" import abc -from dataclasses import dataclass from typing import override -@dataclass(frozen=True, slots=True) -class Timing: - """High/low signal timing.""" - - high_us: int - low_us: int - - class Command(abc.ABC): """Base class for IR commands.""" @@ -25,8 +16,12 @@ def __init__(self, *, modulation: int, repeat_count: int = 0) -> None: self.repeat_count = repeat_count @abc.abstractmethod - def get_raw_timings(self) -> list[Timing]: - """Get raw timings for the command.""" + def get_raw_timings(self) -> list[int]: + """Get raw timings in signed microseconds. + + Positive values are marks (carrier on); negative values are spaces + (carrier off). Timings alternate mark/space starting with a mark. + """ class NECCommand(Command): @@ -49,7 +44,7 @@ def __init__( self.command = command @override - def get_raw_timings(self) -> list[Timing]: + def get_raw_timings(self) -> list[int]: """Get raw timings for the NEC command. NEC protocol timing (in microseconds): @@ -76,7 +71,7 @@ def get_raw_timings(self) -> list[Timing]: initial_frame_gap = 41000 # Gap to make total frame ~108ms frame_gap = 96000 # Gap to make total frame ~108ms - timings: list[Timing] = [Timing(high_us=leader_high, low_us=leader_low)] + timings: list[int] = [leader_high, -leader_low] # Determine if standard (8-bit) or extended (16-bit) address if self.address <= 0xFF: @@ -100,30 +95,22 @@ def get_raw_timings(self) -> list[Timing]: ) for _ in range(32): - bit = data & 1 - if bit: - timings.append(Timing(high_us=bit_high, low_us=one_low)) - else: - timings.append(Timing(high_us=bit_high, low_us=zero_low)) + low = one_low if data & 1 else zero_low + timings.append(bit_high) + timings.append(-low) data >>= 1 - # End pulse - timings.append(Timing(high_us=bit_high, low_us=0)) + # Trailing end pulse (mark only; no following space). + timings.append(bit_high) # Add repeat codes if requested gap = initial_frame_gap for _ in range(self.repeat_count): - # Replace the last timing's low_us with the frame gap - last_timing = timings[-1] - timings[-1] = Timing(high_us=last_timing.high_us, low_us=gap) + # Convert the bare end-pulse mark into mark+space by appending the gap. + timings.append(-gap) gap = frame_gap # Use standard frame gap for subsequent repeats - # Repeat code: leader burst + shorter space + end pulse - timings.extend( - [ - Timing(high_us=leader_high, low_us=repeat_low), - Timing(high_us=bit_high, low_us=0), - ] - ) + # Repeat code: leader burst + shorter space + end pulse mark + timings.extend([leader_high, -repeat_low, bit_high]) return timings diff --git a/tests/test_commands.py b/tests/test_commands.py index 085dbe2..6d94d39 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1,46 +1,48 @@ """Tests for the Infrared protocol definitions.""" -from infrared_protocols import NECCommand, Timing +from infrared_protocols import NECCommand def test_nec_command_get_raw_timings_standard() -> None: """Test NEC command raw timings generation for standard 8-bit address.""" + # fmt: off expected_raw_timings = [ - Timing(high_us=9000, low_us=4500), - Timing(high_us=562, low_us=562), - Timing(high_us=562, low_us=562), - Timing(high_us=562, low_us=1687), - Timing(high_us=562, low_us=562), - Timing(high_us=562, low_us=562), - Timing(high_us=562, low_us=562), - Timing(high_us=562, low_us=562), - Timing(high_us=562, low_us=562), - Timing(high_us=562, low_us=1687), - Timing(high_us=562, low_us=1687), - Timing(high_us=562, low_us=562), - Timing(high_us=562, low_us=1687), - Timing(high_us=562, low_us=1687), - Timing(high_us=562, low_us=1687), - Timing(high_us=562, low_us=1687), - Timing(high_us=562, low_us=1687), - Timing(high_us=562, low_us=562), - Timing(high_us=562, low_us=562), - Timing(high_us=562, low_us=562), - Timing(high_us=562, low_us=1687), - Timing(high_us=562, low_us=562), - Timing(high_us=562, low_us=562), - Timing(high_us=562, low_us=562), - Timing(high_us=562, low_us=562), - Timing(high_us=562, low_us=1687), - Timing(high_us=562, low_us=1687), - Timing(high_us=562, low_us=1687), - Timing(high_us=562, low_us=562), - Timing(high_us=562, low_us=1687), - Timing(high_us=562, low_us=1687), - Timing(high_us=562, low_us=1687), - Timing(high_us=562, low_us=1687), - Timing(high_us=562, low_us=0), + 9000, -4500, + 562, -562, + 562, -562, + 562, -1687, + 562, -562, + 562, -562, + 562, -562, + 562, -562, + 562, -562, + 562, -1687, + 562, -1687, + 562, -562, + 562, -1687, + 562, -1687, + 562, -1687, + 562, -1687, + 562, -1687, + 562, -562, + 562, -562, + 562, -562, + 562, -1687, + 562, -562, + 562, -562, + 562, -562, + 562, -562, + 562, -1687, + 562, -1687, + 562, -1687, + 562, -562, + 562, -1687, + 562, -1687, + 562, -1687, + 562, -1687, + 562, ] + # fmt: on command = NECCommand(address=0x04, command=0x08, repeat_count=0) timings = command.get_raw_timings() assert timings == expected_raw_timings @@ -55,53 +57,55 @@ def test_nec_command_get_raw_timings_standard() -> None: ) timings_with_repeats = command_with_repeats.get_raw_timings() assert timings_with_repeats == [ - *expected_raw_timings[:-1], - Timing(high_us=562, low_us=41000), - Timing(high_us=9000, low_us=2250), - Timing(high_us=562, low_us=96000), - Timing(high_us=9000, low_us=2250), - Timing(high_us=562, low_us=0), + *expected_raw_timings, + -41000, + 9000, -2250, + 562, -96000, + 9000, -2250, + 562, ] def test_nec_command_get_raw_timings_extended() -> None: """Test NEC command raw timings generation for extended 16-bit address.""" + # fmt: off expected_raw_timings = [ - Timing(high_us=9000, low_us=4500), - Timing(high_us=562, low_us=1687), - Timing(high_us=562, low_us=1687), - Timing(high_us=562, low_us=562), - Timing(high_us=562, low_us=1687), - Timing(high_us=562, low_us=1687), - Timing(high_us=562, low_us=1687), - Timing(high_us=562, low_us=1687), - Timing(high_us=562, low_us=1687), - Timing(high_us=562, low_us=562), - Timing(high_us=562, low_us=562), - Timing(high_us=562, low_us=1687), - Timing(high_us=562, low_us=562), - Timing(high_us=562, low_us=562), - Timing(high_us=562, low_us=562), - Timing(high_us=562, low_us=562), - Timing(high_us=562, low_us=562), - Timing(high_us=562, low_us=562), - Timing(high_us=562, low_us=562), - Timing(high_us=562, low_us=562), - Timing(high_us=562, low_us=1687), - Timing(high_us=562, low_us=562), - Timing(high_us=562, low_us=562), - Timing(high_us=562, low_us=562), - Timing(high_us=562, low_us=562), - Timing(high_us=562, low_us=1687), - Timing(high_us=562, low_us=1687), - Timing(high_us=562, low_us=1687), - Timing(high_us=562, low_us=562), - Timing(high_us=562, low_us=1687), - Timing(high_us=562, low_us=1687), - Timing(high_us=562, low_us=1687), - Timing(high_us=562, low_us=1687), - Timing(high_us=562, low_us=0), + 9000, -4500, + 562, -1687, + 562, -1687, + 562, -562, + 562, -1687, + 562, -1687, + 562, -1687, + 562, -1687, + 562, -1687, + 562, -562, + 562, -562, + 562, -1687, + 562, -562, + 562, -562, + 562, -562, + 562, -562, + 562, -562, + 562, -562, + 562, -562, + 562, -562, + 562, -1687, + 562, -562, + 562, -562, + 562, -562, + 562, -562, + 562, -1687, + 562, -1687, + 562, -1687, + 562, -562, + 562, -1687, + 562, -1687, + 562, -1687, + 562, -1687, + 562, ] + # fmt: on command = NECCommand(address=0x04FB, command=0x08, modulation=38000, repeat_count=0) timings = command.get_raw_timings() @@ -116,10 +120,10 @@ def test_nec_command_get_raw_timings_extended() -> None: ) timings_with_repeats = command_with_repeats.get_raw_timings() assert timings_with_repeats == [ - *expected_raw_timings[:-1], - Timing(high_us=562, low_us=41000), - Timing(high_us=9000, low_us=2250), - Timing(high_us=562, low_us=96000), - Timing(high_us=9000, low_us=2250), - Timing(high_us=562, low_us=0), + *expected_raw_timings, + -41000, + 9000, -2250, + 562, -96000, + 9000, -2250, + 562, ]