Pebble SDK 2.0 Tutorial #6: AppMessage for PebbleKit JS

Required Reading

Pebble SDK 2.0 Tutorial #1: Your First Watchapp

Pebble SDK 2.0 Tutorial #2: Telling the Time

Pebble SDK 2.0 Tutorial #3: Images and Fonts

Pebble SDK 2.0 Tutorial #4: Animations and Timers

Pebble SDK 2.0 Tutorial #5: Buttons and Vibrations

A basic working knowledge of JavaScript is recommended, but it shouldn’t be too hard to understand the language syntax as a beginner from the sample code provided at the end, especially coming from any Java or C related background (such as us!).

Introduction

Creating a simple Pebble watch app or watch face is well and fine, but adding an Internet connection to that app to fetch data/communicate with other services adds almost limitless potential. An example of this is my Pebble Tube Status app that fetches information on the status of the London Underground network for line info at a glance. For this tutorial section we will be getting our data from another source: The Openweathermap.org APIs, a free to use and simple example of data a watch face can display from the web.

Now, this is a long one, so make sure you have a good cup of tea or some other soothing beverage near you before you embark!

Basic Watch Face Setup

The first step this time is to create a new CloudPebble project and make sure it is set up in ‘Settings’ as a watchface, not a watch app. Next, copy in the C code below to start a bare-bones app:

#include <pebble.h>

Window* window;

void window_load(Window *window)
{

}

void window_unload(Window *window)
{

}

void init()
{
  window = window_create();
  WindowHandlers handlers = {
    .load = window_load,
    .unload = window_unload
  };
  window_set_window_handlers(window, handlers);

  window_stack_push(window, true);
}

void deinit()
{
  window_destroy(window);
}

int main(void)
{
  init();
  app_event_loop();
  deinit();
}

This is the ‘blank canvas’ on which we will build this weather info app. The next steps are to prepare the watch app to display the data we get from the weather feed. Let’s do this with four TextLayers. These will be for the ‘Openweathermap.org’ title/attribution, the location, the temperature and the time the data was fetched. As you can see from the API page linked previously, there are a lot more fields of data to display, but these will keep the tutorial simple and concise. So, here are our four global TextLayer declarations:

TextLayer *title_layer, *location_layer, *temperature_layer, *time_layer;

This time around we will take a measure to avoid the lengthy process of initialising these TextLayers by using a custom utility function to save space. As I was taught in my first year of University, functions are best used to reduce repetitive code, so this is an ideal use case. Below is a function that will set up a TextLayer to specification provided in the arguments. Place it above window_load() in the very least, as that is where it will be used:

static TextLayer* init_text_layer(GRect location, GColor colour, GColor background, const char *res_id, GTextAlignment alignment)
{
  TextLayer *layer = text_layer_create(location);
  text_layer_set_text_color(layer, colour);
  text_layer_set_background_color(layer, background);
  text_layer_set_font(layer, fonts_get_system_font(res_id));
  text_layer_set_text_alignment(layer, alignment);

  return layer;
}

Thus we can set up the title TextLayer like so in an abbreviated fashion:

void window_load(Window *window)
{
  title_layer = init_text_layer(GRect(5, 0, 144, 30), GColorBlack, GColorClear, "RESOURCE_ID_GOTHIC_18", GTextAlignmentLeft);
  text_layer_set_text(title_layer, "Openweathermap.org");
  layer_add_child(window_get_root_layer(window), text_layer_get_layer(title_layer));
}

Take a moment to match the arguments given in the function call to its declaration and see that by using this function we can save an extra five lines per TextLayer initialisation! The rest of the other layers are set up in a similar fashion:

location_layer = init_text_layer(GRect(5, 30, 144, 30), GColorBlack, GColorClear, "RESOURCE_ID_GOTHIC_18", GTextAlignmentLeft);
text_layer_set_text(location_layer, "Location: N/A");
layer_add_child(window_get_root_layer(window), text_layer_get_layer(location_layer));

