Bramley: 6. Splash Screen and Shutdown

18 November 2020

Communicating with the Bramley via serial cable is fine, but to use the device in my hands, I need to launch a program automatically when it's turned on and - using only the buttons - be able to shut it down cleanly again.

In this post, I hook into various parts of the startup/shutdown process to handle input and display messages to the user.

Startup

The first message you see in the video above is the 'Bramley' name bouncing impatiently. The animation is based on this GIF:

The Bramley splash screen

Decoding the GIF at runtime added around 2 seconds of latency. So in a moment of yak-shaving madness I created a Rust macro to turn it directly into code (I'll probably delete this later as it slows down compile times).

Then, to display the animation as early as I could, I created a systemd service file with as few dependencies as possible.

[Unit]
Description=Splash screen
DefaultDependencies=no
Conflicts=shtudown.target

[Service]
ExecStart=/usr/local/bin/splash

[Install]
WantedBy=default.target

Using systemd-analyze plot, I can check when the splash screen is displayed.

Output of systemd-analyze plot command (cropped)

There's a hard floor of around 6 seconds before systemd starts launching services. The splash screen itself is launched at around 6.7 seconds. I could tweak the order a little, but it's unlikely to get significantly faster.

The problem with starting a program this early is that the kernel's SPI module hasn't been loaded yet, and at first I couldn't draw to the screen. My solution was to compile a custom kernel that included the SPI module statically.

Startup complete

Once the Pi is booted, I run another program to read input and shut down the Pi if I press all the buttons at once.

[Unit]
Description=Bramley menu

[Service]
ExecStartPre=/bin/systemctl stop splash.service
ExecStart=/usr/local/bin/menu
Restart=on-failure

[Install]
WantedBy=basic.target

Unfortunately, the full startup sequence is really slow. To reach the graphical target takes over 35 seconds as measured by systemd (wall clock time from power on is even longer).

pi@raspberrypi:~$ systemd-analyze critical-chain
The time after the unit is active or started is printed after the "@" character.
The time the unit takes to start is printed after the "+" character.

graphical.target @35.464s
└─multi-user.target @35.452s
  └─ssh.service @34.837s +590ms
    └─network.target @34.754s
      └─dhcpcd.service @23.000s +11.730s
        └─basic.target @21.690s
          └─sockets.target @21.684s
            └─triggerhappy.socket @21.671s
              └─sysinit.target @21.557s
                └─systemd-timesyncd.service @18.386s +3.101s
                  └─systemd-tmpfiles-setup.service @17.280s +1.015s
                    └─local-fs.target @17.251s
                      └─boot.mount @17.086s +146ms
                        └─systemd-fsck@dev-disk-by\x2dpartuuid-5f8a5265\x2d01.se
                          └─dev-disk-by\x2dpartuuid-5f8a5265\x2d01.device @13.45

My menu program runs at basic.target, which is currently reached in just under 22 seconds. This is probably slower than it needs to be - Raspbian enthusiastically starts everything by default - and culling unecessary services should speed it up considerably.

I'm now wondering what an acceptable time-to-menu would be. As a child, I remember the GameBoy being slow, but acceptable, and that takes around 6-7 seconds from power on to menu.

The Bramley currently takes around 12 seconds to display the splash screen and 34 seconds to display the menu. If I'm to approach that GameBoy benchmark of 6-7 seconds there's still a long way to go - I'm not even sure if it's possible with a Pi.

Of course, most modern devices cheat and don't power off at all. A standby mode may be worth investigating, but I'm going to leave that problem for another day. For now, I can shutdown the Pi while prototyping and that's enough.

Shutdown

To display the 'Shutting down...' message, I created another systemd service using the shutdown.target:

[Unit]
Description=Shutting down message
DefaultDependencies=no
Conflicts=menu.service

[Service]
ExecStart=/usr/local/bin/shutdown-start

[Install]
WantedBy=shutdown.target

Shutdown complete

At the end of shutdown, once the filesystem has been re-mounted as read-only, and just before halting the system, I display "Bye" - which due to how the screen works, fades out poignantly after the power is cut.

There's no systemd service file for this. Instead, the program is simply symlinked into the /lib/systemd/system-shutdown/ directory and systemd will run it automatically.

I now have basic control over startup and shutdown. Next time, I'll make my first attempt at chorded input.