peterhinch / micropython_ir

Nonblocking device drivers to receive from IR remotes and for IR "blaster" apps.
MIT License
240 stars 51 forks source link

transmit does not work properly when using timer and button IRQ #34

Closed guser99 closed 7 months ago

guser99 commented 8 months ago

Hi, I have recently started using Micropython and am currently using a Raspberry Pi Pico to control an LED strip via button press and timer using your library.

After initial tests with the repl and manual testing of the functions, I have found that when I call these functions using the Timer and Pin.irq functions provided by Micropython, it only works as long as there is only one transmit (Test 1).

Even one sleep prevents the transmit from taking place and the LED strip from lighting up when the function is called up by a timer or by pressing a button (test 2).

When the same function is called up manually, the transmit works (test 3).

It is particularly bad if I have a function in which several transmits are to take place, e.g. if the LED strip is to be switched on and set to the lowest brightness level. After the first transmit has been run through without sending anything, the Pico no longer responds because it seems to be stuck in the loop "while self.busy():" of the transmit function within the ".../micropython_ir-master/ir_tx/init.py" file. The only way to regain access to the Pico is to perform a manual hard reset by briefly disconnecting the Pico from the power supply. (Test 4)

Is the module not timer and button IRQ compatible or how can I get it to work?

This is how I proceed:

git clone https://github.com/peterhinch/micropython_ir

rshell -p /dev/ttyACM0
cp -r micropython_ir-master/ir_tx/ /pyboard/
repl

### repl ### # Basic configuration

from machine import Pin, Timer
from ir_tx.nec import NEC
import time
PIN_IR_TRANSMITTER = Pin(17, Pin.OUT, value=0)
IR_ADDRESS = 0xef00
IR_DATA_ON = 0x03
IR_DATA = 0x03
nec = NEC(PIN_IR_TRANSMITTER)
  1. Test
    
    def timer_or_btn_test(timer_or_pin):
    global IR_ADDRESS, IR_DATA_ON
    print("ON Before")
    nec.transmit(IR_ADDRESS, IR_DATA_ON)
    print("ON After")

Timer().init(mode=Timer.ONE_SHOT, period=200, callback=timer_or_btn_test) btn = Pin(9, Pin.IN, Pin.PULL_DOWN) btn.irq(trigger=Pin.IRQ_RISING, handler=timer_or_btn_test)


result:
- light strip goes on

output:

ON Before ON After

  1. Test (soft reboot)
    
    def timer_or_btn_test(timer_or_pin):
    global IR_ADDRESS, IR_DATA_ON
    print("ON Before")
    nec.transmit(IR_ADDRESS, IR_DATA_ON)
    time.sleep(0.1)
    print("ON After")

Timer().init(mode=Timer.ONE_SHOT, period=200, callback=timer_or_btn_test) btn = Pin(9, Pin.IN, Pin.PULL_DOWN) btn.irq(trigger=Pin.IRQ_RISING, handler=timer_or_btn_test)


result:
- light strip remains off
- the transmit function call is executed, but nothing is sent

output:

ON Before ON After

  1. Test (soft reboot)
    
    def timer_or_btn_test():
    global IR_ADDRESS, IR_DATA_ON
    print("ON Before")
    nec.transmit(IR_ADDRESS, IR_DATA_ON)
    time.sleep(0.1)
    print("ON After")

timer_or_btn_test()


result:
- light strip goes on

output:

timer_or_btn_test() ON Before ON After

  1. Test (soft reboot)
    
    def timer_or_btn_test(timer_or_pin):
    global IR_ADDRESS, IR_DATA_ON
    print("ON Before")
    nec.transmit(IR_ADDRESS, IR_DATA_ON)
    print("ON After")
    for x in range(0, 16):
        print("IR_DATA Before {}".format(x))
        nec.transmit(IR_ADDRESS, IR_DATA)
        print("IR_DATA After {}".format(x))

Timer().init(mode=Timer.ONE_SHOT, period=200, callback=timer_or_btn_test) btn = Pin(9, Pin.IN, Pin.PULL_DOWN) btn.irq(trigger=Pin.IRQ_RISING, handler=timer_or_btn_test)


result:
- light strip remains off and pico does not respond
- the first transmit function call is executed, but nothing is sent
- in the second function call it hangs in the loop (line 92) of the function transmit in the file .../micropython_ir-master/ir_tx/__init__.py
- the only possibility is a hard reset (usb out and in again)

output:

ON Before ON After IR_DATA Before 0

peterhinch commented 8 months ago

In test 1 you transmit from an interrupt service routine (ISR) successfully, presumably by invoking the script manually. The same test fails when invoked by main.py after a reboot (test 2). Yet calling the function directly (rather than via an ISR) works after a soft reboot (test3). Test 4 is definitely wrong. Issuing .transmit causes transmission to occur in the background. It is incorrect to call .transmit in a tight loop because it will be called while the previous transmission is in progress. Also IR_DATA appears to be undefined.

I would suggest inserting a pause after a reboot - it does look as if something is not being initialised correctly.

