Date Tags C / pebble

Mutt: What will be the appropriate designation for the upcoming period of solar rotation in the Anno Domini system as devised by St. Dionysius Exiguus?

Jeff: Why don't you just look at the pie?

Mutt: Couldn't you just answer the question? Why do you always have to be a sarcastic asshole?

Jeff: Seriously, look at the pie.

Mutt: Huh. Yeah. I see what you mean. Sorry dude.

Jeff: No problem, my tinhorn peer. It is after all New Years.

Ens.: And to all a good night.

TL;DR

Consciously uncouple from the yoke of technological precision, using nothing but precision technology and simple calculus.

Also, some quick pointers on getting started with the Pebble SDK.

Chronotourism and the Modern Condition

Leap seconds sure are something, right? Kith and kin, pauper and prince, all glued to their cesium synced devices, each of course making a personal decision about when to begin bating his or her breath, but all sharing equally in the glory of that soon-to-be-slightly-improved coherence between Coordinated Universal Time and Mean Solar Time.

If you're a certain kind of computer program, the business of when and if leap seconds occur can't be ignored, but, if you're a nightowl with state-of-the-art chronometry astride your wrist, or NTP packets fluttering on bands of LTE in the vicinity of your buttocks, it's all about the thrill. With no suitable target for our temporal attentions since June of 2012, it's no wonder that some of us have turned to drink.

Exactitude

The era of big game may be over, but at least, should the trains at some point run on time, we'll be ready. For one thing, the Swiss sell us more than $20B worth wristwatches every year. I used to think it was odd that, Switzerland being one of the only places where the trains actually do run on time, they export 95% of their production, but I've been told that the trains run not just on time but frequently, so it really doesn't matter when you show up. When you're flying United Airlines operated by ExpressJet - DBA United Express, every second counts, or at least you count every second, so remember not to leave that extremely expensive hunk of exotic alloy in the gunk at the bottom of a TSA tray.

I won't have that problem, because I am too cool for words. Well, I will have the United-Airlines-operated-by-ExpressJet- DBA-United-Express problem, but my TSA gunk will be adhering to the handsome, tomato-red plastic exterior of a refurbished first-generation Pebble... that just happens maybe to be displaying a convincing simulacrum of one of those fine Swiss things.

I want my SDK

Maybe. You see, the Pebble also comes with a so-called SDK. That is a technical term, in this case best translated as the reason well enough will not be let alone. There's also a whole repo of example watch faces and apps, just begging to be contorted inanely by people who are supposed to have better things to do.

Depending on how trusting a soul you are, it will take between 0.5 and 5 minutes to download and install the tools. Their curl | sh maneuver is pretty sane overall, even using virtualenv to avoid polluting your python installation, but it does try to slip in an echo $HEINOUSNESS >> "$HOME/.bash_profile", so I preferred to download the script and follow along manually.

Then you just cd to one of the sample directories and

  pebble build

Even to do conventional things with the watch, you'll have already had to install Pebble app on your phone, and it has a "developer mode" option, that listens on a port for things like

  pebble install --phone ${PHONE_IP_ADDRESS}

Nb.

  1. You probably don't want to do this on a public network.
  2. Also, there are other ways to skin this here onion, including an entirely cloud based IDE, but I didn't want my CPU to get cold and lonely, and I can't imagine that a nice person like you does either.
  3. Other than an abandoned private effort, there's no emulator available.

The Clunky Old Watch Face

Be transported to magical olden times when you strap on this beauty. This watch loses or gains several minutes a day, depending on how much it's been wound, where winding is accomplished by switching out and back into the face. Additionally, it believes that every month has 31 days.

You can actually get it at the pebble app store, and it looks like this:

Little ab initio coding was involved, just a few tweaks of the the simple_analog watchface from Pebble's SDK examples.

Rules of disengagement

  1. The winding level of the watch $w$ varies between 0 and 1.
  2. Other things being equal, the watch unwinds at a constant rate $dw/dt = -\upsilon$ until it hits $w=0$.
  3. While $w>0$, watch displays $t+s(t)$, where $t$ is the actual time, and $s$ is a time-varying skew. When $w=0$, it displays $t_z+s(t_z)$, where $t_z$ is moment $w$ hit zero.
  4. When $w>0$, skew changes at a rate proportional to the winding level in excess of a bias-point: $ds/dt = \alpha (w - w_b)$; when $w=0$, $ds/dt=0$.
  5. The minute hand should be a little crooked.
  6. $w$ increases by $\Delta w$ each time the watch face is unloaded and reloaded, subject to $w\leq 1$.
  7. At time $t_f$, when the watch is fully wound and $w=1$, we reset $s=0$.
  8. The displayed day of the month shall be $d_f + (t-t_f)/\tau_d \mod 31$, where $d_f$ is the correct date at $t_f$ and $\tau_d$ is the length of a day.
  9. The day of the month should be a random sequence of letters.
  10. The apparent brand of the watch shall be BOFFO.
  11. The sweep second hand does not change continuously but jumps every $\tau_j$ to $t + \tau_j + U([0,\tau_j])$,

