((λ (x) (create x)) '(knowledge))

Rebble Hackathon #002

Developing apps for a "dead" smartwatch ยท March 25th 2025

So earlier this month was the 2nd Rebble Hackathon, a little week long event where a bunch of us got together to breathe life into the "dead" Pebble smartwatch ecosystem. The flood of applications, watch faces, development tools, art, and generally fervent community development and engagement really drives home the "dead" aspect of this community. Over the course of a week we built a bunch of new applications, even more new watch faces, and a whole slew of tools and art! Maybe we all just felt extra encouraged by the fact that Eric Migicovsky (founder of Pebble) has decided to bring Pebble back! After 10 long years we have the PebbleOS firmware open sourced thanks to the work of former Pebblers and Google's kindness. And with that gift we have a store for that new Pebble!. This is just too freaking exciting, and that comes from someone who has clung desperately to his Pebble Time since he got it. And actively point to it as being one of the first areas in which I dabbled with programming productively.

But what did you do for the Hackathon?

Well, I personally painstakingly wrote ~250 lines of C and desperately tried to turn myself into a graphic designer before Mio kindly volunteered to help. And the result of a weeks worth of 2am hacking sessions and me frantically trying to remember how to do any of this stuff resulted in Pinout!. This legitimately only looks as good as it does thanks to Mio's contributions, all of the art is of their creation, and I am beyond thrilled with the results!

The Pinout Banner!

Application Menu RJ45A Pinout RJ45B Pinout RJ45A Crossover Pinout RJ45B Crossover Pinout

Seriously, you can take a look at my first three attempts at this application to get an idea of just how bad this would have looked without Mio's help. My talents firmly lie inside of the realms of operating cameras when it comes to art, and my brain has an immense amount of patience for wrangling painful things like C, but seems to revolt when faced with creating something that isn't code.

This was my first crack at Pinout, I threw this together while I was setting up a new office network. I couldn't remember the Pinout for RJ45B in the moment, and had to tip cables 30ft in the ceiling so that I could affix APs to an I-Beam. The safest reference I could think of was on my Pebble.

The very first version of Pinout, a vector art thing thrown together years ago. It's just colored lines barely legible.

But of course that poorly rendered, totally in accurate version wasn't acceptable and I eventually stopped using it. I thought that perhaps I could be lazy since I'm not particularly artistic and I could just use a cribbed image I found online. I downscaled and dithered a pinout and threw it on the pebble! It worked! But it wasn't exactly usable.

Validating my image rendering code using down-scaled dithered images, just image a very pixelated cable, it's terrible.

Now Mio has recommended Inkscape to me in the past for creating SVGs, and I gave it my best effort, but after struggling for a few hours to come up with this bland, color in accurate, incorrectly scaled image. And realizing after loading it that I can created an icon and not a serviceable Pinout image, I was at a bit of an impasse and switched over to working on the actual code. Maybe I could figure out a dithering solution or vector art using PDC. I wasn't really sure, but I didn't think continuing with Inkscape was a conducive use of my time.

And this was my attempt at a cable diagram, it lacks the finesse of Mio's work

Admittedly, the last attempt was sort of on the right track, I think if I had had a lot more time and was just adding new cabling diagrams to the app that I could have gotten something acceptable together. Mio was kind enough to provide SVGs for the cable diagrams in Pinout, so I have a legitimately excellent starting point for the next time I try this, and I will definitely be releasing another version with more diagrams in it soon! I personally want to add RJ45 diagrams for rollover cables like you'd use for Cisco console cables, and one way passive cables. Those also open the doors to potentially adding RJ11 diagrams or maybe even serial pinouts! I think though that RJ45 is my primary use case and I just want Pinout to be as useful for as many Pebblers as is possible. It would please me to no end to know other people are using it!

So that C..ommon Lisp code?

Okay lets be real, I procrastinated until the last couple of days on this. I had ideas! But I spent the first 4 days writing Common Lisp libraries and trying to teach myself Inkscape. I think the C scared me, and my reaction when faced with "learn C" has always been "okay, I'll learn C...ommon Lisp". It's all of the ((())), the allure of a good list is just too much. Now there's nothing wrong with this process, and initially I thought it was necessary! The old Pebble tools are written in Python2, and while there has been some work done to update them to Python3 that's not really my style (though I will totally use them once the new Pebble's are release, package them for Alpine even!). There's some awesome work being done to re-implement the entire Pebble runtime in Rust so that it can run on the Playdate, which is wicked cool and the developer, Heiko Behrens, even released a pre-built binary of his PDC tool for the hackathon, so I knew from the get go that long term there's some solid work being done to re-implement these old tools. But if you know me, you know that I don't particularly care for Rust. Nothing wrong with memory safety, but needing hundreds of mbs of libraries to compile anything is ridiculous.

