Original URL: https://www.theregister.com/2013/07/18/how_to_write_apps_for_the_pebble_smartwatch/

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

By Tony Smith

Posted in OSes, 18th July 2013 09:04 GMT

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.

It all clicks

Let’s add some user interaction. Pebble OS now uses a system akin to its event handling mechanism, the better to help the coder give the user more ways to control the three-button watch. This lets you directly accommodate single clicks short and long, double-clicks, and press-and-hold events, rather than simply waiting for a push on a specific button and then trying to deal with the user’s intentions.

The Pebble SDK, then, defines a ClickConfigProvider entity which is essentially an array of function calls for specific buttons and the various ways each of them can be used. This list of calls is attached to the host window.

Pebble smartwatch

The Pebble’s three buttons for user interaction

First, we need to add the line

    window_set_click_config_provider(&window, (ClickConfigProvider)config_provider);

to the handle_init() initialisation function, and we need to run it after the app’s Window - reached using the pointer variable window - has been pushed onto the OS’s Window stack, or it will be ignored. The above line tells the window where to get its array of button configurations from, which it does by calling a second function, the config_provider passed in the first call. Here’s what it looks like in our app:

void config_provider(ClickConfig **config, Window *window)
{
    config[BUTTON_ID_UP]->click.handler = (ClickHandler)up_single_click_handler;
    config[BUTTON_ID_DOWN]->click.handler = (ClickHandler)down_single_click_handler;
}

We punch into the array of configurations handlers for the button-related events we’re interested in. Here, that’s a couple of single-click handlers, added to the click.handler, fields, but we could have added function calls for each button’s multi_click.handler, its long_click.handler and/or its long_click.release_handler fields. Other fields specify how many clicks in a group we’re interested in, or whether we’re only interested in the final click in a batch - it doesn’t matter how many times the user presses the button in rapid sequence, we just deal with the last one. The mechanism can handle hold-to-repeat actions too.

With your handlers registered this way, all you have to do now is write the handlers themselves. This app uses the top and bottom buttons to trigger a sudden change in the bouncing ball’s direction:

void up_single_click_handler(ClickRecognizerRef recognizer, Window *window)
{
    delta_x = delta_x * -1;
}

and

void down_single_click_handler(ClickRecognizerRef recognizer, Window *window)
{
    delta_y = delta_y * -1;
}

These handlers and, indeed, the config_provider function require declarations at the top of the file, or in a separate header file if you’re using one.

Updating the ball’s movement every second doesn’t make for a very dynamic display, of course. As I say, Pebble OS’ .tick_info handler doesn’t generate more than one event a second, so we’ll have to use its timer_handler instead. To do so, edit the pbl_main function, to remove the .tick_info section from the list of PebbleAppHandlers and replace it with .timer_handler = &handle_timer. The function should now look like this:

void pbl_main(void *params)
{
    AppContextRef ctxt = (AppContextRef) params;
    
    PebbleAppHandlers handlers =
    {
        .init_handler = &handle_init,
        .deinit_handler = &handle_deinit,
        .timer_handler = &handle_timer
    };
  
    app_event_loop(ctxt, &handlers);
}

You’ll notice that I’ve also added a second new handler, .deinit_handler, which is called when the app quits, and is here used to politely cancel any timers in the event queue that have yet to fire:

void handle_deinit(AppContextRef ctxt)
{
    app_timer_cancel_event(ctxt, timerHandle);
}

The value timerHandle is a global variable declared at the start of the program alongside Window window. It provides a reference to the AppTimerHandle timer in memory. We’ll set this up in the handle_init() with an extra line at the end of that function:

    timerHandle = app_timer_send_event(ctxt, 500, 1);

It takes the usual AppContextRef pointer to the app itself, a time in milliseconds before which the timer fires - half a second here - and a unique integer, called a “cookie” by the SDK, to identify the specific timer to the handler when you’re running more than one at once. The timer’s handle and its cookie value are passed to the timer handler function when it’s triggered, so you can check which timer has triggered the code.

Delete the handle_tick() function if you like, but copy it first to form the basis for your handle_timer() function:

void handle_timer(AppContextRef ctxt, AppTimerHandle handle, uint32_t cookie)
{
    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;
    }
    
    timerHandle = app_timer_send_event(ctxt, 100, 1);
    
    Layer *root = window_get_root_layer(&window);
    layer_mark_dirty(root);
}

One extra line goes in, to call the app_timer_send_event() function as per the line in handle_init(). As yet, Pebble OS timers don’t fire continuously, so we add a new one to the event queue each time the timer fires. I’ve set this time to 100ms.

Compile the app, transfer it to your watch, select it from the Pebble’s menu and you should see a ball moving at a moderate pace around the screen. You can change its direction with a press of the top or bottom buttons. Not very impressive, of course, but the code I've outlined here can form the basis for much more interesting and more useful apps.

Resources boom

