This article is more than 1 year old

It's all in the wrist: How to write apps for the Pebble smartwatch

Code and example video - the (bouncing on-screen) ball's in your court

Feature Pebble didn’t invent the smartwatch, but it has done more than most to bring the product category to the attention of World+Dog, largely thanks to its hugely successful and well-reported Kickstarter funding campaign.

Not only is Pebble’s smartwatch - also called Pebble - the only product of its kind, but it remains one of the few that go beyond duplicating a host phone’s notifications and messages on its own screen.

Pebble will do that, but much more interesting is the SDK Pebble provides to allow C programmers to create clever new watch faces and, better still, native apps to run on the smartwatch’s 144 x 168 black-and-white screen.

Writing apps for the Pebble requires some sort of flavour of Unix: either a Mac running OS X 10.7 or 10.8, or an Ubuntu 12.04 box, officially. Pebble’s own developer pages have a good, by-the-numbers guide to installing the Pebble SDK and the ARM compiler tools it requires, so I won’t be going over that again here. It will also tell you how to create a new project, configure it and, when you’ve written some code, build a binary.

Pebble

The complete app, imaginatively called ‘Ball’, in the Pebble’s menu

What Pebble doesn’t yet provide is a clear step-by-step introduction to programming its smartwatch. Its documentation is a work in progress, but for the moment would-be Pebble app writers have a set of example watch faces and apps to examine alongside the API reference.

In addition, Pebble’s “Developer Guides” provide information that explores the concepts behind the way apps are structured and how they operate, but again they don’t amount to a beginner’s tutorial. So that’s what I hope to provide here. It’s a simple graphical toy that presents a ball bouncing around the screen.

Pebble apps are organised into code and resources, all bundled into a .pbw file which then has to be transferred to the watch. The resources can include extra fonts, screen and menu bitmap graphics, and data files. They are read by the compiler as it parses the JSON-format list you produce while you’re writing your app. The SDK provides a template.

As you might expect from a resource-limited system that aggressively attempts to eke out battery life, Pebble apps are event-driven: an event - a tick of the on-board clock, the user presses a button or the firing of a timer, say - takes place and the app in the foreground responds to it as necessary. In that sense, a Pebble app is simply a set of reactive event handlers which are provided to the OS at run time.

All Pebble apps contain a pbl_main() function. This is where execution kicks off and it’s where you tie your event handlers into the Pebble event loop. Here’s the one in my test app. It’s not formatted here as a professional coder might key it in, but to help make the structure more clear to beginners.

void pbl_main(void *params)
{
    AppContextRef ctxt = (AppContextRef)params;
    
    PebbleAppHandlers handlers =
    {
        .init_handler = &handle_init,
        .tick_info =
        {
            .tick_handler = &handle_tick,
            .tick_units = SECOND_UNIT
        }
    };
  
    app_event_loop(ctxt, &handlers);
}

The first line of the function gives you, in ctxt, the operating system's reference to your app in memory. Next, you create and populate a defined data structure that holds the addresses of your event functions. Some, like the app initialisation handler - stored in the .init_handler field - don’t require extra parameters, but others, such as .tick_info do: here, not only do you provide the address of the function that handles clock-tick events, but also a constant that indicates the frequency of the ticks you’re interested in. I’ve used SECOND_UNIT but the SDK also provides minute, hour, day, month and year equivalents. Millisecond timings are available through Pebble OS’s timers, and I’ll look at these later.

Finally, the app uses the app_event_loop() function to provide the watch's operating system a pointer to this app’s handlers by way of the data structure you’ve just created.

The next part of the app is the initialisation handler, and it’s called by the OS once pbl_main() has done its stuff. The Pebble app template provides a pointer to the app’s main on-screen Window as a global variable, window. We use the address stored here to access the Window data in memory to initialise it and give it a name, then to set its background colour, and finally to push it onto the OS’s stack of Windows.

The true parameter in window_stack_push() tells the OS you want it to animate the appearance of the window by sliding it in from the right; pass false if you want it to appear immediately. All apps need to initialise at least one window.

Here’s the code:

void handle_init(AppContextRef ctxt)
{
    window_init(&window, "Ball");
    window_set_background_color(&window, GColorBlack);
    window_stack_push(&window, false);
    
    Layer *root = window_get_root_layer(&window);
    layer_set_update_proc(root, draw_layer);
    
    srand(time(NULL));
    
    pos_x = 68 + rand() % 8;
    pos_y = 80 + rand() % 8;
    
    delta_x = 8;
    delta_y = 8;
}