Wait, back up, PDC? Oh yeah, this is the fun stuff, if you thought Pinout was cool then lets take a detour into obscure binary formats, because that is precisely what PDC is! So the Pebble smartwatch can render icons and images via vector graphics. Animations occur throughout the Pebble smartwatch, like if you delete an item from your timeline you'll see a skull that expands and bursts. If you have an alarm you get a little clock that bounces up and down. Dismissing something results in a swoosh message disappearing, or clearing your notification runs them through a little animated shredder. This functionality is wicked cool! And unfortunately was largely lost due to the tooling being just old, the PDC format being undocumented publicly, and there just not being enough motivation to revive it.

Now for Pinout specifically I initially thought I'd do an application like the cards demo application where users could flip through cable diagrams instead of implementing a menu. This addressed two things for me 1) menu logic and 2) I thought I could do the diagrams as PDC sequences so that the pins would pop up one after another on the screen.

Ultimately despite documenting the PDC binary format thoroughly and even developing a parser for existent PDC files and the Pebble color space, I ruled that this was wildly out of scope for the limited time I had to make the app, and after fixating on it for 4 days straight only to be faced with the fact that I had nothing to show for the hackathon I made a hard pivot back into C land. None of this was time wasted in my mind, this is still a viable 2.0 option for Pinout that would be wicked cool to implement! And I feel I have enough of a grasp on the PDC binary format to potentially make an SVG -> PDC conversion tool, and eventually that may lead to an animation sequencing tool! I don't expect it to be officially adopted by Rebble or the Pebble folks frankly, but I like building my own weird tools so I don't care about all that, I'm hear to learn.

And I think this snippet from cl-pdc really emphasizes what all of that learning was about. Being able to describe what a PDC binary comprises of is meaningful progress in being able to translate it to either a different format (png, svg) or stitch them together into sequenced animations!

* (pdc:desc (pdc:parse "../ref/Pebble_50x50_Heavy_snow.pdc"))
PDC Image (v1): 50x50 with 14 commands
  1. Path: [fill color:255; stroke color:192; stroke width:2] open [(9, 34) (9, 30) ]
  2. Path: [fill color:255; stroke color:192; stroke width:2] open [(7, 32) (11, 32) ]
  3. Path: [fill color:255; stroke color:192; stroke width:2] open [(26, 32) (30, 32) ]
  4. Path: [fill color:255; stroke color:192; stroke width:2] open [(28, 34) (28, 30) ]
  5. Path: [fill color:255; stroke color:192; stroke width:2] open [(26, 45) (30, 45) ]
  6. Path: [fill color:255; stroke color:192; stroke width:2] open [(28, 47) (28, 43) ]
  7. Path: [fill color:255; stroke color:192; stroke width:2] open [(17, 38) (21, 38) ]
  8. Path: [fill color:255; stroke color:192; stroke width:2] open [(19, 40) (19, 36) ]
  9. Path: [fill color:255; stroke color:192; stroke width:2] open [(7, 45) (11, 45) ]
  10. Path: [fill color:255; stroke color:192; stroke width:2] open [(9, 47) (9, 43) ]
  11. Path: [fill color:255; stroke color:192; stroke width:2] open [(35, 38) (39, 38) ]
  12. Path: [fill color:255; stroke color:192; stroke width:2] open [(37, 40) (37, 36) ]
  13. Path: [fill color:255; stroke color:192; stroke width:3] closed [(42, 25) (46, 21) (46, 16) (42, 12) (31, 12) (27, 8) (16, 8) (11, 13) (7, 13) (3, 17) (3, 21) (7, 25) ]
  14. Path: [fill color:0; stroke color:192; stroke width:2] open [(12, 14) (18, 14) (21, 17) ]

