]> vault307.fbx.one Git - micorpython_ir.git/commitdiff
Prior to merge.
authorPeter Hinch <peter@hinch.me.uk>
Tue, 10 Mar 2020 13:26:21 +0000 (13:26 +0000)
committerPeter Hinch <peter@hinch.me.uk>
Tue, 10 Mar 2020 13:26:21 +0000 (13:26 +0000)
README.md
RECEIVER.md [new file with mode: 0644]
TRANSMITTER.md [new file with mode: 0644]
ir_rx/test.py
ir_rx_test.py [deleted file]
ir_tx.py [deleted file]
ir_tx/__init__.py [new file with mode: 0644]
ir_tx/nec.py [new file with mode: 0644]
ir_tx/philips.py [new file with mode: 0644]
ir_tx/sony.py [new file with mode: 0644]
ir_tx/test.py [moved from ir_tx_test.py with 59% similarity]

index 3c78771b3cdb325a264ee73bce8d6d4b9c7fbba9..371615fbfb08d8fcc035c38773fd8ea9af470e32 100644 (file)
--- a/README.md
+++ b/README.md
@@ -2,20 +2,24 @@
 
 This repo provides a driver to receive from IR (infra red) remote controls and
 a driver for IR "blaster" apps. The device drivers are nonblocking. They do not
 
 This repo provides a driver to receive from IR (infra red) remote controls and
 a driver for IR "blaster" apps. The device drivers are nonblocking. They do not
