For the last 1.5 years, the old Virtual Chime system has been running as our doorbell and has worked very well overall with only minimal issues. At the same time, a few components never reached the reliability level I wanted and were approriate for a doorbell system. After running the system in real life for a long time now, I have a much clearer picture of what should exist, what should not, and how the architecture should be built. And this meant starting completley over, rewriting the entire system from scratch. I wiped the whole repository, put the old code in a separate branch, and started by defining what the new architecture should look like. This lead me in a new direction and made me realize I needed to use tools and technologies I had never used before.
After wiping the repository, I started with the most difficult part for me first: the custom Linux image. I had never build a Linux image myself and had to learn the hard way how to build a custom Linux image with Buildroot. Buildroot was not my first choice, but as I dug deeper into Yocto, I realized, that this was going to be an overkill for my use case. So I searched for a simpler solution and found Buildroot. I started experimenting with the configuration and after a few hours, I had a working Linux image that booted and was able to expand from there.
I wanted to start with the simplest and most basic part of the Virtual Chime stack: the speaker box that would play a sound when the doorbell is rung. It got the appropriate name "chime". Since it is the most basic part, it is also the easiest to test and iterate on. So I started by building a speaker box that would play a sound when the doorbell is rung. I used a Raspberry Pi Zero W, a MAX98357A amplifier, and a LSM-104F-8 speaker. I 3D printed a body and face cover. Getting everything working was a bit of a challenge, but now its in a state where its used in my home together with the old doorbell.
Why a Full Rewrite
The old implementation worked, but was hacked together and too fragile. I wanted to build a system, that I can plug in and forget about. So instead of patching around in the old code, I moved to a clean architecture with:
- compiled C++ services instead of a Python runtime on-device,
- explicit separation between product runtime and configuration runtime,
- a minimal Linux image tailored to device startup and reliability.
I had several requirements for the new architecture:
- For ease of use and development, I wanted to have a single repository that contains OS, application, Web UI, scripts, and hardware assets.
- For reliability, I wanted to have a minimal Linux image which booted fast and was reliably duplicated across devices.
- Clear separation between software services, web services and configuration services.
- OTA updates for the software services and the Web UI.
- A simple and lightweight Web UI for configuration and monitoring.
I had several Raspberry Pi Zero W devices laying around, so I decided to use them as the development platform. They are most definitly overkill for the application, but until I get to the final stages of development, I don't want to lock myself into a specific hardware platform. With modularity in mind, the Raspberry Pi can stay or be replaced by a more fitting and efficient system, but it's not a hard requirement yet.
Current Rewrite Status
The first rewritten product in the VC family is the chime speaker box, as it is the most basic and simplest product in the family. This gives me enough feedback on end-to-end behavior (boot, networking, MQTT, audio, remote config) while keeping hardware complexity low. Now after a few weeks of development, it is in a state where it is used in my home together with the old doorbell.
The current chime platform is:
- Raspberry Pi Zero W,
- MAX98357A I2S amplifier,
- LSM-104F-8 speaker,
- compact 3D-printed enclosure.
Custom Linux
The custom Linux image is my first full venture into building Linux images with Buildroot or any kind of build system. There were many problem, build failures and broken images before I got it working reliably after flashing. The reason for the custom Linux image is to have a minimal system that boots as fast as possible and is reliable because it has no unnecessary packages and services running. My hope is that this will make the system more predictable and easier to maintain in the long run.
Right now the image only conatinas the most essential services and packages that are needed for the chime product to run. This includes:
- network bring-up,
- time sync,
- SSH,
- 'chime-webd' web daemon,
- and 'chime' audio/MQTT runtime.
The result is a ~300MB image instead of ~2GB for a standard Raspberry Pi OS. It contains exactly what I need: BusyBox for basic utilities, Dropbear for SSH, wpa_supplicant for WiFi, and my application code. Nothing else. The boot process is controlled by init scripts I wrote myself, not systemd. This gives me complete visibility into what runs when, and the minimal attack surface is a nice security bonus
This allows the system to boot in under 2 seconds and is ready to use in under 5 seconds. These values are not yet optimized and not even correctly tracked, but they are a good starting point.
The runtime is split into two independent daemons:
- 'chime': MQTT-to-audio service for processingring events.
- 'chime-webd': HTTPS control daemon exposing configuration APIs and hosting the Web UI.
With this separation, the web/config functionality should never compromise the core ring path. Playing the ring sound is the most important task of the system and no other service should be able to interrupt it. At least, thats the plan.
Struggles for a first time Linux builder
The Raspberry Pi Zero W has a weired boot process that fooled me multiple times. The GPU wakes up first and loads firmware from the SD card before the ARM CPU even starts. I had to look at a lot of 4 blinks from the status LED, meaning the GPU couldn't load its firmware. I spent hours debugging boot failures that were actually just a malformed 'config.txt' file preventing the GPU from handing off to the CPU.
Getting WiFi working was surprisingly the most complex challenge. The WiFi chip on the Pi Zero W requires a stack of kernel modules to be loaded in the right order: the wireless configuration layer, the driver, the companion module, and finally the firmware. Buildroot compresses kernel modules with XZ to save space, but the kmod tool it builds couldn't decompress them. I ended up writing a custom init script that handled the decompression and loading of the modules in the right order. This and other wifi issues took me a few days to get working.
I2S Audio
The audio setup uses a MAX98357A I2S amplifier chip connected to the Pi's I2S pins. But getting Linux to recognize and bind all the audio components together was several hours of debugging.
Linux's ALSA framework needs three things to create a working sound card: a CPU driver for the I2S, a codec driver for the amplifier board, and a driver that binds them together based on the configuration. I spent hours loading kernel modules, but '/dev/snd/' remained empty and no audio was coming out.
The problem was a missmatched link between the device overlay and the machine driver. The MAX98357A uses "simple-audio-card", which needed the generic 'snd-soc-simple-card' module, but I was loading the Raspberry Pi-specific 'snd-soc-rpi-simple-soundcard' module. Looking back at it, I probably could have solved this in a few minutes, if I had read the documentation. At all.
Web UI
To make the system more user-friendly and easier to configure, I wanted to have a simple and lightweight Web UI for configuration and monitoring. I chose to build it with Svelte and compile it for lightweight runtime usage to keep the system lean and fast. Svelte is my go to framework for building lightweight applications and in this case the complied runtime is ideal as an alternative to a heavy webserver rendering something like React.
What Comes Next
With the chime box now rewritten, the same architecture will be extended to the rest of the VC family starting with the actual doorbell intuatively named bell. This will need more of everything: a more complex Linux image and Web UI, more services running in parallel and more hardware. But the core architecture will remain the same and I can hopefully reuse a lot of the code accross the different devices.