And it's even cooler to see the same PDC file consumed into a struct that we could in theory pass around to various transformation functions. So close!!

* (pdc:parse "../ref/Pebble_50x50_Heavy_snow.pdc")
#S(PDC::PDC-IMAGE
   :VERSION 1
   :WIDTH 50
   :HEIGHT 50
   :COMMANDS (#S(PDC::COMMAND
                 :TYPE 1
                 :STROKE-COLOR 192
                 :STROKE-WIDTH 2
                 :FILL-COLOR 255
                 :POINTS (#S(PDC::POINT :X 9 :Y 34) #S(PDC::POINT :X 9 :Y 30))
                 :OPEN-PATH T
                 :RADIUS NIL)
              #S(PDC::COMMAND
                 :TYPE 1
                 :STROKE-COLOR 192
                 :STROKE-WIDTH 2
                 :FILL-COLOR 255
                 :POINTS (#S(PDC::POINT :X 7 :Y 32) #S(PDC::POINT :X 11 :Y 32))
                 :OPEN-PATH T
                 :RADIUS NIL)
              #S(PDC::COMMAND
                 :TYPE 1
                 :STROKE-COLOR 192
                 :STROKE-WIDTH 2
                 :FILL-COLOR 255
                 :POINTS (#S(PDC::POINT :X 26 :Y 32)
                          #S(PDC::POINT :X 30 :Y 32))
                 :OPEN-PATH T
                 :RADIUS NIL)
              #S(PDC::COMMAND
                 :TYPE 1
                 :STROKE-COLOR 192
                 :STROKE-WIDTH 2
                 :FILL-COLOR 255
                 :POINTS (#S(PDC::POINT :X 28 :Y 34)
                          #S(PDC::POINT :X 28 :Y 30))
                 :OPEN-PATH T
                 :RADIUS NIL)
              #S(PDC::COMMAND
                 :TYPE 1
                 :STROKE-COLOR 192
                 :STROKE-WIDTH 2
                 :FILL-COLOR 255
                 :POINTS (#S(PDC::POINT :X 26 :Y 45)
                          #S(PDC::POINT :X 30 :Y 45))
                 :OPEN-PATH T
                 :RADIUS NIL)
              #S(PDC::COMMAND
                 :TYPE 1
                 :STROKE-COLOR 192
                 :STROKE-WIDTH 2
                 :FILL-COLOR 255
                 :POINTS (#S(PDC::POINT :X 28 :Y 47)
                          #S(PDC::POINT :X 28 :Y 43))
                 :OPEN-PATH T
                 :RADIUS NIL)
              #S(PDC::COMMAND
                 :TYPE 1
                 :STROKE-COLOR 192
                 :STROKE-WIDTH 2
                 :FILL-COLOR 255
                 :POINTS (#S(PDC::POINT :X 17 :Y 38)
                          #S(PDC::POINT :X 21 :Y 38))
                 :OPEN-PATH T
                 :RADIUS NIL)
              #S(PDC::COMMAND
                 :TYPE 1
                 :STROKE-COLOR 192
                 :STROKE-WIDTH 2
                 :FILL-COLOR 255
                 :POINTS (#S(PDC::POINT :X 19 :Y 40)
                          #S(PDC::POINT :X 19 :Y 36))
                 :OPEN-PATH T
                 :RADIUS NIL)
              #S(PDC::COMMAND
                 :TYPE 1
                 :STROKE-COLOR 192
                 :STROKE-WIDTH 2
                 :FILL-COLOR 255
                 :POINTS (#S(PDC::POINT :X 7 :Y 45) #S(PDC::POINT :X 11 :Y 45))
                 :OPEN-PATH T
                 :RADIUS NIL)
              #S(PDC::COMMAND
                 :TYPE 1
                 :STROKE-COLOR 192
                 :STROKE-WIDTH 2
                 :FILL-COLOR 255
                 :POINTS (#S(PDC::POINT :X 9 :Y 47) #S(PDC::POINT :X 9 :Y 43))
                 :OPEN-PATH T
                 :RADIUS NIL)
              #S(PDC::COMMAND
                 :TYPE 1
                 :STROKE-COLOR 192
                 :STROKE-WIDTH 2
                 :FILL-COLOR 255
                 :POINTS (#S(PDC::POINT :X 35 :Y 38)
                          #S(PDC::POINT :X 39 :Y 38))
                 :OPEN-PATH T
                 :RADIUS NIL)
              #S(PDC::COMMAND
                 :TYPE 1
                 :STROKE-COLOR 192
                 :STROKE-WIDTH 2
                 :FILL-COLOR 255
                 :POINTS (#S(PDC::POINT :X 37 :Y 40)
                          #S(PDC::POINT :X 37 :Y 36))
                 :OPEN-PATH T
                 :RADIUS NIL)
              #S(PDC::COMMAND
                 :TYPE 1
                 :STROKE-COLOR 192
                 :STROKE-WIDTH 3
                 :FILL-COLOR 255
                 :POINTS (#S(PDC::POINT :X 42 :Y 25) #S(PDC::POINT :X 46 :Y 21)
                          #S(PDC::POINT :X 46 :Y 16) #S(PDC::POINT :X 42 :Y 12)
                          #S(PDC::POINT :X 31 :Y 12) #S(PDC::POINT :X 27 :Y 8)
                          #S(PDC::POINT :X 16 :Y 8) #S(PDC::POINT :X 11 :Y 13)
                          #S(PDC::POINT :X 7 :Y 13) #S(PDC::POINT :X 3 :Y 17)
                          #S(PDC::POINT :X 3 :Y 21) #S(PDC::POINT :X 7 :Y 25))
                 :OPEN-PATH NIL
                 :RADIUS NIL)
              #S(PDC::COMMAND
                 :TYPE 1
                 :STROKE-COLOR 192
                 :STROKE-WIDTH 2
                 :FILL-COLOR 0
                 :POINTS (#S(PDC::POINT :X 12 :Y 14) #S(PDC::POINT :X 18 :Y 14)
                          #S(PDC::POINT :X 21 :Y 17))
                 :OPEN-PATH T
                 :RADIUS NIL)))

