This script waits for a single burst from the remote and prints the timing of
the pulses followed by its best guess at the protocol. It correctly identifies
supported protocols, but can wrongly identify unsupported protocols. The
-behaviour of the script exposed to an unknown protocol is unpredictable.
+report produced by the script exposed to an unknown protocol is unpredictable.
+The `test()` function returns a list of the mark and space periods (in μs).
# 3. The driver
a Pyboard D SF2W at stock frequency. They were: NEC 1ms for normal data, 100μs
for a repeat code. Philips codes: RC-5 900μs, RC-6 mode 0 5.5ms.
+# 7. Unsupported protocols
+
+It is possible to capture an IR burst from a remote and to re-create it using
+the transmitter. This has limitations and is discussed in detail in
+[the transmitter doc](./TRANSMITTER.md#5-unsupported-protocols).
+
# Appendix 1 NEC Protocol description
A normal burst comprises exactly 68 edges, the exception being a repeat code
3. `verbose=False` If `True` emits (a lot of) debug output.
Method:
- 1. `transmit(addr, data, toggle=0)` Integer args. `addr` and `data` are
- normally 8-bit values and `toggle` is normally 0 or 1; details are protocol
- dependent and are described below.
+ 1. `transmit(addr, data, toggle=0, validate=False)` Args `addr`, `data` and
+ `toggle` are positive integers. The maximum vaues are protocol dependent. If
+ `validate` is `True` passed values are checked and a `ValueError` raised if
+ they are out of range. If `validate` is false invalid bits are silently
+ discarded. For example if an address of 0x11 is passed to `MCE.transmit`, the
+ address sent will be 1 because that protocol supports only a four bit address
+ field. The `toggle` field is unused by some protocols when 0 should be passed.
+
+Class method:
+ 1. `active_low` No args. Pyboard only. A `ValueError` will be thrown on ESP32.
+ The IR LED drive circuit is usually designed to turn the LED on if the driver
+ pin is high. If it has opposite polarity the method must be called before
+ instantiating the class - it will be ineffective if called later.
+
+Class varaible:
+ 1. `timeit=False` If `True` the `.transmit` method times itself and prints the
+ result in μs.
The `transmit` method is synchronous with rapid return. Actual transmission
-occurs as a background process, on the Pyboard controlled by timers 2 and 5.
-Execution times on a Pyboard 1.1 were 3.3ms for NEC, 1.5ms for RC5 and 2ms
-for RC6.
-
-Class variable:
- 1. `active_high=True` Normally the IR LED drive circuit turns the LED on if
- the pin goes high. If it works with the opposite polarity the variable should
- be set `False` before instantiating.
+occurs as a background process, on the Pyboard controlled by timers 2 and 5. On
+ESP32 the RMT class is used. Execution times were measured on a Pyboard 1.1 and
+the ESP32 reference board without SPIRAM. Tests were done at stock frequency and
+with `validate=True`, `verbose=False`. A small saving could be achieved by
+skipping validation.
+
+| Protocol | ESP32 | Pyboard |
+|:--------:|:-----:|:-------:|
+| NEC | 7.8ms | 3.2ms |
+| SONY12 | 3.2ms | 1.3ms |
+| SONY15 | 3.6ms | 1.5ms |
+| SONY20 | 4.5ms | 1.9ms |
+| RC5 | 4.9ms | 1.5ms |
+| RC6_M0 | 6.0ms | 2.0ms |
+| MCE | 6.7ms | 2.0ms |
#### NEC class
# 4. Principle of operation
-## 4.1 Pyboard
-
The classes inherit from the abstract base class `IR`. This has an array `.arr`
to contain the duration (in μs) of each carrier on or off period. The
`transmit` method calls a `tx` method of the subclass which populates this
The `.add` method takes a single μs time value and adds it to the last value
in the array: this pulse lengthening is used in bi-phase encodings.
-On completion of the subclass `.tx`, `.transmit` appends a special `STOP` value
-and initiates physical transmission which occurs in an interrupt context.
+On completion of the subclass `.tx`, `.transmit` calls `.trigger` which
+initiates transmission as a background process. Its behaviour is platform
+dependent.
+
+## 4.1 Pyboard
-This is performed by two hardware timers initiated in the constructor. Timer 2,
-channel 1 is used to configure the output pin as a PWM channel. Its frequency
-is set in the constructor. The OOK is performed by dynamically changing the
-duty ratio using the timer channel's `pulse_width_percent` method: this varies
-the pulse width from 0 to a duty ratio passed to the constructor.
+Tramsmission is performed by two hardware timers initiated in the constructor.
+Timer 2, channel 1 is used to configure the output pin as a PWM channel. Its
+frequency is set in the constructor. The OOK is performed by dynamically
+changing the duty ratio using the timer channel's `pulse_width_percent` method:
+this varies the pulse width from 0 to the duty ratio passed to the constructor.
The duty ratio is changed by the Timer 5 callback `._cb`. This retrieves the
next duration from the array. If it is not `STOP` it toggles the duty cycle
-and re-initialises T5 for the new duration.
+and re-initialises T5 for the new duration. If it is `STOP` it ensures that the
+duty ratio is set to the `_SPACE`
+
+Here `.trigger` appends a special `STOP` value and initiates physical
+transmission by calling the Timer5 callback.
## 4.2 ESP32
constructor converts the 0-100 duty ratio specified by the subclass to the
0-1023 range used by ESP32.
+The `.trigger` method calls `RMT.write_pulses` and returns with `RMT` operating
+in the background.
+
## 4.3 Duty ratio
In every case where I could find a specified figure it was 30%. I measured
that from a variety of remotes, and in every case it was close to that figure.
+
+# 5. Unsupported protocols
+
+You can use the receiver module to capture an IR burst and replay it with the
+transmitter. This enables limited support for unknown protocols. This is
+strictly for experimenters and I haven't documented it in detail.
+
+There are two limitations. The first is timing accuracy: both receiving and
+transmitting processes introduce some timing uncertainty. This is only likely
+to be a practical problem with fast protocols. In brief testing with a known
+protocol the scripts below worked.
+
+The more tricky problem is handling repeat keys: different protocols use widely
+varying approaches. If repeat keys are to be supported some experimentation and
+coding is likely to be required.
+
+The following captures a single burst and saves it to a file:
+```python
+from ir_rx.acquire import test
+import ujson
+
+lst = test() # May report unsupported or unknown protocol
+with open('burst.py', 'w') as f:
+ ujson.dump(lst, f)
+```
+This replays it:
+```python
+from ir_tx import Player
+from sys import platform
+import ujson
+
+if platform == 'esp32':
+ from machine import Pin
+ pin = (Pin(23, Pin.OUT, value = 0), Pin(21, Pin.OUT, value = 0))
+else:
+ from pyb import Pin, LED
+ pin = Pin('X1')
+with open('burst.py', 'r') as f:
+ lst = ujson.load(f)
+ir = Player(pin)
+ir.play(lst)
+```
+The `ir_tx.Player` class is a minimal subclass supporting only the `.play`
+method. This takes as an arg an iterable comprising time values of successive
+mark and space periods (in μs).
+
+The `ir_rx.acquire.test` function makes assumptions about the likely maximum
+length and maximum duration of a burst. In some cases this may require some
+modification e.g. to instantiate `IR_GET` with different args.
pin = Pin(23, Pin.IN)
irg = IR_GET(pin)
print('Waiting for IR data...')
- irg.acquire()
+ return irg.acquire()
val = 0 # Data received, LSB 1st
x = 2
bit = 1
- while x < nedges - 2:
+ while x <= nedges - 2:
if ticks_diff(self._times[x + 1], self._times[x]) > 900:
val |= bit
bit <<= 1
x += 2
-
cmd = val & 0x7f # 7 bit command
val >>= 7
if nedges < 42:
from micropython import const
from array import array
-import micropython
-
+from time import ticks_us, ticks_diff
+# import micropython
# micropython.alloc_emergency_exception_buf(100)
-# Duty ratio in carrier off state.
-_SPACE = const(0)
# On ESP32 gate hardware design is led_on = rmt and carrier
# Shared by NEC
# IR abstract base class. Array holds periods in μs between toggling 36/38KHz
# carrier on or off. Physical transmission occurs in an ISR context controlled
-# by timer 2 and timer 5. See README.md for details of operation.
+# by timer 2 and timer 5. See TRANSMITTER.md for details of operation.
class IR:
- active_high = True # Hardware turns IRLED on if pin goes high.
+ _active_high = True # Hardware turns IRLED on if pin goes high.
+ _space = 0 # Duty ratio that causes IRLED to be off
+ timeit = False # Print timing info
+
+ @classmethod
+ def active_low(cls):
+ if ESP32:
+ raise ValueError('Cannot set active low on ESP32')
+ cls._active_high = False
+ cls._space = 100
def __init__(self, pin, cfreq, asize, duty, verbose):
- if not IR.active_high:
- duty = 100 - duty
if ESP32:
self._pwm = PWM(pin[0]) # Continuous 36/38/40KHz carrier
self._pwm.deinit()
self._pwm.init(freq=cfreq, duty=round(duty * 10.23))
self._rmt = RMT(0, pin=pin[1], clock_div=80) # 1μs resolution
else: # Pyboard
+ if not IR._active_high:
+ duty = 100 - duty
tim = Timer(2, freq=cfreq) # Timer 2/pin produces 36/38/40KHz carrier
self._ch = tim.channel(1, Timer.PWM, pin=pin)
- self._ch.pulse_width_percent(_SPACE) # Turn off IR LED
+ self._ch.pulse_width_percent(self._space) # Turn off IR LED
# Pyboard: 0 <= pulse_width_percent <= 100
self._duty = duty
self._tim = Timer(5) # Timer 5 controls carrier on/off times
p = self.aptr
v = self._arr[p]
if v == STOP:
- self._ch.pulse_width_percent(_SPACE) # Turn off IR LED.
+ self._ch.pulse_width_percent(self._space) # Turn off IR LED.
return
- self._ch.pulse_width_percent(_SPACE if p & 1 else self._duty)
+ self._ch.pulse_width_percent(self._space if p & 1 else self._duty)
self._tim.init(prescaler=84, period=v, callback=self._tcb)
self.aptr += 1
# Public interface
# Before populating array, zero pointer, set notional carrier state (off).
- def transmit(self, addr, data, toggle=0): # NEC: toggle is unused
+ def transmit(self, addr, data, toggle=0, validate=False): # NEC: toggle is unused
+ t = ticks_us()
+ if validate:
+ if addr > self.valid[0] or addr < 0:
+ raise ValueError('Address out of range', addr)
+ if data > self.valid[1] or data < 0:
+ raise ValueError('Data out of range', data)
+ if toggle > self.valid[2] or toggle < 0:
+ raise ValueError('Toggle out of range', toggle)
self.aptr = 0 # Inital conditions for tx: index into array
self.carrier = False
self.tx(addr, data, toggle) # Subclass populates ._arr
self.trigger() # Initiate transmission
+ if self.timeit:
+ dt = ticks_diff(ticks_us(), t)
+ print('Time = {}μs'.format(dt))
# Subclass interface
def trigger(self): # Used by NEC to initiate a repeat frame
self.verbose and print('add', t)
# .carrier unaffected
self._arr[self.aptr - 1] += t
+
+
+# Given an iterable (e.g. list or tuple) of times, emit it as an IR stream.
+class Player(IR):
+
+ def __init__(self, pin, freq=38000, verbose=False): # NEC specifies 38KHz
+ super().__init__(pin, freq, 68, 33, verbose) # Measured duty ratio 33%
+
+ def play(self, lst):
+ for x, t in enumerate(lst):
+ self._arr[x] = t
+ self.aptr = x + 1
+ self.trigger()
class MCE(IR):
+ valid = (0xf, 0x3f, 3) # Max addr, data, toggle
init_cs = 4 # http://www.hifi-remote.com/johnsfine/DecodeIR.html#OrtekMCE says 3
def __init__(self, pin, freq=38000, verbose=False):
self.stop = False
def cfunc(self): # Button push: send data and set up for repeats
- print('start')
- self.irb.transmit(self.addr, self.data, _FIRST)
+ self.irb.transmit(self.addr, self.data, _FIRST, True)
self.tim.trigger(_REP_DELAY)
def ofunc(self): # Button release: cancel repeat timer
if self.stop: # Button has been released: send last message
self.stop = False
self.tim.stop() # Not strictly necessary
- self.irb.transmit(self.addr, self.data, _END)
- print('stop')
+ self.irb.transmit(self.addr, self.data, _END, True)
else:
- print('rep')
self.tim.trigger(_REP_DELAY)
- self.irb.transmit(self.addr, self.data, _REP)
+ self.irb.transmit(self.addr, self.data, _REP, True)
async def main():
if ESP32: # Pins for IR LED gate
pin = (Pin(23, Pin.OUT, value = 0), Pin(21, Pin.OUT, value = 0))
else:
pin = Pin('X1')
- irb = MCE(pin, verbose=True)
+ irb = MCE(pin) # verbose=True)
+ # Uncomment the following to print transmit timing
+ # irb.timeit = True
b = [] # Rbutton instances
px3 = Pin(18, Pin.IN, Pin.PULL_UP) if ESP32 else Pin('X3', Pin.IN, Pin.PULL_UP)
_T_ONE = const(1687)
class NEC(IR):
+ valid = (0xffff, 0xff, 0) # Max addr, data, toggle
def __init__(self, pin, freq=38000, verbose=False): # NEC specifies 38KHz
super().__init__(pin, freq, 68, 33, verbose) # Measured duty ratio 33%
class RC5(IR):
+ valid = (0x1f, 0x3f, 1) # Max addr, data, toggle
def __init__(self, pin, freq=36000, verbose=False):
super().__init__(pin, freq, 28, 30, verbose)
_T2_RC6 = const(889)
class RC6_M0(IR):
+ valid = (0xff, 0xff, 1) # Max addr, data, toggle
def __init__(self, pin, freq=36000, verbose=False):
super().__init__(pin, freq, 44, 30, verbose)
# Sony specifies 40KHz
class SONY_12(SONY_ABC):
+ valid = (0x1f, 0x7f, 0) # Max addr, data, toggle
def __init__(self, pin, freq=40000, verbose=False):
super().__init__(pin, 12, freq, verbose)
class SONY_15(SONY_ABC):
+ valid = (0xff, 0x7f, 0) # Max addr, data, toggle
def __init__(self, pin, freq=40000, verbose=False):
super().__init__(pin, 15, freq, verbose)
class SONY_20(SONY_ABC):
+ valid = (0x1f, 0x7f, 0xff) # Max addr, data, toggle
def __init__(self, pin, freq=40000, verbose=False):
super().__init__(pin, 20, freq, verbose)
loop = asyncio.get_event_loop()
+# If button is held down normal behaviour is to retransmit
+# but most NEC models send a REPEAT code
class Rbutton:
toggle = 1 # toggle is ignored in NEC mode
- def __init__(self, irb, pin, addr, data, rep_code=False):
+ def __init__(self, irb, pin, addr, data, proto):
self.irb = irb
self.sw = Switch(pin)
self.addr = addr
self.data = data
- self.rep_code = rep_code
+ self.proto = proto
+
self.sw.close_func(self.cfunc)
self.sw.open_func(self.ofunc)
self.tim = Delay_ms(self.repeat)
def cfunc(self): # Button push: send data
- self.irb.transmit(self.addr, self.data, Rbutton.toggle)
+ tog = 0 if self.proto < 3 else Rbutton.toggle # NEC, sony 12, 15: toggle==0
+ self.irb.transmit(self.addr, self.data, tog, True) # Test validation
# Auto repeat. The Sony protocol specifies 45ms but this is tight.
# In 20 bit mode a data burst can be upto 39ms long.
self.tim.trigger(108)
await asyncio.sleep(0) # Let timer stop before retriggering
if not self.sw(): # Button is still pressed: retrigger
self.tim.trigger(108)
- if self.rep_code:
+ if self.proto == 0:
self.irb.repeat() # NEC special case: send REPEAT code
else:
- self.irb.transmit(self.addr, self.data, Rbutton.toggle)
+ tog = 0 if self.proto < 3 else Rbutton.toggle # NEC, sony 12, 15: toggle==0
+ self.irb.transmit(self.addr, self.data, tog, True) # Test validation
async def main(proto):
- # Test uses a 38KHz carrier. Some Philips systems use 36KHz.
- # If button is held down normal behaviour is to retransmit
- # but most NEC models send a REPEAT code
- rep_code = proto == 0 # Rbutton constructor requires False for RC-X. NEC protocol only.
+ # Test uses a 38KHz carrier.
if ESP32: # Pins for IR LED gate
pin = (Pin(23, Pin.OUT, value = 0), Pin(21, Pin.OUT, value = 0))
else:
pin = Pin('X1')
classes = (NEC, SONY_12, SONY_15, SONY_20, RC5, RC6_M0)
irb = classes[proto](pin, 38000) # My decoder chip is 38KHz
+ # Uncomment the following to print transmit timing
+ # irb.timeit = True
b = [] # Rbutton instances
px3 = Pin(18, Pin.IN, Pin.PULL_UP) if ESP32 else Pin('X3', Pin.IN, Pin.PULL_UP)
px4 = Pin(19, Pin.IN, Pin.PULL_UP) if ESP32 else Pin('X4', Pin.IN, Pin.PULL_UP)
- b.append(Rbutton(irb, px3, 0x1, 0x7, rep_code))
- b.append(Rbutton(irb, px4, 0x10, 0xb, rep_code))
+ b.append(Rbutton(irb, px3, 0x1, 0x7, proto))
+ b.append(Rbutton(irb, px4, 0x10, 0xb, proto))
if ESP32:
while True:
print('Running')