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 upawake.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.
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!