So that C code?

So now that we've demoed the thing I think I'm good at, lets look at what I think I'm not that good at. Pinout is an amalgamation of several example applications, and some code stolen from the other two watch faces I had previously published. That's a bit of a recurring theme for me, and probably most people. If I figure out a way to do something I re-implement it elsewhere because that just makes sense. Maybe these aren't the best ways to do any of this, but that's I think OK.

Pinout has three key components, a menu that allows you to select a diagram which then displays an image of the selected diagram, a battery widget, and a clock widget. Of the three the only one I had previously implemented was the clock widget.

Time Handling

This code was lifted straight from my emacs watch face. And it's really simplistic, I think it's in fact from one of the original watch face tutorials that Pebble provided. The only thing unique about it is that there's a check to ensure that a text layer (s_time_layer) exists before attempting to render to the screen. Since Pinout transitions between several different screens we need to make sure we don't attempt to render either the battery or time widget while transitioning.

//Update time handler
static void update_time() {
  time_t temp = time(NULL);
  struct tm *tick_time = localtime(&temp);

  static char s_buffer[8];
  // convert time to string, and update text
  strftime(s_buffer, sizeof(s_buffer), clock_is_24h_style() ?
	   "%H:%M" : "%I:%M", tick_time);

  // only updat the text if the layer exists, which won't happen until the menu item is selected
  if (s_time_layer) {
    text_layer_set_text(s_time_layer, s_buffer);
  }
}

