Tea & Tech (🍵)

Safe-to-Wake Light, Part 5: Clojure/Python Interop

January 23, 2020

I had high hopes for Clojure-Python interop after learning about libpython-clj. It’s magical how well the interop works, and it’s quite obvious that Chris has put a lot of time into it. He even gave a talk about it at Clojure conj 2019!

So this is basically perfect for Squishy Kitty’s needs, only… it doesn’t work on 32-bit ARM processors (our Pi Zero). We’ve logged an issue about it, tried some quick troubleshooting, and I even built Chris his own Raspberry Pi Zero and mailed it out to him!

Unfortunately, for the time being, elegant interop is off the table. There’s no telling if it would work, either, because driving the lights requires access to /dev/mem, which means that every time we invoke a Python script to drive the lights, we need root access.

I gave a lot of thought (and research) to the best approach to driving the lights from Clojure when all the libs are in Python, and what I landed on was inelegant, but practical and easy-to-understand.

Python Scripts

Inside my resources/ folder, I created a new folder, python/, where my Python scripts live.

I have a different Python script for each lighting setting that Squishy Kitty can have:

  • sleep.py - It’s bedtime. You should not be awake right now.
  • quiet.py - It’s early morning; grab a book and read quietly until it’s time to get up
  • awake.py - Time to wake up! Throw open your door and greet the new day!
  • party.py - Party time 🎉
  • off.py - Squish Kitty is off duty and not shining at all

These scripts are pretty straightforward. They’re procedural, and it’s not Clojure, but they get the job done. When working with microprocessors, beggars cannot be choosers.

Here’s the script for sleep mode:

#!/usr/bin/env python

import unicornhat as unicorn

unicorn.set_layout(unicorn.AUTO)
unicorn.rotation(0)
unicorn.brightness(0.5)
width, height = unicorn.get_shape()

for y in range(height):
    for x in range(width):
        unicorn.set_pixel(x, y, 255, 0, 0)
        while True:
          unicorn.show()

Basically: “set every pixel to red at 50% brightness”.

Clojure Interop

So we’ve got these Python scripts, and they live in our resources folder, which means that they will be bundled and included in our JAR file. In order to run them on the pi, however, we need to extract them out of the JAR to the local filesystem, because Python has no way of reaching into the JAR to read the resource files.

Extracting Python files

To extract the files from the JAR, I came up with this helper function:

(def DEFAULT_SCRIPT_DIR "/home/pi/.squishy-kitty/")

(defn extract-resource!
  ([resource filename]
    (extract-resource! resource filename DEFAULT_SCRIPT_DIR))
  ([resource filename dest]
    (let [output (str dest filename)]
      (io/make-parents output)
      (io/copy (io/input-stream resource) (io/file output))
      output ;; Returns the path to the extracted file, for convenience
      )))

If you want to extract the “sleep.py” script from the JAR, you could do so like:

(let [filename "sleep.py"]
  (-> (str "python/" filename)
      (io/resource)
      (extract-resource! filename)))

Invoking Python from Clojure

Once we have Python scripts on the local file system, we need to be able to invoke them. As root. Yuck.

One more caveat to this whole lights-via-Python business is: As long as the lights are on, the Python script is running. It does not terminate.

Fortunately, I was able to navigate this without any issues using conch, a Clojure library that provides some nice high level wrapping around low-level Java process APIs. This allows us to execute Python in a different thread so it does not block the main thread.

To put Squishy Kitty to sleep, for example, I do something like:

(defn run-script
  [filename]
  (kill-running-scripts)
  (let [path (-> (str "python/" filename)
                 (io/resource)
                 (extract-resource! filename))]
    (sh/proc "sudo" "python3" path)))

Look familiar? It’s basically our extraction code from the block above, boxed in by two new lines:

(kill-running-scripts)

This is a quick function I wrote to kill every single python process running on the Pi. Essentially, it ends up invoking sudo killall python3.

(sh/proc "sudo" "python3" path)

The sh/proc function comes from conch, and it’s like saying “invoke the following command in a new thread.”

For example, if we invoke our run-script like (run-script "sleep.py"), we ultimately end up running the command sudo python3 /home/pi/.sleepy-kitty/sleep.py in a new thread, which turns all the lights red at 50% brightness.

Look at this colorful kitty!

By invoking the run-script Clojure function with the names of our Python scripts, we can make kitty change colors! The fact that it doesn’t block is important, because the next step is wrapping all of this in a web server.

In our next post, we serve up a mobile-friendly webpage from the pi for changing Kitty’s colors from our phones. Stay tuned!


Andrew J. Pierce collects yixing teapots and lives in Virginia with his wife, son, and cat Ziggy. You can follow him on Twitter.