temperature_layer = init_text_layer(GRect(5, 60, 144, 30), GColorBlack, GColorClear, "RESOURCE_ID_GOTHIC_18", GTextAlignmentLeft);
text_layer_set_text(temperature_layer, "Temperature: N/A");
layer_add_child(window_get_root_layer(window), text_layer_get_layer(temperature_layer));

time_layer = init_text_layer(GRect(5, 90, 144, 30), GColorBlack, GColorClear, "RESOURCE_ID_GOTHIC_18", GTextAlignmentLeft);
text_layer_set_text(time_layer, "Last updated: N/A");
layer_add_child(window_get_root_layer(window), text_layer_get_layer(time_layer));

We mustn’t forget to destroy these in the appropriate place, as always!

void window_unload(Window *window)
{
  text_layer_destroy(title_layer);
  text_layer_destroy(location_layer);
  text_layer_destroy(temperature_layer);
  text_layer_destroy(time_layer);
}

The watch app (once compiled) should look like this:

pebble-screenshot_2014-02-02_13-16-29

Setting up AppMessage
Before we fetch the data from the Internet, we will need to set up the watch app to receive AppMessage messages from the Pebble phone app. Remember that with PebbleKit JS, the JavaScript code runs on the phone, and the results are sent via AppMessage to the watch for display. A basic overview of how that messaging system works can be seen in the “AppMessage system overview” section in the SDK 1.X Tutorial Section on the subject, but the methodology has changed with SDK 2.0. With that in mind, let’s add some basic AppMessage framework:

Step 1: Declaring keys. Keys are ‘labels’ used to tell each side of the system what the data value means. For example, a key called ‘temperature’ could have it’s associated value treated as a temperature value. The names of keys and how they are interpreted are entirely up to the programmer, as you will soon see. The list of keys we will use are shown in the declaration below:

enum {
  KEY_LOCATION = 0,
  KEY_TEMPERATURE = 1,
};

Step 2: Create a callback for receiving data from the phone. There are other callbacks for failed events, but we won’t worry about them here:

static void in_received_handler(DictionaryIterator *iter, void *context)
{

}

Step 3: Setting up AppMessage itself. This is done in init(), but before window_stack_push():

//Register AppMessage events
app_message_register_inbox_received(in_received_handler);
app_message_open(app_message_inbox_size_maximum(), app_message_outbox_size_maximum());    //Largest possible input and output buffer sizes

Step 4: Set up how we will process the received Tuples. After multiple AppMessage implementations, I’ve found the most reliable method is to read the first item, then repeat reading until no more are returned, using a switch based process_tuple() function to separate out the process. Here’s how that is best done:

static void in_received_handler(DictionaryIterator *iter, void *context) 
{
	(void) context;
	
	//Get data
	Tuple *t = dict_read_first(iter);
	while(t != NULL)
	{
		process_tuple(t);
		
		//Get next
		t = dict_read_next(iter);
	}
}

Next, declare some character buffers to store the last displayed data. The function text_layer_set_text() requires that the storage for the string it displays (when not a literal) is long-lived, so let’s declare it globally:

char location_buffer[64], temperature_buffer[32], time_buffer[32];

Thus, we must define what process_tuple() will do. This is the important part, as here is where the incoming Tuples will be dissected and acted upon. The key and value of each Tuple is read and the key used in a switch statement to decide what to do with the accompanying data:

void process_tuple(Tuple *t)
{
  //Get key
  int key = t->key;

  //Get integer value, if present
  int value = t->value->int32;

  //Get string value, if present
  char string_value[32];
  strcpy(string_value, t->value->cstring);

  //Decide what to do
  switch(key) {
    case KEY_LOCATION:
      //Location received
      snprintf(location_buffer, sizeof("Location: couldbereallylongname"), "Location: %s", string_value);
      text_layer_set_text(location_layer, (char*) &location_buffer);
      break;
    case KEY_TEMPERATURE:
      //Temperature received
      snprintf(temperature_buffer, sizeof("Temperature: XX \u00B0C"), "Temperature: %d \u00B0C", value);
      text_layer_set_text(temperature_layer, (char*) &temperature_buffer);
      break;
  }

  //Set time this update came in
  time_t temp = time(NULL);
  struct tm *tm = localtime(&temp);
  strftime(time_buffer, sizeof("Last updated: XX:XX"), "Last updated: %H:%M", tm);
  text_layer_set_text(time_layer, (char*) &time_buffer);
}

That concludes the Pebble side of the system for now.

PebbleKit JS Setup
The Pebble phone app runs JavaScript code that actually fetches the data using the phone’s data connection, and then sends the results as AppMessage dictionaries to the watch for interpretation and display (as already mentioned). To start, on the left side of the CloudPebble screen, choose ‘JS’, and begin the file with this code segment to listen for when the Pebble app is opened:

Pebble.addEventListener("ready",
  function(e) {
    //App is ready to receive JS messages
  }
);

The next step is to declare the same set of keys to the JavaScript side as to the C side. To do this, go to Settings, and scroll down to ‘PebbleKit JS Message Keys’, and enter the same keys as defined in the C code , like so:


KEY_LOCATION 0
KEY_TEMPERATURE 1

Then hit ‘Save changes’.

We’ve already initialised the JavaScript file to respond when the watch app is opened, with the ‘ready’ event. Now we will modify it to request the weather information and parse the result. The code below will do that, and follows a process similar to that laid out in the Pebble weather app example. First, create a method that will connect to an URL and return the response with a XMLHttpRequest object. Here is an example method:

function HTTPGET(url) {
	var req = new XMLHttpRequest();
	req.open("GET", url, false);
	req.send(null);
	return req.responseText;
}

Next, invoke this method with the correct URL for the location you want from the Openweathermap.org API. Once this is done, we will obtain the response as plain text. It will need to be parsed as a JSON object so we can read the individual data items. After this, we construct a dictionary of the information we’re interested in using our pre-defined keys and send this to the watch. This whole process is shown below in a method called getWeather(), called in the ‘ready’ event callback:

var getWeather = function() {
	//Get weather info
	var response = HTTPGET("http://api.openweathermap.org/data/2.5/weather?q=London,uk");

	//Convert to JSON
	var json = JSON.parse(response);

	//Extract the data
	var temperature = Math.round(json.main.temp - 273.15);
	var location = json.name;

	//Console output to check all is working.
	console.log("It is " + temperature + " degrees in " + location + " today!");

	//Construct a key-value dictionary
	var dict = {"KEY_LOCATION" : location, "KEY_TEMPERATURE": temperature};

	//Send data to watch for display
	Pebble.sendAppMessage(dict);
};

Pebble.addEventListener("ready",
  function(e) {
    //App is ready to receive JS messages
	getWeather();
  }
);

After completing all this, the project is almost complete. After compiling and installing, you should get something similar to this:

pebble-screenshot_2014-02-02_18-05-17

Final Steps

So we have our web-enabled watch app working as it should. If this were a watch face, we’d want it to update itself every so often for as long as it is open. Seeing as this is a demo app, this isn’t too critical, but let’s do it anyway as a learning experience. It only requires a few more lines of C and JS.

Return to your C file and subscribe to the tick timer service for minutely updates in init(), like so:

//Register to receive minutely updates
tick_timer_service_subscribe(MINUTE_UNIT, tick_callback);

Add the corresponding de-init procedure:

tick_timer_service_unsubscribe();

And finally the add callback named in the ‘subscribe’ call (as always, above where it is registered!):

void tick_callback(struct tm *tick_time, TimeUnits units_changed)
{

}

