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

Errno 1 Operation not permitted #43

Closed CrazyIvan359 closed 3 years ago

CrazyIvan359 commented 3 years ago

This may not be an issue with periphery itself, but I'm hoping for some help in diagnosing a problem that currently has me stumped.

If I strip my code down the basics, I end up with the follow, that works:

from periphery import GPIO
import multiprocessing as mproc
from time import sleep

def proc1():
  p2=GPIO("/dev/gpiochip0", 2, "in")
  p1=GPIO("/dev/gpiochip0", 1, "out")
  p1.write(True)
  sleep(2)
  p1.write(False)
  p1.close()
  p2.close():

mproc.Process(target=proc).start()

My actual program is more complicated obviously, but at its core I have the same thing happening. A process forked from the main process accessing multiple pins. I can open the pins no problem, I can read all of them no problem, but I get errors when I try to call write on the GPIO instance.

Test and program run using sudo python3 so permissions shouldn't be an issue. I also tried running python3 directly as root using sudo -i but no change.

I have different results on different hardware, currently I have an RPi3B and an Orange Pi Zero LTS.

Any pointers on where to look or how to get more debug information would be most appreciated. For reference, I am working on switching MQTTany over to cdev gpio access and found this library to be the best option. The branch is private, but if you are willing to dig that deep into it, I can publish it.

vsergeev commented 3 years ago

At which calls are you getting the two errors?

Operation not permitted -- check the groups and permissions (ls -l /dev/gpiochip0). You may want to add a udev rule to assign a group to the gpiochip device that your user is in (e.g. a gpio group).

Invalid argument -- this may be due to python-periphery using a newer cdev GPIO feature than the running kernel supports. In c-periphery/lua-periphery, this is determined at compile time from the kernel headers, but the Python implementation doesn't have that luxury. Let me know what kernel version it is (uname -a), and I'll see what can be done to try to handle it more gracefully in python-periphery.

CrazyIvan359 commented 3 years ago

Sorry, missed some details. I'm getting it on the write calls. Always running sudo python3, I even tried sudo -i to run directly as root, no change.

Good to know I can use a udev rule, I don't know much about udev. I will include that in my docs as it beats running as root, thank you.

The RPi is running Raspbian Buster, apt upgraded from Stretch and reports:

5.4.51-v7+ #1333 SMP Mon Aug 10 16:45:19 BST 2020 armv7l GNU/Linux

The OPi is running a fresh Armbian Buster install and reports:

5.8.6-sunxi #20.08.2 SMP Fri Sep 4 09:16:37 CEST 2020 armv7l GNU/Linux

I would spin up c/lua but as I'm having trouble pinning down the source, I'm not sure how to code the test to find the problem.

CrazyIvan359 commented 3 years ago

Just tried adding udev rule below and no change on either device:

KERNEL=="gpiochip*" SUBSYSTEM=="gpio", GROUP="gpio", MODE="0660"

Confirmed working:

crw-rw---- 1 root gpio 254, 0 Oct 11 17:18 /dev/gpiochip0
crw-rw---- 1 root gpio 254, 1 Oct 11 17:18 /dev/gpiochip1

I setup Lua 5.1 and ran the following on both devices and it is working:

local GPIO = require('periphery').GPIO

-- Open GPIO /dev/gpiochip0 line 10 with input direction
local result, gpio_in = pcall(GPIO, "/dev/gpiochip0", 11, "in")
if result == false then
    print(gpio_in)
    os.exit()
end

-- Open GPIO /dev/gpiochip0 line 12 with output direction
local result, gpio_out = pcall(GPIO, "/dev/gpiochip0", 12, "out")
if result == false then
    print(gpio_in)
    os.exit()
end

local value = gpio_in:read()
gpio_out:write(not value)

gpio_in:close()
gpio_out:close()
vsergeev commented 3 years ago

