jonah.id / Doorbell
  1. 2024-03-11 — Doorbell ⬿

Doorbell

Intro & Assets

A hardware and software project to monitor/control my building’s doorbell/lock panel.

Repository: sr.ht/~jonahbron/doorbell

Motivation & Background

I’m never home when the mail comes.

I love the USPS and use them whenever I have a choice for shipping. They have the key to my building’s lobby. UPS and Fedex do not. Package theft is not uncommon, thus they often will not leave a package if nobody answers the door. They almost always deliver during weekday in work hours, which is pretty reasonable considering the delivery workers are… working. The problem is that I’m working too. And not at home.

I need a way to let them in without being home. My particular building has a very simple 16v doorbell/door lock system. In my unit is a panel with a bell inside, and a button that will unlock the front door. This is a very simple electric system: one input, one output. Sounds like a job for a microcontroller to me.

Design

The principal constraint guiding this project’s design was my desire to have an entirely self-contained system in a single device. No cloud services, no secondary hardware, no phone apps. By reducing the number of interconnected and interdependent systems, I can have more control over what could become obsolete over time. And that’s important in something like this. It needs to be set-and-forget (unless the doorbell rings).

The goal is to receive a notification on my phone when the doorbell rings, and to be able to unlock the door remotely.

Between that constraint and that goal, the design I settled on was thus:

A small Wi-Fi capable microcontroller connected to an optocoupler for doorbell sensing and a relay for door lock control, notifying me of doorbell events via the Web Push API, and accepting unlock commands over HTTP.

Hardware

The microcontroller I settled on is the Seeedstudio XIAO ESP32C3.

Beyong that, the project calls for a daughter board that can connect the microcontroller to the optocoupler and the relay. With only two main components to add, the wiring is fairly straightforward.

Wiring schematic

To validate the design, I prototyped it on a solderless breadboard.

Overview of breadboard

After verifying that its electronic properties have the basic capability to do what is needed (receive a signal on doorbell and switch the relay), I ordered a PCB.

Front of the PCB design Back of the PCB design

After soldering, it all came out as a compact and clean looking piece of (raw) hardware.

Front of the PCB design Back of the PCB design

All of the source files can be found in the Git repository.

Software

In order to maximize productivity, I opted for a MicroPython runtime, to take advantage of its mature library ecosystem. The software has three primary responsibilities.

Setup

Before any notifications can be received, the user agent must create a Push Subscription and provide it to the doorbell device. This is done by visiting a web server hosted on-device. The user is prompted to give notification permissions, and once that is done, the server is handed the subscription endpoint.

Doorbell Monitoring

At all times, the device is monitoring a GPIO pin connected to the optocoupler. When the doorbell is pressed, the current throughh the optocoupler will open a transistor that pulls the GPIO pin to ground. That triggers an interrupt in the software. When that interrupt is detected, the software will iterate through all Web Push subscription endpoints, and send a notification to each with a signed VAPID token. If any endpoints return an error, they are removed from the list for future triggers. Any valid endpoint pushes will result in a browser push notification on the user agent (phone, computer, etc).

Door Unlocking

The Web Service Worker that is bound to the application will send an HTTP request with an unlock command to the doorbell device if the user taps a notification they were shown. When the device receives this request, it will signal the relay to toggle, hold it there for a brief period, then end the signal to allow the door to re-lock.

Secondary Responsibilities

Besides the primary activities of the device software, there are a few other things it must do.

Logging

A full record of doorbell presses, unlock commands, and system faults is logged out to the filesystem for audit.

WDT

Because this is a security-sensitive device and potentially impacts multiple people in the building besides myself, it’s important for the device to be stable and reliable. A last line of defense is to use a hardware Watchdog Timer to monitor for a crash. If the software stops operating correctly, the WDT will reset the entire system, which will ensure the door is re-locked immediately.

OTA Updates

