After a year long wait my Pebble smart watch finally arrived. A lot of good reviews have been written about it by other people, and I don’t really want to repeat that. I like that I can change watch faces and I really like some of the watch faces that are bundled with the app.
However, as a software developer, I really had to create one myself. So I did and the result is ‘Planetarium’. The source files are available at Github.

So what does Planetarium do? The time is displayed in the form of three planets that rotate around a central sun. The closest planet to the sun shows the seconds, the outer planet shows the hour and the planet in between shows the minutes. In real planetary systems the inner planets have shorter periods than outer planets.
If you want to try the watch face yourself, you can download it for your Pebble at mypebblefaces.com.
After installing the SDK tools from the instruction at http://developer.getpebble.com, developing a watch face is actually pretty easy. There are several examples and demo projects available to help you with your watch face.
So let’s look at the code of Planetarium. The watch face is coded in one C source file, called planets.c. First we need to identify the app, with an identifier, a name, creator and version.
1 2 3 4 5 |
PBL_APP_INFO(MY_UUID, "Planetarium", "Dev Discoveries", 1, 0, /* App version */ RESOURCE_ID_IMAGE_MENU_ICON, APP_INFO_WATCH_FACE); |
Then we have the init method, where the watch face application is started.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
void pbl_main(void *params) { AppContextRef ctx = (AppContextRef) params; // Register the delegate methods. PebbleAppHandlers handlers = { .init_handler = &handle_init, .deinit_handler = &handle_deinit, .tick_info = { .tick_handler = &handle_second_tick, .tick_units = SECOND_UNIT } }; app_event_loop(ctx, &handlers); } |
In this method, the app registers itself for the initialisation, destroy and timing events (every second in this case).
Let’s look at the handle_init method . Here we initialise all parts, that are needed for the watch face.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
void handle_init(AppContextRef ctx) { (void)ctx; init = false; // Initialize the window. window_init(&window, "Planets Window"); window_stack_push(&window, true /* Animated */); // Load the resources. resource_init_current_app(&APP_RESOURCES); // Set up a layer for the static watch face background bmp_init_container(RESOURCE_ID_IMAGE_BACKGROUND, &background_image_container); layer_add_child(&window.layer, &background_image_container.layer.layer); // Set up a layer for the hour hand rotbmp_init_container(RESOURCE_ID_IMAGE_HOUR_HAND, &hour_hand_image_container); GSize hourSize = layer_get_frame(&hour_hand_image_container.layer.layer).size; hourOffset = GPoint(hourSize.w / 2, hourSize.h / 2); layer_add_child(&window.layer, &hour_hand_image_container.layer.layer); // Set up a layer for the minute hand rotbmp_init_container(RESOURCE_ID_IMAGE_MINUTE_HAND, &minute_hand_image_container); GSize minSize = layer_get_frame(&minute_hand_image_container.layer.layer).size; minOffset = GPoint(minSize.w / 2, minSize.h / 2); layer_add_child(&window.layer, &minute_hand_image_container.layer.layer); // Set up a layer for the second hand rotbmp_init_container(RESOURCE_ID_IMAGE_SECOND_HAND, &second_hand_image_container); GSize secSize = layer_get_frame(&second_hand_image_container.layer.layer).size; secOffset = GPoint(secSize.w / 2, secSize.h / 2); layer_add_child(&window.layer, &second_hand_image_container.layer.layer); // Update the positions of the planets. update_hand_positions(); init = true; } |
Here we setup the window and all layers needed to draw the watch face. The planets are drawn with rotating bmp containers, so that we can rotate the planet images when they circle around the sun. The central sun is part of the background image and does not need to be drawn separately. At the end, we update the clock hand positions, to position the planets correctly with respect to the sun.
All these resources (images) are compiled by the sdk and bundled in the .pbw file. The information needed by the app to load the images is listed in a JSON file called resource_map.json.
The destroy code is simple, we unload all the loaded resources.
1 2 3 4 5 6 7 |
void handle_deinit(AppContextRef ctx) { (void)ctx; bmp_deinit_container(&background_image_container); rotbmp_deinit_container(&second_hand_image_container); rotbmp_deinit_container(&minute_hand_image_container); rotbmp_deinit_container(&hour_hand_image_container); } |
When the app is notified at a second time event, the positions of the planets need to be updated:
1 2 3 4 |
void handle_second_tick(AppContextRef ctx, PebbleTickEvent *t) { (void)t; update_hand_positions(); } |
In the update_hand_positions method, we calculate the angles with which the planets are rotated with respect to twelve o’ clock.
We only update the minute and hour occasionally. This is done to minimize the part of the window that needs to be redrawn. This in turn should improve battery life.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
void update_hand_positions() { //Get the time parts from Pebble OS. PblTm t; get_time(&t); int sec = t.tm_sec; int min = t.tm_min; int hour = t.tm_hour; // Set the seconds position. int32_t aSec = TRIG_MAX_ANGLE * (sec * 6) /360; set_angled_position(&second_hand_image_container, aSec, ySec, secOffset); second_hand_image_container.layer.rotation = aSec; layer_mark_dirty(&second_hand_image_container.layer.layer); // Set the minutes position. if (!init || sec % 10 == 0) { int32_t aMin = TRIG_MAX_ANGLE * ((min * 6) + sec / 10) /360; set_angled_position(&minute_hand_image_container, aMin, yMin, minOffset); minute_hand_image_container.layer.rotation = aMin; layer_mark_dirty(&minute_hand_image_container.layer.layer); } // Set the hours position. if (!init || min % 5 == 0) { int32_t aHour = TRIG_MAX_ANGLE * (((hour % 12) * 30 + min / 2)) / 360; set_angled_position(&hour_hand_image_container, aHour, yHour, hourOffset); hour_hand_image_container.layer.rotation = aHour; layer_mark_dirty(&hour_hand_image_container.layer.layer); } } |
There are two utility methods to place the image on the screen and to calculate the positions based on the angle with respect to twelve o’ clock.
The Pebble coordinate system starts with (0,0) in the upper left corner, with the y-axis pointing downward. But when doing the trigonometry, it is easier to have (0,0) in the center of the screen. That is why coordinates are transformed in the set_init_coords method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
void set_init_coords(RotBmpContainer *image, GPoint initCoords) { GRect r = layer_get_frame(&image->layer.layer); // Translate from (0,0) r.origin.x = initCoords.x + 144 / 2; r.origin.y = -initCoords.y + 168 / 2; layer_set_frame(&image->layer.layer, r); } void set_angled_position(RotBmpContainer *image, int32_t angle, int32_t length, GPoint offset) { // Calculate (x,y) from angle. GPoint r = GPoint((length * sin_lookup(angle) / TRIG_MAX_RATIO) - offset.x, (length * cos_lookup(angle) / TRIG_MAX_RATIO) + offset.y); set_init_coords(image, r); } |
This was basically all the code needed to run the watch face.
Update:
A bug in the source code example has been corrected with the seconds planet.