In the summer of 2021, I bought a Watchy: a hobbyist watch with a tiny (200x200 pixel, 4cm) e-ink screen, an ESP32 processor (plus separate real-time clock chip), 4 buttons, a step-counting accelerometer, and 200mAh battery. I was doing a lot of ESP32 coding at work, so the idea of writing my own watch software sounded like fun, and the ESP32 is pretty powerful: tons of memory, bluetooth/wifi support, and good low-power modes.
Over the course of the next year, I iterated on a unique watch design that I really liked, and had fun showing off to other nerds.
How does it work?
In normal operation, Watchy’s ESP32 is powered down. That’s right, the cores and all but 8KB of RAM are turned off in what they call “deep sleep” mode. The e-ink screen is also off, conveniently still showing the most recently-drawn clock face. Only the RTC (real-time clock) chip is powered, to track the current time. When that time rolls over to the next minute, the RTC triggers an interrupt that wakes up (actually: boots up) the ESP32. The four buttons are also connected to a different interrupt/wake pin so they can have a sub-minute reaction time. As it “boots up” once a minute, the sample Watchy code initializes the e-ink display, updates it for the current time, and then powers everything off again.
But the fun of owning a hobbyist watch like this is replacing the sample code with something personalized, so of course that’s what I did.
First, I drew a round clock face to show the current (12-hour) time, with no number markings, because it looked cooler that way. Then I put the digital (24-hour) time and current day in the bottom corners, and an icon of the battery level in the top corner. I’m a visual learner, so the large clock face gave me an instant impression of roughly what time it is, while the digital time in the corner let me know the exact minute if I was about to be late for a meeting.
I’d written code for an e-ink display before (the Inkplate), and that one could take about a dozen quick-refresh partial updates before the screen started to show the faded remnants of old content, like a half-erased etch-a-sketch. To avoid that, you needed to do a full refresh, which causes the screen to distractingly flash black and white a few times. Whatever e-ink display they used in the Watchy does not have that problem. It looked like I could update the clock face every minute for hours (maybe even days?) at a time without the full refresh. I ended up doing a full refresh for any user interaction via button press that was going to change the whole screen anyway, but I’m not sure it needed even that. Amazing!
The most obvious use for the built-in wifi is NTP, and I believe some of the example code from Watchy already demonstrated this. It’s easy because an NTP client is built into the ESP’s standard library. Wifi, however, is extremely power hungry for a tiny embedded device, so I set it to connect only at the top of each hour. The RTC chip is not very accurate, losing about 5 seconds a day, but an hourly wifi check was enough to keep it pretty accurate without draining the battery. Given how much time I spent away from my home wifi without reconfiguring the watch’s connection, it was probably good enough to be unnoticeable even if it only updated every day or two.
One improvement I made was to set the RTC’s time in UTC (as it comes from the NTP server), then use the ESP’s standard library to convert to the current time zone. It already has support for one of the standard time zone description formats, including daylight savings, so it was both easier and better than trying to do it by hand, and left open the possibility of switching between a set of predefined time zones (foreshadowing).
At that point, I thought: But this watch has wifi. Much more is possible!
Weather conditions
I used to track our house’s thermostat usage against the outside temperature, so I got skilled at bringing up a new API client as each weather service either died (Weather Underground), enshittified (Dark Sky), or both. I’m currently using OpenWeather and like it just fine. I drew some tiny icons to represent the current conditions, and displayed the “current” temperature and humidity from the last time it connected to wifi.
Wildfires and the day the sun didn’t rise were also top-of mind, so I connected to the AirNow API too, and drew some tiny icons representing 5 different levels of air quality, from “great” to “gas mask time”.
Here are the icons for your amusement, and feel free to use them for your own projects if you like. If it’s possible to license a 24-pixel icon, I hereby release them under CC0:
What next? Why not sunrise and sunset? Why not the current moon phase, and moonrise and moonset? I often go for night walks, so this is not useless info! Turns out you can get a great estimate of all of these if you put the current time and your latitude/longitude into a set of cubic equations that first appeared as a Pascal program on a floppy disk included with a paper book about astronomy in 1994. I only mention that because almost every code sample you can find online is a direct descendant of that code – including mine. You can find this same code translated into QBasic, Javascript, and Python. It deserves its own “secretly influential code” blog post. (The equations are complicated because not only are all three orbits elliptical, but everything wobbles.)
I’m not a great artist (please act shocked), so I bought high-quality icons of the moon phases from the Noun Project and down-res’d the images to 48 pixels, which did them a lot of injustice, but still looked great.
While working on this feature, I took a vacation to Hawai'i, which bumped up the priority of supporting multiple time zones. Naturally I had to stay up all night on the lanai, hacking on my watch as the moon rose. I ended up making a table of locations popular with my friends and family, and letting you switch between these locations by name. Each location has a time zone descriptor and latitude/longitude, which is enough to localize the time, weather, and astronomical conditions. It’s easy to add new ones by looking up a city’s entry on wikipedia, which has all the relevant data in a handy table in a sidebar.
Now it gets excessive
All this weather and moon information really needed to be on a second screen to keep the main clock face from getting cluttered. There also needed to be a way to change the current location (and possibly other future settings) without recompiling and re-installing the watch each time. So, of course, I decided to write a custom UI toolkit, and assigned a purpose to each button: menu/select, back, up, and down.
Several KB of RAM are preserved when the ESP32 is in deep sleep, and that’s more than enough to store all the state you need to track user interactions: which “view” is currently displayed, a small stack of previous views for navigating “back”, and the state of each view (like which menu item is highlighted). It’s a small screen that can only display 8 lines of 3mm-height text, so any long text or menu also needs to support scrolling and remembering its scroll position. Aside from memory reserved for the system log and a dynamic demo, it all uses less than 1KB of state… and most of that is for caching the current weather & air quality across 10 locations.
I was so pleased when I got word-wrap and scroll working – with a scroll bar and long-press to scroll by a page! – that I recorded a small video demonstrating it on a famous PKD quote. Then I built a small menu widget for changing settings on the fly (like location). Preferences are stored in NVS, a tiny wear-leveled key-value flash store that’s part of the ESP standard library. EZPZ.
You may have looked at the images so far and thought, “What bitmap font is that? I haven’t seen one that odd before.” That’s because I took my Bizcat font and adapted it to a larger size (24-pixel) proportional style to fit the watch.
Nothing lasts forever
For a while, I had a lot of fun writing the code and adding features. Then it felt sort of “done” and it was my working watch for two years, which may be the highest praise to give a thing like a wristwatch: after a while, I took it for granted. Every once in a while, someone would notice it and I could show it off, but most of the time, I just … used it.
I tried to connect with other Watchy fans to share code & ideas and get inspired, but the only active forum seemed to be a Discord guild. As with all discords, information tended to scroll gently away into oblivion, with no way to pull out or organize the good stuff. A message board and/or a group wiki for tracking projects and toolkits would have been invaluable, but the community never got enough traction to make the leap, and the creator wasn’t really engaged with it by then.
If you feel like iterating on the Watchy design, the schematics are online. There were two common suggestions from users for a 2nd rev:
- Add some minimal amount of water resistance to the case. Some people reported that a single drop of water could kill the device. This makes it hard to wear if you live in a place where it rains.
- Add a little LED light like old Casios had. I don’t think it would have hurt the battery life much since you usually don’t hold the light on for very long. Without this, the watch was unreadable outside at night.
I found network connections over wifi to be extremely flaky, though it seemed to be a software problem, not hardware. At the top of each hour, if I was home, my Watchy would connect to the house wifi and update NTP and weather data. Weather involved making a separate HTTP request to OpenWeather and AirNow for each of the 4 or 5 cities I was tracking, but I rarely saw it complete all of the cities before it would start getting network errors as if the wifi had dropped out. (The wifi was fine.) Sometimes it would only get one city, sometimes almost all of them. I tried adding delays and fussing around to triple-ensure that all resources were closed after each HTTP request, but nothing worked. It might have been a bug in the ESP network stack… or maybe there was one last elusive resource I didn’t close correctly. I never figured it out.
As you can see, mine got a lot of use despite those shortcomings.
My first watch died after about a year for mysterious reasons. I bought a replacement and felt newly inspired to hook up the step counter and track a small history of daily steps in flash. I factored out common code into a “Watchy OS” library that others could reuse, but never felt motivated to actually post it as a separate repo. Later I found out this “shared base OS” idea had percolated through the community a few times before without much success.
My second watch died within about a year for similarly mysterious reasons. This time, it was still working, but could no longer be charged. Once it drained its battery completely, it was an ex-watch, permanently showing the last time it had enough juice to update the screen.
I decided not to buy a third, because it was kind of expensive for a watch that lasts one year, and it was clear the product had quietly reached end-of-life. I still miss it.