- Bind an interrupt handler to the pin.
- This interrupt handler then notes down the utime.ticks_ms() when the interrupt occured and then uses micropython.schedule to run a function that creates a "debouncing" task.
- That function creates a task that asynchronously sleeps until the interrupt timestamp is more than 10ms ago. After that time the pin is considered debounced and another function is called to work with the value it settled on.
- If the IRQ re-triggers while the function is sleeping, the timestamp will be updated, causing the function re-calculate wait time once it wakes up again. There’s a flag in the class that prevents launching more than one task simultaneously.
This makes sense, I guess: The sleeping tasks won’t be interrupted; this is not real concurrency after all.
But then I though: Oh, wait, you can cancel tasks. If I have my “endless loop” always-on coro running and cancel it after calling create_task(), i.e. inject the CancelledError into it, might that interrupt its sleep? But turns out, at the moment I call create_task(), the endless-loop coro is considered active and I get an exception that a task can’t cancel itself (why though?).
So… I guess there’s no way to use dynamically started uasyncio tasks for debouncing? And yes, I’ve seen Peter’s Switch class, but it a) relies on an always-running task that wakes up every n ms (with n being the debounce time), which is somewhat wasteful, and b) doesn’t handle re-triggers during that time.
If there’s no better way, I might go for timer interrupts instead then, but since the ESP32 has only four of them and no virtual ones, I kind of wanted to avoid that.