---
title: Raspberry Pi HAT tutorials part 2 – Blinkenlights and micro-Pong!
description: Welcome to part 2 of our Raspberry Pi Unicorn HAT tutorial series. In
  this post, we learn to build a handheld micro-Pong device and a system monitor.
url: https://canonical.com/blog/raspberry-pi-hat-tutorials-part-2-blinkenlights-and-micro-pong?format=md
---

1. [Blog](https://canonical.com/blog)
2. Article

---

[Oliver Smith](https://canonical.com/blog/author/local-optimum "More about Oliver Smith")

14 July 2022

# Raspberry Pi HAT tutorials part 2 – Blinkenlights and micro-Pong!

[Raspberry Pi](https://canonical.com/blog/tag/raspberry-pi)

---

Share the article

Photo by Feelfarbig Magazine, [unsplash](https://unsplash.com/photos/0nCFpNXJsA0)

*Welcome to part 2 of our Raspberry Pi HAT tutorial series written by our resident Pi developers, Dave ‘waveform’ Jones and William ‘jawn-smith’ Wilson. You can read part 1 [here](https://ubuntu.com/blog/raspberry-pi-hat-tutorials-part-1).*

*In this post, they teach us how to build a handheld micro-Pong device with the Unicorn HAT Mini and follow it up with a system monitor on the Unicorn HAT HD.*

*Check out part 1, where William got us started with the Unicorn pHAT and Dave shared his piwheels project dashboard!*

*This is a guest post from William’s own* [*blog*](https://blogjawn.stufftoread.com/)*, which he’s kindly allowed us to share here. Check out his site for more great Pi tutorials as well as some equally colorful 3D printing projects.*

*That’s enough from me, over to William and Dave.*

---

As of [Ubuntu 22.04](https://ubuntu.com/blog/whats-new-for-raspberry-pi-ubuntu-22-04-lts), the Raspberry Pi [Pimoroni Unicorn HATs](https://shop.pimoroni.com/search?q=unicorn) are supported on Ubuntu out of the box. This includes the standard Unicorn Hat, Unicorn pHAT, Unicorn HAT Mini, and Unicorn HAT HD.

To install the libraries for each HAT, run the following commands:

```
sudo apt install python3-unicornhat
sudo apt install python3-unicornhathd
sudo apt install python3-unicornhatmini
```

Below are some examples of how to use them!

## **Tutorial: Micro-Pong on the Unicorn HAT Mini**

*Section written by William Wilson*

While small, the Unicorn HAT Mini has more pixels than the pHAT so it can display much more. It also has four buttons, making things like simple games possible. This example is a very simple game of Pong that I’ve created. Every time the ball hits a paddle, the color of the ball changes and the game speeds up a little.

```
import time
import math
import random
import colorsys

from gpiozero import Button
from unicornhatmini import UnicornHATMini

unicornhatmini = UnicornHATMini()
unicornhatmini.set_brightness(0.5)
width, height = unicornhatmini.get_shape()

class Pong():
    def __init__(self):
        # start the paddles roughly halfway vertically
        self.l_paddle_y = 3
        self.r_paddle_y = 3
        self.delay = 0.3
        self.ball_x = 1
        self.ball_y = 1
        self.ball_horiz = 1
        self.ball_vert = 1
        self.game_on = True
        self.paddle_height = 3
        self.colors = (
            (255, 0, 0),
            (0, 255, 0),
            (0, 0, 255),
            (255, 255, 0),
            (255, 0, 255),
            (0, 255, 255),
            (255, 255, 255)
        )
        self.color = random.choice(self.colors)

    def l_paddle_down(self):
        if self.l_paddle_y < height - self.paddle_height:
            self.l_paddle_y += 1

    def l_paddle_up(self):
        if self.l_paddle_y > 0:
            self.l_paddle_y -= 1

    def r_paddle_down(self):
        if self.r_paddle_y < height - self.paddle_height:
            self.r_paddle_y += 1

    def r_paddle_up(self):
        if self.r_paddle_y > 0:
            self.r_paddle_y -= 1

    def update(self):
        # check if the game is over
        if self.ball_x in (0, width - 1):
            self.game_on = False
            return

        # clear the board state
        unicornhatmini.clear()

        # set the position of the paddles
        for i in range(3):
            unicornhatmini.set_pixel(0,
                    self.l_paddle_y + i,
                    255, 255, 255)
            unicornhatmini.set_pixel(width - 1,
                    self.r_paddle_y + i,
                    255, 255, 255)

        # calculate the next position of the ball
        ball_x_next = self.ball_x + self.ball_horiz
        ball_y_next = self.ball_y + self.ball_vert

        # check if the ball needs to bounce off of a paddle
        if (
                (ball_x_next == 0 and ball_y_next in
                    (self.l_paddle_y + i for i in range(3))) or
                (ball_x_next == width - 1 and ball_y_next in
                    (self.r_paddle_y + i for i in range(3)))
            ):
            # the paddle has hit the ball, so change direction
            self.ball_horiz = -self.ball_horiz
            # recalculate ball_x_next
            ball_x_next = self.ball_x + self.ball_horiz

            # since the ball hit a paddle
            # reduce the delay to speed up the game
            self.delay -= 0.01

            # change the color of the ball every time
            # the ball hits a paddle
            self.color = random.choice(self.colors)

        # check if the ball needs to bounce off of an edge
        if (
                (self.ball_y == 6 and self.ball_vert == 1) or
                (self.ball_y == 0 and self.ball_vert == -1)
            ):
            self.ball_vert = -self.ball_vert
            ball_y_next = self.ball_y + self.ball_vert

        self.ball_x = ball_x_next
        self.ball_y = ball_y_next
        unicornhatmini.set_pixel(self.ball_x,
                self.ball_y,
                *self.color)

        # show the game state
        unicornhatmini.show()

pong_game = Pong()

button_a = Button(5)   # left paddle up
button_b = Button(6)   # left paddle down
button_x = Button(16)  # right paddle up
button_y = Button(24)  # right paddle down

button_a.when_pressed = pong_game.l_paddle_up
button_b.when_pressed = pong_game.l_paddle_down
button_x.when_pressed = pong_game.r_paddle_up
button_y.when_pressed = pong_game.r_paddle_down

while pong_game.game_on:
    pong_game.update()
    time.sleep(pong_game.delay)
```

Pimoroni has many more [examples](https://github.com/pimoroni/unicornhatmini-python/tree/master/examples) on their GitHub repo!

## **Tutorial: Unicorn HAT HD system monitor**

*Section written by Dave Jones*

**Moar Pixels!**

The Unicorn HAT HD has an outrageous number of neopixels, just waiting to spew rainbows at unsuspecting eyeballs. So much so, it seems a shame to waste them all on something as mundane as a two-tone monitoring service. Let’s try for something more … colorful!

Old computers had huge arrays of lights, now often referred to (slightly tongue in cheek) as [blinkenlights](https://en.wikipedia.org/wiki/Blinkenlights). These often visualized actual machine registers, bus addresses and the like. These days, memory is too large and too fast to make this useful. However, it’s fairly typical (on an Ubuntu for Raspberry Pi desktop image running a few things like a browser, terminal, etc.) to have a number of userland processes that would fit quite nicely within the 256 neopixels on the Unicorn HAT HD.

Let’s build a modern blinkenlights variation that visualises the process table. We’ll use red to represent CPU usage, green to represent memory usage, and blue to represent the inverted age of a process (so brand new processes will show up bright blue, then fade as they age).

We can use [ps](https://manpages.ubuntu.com/manpages/jammy/en/man1/ps.1.html) to query the process table. We’ll exclude PID 2, which on Linux represents the kernel, and any process under PID 2 (the kernel’s various “threads” which are largely static and thus not very visually interesting). We’ll ask **ps** for **cp** (per-mill CPU usage), **rss** (resident set size, a measure of the amount of actual RAM a process is using), and **etimes** (age of the process in seconds). Finally, we’ll turn off the headers because we don’t need them:

```
$ ps --pid 2 --ppid 2 --deselect -o cp,rss,etimes --no-headers
  1 10760   91980
  0 35128   91975
  0 23432   91975
  0  2916   91975
  0  3332   91971
  3  3364   91971
  0  7368   91970
  0  3868   91970
  0  3428   91969
  ....
```

This looks reasonable, so let’s get coding. First, some imports:

```
#!/usr/bin/python3

import shlex
import subprocess as sp
from time import sleep
from threading import Thread, Event
from itertools import zip_longest

import unicornhathd
```

Next, our main monitor function:

```
def monitor():
    unicornhathd.rotation(0)
    unicornhathd.brightness(1.0)
    width, height = unicornhathd.get_shape()

    processes = {
        # coord  CPU   mem   age
        (x, y): (None, None, None)
        for y in range(height)
        for x in range(width)
    }
    limits = {'max-cpu': 0, 'max-mem': 0}
    updates = UpdateThread(processes, limits)
    updates.start()
    try:
        while True:
            if limits['max-cpu']:
                for (x, y), (cpu, mem, age) in processes.items():
                    if cpu is not None:
                        r = min(255, int(255 * cpu / limits['max-cpu']))
                        g = min(255, int(255 * mem / limits['max-mem']))
                        b = max(0, min(255, int(255 * (10 - age) / 10)))
                        unicornhathd.set_pixel(y, x, r, g, b)
                    else:
                        unicornhathd.set_pixel(y, x, 0, 0, 0)
            unicornhathd.show()
            sleep(1/30)
    finally:
        unicornhathd.off()
        updates.stop()
        updates.join()
```

This is fairly simple but let’s go through it piece by piece. We start off by initializing:

* The Unicorn HAT HD itself, with the **unicornhathd** module. We also grab its shape (which should always be 16×16).
* **processes** – a dictionary mapping coordinates on the display to a three-tuple representing CPU usage, RAM usage, and process age. These will be **None** by default (for any pixel not currently representing a process).
* **limits** – a simple dictionary acting as a way to pass a couple of other values (the current maximum CPU and memory usage) between the update thread and the main thread.
* **updates** – an instance of **UpdateThread** (which we’ll define a bit later on), which we start before entering the main loop.

The main loop simply checks that we have a valid max-cpu value then updates the display from the values in the processes dictionary with some simple maths. All the values are scaled to the maximum CPU and memory usage so we get a nice distribution of color.

Now the **UpdateThread** itself:

```
class UpdateThread(Thread):
    def __init__(self, processes, limits):
        super().__init__(target=self.update, args=(processes, limits),
                         daemon=True)
        self._done = Event()

    def stop(self):
        self._done.set()

    def update(self, processes, limits):
        cmdline = shlex.split(
            'ps --pid 2 --ppid 2 --deselect -o cp,rss,etimes --no-headers')
        while not self._done.wait(1/30):
            proc = sp.run(cmdline, capture_output=True, text=True)
            output = proc.stdout.splitlines()
            max_cpu = max_mem = 0
            for coord, line in zip_longest(processes, output):
                if not coord:
                    break
                elif line:
                    cpu, mem, age = (int(i) for i in line.split())
                    max_cpu = max(cpu, max_cpu)
                    max_mem = max(mem, max_mem)
                    processes[coord] = (cpu, mem, age)
                else:
                    processes[coord] = (None, None, None)
            limits['max-cpu'] = max_cpu
            limits['max-mem'] = max_mem
```

This is also fairly simple; the **update** method just calls **ps** and loops over the lines of output, sticking values into the **processes** dictionary and updating the **limits** dictionary with the biggest CPU and memory values found at the end.

Hang on a minute … how is this safe? Surely both the “main” thread, and our background update thread are going to be bashing away at these structures simultaneously?! In most languages this would indeed be a no-no. In Python it’s safe, *provided* that the dictionary doesn’t grow or shrink while we’re iterating over it.

> **Note:** The limitation on not inserting into / deleting from dictionaries while iterating over them is the primary reason for the structure of both these demo scripts. The fixed size of the dictionaries in both cases avoids the need for any explicit locking, keeping the scripts both simple and reasonably efficient.
>
> For those curious about what goes on under the covers, there is still some locking going on: the [Python GIL](https://docs.python.org/3/faq/library.html#what-kinds-of-global-value-mutation-are-thread-safe) is ensuring that both threads leave the structures in a consistent state between Python statements (actually between bytecodes, but let’s not split hairs!).

It’s also worth noting that, as there’s no locking, we can’t guarantee that  the **limits** are actually accurate when the main display thread is iterating over the **processes** dictionary. This is why we’re using **min** and **max** to clamp the calculation results in the main thread.

Finally, we just need to kick things off:

```
if __name__ == '__main__':
    monitor()
```

And we’re done! Run [the full script](https://blogjawn.stufftoread.com/examples/blinkenlights.py) (you don’t need **sudo** for this one as the Unicorn HAT HD has a very different control mechanism) and bask in the glow of das blinkenlights!

Try stopping and starting your web browser while it’s running and see just how many processes a modern browser encapsulates.

You may be surprised to see processes appearing and disappearing constantly at the “end” of the table. This is actually quite normal for a modern system. You may also,see that the end of the table has one persistently red (CPU load) and one persistently blue (“new”) process, when your system is idle. Have a think about which processes these might be! In particular, why is one process *always* “new” …

Press Ctrl+C to exit the script.

If you wish to run the script automatically on boot, place [this service definition](https://blogjawn.stufftoread.com/examples/das-blinkenlights.service) in **/etc/systemd/system/das-blinkenlights.service** (this assumes you’ve saved the script as **/usr/local/bin/blinkenlights.py**):

```
[Unit]
Description=Unicorn HAT HD based process table monitor
After=local-fs.target

[Service]
Type=simple
User=ubuntu
Restart=on-failure
ExecStart=/usr/bin/python3 /usr/local/bin/blinkenlights.py

[Install]
WantedBy=multi-user.target
```

Then run the following and you should find that the monitor will start automatically on the next reboot:

```
$ sudo systemctl daemon-reload
$ sudo systemctl enable das-blinkenlights
```

Now, you can keep track of your system performance without digging around on your device.

---

*And that’s all for this series! A big thanks to William and Dave for allowing us to reproduce their tutorials.*

*If these ideas have sparked the imagination, don’t forget you can share your HAT projects in the* [*Raspberry Pi category*](https://discourse.ubuntu.com/c/raspberry-pi/97) *on the Ubuntu Discourse!*

*For tips on getting started with the Raspberry Pi as well as further project ideas, check out some of the links below.*

### **Tutorials**

* [Install Ubuntu Desktop on the Raspberry Pi 4 (2GB and above)](https://ubuntu.com/tutorials/how-to-install-ubuntu-desktop-on-raspberry-pi-4#1-overview)
* [How to install Ubuntu Server on your Raspberry Pi](https://ubuntu.com/tutorials/how-to-install-ubuntu-on-your-raspberry-pi#2-prepare-the-sd-card)
* [How to install Ubuntu Core on your Raspberry Pi](https://ubuntu.com/tutorials/how-to-install-ubuntu-core-on-raspberry-pi)

### **Projects**

* [Host a Minecraft server on Ubuntu Desktop using the Raspberry Pi](https://ubuntu.com/blog/linux-gaming-tutorial-raspberry-pi-minecraft-server-on-ubuntu-desktop)
* [Run Flutter Applications on Ubuntu Core](https://ubuntu.com/tutorials/run-flutter-applications-on-ubuntu-core#1-overview)
* [Build your own bare metal cloud using a Raspberry Pi cluster with MAAS](https://discourse.ubuntu.com/t/takeovers-for-ubuntu-com/17229)
* [How to build a Raspberry Pi Kubernetes cluster using MicroK8s](https://ubuntu.com/tutorials/how-to-kubernetes-cluster-on-raspberry-pi)

[Get in touch

Interested in running Ubuntu in your organization?](https://ubuntu.com/about/contact-us/form)

## Sign up for our newsletter

Get the latest Canonical news and updates in your inbox.

Work email:

\*I agree to receive information about Canonical's
products and services.

By submitting this form, I confirm that I have read and agree to [Canonical's Privacy Policy](https://canonical.com/legal/dataprivacy).

Sign up

## Share on

---

## Related posts

[### Hacker’s guide to the Raspberry Pi AI kit on Ubuntu](https://canonical.com/blog/hackers-guide-to-the-raspberry-pi-ai-kit-on-ubuntu)

In this blog post we discuss our experiences trying to get the Hailo-8L accelerator and its software running on Ubuntu 24.04 LTS.

[JP Meijers](https://canonical.com/blog/author/jpm)

21 August 2024

[### End of year review: Ubuntu Desktop in 2023](https://canonical.com/blog/end-of-year-review-ubuntu-desktop-in-2023)

As 2023 draws to a close, it’s time to look back on the evolution of Ubuntu Desktop over the last twelve months.

[Oliver Smith](https://canonical.com/blog/author/local-optimum)

11 December 2023

[### Canonical, 우분투 23.10 맨틱 미노타우르스 출시](https://canonical.com/blog/canonical-releases-ubuntu-23-10-mantic-minotaur-kr)

강화된 보안, 향상된 데스크톱 앱 검색 및 새로운 하드웨어 지원이 최신 우분투 출시를 주도합니다. 2023년 10월 12일: 오늘 Canonical은 코드명 “맨틱 미노타우르스”인 우분투 23.10의 출시를 발표하였으며, https://ubuntu.com/download에서 다운로드하여 설치할 수 있습니다....

[Canonical](https://canonical.com/blog/author/canonical)

13 October 2023

[### Canonical releases Ubuntu 23.10 Mantic Minotaur](https://canonical.com/blog/canonical-releases-ubuntu-23-10-mantic-minotaur)

Fortified security, enhanced desktop app discovery and new hardware support lead the latest release of Ubuntu 23.10 Mantic Minotaur.

[Canonical](https://canonical.com/blog/author/canonical)

12 October 2023
