vsergeev / python-periphery

A pure Python 2/3 library for peripheral I/O (GPIO, LED, PWM, SPI, I2C, MMIO, Serial) in Linux.
MIT License
519 stars 139 forks source link

GPIOError: [Errno 13] in conjunction with udev rules #33

Closed felix-ht closed 4 years ago

felix-ht commented 4 years ago

I have setup udev rules so that i can run this library without root permissions. Im am using the library to access gpios. In this environment the library throws an error, when i try to acess any gpio:

>>> gpio = GPIO(42)
periphery.gpio.GPIOError: [Errno 13] Setting GPIO direction: Permission denied

The cause for this is that after the device is exported it takes a moment until the udev rules get applied. However, this library is trying to change the direction for the GPIO right away. As the new udev defined permissions are not yet set, this fails with the aforementioned error.

To fix there error i monkey patched the _open function in the library, by adding a short sleep after the export:

## Monkey Patch for compatibility with udev rules
def _open(self, pin, direction):
    if not isinstance(pin, int):
        raise TypeError("Invalid pin type, should be integer.")
    if not isinstance(direction, str):
        raise TypeError("Invalid direction type, should be string.")
    if direction.lower() not in ["in", "out", "high", "low", "preserve"]:
        raise ValueError("Invalid direction, can be: \"in\", \"out\", \"high\", \"low\", \"preserve\".")

    gpio_path = "/sys/class/gpio/gpio%d" % pin

    if not os.path.isdir(gpio_path):
        # Export the pin
        try:
            with open("/sys/class/gpio/export", "w") as f_export:
                f_export.write("%d\n" % pin)
        except IOError as e:
            raise GPIOError(e.errno, "Exporting GPIO: " + e.strerror)

        # Loop until GPIO is exported
        exported = False
        for i in range(GPIO.GPIO_EXPORT_STAT_RETRIES):
            if os.path.isdir(gpio_path):
                exported = True
                break
            time.sleep(GPIO.GPIO_EXPORT_STAT_DELAY)

        if not exported:
            raise TimeoutError("Exporting GPIO: waiting for '%s' timed out" % gpio_path)

###   THIS IS THE NEW CODE ###############
    time.sleep(GPIO.GPIO_EXPORT_STAT_DELAY) 
#########################################################
    # Write direction, if it's not to be preserved
    direction = direction.lower()
    if direction != "preserve":
        try:
            with open("/sys/class/gpio/gpio%d/direction" % pin, "w") as f_direction:
                f_direction.write(direction + "\n")
        except IOError as e:
            raise GPIOError(e.errno, "Setting GPIO direction: " + e.strerror)
    # Open value
    try:
        self._fd = os.open("/sys/class/gpio/gpio%d/value" % pin, os.O_RDWR)
    except OSError as e:
        raise periphery.GPIOError(e.errno, "Opening GPIO: " + e.strerror)

    self._pin = pin

GPIO._open = _open
felix-ht commented 4 years ago

Another option instead of the sleep, would be to try opening the direction file a couple of times. Similar to how it is solved in for i in range(GPIO.GPIO_EXPORT_STAT_RETRIES): ...

vsergeev commented 4 years ago

Thanks for raising this. I'll add code for a retry loop around opening/writing direction, as you mentioned.