The timers and buttons can be used to start and stop and count lap times in a stopwatch app, for instance. Or you might create simple games like Snake or Breakout. Two utilities, httpPebble on iOS and Pebble Connect on Android allow Pebble apps to route web requests through your phone, allowing you to create a variety of check-at-a-glance apps for the smartwatch. There’s a scope in the Pebble SDK and OS to create some interesting and useful apps.

As it stands, the app appears in the Pebble’s menu simply as a name, Ball, which is entered into the boilerplate PBL_APP_INFO created by the SDK’s create_pebble_project.py script. This also sets the app’s unique UUID, which you’ll see at the top of the file. You can also modify this to set the app’s version number and to add your name as author. But what’s really needed is a menu icon, and you can add one by editing the resource_map.json created for you in the /resources/src folder within the project folder.

The app in action

Menu icons need to be monochrome 28 x 28 .png files with no transparency, and it’s easy enough to make on in any graphics package. Actually, transparency is supported, but you need to tweak the image’s ‘type’ in its entry in the resource map, which is read by the compiler and used to add necessary resources into the app binary, from ‘png’ to ‘png-trans’. It’s easier to flatten the image and stick with ‘png’. Other pre-defined resource types are ‘font’ and ‘raw’, the latter a catch-all for every other data type you want to use. Adding the image to the app’s resource map is just a matter of adding some keywords into the media section of the boilerplate copy:

{
    "friendlyVersion": "VERSION",
    "versionDefName": "APP_RESOURCES",
    "media": 
    [
        {
           "defName": "IMAGE_MENU_ICON",
           "type": "png",
           "file": "icon.png"
        }
   ]
}

This assigns the image the resource name (defName) IMAGE_MENU_ICON. This label is pre-inserted into the app code in the PBL_APP_INFO, prefixed with RESOURCE_ID_, which the SDK adds to each resource’s defName to create its resource ID. You can give the image whatever defName you like, as long as you prefix it with RESOURCE_ID_ when you edit the PBL_APP_INFO code:

PBL_APP_INFO(MY_UUID,
             "Ball", 
             "Tony Smith",
             1, 
             0,
             RESOURCE_ID_IMAGE_MENU_ICON,
             APP_INFO_STANDARD_APP);

The APP_INFO_STANDARD_APP flag tells the Pebble it’s dealing with an app rather than a watch face, which is a special kind of app managed by the Pebble’s on-board watch application. For the latter, replace APP_INFO_STANDARD_APP with APP_INFO_WATCH_FACE.

Compile and transfer the app, and you’ll see your icon in the menu.

The app’s icon image is handled automatically by the OS. Other resources need to be initialised before they’re used, and this needs to take place within the handle_init() function you’ve written. You do this by adding the line

        resource_init_current_app(&APP_RESOURCES);

before any resources are used. The &APP_RESOURCE input comes from the .json file’s versionDefName field. Whatever versionDefName you enter into the .json file - the SDK simply drops in VERSION - make sure you use it in the resource_init_current_app() function call too.

Resources can then be loaded into RAM using the resource_load() function, which takes the resource’s handle (ResHandle) as an argument, along with a buffer in which to store the bits and the size of the resource in bytes. That handle is generated with the resource_get_handle() function to which you pass the resource’s resource ID - which is not the defName, remember, though it’s easy to convert from one to the other by adding RESOURCE_ID_ at the start.

If it’s a bitmap you’re loading, say to present icons that represent what the Pebble’s three main buttons do, the SDK provides the convenience function bmp_init_container(), which takes a resource ID and a pointer to a Bitmap Container (BmpContainer) structure into which the resource data will be stored. The BmpContainer can then be added to a new Layer which, in turn, can be added to the root Layer as one of its child Layers.

Graphics resources loaded this way can be used to replace the demo app’s simple circle with a picture of a ball, perhaps with some additional images to make it appear to be squashed against the edges of the screen just before it bounces. I dropped in vibes_short_pulse() calls so the Pebble momentarily vibrates every time the ball bounces. I’ve made some other tweaks to the code listed above too - they’re all in the complete listing below. ®

test_project.c

#include "pebble_os.h"
#include "pebble_app.h"
#include "pebble_fonts.h"


#define MY_UUID { 0x64, 0x8C, 0xE8, 0xC6, 0xBF, 0x52, 0x46, 0x5F, 0x95, 0x0E, 0x8F, 0xA2, 0xD8, 0x70, 0x5A, 0xB1 }

PBL_APP_INFO(MY_UUID,
             "Ball", "Black Pyramid Software",
             1, 0, /* App version */
             RESOURCE_ID_IMAGE_MENU_ICON,
             APP_INFO_STANDARD_APP);

#define time_duration 100


// Function Prototypes

void draw_layer(Layer *layer, GContext *gctxt);
void config_provider(ClickConfig **config, Window *winder);
void up_single_click_handler(ClickRecognizerRef recognizer, Window *winder);
void down_single_click_handler(ClickRecognizerRef recognizer, Window *winder);
void shift_single_click_handler(ClickRecognizerRef recognizer, Window *winder);


