5050 https://github.com/adafruit/circuitpython/releases
5151
5252"""
53-
54- # Pretend self matter because we may add object level config later.
55- # pylint: disable=no-self-use
56-
5753import array
54+ from collections import namedtuple
5855import time
5956
6057__version__ = "0.0.0-auto.0"
@@ -69,118 +66,219 @@ class IRNECRepeatException(Exception):
6966 """Exception when a NEC repeat is decoded"""
7067
7168
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.
7471
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 )
77170
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 ))
109178
110- if len (pulses ) < 10 :
111- raise IRDecodeException ("10 pulses minimum" )
112179
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"
118182
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"
121185
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"
125188
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 ]
129189
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"
132192
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" )
173268
174269 def _read_pulses_non_blocking (
175270 self , input_pulses , max_pulse = 10000 , pulse_window = 0.10
176- ):
271+ ): # pylint: disable=no-self-use
177272 """Read out a burst of pulses without blocking until pulses stop for a specified
178273 period (pulse_window), pruning pulses after a pulse longer than ``max_pulse``.
179274
180275 :param ~pulseio.PulseIn input_pulses: Object to read pulses from
181276 :param int max_pulse: Pulse duration to end a burst
182277 :param float pulse_window: pulses are collected for this period of time
183278 """
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.
184282 received = None
185283 recent_count = 0
186284 pruning = False
@@ -209,7 +307,7 @@ def read_pulses(
209307 max_pulse = 10000 ,
210308 blocking = True ,
211309 pulse_window = 0.10 ,
212- blocking_delay = 0.10
310+ blocking_delay = 0.10 ,
213311 ):
214312 """Read out a burst of pulses until pulses stop for a specified
215313 period (pulse_window), pruning pulses after a pulse longer than ``max_pulse``.
0 commit comments