]> 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
-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
-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
@@ -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
-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.
 
@@ -39,349 +43,20 @@ dependent.
 
 # 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
-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.
 
-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))
 
-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
@@ -46,13 +46,13 @@ def run(proto=0):
 # **** 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.'''
 
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.
-
-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
-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()
 
@@ -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
-    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.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
-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.
 
-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)
 
+
 def test(proto=0):
     loop.run_until_complete(main(proto))