-require `uasyncio` but are compatible with it.
+require `uasyncio` but are compatible with it, and are designed for standard
+firmware builds.
 
 
-The transmitter driver is specific to the Pyboard. The receiver is cross
-platform and has been tested on Pyboard, ESP8266 and ESP32. See
-[Receiver platforms](./README.md#42-receiver-platforms) for test results and
-limitations.
+The receiver is cross platform and has been tested on Pyboard, ESP8266 and
+ESP32. The transmitter driver is compatible with Pyboard (1.x and D series) and
+also (subject to limitations) with ESP32. For the transmitter a Pyboard is
+recommended.
+
+The transmitter is documented [here](./TRANSMITTER.md) and the receiver is
+[here](./RECEIVER.md).
 
 # 1. IR communication
 
 IR communication uses a carrier frequency to pulse the IR source. Modulation
 takes the form of OOK (on-off keying). There are multiple protocols and at
 
 # 1. IR communication
 
 IR communication uses a carrier frequency to pulse the IR source. Modulation
 takes the form of OOK (on-off keying). There are multiple protocols and at
-least three options for carrier frequency, namely 36KHz, 38KHz and 40KHz.
+least three options for carrier frequency: 36, 38 and 40KHz.
 
 
-The drivers support NEC and Sony protocols and two Philips protocols, namely
+The drivers support NEC and Sony protocols plus two Philips protocols, namely
 RC-5 and RC-6 mode 0. In the case of the transmitter the carrier frequency is a
 runtime parameter: any value may be specified. The receiver uses a hardware
 demodulator which should be purchased for the correct frequency. The receiver
 RC-5 and RC-6 mode 0. In the case of the transmitter the carrier frequency is a
 runtime parameter: any value may be specified. The receiver uses a hardware
 demodulator which should be purchased for the correct frequency. The receiver
@@ -25,7 +29,7 @@ agnostic.
 Examining waveforms from various remote controls it is evident that numerous
 protocols exist. Some are doubtless proprietary and undocumented. The supported
 protocols are those for which I managed to locate documentation. My preference
 Examining waveforms from various remote controls it is evident that numerous
 protocols exist. Some are doubtless proprietary and undocumented. The supported
 protocols are those for which I managed to locate documentation. My preference
-is for the NEC version. It has conservative timing and ample scope for error
+is for the NEC version. It has conservative timing and good provision for error
 detection. RC-5 has limited error detection, and RC-6 mode 0 has rather fast
 timing.
 
 detection. RC-5 has limited error detection, and RC-6 mode 0 has rather fast
 timing.
 
@@ -39,349 +43,20 @@ dependent.
 
 # 2. Hardware Requirements
 
 
 # 2. Hardware Requirements
 
+These are discussed in detail in the relevant docs; the following provides an
+overview.
+
 The receiver is cross-platform. It requires an IR receiver chip to demodulate
 the carrier. The chip must be selected for the frequency in use by the remote.
 For 38KHz devices a receiver chip such as the Vishay TSOP4838 or the
 [adafruit one](https://www.adafruit.com/products/157) is required. This
 demodulates the 38KHz IR pulses and passes the demodulated pulse train to the
 The receiver is cross-platform. It requires an IR receiver chip to demodulate
 the carrier. The chip must be selected for the frequency in use by the remote.
 For 38KHz devices a receiver chip such as the Vishay TSOP4838 or the
 [adafruit one](https://www.adafruit.com/products/157) is required. This
 demodulates the 38KHz IR pulses and passes the demodulated pulse train to the
-microcontroller. The tested chip returns a 0 level on carrier detect, but the
-driver design ensures operation regardless of sense.
+microcontroller.
 
 In my testing a 38KHz demodulator worked with 36KHz and 40KHz remotes, but this
 is obviously neither guaranteed nor optimal.
 
 
 In my testing a 38KHz demodulator worked with 36KHz and 40KHz remotes, but this
 is obviously neither guaranteed nor optimal.
 
-The pin used to connect the decoder chip to the target is arbitrary. The test
-program assumes pin X3 on the Pyboard, pin 23 on ESP32 and pin 13 on ESP8266.
-On the WeMos D1 Mini the equivalent pin is D7.
-
-The transmitter requires a Pyboard 1.x (not Lite) or a Pyboard D. Output is via
-an IR LED which will normally need a transistor to provide sufficient current.
-Typically these need 50-100mA of drive to achieve reasonable range and data
-integrity. A suitable LED is [this one](https://www.adafruit.com/product/387).
-
-The transmitter test script assumes pin X1 for IR output. It can be changed,
-but it must support Timer 2 channel 1. Pins for pushbutton inputs are
-arbitrary: X3 and X4 are used.
-
-# 3. Installation
-
-On import, demos print an explanation of how to run them.
-
-## 3.1 Receiver
-
-This is a Python package. This minimises RAM usage: applications only import
-the device driver for the protocol in use.
-
-Copy the following to the target filesystem:
- 1. `ir_rx` Directory and contents. Contains the device drivers.
- 2. `ir_rx_test.py` Demo of a receiver.
-
-There are no dependencies.
-
-The demo can be used to characterise IR remotes. It displays the codes returned
-by each button. This can aid in the design of receiver applications. The demo
-prints "running" every 5 seconds and reports any data received from the remote.
-
-## 3.2 Transmitter
-
-Copy the following files to the Pyboard filesystem:
- 1. `ir_tx.py` The transmitter device driver.
- 2. `ir_tx_test.py` Demo of a 2-button remote controller.
-
-The device driver has no dependencies. The test program requires `uasyncio`
-from the official library and `aswitch.py` from
-[this repo](https://github.com/peterhinch/micropython-async).
-
-# 4. Receiver
-
-This implements a class for each supported protocol. Applications should
-instantiate the appropriate class with a callback. The callback will run
-whenever an IR pulse train is received. Example running on a Pyboard:
-
-```python
-import time
-from machine import Pin
-from pyb import LED
-from ir_rx.nec import NEC_8  # NEC remote, 8 bit addresses
-
-red = LED(1)
-
-def callback(data, addr, ctrl):
-    if data < 0:  # NEC protocol sends repeat codes.
-        print('Repeat code.')
-    else:
-        print('Data {:02x} Addr {:04x}'.format(data, addr))
-
-ir = NEC_8(Pin('X3', Pin.IN), callback)
-while True:
-    time.sleep_ms(500)
-    red.toggle()
-```
-
-#### Common to all classes
-
-Constructor:  
-Args:  
- 1. `pin` is a `machine.Pin` instance configured as an input, connected to the
- IR decoder chip.  
- 2. `callback` is the user supplied callback.
- 3. `*args` Any further args will be passed to the callback.  
-
-The user callback takes the following args:  
- 1. `data` (`int`) Value from the remote. Normally in range 0-255. A value < 0
- signifies an NEC repeat code.
- 2. `addr` (`int`) Address from the remote.
- 3. `ctrl` (`int`) The meaning of this is protocol dependent:  
- NEC: 0  
- Philips: this is toggled 1/0 on repeat button presses. If the button is held
- down it is not toggled. The  transmitter demo implements this behaviour.  
- Sony: 0 unless receiving a 20-bit stream, in which case it holds the extended
- value.
- 4. Any args passed to the constructor.
-
-Bound variable:  
- 1. `verbose=False` If `True` emits debug output.
-
-Method:
- 1. `error_function` Arg: a function taking a single arg. If this is specified
- it will be called if an error occurs. The value corresponds to the error code
- (see below).
-
-A function is provided to print errors in human readable form. This may be
-invoked as follows:
-
-```python
-from ir_rx.print_error import print_error  # Optional print of error codes
-# Assume ir is an instance of an IR receiver class
-ir.error_function(print_error)
-```
-
-#### NEC classes
-
-`NEC_8`, `NEC_16`
-
-```python
-from ir_rx.nec import NEC_8
-```
-
-Remotes using the NEC protocol can send 8 or 16 bit addresses. If the `NEC_16`
-class receives an 8 bit address it will get a 16 bit value comprising the
-address in bits 0-7 and its one's complement in bits 8-15.  
-The `NEC_8` class enables error checking for remotes that return an 8 bit
-address: the complement is checked and the address returned as an 8-bit value.
-A 16-bit address will result in an error.
-
-#### Sony classes
-
-`SONY_12`, `SONY_15`, `SONY_20`
-
-```python
-from ir_rx.sony import SONY_15
-```
-
-The SIRC protocol comes in 3 variants: 12, 15 and 20 bits. `SONY_20` handles
-bitstreams from all three types of remote. Choosing a class matching the remote
-improves the timing reducing the likelihood of errors when handling repeats: in
-20-bit mode SIRC timing when a button is held down is tight. A worst-case 20
-bit block takes 39ms nominal, yet the repeat time is 45ms nominal.  
-A single physical remote can issue more than one type of bitstream. The Sony
-remote tested issued both 12 bit and 15 bit streams.
-
-#### Philips classes
-
-`RC5_IR`, `RC6_M0`
-
-```python
-from ir_rx.philips import RC5_IR
-```
-
-These support the RC-5 and RC-6 mode 0 protocols respectively.
-
-# 4.1 Errors
-
-IR reception is inevitably subject to errors, notably if the remote is operated
-near the limit of its range, if it is not pointed at the receiver or if its
-batteries are low. The user callback is not called when an error occurs.
-
-On ESP8266 and ESP32 there is a further source of errors. This results from the
-large and variable interrupt latency of the device which can exceed the pulse
-duration. This causes pulses to be missed or their timing measured incorrectly.
-On ESP8266 some improvment may be achieved by running the chip at 160MHz.
-
-In general applications should provide user feedback of correct reception.
-Users tend to press the key again if the expected action is absent.
-
-In debugging a callback can be specified for reporting errors. The value passed
-to the error function are represented by constants indicating the cause of the
-error. These are as follows:
-
-`BADSTART` A short (<= 4ms) start pulse was received. May occur due to IR
-interference, e.g. from fluorescent lights. The TSOP4838 is prone to producing
-200µs pulses on occasion, especially when using the ESP8266.  
-`BADBLOCK` A normal data block: too few edges received. Occurs on the ESP8266
-owing to high interrupt latency.  
-`BADREP` A repeat block: an incorrect number of edges were received.  
-`OVERRUN` A normal data block: too many edges received.  
-`BADDATA` Data did not match check byte.  
-`BADADDR` (`NEC_IR`) If `extended` is `False` the 8-bit address is checked
-against the check byte. This code is returned on failure.  
-
-# 4.2 Receiver platforms
-
-Currently the ESP8266 suffers from [this issue](https://github.com/micropython/micropython/issues/5714).
-Testing was therefore done without WiFi connectivity.
-
-Philips protocols (especially RC-6) have tight timing constraints with short
-pulses whose length must be determined with reasonable accuracy. The Sony 20
-bit protocol also has a timing issue in that the worst case bit pattern takes
-39ms nominal, yet the repeat time is 45ms nominal. These issues can lead to
-errors particularly on slower targets. As discussed above, errors are to be
-expected. It is up to the user to decide if the error rate is acceptable.
-
-Reception was tested using Pyboard D SF2W, ESP8266 and ESP32 with signals from
-remote controls (where available) and from the tranmitter in this repo. Issues
-are listed below.
-
-NEC: No issues.  
-Sony 12 and 15 bit: No issues.  
-Sony 20 bit: On ESP32 some errors occurred when repeats occurred.  
-Philips RC-5: On ESP32 with one remote control many errors occurred, but paired
-with the transmitter in this repo it worked.  
-Philips RC-6: No issues. Only tested against the transmitter in this repo.
-
-# 4.3 Principle of operation
-
-Protocol classes inherit from the abstract base class `IR_RX`. This uses a pin
-interrupt to store in an array the start and end times of pulses (in μs).
-Arrival of the first pulse triggers a software timer which runs for the
-expected duration of an IR block (`tblock`). When it times out its callback
-(`.decode`) decodes the data and calls the user callback. The use of a software
-timer ensures that `.decode` and the user callback can allocate.
-
-The size of the array and the duration of the timer are protocol dependent and
-are set by the subclasses. The `.decode` method is provided in the subclass.
-
-CPU times used by `.decode` (not including the user callback) were measured on
-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.
-
-# 5 Transmitter
-
-This is specific to Pyboard D and Pyboard 1.x (not Lite).
-
-It implements a class for each supported protocol, namely `NEC`, `SONY`, `RC5`
-and `RC6_M0`. The application instantiates the appropriate class and calls the
-`transmit` method to send data.
-
-Constructor  
-All constructors take the following args:  
- 1. `pin` An initialised `pyb.Pin` instance supporting Timer 2 channel 1: `X1`
- is employed by the test script. Must be connected to the IR diode as described
- below.
- 2. `freq=default` The carrier frequency in Hz. The default for NEC is 38000,
- Sony is 40000 and Philips is 36000.
- 3. `verbose=False` If `True` emits debug output.
-
-The `SONY` constructor is of form `pin, bits=12, freq=40000, verbose=False`.
-The `bits` value may be 12, 15 or 20 to set SIRC variant in use. Other args are
-as above.
-
-Method:
- 1. `transmit(addr, data, toggle=0)` Integer args. `addr` and `data` are
- normally 8-bit values and `toggle` is normally 0 or 1.  
- In the case of NEC, if an address < 256 is passed, normal mode is assumed and
- the complementary value is appended. 16-bit values are transmitted as extended
- addresses.  
- In the case of NEC the `toggle` value is ignored. For Philips protocols it
- should be toggled each time a button is pressed, and retained if the button is
- held down. The test program illustrates a way to do this.  
- `SONY` ignores `toggle` unless in 20-bit mode, in which case it is transmitted
- as the `extended` value and can be any integer in range 0 to 255.
-
-The `transmit` method is synchronous with rapid return. Actual transmission
-occurs as a background process, 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.
-
-# 5.1 Wiring
-
-I use the following circuit which delivers just under 40mA to the diode. R2 may
-be reduced for higher current.  
-![Image](images/circuit.png)
-
-This alternative delivers a constant current of about 53mA if a higher voltage
-than 5V is available. R4 determines the current value and may be reduced to
-increase power.  
-![Image](images/circuit2.png)
-
-The transistor type is not critical.
-
-The driver assumes circuits as shown. Here the carrier "off" state is 0V,
-which is the driver default. If using a circuit where "off" is required to be
-3.3V, the constant `_SPACE` in `ir_tx.py` should be changed to 100.
-
-# 5.2 Principle of operation
-
-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
-array. On completion `transmit` appends a special `STOP` value and initiates
-physical transmission which occurs in an interrupt context.
-
-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. The NEC
-protocol defaults to 50%, the Sony and Philips ones to 30%.
-
-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.
-
-The `IR.append` enables times to be added to the array, keeping track of the
-notional carrier on/off state for biphase generation. The `IR.add` method
-facilitates lengthening a pulse as required in the biphase sequences used in
-Philips protocols.
-
-# 6. References
-
-[General information about IR](https://www.sbprojects.net/knowledge/ir/)
-
-The NEC protocol:  
-[altium](http://techdocs.altium.com/display/FPGA/NEC+Infrared+Transmission+Protocol)  
-[circuitvalley](http://www.circuitvalley.com/2013/09/nec-protocol-ir-infrared-remote-control.html)
-
-Philips protocols:  
-[RC5](https://en.wikipedia.org/wiki/RC-5)  
-[RC6](https://www.sbprojects.net/knowledge/ir/rc6.php)
-
-Sony protocol:  
-[SIRC](https://www.sbprojects.net/knowledge/ir/sirc.php)
-
-# Appendix 1 NEC Protocol description
-
-A normal burst comprises exactly 68 edges, the exception being a repeat code
-which has 4. An incorrect number of edges is treated as an error. All bursts
-begin with a 9ms pulse. In a normal code this is followed by a 4.5ms space; a
-repeat code is identified by a 2.25ms space. A data burst lasts for 67.5ms.
-
-Data bits comprise a 562.5µs mark followed by a space whose length determines
-the bit value. 562.5µs denotes 0 and 1.6875ms denotes 1.
-
-In 8 bit address mode the complement of the address and data values is sent to
-provide error checking. This also ensures that the number of 1's and 0's in a
-burst is constant, giving a constant burst length of 67.5ms. In extended
-address mode this constancy is lost. The burst length can (by my calculations)
-run to 76.5ms.
-
-A pin interrupt records the time of every state change (in µs). The first
-interrupt in a burst sets an event, passing the time of the state change. A
-coroutine waits on the event, yields for the duration of a data burst, then
-decodes the stored data before calling the user-specified callback.
-
-Passing the time to the `Event` instance enables the coro to compensate for
-any asyncio latency when setting its delay period.
-
-The algorithm promotes interrupt handler speed over RAM use: the 276 bytes used
-for the data array could be reduced to 69 bytes by computing and saving deltas
-in the interrupt service routine.
+The transmitter requires a Pyboard 1.x (not Lite), a Pyboard D or an ESP32.
+Output is via an IR LED which will need a transistor to provide sufficient
+current. The ESP32 has significant limitations as a transmitter discussed
+[here](./TRANSMITTER.md#52-esp32).
diff --git a/RECEIVER.md b/RECEIVER.md
new file mode 100644 (file)
index 0000000..db0f164
--- /dev/null
@@ -0,0 +1,264 @@
+# IR Receiver
+
+##### [Main README](./README.md#1-ir-communication)
+
+# 1. Hardware Requirements
+
+The receiver is cross-platform. It requires an IR receiver chip to demodulate
+the carrier. The chip must be selected for the frequency in use by the remote.
+For 38KHz devices a receiver chip such as the Vishay TSOP4838 or the
+[adafruit one](https://www.adafruit.com/products/157) is required. This
+demodulates the 38KHz IR pulses and passes the demodulated pulse train to the
+microcontroller. The tested chip returns a 0 level on carrier detect, but the
+driver design ensures operation regardless of sense.
+
+In my testing a 38KHz demodulator worked with 36KHz and 40KHz remotes, but this
+is obviously neither guaranteed nor optimal.
+
+The pin used to connect the decoder chip to the target is arbitrary. The test
+program assumes pin X3 on the Pyboard, pin 23 on ESP32 and pin 13 on ESP8266.
+On the WeMos D1 Mini the equivalent pin is D7.
+
+A remote using the NEC protocol is [this one](https://www.adafruit.com/products/389).
+
+# 2. Installation and demo script
+
+The receiver is a Python package. This minimises RAM usage: applications only
+import the device driver for the protocol in use.
+
+
+Copy the following to the target filesystem:
+ 1. `ir_rx` Directory and contents.
+
+There are no dependencies.
+
+The demo can be used to characterise IR remotes. It displays the codes returned
+by each button. This can aid in the design of receiver applications. The demo
+prints "running" every 5 seconds and reports any data received from the remote.
+
+```python
+from ir_rx.test import test
+```
+Instructions will be displayed at the REPL.
+
+# 3. The driver
+
+This implements a class for each supported protocol. Each class is subclassed
+from a common abstract base class in `__init__.py`.
+
+Applications should instantiate the appropriate class with a callback. The
+callback will run whenever an IR pulse train is received. Example running on a
+Pyboard:
+```python
+import time
+from machine import Pin
+from pyb import LED
+from ir_rx.nec import NEC_8  # NEC remote, 8 bit addresses
+
+red = LED(1)
+
+def callback(data, addr, ctrl):
+    if data < 0:  # NEC protocol sends repeat codes.
+        print('Repeat code.')
+    else:
+        print('Data {:02x} Addr {:04x}'.format(data, addr))
+
+ir = NEC_8(Pin('X3', Pin.IN), callback)
+while True:
+    time.sleep_ms(500)
+    red.toggle()
+```
+
+#### Common to all classes
+
+Constructor:  
+Args:  
+ 1. `pin` is a `machine.Pin` instance configured as an input, connected to the
+ IR decoder chip.  
+ 2. `callback` is the user supplied callback.
+ 3. `*args` Any further args will be passed to the callback.  
+
+The user callback takes the following args:  
+ 1. `data` (`int`) Value from the remote. Normally in range 0-255. A value < 0
+ signifies an NEC repeat code.
+ 2. `addr` (`int`) Address from the remote.
+ 3. `ctrl` (`int`) The meaning of this is protocol dependent:  
+ NEC: 0  
+ Philips: this is toggled 1/0 on repeat button presses. If the button is held
+ down it is not toggled. The  transmitter demo implements this behaviour.  
+ Sony: 0 unless receiving a 20-bit stream, in which case it holds the extended
+ value.
+ 4. Any args passed to the constructor.
+
+Bound variable:  
+ 1. `verbose=False` If `True` emits debug output.
+
+Method:
+ 1. `error_function` Arg: a function taking a single `int` arg. If this is
+ specified it will be called if an error occurs. The value corresponds to the
+ error code (see below). Typical usage might be to provide some user feedback
+ of incorrect reception although beware of occasional triggers by external
+ events. In my testing the TSOP4838 produces 200µs pulses on occasion for no
+ obvious reason. See [section 4](./RECEIVER.md#4-errors).
+
+A function is provided to print errors in human readable form. This may be
+invoked as follows:
+
+```python
+from ir_rx.print_error import print_error  # Optional print of error codes
+# Assume ir is an instance of an IR receiver class
+ir.error_function(print_error)
+```
+Class variables:
+ 1. These are constants defining the NEC repeat code and the error codes sent
+ to the error function. They are discussed in [section 4](./RECEIVER.md#4-errors).
+
+#### NEC classes
+
+`NEC_8`, `NEC_16`
+
+Typical invocation:
+```python
+from ir_rx.nec import NEC_8
+```
+
+Remotes using the NEC protocol can send 8 or 16 bit addresses. If the `NEC_16`
+class receives an 8 bit address it will get a 16 bit value comprising the
+address in bits 0-7 and its one's complement in bits 8-15.  
+The `NEC_8` class enables error checking for remotes that return an 8 bit
+address: the complement is checked and the address returned as an 8-bit value.
+A 16-bit address will result in an error.
+
+#### Sony classes
+
+`SONY_12`, `SONY_15`, `SONY_20`
+
+Typical invocation:
+```python
+from ir_rx.sony import SONY_15
+```
+
+The SIRC protocol comes in 3 variants: 12, 15 and 20 bits. `SONY_20` handles
+bitstreams from all three types of remote. Choosing a class matching the remote
+improves the timing reducing the likelihood of errors when handling repeats: in
+20-bit mode SIRC timing when a button is held down is tight. A worst-case 20
+bit block takes 39ms nominal, yet the repeat time is 45ms nominal.  
+A single physical remote can issue more than one type of bitstream. The Sony
+remote tested issued both 12 bit and 15 bit streams.
+
+#### Philips classes
+
+`RC5_IR`, `RC6_M0`
+
+Typical invocation:
+```python
+from ir_rx.philips import RC5_IR
+```
+
+These support the RC-5 and RC-6 mode 0 protocols respectively.
+
+# 4. Errors
+
+IR reception is inevitably subject to errors, notably if the remote is operated
+near the limit of its range, if it is not pointed at the receiver or if its
+batteries are low. The user callback is not called when an error occurs.
+
+On ESP8266 and ESP32 there is a further source of errors. This results from the
+large and variable interrupt latency of the device which can exceed the pulse
+duration. This causes pulses to be missed or their timing measured incorrectly.
+On ESP8266 some improvment may be achieved by running the chip at 160MHz.
+
+In general applications should provide user feedback of correct reception.
+Users tend to press the key again if the expected action is absent.
+
+In debugging a callback can be specified for reporting errors. The value passed
+to the error function are represented by constants indicating the cause of the
+error. These are driver ABC class variables and are as follows:
+
+`BADSTART` A short (<= 4ms) start pulse was received. May occur due to IR
+interference, e.g. from fluorescent lights. The TSOP4838 is prone to producing
+200µs pulses on occasion, especially when using the ESP8266.  
+`BADBLOCK` A normal data block: too few edges received. Occurs on the ESP8266
+owing to high interrupt latency.  
+`BADREP` A repeat block: an incorrect number of edges were received.  
+`OVERRUN` A normal data block: too many edges received.  
+`BADDATA` Data did not match check byte.  
+`BADADDR` (`NEC_IR`) If `extended` is `False` the 8-bit address is checked
+against the check byte. This code is returned on failure.  
+
+# 5. Receiver platforms
+
+Currently the ESP8266 suffers from [this issue](https://github.com/micropython/micropython/issues/5714).
+Testing was therefore done without WiFi connectivity. If the application uses
+the WiFi regularly, or if an external process pings the board repeatedly, the
+crash does not occur.
+
+Philips protocols (especially RC-6) have tight timing constraints with short
+pulses whose length must be determined with reasonable accuracy. The Sony 20
+bit protocol also has a timing issue in that the worst case bit pattern takes
+39ms nominal, yet the repeat time is 45ms nominal. These issues can lead to
+errors particularly on slower targets. As discussed above, errors are to be
+expected. It is up to the user to decide if the error rate is acceptable.
+
+Reception was tested using Pyboard D SF2W, ESP8266 and ESP32 with signals from
+remote controls (where available) and from the tranmitter in this repo. Issues
+are listed below.
+
+NEC: No issues.  
+Sony 12 and 15 bit: No issues.  
+Sony 20 bit: On ESP32 some errors occurred when repeats occurred.  
+Philips RC-5: On ESP32 with one remote control many errors occurred, but paired
+with the transmitter in this repo it worked.  
+Philips RC-6: No issues. Only tested against the transmitter in this repo.
+
+# 6. Principle of operation
+
+Protocol classes inherit from the abstract base class `IR_RX`. This uses a pin
+interrupt to store in an array the time (in μs) of each transition of the pulse
+train from the receiver chip. Arrival of the first edge starts a software timer
+which runs for the expected duration of an IR block (`tblock`). The use of a
+software timer ensures that `.decode` and the user callback can allocate.
+
+When the timer times out its callback (`.decode`) decodes the data. `.decode`
+is a method of the protocol specific subclass; on completion it calls the
+`do_callback` method of the ABC. This resets the edge reception and calls
+either the user callback or the error function (if provided). 
+
+The size of the array and the duration of the timer are protocol dependent and
+are set by the subclasses. The `.decode` method is provided in the subclass.
+
+CPU times used by `.decode` (not including the user callback) were measured on
+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. References
+
+[General information about IR](https://www.sbprojects.net/knowledge/ir/)
+
+The NEC protocol:  
+[altium](http://techdocs.altium.com/display/FPGA/NEC+Infrared+Transmission+Protocol)  
+[circuitvalley](http://www.circuitvalley.com/2013/09/nec-protocol-ir-infrared-remote-control.html)
+
+Philips protocols:  
+[RC5](https://en.wikipedia.org/wiki/RC-5)  
+[RC5](https://www.sbprojects.net/knowledge/ir/rc5.php)  
+[RC6](https://www.sbprojects.net/knowledge/ir/rc6.php)
+
+Sony protocol:  
+[SIRC](https://www.sbprojects.net/knowledge/ir/sirc.php)
+
+# Appendix 1 NEC Protocol description
+
+A normal burst comprises exactly 68 edges, the exception being a repeat code
+which has 4. An incorrect number of edges is treated as an error. All bursts
+begin with a 9ms pulse. In a normal code this is followed by a 4.5ms space; a
+repeat code is identified by a 2.25ms space. A data burst lasts for 67.5ms.
+
+Data bits comprise a 562.5µs mark followed by a space whose length determines
+the bit value. 562.5µs denotes 0 and 1.6875ms denotes 1.
+
+In 8 bit address mode the complement of the address and data values is sent to
+provide error checking. This also ensures that the number of 1's and 0's in a
+burst is constant, giving a constant burst length of 67.5ms. In extended
+address mode this constancy is lost. The burst length can (by my calculations)
+run to 76.5ms.
diff --git a/TRANSMITTER.md b/TRANSMITTER.md
new file mode 100644 (file)
index 0000000..3478ba8
--- /dev/null
@@ -0,0 +1,193 @@
+# IR Transmitter
+
+##### [Main README](./README.md#1-ir-communication)
+
+# 1. Hardware Requirements
+
+The transmitter requires a Pyboard 1.x (not Lite), a Pyboard D or an ESP32.
+Output is via an IR LED which will need a transistor to provide sufficient
+current. Typically these need 50-100mA of drive to achieve reasonable range and
+data integrity. A suitable 940nm LED is [this one](https://www.adafruit.com/product/387).
+
+On the Pyboard the transmitter test script assumes pin X1 for IR output. It can
+be changed, but it must support Timer 2 channel 1. Pins for pushbutton inputs
+are arbitrary: X3 and X4 are used. The driver uses timers 2 and 5.
+
+On ESP32 pin 23 is used for IR output and pins 18 and 19 for pushbuttons. The
+ESP32 solution has limitations discussed in [section 5.2](./TRANSMITTER.md#52-esp32).
+
+## 1.1 Wiring
+
+I use the following circuit which delivers just under 40mA to the diode. R2 may
+be reduced for higher current.  
+![Image](images/circuit.png)
+
+This alternative delivers a constant current of about 53mA if a higher voltage
+than 5V is available. R4 determines the current value and may be reduced to
+increase power.  
+![Image](images/circuit2.png)
+
+The transistor type is not critical.
+
+The driver assumes circuits as shown. Here the carrier "off" state is 0V,
+which is the driver default. If using a circuit where "off" is required to be
+3.3V, the constant `_SPACE` in `ir_tx.__init__.py` should be changed to 100.
+
+# 2. Installation
+
+The transmitter is a Python package. This minimises RAM usage: applications
+only import the device driver for the protocol in use.
+
+Copy the following to the target filesystem:
+ 1. `ir_tx` Directory and contents.
+
+The device driver has no dependencies.
+
+The demo program requires `uasyncio` from the official library and `aswitch.py`
+from [this repo](https://github.com/peterhinch/micropython-async). The demo is
+of a 2-button remote controller with auto-repeat. It may be run by issuing:
+
+```python
+from ir_tx.test import test
+```
+Instructions will be displayed at the REPL.
+
+# 3. The driver
+
+This is specific to Pyboard D, Pyboard 1.x (not Lite) and ESP32.
+
+It implements a class for each supported protocol, namely `NEC`, `SONY_12`,
+`SONY_15`, `SONY_20`, `RC5` and `RC6_M0`.  Each class is subclassed from a
+common abstract base class in `__init__.py`. The application instantiates the
+appropriate class and calls the `transmit` method to send data.
+
+The ESP32 platform is marginal in this application because of imprecision in
+its timing. The Philips protocols are unsupported as they require unachievable
+levels of precision. Test results are discussed [here](./TRANSMITTER.md#52-esp32).
+
+Constructor  
+All constructors take the following args:  
+ 1. `pin` An initialised `pyb.Pin` instance supporting Timer 2 channel 1: `X1`
+ is employed by the test script. Must be connected to the IR diode as described
+ below.
+ 2. `freq=default` The carrier frequency in Hz. The default for NEC is 38000,
+ Sony is 40000 and Philips is 36000.
+ 3. `verbose=False` If `True` emits 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.
+
+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.
+
+#### NEC class
+
+This has an additional method `.repeat` (no args). This causes a repeat code to
+be transmitted. Should be called every 108ms if a button is held down.
+
+The NEC protocol accepts 8 or 16 bit addresses. In the former case, a 16 bit
+value is transmitted comprising the 8 bit address and its one's complement,
+enabling the receiver to perform a simple error check. The `NEC` class supports
+these modes by checking the value of `addr` passed to `.transmit` and sending
+the complement for 8 bit values.
+
+`toggle` is ignored.
+
+#### Sony classes
+
+The SIRC protocol supports three sizes, supported by the following classes:
+ 1. 12 bit (7 data, 5 address) `SONY_12`
+ 2. 15 bit (7 data, 8 address) `SONY_15`
+ 3. 20 bit (7 data, 5 addresss, 8 extended) `SONY_20`
+
+The `.transmit` method masks `addr` and `data` values to the widths listed
+above. `toggle` is ignored except by `SONY_20` which treats it as the extended
+value.
+
+#### Philips classes
+
+The RC-5 protocol supports a 5 bit address and 6 or 7 bit (RC5X) data. The
+driver uses the appropriate mode depending on the `data` value provided.
+
+The RC-6 protocol accepts 8 bit address and data values.
+
+Both send a `toggle` bit which remains constant if a button is held down, but
+changes when the button is released. The application should implement this
+behaviour, setting the `toggle` arg of `.transmit` to 0 or 1 as required.
+
+# 4. Test results
+
+# 5. Principle of operation
+
+## 5.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
+array. This is done by two methods of the base class, `.append` and `.add`. The
+former takes a list of times (in μs) and appends them to the array. A bound
+variable `.carrier` keeps track of the notional on/off state of the carrier:
+this is required for bi-phase (manchester) codings.
+
+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.
+
+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. The NEC
+protocol defaults to 50%, the Sony and Philips ones to 30%.
+
+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.
+
+## 5.2 ESP32
+
+This is something of a hack because my drivers work with standard firmware.
+
+A much better solution will be possible when the `esp32.RMT` class supports the
+`carrier` option. A fork supporting this is
+[here](https://github.com/mattytrentini/micropython). You may want to adapt the
+base class to use this fork: it should be easy and would produce a solution
+capable of handling all protocols.
+
+A consequence of this hack is that timing is imprecise. In testing NEC
+protocols were reliable. Sony delivered some erroneous bitsreams but may be
+usable. Philips protocols require timing precision which is unachievable; these
+are unsupported and an exception will be thrown on an attempt to instantiate a
+Philips class on an ESP32.
+
+The ABC stores durations in Hz rather than in μs. This is because the `period`
+arg of `Timer.init` expects an integer number of ms. Passing a `freq` value
+enables slightly higher resolution timing. In practice timing lacks precision
+with the code having a hack which subtracts a nominal amount from each value to
+compensate for the typical level of overrun.
+
+The carrier is generated by PWM instance `.pwm` with its duty cycle controlled
+by software timer `._tim` in a similar way to the Pyboard Timer 5 described
+above. The ESP32 duty value is in range 0-1023 as against 0-100 on the Pyboard.
+
+# 6. References
+
+[General information about IR](https://www.sbprojects.net/knowledge/ir/)
+
+The NEC protocol:  
+[altium](http://techdocs.altium.com/display/FPGA/NEC+Infrared+Transmission+Protocol)  
+[circuitvalley](http://www.circuitvalley.com/2013/09/nec-protocol-ir-infrared-remote-control.html)
+
+Philips protocols:  
+[RC5](https://en.wikipedia.org/wiki/RC-5)  
+[RC5](https://www.sbprojects.net/knowledge/ir/rc5.php)  
+[RC6](https://www.sbprojects.net/knowledge/ir/rc6.php)
+
+Sony protocol:  
+[SIRC](https://www.sbprojects.net/knowledge/ir/sirc.php)
index 354a6564a614c2f4940a80e02522eb599fd59141..112db2ee9cc3291fe07a6d1b12981ea536fb81c5 100644 (file)
@@ -32,7 +32,7 @@ def cb(data, addr, ctrl):
     else:
         print('Data {:02x} Addr {:04x} Ctrl {:02x}'.format(data, addr, ctrl))
 
     else:
         print('Data {:02x} Addr {:04x} Ctrl {:02x}'.format(data, addr, ctrl))
 
-def run(proto=0):
+def test(proto=0):
     classes = (NEC_8, NEC_16, SONY_12, SONY_15, SONY_20, RC5_IR, RC6_M0)
     ir = classes[proto](p, cb)  # Instantiate receiver
     ir.error_function(print_error)  # Show debug information
     classes = (NEC_8, NEC_16, SONY_12, SONY_15, SONY_20, RC5_IR, RC6_M0)
     ir = classes[proto](p, cb)  # Instantiate receiver
     ir.error_function(print_error)  # Show debug information
@@ -46,13 +46,13 @@ def run(proto=0):
 # **** DISPLAY GREETING ****
 s = '''Test for IR receiver. Run:
 from ir_rx import test
 # **** DISPLAY GREETING ****
 s = '''Test for IR receiver. Run:
 from ir_rx import test
-test.run() for NEC 8 bit protocol,
-test.run(1) for NEC 16 bit,
-test.run(2) for Sony SIRC 12 bit,
-test.run(3) for Sony SIRC 15 bit,
-test.run(4) for Sony SIRC 20 bit,
-test.run(5) for Philips RC-5 protocol,
-test.run(6) for RC6 mode 0.
+test() for NEC 8 bit protocol,
+test(1) for NEC 16 bit,
+test(2) for Sony SIRC 12 bit,
+test(3) for Sony SIRC 15 bit,
+test(4) for Sony SIRC 20 bit,
+test(5) for Philips RC-5 protocol,
+test(6) for RC6 mode 0.
 
 Hit ctrl-c to stop, then ctrl-d to soft reset.'''
 
 
 Hit ctrl-c to stop, then ctrl-d to soft reset.'''
 
diff --git a/ir_rx_test.py b/ir_rx_test.py
deleted file mode 100644 (file)
index badd2b5..0000000
+++ /dev/null
@@ -1,59 +0,0 @@
-# ir_rx_test.py Test program for IR remote control decoder arem.py
-# Supports Pyboard and ESP8266
-
-# Author: Peter Hinch
-# Copyright Peter Hinch 2020 Released under the MIT license
-
-# Run this to characterise a remote.
-
-from sys import platform
-import time
-import gc
-from machine import Pin, freq
-from ir_rx.print_error import print_error  # Optional print of error codes
-# Import all implemented classes
-from ir_rx.nec import NEC_8, NEC_16
-from ir_rx.sony import SONY_12, SONY_15, SONY_20
-from ir_rx.philips import RC5_IR, RC6_M0
-
-# Define pin according to platform
-if platform == 'pyboard':
-    p = Pin('X3', Pin.IN)
-elif platform == 'esp8266':
-    freq(160000000)
-    p = Pin(13, Pin.IN)
-elif platform == 'esp32' or platform == 'esp32_LoBo':
-    p = Pin(23, Pin.IN)  # was 27
-
-# User callback
-def cb(data, addr, ctrl):
-    if data < 0:  # NEC protocol sends repeat codes.
-        print('Repeat code.')
-    else:
-        print('Data {:02x} Addr {:04x} Ctrl {:02x}'.format(data, addr, ctrl))
-
-def test(proto=0):
-    classes = (NEC_8, NEC_16, SONY_12, SONY_15, SONY_20, RC5_IR, RC6_M0)
-    ir = classes[proto](p, cb)  # Instantiate receiver
-    ir.error_function(print_error)  # Show debug information
-    #ir.verbose = True
-    # A real application would do something here...
-    while True:
-        print('running')
-        time.sleep(5)
-        gc.collect()
-
-# **** DISPLAY GREETING ****
-s = '''Test for IR receiver. Run:
-from ir_rx_test import test
-test() for NEC 8 bit protocol,
-test(1) for NEC 16 bit,
-test(2) for Sony SIRC 12 bit,
-test(3) for Sony SIRC 15 bit,
-test(4) for Sony SIRC 20 bit,
-test(5) for Philips RC-5 protocol,
-test(6) for RC6 mode 0.
-
-Hit ctrl-c to stop, then ctrl-d to soft reset.'''
-
-print(s)
diff --git a/ir_tx.py b/ir_tx.py
deleted file mode 100644 (file)
index 4a91336..0000000
--- a/ir_tx.py
+++ /dev/null
@@ -1,175 +0,0 @@
-# ir_tx.py Nonblocking IR blaster
-# Runs on Pyboard D or Pyboard 1.x only (not Pyboard Lite)
-
-# Released under the MIT License (MIT). See LICENSE.
-
-# Copyright (c) 2020 Peter Hinch
-
-from pyb import Pin, Timer
-from micropython import const
-from array import array
-import micropython
-
-# micropython.alloc_emergency_exception_buf(100)
-
-# Common
-_SPACE = const(0)  # Or 100. Depends on wiring: 0 assumes logic 0 turns IR off.
-_STOP = const(0)  # End of data
-# NEC
-_TBURST = const(563)
-_T_ONE = const(1687)
-# RC5
-_T_RC5 = const(889)  # Time for pulse of carrier
-# RC6_M0
-_T_RC6 = const(444)
-_T2_RC6 = const(889)
-
-# 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.
-class IR:
-
-    def __init__(self, pin, freq, asize, duty, verbose):
-        tim = Timer(2, freq=freq)  # Timer 2/pin produces 36/38KHz carrier
-        self._ch = tim.channel(1, Timer.PWM, pin=pin)
-        self._ch.pulse_width_percent(_SPACE)  # Turn off IR LED
-        self._duty = duty if not _SPACE else (100 - duty)
-        self._tim = Timer(5)  # Timer 5 controls carrier on/off times
-        self._tcb = self.cb  # Pre-allocate
-        self.verbose = verbose
-        self.arr = array('H', 0 for _ in range(asize))  # on/off times (μs)
-        self.carrier = False  # Notional carrier state while encoding biphase
-        self.aptr = 0  # Index into array
-
-    # Before populating array, zero pointer, set notional carrier state (off).
-    def transmit(self, addr, data, toggle=0):  # NEC: toggle is unused
-        self.aptr = 0  # Inital conditions for tx: index into array
-        self.carrier = False
-        self.tx(addr, data, toggle)
-        self.append(_STOP)
-        self.aptr = 0  # Reset pointer
-        self.cb(self._tim)  # Initiate physical transmission.
-
-    def cb(self, t):  # T5 callback, generate a carrier mark or space
-        t.deinit()
-        p = self.aptr
-        v = self.arr[p]
-        if v == _STOP:
-            self._ch.pulse_width_percent(_SPACE)  # Turn off IR LED.
-            return
-        self._ch.pulse_width_percent(_SPACE if p & 1 else self._duty)
-        self._tim.init(prescaler=84, period=v, callback=self._tcb)
-        self.aptr += 1
-
-    def append(self, *times):  # Append one or more time peiods to .arr
-        for t in times:
-            self.arr[self.aptr] = t
-            self.aptr += 1
-            self.carrier = not self.carrier  # Keep track of carrier state
-            self.verbose and print('append', t, 'carrier', self.carrier)
-
-    def add(self, t):  # Increase last time value
-        self.verbose and print('add', t)
-        self.arr[self.aptr - 1] += t  # Carrier unaffected
-
-# NEC protocol
-class NEC(IR):
-
-    def __init__(self, pin, freq=38000, verbose=False):  # NEC specifies 38KHz
-        super().__init__(pin, freq, 68, 50, verbose)
-
-    def _bit(self, b):
-        self.append(_TBURST, _T_ONE if b else _TBURST)
-
-    def tx(self, addr, data, _):  # Ignore toggle
-        self.append(9000, 4500)
-        if addr < 256:  # Short address: append complement
-            addr |= ((addr ^ 0xff) << 8)
-        for _ in range(16):
-            self._bit(addr & 1)
-            addr >>= 1
-        data |= ((data ^ 0xff) << 8)
-        for _ in range(16):
-            self._bit(data & 1)
-            data >>= 1
-        self.append(_TBURST)
-
-    def repeat(self):
-        self.aptr = 0
-        self.append(9000, 2250, _TBURST, _STOP)
-        self.aptr = 0  # Reset pointer
-        self.cb(self._tim)  # Initiate physical transmission.
-
-# NEC protocol
-class SONY(IR):
-
-    def __init__(self, pin, bits=12, freq=40000, verbose=False):  # Sony specifies 40KHz
-        super().__init__(pin, freq, 3 + bits * 2, 30, verbose)
-        if bits not in (12, 15, 20):
-            raise ValueError('bits must be 12, 15 or 20.')
-        self.bits = bits
-
-    def tx(self, addr, data, ext):
-        self.append(2400, 600)
-        bits = self.bits
-        v = data & 0x7f
-        if bits == 12:
-            v |= (addr & 0x1f) << 7
-        elif bits == 15:
-            v |= (addr & 0xff) << 7
-        else:
-            v |= (addr & 0x1f) << 7
-            v |= (ext & 0xff) << 12
-        for _ in range(bits):
-            self.append(1200 if v & 1 else 600, 600)
-            v >>= 1
-
-
-# Philips RC5 protocol
-class RC5(IR):
-
-    def __init__(self, pin, freq=36000, verbose=False):
-        super().__init__(pin, freq, 28, 30, verbose)
-
-    def tx(self, addr, data, toggle):
-        d = (data & 0x3f) | ((addr & 0x1f) << 6) | ((data & 0x40) << 6) | ((toggle & 1) << 11)
-        self.verbose and print(bin(d))
-        mask = 0x2000
-        while mask:
-            if mask == 0x2000:
-                self.append(_T_RC5)
-            else:
-                bit = bool(d & mask)
-                if bit ^ self.carrier:
-                    self.add(_T_RC5)
-                    self.append(_T_RC5)
-                else:
-                    self.append(_T_RC5, _T_RC5)
-            mask >>= 1
-
-# Philips RC6 mode 0 protocol
-class RC6_M0(IR):
-
-    def __init__(self, pin, freq=36000, verbose=False):
-        super().__init__(pin, freq, 44, 30, verbose)
-
-    def tx(self, addr, data, toggle):
-        # leader, 1, 0, 0, 0
-        self.append(2666, _T2_RC6, _T_RC6, _T2_RC6, _T_RC6, _T_RC6, _T_RC6, _T_RC6, _T_RC6)
-        # Append a single bit of twice duration
-        if toggle:
-            self.add(_T2_RC6)
-            self.append(_T2_RC6)
-        else:
-            self.append(_T2_RC6, _T2_RC6)
-        d = (data & 0xff) | ((addr & 0xff) << 8)
-        mask = 0x8000
-        self.verbose and print('toggle', toggle, self.carrier, bool(d & mask))
-        while mask:
-            bit = bool(d & mask)
-            if bit ^ self.carrier:
-                self.append(_T_RC6, _T_RC6)
-            else:
-                self.add(_T_RC6)
-                self.append(_T_RC6)
-            mask >>= 1
diff --git a/ir_tx/__init__.py b/ir_tx/__init__.py
new file mode 100644 (file)
index 0000000..ef52e84
--- /dev/null
@@ -0,0 +1,113 @@
+# __init__.py Nonblocking IR blaster
+# Runs on Pyboard D or Pyboard 1.x (not Pyboard Lite) and ESP32
+
+# Released under the MIT License (MIT). See LICENSE.
+
+# Copyright (c) 2020 Peter Hinch
+from sys import platform
+ESP32 = platform == 'esp32' or platform == 'esp32_LoBo'
+if ESP32:
+    from machine import Pin, Timer, PWM, freq
+else:
+    from pyb import Pin, Timer  # Pyboard does not support machine.PWM
+
+from micropython import const
+from array import array
+import micropython
+
+# micropython.alloc_emergency_exception_buf(100)
+
+# ABC only
+_SPACE = const(0)
+# If the wiring is such that 3.3V turns the LED off, set _SPACE as follows
+# On Pyboard 100, on ESP32 1023
+# Shared by NEC
+STOP = const(0)  # End of data
+
+# 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.
+class IR:
+
+    def __init__(self, pin, cfreq, asize, duty, verbose):
+        if ESP32:
+            freq(240000000)
+            self._pwm = PWM(pin)  # Produces 36/38/40KHz carrier
+            self._pwm.deinit()
+            self._pwm.init(freq=cfreq, duty=_SPACE)
+            # ESP32: 0 <= duty <= 1023
+            self._duty = round((duty if not _SPACE else (100 - duty)) * 10.23)
+            self._tim = Timer(-1)  # Controls carrier on/off times
+            self._off = self.esp_off  # Turn IR LED off
+            self._onoff = self.esp_onoff  # Set IR LED state and refresh timer
+        else:  # Pyboard
+            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
+            # Pyboard: 0 <= pulse_width_percent <= 100
+            self._duty = duty if not _SPACE else (100 - duty)
+            self._tim = Timer(5)  # Timer 5 controls carrier on/off times
+            self._off = self.pb_off
+            self._onoff = self.pb_onoff
+        self._tcb = self.cb  # Pre-allocate
+        self.verbose = verbose
+        self.arr = array('H', 0 for _ in range(asize))  # on/off times (μs)
+        self.carrier = False  # Notional carrier state while encoding biphase
+        self.aptr = 0  # Index into array
+
+    # Before populating array, zero pointer, set notional carrier state (off).
+    def transmit(self, addr, data, toggle=0):  # NEC: toggle is unused
+        self.aptr = 0  # Inital conditions for tx: index into array
+        self.carrier = False
+        self.tx(addr, data, toggle)
+        self.append(STOP)
+        self.aptr = 0  # Reset pointer
+        self.cb(self._tim)  # Initiate physical transmission.
+
+    # Turn IR LED off (pyboard and ESP32 variants)
+    def pb_off(self):
+        self._ch.pulse_width_percent(_SPACE)
+
+    def esp_off(self):
+        self._pwm.duty(_SPACE)
+
+    # Turn IR LED on or off and re-initialise timer (pyboard and ESP32 variants)
+    @micropython.native
+    def pb_onoff(self, p, v):
+        self._ch.pulse_width_percent(_SPACE if p & 1 else self._duty)
+        self._tim.init(prescaler=84, period=v, callback=self._tcb)
+
+    @micropython.native
+    def esp_onoff(self, p, v):
+        self._pwm.duty(_SPACE if p & 1 else self._duty)
+        self._tim.init(mode=Timer.ONE_SHOT, freq=v, callback=self.cb)  
+
+    def cb(self, t):  # T5 callback, generate a carrier mark or space
+        t.deinit()
+        p = self.aptr
+        v = self.arr[p]
+        if v == STOP:
+            self._off()  # Turn off IR LED.
+            return
+        self._onoff(p, v)
+        self.aptr += 1
+
+    def append(self, *times):  # Append one or more time peiods to .arr
+        for t in times:
+            if ESP32 and t:
+                t -= 350  # ESP32 sluggishness
+                t = round(1_000_000 / t)  # Store in Hz
+            self.arr[self.aptr] = t
+            self.aptr += 1
+            self.carrier = not self.carrier  # Keep track of carrier state
+            self.verbose and print('append', t, 'carrier', self.carrier)
+
+    def add(self, t):  # Increase last time value
+        assert t > 0
+        self.verbose and print('add', t)
+        # .carrier unaffected
+        if ESP32:
+            t -= 350
+            self.arr[self.aptr - 1] = round((self.arr[self.aptr - 1] / 1_000_000 + t) / 1_000_000)
+        else:
+            self.arr[self.aptr - 1] += t
diff --git a/ir_tx/nec.py b/ir_tx/nec.py
new file mode 100644 (file)
index 0000000..8cf39e7
--- /dev/null
@@ -0,0 +1,38 @@
+# nec.py Encoder for IR remote control using synchronous code
+# NEC protocol.
+
+# Author: Peter Hinch
+# Copyright Peter Hinch 2020 Released under the MIT license
+
+from micropython import const
+from ir_tx import IR, STOP
+
+_TBURST = const(563)
+_T_ONE = const(1687)
+
+class NEC(IR):
+
+    def __init__(self, pin, freq=38000, verbose=False):  # NEC specifies 38KHz
+        super().__init__(pin, freq, 68, 50, verbose)
+
+    def _bit(self, b):
+        self.append(_TBURST, _T_ONE if b else _TBURST)
+
+    def tx(self, addr, data, _):  # Ignore toggle
+        self.append(9000, 4500)
+        if addr < 256:  # Short address: append complement
+            addr |= ((addr ^ 0xff) << 8)
+        for _ in range(16):
+            self._bit(addr & 1)
+            addr >>= 1
+        data |= ((data ^ 0xff) << 8)
+        for _ in range(16):
+            self._bit(data & 1)
+            data >>= 1
+        self.append(_TBURST)
+
+    def repeat(self):
+        self.aptr = 0
+        self.append(9000, 2250, _TBURST, STOP)
+        self.aptr = 0  # Reset pointer
+        self.cb(self._tim)  # Initiate physical transmission.
diff --git a/ir_tx/philips.py b/ir_tx/philips.py
new file mode 100644 (file)
index 0000000..b069361
--- /dev/null
@@ -0,0 +1,68 @@
+# philips.py Encoder for IR remote control using synchronous code
+# RC-5 and RC-6 mode 0 protocols.
+
+# Author: Peter Hinch
+# Copyright Peter Hinch 2020 Released under the MIT license
+
+from micropython import const
+from sys import platform
+ESP32 = platform == 'esp32' or platform == 'esp32_LoBo'
+from ir_tx import IR
+
+# Philips RC5 protocol
+_T_RC5 = const(889)  # Time for pulse of carrier
+ermsg = 'ESP32 does not support Philips protocols'
+class RC5(IR):
+
+    def __init__(self, pin, freq=36000, verbose=False):
+        if ESP32:
+            raise RuntimeError(ermsg)
+        super().__init__(pin, freq, 28, 30, verbose)
+
+    def tx(self, addr, data, toggle):
+        d = (data & 0x3f) | ((addr & 0x1f) << 6) | ((data & 0x40) << 6) | ((toggle & 1) << 11)
+        self.verbose and print(bin(d))
+        mask = 0x2000
+        while mask:
+            if mask == 0x2000:
+                self.append(_T_RC5)
+            else:
+                bit = bool(d & mask)
+                if bit ^ self.carrier:
+                    self.add(_T_RC5)
+                    self.append(_T_RC5)
+                else:
+                    self.append(_T_RC5, _T_RC5)
+            mask >>= 1
+
+# Philips RC6 mode 0 protocol
+_T_RC6 = const(444)
+_T2_RC6 = const(889)
+
+class RC6_M0(IR):
+
+    def __init__(self, pin, freq=36000, verbose=False):
+        if ESP32:
+            raise RuntimeError(ermsg)
+        super().__init__(pin, freq, 44, 30, verbose)
+
+    def tx(self, addr, data, toggle):
+        # leader, 1, 0, 0, 0
+        self.append(2666, _T2_RC6, _T_RC6, _T2_RC6, _T_RC6, _T_RC6, _T_RC6, _T_RC6, _T_RC6)
+        # Append a single bit of twice duration
+        if toggle:
+            self.add(_T2_RC6)
+            self.append(_T2_RC6)
+        else:
+            self.append(_T2_RC6, _T2_RC6)
+        d = (data & 0xff) | ((addr & 0xff) << 8)
+        mask = 0x8000
+        self.verbose and print('toggle', toggle, self.carrier, bool(d & mask))
+        while mask:
+            bit = bool(d & mask)
+            if bit ^ self.carrier:
+                self.append(_T_RC6, _T_RC6)
+            else:
+                self.add(_T_RC6)
+                self.append(_T_RC6)
+            mask >>= 1
diff --git a/ir_tx/sony.py b/ir_tx/sony.py
new file mode 100644 (file)
index 0000000..86a23dd
--- /dev/null
@@ -0,0 +1,45 @@
+# sony.py Encoder for IR remote control using synchronous code
+# Sony SIRC protocol.
+
+# Author: Peter Hinch
+# Copyright Peter Hinch 2020 Released under the MIT license
+
+from micropython import const
+from ir_tx import IR
+
+class SONY_ABC(IR):
+
+    def __init__(self, pin, bits, freq, verbose):
+        super().__init__(pin, freq, 3 + bits * 2, 30, verbose)
+        if bits not in (12, 15, 20):
+            raise ValueError('bits must be 12, 15 or 20.')
+        self.bits = bits
+
+    def tx(self, addr, data, ext):
+        self.append(2400, 600)
+        bits = self.bits
+        v = data & 0x7f
+        if bits == 12:
+            v |= (addr & 0x1f) << 7
+        elif bits == 15:
+            v |= (addr & 0xff) << 7
+        else:
+            v |= (addr & 0x1f) << 7
+            v |= (ext & 0xff) << 12
+        for _ in range(bits):
+            self.append(1200 if v & 1 else 600, 600)
+            v >>= 1
+
+# Sony specifies 40KHz
+class SONY_12(SONY_ABC):
+    def __init__(self, pin, freq=40000, verbose=False):
+        super().__init__(pin, 12, freq, verbose)
+
+class SONY_15(SONY_ABC):
+    def __init__(self, pin, freq=40000, verbose=False):
+        super().__init__(pin, 15, freq, verbose)
+
+class SONY_20(SONY_ABC):
+    def __init__(self, pin, freq=40000, verbose=False):
+        super().__init__(pin, 20, freq, verbose)
+
similarity index 59%
rename from ir_tx_test.py
rename to ir_tx/test.py
index ad55a22acb181d912218968f23acc9b8d4bed069..bb54329affaf379001b6876ab6eccf6da660abd0 100644 (file)
@@ -5,11 +5,18 @@
 # Copyright (c) 2020 Peter Hinch
 
 # Implements a 2-button remote control on a Pyboard with auto repeat.
 # Copyright (c) 2020 Peter Hinch
 
 # Implements a 2-button remote control on a Pyboard with auto repeat.
-
-from pyb import Pin, LED
+from sys import platform
+ESP32 = platform == 'esp32' or platform == 'esp32_LoBo'
+if ESP32:
+    from machine import Pin
+else:
+    from pyb import Pin, LED
 import uasyncio as asyncio
 from aswitch import Switch, Delay_ms
 import uasyncio as asyncio
 from aswitch import Switch, Delay_ms
-from ir_tx import NEC, SONY, RC5, RC6_M0
+# Import all implemented classes
+from ir_tx.nec import NEC
+from ir_tx.sony import SONY_12, SONY_15, SONY_20
+from ir_tx.philips import RC5, RC6_M0
 
 loop = asyncio.get_event_loop()
 
 
 loop = asyncio.get_event_loop()
 
@@ -48,40 +55,50 @@ 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
     # 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 = False  # Rbutton constructor requires False for RC-X. NEC protocol only.
-    pin = Pin('X1')
-    if not proto:
-        irb = NEC(pin)  # Default NEC freq == 38KHz
-        # Option to send REPEAT code. Most remotes do this.
-        rep_code = True
-    elif proto < 4:
-        bits = (12, 15, 20)[proto - 1]
-        irb = SONY(pin, bits, 38000)  # My decoder chip is 38KHz
-    elif proto == 5:
-        irb = RC5(pin, 38000)  # My decoder chip is 38KHz
-    elif proto == 6:
-        irb = RC6_M0(pin, 38000)
+    rep_code = proto == 0  # Rbutton constructor requires False for RC-X. NEC protocol only.
+    pin = Pin(23, Pin.OUT) if ESP32 else Pin('X1')
+    classes = (NEC, SONY_12, SONY_15, SONY_20, RC5, RC6_M0)
+    irb = classes[proto](pin, 38000)  # My decoder chip is 38KHz
 
     b = []  # Rbutton instances
 
     b = []  # Rbutton instances
-    b.append(Rbutton(irb, Pin('X3', Pin.IN, Pin.PULL_UP), 0x1, 0x7, rep_code))
-    b.append(Rbutton(irb, Pin('X4', Pin.IN, Pin.PULL_UP), 0x10, 0xb, rep_code))
-    led = LED(1)
-    while True:
-        await asyncio.sleep_ms(500)  # Obligatory flashing LED.
-        led.toggle()
+    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))
+    if ESP32:
+        while True:
+            print('Running')
+            await asyncio.sleep(5)
+    else:
+        led = LED(1)
+        while True:
+            await asyncio.sleep_ms(500)  # Obligatory flashing LED.
+            led.toggle()
 
 s = '''Test for IR transmitter. Run:
 from ir_tx_test import test
 test() for NEC protocol
 test(1) for Sony SIRC 12 bit
 test(2) for Sony SIRC 15 bit
 
 s = '''Test for IR transmitter. Run:
 from ir_tx_test import test
 test() for NEC protocol
 test(1) for Sony SIRC 12 bit
 test(2) for Sony SIRC 15 bit
-test(3) for Sony SIRC 20 bit
+test(3) for Sony SIRC 20 bit'''
+spb = '''
 test(5) for Philips RC-5 protocol
 test(6) for Philips RC-6 mode 0.
 
 test(5) for Philips RC-5 protocol
 test(6) for Philips RC-6 mode 0.
 
-Ground X3 to send addr 1 data 7
-Ground X4 to send addr 0x10 data 0x0b.'''
+IR LED on pin X1
+Ground pin X3 to send addr 1 data 7
+Ground pin X4 to send addr 0x10 data 0x0b.'''
+sesp = '''
+
+IR LED on pin 23
+Ground pin 18 to send addr 1 data 7
+Ground pin 19 to send addr 0x10 data 0x0b.'''
+if ESP32:
+    s = ''.join((s, sesp))
+else:
+    s = ''.join((s, spb))
 print(s)
 
 print(s)
 
+
 def test(proto=0):
     loop.run_until_complete(main(proto))
 def test(proto=0):
     loop.run_until_complete(main(proto))