RSSAmplifier

Musings of a Mildly Misanthropic Technologist · Apr 26, 2026

Retro Digital Signage for a Party

0
Sign in to vote or save

Matthew Ernisse · going-flying.com

April 26, 2026 @10:21

Later this year I'm having a party for some friends and family. I am not historically enthused by throwing or attending such things but sometimes needs must. We secured a venue on a nearby beach and have catering secured so of course my thoughts have moved towards matters more involving ambiance. Followers of my Thoughts microblog may have noticed a wee ATtiny3224 DMX transmitter project that I shared some photos of. While I have not actually built the lighting rig yet, I have built some effects around a pair of moving head lights and 4 RGBW LED PAR style lights that I think will add some interesting background ambiance. Having that project reach a sort of mostly-done state I started thinking of how to design menus to scatter about the venue and the thought struck me. If I am going to do something, why not go overboard? When I bought the house I live in the previous owners left a bunch of stuff behind and while I cleaned most of it out one of the things that remain is a 13" color CRT TV. Well I cleaned it up and dug out an old RF modulator, an old RaspberryPi and some cables and it turns out it still works.

Generating A Movie

When I wrote the blog entry about my experiences with the Apple Watch I used a copy of xanalogtv's CLI from jwz's irreplaceable xscreensaver to make a the little lede animation. It pretty accurately simulates a number of artifacts common to analog TV of yesteryear so of course I thought this was a perfect use case for a CRT digital sign!

First I whipped up several slides as PNG files using a pretty decent copy of an old VCR's OSD font and then hacked together a Makefile to generate the various effects and concatenate them together.

FILTER=[0:v:0][1:v:0]concat=n=2:v=1[outv]
GEOM=640x480
MOVIES=frame1.mp4 frame2.mp4 frame3.mp4 frame4.mp4
MOVIE=attract.mov
all: $(MOVIE)
%.mp4: %.png
    ~/analogtv-cli -size $(GEOM) -duration 10 $< $@
$(MOVIE): $(MOVIES)
    @if [ ! -f "$@" ]; then cp $< $@; fi
    @for i in $(MOVIES);do              \
        echo "encoding $$i";            \
        ffmpeg -hide_banner -loglevel error \
        -i "$@" -i "$$i"            \
        -filter_complex "$(FILTER)"     \
        -preset slow -tune fastdecode       \
         -map "[outv]" -r 29.97         \
        $(basename $@)-temp.mov;        \
        mv -- $(basename $@)-temp.mov $@;   \
    done

On the RaspberryPi I got the composite video output working, disabled the various virtual console getty processes and disabled the blinking cursor so the screen would be completely blank. This let me simply use mplayer's fbdev2 output to play the movie without having to have X or anything installed. I created a custom systemd target and service to have this happen after everything else so it wouldn't stutter or thrash while other stuff was happening.

/etc/systemd/system/attract.target

[Unit]
Description=Attract Screen
Requires=multi-user.target
After=multi-user.target
AllowIsolate=yes

/etc/systemd/system/attract.service

[Unit]
Description=Play attract movie with mplayer(1)
After=multi-user.target
ConditionPathExists=/home/mernisse/attract.mp4
[Service]
Type=simple
Restart=always
ExecStart=/usr/bin/mplayer -quiet -loop 0 -vo fbdev2 /home/mernisse/attract.mp4
[Install]
WantedBy=attract.target

Speeding Iteration

The TV showing the title card Once I was working on everything I had the RaspberryPi plugged into the network so I could just ssh in and scp files to it. It was very convenient but the thought struck me that on the day of the party I'm not going to want to have to build a network if I need to update the movie file that's playing. Hell, I don't even want to bring my laptop to the venue if I don't have to, why would I build a pocket-sized DMX transmitter to run a lightshow when I could just have my laptop do it (other than I like making things with microcontrollers and C of course). I decided that the best way to update the content on the device was to simply plug a USB stick in and copy the file over the old one and restart the attract service. So how do you do that?

Well, there have been several different attempts at hotplugging devices on Linux over the years and many of them are extremely annoying. Thankfully I already use udev rules to activate systemd services for a number of my other microcontroller projects so it was a simple matter of creating a systemd service and some udev rules to watch the usb block devices.

/etc/udev/rules.d/99-attract.rules

KERNEL=="sd[a-z][0-9]", SUBSYSTEMS=="usb", ACTION=="add", TAG+="systemd", ENV{SYSTEMD_WANTS}+="update-attract@%k.service"

/etc/systemd/system/update-attract@.service