// Globals

Window window;
AppTimerHandle timerHandle;
int pos_x, pos_y, delta_x, delta_y, old_x, old_y;
bool initialWipeFlag;


// Special Functions

void draw_layer(Layer *layer, GContext *gctxt)
{
    if (initialWipeFlag)
    {
        // Erase screen on first run
        
        graphics_context_set_fill_color(gctxt, GColorBlack);
        GRect rect = GRect(0,0,144,168);
        graphics_fill_rect(gctxt, rect, 0, GCornerNone);
        initialWipeFlag = false;
    }
    
    // Wipe old circle
    
    GPoint point = GPoint(old_x, old_y);
    graphics_context_set_fill_color(gctxt, GColorBlack);
    graphics_fill_circle(gctxt, point, 8);
    
    // Draw new circle
    
    point = GPoint(pos_x, pos_y);
    graphics_context_set_fill_color(gctxt, GColorWhite);
    graphics_fill_circle(gctxt, point, 8);
}


// Handler functions

void handle_init(AppContextRef ctxt)
{
    initialWipeFlag = true;
    
    window_init(&window, "Ball");
    window_set_background_color(&window, GColorBlack);
    window_set_fullscreen(&window, true);
    window_stack_push(&window, false);
    
    Layer *root = window_get_root_layer(&window);
    layer_set_update_proc(root, draw_layer);
    
    layer_mark_dirty(root);
    
    // Set up button monitoring DO AFTER PUSHING WINDOW TO STACK
    
    window_set_click_config_provider(&window, (ClickConfigProvider)config_provider);
    
    srand(time(NULL));
    
    pos_x = 68 + rand() % 8;
    pos_y = 80 + rand() % 8;
    
    delta_x = 8;
    delta_y = 8;
    
    old_x = 0;
    old_y = 0;
    
    // Set up timer
    
    timerHandle = app_timer_send_event(ctxt, 500, 1);
}


void handle_deinit(AppContextRef ctxt)
{
    app_timer_cancel_event(ctxt, timerHandle);
}


// Click configuration functions

void config_provider(ClickConfig **config, Window *winder)
{
    config[BUTTON_ID_UP]->click.handler = (ClickHandler)up_single_click_handler;
    config[BUTTON_ID_DOWN]->click.handler = (ClickHandler)down_single_click_handler;
    config[BUTTON_ID_SELECT]->click.handler = (ClickHandler)shift_single_click_handler;
}


void up_single_click_handler(ClickRecognizerRef recognizer, Window *winder)
{
    delta_x = delta_x * -1;
}


void down_single_click_handler(ClickRecognizerRef recognizer, Window *winder)
{
    delta_y = delta_y * -1;
}


void shift_single_click_handler(ClickRecognizerRef recognizer, Window *winder)
{
    old_x = pos_x;
    old_y = pos_y;
    
    pos_x = rand() % 140;
    pos_y = rand() % 160;
    
    // Tell layer to redraw
    
    Layer *root = window_get_root_layer(&window);
    layer_mark_dirty(root);
}


// Event handlers

void handle_timer(AppContextRef ctxt, AppTimerHandle handle, uint32_t cookie)
{
    old_x = pos_x;
    old_y = pos_y;
    
    pos_x = pos_x + delta_x;
    pos_y = pos_y + delta_y;
    
    if (pos_x > 140)
    {
        pos_x = 132;
        delta_x = -8;
        vibes_short_pulse();
    }
    
    if (pos_x < 4)
    {
        pos_x = 12;
        delta_x = 8;
        vibes_short_pulse();
    }
    
    if (pos_y > 162)
    {
        pos_y = 154;
        delta_y = -8;
        vibes_short_pulse();
    }
    
    if (pos_y < 4)
    {
        pos_y = 12;
        delta_y = 8;
        vibes_short_pulse();
    }
    
    // Reset timer
    
    timerHandle = app_timer_send_event(ctxt, time_duration, 1);
    
    // Tell layer to redraw
    
    Layer *root = window_get_root_layer(&window);
    layer_mark_dirty(root);
}


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);
}


// Main app entry point

void pbl_main(void *params)
{
    AppContextRef ctxt = (AppContextRef)params;
    
    PebbleAppHandlers handlers =
    {
        .init_handler = &handle_init,
        .deinit_handler = &handle_deinit,
        .timer_handler = &handle_timer
    };
  
    app_event_loop(ctxt, &handlers);
}

resource_map.json

{
    "friendlyVersion": "VERSION",
    "versionDefName": "APP_RESOURCES",
    "media": 
    [
        {
           "defName": "IMAGE_MENU_ICON",
           "type": "png",
           "file": "icon.png"
        }
   ]
}

You can download the source files here, and the .pbw file can be downloaded here.