time.sleep(2)  # Wait before running
def timer_or_btn_test(timer_or_pin):
    global IR_ADDRESS, IR_DATA_ON
    print("ON Before")
    nec.transmit(IR_ADDRESS, IR_DATA_ON)
    time.sleep(0.1)
    print("ON After")

Timer().init(mode=Timer.ONE_SHOT, period=200, callback=timer_or_btn_test)
btn = Pin(9, Pin.IN, Pin.PULL_DOWN)
btn.irq(trigger=Pin.IRQ_RISING, handler=timer_or_btn_test)

As a general point it is not recommended to have sleep() instructions in an ISR, but I don't think this is the cause of your problem.

guser99 commented 7 months ago

Hello, thanks for the quick reply.

I entered all the tests (1-4) directly into the repl via my computer. The "soft reboot" was probably misleading, I just wanted to express that there is a clean system, I pressed Ctrl+D within the repl and then re-entered (copied) everything. I do not (yet) have a main.py that is automatically executed when the Pico is started.

  1. test works via a timer as well as via a button IRQ.
  2. test is like test 1 only with a sleep exactly after the .transmit, but does not work anymore.
  3. is similar to test 2 only this time the function is called manually from the repl and not by a timer or button IRQ, this time it works as I expect it to. The light goes on.
  4. i didn't know that the transmission takes place in the background. This is probably why I had included a sleep during my tests, as it didn't work without the sleep.
from machine import Pin, Timer
from ir_tx.nec import NEC
import time
PIN_IR_TRANSMITTER = Pin(17, Pin.OUT, value=0)
IR_ADDRESS = 0xef00
IR_DATA_ON = 0x03
IR_DATA = 0x01
nec = NEC(PIN_IR_TRANSMITTER)

def timer_or_btn_test(timer_or_pin):
    global IR_ADDRESS, IR_DATA_ON, IR_DATA
    print("ON Before")
    nec.tx(IR_ADDRESS, IR_DATA_ON)
    time.sleep(0.1)
    print("ON After")
    for x in range(0, 4):
        print("IR_DATA Before {}".format(x))
        nec.transmit(IR_ADDRESS, IR_DATA)
        time.sleep(0.1)
        print("IR_DATA After {}".format(x))

The call via a timer or button IRQ is no longer blocked, but nothing is sent that controls the LED strip.

Timer().init(mode=Timer.ONE_SHOT, period=200, callback=timer_or_btn_test)
btn = Pin(9, Pin.IN, Pin.PULL_DOWN)
btn.irq(trigger=Pin.IRQ_RISING, handler=timer_or_btn_test)

Output:

>>> ON Before
ON After
IR_DATA Before 0
IR_DATA After 0
IR_DATA Before 1
IR_DATA After 1
IR_DATA Before 2
IR_DATA After 2
IR_DATA Before 3
IR_DATA After 3

When the function is called up manually via the repl, everything works as expected. The LED strip is switched on and becomes darker. The output is the same as with the timer, except that all commands are sent.

timer_or_btn_test(False)

How can I make the LED strip get darker quickly without using a loop? The brightness of the LED strip can be adjusted in 16 steps. I could not see any function that allows me to send the same command 16 times in a row, for example.

A side question: Why is it not recommended to have sleep() functions in an ISR?

peterhinch commented 7 months ago

The docs state

Actual transmission occurs as a background process

This means that the transmit call returns immediately, with transmission running in the background. The application should check the busy status before starting another transmission - alternatively it should pause for a long enough period to guarantee completion. If you are using an interrupt to handle a pushbutton, you need to be aware of contact bounce.

My approach would be to forget interrupts and timers and use asyncio with a driver such as this one to interface the switch.

import asyncio
from machine import Pin
from primitives import ESwitch  # Switch device driver
from ir_tx.nec import NEC
es = ESwitch(Pin(9, Pin.IN, Pin.PULL_DOWN), lopen=0)  # Create a switch instance. Close is high

PIN_IR_TRANSMITTER = Pin(17, Pin.OUT, value=0)
IR_ADDRESS = 0xef00
IR_DATA_ON = 0x03
IR_DATA = 0x03
nec = NEC(PIN_IR_TRANSMITTER)

async def switch_handler():
    while True:
        es.close.clear()  # Clear the closure event
        await es.close.wait()  # Wait for the switch closure
        print("Closed")
        nec.transmit(IR_ADDRESS, IR_DATA_ON)
        while nec.busy():  # Ensure transmission is complete before starting another
            await asyncio.sleep_ms(100)
        print("After transmission")

async def main():
    await switch_handler()  # Run forever

asyncio.run(main())

Timers and interrupts are relatively advanced techniques. See official docs for explanation about delays in interrupt handlers.

While asyncio takes some learning, the effort is well worthwhile. See the tutorial.

guser99 commented 7 months ago

Thanks for the explanation. Timers and interrupts seemed to me to be the best solution and much easier to implement than asyncio. I'll look into it in the new year, unfortunately I can't test anything this year. Many thanks in advance.

peterhinch commented 7 months ago

I can assure you that asyncio is easier: you have already encountered a few of the problems with timers and interrupts.

Either way, you have a learning curve, but asyncio is more generally useful. I speak with nearly 50 years experience of writing firmware.