[Unit]
Description=Check plugged USB drive on %i for attract.mp4 update
After=attract.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/update-attract.sh %i

This essentially binds the event to a oneshot service that just runs a script that can check the drive for an updated movie and copy it in place if it finds it.

/usr/local/sbin/update-attract.sh

#!/bin/sh
# update-attract.sh (c) 2026 Matthew J. Ernisse <matt@going-flying.com>
# All Rights Reserved.
MOUNTOPTS="rw,relatime,users,uid=1000,gid=1000,umask=000,utf8=1,flush"
LOCALPATH=/home/mernisse/attract.mp4
LOCALMTIME=$(stat -c "%Y" $LOCALPATH)
find_available_mountpoint()
{
    _mounted=$(mount | awk '{ print $3 }' | grep '^/media')
    _available=$(find /media -maxdepth 1 -mindepth 1 -type d | sort)
    for dir in $_available; do
        if ! $(echo $_mounted | grep -q $dir); then
            echo $dir
            return
        fi
    done
}
DEVNAME="/dev/$1"
if [ ! -e "$DEVNAME" ]; then
    echo "cannot find $DEVNAME"
    exit 1
fi
mountpoint=$(find_available_mountpoint)
if [ -z "$mountpoint" ]; then
    echo "Failed to find suitable mount pount."
    exit 1
fi
fstype=$(blkid -o export "$DEVNAME" | sed -n 's/^TYPE=\(.*\)/\1/p')
if [ -z "$fstype" ] || [ ! "$fstype" = "vfat" ]; then
    echo "Invalid fs on $DEVNAME, ignoring."
    exit 1
fi
echo "Mounting $DEVNAME on $mountpoint"
mount -t vfat -o "$MOUNTOPTS" "$DEVNAME" "$mountpoint"
if [ ! -f "$mountpoint/attract.mp4" ]; then
    echo "No attract.mp4 found, ignoring"
    umount "$mountpoint"
    exit 0
fi
newmtime=$(stat -c "%Y" "$mountpoint/attract.mp4")
if [ -z "$newmtime" ]; then
    echo "unable to stat $mountpoint/attract.mp4"
    umount "$mountpoint"
    exit 0
fi
if [ "$newmtime" -le "$LOCALMTIME" ]; then
    echo "$mountpoint/attract.mp4 is older than local copy."
    umount "$mountpoint"
    exit 0
fi
echo "Updating attract.mp4."
systemctl stop attract.service
cp -- "$mountpoint/attract.mp4" "$LOCALPATH"
systemctl start attract.service

A brief reboot later and I was rewarded by the following in the system log.

Apr 25 17:44:18 teevee systemd[1]: Starting update-attract@sda1.service - Check plugged USB drive on sda1 for attract.mp4 update...
Apr 25 17:44:20 teevee update-attract.sh[457]: Mounting /dev/sda1 on /media/usb0
Apr 25 17:44:20 teevee update-attract.sh[457]: /media/usb0/attract.mp4 is older than local copy.
Apr 25 17:44:20 teevee systemd[1]: update-attract@sda1.service: Deactivated successfully.
Apr 25 17:44:20 teevee systemd[1]: Finished update-attract@sda1.service - Check plugged USB drive on sda1 for attract.mp4 update.
Apr 25 17:44:20 teevee systemd[1]: update-attract@sda1.service: Consumed 1.138s CPU time.

The title card without distortion on the CRT A short moment after that I was greeted by the updated movie playing on the TV. All that is left is to build a few more things, collect as many extension cords as I can find and hope people show up. As an aside, when people provide you with a link and QR code to RSVP to a party — for the love of everything good in this world please use it.

If you were to try to replicate this with a more modern HDMI connected display instead of the composite connected CRT you might find yourself needing to figure out how to automatically login to an X session and start mplayer there. The console is convenient in this case but it doesn't use the GPU acceleration and so it's very likely that the absolute crap CPU in the RaspberryPi is not up to the task of decoding and bliting 1080p video (it's using almost an entire CPU core to do 480i ffs). While I'm at it, shout-out to jwz for the DNA Lounge flyer screens (and xscreensaver and xanalogtv-cli) for providing some inspiration for this nonsense. I'm sure almost no one out there will be finding themselves needing to turn a random old CRT TV into signage, but the bits wrangling the interminable heap of mid ideas, bad intentions, nigh on impenetrable documentation, and one of the worst configuration file formats imagined that is udev and systemd may, hopefully, prove useful.

Read the original on going-flying.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.