Qualitatively, the watch gains time when the mainspring is tight, but loses time as the spring loosens.

Precision inaccuracy

Now, we could simulate the evolution of skew by incrementing the various state variables at small time intervals, but this post is about precision, and, goddamnit, our inaccuracy is going to be precise. Even more importantly (if you can imagine that), the user might switch to some other, inferior watch face for a spell, and it could potentially take them hours to realize the error of their ways and switch back.

Given the state $s_0$, $w_0$ as of $t_0$, we can integrate directly:

$$\begin{eqnarray} s_t &=& s_0 + \int_{t_0}^t \alpha (w-w_b) dt' \ &=& s_0 + \int_{t_0}^t \alpha (w_0 - \upsilon (t'-t_0) -w_b) dt' \ &=& s_0 + \alpha [w_0 + \upsilon t_0 - w_b] (t-t_0) - \frac{1}{2} \alpha \upsilon (t-t_0)(t+t_0) \ &=& s_0 + \alpha (t-t_0) [(w_0-w_b) - \frac{1}{2} \upsilon (t-t_0)] \ \end{eqnarray}$$

Of course, the clock stops when $w$ will hit zero at

$$t_1 = t_0 + w_0/\upsilon$$

so if $t_1<t$, we'll use it instead of $t$ in the expression for skew.

With the winding bias $w_b=0.6$, unwind rate $\upsilon=0.04/{hr}$ and skew/winding coefficient $\alpha=2 {min}/{day}$, skew evolves like this:

Skeleton code

In the basic watch face you are required to have a main method, and it should probably call app_event_loop():

/* Lots
   of
   file
   static
   variables */

int main(void) {
  /* Initialize stuff. */
  app_event_loop(); /* built-in function */
  /* Clean up stuff */
}

The initialization/clean-up will comprise:

  1. Recovering/persisting the various $x_0$ values.
  2. Setting up and tearing down the graphics objects.
  3. Scheduling/canceling callbacks.

None of the examples set a return value, so it's apparently discarded.

Workflow

Since it's not possible to run watch code except on the watch itself, I recommend an incremental approach, with frequent re-installations and lots of logging. There's a handy macro for the latter,

    APP_LOG(APP_LOG_LEVEL_DEBUG, "Minute tick.  Setting skew.");

messages from which can be viewed with

  pebble logs --phone ${PHONE_IP_ADDRESS}

The whole process is pleasantly retro. I pity the style-conscious fops who will find it necessary to design apps for the iWatch.

State

Our watch face "app" is not going to be running continuously; indeed, absent any better metaphor for winding, we will be requiring the user to exit and re-launch the face using the little up/down buttons on the side of the watch. During the painful hiatuses, we'll need to store the various $0$ subscripted values persistently. The SDK provides for this sort of thing via

  void persist_write_data(int32_t key, const void *data, size_t len);
  void persist_read_data(int32_t key, void *data, size_t len);

with some specialized forms, for what are supposed to be the most commonly stored data types. The key is arbitrary, but obviously unique within our watchface. Since, at least once (more, if the user deletes us but subsequently repents of this hasty decision), the face will have no stored data, we must detect and deal with that situation. Thus, for example:

