Laconic watch face for Bangle.js 2


Thu 15 Dec 2022

I would love to run Scheme code on my watch, and I'm getting closer. For the original Pebble watch, I wrote a watch face app in C that I called Laconic. I ported Laconic to JavaScript for the Fitbit Versa, and have now done a total rewrite for the Bangle.js 2. It's not yet Scheme, but JavaScript is closer than C.

The Bangle.js 2 is an inexpensive smartwatch with an ARM processor, 256K of RAM, 9MB of flash memory, a touch screen, GPS, a vibration motor, and a heart rate monitor and other sensors. Apps are written in JavaScript. The watch uses a special version of the Gadgetbridge Android app, which can forward notifications from selected phone apps to the watch over Bluetooth. Watch apps can retrieve data from web sites using Gadgetbridge.

The development environment is a web-based IDE that communicates with the watch via Bluetooth. It has both an editor and a JavaScript REPL. The API is powerful, with minimal boilerplate, and is well documented.

Laconic is now exactly right for me. It displays the time, the date, and the day of the week, and a simple diagram that shows the times of all the events I have in the next twelve hours, as well as the times of all the alarms I have set to remind me about those events. It silently buzzes my wrist whenever one of those alarms goes off. The screenshot at the top of this page shows six alarms for three upcoming events.

  "alarms": [ 1670456700000, 1670459400000, ... ],
  "events": [ 1670459400000, 1670466600000, ... ]

Laconic periodically fetches a JSON object with event and alarm times from my calendar server, which is written in Scheme, then stores them in flash memory. It makes sure that I don't miss any appointments, and keeps working even when I'm offline for a few days.

To illustrate how expressive the Bangle's API is, here's Laconic's draw function, which is called once per minute, or whenever the display needs updating:

function draw() {
  let now = new Date();
  let dayDate = Locale.dow(now, 1) + " " + now.getDate();

  g.clearRect(0, 0, WIDTH, HEIGHT);
  for (let i = 0; i <= 360; i += 6) {
    drawCircularMark(i, -1, i % 5 == 0 ? 2 : 1);
  twelveHourRange(now, calendarData.alarms).
      a => drawWedgeMark(dateToDegrees(new Date(a)), 3, -5, 5));
      e => drawTriangularMark(dateToDegrees(new Date(e)), 10, -25, -5));
  drawTriangularMark(dateToDegrees(now), 10, -25, 5);
  g.setFontAlign(0, 0);
  g.drawString(formatTime(now), CENTER_X, CENTER_Y - 15);
  g.drawString(dayDate, CENTER_X, CENTER_Y + 30);

The built-in g object gives access to all the 2D graphics primitives. I wrote drawCircularMark, drawTriangularMark, drawWedgeMark, and twelveHourRange, each of which is about ten lines long. The entire source code for Laconic is only 15K, of which 7K is font data. That's a pleasant contrast to almost every other kind of modern development.

Some day, I'll have Scheme running on my watch. In the meantime, I'm thrilled with my Bangle.js 2.