Responding to GPIO input events in Clojure on a Rasp Pi 4

In a previous article we discussed how to generate output signals using the dvlopt.linux.gpio library in Clojure.
In this article, I’ll be delving into processing input signals, cleaning them up, and using the core.async library to build event-driven code into the application.

Basic microswitch input

I wired up a large resistor from 3.3V+ to a switch, hooked the other end of the switch to GPIO pin #4 on the Pi, and connected that line via another large resistor go ground. Next, the following code loads up the dvlopt.linux.gpio library and adds a watcher onto pin 4.

Library information in project.clj:


(defproject test-switch "0.1.0-SNAPSHOT"
  :description "A Clojure library that demonstrates de-bouncing a microswitch being read via the dvlopt.linux.gpio library."
  :url "http://stronganchortech.com.strng.us/responding-to-gpio-input-events-in-clojure-on-a-rasp-pi-4"
  :license {:name "GPL-3.0-or-later"
            :url "https://www.eclipse.org/legal/epl-2.0/"}
  :dependencies [[org.clojure/clojure "1.10.0"]
                 [org.clojure/core.async "0.7.559"]
                 [dvlopt/linux.gpio "1.0.0"]]
  :repl-options {:init-ns test-switch.core})

Setting up pin 4 to be an input pin that triggers when it transitions from low to high:


(ns test-switch.core
  (:require [dvlopt.linux.gpio :as gpio]
            [clojure.core.async :refer :all]))

(defn test-switch []
  (with-open [device  (gpio/device 0) ; device 0 is the Pi's GPIO bank                                                                                                                                      
              watcher (gpio/watcher
                       device
                       {4 {::gpio/tag :switch    ; :switch is just a name we give to the pin                                                                                                                
                           ::gpio/direction :input
                           ::gpio/edge-detection :rising}})]
    (while true
      (if-some [event (gpio/event watcher -1)]
        (println "Event: " event)
          ))))

The while loop will loop forever and blocks (efficiently) at (gpio/event watcher -1).
This will sit until an event occurs on one of the pins being monitored by the watcher — in this case there’s only one pin, #4.
The -1 is a timeout value in milliseconds, meaning that we won’t time out.

Testing it out in the REPL and moving the switch from off to on once:


test-switch.core> (test-switch)
Event:  #:dvlopt.linux.gpio{:edge :rising, :nano-timestamp 1579354777741371070, :tag :switch}
Event:  #:dvlopt.linux.gpio{:edge :rising, :nano-timestamp 1579354777741594526, :tag :switch}
Event:  #:dvlopt.linux.gpio{:edge :rising, :nano-timestamp 1579354777741626322, :tag :switch}
Event:  #:dvlopt.linux.gpio{:edge :rising, :nano-timestamp 1579354777741757058, :tag :switch}

If you haven’t worked with microswitches before, you might be surprised to see four events captured here.
This is occurring because the mechanical contacts of the switch are bouncing as the switch opens.
Looking at the timestamps, these subsequent events happened 223, 255, and 386 microseconds after the initial event.

For our application, we don’t need such a high fidelity. We need to transform the input from an electrical signal into a logical one.
This process is called “de-bouncing”.

To do this de-bouncing in an event-driven way, I’ll be using Clojure’s excellent core.async library.
Since this library can be novel to many folks, I’ll build this up in steps and demonstrate each step along the way.

Creating a thread to watch for input

First, I need to be able to create a watcher job that runs in its own thread. This is because the gpio/event function blocks.
I can’t have it on the main thread, since then the program would halt until input happened.
core.async lets us easily create threads using the thread macro.
Here’s what the API docs say about it:


Usage: (thread & body)

Executes the body in another thread, returning immediately to the
calling thread. Returns a channel which will receive the result of
the body when completed, then close.

Here’s a basic function to test the threading:


(defn create-watcher-chan []                                                                                                                                                                                
  (let [off-switch (atom false)]                                                                                                                                                                            
    (thread (while (not @off-switch)                                                                                                                                                                            
              (println "In loop")                                                                                                                                                                               
              (Thread/sleep 10000))                                                                                                                                                                             
            (println "Closing watcher"))                                                                                                                                                                        
    off-switch))

The thread macro starts a new thread and starts to loop forever, printing out a message every ten seconds.
We return an off-switch variable which is an atom.
Atoms are a neat functionality that Clojure provides for performing atomic references. @off-switch de-references it to get the value.
The reason we need an atom to act as an off-switch is that, since this runs in the background, we lose the easy ability to manipulate the running code via the REPL.

Here’s the code in action:

test-switch.core> (def foo (create-watcher-chan))
#’test-switch.core/fooIn loop
In loop
test-switch.core> (reset! foo true)
trueClosing watcher

test-switch.core>

I saved off the off-switch into foo. The code hit the loop twice, and I used reset! to atomically set the off-switch to true, causing it to exit the next time through the loop.

Now that we can control a thread, let’s go ahead and add in a channel that we can monitor.


(defn create-watcher-chan []                                                                                                                                                                                
  (let [gpio-chan (chan)                                                                                                                                                                                    
        off-switch (atom false)                                                                                                                                                                             
        device  (gpio/device 0)                                                                                                                                                                             
        watcher (gpio/watcher                                                                                                                                                                               
                 device                                                                                                                                                                                     
                 {4 {::gpio/tag :switch                                                                                                                                                                     
                     ::gpio/direction :input                                                                                                                                                                
                     ::gpio/edge-detection :rising}})                                                                                                                                                       
        ]                                                                                                                                                                                                   
    (thread (while (not @off-switch)                                                                                                                                                                        
              (if-some [event (gpio/event watcher 1000)]                                                                                                                                                    
                (do                                                                                                                                                                                         
                  (println "Event: " event)                                                                                                                                                                 
                  (put! gpio-chan event))))                                                                                                                                                                 
            (println "Closing watcher")                                                                                                                                                                     
            (gpio/close watcher)                                                                                                                                                                            
            (gpio/close device)                                                                                                                                                                             
            (println "Closed everything allocated."))                                                                                                                                                       
    [gpio-chan off-switch]))

This code introduces the new concept of a core.async channel created using (chan).
Channels act as a buffer that you can put and take messages from.
In this case, we’re sticking every event onto the channel.

Let’s load this up in the REPL to see what taking messages off the channel looks like.


test-switch.core> (def foo (create-watcher-chan))
#'test-switch.core/foo
test-switch.core> (def gpio-chan (first foo))
#'test-switch.core/gpio-chan
test-switch.core> (def off-switch (second foo))
#'test-switch.core/off-switch                                                                                                                                            
test-switch.core> (<!! gpio-chan)
Event:  #:dvlopt.linux.gpio{:edge :rising, :nano-timestamp 1579358946832508325, :tag :switch}                                                                                                               
Event:  #:dvlopt.linux.gpio{:edge :rising, :nano-timestamp 1579358946832744469, :tag :switch}                                                                                                               
Event:  #:dvlopt.linux.gpio{:edge :rising, :nano-timestamp 1579358946832801357, :tag :switch}                                                                                                               
Event:  #:dvlopt.linux.gpio{:edge :rising, :nano-timestamp 1579358946832848023, :tag :switch}                                                                                                               
#:dvlopt.linux.gpio{:edge :rising,
                    :nano-timestamp 1579358946832508325,
                    :tag :switch}
test-switch.core> (<!! gpio-chan)
#:dvlopt.linux.gpio{:edge :rising,
                    :nano-timestamp 1579358946832744469,
                    :tag :switch}
test-switch.core> (<!! gpio-chan)
#:dvlopt.linux.gpio{:edge :rising,
                    :nano-timestamp 1579358946832801357,
                    :tag :switch}
test-switch.core> (<!! gpio-chan)
#:dvlopt.linux.gpio{:edge :rising,
                    :nano-timestamp 1579358946832848023,
                    :tag :switch}