So you weren't able to run under the normal user with the udev permissions? Did you add the user to the gpio group? Keep in mind you'll need to login and logout for a group change to take effect (or use newgrp gpio).

Could you provide a stack trace of the write error from your example?

CrazyIvan359 commented 3 years ago

While setting up the udev rules I restarted several times (I also setup sysfs, i2c, and w1 rules), creating the group and adding the login user to it was the first thing I did. I don't think file permissions are the issue because I get the same results running with sudo or directly as root.

Here is the stack trace from mqttany on the Orange Pi. Reading works, writing throws error 1 not permitted on the OPi and error 22 on the RPi, both stack traces are the same except for the error.

Traceback (most recent call last):

File "/home/pi/.local/lib/python3.7/site-packages/periphery/gpio.py", line 683, in write
    fcntl.ioctl(self._line_fd, CdevGPIO._GPIOHANDLE_SET_LINE_VALUES_IOCTL, data)

PermissionError: [Errno 1] Operation not permitted

During handling of the above exception, another exception occurred:

Traceback (most recent call last):

File "/opt/mqttany/mqttany/gpio/pins/digital.py", line 203, in write
    self._interface.write(state)

File "/home/pi/.local/lib/python3.7/site-packages/periphery/gpio.py", line 685, in write
    raise GPIOError(e.errno, "Setting line value: " + e.strerror)

periphery.gpio.GPIOError: [Errno 1] Setting line value: Operation not permitted
CrazyIvan359 commented 3 years ago

Side note, the try: around that fcntl call is not catching PermissionErrors, which is causing the error while handling an error. But because of the fact that you aren't catching PermissionErrors it shouldn't enter the except: anyway...

https://github.com/vsergeev/python-periphery/blob/77ae0a3680e58dba299a7af8f7abb346191c0d2e/periphery/gpio.py#L682-L685

vsergeev commented 3 years ago

Could it be your script is calling write() on a GPIO opened for input?

edit: you could print a string representation of the GPIO before writing, it will have the direction (print(gpio))

CrazyIvan359 commented 3 years ago

It shouldn't be, that was one of the first things I checked. I'm using enums and I have a lookup dictionary to go from my pin mode enum to the string directions, I double checked that and it's fine.

I will log the GPIO instance, and for completeness repeat my test script with a pin opened as input, in the morning.

Thank you

CrazyIvan359 commented 3 years ago

You were right, attempting to write to a pin opened for for input.

GPIO 1 (name="", label="MQTTany", device=/dev/gpiochip0, line_fd=19, chip_fd=18, direction=in, edge=none, bias=default, drive=default, inverted=False, chip_name="gpiochip0", chip_label="1c20800.pinctrl", type=cdev)

Stupidly in mqttany.gpio.pins.digital.Digital I set a default value for the pin_mode arg, even though the default INPUT comes from way higher up in the program. Because of that I wasn't seeing the missing arg errors that would have told me I wasn't passing pin mode to that class.

Thank you for the troubleshooting help! I will submit a PR later to trap the PermissionError and another to catch attempts to write to a pin opened for input and produce less confusing errors.

CrazyIvan359 commented 3 years ago

to trap the PermissionError

So this is a weird one because this definition class PermissionError(OSError) means that except OSError: should catch PermissionErrors... and yet both PermissionError and GPIOError get raised in the above stack trace.

In gpio.CdevGPIO.write this is the only way I was able to stop the double exception. This leads me to believe that something else may be wrong? I'm not sure where to go from here, or if it's even worth it (PermissionErrors may occur anywhere accessing an fd occurs, but are less misleading) given that #44 improves clarity in this function.

        try:
            fcntl.ioctl(self._line_fd, CdevGPIO._GPIOHANDLE_SET_LINE_VALUES_IOCTL, data)
        except PermissionError as e:
            raise
        except (OSError, IOError) as e:
            raise GPIOError(e.errno, "Setting line value: " + e.strerror)
vsergeev commented 3 years ago