In Pebble OS, a Layer is a data structure describing any given on-screen element. There can be multiple Layers overlaid on the app’s Window and each maintains a bitmap canvas for drawing onto. Layers are stacked in a parent-child hierarchy. The great-granddaddy of them all is the root Layer, which we access by getting its address from the app’s Window using the window_get_root_layer() function.

The next line calls the layer_set_update_proc() function to provide the root Layer with the address of the function it needs to call when it is told it needs to update its graphics.

Finally, the code initialises the standard random number generator’s seed value, then used it to set the position of the ball somewhere around the centre of the Pebble’s 144 x 168 display. I’ve stored the ball’s current position and its speed in each direction in global integer variables.

Before looking at the remaining event handlers, here is the code for the draw_layer() function which I passed to the root Layer for it to call when it needs to be redrawn:

void draw_layer(Layer *layer, GContext *gctxt)
{
    graphics_context_set_fill_color(gctxt, GColorBlack);
    GRect rect = GRect(0,0,144,168);
    graphics_fill_rect(gctxt, rect, 0, GCornerNone);
    
    GPoint point = GPoint(pos_x, pos_y);
    graphics_context_set_fill_color(gctxt, GColorWhite);
    graphics_fill_circle(gctxt, point, 8);
}

The function has to follow a pattern set by the SDK: it has to be set to receive the address of the Layer it will be drawing and the address of its Graphics Context (GContext), the entity in which the Pebble OS embeds the bitmap it actually draws to.

The first two lines erase the screen by setting the Graphics Context’s fill colour to black and then we define a rectangle (GRect) that’s the same size as the screen. Next, we tell the Graphics Context to fill that rectangle. The final two parameters in graphics_fill_rect() specify the radius of the rectangle's rounded corners. This app doesn’t need them, so the radius is set to zero and the bitmap mask used to render the corners is set to the constant GCornerNone.

The next section creates a Point variable based on the current x and y co-ordinates of the ball, sets the ink colour to white, and then draws and fills a circle of radius eight pixels around that point.

You need to tell the compiler about draw_layer() as it’s not a standard Pebble OS function - you must ‘declare its prototype’, in the jargon. To do so, add this line up toward the top of the file under the PBL_APP_INFO(...) section the SDK puts in for you:

   void draw_layer(Layer *layer, GContext *gctxt);

A Layer calls its update function - draw_layer() here - when it's marked as needing updating by the OS or the app. This happens here in the tick event handler, which, as I specified earlier, is called by the OS once every second:

void handle_tick(AppContextRef ctxt, PebbleTickEvent *event)
{
    pos_x = pos_x + delta_x;
    pos_y = pos_y + delta_y;
    
    if (pos_x > 140)
    {
        pos_x = 132;
        delta_x = -8;
    }
    
    if (pos_x < 4)
    {
        pos_x = 12;
        delta_x = 8;
    }
    
    if (pos_y > 162)
    {
        pos_y = 154;
        delta_y = -8;
    }
    
    if (pos_y < 4)
    {
        pos_y = 12;
        delta_y = 8;
    }
    
    Layer *root = window_get_root_layer(&window);
    layer_mark_dirty(root);
}

Again, Pebble OS expects tick handlers to receive key data: a pointer to the app itself and a pointer to the event record. I don’t need them here, though. The function is simple: the ball’s new position is calculated, and then checked to see if that takes it past the edges of the screen. If it does, the code re-positions the ball away from the edge.

However, the ball has moved, so we get the address of the root Layer from the app’s Window and mark it as ’dirty’ to tell it its content needs to be redrawn. This triggers the draw_layer() function.

Compile the app as Pebble’s documentation describes. I usually duplicate the resulting .pbw file into my Dropbox folder. The app above compiles to under 6KB, so it’s quick to upload, and then it can easily be accessed through your phone’s own Dropbox app and transferred to the Pebble smartphone app, which will immediately copy it to your watch.

A useful trick, to help with debugging your app code, is to make use of the function call vibes_short_pulse(). It triggers a short buzz of the Pebble’s vibrator, giving you non-screen feedback that a section of code has run correctly.

Next page: It all clicks

More about

TIP US OFF

Send us news


Other stories you might like