How to program the Pebble smartwatch: Part 3

Update Pebble has released version 2 of its OS and this invalidates much of what follows, which was written for an earlier version of the OS.

As it stands, the app I created in Part 2 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.

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’.

My app's icon

My app’s icon

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.

Pebble

The app in the Pebble’s 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 already 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.

An edited version of this article originally appeared in The Register