This device is installed inside of a panel secured with screws in my wall, and hard-wired into screw terminals. It’s a pain to take out. To make software changes easier, the onboard server has an endpoint that will accept a software bundle. When it receives a request with a bundle, it will extract the python files and write them to the filesystem, then reset itself.

The full source can be gound in the Git repository.

Challenges

Memory Limitations

The microcontroller I chose does not have a lot of memory. The basic tasks it is performing (interfacing with two GPIO pins) do not require a lot of memory. What does requirei a health amount of memory is communicating over HTTPS, both as a client and a server. To get all of the network code to work without immediately running out of memory, it was necessary to freeze the HTTP libraries into the MicroPython firmware with a manifest.

Repeatable MicroPython Builds

Installing all of the dependencies needed to build MicroPython is non-trivial. And as someone who is solidly in his Nix era, doing that manually was not a solution I was going to accept. To address this, I created a Nix flake with the ability to build MicroPython firmware, and more importantly, for a user to specify how to build it (what libraries to include, etc.) as part of their own Nix flake. Doing this was incredibly challenging, as it runs up against the purity of Nix. The number of dead-end solutions and late nights thinking I was close to having it working is painful to think about. One of the biggest issues came from the fact that the ESP-IDF toolchain necessarily communicates with the network during the build process. I thought I could get around this, but in the end I had to use a known-output derivation to get it working. This will apparently change in the future, so hopefully a truely pure MicroPython builder will arise soon.

Inadequate Libraries

There were a few libraries that did not fully meet the needs of my application, and they follow a chain of dependencies. Ultimately, I need VAPID signing. If a library already existed for that, I would have been able to focus much more on application code. But it didn’t. So I had to make it. But to do that, I needed JWTs with the ES256 algorithm. There was an existing JWT library, but it only supported HMAC. So I had to add ES256 support. But to do that, I needed to be able to generate and sign with Clliptic Curve keys. There are already libraries for asymmetric key encryption, but none supported EC and the ESP32C3. Luckily, ucryptogrpahy was able to add support for the ESP32C3 quite quickly.

Reliability

Several times, I believed the project to be working and fine. But when I left it running, it would unpredictably stop working. Adding loggin revealed that it was running out of memory, which would crash the runtime. Tweaks to avoid the OOM error were of no avail. At one point, I think the device might have crashed while holding the door unlocked, which caused some issues for my landlord. It was essential to avoid a condition like that, so I added a WDT that can gracefully handle a fatal condition.

Power Supply

Since the hardware is sitting in a panel connected to wires, I hoped that there might be a way to parasitically draw enough current from the doorbell system to power the hardware. But try as I might, I never found a way. As a result, the board is being powered by a very log USB cable punched through the opposite side of the wall into a closet with a power outlet. Power source

Prerequisite Projects (stuff I had to make first)

MicroPython Builder

As with many embedded systems, setting up an environment with all the dependencies installed to build a working binary is a huge pain. I’ve been using Nix for a lot of things recently, and its repeatability can be really helpful with something like this. There was no existing completely integrated solution for building a MicroPython firmware binary with Nix, so I made one. It depends directly on nixpkgs-esp-dev and draws heavy inspiration from micropython-esp32.nix.

micropython-lib VAPID

Connecting to the Push API from the server requires the ability to invoke pusher service endpoints using VAPID authentication. In short, the server (the device in this case) holds a private key that it uses to sign any notifications it sends out, that way the pusher service can be certain that the push came from an authorized source. This code could have stayed in my particular application, but I decided to upstream it to make it less brittle and more generally useful to the MicroPython ecosystem.

micropython-lib JWT

VAPID depends on JWT (Json Web Tokens) for the push signing. While there was already a JWT library for MicroPython, it only supported HMAC signing (symmetric). It did not support ES256 signing (asymmetric), which is the scheme that VAPID requires (since the client-side needs a public key). To meet this requirement, I contributed that extra capability to the official library. It depends on the ucryptography library.

Dependencies & Tools (stuff other people made first)

Resources & Inspiration (stuff other people wrote)

Creative Commons License