static const uint32_t WLEVEL_KEY = 4;
static double w0;  // initial winding level
static double w;   // evolving winding level
// ...
if(persist_exists(WLEVEL_KEY) {
   persist_read_data(WLEVEL_KEY,&w0,sizeof(w0));
   w = w0;
} else {
   w = w0 = 1.0;
}
// ...
persist_write_data(WLEVEL_KEY,&w,sizeof(w));

We'll deal with the gradual unwinding of $w$ in callback code below.

Graphics

The graphics code is pretty standard, though somewhat more verbose than is necessary in fancy languages. You start by creating a window and setting callbacks for loading and unloading it:

  window = window_create();
  window_set_window_handlers(window, (WindowHandlers) {
    .load = window_load,
    .unload = window_unload,
  });

We'll also build up little graphics objects, like a pointy minute hand,

const GPathInfo MINUTE_HAND_POINTS =
    { 5, (GPoint []) {{ -8, 20 }, { 8, 20 }, { 0, -30}, { 0, -80 }, { -8, -30}  }};
minute_arrow = gpath_create(&MINUTE_HAND_POINTS);

which would be even simpler if we hadn't added extra points to make it jagged.

Set up a tree of layers

In window_load, we define a tree of "layers", which at this point have no content other than further callbacks that will render them when they, or a parent layer, are marked dirty.

Window *window;
Layer *simple_bg_layer;
Layer *hands_layer;
Layer *date_layer;
TextLayer *num_label;
char num_buffer[4];

static void window_load(Window *window) {
  // Root layer
  Layer *window_layer = window_get_root_layer(window);
  GRect bounds = layer_get_bounds(window_layer);

  // Child 1: the watchface background
  simple_bg_layer = layer_create(bounds);
  layer_set_update_proc(simple_bg_layer, bg_update_proc);
  layer_add_child(window_layer, simple_bg_layer);

  // Child 2: the hands
  hands_layer = layer_create(bounds);
  layer_set_update_proc(hands_layer, hands_update_proc);
  layer_add_child(window_layer, hands_layer);

  // Child 3: the date
  date_layer = layer_create(bounds);
  layer_set_update_proc(date_layer, date_update_proc);
  layer_add_child(window_layer, date_layer);

  // Child 1 of date: the day of the month
  num_label = text_layer_create(GRect(73, 114, 18, 20));
  text_layer_set_fonts_and_stuff_like_that(num_label ...)
  layer_add_child(date_layer, text_layer_get_layer(num_label));

  // Child 2 of date: the day of the week
  // ... you can imagine this

  // more stuff
}

Individual layer callbacks

The main business happens in the various ...update_proc functions. Here, for example, we can maintain the date layer and its children, using familiar stdlib utilities:

static void date_update_proc(Layer *layer, GContext *ctx) {
  time_t now = time(NULL);
  struct tm *t = localtime(&now);

  strftime(day_buffer, sizeof(day_buffer), "%a", t);
  text_layer_set_text(day_label, day_buffer);

  strftime(num_buffer, sizeof(num_buffer), "%d", t);
  text_layer_set_text(num_label, num_buffer);
}

Of course, that's really boring. On our watch, the date simply advances every 24 hours, modulo 31, which is usually the number of days in the month:

static void date_update_proc(Layer *layer, GContext *ctx) {
  time_t now = time(NULL);
  int d = dwound + (((now-twound)/(24*3600)) % 31);
  snprintf(num_buffer,sizeof(num_buffer),"%d",d);
  ...
}

This twound is the time that the watch was last fully wound and, we assume, set to the correct date. It's persisted along with other state variables.

The hands update is more graphically intense. We start by getting the incorrect time

static void hands_update_proc(Layer *layer, GContext *ctx) {
  time_t now = time(NULL) + s;
  struct tm *t = localtime(&now);

where the skew s is going to be updated separately in a different callback. We rotate the hands into place and draw them:

  gpath_rotate_to(minute_arrow, TRIG_MAX_ANGLE * t->tm_min / 60);
  gpath_draw_filled(ctx, minute_arrow);
  gpath_draw_outline(ctx, minute_arrow);

In addition to simple skew, the second hand also suffers from a clunking twitch,

  int jsec = t->tm_sec + (rand() % JUMP_SEC);

but it's still just a straight line and very easy to draw:

  int32_t second_angle = TRIG_MAX_ANGLE * jsec / 60;
  secondHand.y = (int16_t)(-cos_lookup(second_angle) * (int32_t)secondHandLength / TRIG_MAX_RATIO) + center.y;
  secondHand.x = (int16_t)(sin_lookup(second_angle) * (int32_t)secondHandLength / TRIG_MAX_RATIO) + center.x;
  graphics_draw_line(ctx, secondHand, center);

Temporal callbacks

The graphics callbacks are triggered through the cascade of layers. To respond simply to the passage of time, we set up a handler explicitly:

  tick_timer_service_subscribe(SECOND_UNIT|HOUR_UNIT|MINUTE_UNIT, handle_tick);

It wasn't clear to me at first that there could be only one timer subscription, so I originally had separate callbacks for hours, minutes and seconds. This produced very odd results, as repeated calls to tick_timer_service_subscribe only overwrite some of the previously set information. The one callback can still detect why it was called. Once a minute, we'll update the winding level and skew:

static void handle_tick(struct tm *tick_time, TimeUnits units_changed) {
  if(units_changed & MINUTE_UNIT) {
    t1 = time(NULL);
    w = w0 - upsilon * (t1-t0);
    if(w<=0.0) {
      t1 = w0/upsilon + t0;  // time the clock stopped
      w = 0.0
    }
    s = s0 + alpha * (t1-t0) * (w0 - W0 - 0.5 * upsilon * (t1-t0));
  }
  ...

Note that we don't count on actually being called every minute. The level and skew are calculated from scratch, and we keep track of the time t1 we calculated them so we can later persist it.

On the per-second callback, we'd typically just mark the root layer dirty and let the graphics callbacks take care of the rest,

  if(units_changed & SECOND_UNIT)
    layer_mark_dirty(window_get_root_layer(window));

but our watch stops working when $w=0.0$, and it only ticks every seven seconds:

  if(units_changed & SECOND_UNIT &&
     (tick_time->tm_sec % JUMP_SEC)==0  && w>0.0) {
    layer_mark_dirty(window_get_root_layer(window));
  }

Low standards

Unlike the famously picky iPhone app store, it seems that you can publish anything to Pebble. Once you set up a developer account, you create a new app or watchface by uploading the .pbw file created during the build and clicking "Publish." You wait about 5 seconds for approval, the main criterion for which seems to be having set a previously unused UUID for the app in a JSON file. What's more, despite the total lack of oversight and a ridiculously bad search facility, more than zero people might actually install your stuff. Someone even hearted mine!

And if the BOFFO is not quite clunky enough, you can either fork it or look at the pie.1


  1. From The Silver Spoon, also transcribed here



Comments

comments powered by Disqus