It is actually behaving as you think -- the PermissionError is getting caught in that except block -- but you're also seeing Python 3's more thorough chained exception trackback reporting. You can prove to yourself that the PermissionError is getting caught in the except block by adding print(e) or print(isinstance(e, PermissionError)) before the raise GPIOError(...), or by forcing it to cause another known OSError, like setting self._line_fd to a bogus file descriptor, e.g. 99, before the fcntl.ioctl(), to see that the reporting is similar.

As for the reporting, there's three ways of handling this in Python 3:

Current/do nothing way:

try:
    fcntl.ioctl(self._line_fd, CdevGPIO._GPIOHANDLE_SET_LINE_VALUES_IOCTL, data)
except (OSError, IOError) as e:
    raise GPIOError(e.errno, "Setting line value: " + e.strerror)

Results in what you've seen:

Traceback (most recent call last):
  File "/home/alarm/python-periphery/periphery/gpio.py", line 683, in write
    fcntl.ioctl(self._line_fd, CdevGPIO._GPIOHANDLE_SET_LINE_VALUES_IOCTL, data)
PermissionError: [Errno 1] Operation not permitted

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "test_perm.py", line 10, in <module>
    gpio_in.write(False)
  File "/home/alarm/python-periphery/periphery/gpio.py", line 686, in write
    raise GPIOError(e.errno, "Setting line value: " + e.strerror)

This reports both exceptions involved, but is somewhat confusing as you've pointed out, because it seems like two unrelated exceptions occurred.

Explicit exception chaining:

try:
    fcntl.ioctl(self._line_fd, CdevGPIO._GPIOHANDLE_SET_LINE_VALUES_IOCTL, data)
except (OSError, IOError) as e:
    raise GPIOError(e.errno, "Setting line value: " + e.strerror) from e

Results in:

Traceback (most recent call last):
  File "/home/alarm/python-periphery/periphery/gpio.py", line 683, in write
    fcntl.ioctl(self._line_fd, CdevGPIO._GPIOHANDLE_SET_LINE_VALUES_IOCTL, data)
PermissionError: [Errno 1] Operation not permitted

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "test_perm.py", line 10, in <module>
    gpio_in.write(False)
  File "/home/alarm/python-periphery/periphery/gpio.py", line 685, in write
    raise GPIOError(e.errno, "Setting line value: " + e.strerror) from e
periphery.gpio.GPIOError: [Errno 1] Setting line value: Operation not permitted

This provides the same chained exception context as above, but the reporting is not as confusing as the unlinked version. This is the style I would prefer for Python 3.

Exception suppression:

try:
    fcntl.ioctl(self._line_fd, CdevGPIO._GPIOHANDLE_SET_LINE_VALUES_IOCTL, data)
except (OSError, IOError) as e:
    raise GPIOError(e.errno, "Setting line value: " + e.strerror) from None

Results in:

Traceback (most recent call last):
  File "test_perm.py", line 10, in <module>
    gpio_in.write(False)
  File "/home/alarm/python-periphery/periphery/gpio.py", line 685, in write
    raise GPIOError(e.errno, "Setting line value: " + e.strerror) from None
periphery.gpio.GPIOError: [Errno 1] Setting line value: Operation not permitted

This suppresses the underlying exception, and is probably what you were expecting. This is how Python 2 reports an exception traceback, as exception chaining isn't available in Python 2.


Unfortunately, since it's still a goal of python-periphery to support both Python 2 and Python 3 seamlessly, we can't use the from e or from None, as that syntax isn't available in Python 2. So we have to settle for the slightly confusing reporting under Python 3. Note that it will only be shown when printing a traceback. Programatically, you would just catch GPIOError as usual.

CrazyIvan359 commented 3 years ago

Thank you for the detailed explaination! I was not even aware of that syntax in Python 3. I agree with everything. Too bad Python doesn't have the dynamic includes you can do with C.