What’s troubling me with the RPi.GPIO python library?

AlarmPi needs to monitor 2 inputs currently: the switch and the PIR.  Once the AlarmHub is finished, it needs to track a TCP socket as well so that when one Alarm goes off, the hub can tell all the others to go off too.

There are several ways for the RPi.GPIO library to monitor and report inputs:

  • RPi.GPIO.input(channel) but you’d need to poll frequently and would probably miss a change
  • RPi.GPIO.wait_for_edge() blocks, so you wouldn’t miss the event, but it can only track one channel; the Alarm + Hub needs at least 3
  • RPi.GPIO.edge_detected() doesn’t block, which means that although it still only covers 1 channel again, each could be checked in turn; in addition, the input is latched until read meaning it can’t be missed.  The downside is that you’d need to keep checking say once a second to detect switch and PIR changes
  • RPi.GPIO.add_event_detect() allows a callback to be made when an edge is detected; unfortunately this happens on a separate thread, and does not wake a sleeping main thread.  The only way to work around this is for the callback thread to send os.signal(signal.SIGINT) to wake the sleeping main thread via a signal handler, but that then makes it harder to use ctrl-C to stop the code.

AlarmPi currently uses this last option as the only one that can be made to work efficiently, but the code shows the messy interactions between callbacks, the main thread. and the signal handler. It’s also forced to have a super extended debounce selected (30s) on the switch callback; once the switch is turned on, it needs to beep / light the LED for 30s to allow the user to leave the room before the PIR is enabled. Because the switch callback doesn’t wake the main thread, this 30s processing takes place in the callback itself. To allow this to work, the callback bounce delay must be longer than 30s. If it isn’t, then when the alarm is turned on, any bounce in the switch is queued until the 30s callback has finished, and then it is processed, immediately toggling the switch off again disabling the PIR as though the user had turned it off. With this hacky debounce delay of 30s, this actually means once the hub exists that if you accidentally turn on the alarm, you can’t turn it off until the PIR is active, at which point attempting to turn off the alarm will trigger the PIR, and most likely deploy the CS gas, ring the police etc. Yet without the hacky fix, any switch bounce (likely) will automatically turn the alarm off immediately every time you try to turn it on; catch 22.

When the Hub comes along, the situation gets worse as the main thread would need to sleep until an intruder is detected, and then use a sockets.select() call for receiving data, meaning yet more messy interactions with callbacks and signal handlers.

So I’m looking at modifying the RPi.GPIO library.  Here’s my current plan:

  • Make the RPi.GPIO library more object oriented:
  • GPIO.setup() returns a gpio object representing a single channel / GPIO pin.  Errors are reported via Try: Except handlers
  • Currently, in the C library, each GPIO pin is accessed via a file descriptor (fd) passed to epoll() for input and write() for output – these would now be stored inside the GPIO class object, one per channel
  • A new python class function gpio.socket() returns the fd in a socket object.
  • In turn, this fd can be used by socket.select() (I hope!) just as other TCP sockets are used; the advantage here is that select can watch many fds at a time, sleep when nothing is happening, and wake when one or more sockets have something to report.
  • The current blocking functions RPi.GPIO.input and output would still be supported.
  • The current callbacks would become unnecessary, as would the wait_for_edge, edge_detected, and add_event_detect – the sockets solution provides a solution covering these and more, although support for them should be retained if at all possible for back-compatibility reasons.
  • In passing, I’ll also fix another restriction I hit in the Turtle project, where a GPIO output always is set up with default value 0; instead RPi.GPIO.setup() will carry an extra parameter, defaulted to 0, but allowing the user to specify 1 if needed.

The only problem is I have no idea how Python libraries really work.  I have the ‘C’ code for the RPi.GPIO library, and 22 years experience of writing C code.  I’m just not 100% confident that the GPIO fds can be used by socket.select (although I think it should work since select() uses epoll() under the surface) nor am I experienced in writing the C code to support the new python class required.

Sounds like an interesting challenge. Not sure whether I’m up to it, but I’ll give it a try in the background.

Running to stand still…

Just as my python code was nearly complete, GPIO interrupt support has been added. To me this is fantastic as I loathed the fact my drone remote control and the drone itself we’re having to periodically (every 0.1 seconds) check its inputs to update itself. Now I’ll be able to

  • add the joystick interrupts on the RC, for both the switches and the joystick movements, so the remote control can just sleep pending an interrupt. It’s not actually quite that simple as in fact, the remote control periodically sends a KEEPALIVE message to the drone so the drone knows it’s still in contact with the RC. If it finds it’s not, it switches to auto-landing mode, but it does at least reduce the work when there is no change on the joysticks.
  • add accelerometer interrupts to the drone itself, so if it’s running smoothly, there’s no need for it to poll the accelerometer; it can just sleep on select() waiting for the next RC commands.
  • Together this means lower battery usage on both, and faster reaction to critical events. A double plus!

    The only downside is the rewrite of the code scheduling and resultant testing. I think for the mo, I’ll add this to my todo list and concentrate on getting the drone airborne and stable first.