Drawing pixels on a Raspberry Pi Sense HAT with Lua

✍️ → Written on 2020-08-15 in 1122 words. Part of cs software-development

Background

The Raspberry Pi is a famous single-board computer. For my Lua experiments, I used version 3 Model B whereas my python experiments ran on a Raspberry Pi 2 Model B. The Sense HAT is an 8×8 LED matrix besides other features that are irrelevant here.

I have experience with the Sense HAT. I got it about 5 years ago and had fun using the python library to show some New Years Eve greetings. As you can see from the documentation, the API already features rendering text onto the LED matrix.

In February 2020, I organized a pygraz meetup and we were desperate to get some coding dojo ready. The night before the event, I programmed a prototype where a flask web service offers an 8×8 grid. Clicking a field on this grid colored it in the user’s color. The user’s color was statically determined from a hash value of the browser’s user agent. I was not sure about the setup at realraum, so I was nervous that day. Networkwise, everyone at realraum is in the same network (LAN and Wifi devices), so this turned out to be no-brainer. But hooking up raspberry pi’s HDMI to the projector was impossible (voltage issue IIRC). So development had to be done on my private laptop. We started the coding dojo. The participants implemented taking a request from the flask middleware and updated the LED state on the raspberry pi in a test-driven development manner. The deployment to the raspberry pi was cumbersome and done by myself in parallel. It turned out to be great fun. I think it was inspiring for many. People had fun to draw patterns in the grid and show prevalence with their color. gebi simultaneously started a DDoS on this service and flask ran out of memory twice and had to be restarted.

A few months later, I taught a beginner some Lua programming. Lua is a neat, lightweight programming language. My current opinion is that the entire software stack (luarocks package, versioning, debugging, tooling, …) is in its infancy, but the programming language itself is okay. Some of the criticism is related to the fact that many libraries are written in C and are used from Lua (easy interoperability). And I don’t need to rant about C dependency management here. The final session was the plan to use the Sense HAT from Lua. I barely got it running (I had to replace a broken SD card in the morning) and it did not happen (she didn’t want to), so I looked into it myself. It turned out to be more difficult. I implemented the necessary parts natively.

Implementation

There is one blog artice from 2019-04 about RPi Sense HAT in combination with Lua. Recognize that this implementation uses luasocket and lua-periphery. The latter goes down a dependency rabbit hole and I ended up not being able to get RTIMULib compiled to the desired place (disclaimer: I am bad at this). Be aware that this implementation also requires Lua 5.1, whereas Lua 5.4 was just published and (e.g.) on Lua 5.3 or later supports bitwise operations. Lua also suffers from versioning issues like many other programming languages: If you install a package, you never know whether it’s compatible with your Lua version until runtime.

So I kept wondering what is so difficult. I guessed that it boiled down to writing bytes to a framebuffer. And boy, I was right. I referred to the python implementation and implemented a small Lua 5.3 file.

First, we determine which file matching /sys/class/graphics/fb*/name returns the name RPi-Sense FB.

function sense_hat_device_path()
  local target_name = "RPi-Sense FB"

  for node in list_directory("/sys/class/graphics") do
    if string.sub(node, 1, 2) == "fb" then
      local name_path = "/sys/class/graphics/" .. node .. "/name"
      if file_exists(name_path) then
        local fd = io.open(name_path, "r")
        local content = fd:read()
        fd:close()

        if content == target_name then
          return "/dev/" .. node
        end
      end
    end
  end
end

fb0 in my case. So we read and write a framebuffer to the device file /dev/fb0.

function set_pixels(pixels)
  local fd = io.open(sense_hat_device_path(), "w")
  for idx, color in pairs(pixels) do
    fd:seek("set", 2 * (idx - 1))
    fd:write(color_to_bin(color))
  end
end

But which data is written? A proper encoding is required. This is implemented by the call to color_to_bin.

I specify colors in 6-digit hexadecimal notation. Then I need to convert it to RGB565. I spent a lot of time trying to determine how to convert by 16bit integer into an appropriate (byte) string, but eventually I learned about string.pack. This is almost a 1:1 copy of python code to Lua:

function color_to_bin(rgb)
  if rgb:len() ~= 6 then
    error("color " .. rgb .. " is not a 6-digit hex color")
  end

  local r = tonumber(rgb:sub(1, 2), 16)
  local g = tonumber(rgb:sub(3, 4), 16)
  local b = tonumber(rgb:sub(5, 6), 16)

  r = (r >> 3) & 0x1F
  g = (g >> 2) & 0x3F
  b = (b >> 3) & 0x1F
  local comb = (r << 11) + (g << 5) + b

  return string.pack("H", comb)
end

Now, I can show you my clear_pixels function disabling (i.e. setting to black) all pixels on the Sense HAT illustrating a call of set_pixels:

function clear_pixels()
  local color_table = {}
  for i = 1, 64 do
    color_table[i] = "000000"
  end

  set_pixels(color_table)
end

On top of that, I implemented a convenience API to draw anything. Take a string with 8 rows and 8 columns …

[[

 00  00
 00  00

00    00
 00  00
  0000

]]

… and a map associating each character (here: ‘ ’ and ‘0’) with a color:

local color_table = {}
color_table[" "] = "000000"
color_table["0"] = "FFFFFF"

This can be transformed into a pixels table as required by set_pixels. This is done by the pixel_from_picture function. My script shows the letters HELLO and finishes with a smiley 🙂.

A neat hack and glad to see, that you don’t need to go all the way down to dependency hell. Implemented natively!

Watch the final result video.