Skip to content

Commit bcd728b

Browse files
balloobclaude
andauthored
refactor(api)!: replace Timing class with signed int timings (#19)
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4052688 commit bcd728b

4 files changed

Lines changed: 182 additions & 125 deletions

File tree

AGENTS.md

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ pytest tests/test_commands.py::test_nec_command_get_raw_timings_standard
7777
```
7878
infrared_protocols/ # Library source (only this directory is linted/type-checked)
7979
__init__.py # Public API: defines __all__ and re-exports
80-
commands.py # All domain logic: Command ABC, NECCommand, Timing
80+
commands.py # All domain logic: Command ABC, NECCommand
8181
tests/
8282
test_commands.py # pytest suite (all tests in one file)
8383
script/
@@ -111,7 +111,7 @@ pyproject.toml # Build config, ruff rules, pyright settings
111111
|---|---|---|
112112
| Files / modules | `snake_case` | `commands.py` |
113113
| Packages | `snake_case` | `infrared_protocols` |
114-
| Classes | `PascalCase` | `NECCommand`, `Timing` |
114+
| Classes | `PascalCase` | `NECCommand`, `Command` |
115115
| Functions / methods | `snake_case` | `get_raw_timings` |
116116
| Variables / attributes | `snake_case` | `high_us`, `repeat_count` |
117117
| Local numeric constants | `snake_case` (not `UPPER_CASE`) | `leader_high = 9000` |
@@ -125,11 +125,11 @@ pyproject.toml # Build config, ruff rules, pyright settings
125125
- Type checker: `basedpyright` with `typeCheckingMode = "standard"`.
126126
- **All** function parameters and return types must be annotated.
127127
- `-> None` must be explicit on `__init__` and void methods.
128-
- Use PEP 585 lowercase generics: `list[Timing]`, not `List[Timing]`.
128+
- Use PEP 585 lowercase generics: `list[int]`, not `List[int]`.
129129
- Use PEP 604 union syntax: `T | None`, not `Optional[T]`.
130130
- Use `@override` (from `typing`, Python 3.12+) on every overridden method.
131131
- No `Any`; avoid `cast`; prefer real type narrowing.
132-
- Inline variable annotations where needed: `timings: list[Timing] = []`.
132+
- Inline variable annotations where needed: `timings: list[int] = []`.
133133

134134
### Classes
135135
- Abstract base classes use `abc.ABC` and `@abc.abstractmethod`.
@@ -144,7 +144,7 @@ pyproject.toml # Build config, ruff rules, pyright settings
144144
parameter sections unless complexity demands it.
145145

146146
```python
147-
def get_raw_timings(self) -> list[Timing]:
147+
def get_raw_timings(self) -> list[int]:
148148
"""Get raw timings for the NEC command.
149149
150150
NEC protocol timing (in microseconds):
@@ -159,33 +159,36 @@ def get_raw_timings(self) -> list[Timing]:
159159

160160
### Adding a New Protocol
161161
1. Subclass `Command` (ABC) in `infrared_protocols/commands.py`.
162-
2. Implement `get_raw_timings(self) -> list[Timing]`.
162+
2. Implement `get_raw_timings(self) -> list[int]`.
163163
3. Decorate the override with `@override`.
164164
4. Define timing constants as local `snake_case` variables inside the method.
165165
5. Re-export the new class from `infrared_protocols/__init__.py` and add it to
166166
`__all__`.
167167

168168
### Key Abstractions
169-
- **`Timing(high_us, low_us)`** — frozen dataclass representing one pulse+space pair
170-
(microseconds). Immutable, comparable by value.
169+
- **Raw timings** — a flat `list[int]` of microsecond durations. Positive values are
170+
pulse (high) durations; negative values are space (low) durations. A transmission
171+
that ends on a pulse is represented by a trailing positive int with no paired
172+
space.
171173
- **`Command` (ABC)** — base class for all IR protocol encoders. Holds `modulation`
172174
and `repeat_count`.
173175
- **`NECCommand(Command)`** — encodes the NEC protocol; reference implementation.
174176

175177
### Patterns to Follow
176-
- Build timing lists by starting with a base list, appending in a loop, then using
177-
`extend()` for repeat frames.
178-
- Repeat-code frame gaps replace the last timing's `low_us` (see `NECCommand`).
178+
- Build timing lists by starting with a base list, appending pulse/space ints in a
179+
loop, then using `extend()` for repeat frames.
180+
- Repeat-code frame gaps are appended as a negative int directly after the preceding
181+
end-pulse (see `NECCommand`).
179182
- Bit manipulation uses masks like `data & 1`, `data >>= 1`, `(~x) & 0xFF`.
180183

181184
---
182185

183186
## Testing
184187

185188
- No mocking. Tests use pure value comparison against manually constructed
186-
`list[Timing]` fixtures.
189+
`list[int]` fixtures of signed pulse/space durations.
187190
- One assertion per logical case; reuse expected values with list unpacking
188-
(`[*expected[:-1], ...]`) rather than duplicating fixtures.
191+
(`[*expected, ...]`) rather than duplicating fixtures.
189192
- Tests live in `tests/test_commands.py`; keep them in one file unless the suite
190193
grows substantially.
191194
- Pre-commit hooks (`prek`) do **not** run on `tests/`; the type checker and linter

infrared_protocols/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
"""Library to decode and encode infrared signals."""
22

3-
from .commands import Command, NECCommand, Timing
3+
from .commands import Command, NECCommand
44

55
__all__ = [
66
"Command",
77
"NECCommand",
8-
"Timing",
98
]

infrared_protocols/commands.py

Lines changed: 12 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,9 @@
11
"""Common IR command definitions."""
22

33
import abc
4-
from dataclasses import dataclass
54
from typing import override
65

76

8-
@dataclass(frozen=True, slots=True)
9-
class Timing:
10-
"""High/low signal timing."""
11-
12-
high_us: int
13-
low_us: int
14-
15-
167
class Command(abc.ABC):
178
"""Base class for IR commands."""
189

@@ -25,8 +16,12 @@ def __init__(self, *, modulation: int, repeat_count: int = 0) -> None:
2516
self.repeat_count = repeat_count
2617

2718
@abc.abstractmethod
28-
def get_raw_timings(self) -> list[Timing]:
29-
"""Get raw timings for the command."""
19+
def get_raw_timings(self) -> list[int]:
20+
"""Get raw timings for the command.
21+
22+
Positive values are pulse (high) durations in microseconds; negative
23+
values are space (low) durations in microseconds.
24+
"""
3025

3126

3227
class NECCommand(Command):
@@ -49,7 +44,7 @@ def __init__(
4944
self.command = command
5045

5146
@override
52-
def get_raw_timings(self) -> list[Timing]:
47+
def get_raw_timings(self) -> list[int]:
5348
"""Get raw timings for the NEC command.
5449
5550
NEC protocol timing (in microseconds):
@@ -76,7 +71,7 @@ def get_raw_timings(self) -> list[Timing]:
7671
initial_frame_gap = 41000 # Gap to make total frame ~108ms
7772
frame_gap = 96000 # Gap to make total frame ~108ms
7873

79-
timings: list[Timing] = [Timing(high_us=leader_high, low_us=leader_low)]
74+
timings: list[int] = [leader_high, -leader_low]
8075

8176
# Determine if standard (8-bit) or extended (16-bit) address
8277
if self.address <= 0xFF:
@@ -101,29 +96,17 @@ def get_raw_timings(self) -> list[Timing]:
10196

10297
for _ in range(32):
10398
bit = data & 1
104-
if bit:
105-
timings.append(Timing(high_us=bit_high, low_us=one_low))
106-
else:
107-
timings.append(Timing(high_us=bit_high, low_us=zero_low))
99+
timings.append(bit_high)
100+
timings.append(-one_low if bit else -zero_low)
108101
data >>= 1
109102

110103
# End pulse
111-
timings.append(Timing(high_us=bit_high, low_us=0))
104+
timings.append(bit_high)
112105

113106
# Add repeat codes if requested
114107
gap = initial_frame_gap
115108
for _ in range(self.repeat_count):
116-
# Replace the last timing's low_us with the frame gap
117-
last_timing = timings[-1]
118-
timings[-1] = Timing(high_us=last_timing.high_us, low_us=gap)
109+
timings.extend([-gap, leader_high, -repeat_low, bit_high])
119110
gap = frame_gap # Use standard frame gap for subsequent repeats
120111

121-
# Repeat code: leader burst + shorter space + end pulse
122-
timings.extend(
123-
[
124-
Timing(high_us=leader_high, low_us=repeat_low),
125-
Timing(high_us=bit_high, low_us=0),
126-
]
127-
)
128-
129112
return timings

0 commit comments

Comments
 (0)