//Latch tick to update function
static void tick_handler(struct tm *tick_time, TimeUnits units_changed) {
  update_time();
}

Inside of our image layer renderer we create a text layer in which we insert the current time. Note that we call update_time directly so that when the image is rendered we immediately have the current time rendered at the top of the application.

// Image window callbacks
static void image_window_load(Window *window) {
  Layer *window_layer = window_get_root_layer(window);
  GRect bounds = layer_get_bounds(window_layer);

  //Image handling code removed for brevity.

  //Allocate Time Layer
  s_time_layer = text_layer_create(GRect(110, 0, 30, 20));
  text_layer_set_text_color(s_time_layer, GColorBlack);
  text_layer_set_background_color(s_time_layer, GColorLightGray);
  layer_add_child(window_layer, text_layer_get_layer(s_time_layer));

  //Update handler
  update_time();
}

While the application is running we subscribe to the tick timer service, so that each time the time ticks up we get a call back to update the time in the application.

static void init(void) {
  //Subscribe to timer/battery tick
  tick_timer_service_subscribe(MINUTE_UNIT, tick_handler);

All of this works because once the application is launched its primary function is to initialize and then query those tick handlers then wait for user input.

int main(void) {
  init();
  app_event_loop();
  deinit();
}

Battery Handling

You're probably not surprised that the battery widget works almost exactly the same as the time widget. We define an update function that checks the current state, and only renders if our containing layer exists.

// Current battery level
static int s_battery_level;

// record battery level on state change
static void battery_callback(BatteryChargeState state) {
  s_battery_level = state.charge_percent;
  
  static char s_buffer[8];
  // convert battery state to string, and update text
  snprintf(s_buffer, sizeof(s_buffer), "%d%%", s_battery_level);

  // only update the text if the layer exists, which won't happen until the menu item is selected
  if (s_battery_layer) {
    text_layer_set_text(s_battery_layer, s_buffer);

    layer_mark_dirty(text_layer_get_layer(s_battery_layer));
  }
}

And we described another text layer inside of our image renderer with an update callback.

static void image_window_load(Window *window) {
  Layer *window_layer = window_get_root_layer(window);
  GRect bounds = layer_get_bounds(window_layer);

  //Image handling code removed for brevity.

  //Time handling code removed for brevity.

  //Allocate Battery Layer
  s_battery_layer = text_layer_create(GRect(5, 0, 30, 20));
  text_layer_set_text_color(s_battery_layer, GColorBlack);
  text_layer_set_background_color(s_battery_layer, GColorLightGray);
  layer_add_child(window_layer, text_layer_get_layer(s_battery_layer));

  //Update battery handler
  battery_callback(battery_state_service_peek());
}

And the subscribe to the tick service in init! Almost exactly the same!

static void init(void) {
  //Subscribe to timer/battery tick
  tick_timer_service_subscribe(MINUTE_UNIT, tick_handler);
  battery_state_service_subscribe(battery_callback);

  // Get initial battery state
  battery_callback(battery_state_service_peek());

It's really nice to see consistency like this. The Pebble C SDK is really well designed and documented with tons of examples from when Pebble was still in business. They really were something super unique, and it still shows today.

Menus & Image Rendering

Now image rendering and menu handling was new to me, but I was able to find this tutorial that helped immensely. Once I had my arms around the idea of how I thought the application might work it ended up being incredibly simple.

We define our menu as a simple enum and define the total length of the menu statically. Then we setup an array of IDs that are used to reference the PNG images for each diagram.

#define NUM_MENU_ITEMS 4

typedef enum {
  MENU_ITEM_RJ45A,
  MENU_ITEM_RJ45B,
  MENU_ITEM_RJ45A_CROSSOVER,
  MENU_ITEM_RJ45B_CROSSOVER
} MenuItemIndex;

// Images for each pinout
static GBitmap *s_pinout_images[NUM_MENU_ITEMS];
static uint32_t s_resource_ids[NUM_MENU_ITEMS] = {
  RESOURCE_ID_RJ45A,
  RESOURCE_ID_RJ45B,
  RESOURCE_ID_RJ45A_CROSSOVER,
  RESOURCE_ID_RJ45B_CROSSOVER
};

We keep track of where we are in the application by re-defining the currently displayed image upon selection. And then the menu rendering is as simple as iterating over the total length of of menu, and carving out a section of the screen for as many entries as will fit.

// Currently displayed image
static GBitmap *s_current_image;

// Menu callbacks
static uint16_t menu_get_num_sections_callback(MenuLayer *menu_layer, void *data) {
  return 1; // We're only using a single menu layer, but maybe down the line the image will be a sub menu to textual information about the diagram.
}

// Return the number of menu rows at point
static uint16_t menu_get_num_rows_callback(MenuLayer *menu_layer, uint16_t section_index, void *data) {
  return NUM_MENU_ITEMS;
}

// Get the height of the menu header from section in menu
static int16_t menu_get_header_height_callback(MenuLayer *menu_layer, uint16_t section_index, void *data) {
  return MENU_CELL_BASIC_HEADER_HEIGHT;
}

When we click on a menu item using the middle select button we trigger the image layer rendering function described in the last two sections, but this time we have a complete picture of what happens. We take the image correlated with the menu entry and render it to the screen as a bitmap layer, then overlay our text layers on top of the bitmap!

static void image_window_load(Window *window) {
  Layer *window_layer = window_get_root_layer(window);
  GRect bounds = layer_get_bounds(window_layer);
  
  // Create the bitmap layer for displaying the image
  s_image_layer = bitmap_layer_create(bounds);
  bitmap_layer_set_compositing_mode(s_image_layer, GCompOpAssign);
  bitmap_layer_set_bitmap(s_image_layer, s_current_image);
  bitmap_layer_set_alignment(s_image_layer, GAlignCenter);
  
  layer_add_child(window_layer, bitmap_layer_get_layer(s_image_layer));

  //Allocate Time Layer
  s_time_layer = text_layer_create(GRect(110, 0, 30, 20));
  text_layer_set_text_color(s_time_layer, GColorBlack);
  text_layer_set_background_color(s_time_layer, GColorLightGray);
  layer_add_child(window_layer, text_layer_get_layer(s_time_layer));

  //Allocate Battery Layer
  s_battery_layer = text_layer_create(GRect(5, 0, 30, 20));
  text_layer_set_text_color(s_battery_layer, GColorBlack);
  text_layer_set_background_color(s_battery_layer, GColorLightGray);
  layer_add_child(window_layer, text_layer_get_layer(s_battery_layer));

  //Update time handler
  update_time();
  battery_callback(battery_state_service_peek());
}

Incredibly simple right? This is the beauty of the pebble ecosystem, you can make a very nice and polished looking application incredibly simply.

The full source for Pinout can be found here and you can find it on the Rebble store. I sort of just glossed over the initialization functionality, it's pretty standard stuff. All just sequenced functions to allocate memory for our various layers, and then destroy them when done. Typically C memory allocation stuff in a nice wrapper the SDK provides.

So what does this look like in real life?

Yeah I knew you'd want to know that, I can't just build an app for a 10 year old smart watch and swear up and down I actively use it without proving that point. So just for you dear reader, here a promotional photo of my hairy wrist in the hot Florida sun showing off the excellent functionality of Pinout! These were taken while tipping cables for a 60Ghz point to point antenna installation, gotta make sure those cables are wired up correctly so I don't have to make a second trip out!

Pinout on a Pebble Time smartwatch, showing an RJ45B Pinout in the Florida sun!

If you happen to use Pinout I would love a picture of you using it and will gleefully add it above! Shoot me an email at durrendal (at) lambdacreate.com if you want to show your support!

Last but not least

It is incredibly important for me to express my sincere and utmost thanks and gratitude to Mio for helping with the graphic design work for Pinout. If you got this far you have a great sense of what Pinout would probably of looked like without their help. I bet it would have worked, but it would have been uglier than my C code.

Thanks again Mio, I couldn't have done it without you!!