MicroPython - Poll all switches from one timer without breaking garbage collection

Viewed 41

I am working on a Switch class that will poll and debounce all switches on my board with a single timer. I have the debounce logic solved thanks to https://www.ganssle.com, but my implementation requires I maintain a list of references to all my Switch instances so the timer can check them all. Unfortunately, this stops the Switch.__del__() method from being called when my Switch instances would normally go out-of-scope since there is still a reference to them in that list. Does anyone know a way around this?

Switch class

from machine import Pin, Timer


class Switch:
    CHECK_MS = 5
    PRESS_MS = 10
    RELEASE_MS = 20

    _instances = []
    _all_instance_poller = Timer(-1)

    def __init__(
        self,
        pin_number,
        close_callback=None,
        open_callback=None,
        start_active=False,
        pull_down=True,
    ):
        self._pin = Pin(pin_number, mode=Pin.IN, pull=Pin.PULL_DOWN if pull_down else Pin.PULL_UP)
        self._pull_down = pull_down
        self._debounced_state = pull_down if start_active else not pull_down
        self._reset_counter()
        self._close_callback = close_callback or (lambda: ...)
        self._open_callback = open_callback or (lambda: ...)
        Switch._instances.append(self)

    def closed(self):
        return self._debounced_state == self._pull_down

    def raw_state(self):
        return self._pin.value()

    def _update(self):
        raw_state = self.raw_state()
        if raw_state == self._debounced_state:
            self._reset_counter()
        else:
            self._counter -= 1
            if self._counter == 0:
                self._debounced_state = raw_state
                if self.closed():
                    self._close_callback()
                else:
                    self._open_callback()
                self._reset_counter()

    def _reset_counter(self):
        if self.closed:
            self._counter = Switch.PRESS_MS / Switch.CHECK_MS
        else:
            self._counter = Switch.RELEASE_MS / Switch.CHECK_MS

    def __del__(self):
        Switch._instances.remove(self)


def _check_button_pins(timer):
    for button in Switch._instances:
        button._update()


Switch._all_instance_poller.init(mode=Timer.PERIODIC, period=Switch.CHECK_MS, callback=_check_button_pins)

This passes many test cases with open and close callbacks set on pull-up and pull-down switches. However, it fails this case with an AssertionError (Switch not destroyed):

Failing test case

from utime import sleep_ms
from components.switch import Switch
from machine import Pin

LEFT_SWITCH_PULL_UP_PIN = 15


def test_switch_auto_delete():
    signal = Signal()

    def create_and_delete_switch():
        switch = Switch(
            pin_number=LEFT_SWITCH_PULL_UP_PIN,
            open_callback=signal.set_true,
            pull_down=False,
        )
        print("Close and open left switch")
        while not signal.value:
            sleep_ms(500)
        # __del__() should be called here by Python, but it is not being called.
        # Adding a call to del(switch) at this line causes the test to pass.

    create_and_delete_switch()

    assert len(Switch._instances) == 0, "Switch not destroyed"


class Signal:
    def __init__(self, value=False):
        self.value = value

    def set_true(self):
        self.value = True

    def set_false(self):
        self.value = False

    def toggle(self):
        self.value = not self.value
0 Answers
Related