50
50
https://github.com/adafruit/circuitpython/releases
51
51
52
52
"""
53
-
54
- # Pretend self matter because we may add object level config later.
55
- # pylint: disable=no-self-use
56
-
57
53
import array
54
+ from collections import namedtuple
58
55
import time
59
56
60
57
__version__ = "0.0.0-auto.0"
@@ -69,118 +66,219 @@ class IRNECRepeatException(Exception):
69
66
"""Exception when a NEC repeat is decoded"""
70
67
71
68
72
- class GenericDecode :
73
- """Generic decoding of infrared signals"""
69
+ def bin_data ( pulses ) :
70
+ """Compute bins of pulse lengths where pulses are +-25% of the average.
74
71
75
- def bin_data (self , pulses ):
76
- """Compute bins of pulse lengths where pulses are +-25% of the average.
72
+ :param list pulses: Input pulse lengths
73
+ """
74
+ bins = [[pulses [0 ], 0 ]]
75
+
76
+ for _ , pulse in enumerate (pulses ):
77
+ matchedbin = False
78
+ # print(pulse, end=": ")
79
+ for b , pulse_bin in enumerate (bins ):
80
+ if pulse_bin [0 ] * 0.75 <= pulse <= pulse_bin [0 ] * 1.25 :
81
+ # print("matches bin")
82
+ bins [b ][0 ] = (pulse_bin [0 ] + pulse ) // 2 # avg em
83
+ bins [b ][1 ] += 1 # track it
84
+ matchedbin = True
85
+ break
86
+ if not matchedbin :
87
+ bins .append ([pulse , 1 ])
88
+ # print(bins)
89
+ return bins
90
+
91
+
92
+ def decode_bits (pulses ):
93
+ """Decode the pulses into bits."""
94
+ # pylint: disable=too-many-branches,too-many-statements
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 = tuple (pulses )
100
+ pulses = list (pulses ) # Copy to avoid mutating input.
101
+
102
+ # special exception for NEC repeat code!
103
+ if (
104
+ (len (pulses ) == 3 )
105
+ and (8000 <= pulses [0 ] <= 10000 )
106
+ and (2000 <= pulses [1 ] <= 3000 )
107
+ and (450 <= pulses [2 ] <= 700 )
108
+ ):
109
+ return NECRepeatIRMessage (input_pulses )
110
+
111
+ if len (pulses ) < 10 :
112
+ msg = UnparseableIRMessage (input_pulses , reason = "Too short" )
113
+ raise FailedToDecode (msg )
114
+
115
+ # Ignore any header (evens start at 1), and any trailer.
116
+ if len (pulses ) % 2 == 0 :
117
+ pulses_end = - 1
118
+ else :
119
+ pulses_end = None
120
+
121
+ evens = pulses [1 :pulses_end :2 ]
122
+ odds = pulses [2 :pulses_end :2 ]
123
+
124
+ # bin both halves
125
+ even_bins = bin_data (evens )
126
+ odd_bins = bin_data (odds )
127
+
128
+ outliers = [b [0 ] for b in (even_bins + odd_bins ) if b [1 ] == 1 ]
129
+ even_bins = [b for b in even_bins if b [1 ] > 1 ]
130
+ odd_bins = [b for b in odd_bins if b [1 ] > 1 ]
131
+
132
+ if not even_bins or not odd_bins :
133
+ msg = UnparseableIRMessage (input_pulses , reason = "Not enough data" )
134
+ raise FailedToDecode (msg )
135
+
136
+ if len (even_bins ) == 1 :
137
+ pulses = odds
138
+ pulse_bins = odd_bins
139
+ elif len (odd_bins ) == 1 :
140
+ pulses = evens
141
+ pulse_bins = even_bins
142
+ else :
143
+ msg = UnparseableIRMessage (input_pulses , reason = "Both even/odd pulses differ" )
144
+ raise FailedToDecode (msg )
145
+
146
+ if len (pulse_bins ) == 1 :
147
+ msg = UnparseableIRMessage (input_pulses , reason = "Pulses do not differ" )
148
+ raise FailedToDecode (msg )
149
+ if len (pulse_bins ) > 2 :
150
+ msg = UnparseableIRMessage (input_pulses , reason = "Only mark & space handled" )
151
+ raise FailedToDecode (msg )
152
+
153
+ mark = min (pulse_bins [0 ][0 ], pulse_bins [1 ][0 ])
154
+ space = max (pulse_bins [0 ][0 ], pulse_bins [1 ][0 ])
155
+
156
+ if outliers :
157
+ # skip outliers
158
+ pulses = [
159
+ p for p in pulses if not (outliers [0 ] * 0.75 ) <= p <= (outliers [0 ] * 1.25 )
160
+ ]
161
+ # convert marks/spaces to 0 and 1
162
+ for i , pulse_length in enumerate (pulses ):
163
+ if (space * 0.75 ) <= pulse_length <= (space * 1.25 ):
164
+ pulses [i ] = False
165
+ elif (mark * 0.75 ) <= pulse_length <= (mark * 1.25 ):
166
+ pulses [i ] = True
167
+ else :
168
+ msg = UnparseableIRMessage (input_pulses , reason = "Pulses outside mark/space" )
169
+ raise FailedToDecode (msg )
77
170
78
- :param list pulses: Input pulse lengths
79
- """
80
- bins = [[pulses [0 ], 0 ]]
81
-
82
- for _ , pulse in enumerate (pulses ):
83
- matchedbin = False
84
- # print(pulse, end=": ")
85
- for b , pulse_bin in enumerate (bins ):
86
- if pulse_bin [0 ] * 0.75 <= pulse <= pulse_bin [0 ] * 1.25 :
87
- # print("matches bin")
88
- bins [b ][0 ] = (pulse_bin [0 ] + pulse ) // 2 # avg em
89
- bins [b ][1 ] += 1 # track it
90
- matchedbin = True
91
- break
92
- if not matchedbin :
93
- bins .append ([pulse , 1 ])
94
- # print(bins)
95
- return bins
96
-
97
- def decode_bits (self , pulses ):
98
- """Decode the pulses into bits."""
99
- # pylint: disable=too-many-branches,too-many-statements
100
-
101
- # special exception for NEC repeat code!
102
- if (
103
- (len (pulses ) == 3 )
104
- and (8000 <= pulses [0 ] <= 10000 )
105
- and (2000 <= pulses [1 ] <= 3000 )
106
- and (450 <= pulses [2 ] <= 700 )
107
- ):
108
- raise IRNECRepeatException ()
171
+ # convert bits to bytes!
172
+ output = [0 ] * ((len (pulses ) + 7 ) // 8 )
173
+ for i , pulse_length in enumerate (pulses ):
174
+ output [i // 8 ] = output [i // 8 ] << 1
175
+ if pulse_length :
176
+ output [i // 8 ] |= 1
177
+ return IRMessage (tuple (input_pulses ), code = tuple (output ))
109
178
110
- if len (pulses ) < 10 :
111
- raise IRDecodeException ("10 pulses minimum" )
112
179
113
- # Ignore any header (evens start at 1), and any trailer.
114
- if len (pulses ) % 2 == 0 :
115
- pulses_end = - 1
116
- else :
117
- pulses_end = None
180
+ IRMessage = namedtuple ("IRMessage" , ("pulses" , "code" ))
181
+ "Pulses and the code they were parsed into"
118
182
119
- evens = pulses [ 1 : pulses_end : 2 ]
120
- odds = pulses [ 2 : pulses_end : 2 ]
183
+ UnparseableIRMessage = namedtuple ( "IRMessage" , ( " pulses" , "reason" ))
184
+ "Pulses and the reason that they could not be parsed into a code"
121
185
122
- # bin both halves
123
- even_bins = self .bin_data (evens )
124
- odd_bins = self .bin_data (odds )
186
+ NECRepeatIRMessage = namedtuple ("NECRepeatIRMessage" , ("pulses" ,))
187
+ "Pulses interpreted as an NEC repeat code"
125
188
126
- outliers = [b [0 ] for b in (even_bins + odd_bins ) if b [1 ] == 1 ]
127
- even_bins = [b for b in even_bins if b [1 ] > 1 ]
128
- odd_bins = [b for b in odd_bins if b [1 ] > 1 ]
129
189
130
- if not even_bins or not odd_bins :
131
- raise IRDecodeException ( "Not enough data" )
190
+ class FailedToDecode ( Exception ) :
191
+ "Raised by decode_bits. Error argument is UnparseableIRMessage"
132
192
133
- if len (even_bins ) == 1 :
134
- pulses = odds
135
- pulse_bins = odd_bins
136
- elif len (odd_bins ) == 1 :
137
- pulses = evens
138
- pulse_bins = even_bins
139
- else :
140
- raise IRDecodeException ("Both even/odd pulses differ" )
141
-
142
- if len (pulse_bins ) == 1 :
143
- raise IRDecodeException ("Pulses do not differ" )
144
- if len (pulse_bins ) > 2 :
145
- raise IRDecodeException ("Only mark & space handled" )
146
-
147
- mark = min (pulse_bins [0 ][0 ], pulse_bins [1 ][0 ])
148
- space = max (pulse_bins [0 ][0 ], pulse_bins [1 ][0 ])
149
-
150
- if outliers :
151
- # skip outliers
152
- pulses = [
153
- p
154
- for p in pulses
155
- if not (outliers [0 ] * 0.75 ) <= p <= (outliers [0 ] * 1.25 )
156
- ]
157
- # convert marks/spaces to 0 and 1
158
- for i , pulse_length in enumerate (pulses ):
159
- if (space * 0.75 ) <= pulse_length <= (space * 1.25 ):
160
- pulses [i ] = False
161
- elif (mark * 0.75 ) <= pulse_length <= (mark * 1.25 ):
162
- pulses [i ] = True
163
- else :
164
- raise IRDecodeException ("Pulses outside mark/space" )
165
-
166
- # convert bits to bytes!
167
- output = [0 ] * ((len (pulses ) + 7 ) // 8 )
168
- for i , pulse_length in enumerate (pulses ):
169
- output [i // 8 ] = output [i // 8 ] << 1
170
- if pulse_length :
171
- output [i // 8 ] |= 1
172
- return output
193
+
194
+ class NonblockingGenericDecode :
195
+ """
196
+ Decode pulses into bytes in a non-blocking fashion.
197
+
198
+ :param ~pulseio.PulseIn input_pulses: Object to read pulses from
199
+ :param int max_pulse: Pulse duration to end a burst. Units are microseconds.
200
+
201
+ >>> pulses = PulseIn(...)
202
+ >>> decoder = NonblockingGenericDecoder(pulses)
203
+ >>> for message in decoder.read():
204
+ ... if isinstace(message, IRMessage):
205
+ ... message.code # TA-DA! Do something with this in your application.
206
+ ... else:
207
+ ... # message is either NECRepeatIRMessage or
208
+ ... # UnparseableIRMessage. You may decide to ignore it, raise
209
+ ... # an error, or log the issue to a file. If you raise or log,
210
+ ... # it may be helpful to include message.pulses in the error message.
211
+ ... ...
212
+ """
213
+
214
+ def __init__ (self , pulses , max_pulse = 10_000 ):
215
+ self .pulses = pulses # PulseIn
216
+ self .max_pulse = max_pulse
217
+ self ._unparsed_pulses = [] # internal buffer of partial messages
218
+
219
+ def read (self ):
220
+ """
221
+ Consume all pulses from PulseIn. Yield decoded messages, if any.
222
+
223
+ If a partial message is received, this does not block to wait for the
224
+ rest. It stashes the partial message, to be continued the next time it
225
+ is called.
226
+ """
227
+ # Consume from PulseIn.
228
+ while self .pulses :
229
+ pulse = self .pulses .popleft ()
230
+ self ._unparsed_pulses .append (pulse )
231
+ if pulse > self .max_pulse :
232
+ # End of message! Decode it and yield a BaseIRMessage.
233
+ try :
234
+ yield decode_bits (self ._unparsed_pulses )
235
+ except FailedToDecode as err :
236
+ # If you want to debug failed decodes, this would be a good
237
+ # place to print/log or (re-)raise.
238
+ (unparseable_message ,) = err .args
239
+ yield unparseable_message
240
+ self ._unparsed_pulses .clear ()
241
+ # TODO Do we need to consume and throw away more pulses here?
242
+ # I'm unclear about the role that "pruning" plays in the
243
+ # original implementation in GenericDecode._read_pulses_non_blocking.
244
+ # When we reach here, we have consumed everything from PulseIn.
245
+ # If there are some pulses in self._unparsed_pulses, they represent
246
+ # partial messages. We'll finish them next time read() is called.
247
+
248
+
249
+ class GenericDecode :
250
+ """Generic decoding of infrared signals"""
251
+
252
+ # Note: pylint's complaint about the following three methods (no self-use)
253
+ # is absolutely correct, which is why the code was refactored, but we need
254
+ # this here for back-compat, hence we disable pylint for that specific
255
+ # complaint.
256
+
257
+ def bin_data (self , pulses ): # pylint: disable=no-self-use
258
+ "Wraps the top-level function bin_data for backward-compatibility."
259
+ return bin_data (pulses )
260
+
261
+ def decode_bits (self , pulses ): # pylint: disable=no-self-use
262
+ "Wraps the top-level function decode_bits for backward-compatibility."
263
+ result = decode_bits (pulses )
264
+ if isinstance (result , NECRepeatIRMessage ):
265
+ raise IRNECRepeatException ()
266
+ if isinstance (result , UnparseableIRMessage ):
267
+ raise IRDecodeException ("10 pulses minimum" )
173
268
174
269
def _read_pulses_non_blocking (
175
270
self , input_pulses , max_pulse = 10000 , pulse_window = 0.10
176
- ):
271
+ ): # pylint: disable=no-self-use
177
272
"""Read out a burst of pulses without blocking until pulses stop for a specified
178
273
period (pulse_window), pruning pulses after a pulse longer than ``max_pulse``.
179
274
180
275
:param ~pulseio.PulseIn input_pulses: Object to read pulses from
181
276
:param int max_pulse: Pulse duration to end a burst
182
277
:param float pulse_window: pulses are collected for this period of time
183
278
"""
279
+ # Note: pylint's complaint (no self-use) is absolutely correct, which
280
+ # is why the code was refactored, but we need this here for
281
+ # back-compat, hence we disable pylint.
184
282
received = None
185
283
recent_count = 0
186
284
pruning = False
@@ -209,7 +307,7 @@ def read_pulses(
209
307
max_pulse = 10000 ,
210
308
blocking = True ,
211
309
pulse_window = 0.10 ,
212
- blocking_delay = 0.10
310
+ blocking_delay = 0.10 ,
213
311
):
214
312
"""Read out a burst of pulses until pulses stop for a specified
215
313
period (pulse_window), pruning pulses after a pulse longer than ``max_pulse``.
0 commit comments