We’re going to use this tick handler to request new updates on the weather from the phone. The next step is to create a function to use AppMessage to send something back to the phone. Below is just such a function, accepting a key and a value (be sure to add this function above the tick callback!):

void send_int(uint8_t key, uint8_t cmd)
{
	DictionaryIterator *iter;
 	app_message_outbox_begin(&iter);

 	Tuplet value = TupletInteger(key, cmd);
 	dict_write_tuplet(iter, &value);

 	app_message_outbox_send();
}

Every five minutes (it can be any interval) we will request new information. Seeing as this is the only time the watch app will ever communicate back this way, it doesn’t matter which key or value we use. It is merely a ‘hey!’ sort of message. If you wanted to distinguish between the messages sent back to the phone, you’d use the exact same method of defining keys as we did for location and temperature values. So, we change the tick handler to look a little more like this:

void tick_callback(struct tm *tick_time, TimeUnits units_changed)
{
	//Every five minutes
	if(tick_time->tm_min % 5 == 0)
	{
		//Send an arbitrary message, the response will be handled by in_received_handler()
		send_int(5, 5);
	}
}

The final piece of the puzzle is to set up the JavaScript file to respond in turn to these requests from the watch. We do that by registering to receive the ‘appmessage’ events, like so:

Pebble.addEventListener("appmessage",
  function(e) {
    //Watch wants new data!
	getWeather();
  }
);

And there we have it! Every five minutes the watch will ask for updated data, and receive this new information after the phone querys openweathermap.org.

Conclusions
That was a rather long journey, but it’s an important one for stretching the usefulness of your Pebble beyond telling the time and date! It also introduces a lot of new concepts at once, which may confuse some. If you have a query, post it here and I’ll do my best to answer it!

The full project source code that results from this Tutorial section can be found on GitHub here.

Thanks for reading, and keep an eye out for more soon!

