Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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` |
Expand All @@ -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`.
Expand All @@ -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):
Expand All @@ -173,31 +173,33 @@ 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`.

---

## 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
Expand Down
3 changes: 1 addition & 2 deletions infrared_protocols/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
]
47 changes: 17 additions & 30 deletions infrared_protocols/commands.py
Original file line number Diff line number Diff line change
@@ -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."""

Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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:
Expand All @@ -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
166 changes: 85 additions & 81 deletions tests/test_commands.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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()
Expand All @@ -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,
]
Loading