test-switch.core> 

As you can see, when I first tried to do a blocking take (<!!) off of the channel, the REPL paused. Then toggling the microswitch printed out four events (as before) and the blocking take returned the first event.
The remaining three events were left on the channel and could be taken off one-at-a-time via <!!.

You may be wondering what’s up with the (def gpio-chan (first foo)). Since I need create-watcher-chan to return two different things, a channel and an atom to act as an off switch, I return them in an array and then use first and second to pull them back out.

Doing the actual de-bouncing


(defn create-watcher-chan []                                                                                                                                                                                
  (let [gpio-chan (chan)                                                                                                                                                                                    
        off-switch (atom false)                                                                                                                                                                             
        device  (gpio/device 0)                                                                                                                                                                             
        watcher (gpio/watcher                                                                                                                                                                               
                 device                                                                                                                                                                                     
                 {4 {::gpio/tag :switch                                                                                                                                                                     
                     ::gpio/direction :input                                                                                                                                                                
                     ::gpio/edge-detection :rising}})                                                                                                                                                       
        last-event-timestamp (atom 0)                                                                                                                                                                       
        debounce-wait-time-ns (* 1 1000 1000)                                                                                                                                                                        
        ]                                                                                                                                                                                                   
    (thread (while (not @off-switch)                                                                                                                                                                        
              (if-some [event (gpio/event watcher 1000)]                                                                                                                                                    
                (do                                                                                                                                                                                         
                  (println "Event: " event)                                                                                                                                                                 
                  (swap! last-event-timestamp                                                                                                                                                               
                         (fn [timestamp]                                                                                                                                                                    
                           (if (> (:dvlopt.linux.gpio/nano-timestamp event)                                                                                                                                 
                                  (+ timestamp debounce-wait-time-ns))                                                                                                                                      
                             (do                                                                                                                                                                            
                               (put! gpio-chan event)                                                                                                                                                       
                               (:dvlopt.linux.gpio/nano-timestamp event))                                                                                                                                   
                             timestamp)                                                                                                                                                                     
                           ))                                                                                                                                                                               
                  (println "@last-event-timestamp is now " @last-event-timestamp)                                                                                                                           
                  )))                                                                                                                                                                                       
            (println "Closing watcher")                                                                                                                                                                     
            (gpio/close watcher)                                                                                                                                                                            
            (gpio/close device)  
            (close! gpio-chan)                                                                                                                                                 
            (println "Closed everything allocated."))                                                                                                                                                       
    [gpio-chan off-switch]))

Here we add in a new local atom called last-event-timestamp-ns.
Then, once we get an event, we check to see if the timestamp of the event is more than 1 millisecond after the last time we put something on the channel.
If it is, then we put that event onto the channel and update the last timestamp. If not, then we don’t put it on the channel and set the timestamp to the same value it was before.

If you’re new to Clojure, you may find the swap! to be an odd construction.
Since atoms are, well, atomic, we can’t just read the value, do some logic, and then write the new value using reset!.
This is because doing so would make it no longer thread-safe. Another thread could write a value after the first thread reads the value, and then the first thread would overwrite that value. Instead, swap! takes a function of one parameter (the de-referenced value) and then runs that function atomically.

Let’s see if the de-bouncing works:


test-switch.core> (def foo (create-watcher-chan))
#'test-switch.core/foo
test-switch.core> (<!! (first foo))
Event:  #:dvlopt.linux.gpio{:edge :rising, :nano-timestamp 1579361441138382609, :tag :switch}                                                                                                               
@last-event-timestamp is now  1579361441138382609                                                                                                                                                           
Event:  #:dvlopt.linux.gpio{:edge :rising, :nano-timestamp 1579361441138410034, :tag :switch}                                                                                                               
@last-event-timestamp is now  1579361441138382609                                                                                                                                                           
Event:  #:dvlopt.linux.gpio{:edge :rising, :nano-timestamp 1579361441138441534, :tag :switch}                                                                                                               
@last-event-timestamp is now  1579361441138382609                                                                                                                                                           
Event:  #:dvlopt.linux.gpio{:edge :rising, :nano-timestamp 1579361441138611734, :tag :switch}                                                                                                               
@last-event-timestamp is now  1579361441138382609                                                                                                                                                           
Event:  #:dvlopt.linux.gpio{:edge :rising, :nano-timestamp 1579361441138645752, :tag :switch}                                                                                                               
@last-event-timestamp is now  1579361441138382609                                                                                                                                                           
Event:  #:dvlopt.linux.gpio{:edge :rising, :nano-timestamp 1579361441138674159, :tag :switch}                                                                                                               
@last-event-timestamp is now  1579361441138382609                                                                                                                                                           
Event:  #:dvlopt.linux.gpio{:edge :rising, :nano-timestamp 1579361441138713233, :tag :switch}                                                                                                               
@last-event-timestamp is now  1579361441138382609                                                                                                                                                           
#:dvlopt.linux.gpio{:edge :rising,
                    :nano-timestamp 1579361441138382609,
                    :tag :switch}
test-switch.core> (poll! (first foo))
nil
test-switch.core> 

As you can see, flipping the switch triggers quite a few events, but only one gets put on the channel.
When we use poll! to verify that the channel is indeed empty after returning the first event from <!!.

Using the new events channel

Let’s take a look at using our new events channel we just made.
We can create a listener that spins off a new thread and sits and waits for a new message to come across the channel.
When the channel closes, it will also exit and close down its thread.


defn event-printer [gpio-chan]                                                                                                                                                                             
  (thread (while (if-some [event (<!! gpio-chan)]                                                                                                                                                           
                   (do                                                                                                                                                                                      
                     (println "Event: " event)                                                                                                                                                              
                     true)                                                                                                                                                                                  
                   false))                                                                                                                                                                                  
          (println "Exiting event-printer")))

The closing functionality works by virtue of <!! returning nil, and not blocking, when a channel gets closed.

Let’s take a look in the REPL:


test-switch.core> (def foo (create-watcher-chan))
#'test-switch.core/foo
test-switch.core> (event-printer (first foo))
#object[clojure.core.async.impl.channels.ManyToManyChannel 0xdea0ab "clojure.core.async.impl.channels.ManyToManyChannel@dea0ab"]Event:  #:dvlopt.linux.gpio{:edge :rising, :nano-timestamp 1579364600311430\
563, :tag :switch}                                                                                                                                                                                          
Event:  #:dvlopt.linux.gpio{:edge :rising, :nano-timestamp 1579364600312492168, :tag :switch}                                                                                                               
Event:  #:dvlopt.linux.gpio{:edge :rising, :nano-timestamp 1579364604457856193, :tag :switch}                                                                                                               
Event:  #:dvlopt.linux.gpio{:edge :rising, :nano-timestamp 1579364605848553509, :tag :switch}                                                                                                               
Event:  #:dvlopt.linux.gpio{:edge :rising, :nano-timestamp 1579364608406071012, :tag :switch}                                                                                                               
Event:  #:dvlopt.linux.gpio{:edge :rising, :nano-timestamp 1579364608865507208, :tag :switch}                                                                                                               

test-switch.core> (reset! (second foo) true)
trueClosing watcher                                                                                                                                                                                         
Closed everything allocated.                                                                                                                                                                                
Exiting event-printer                                                                                                                                                                                       
test-switch.core>

We can see that it works, printing out only de-bounced messages coming across the channel (I took out the printlns in create-watcher-chan).
When I close down the watcher, the event-printer also shuts down.

In real-world code, the event-watcher could dispatch messages to various helper functions based on the :tag and do thing like turn on an LED and set an atom telling the motors to quit turning in a certain direction when a limit switch is activated.

Code for this article is available in this GitHub repo.


Encountering challenges with motor control or IOT devices for your business’ needs?
Let us help: contact me at aaron at stronganchortech.com .

Responding to GPIO input events in Clojure on a Rasp Pi 4