Building Custom Apps for Pebble

iOS R&D engineer

Smart watches these days remind me of the ankle monitors that judges make people under house arrest wear.
Like it or hate it, the era of wearable gear is coming!
Fred_Flinstone_watch
Fred Flinstone seems happy, the rest of us is quite excited too. The subject itself isn’t something supper innovative: starting from Seiko’s Pulsar Watch that could store 24 digits of information, introduced in the early 70’s, we keep hearing about the reincarnation of the idea here and there. Last year, Pebble kickstarter project raised over 10 million US dollars! And this is what it looks like:
ELEKS_Labs_Pebble_2
Pebble comes in two models: plastic 150$ and steel 229$ a piece. It is supplied with a 1.26-inch e-paper display, a lithium-ion polymer battery, an ARM contex-M3 processor and weights 38g. As for sensors, there is an accelerometer, a compass and an ambient light sensor. And the best part is it’s waterproof. While looking a bit geeky, it provides some really cool features: you can read notifications, answer/decline phone calls, read text messages, control music, read emails and receive notifications (it vibrates).

Pebble is not a standalone gear, it requires a smartphone to reveal its true potential. The smartwatch communicates with your iOS or Android device via bluetooth and with the help of an app installed on your phone allows browsing Pebble appstore and install up to 8 smart watch apps on it. There are a few more things worth mentioning: it doesn’t have a camera, a mic or a GPS (though GPS data can be received via bluetooth from phone).

Now let’s get our hands dirty with some Pebble development!

To start coding for Pebble, you need to install Pebble SDK by simply typing the following into the terminal:

curl -sSL https://developer.getpebble.com/install.sh | sh && source ~/.bash_profile

or by using CloudPebble. The command for creating a standard “Hello World!” app would be

pebble new-project hello_world

It creates a placeholder project that simply displays the “Hello World!” message on the screen of the watch.

Build pebble build and install the following on your watch:

pebble install --phone IP_ADDRESS_OF_YOUR_PHONE

Before installation, make sure that your phone and your computer are in the same wireless network and Developer mode in the phone settings are enabled.
ELEKS_Labs_Pebble_3
ELEKS_Labs_Pebble_4

Grab your phone’s IP from Pebble app
ELEKS_Labs_Pebble_5
You can’t simply get into the app and see this info – your phone has to be paired with watch first. And this part drove me crazy. I’ve installed the Pebble app on my iPhone 5 running iOS 6, set up everything, enabled the Developer mode, got the IP, installed the first watchapp. So far so good. Made some changes in the watchapp code, opened the Pebble phone app again to pair and boom! Pebble phone app crashes. After 3 hours of install/uninstall, I finally got it working to install the watchapp one more time, but that wasn’t the end of story. I ended up just borrowing my friend’s iPhone running iOS 7. Pebble app crashes there too, but far less frequently.

Anyways, the default Hello World app is a really good starting point for development. Oh, btw, you’ll need to get to know C in order to do so. Here’s how the app folder structure looks like:
ELEKS_Labs_Pebble_6

appinfo.json contains JSON payload with app information

{
  "uuid": "723eb2d2-b64c-48a9-a12e-cbd0ba62df9f",
  "shortName": "hello_world",
  "longName": "hello_world",
  "companyName": "MakeAwesomeHappen",
  "versionCode": 1,
  "versionLabel": "1.0.0",
  "watchapp": {
    "watchface": false
  },
  "appKeys": {
    "dummy": 0
  },
  "resources": {
    "media": []
  }
}

To add an image in the watch, you need to add it to the Resources folder and define it in the “media” JSON array

"resources": {
    "media": [
        {
          "type": "png",
          "name": "IMAGE_NO_LITTER",
          "file": "images/no_litter_crop.png"
        }
      ]
  }

The following table describes valid fields for resource definition

Property Required Type Description
name yes string Resource name. This will be prefixed with RESOURCE_ID_ in your C code.
type yes string Valid types of resources include: raw, png, png-trans and font.
file yes string The path to the resource.
menuIcon no boolean True if this resource should be used as the system menu icon for this application. See below for more information.

A special point of interest is the “uuid” JSON field which should be used in your phone app to establish communication with the watch as follows:

// Configure our communications channel to target the stopwatch app:
// See pebble-stopwatch-master/appinfo.json in the native watch app SDK for the same definition on the watch's end:
            uint8_t bytes[] = {0x72, 0x3e, 0xb2, 0xd2, 0xb6, 0x4c, 0x48, 0xa9, 0xa1, 0x2e, 0xcb, 0xd0, 0xba, 0x62, 0xdf, 0x9f};
            NSData *uuid = [NSData dataWithBytes:bytes length:sizeof(bytes)];
            [[PBPebbleCentral defaultCentral] setAppUUID:uuid];

The main entry point in an application is the main function

