A TV coupled with a Raspberry Pi makes a good information monitor. We’ve used this in my day job to provide to provide build radiators. As the dashboard showing the build status is browser based, this means we have to configure:
- Autologin to the desktop GUI
- Autostart the web browser
- start page is dashboard
- start in full-screen mode
This works, but can be a bit slow to start, and is beyond the means of a Raspberry Pi Zero.
Prompted by pub conversations about a track diagram for a minature railway, I wondered about developing an application that could be started automatically at boot, but not require a heavyweight desktop and browser.
Why Python & Pygame?
Python is a great language for accessing peripherals connected to the Pi, with modules providing easy access to web services, GPIO, I2C or SPI.
Pygame uses SDL for access to graphics, and works using “x11” when run from the desktop GUI, and “fbcon” when run on the console. This dual-mode operation means that development can be done using your favourite IDE (on a more powerful Pi), but actual operation is bare-metal console.
There are a couple of considerations that need to be made to achieve this dual-mode. I’ll show how to get an application that will run under both “x11” and “fbcon”, and then how to get the application started at boot using a systemd unit file
The animation shows a Pi Zero W being powered up and booting into the clock display.
This takes about 40 seconds from power on. The time jumping forward from 3:00:57 to 3:01:58 will be when the time is set correctly.
I’ll just talk about the key points. The full example is available in the console-pygame repository. The application is just a digital clock, but could be whatever you want it to be.
This is the
main function taken from the demo application.
1signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit()) 2 3try: 4 pygame.display.init() 5 pygame.mouse.set_visible(False) 6 7 screen = pygame.display.set_mode((1280, 720)) 8 clock = pygame.time.Clock() 9 10 while True: 11 events = pygame.event.get() 12 if list(filter(lambda e: e.type == pygame.QUIT, events)): 13 return 14 15 draw(screen) 16 17 pygame.display.flip() 18 clock.tick(10) 19except KeyboardInterrupt: 20 pass 21finally: 22 pygame.display.quit()
- Line 1
systemdservice is stopped by sending a
SIGTERMsignal. This signal handler causes the application to exit.
- Lines 4–5
- I only initialise the Pygame modules being used, rather than using
pygame.init(). The mouse cursor is hidden as it not appropriate for running on the console. You have to get used to it not being drawn as your mouse moves across the desktop window.
- Line 7
- I picked the arbitrary window size of 1280x720. This corresponds to 720p on a TV, and is likely to be a suitable size for your desktop environment.
- Lines 8,18
- The Pygame
Clockclass allows you to control the frame rate. I’ve picked 10 FPS.
- Lines 11–13
- This looks for the
QUITevent when the window system close is clicked when running on your desktop.
- Line 15
- The function
drawis responsible for updating the display, and is where you get creative.
- Lines 19–20
- If you are running from a terminal window, then this stops
Control-Cspitting out a stack trace.
- Line 21–22
- Tidy up any Pygame resources. Not actually required here, as it would be done automatically as application exits.
If you were using other Pygame modules (e.g.
pygame.font) then you need
to initialise those too.
In my clock example I have no external inputs. If I needed to interact with the outside world, I would use some form of MVC approach to update a shared model:
- Interrupt driven from GPIO pins
- Thread reading from serial port
- Thread periodically accessing web service
Systemd Unit File
This is the “unit file” used by
systemd to start the application.
1[Unit] 2Description=Pygame Console Demo 3 4[Service] 5Type=idle 6 7User=nobody 8Group=nogroup 9SupplementaryGroups=video input dialout spi i2c gpio 10 11StandardInput=tty 12StandardOutput=journal 13StandardError=journal 14 15TTYPath=/dev/tty8 16TTYVHangup=yes 17TTYVTDisallocate=yes 18 19Environment=PYTHONUNBUFFERED=TRUE 20 21ExecStartPre=/bin/chvt 8 22ExecStart=/usr/local/lib/console-demo.py 23 24[Install] 25WantedBy=multi-user.target
- Line 5
- The type is set to
idleto delay execution until all active jobs are dispatched.
- Lines 7–9
- To avoid running as “root” the application is run as “nouser”/“nogroup”. The group permissions required for my clock example are “video” and “input”, but I’ve put in the other groups for things you might need for your data collection.
- Lines 11–13
- The output is sent to the system journal to avoid it clashing with display, and so any diagnostic messages can be viewed. The input is connected to the allocated TTY for SDL to detect..
- Line 15
- The TTY allocated is the 8th virtual terminal, which is otherwise unused.
- Line 16
- This ensures that there are no other processes connected to the TTY.
- Line 17
- When the service stops, the VT is deallocated to ensure no stray output is visible.
- Line 19
- Set the output to unbuffered, so any messages are captured by the systemd journal.
- Line 21
- Switch to the VT so the application is visible.
- Line 22
- Path to where the script has been placed.
- Line 25
- Script will be started when system is ready for multi-user operation.
If you want to try for yourself, get the
console-pygame.service files from the
The following commands will place the Python script in
configure it to run as a service, and start it.
sudo apt install -y python3-pygame sudo install -t /usr/local/lib console-demo.py sudo install -t /etc/systemd/system console-pygame.service sudo systemctl daemon_reload sudo systemctl enable console-pygame sudo systemctl start console-pygame
To view the status of the service:
sudo systemctl status console-pygame
To view the output from the service:
sudo journalctl -u console-pygame
To stop and disable:
sudo systemctl stop console-pygame sudo systemctl disable console-pygame