Skip to content

Commit a41f819

Browse files
committed
Add commands
1 parent c6794d7 commit a41f819

File tree

4 files changed

+255
-2
lines changed

4 files changed

+255
-2
lines changed

infrared_protocols/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""Library to decode and encode infrared signals."""
2+
3+
from .commands import Command, NECCommand, Timing
4+
5+
__all__ = [
6+
"Command",
7+
"NECCommand",
8+
"Timing",
9+
]

infrared_protocols/commands.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
"""Common IR command definitions."""
2+
3+
import abc
4+
from dataclasses import dataclass
5+
from typing import override
6+
7+
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+
16+
class Command(abc.ABC):
17+
"""Base class for IR commands."""
18+
19+
repeat_count: int
20+
modulation: int
21+
22+
def __init__(self, *, modulation: int, repeat_count: int = 0) -> None:
23+
"""Initialize the IR command."""
24+
self.modulation = modulation
25+
self.repeat_count = repeat_count
26+
27+
@abc.abstractmethod
28+
def get_raw_timings(self) -> list[Timing]:
29+
"""Get raw timings for the command."""
30+
31+
32+
class NECCommand(Command):
33+
"""NEC IR command."""
34+
35+
address: int
36+
command: int
37+
38+
def __init__(
39+
self, *, address: int, command: int, modulation: int, repeat_count: int = 0
40+
) -> None:
41+
"""Initialize the NEC IR command."""
42+
super().__init__(modulation=modulation, repeat_count=repeat_count)
43+
self.address = address
44+
self.command = command
45+
46+
@override
47+
def get_raw_timings(self) -> list[Timing]:
48+
"""Get raw timings for the NEC command.
49+
50+
NEC protocol timing (in microseconds):
51+
- Leader pulse: 9000µs high, 4500µs low
52+
- Logical '0': 562µs high, 562µs low
53+
- Logical '1': 562µs high, 1687µs low
54+
- End pulse: 562µs high
55+
- Repeat code: 9000µs high, 2250µs low, 562µs end pulse
56+
- Frame gap: ~96ms between end pulse and next frame (total frame ~108ms)
57+
58+
Data format (32 bits, LSB first):
59+
- Standard NEC: address (8-bit) + ~address (8-bit) + command (8-bit)
60+
+ ~command (8-bit)
61+
- Extended NEC: address_low (8-bit) + address_high (8-bit) + command (8-bit)
62+
+ ~command (8-bit)
63+
"""
64+
# NEC timing constants (microseconds)
65+
leader_high = 9000
66+
leader_low = 4500
67+
bit_high = 562
68+
zero_low = 562
69+
one_low = 1687
70+
repeat_low = 2250
71+
frame_gap = 96000 # Gap to make total frame ~108ms
72+
73+
timings: list[Timing] = [Timing(high_us=leader_high, low_us=leader_low)]
74+
75+
# Determine if standard (8-bit) or extended (16-bit) address
76+
if self.address <= 0xFF:
77+
# Standard NEC: address + inverted address
78+
address_low = self.address & 0xFF
79+
address_high = (~self.address) & 0xFF
80+
else:
81+
# Extended NEC: 16-bit address (no inversion)
82+
address_low = self.address & 0xFF
83+
address_high = (self.address >> 8) & 0xFF
84+
85+
command_byte = self.command & 0xFF
86+
command_inverted = (~self.command) & 0xFF
87+
88+
# Build 32-bit command data (LSB first in transmission)
89+
data = (
90+
address_low
91+
| (address_high << 8)
92+
| (command_byte << 16)
93+
| (command_inverted << 24)
94+
)
95+
96+
for _ in range(32):
97+
bit = data & 1
98+
if bit:
99+
timings.append(Timing(high_us=bit_high, low_us=one_low))
100+
else:
101+
timings.append(Timing(high_us=bit_high, low_us=zero_low))
102+
data >>= 1
103+
104+
# End pulse
105+
timings.append(Timing(high_us=bit_high, low_us=0))
106+
107+
# Add repeat codes if requested
108+
for _ in range(self.repeat_count):
109+
# Replace the last timing's low_us with the frame gap
110+
last_timing = timings[-1]
111+
timings[-1] = Timing(high_us=last_timing.high_us, low_us=frame_gap)
112+
113+
# Repeat code: leader burst + shorter space + end pulse
114+
timings.extend(
115+
[
116+
Timing(high_us=leader_high, low_us=repeat_low),
117+
Timing(high_us=bit_high, low_us=0),
118+
]
119+
)
120+
121+
return timings

pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,4 @@ max-complexity = 25
4444
typeCheckingMode = "standard"
4545

4646
[project.optional-dependencies]
47-
dev = ["prek"]
48-
47+
dev = ["pytest", "prek"]

tests/test_commands.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
"""Tests for the Infrared protocol definitions."""
2+
3+
from infrared_protocols import NECCommand, Timing
4+
5+
6+
def test_nec_command_get_raw_timings_standard() -> None:
7+
"""Test NEC command raw timings generation for standard 8-bit address."""
8+
expected_raw_timings = [
9+
Timing(high_us=9000, low_us=4500),
10+
Timing(high_us=562, low_us=562),
11+
Timing(high_us=562, low_us=562),
12+
Timing(high_us=562, low_us=1687),
13+
Timing(high_us=562, low_us=562),
14+
Timing(high_us=562, low_us=562),
15+
Timing(high_us=562, low_us=562),
16+
Timing(high_us=562, low_us=562),
17+
Timing(high_us=562, low_us=562),
18+
Timing(high_us=562, low_us=1687),
19+
Timing(high_us=562, low_us=1687),
20+
Timing(high_us=562, low_us=562),
21+
Timing(high_us=562, low_us=1687),
22+
Timing(high_us=562, low_us=1687),
23+
Timing(high_us=562, low_us=1687),
24+
Timing(high_us=562, low_us=1687),
25+
Timing(high_us=562, low_us=1687),
26+
Timing(high_us=562, low_us=562),
27+
Timing(high_us=562, low_us=562),
28+
Timing(high_us=562, low_us=562),
29+
Timing(high_us=562, low_us=1687),
30+
Timing(high_us=562, low_us=562),
31+
Timing(high_us=562, low_us=562),
32+
Timing(high_us=562, low_us=562),
33+
Timing(high_us=562, low_us=562),
34+
Timing(high_us=562, low_us=1687),
35+
Timing(high_us=562, low_us=1687),
36+
Timing(high_us=562, low_us=1687),
37+
Timing(high_us=562, low_us=562),
38+
Timing(high_us=562, low_us=1687),
39+
Timing(high_us=562, low_us=1687),
40+
Timing(high_us=562, low_us=1687),
41+
Timing(high_us=562, low_us=1687),
42+
Timing(high_us=562, low_us=0),
43+
]
44+
command = NECCommand(address=0x04, command=0x08, modulation=38000, repeat_count=0)
45+
timings = command.get_raw_timings()
46+
assert timings == expected_raw_timings
47+
48+
# Same command now with 2 repeats
49+
command_with_repeats = NECCommand(
50+
address=command.address,
51+
command=command.command,
52+
modulation=command.modulation,
53+
repeat_count=2,
54+
)
55+
timings_with_repeats = command_with_repeats.get_raw_timings()
56+
assert timings_with_repeats == [
57+
*expected_raw_timings[:-1],
58+
Timing(high_us=562, low_us=96000),
59+
Timing(high_us=9000, low_us=2250),
60+
Timing(high_us=562, low_us=96000),
61+
Timing(high_us=9000, low_us=2250),
62+
Timing(high_us=562, low_us=0),
63+
]
64+
65+
66+
def test_nec_command_get_raw_timings_extended() -> None:
67+
"""Test NEC command raw timings generation for extended 16-bit address."""
68+
expected_raw_timings = [
69+
Timing(high_us=9000, low_us=4500),
70+
Timing(high_us=562, low_us=1687),
71+
Timing(high_us=562, low_us=1687),
72+
Timing(high_us=562, low_us=562),
73+
Timing(high_us=562, low_us=1687),
74+
Timing(high_us=562, low_us=1687),
75+
Timing(high_us=562, low_us=1687),
76+
Timing(high_us=562, low_us=1687),
77+
Timing(high_us=562, low_us=1687),
78+
Timing(high_us=562, low_us=562),
79+
Timing(high_us=562, low_us=562),
80+
Timing(high_us=562, low_us=1687),
81+
Timing(high_us=562, low_us=562),
82+
Timing(high_us=562, low_us=562),
83+
Timing(high_us=562, low_us=562),
84+
Timing(high_us=562, low_us=562),
85+
Timing(high_us=562, low_us=562),
86+
Timing(high_us=562, low_us=562),
87+
Timing(high_us=562, low_us=562),
88+
Timing(high_us=562, low_us=562),
89+
Timing(high_us=562, low_us=1687),
90+
Timing(high_us=562, low_us=562),
91+
Timing(high_us=562, low_us=562),
92+
Timing(high_us=562, low_us=562),
93+
Timing(high_us=562, low_us=562),
94+
Timing(high_us=562, low_us=1687),
95+
Timing(high_us=562, low_us=1687),
96+
Timing(high_us=562, low_us=1687),
97+
Timing(high_us=562, low_us=562),
98+
Timing(high_us=562, low_us=1687),
99+
Timing(high_us=562, low_us=1687),
100+
Timing(high_us=562, low_us=1687),
101+
Timing(high_us=562, low_us=1687),
102+
Timing(high_us=562, low_us=0),
103+
]
104+
105+
command = NECCommand(address=0x04FB, command=0x08, modulation=38000, repeat_count=0)
106+
timings = command.get_raw_timings()
107+
assert timings == expected_raw_timings
108+
109+
# Same command now with 2 repeats
110+
command_with_repeats = NECCommand(
111+
address=command.address,
112+
command=command.command,
113+
modulation=command.modulation,
114+
repeat_count=2,
115+
)
116+
timings_with_repeats = command_with_repeats.get_raw_timings()
117+
assert timings_with_repeats == [
118+
*expected_raw_timings[:-1],
119+
Timing(high_us=562, low_us=96000),
120+
Timing(high_us=9000, low_us=2250),
121+
Timing(high_us=562, low_us=96000),
122+
Timing(high_us=9000, low_us=2250),
123+
Timing(high_us=562, low_us=0),
124+
]

0 commit comments

Comments
 (0)