int main(void) {
  init();

  APP_LOG(APP_LOG_LEVEL_DEBUG, "Done initializing, pushed window: %p", window);

  app_event_loop();
  deinit();
}

On init we set up everything and clean up on deinit, main work is done in app_even_loop(); To handle user button click (Pebble is not a touch-screen device), we need to set up a click config provider:

window_set_click_config_provider(window, click_config_provider);

static void click_config_provider(void *context) {
  window_single_click_subscribe(BUTTON_ID_SELECT, select_click_handler);
  window_single_click_subscribe(BUTTON_ID_UP, up_click_handler);
  window_single_click_subscribe(BUTTON_ID_DOWN, down_click_handler);
}

And an example handler:

static void up_click_handler(ClickRecognizerRef recognizer, void *context) {
  text_layer_set_text(text_layer, "Up");
}

To add some text, we need to add a text layer to our window layer

Layer *window_layer = window_get_root_layer(window);
  GRect bounds = layer_get_bounds(window_layer);

  text_layer = text_layer_create((GRect) { .origin = { 0, 72 }, .size = { bounds.size.w, 20 } });
  text_layer_set_text(text_layer, "Press a button");
  text_layer_set_text_alignment(text_layer, GTextAlignmentCenter);
  layer_add_child(window_layer, text_layer_get_layer(text_layer));

With graphic context, we can easily draw primitives. Line drawing code would look like this:

void draw_line(Layer *me, GContext* ctx) {
    graphics_context_set_stroke_color(ctx, GColorWhite);
    graphics_draw_line(ctx, GPoint(0, 0), GPoint(140, 0));
    graphics_draw_line(ctx, GPoint(0, 1), GPoint(140, 1));
}

Layers can also be animated

PropertyAnimation
*animation = property_animation_create_layer_frame(layer, NULL, target);
		animation_set_duration((Animation*)*animation, 250);
		animation_set_curve((Animation*)*animation, AnimationCurveLinear);
		animation_set_handlers((Animation*)*animation, (AnimationHandlers){
			.stopped = (AnimationStoppedHandler)animation_stopped
		}, NULL);

To push a new window, simply call

window_stack_push(window, true);

The console output for a build command will look somewhat similar to this:

Users-Mac-mini:pebble-stopwatch-master user$ pebble build
Setting top to : /Users/user/pebble-stopwatch-master
Setting out to : /Users/user/pebble-stopwatch-master/build
Checking for program gcc,cc : arm-none-eabi-gcc
Checking for program ar : arm-none-eabi-ar
Found Pebble SDK in : /Users/user/pebble-dev/PebbleSDK-2.0.2/Pebble
‘configure’ finished successfully (0.517s)
Waf: Entering directory `/Users/user/pebble-stopwatch-master/build’

[ 9/21] app_resources.pbpack.manifest: build/app_resources.pbpack.data ../pebble-dev/PebbleSDK-2.0.2/Pebble/tools/pbpack_meta_data.py -> build/app_resources.pbpack.manifest
[10/21] resource_ids.auto.h: build/resources/fonts/DejaVuSans.ttf.FONT_DEJAVU_SANS_SUBSET_18.pfo build/resources/fonts/DejaVuSans.ttf.FONT_DEJAVU_SANS_SUBSET_22.pfo build/resources/fonts/DejaVuSans-Bold.ttf.FONT_DEJAVU_SANS_BOLD_SUBSET_30.pfo build/resources/images/stopwatch.png.pbi build/resources/images/buttons.png.pbi ../pebble-dev/PebbleSDK-2.0.2/Pebble/tools/generate_resource_code.py build/app_resources.pbpack.data -> build/src/resource_ids.auto.h
[11/21] app_resources.pbpack: build/app_resources.pbpack.manifest build/app_resources.pbpack.table build/app_resources.pbpack.data -> build/app_resources.pbpack
[14/21] c: src/spritz.c -> build/src/spritz.c.12.o
[16/21] c: build/appinfo.auto.c -> build/appinfo.auto.c.12.o
../src/spritz.c: In function ‘init_spritz_window’:
../src/spritz.c:60:2: error: expected ‘;’ before ‘spritzLayer’
../src/spritz.c: At top level:
../src/spritz.c:39:13: warning: ‘spritz_text’ defined but not used [-Wunused-variable]
Waf: Leaving directory `/Users/user/pebble-stopwatch-master/build’
Build failed
-> task in ‘pebble-app.elf’ failed (exit status 1):
{task 4492086736: c spritz.c -> spritz.c.12.o}
[‘arm-none-eabi-gcc’, ‘-std=c99’, ‘-mcpu=cortex-m3’, ‘-mthumb’, ‘-ffunction-sections’, ‘-fdata-sections’, ‘-g’, ‘-Os’, ‘-Wall’, ‘-Wextra’, ‘-Werror’, ‘-Wno-unused-parameter’, ‘-Wno-error=unused-function’, ‘-Wno-error=unused-variable’, ‘-fPIE’, ‘-I/Users/user/pebble-stopwatch-master/pebble-dev/PebbleSDK-2.0.2/Pebble/include’, ‘-I/Users/user/pebble-dev/PebbleSDK-2.0.2/Pebble/include’, ‘-I/Users/user/pebble-stopwatch-master/build’, ‘-I/Users/user/pebble-stopwatch-master’, ‘-I/Users/user/pebble-stopwatch-master/build/src’, ‘-I/Users/user/pebble-stopwatch-master/src’, ‘-DRELEASE’, ‘-D__FILE_NAME__=”spritz.c”‘, ‘../src/spritz.c’, ‘-c’, ‘-o’, ‘src/spritz.c.12.o’]

