A Raspberry Pi Sense HAT firework with Lua

✍️ → Written on 2021-01-03 in 1186 words. Part of cs software-development

Motivation

I spent New Year’s Eve with my girlfriend. Due to Covid19, we couldn’t invite any one else and I collected activity ideas for the two of us. She got quite excited about this firework in Lua.

In a previous blog post, I explained pixels.lua. We implemented it to run the Sense HAT 8×8 LED matrix natively with Lua. Let’s get started to implement some firework (or 花火/hanabi as it is called in Japanese).

Unlike last time, I didn’t implement the program beforehand on my own. We created the design on-the-fly together.

Goal

Implementation steps

Previous code as dependency

First, we reuse the code of the previous blog post and require it. rpi_sensehat_pixels.lua is simply the previous code provided as a library by exporting its functionality with return.

package.path = package.path .. ';?.lua'
local hat = require('rpi_sensehat_pixels')

We extend the search path for the working directory and the functionality of rpi_sensehat_pixels.lua is now accessible via the hat variable.

set_pixel

In the previous blog post, we have seen that setting pixel in the LED matrix corresponds to writing a file. We now write a function set_pixel which takes a LED ID (int ∈ {1, 2, …, 64}) and some color and sets 1 of 64 Sense HAT LEDs to the given color (a hexadecimal string of length 6).

local set_pixel = function (led_id, color)
  local fd = io.open(hat.sense_hat_device_path(), "w")
  fd:seek("set", 2 * (led_id - 1))
  fd:write(hat.color_to_bin(color))
  fd:close()
end

xy_to_id

We really don’t want to deal with LED IDs, but define a zero-based two-dimensional coordinate system (x,y ∈ {0, 1, …, 7}). Because LED ID 1 is the LED at the top left, but we define x=0 y=0 to be at the bottom left, he have some offset of 57:

local xy_to_id = function (x, y)
  return 57 + x - 8 * y
end

point_is_on_circle

There are different ways to implement the circle of a firework, but we reminded ourselves of the circle equation, we learned in high school. Is given point (x, y) on the circle with radius r? The second equation generalizes the equation for a non-origin center (latexmath::[x_c], latexmath::[y_c]).

\[ \begin{align} x^2 + y^2 &= r^2 \\ (x - c_x)^2 + (y - c_y)^2 &= r^2 \\ (x - c_x)^2 + (y - c_y)^2 - r^2 &= 0 \end{align} \]

Now, since we have a discrete system (there is no LED pixel between two LED pixels in consecutive lines), we don’t want to use the equation, but an inequality instead. We define a threshold of 1. Any LED matrix with an Euclidean distance ≤ 1 shall be highlighted. Be aware than inequality ≤ 1 would cover the entire interior disk. So we define a big circle with radius r and a small circle r-1. If the pixel is within the big circle, but not the small one, we return true (i.e. we say “the point is on the circle”).

local point_is_on_circle = function (cx, cy, r, x, y)
  local in_big_circle = (x - cx) * (x - cx) + (y - cy) * (y - cy) - r * r <= 1
  local in_small_circle = (x - cx) * (x - cx) + (y - cy) * (y - cy) - (r-1) * (r-1) <= 1
  return in_big_circle and not in_small_circle
end

Use draw_explosion

But how can we use point_is_on_circle? The idea is to iterate over all LEDs and determine whether the given pixel is on the circle. If so, then we draw the corresponding circle. This is design of draw_explosion. We take a color, some center (cx, cy), and time t. At time t=0, we draw a single point and at any later time t (max is 4), we draw a circle of radius t.

local draw_explosion = function (color, cx, cy, t)
  if t == 0 then
    set_pixel(xy_to_id(cx, cy), color)
  end
  if t > 0 and t <= 4 then
    for x = 0, 7 do
      for y = 0, 7 do
        if point_is_on_circle(cx, cy, t, x, y) then
          set_pixel(xy_to_id(x, y), color)
        end
      end
    end
  end
end

A rising rocket

Drawing a rising rocket is trivial. At time t and with explosion center (cx, cy), we set pixel y=t if y<cy:

for t = 0, (cy - 1) do
  set_pixel(xy_to_id(cx, t), color)
  hat.clear_pixels()
end

In the final implementation, I delayed the execution of the next loop iteration a little bit.

Drawing a firework

Thus, we have all part, we draw a rising rocket and then let the rocket explode by showing a circle. But where? Well, we pick a random location. We use math.random(a, b) which returns a random integer in {a, a+1, …, b}. We recognized that y≤1 looks aesthetically less pleasing and thus ensured that y is at least 2. Also the color is picked randomly from the table.

local draw_firework = function ()
  hat.clear_pixels()

  local cx = math.random(8) - 1
  local cy = math.random(6) + 1
  local color = colortable[math.random(8) - 1]

  for t = 0, (cy - 1) do
    set_pixel(xy_to_id(cx, t), color)
    hat.sleep(0.1)
    hat.clear_pixels()
  end

  for t = 0, 4 do
    draw_explosion(color, cx, cy, t)
    hat.clear_pixels()
  end
end

The recording called draw_firework 13 times consecutively.

Complete source code

You can find the complete code on gist.github.com.

Depending on your preference, you might want to change the timing delays of the firework (hat.sleep calls).

One advanced goal would be to let multiple rockets rise at the same time and explode at different times. This is why we implemented set_pixel instead of using set_pixels from last time. The latter set all pixels and thus clears pixels which are not related to the firework we draw. In the end, I think a more sophisticated design for time handling suffices to implement this usecase. We did not have time for it and switched to preparing cheese fondue.

All the best in 2021!