@@ -93,17 +93,23 @@ def decode_bits(pulses):
93
93
"""Decode the pulses into bits."""
94
94
# pylint: disable=too-many-branches,too-many-statements
95
95
96
+ # TODO The name pulses is redefined several times below, so we'll stash the
97
+ # original in a separate variable for now. It might be worth refactoring to
98
+ # avoid redefining pulses, for the sake of readability.
99
+ input_pulses = pulses
100
+ pulses = list (pulses ) # Copy to avoid mutating input.
101
+
96
102
# special exception for NEC repeat code!
97
103
if (
98
104
(len (pulses ) == 3 )
99
105
and (8000 <= pulses [0 ] <= 10000 )
100
106
and (2000 <= pulses [1 ] <= 3000 )
101
107
and (450 <= pulses [2 ] <= 700 )
102
108
):
103
- raise IRNECRepeatException ( )
109
+ return NECRepeatIRMessage ( input_pulses )
104
110
105
111
if len (pulses ) < 10 :
106
- raise IRDecodeException ( "10 pulses minimum " )
112
+ return UnparseableIRMessage ( input_pulses , reason = "Too short " )
107
113
108
114
# Ignore any header (evens start at 1), and any trailer.
109
115
if len (pulses ) % 2 == 0 :
@@ -123,7 +129,7 @@ def decode_bits(pulses):
123
129
odd_bins = [b for b in odd_bins if b [1 ] > 1 ]
124
130
125
131
if not even_bins or not odd_bins :
126
- raise IRDecodeException ( "Not enough data" )
132
+ return UnparseableIRMessage ( input_pulses , reason = "Not enough data" )
127
133
128
134
if len (even_bins ) == 1 :
129
135
pulses = odds
@@ -132,12 +138,12 @@ def decode_bits(pulses):
132
138
pulses = evens
133
139
pulse_bins = even_bins
134
140
else :
135
- raise IRDecodeException ( "Both even/odd pulses differ" )
141
+ return UnparseableIRMessage ( input_pulses , reason = "Both even/odd pulses differ" )
136
142
137
143
if len (pulse_bins ) == 1 :
138
- raise IRDecodeException ( "Pulses do not differ" )
144
+ return UnparseableIRMessage ( input_pulses , reason = "Pulses do not differ" )
139
145
if len (pulse_bins ) > 2 :
140
- raise IRDecodeException ( "Only mark & space handled" )
146
+ return UnparseableIRMessage ( input_pulses , reason = "Only mark & space handled" )
141
147
142
148
mark = min (pulse_bins [0 ][0 ], pulse_bins [1 ][0 ])
143
149
space = max (pulse_bins [0 ][0 ], pulse_bins [1 ][0 ])
@@ -154,15 +160,112 @@ def decode_bits(pulses):
154
160
elif (mark * 0.75 ) <= pulse_length <= (mark * 1.25 ):
155
161
pulses [i ] = True
156
162
else :
157
- raise IRDecodeException ("Pulses outside mark/space" )
163
+ return UnparseableIRMessage (
164
+ input_pulses , reason = "Pulses outside mark/space"
165
+ )
158
166
159
167
# convert bits to bytes!
160
168
output = [0 ] * ((len (pulses ) + 7 ) // 8 )
161
169
for i , pulse_length in enumerate (pulses ):
162
170
output [i // 8 ] = output [i // 8 ] << 1
163
171
if pulse_length :
164
172
output [i // 8 ] |= 1
165
- return output
173
+ return IRMessage (input_pulses , code = output )
174
+
175
+
176
+ class BaseIRMessage :
177
+ "Contains the pulses that were parsed as one message."
178
+
179
+ def __init__ (self , pulses ):
180
+ # Stash an immutable copy of pulses.
181
+ self .pulses = tuple (pulses )
182
+
183
+ def __repr__ (self ):
184
+ return f"{ self .__class__ .__name__ } ({ self .pulses } )"
185
+
186
+
187
+ class IRMessage (BaseIRMessage ):
188
+ """
189
+ Message interpreted as bytes.
190
+
191
+ >>> m.code # the output of interest (the parsed bytes)
192
+ >>> m.pulses # the original pulses
193
+ """
194
+
195
+ def __init__ (self , pulses , * , code ):
196
+ super ().__init__ (pulses )
197
+ self .code = code
198
+
199
+ def __repr__ (self ):
200
+ return f"{ self .__class__ .__name__ } " f"(pulses={ self .pulses } , code={ self .code } )"
201
+
202
+
203
+ class UnparseableIRMessage (BaseIRMessage ):
204
+ "Message that could not be interpreted."
205
+
206
+ def __init__ (self , pulses , * , reason ):
207
+ super ().__init__ (pulses )
208
+ self .reason = reason
209
+
210
+ def __repr__ (self ):
211
+ return (
212
+ f"{ self .__class__ .__name__ } " f"(pulses={ self .pulses } , reason={ self .reason } )"
213
+ )
214
+
215
+
216
+ class NECRepeatIRMessage (BaseIRMessage ):
217
+ "Message interpreted as an NEC repeat code."
218
+ pass
219
+
220
+
221
+ class NonblockingGenericDecode :
222
+ """
223
+ Decode pulses into bytes in a non-blocking fashion.
224
+
225
+ :param ~pulseio.PulseIn input_pulses: Object to read pulses from
226
+ :param int max_pulse: Pulse duration to end a burst. Units are
227
+ microseconds.
228
+
229
+ >>> pulses = PulseIn(...)
230
+ >>> decoder = NonblockingGenericDecoder(pulses)
231
+ >>> for message in decoder.read():
232
+ ... if isinstace(message, IRMessage):
233
+ ... message.code # TA-DA! Do something with this in your application.
234
+ ... else:
235
+ ... # message is either NECRepeatIRMessage or
236
+ ... # UnparseableIRMessage. You may decide to ignore it, raise
237
+ ... # an error, or log the issue to a file. If you raise or log,
238
+ ... # it may be helpful to include message.pulses in the error message.
239
+ ... ...
240
+ """
241
+
242
+ def __init__ (self , pulses , max_pulse = 10_000 ):
243
+ self .pulses = pulses # PulseIn
244
+ self .max_pulse = max_pulse
245
+ self ._unparsed_pulses = [] # internal buffer of partial messages
246
+
247
+ def read (self ):
248
+ """
249
+ Consume all pulses from PulseIn. Yield decoded messages, if any.
250
+
251
+ If a partial message is received, this does not block to wait for the
252
+ rest. It stashes the partial message, to be continued the next time it
253
+ is called.
254
+ """
255
+ # Consume from PulseIn.
256
+ while self .pulses :
257
+ pulse = self .pulses .popleft ()
258
+ self ._unparsed_pulses .append (pulse )
259
+ if pulse > self .max_pulse :
260
+ # End of message! Decode it and yield a BaseIRMessage.
261
+ yield decode_bits (self ._unparsed_pulses )
262
+ self ._unparsed_pulses .clear ()
263
+ # TODO Do we need to consume and throw away more pulses here?
264
+ # I'm unclear about the role that "pruning" plays in the
265
+ # original implementation in GenericDecode._read_pulses_non_blocking.
266
+ # When we reach here, we have consumed everything from PulseIn.
267
+ # If there are some pulses in self._unparsed_pulses, they represent
268
+ # partial messages. We'll finish them next time read() is called.
166
269
167
270
168
271
class GenericDecode :
@@ -174,7 +277,11 @@ def bin_data(self, pulses):
174
277
175
278
def decode_bits (self , pulses ):
176
279
"Wraps the top-level function decode_bits for backward-compatibility."
177
- return decode_bits (pulses )
280
+ result = decode_bits (pulses )
281
+ if isinstance (result , NECRepeatIRMessage ):
282
+ raise IRNECRepeatException ()
283
+ elif isinstance (result , UnparseableIRMessage ):
284
+ raise IRDecodeException ("10 pulses minimum" )
178
285
179
286
def _read_pulses_non_blocking (
180
287
self , input_pulses , max_pulse = 10000 , pulse_window = 0.10
@@ -214,7 +321,7 @@ def read_pulses(
214
321
max_pulse = 10000 ,
215
322
blocking = True ,
216
323
pulse_window = 0.10 ,
217
- blocking_delay = 0.10
324
+ blocking_delay = 0.10 ,
218
325
):
219
326
"""Read out a burst of pulses until pulses stop for a specified
220
327
period (pulse_window), pruning pulses after a pulse longer than ``max_pulse``.
0 commit comments