[ERROR ] A compilation error occurred

As we can see, the build failed due to a compilation error, the red output isn’t the thing we need over here, the actual reason for failure lies in the actual compile outputs, here

[14/21] c: src/spritz.c -> build/src/spritz.c.12.o
[16/21] c: build/appinfo.auto.c -> build/appinfo.auto.c.12.o
../src/spritz.c: In function ‘init_spritz_window’:
../src/spritz.c:60:2: error: expected ‘;’ before ‘spritzLayer’
../src/spritz.c: At top level:
../src/spritz.c:39:13: warning: ‘spritz_text’ defined but not used [-Wunused-variable]
Waf: Leaving directory `/Users/user/pebble-stopwatch-master/build’

“error expected ‘;’” tells exactly what went wrong: we just missed a semicolon before line 60

../src/spritz.c:60:2: error: expected ‘;’ before ‘spritzLayer’

and that’s it: 'build' finished successfully (1.344s)

On successful build, the console also shows memory usage

Memory usage:
Total app footprint in RAM: 10069 bytes / ~24kb
Free RAM available (heap): 14507 bytes

The debugging process is painful, since there is no debugger. The best you can do is use Pebble command line tool to grab logs and to cover you C code with logs:

APP_LOG(APP_LOG_LEVEL_DEBUG, "App Message Sync Error: %d", app_message_error);

Pebble SDK provides several ways of communication between a watch and a phone: AppSync, DataLogging and AppMessage. AppSync is a UI Synchronization layer for AppMessage and it makes life easier for working with messages. DataLogging is one-way communication from the watch to the smartphone app. It’s useful when you have to transfer larger sets of data (like accelerometer values). Also, starting from Pebble 2.0 SDK, you can store some data in the watch’s persistent storage, which is nice. Let’s have a look at simple AppSync setup for transferring location data.

On the watch’s side:

static AppSync sync;
static uint8_t sync_buffer[32];

Callbacks

static void sync_error_callback(DictionaryResult dict_error, AppMessageResult app_message_error, void *context) {
    APP_LOG(APP_LOG_LEVEL_DEBUG, "App Message Sync Error: %d", app_message_error);
}

static void sync_tuple_changed_callback(const uint32_t key, const Tuple* new_tuple, const Tuple* old_tuple, void* context) {
    switch (key) {
        case 0:
            // App Sync keeps new_tuple in sync_buffer, so we may use it directly
            text_layer_set_text(location_layer, new_tuple->value->cstring);
            break;
    }
}

and on the init handler:

Tuplet initial_values[] = {
        TupletCString(0, "No Location"),
    };
    app_sync_init(&sync, sync_buffer, sizeof(sync_buffer), initial_values, ARRAY_LENGTH(initial_values),
                  sync_tuple_changed_callback, sync_error_callback, NULL);

on the iPhone side:

// Send data to watch:
// See demos/feature_app_messages/weather.c in the native watch app SDK for the same definitions on the watch's end:
NSNumber *locationKey = @(0); // This is our custom-defined key for the icon ID, which is of type uint8_t.
NSDictionary *update = @{ locationKey:address };

[self.targetWatch appMessagesPushUpdate:update onSent:^(PBWatch *watch, NSDictionary *update, NSError *error)
 {
     NSString *message = error ? [error localizedDescription] : @"Update sent!";
     
     NSLog(@"%@", update);
     
     dispatch_async(dispatch_get_main_queue(), ^{
         [[[UIAlertView alloc] initWithTitle:nil message:message delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
     });
 }];

Simple, right?

Pebble is a great piece of wearable gear. I found call notifications and incoming messages reading features extremely useful. And battery is totally a winner. But it does look a bit in 80s style, its e-paper display and hardware buttons feel unusual in today’s era of touchscreens, and lack of microphone strongly limits the functionality. Nevertheless, guys from Pebble showed to commercial monsters like Apple that people would like to use smart watches and are ready to pay for them. Looking forward to hearing about iWatch at this summer’s WWDC.

tags

Comments