32 comments
    • bonsitm said:

      Hi ‘Ant’, I have not used any simulator but the code should work fine on real hardware! Are you having issues?

      • bonsitm said:

        Hi Ant, can you detail these issues?

  1. Hi,
    Actually I don’t have a pebble at the moment, I am planning to use a simulator. Aparently, in CloudPebble it works as well.

  2. bonsitm, have you any idea about how to work with the accelerometer to make an application to detect the number of applauses that a user can give?

    • bonsitm said:

      Hi again. I’ve not looked into using accelerometer data in bespoke manners, but my first guess would be experimenting to try and see whether each clap registers as a vertical tap?

  3. Hi, why If I try to compile the project I get this message: The Pebble project directory is using an outdated version of the SDK! ?

    • bonsitm said:

      I’ve not seen this before, but my first guess would be that your SDK is outdated.

      • Gibran said:

        La explicacion esta increible pero tengo el mismo problem: The Pebble project directory is using an outdated version of the SDK! yo iso la version PebbleSDK 2.0.1. Gracias por tus aportes.

      • bonsitm said:

        Hi Gibran,

        Are you using CloudPebble.net?

    • bonsitm said:

      Hi, this is the correct repository for this tutorial section. I just tested it, and it compiles and runs perfectly.

  4. Valentin said:

    Hi ! I tried using KEY sending to my pebble but it seams that it does’nt recognize it… When I send for exemple : Pebble.sendAppMessage({“1”:10}); it works but if I try like : Pebble.sendAppMessage({“KEY_TEMPERATURE”:10}); it doesn’t work…
    Thank you for your help

    • bonsitm said:

      Hi Valentin, good question. The “KEY_TEMPERATURE” is a JS string that is converted into the value it represents because it is defined as an App Key (Either in appinfo.json or Settings on CloudPebble). Similarly, if you use “1”, this is a string representation of the number 1, and not declared as an App Key, hence it doesn’t work in the same way. Hope this helps.

  5. Hi Chris

    Thanks again for these tutorials. I do feel I’m making headway into my understanding of Pebble programming.

    A few notes on this Tutorial Part 6:

    * At top in the basic watch face setup, you stated to use watch face and not watch app, but in the rest of this tutorial you referred to it as a watch app. Thinking it would work either way, I stuck with watch face, but once compiled, no openweathermap.org data was shown, it was still at N/A, even though I saw the data in the compilation logs. Changing it to watch app showed the required data.

    * For those not familiar to URL keys (?), if you want your own city’s weather data, and your city name is more than one word, e.g. Kuala Belait, use a + to replace those spaces. So instead of the example’s “..?q=London,uk” I used “..?q=Kuala+Belait,BN”. Of course, please first use OpenWeatherMap.org online “weather in your city” find to check that it has your city’s data.

    * When registering the JSON keys in CloudPebble.net settings page, it wants the ‘0’ key first before the ‘1’ key.

    * I don’t know if the JavaScript file’s name can be any name one chooses, but Chris’s JavaScript’s filename is ‘pebble-js-app.js’

    These were the hiccups I encountered and I hope my comment here can help anyone else encountering them.

    Cheers Chris.

    Sryn

    • bonsitm said:

      Hi Sryn,

      Some very insightful comments there, thanks! I’ll address them in order:

      1. It’s part semantics, but the main difference between a watchapp and watchface is that a watchface is restricted in the elements you can use. You are correct there.

      2. A good tip for constructing the URL!

      3. Having the keys declared in numerical order is the most logical way.

      4. The PebbleKit JS file name must be ‘pebble-js-app.js’ in order for the app to use it. In the native SDK this must also be located in src/js/.

  6. Michael said:

    Hi Chris,

    first I need to thank you for your great tutorials that gave me a very good introduction in developing applications for my new toy (Pebble) 😉

    What I’ve seen when trying to use your code from this tutorial is that the call of init_text_layer seems to be wrong.

    I.e. you write:
    title_layer = init_text_layer(GRect(5, 0, 144, 30), GColorBlack, GColorClear, “RESOURCE_ID_GOTHIC_18”, GTextAlignmentLeft);

    but I assume it should be:
    title_layer = init_text_layer(GRect(5, 0, 144, 30), GColorBlack, GColorClear, RESOURCE_ID_GOTHIC_18, GTextAlignmentLeft);

    Keep up the good work!

    Best regards,
    Michael

    PS: If possible, could you please provide a tutorial for geolocation? This is something I’d like to use but don’t know how to start with it.

    • bonsitm said:

      Hi Michael, thanks for the notice. Glad to help! You are correct, that is most likely a formatting error, which I will correct when I can. Chris

  7. TJ said:

    I modified this tutorial to grab weather from a weather underground weather station. How would I get it to display a non rounded number? From what I have found snprintf cannot display decimals? I tried printf and it displays properly in console but doesn’t display at all on the watch. I’m new to all this so the tutorials have been awesome for a beginner but there are some things I’m not sure about. Thanks for any help!

    • bonsitm said:

      As far as I know it isn’t supported. In previous applications I’ve written my own float-to-string function through successive divisions by ten and taking modulus of the results to get each digit. Hope this helps!

    • bonsitm said:

      Depends how often the source data is updated and how often the app requests it. What source does Glance use?

  8. Jacob said:

    Hi,

    I’m trying to get my roomtemperature on my Pebble. I’ve adjusted the code from the GitHub, but
    1) I dont know if I have adjusted enough
    2) Am I using the right JSON for the temperature?

    Besides the right URL I have adjusted these codelines:
    //Extract the data
    var temperature = Math.round(json.heatlinks.rte);

    This is my source, RTE being the temperature. The Heatlink is the thermostat. I can only get all the data, not just the Heatlink info.

    {“status”: “ok”, “version”: “3.11”, “request”: {“route”: “/get-sensors” }, “response”: {“preset”:0,”time”:”2015-06-19 22:07″,”switches”:[{“id”:0,”name”:”Schuur licht”,”type”:”switch”,”status”:”off”,”favorite”:”no”},{“id”:1,”name”:”Woonkamer”,”type”:”switch”,”status”:”on”,”favorite”:”no”},{“id”:2,”name”:”Schuur buiten A”,”type”:”switch”,”status”:”on”,”favorite”:”no”},{“id”:3,”name”:”Gang schakelaar”,”type”:”switch”,”status”:”off”,”favorite”:”no”},{“id”:4,”name”:”Schuur buiten V”,”type”:”switch”,”status”:”on”,”favorite”:”no”},{“id”:5,”name”:”Slaap Koos”,”type”:”switch”,”status”:”off”,”favorite”:”no”},{“id”:6,”name”:”Slaap Daan”,”type”:”switch”,”status”:”off”,”favorite”:”no”},{“id”:7,”name”:”Slaap alles”,”type”:”switch”,”status”:”off”,”favorite”:”no”},{“id”:8,”name”:”Gang lamp”,”type”:”switch”,”status”:”off”,”favorite”:”no”}],”uvmeters”:[],”windmeters”:[],”rainmeters”:[],”thermometers”:[{“id”:0,”name”:”Thermometer”,”code”:”10992262″,”model”:1,”lowBattery”:”no”,”version”:2.31,”te”:14.2,”hu”:68,”te+”:17.2,”te+t”:”13:56″,”te-“:12.8,”te-t”:”04:21″,”hu+”:83,”hu+t”:”05:32″,”hu-“:59,”hu-t”:”13:57″,”outside”:”yes”,”favorite”:”no”}],”weatherdisplays”:[], “energymeters”: [], “energylinks”: [], “heatlinks”: [{“id”: 0, “favorite”: “no”, “name”: “HeatLink”, “code”: “237027”, “pump”: “off”, “heating”: “off”, “dhw”: “off”, “rte”: 20.136, “rsp”: 14.000, “tte”: 14.000, “ttm”: null, “wp”: 0.000, “wte”: 46.000, “ofc”: 0, “odc”: 0, “presets”: [{ “id”: 0, “te”: 18.00},{ “id”: 1, “te”: 14.00},{ “id”: 2, “te”: 19.00},{ “id”: 3, “te”: 14.00}]}], “hues”: [], “scenes”: [{“id”: 0, “name”: “Woonkamer”, “favorite”: “no”},{“id”: 1, “name”: “Tuin”, “favorite”: “no”},{“id”: 2, “name”: “Slaapkamer”, “favorite”: “no”}], “kakusensors”:

    I hope you can help me…

    Jacob

    • bonsitm said:

      The JSON sample you provided is incomplete, and hard to read. If it is downloaded correctly, then the standard member access (something akin to json.heatlinks.rte) will get you the value. Make sure you are accessing the JSON correctly though.

  9. Nick said:

    Hi, I followed your tutorial thoroughly but I’m still getting “N/A” for the location, temperature and last updated.
    Just to be sure it wasn’t a problem related to my typing of your code, I copied and pasted your entire source code into my C and javascript files. It still gives me “N/A”.

    I had a look in the App Logs and it says:
    “[PHONE] pebble-app.js:?: TypeError: Cannot read property ‘temp’ of undefined
    at getWeather (pebble-js-app.js:16:40)
    at Pebble. (pebble-js-app.js:32:2)
    [PHONE] pebble-app.js:?: TypeError: Cannot read property ‘temp’ of undefined
    at getWeather (pebble-js-app.js:16:40)
    at Pebble. (pebble-js-app.js:39:2)”

    Would you have any idea how to fix it?

    Thanks in advance.

    • bonsitm said:

      Hi Nick. This tutorial is deprecated! Check developer.pebble.com for more up to date information. Anyway, OWM now requires an API key in the request, which may be invalidating the response you’re getting here.

Leave a comment