Pebble SDK 2.0 Tutorial #9: App Configuration

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

Pebble SDK 2.0 Tutorial #6: AppMessage for PebbleKit JS

Pebble SDK 2.0 Tutorial #7: MenuLayers

Pebble SDK 2.0 Tutorial #8: Android App Integration

Introduction

In this section of the tutorial series we will create a basic app that can be configured from the Pebble app. Lots of watchfaces and watchapps use this functionality to let the user tweak various aspects to their own liking. Watchfaces I’ve created before SDK 2.0 get around this by having each tweak in a separate watchface package, which lead to having five or six of the same watchface.

I’ve not yet gotten around to adding configuration to any of my watchfaces (although I plan to in the future) due to the fact that the configuration pages loaded from the Pebble app are not included in the watchapp package itself but are loaded from a remote website, and I have no web hosting to speak of. However, I have since discovered (although I’m sure I’m not the first) that such a page can be hosted on Dropbox. It must be in the Public folder, otherwise it is offered as a download and not as a webpage to view.

Let’s get started!

Watchapp Setup
The watchapp we will be creating will have a single option to keep things simple – the option to invert the colours. To begin with, create a new project and use the following code as a starting point:

#include <pebble.h>

static Window *window;
static TextLayer *text_layer;

static void window_load(Window *window) 
{
  //Create TextLayer
  text_layer = text_layer_create(GRect(0, 0, 144, 168));
  text_layer_set_font(text_layer, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD));
  text_layer_set_text_color(text_layer, GColorBlack);
  text_layer_set_background_color(text_layer, GColorWhite);
  text_layer_set_text(text_layer, "Not inverted!");

  layer_add_child(window_get_root_layer(window), text_layer_get_layer(text_layer));
}

static void window_unload(Window *window) 
{
  text_layer_destroy(text_layer);
}

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

  window_stack_push(window, true);
}

static void deinit(void) 
{
  window_destroy(window);
}

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

This should be familiar: a basic app that has a single TextLayer stating that the app is not inverted. The process I’ve adopted to setup app configuration has the following steps:

1. Setup AppMessage to enable messages containing option data to be sent from PebbleKit JS.
2. Setup the app to be configurable in appinfo.json, or the Settings page in CloudPebble.
3. Setup PebbleKit JS code to load the configuration page and send the result to the watch.
4. Write the HTML page that presents an interface to the user to allow them to choose their options.

Setting Up AppMessage
We will start by declaring the key we will be using to receive the option to invert the watchapp. Don’t forget to declare this in Settings on CloudPebble or in appinfo.json if you are working with the native SDK:

#define KEY_INVERT 0

Next, we create the AppMessageInboxReceived handler that will process any received messages. If they contain our key, we will compare the payload value cstring to set the colours of the app to be inverted or not, depending on the value received. We then use the Persistent Storage API to save the result for the next time the watchapp is opened. This should be placed above init() as it will be called there in a moment:

static void in_recv_handler(DictionaryIterator *iterator, void *context)
{
  //Get Tuple
  Tuple *t = dict_read_first(iterator);
  if(t)
  {
    switch(t->key)
    {
    case KEY_INVERT:
      //It's the KEY_INVERT key
      if(strcmp(t->value->cstring, "on") == 0)
      {
        //Set and save as inverted
        text_layer_set_text_color(text_layer, GColorWhite);
        text_layer_set_background_color(text_layer, GColorBlack);
        text_layer_set_text(text_layer, "Inverted!");

        persist_write_bool(KEY_INVERT, true);
      }
      else if(strcmp(t->value->cstring, "off") == 0)
      {
        //Set and save as not inverted
        text_layer_set_text_color(text_layer, GColorBlack);
        text_layer_set_background_color(text_layer, GColorWhite);
        text_layer_set_text(text_layer, "Not inverted!");

        persist_write_bool(KEY_INVERT, false);
      }
      break;
    }
  }
}

The final step is to actually open AppMessage to enable communication with the phone. Do this in init():

app_message_register_inbox_received((AppMessageInboxReceived) in_recv_handler);
app_message_open(app_message_inbox_size_maximum(), app_message_outbox_size_maximum());

Note we used the app_message_inbox_size_maximum() and app_message_outbox_size_maximum() functions to get the maximum buffer sizes available. While not strictly required here, it is a good best practice. I’ve wasted a lot of time in past projects not realising the buffer sizes I’d chosen were too small!

The final step is to set up the app to load the last stored configuration when the app is restarted, and takes for form of a similar if, else as the AppMessageInboxReceived handler above. Again, we use the Persistent Storage API to get our last saved configuration value. The window_load()function becomes thus:

static void window_load(Window *window) 
{
  //Check for saved option
  bool inverted = persist_read_bool(KEY_INVERT);

  //Create TextLayer
  text_layer = text_layer_create(GRect(0, 0, 144, 168));
  text_layer_set_font(text_layer, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD));

  //Option-specific setup
  if(inverted == true)
  {
    text_layer_set_text_color(text_layer, GColorWhite);
    text_layer_set_background_color(text_layer, GColorBlack);
    text_layer_set_text(text_layer, "Inverted!");
  }
  else
  {
    text_layer_set_text_color(text_layer, GColorBlack);
    text_layer_set_background_color(text_layer, GColorWhite);
    text_layer_set_text(text_layer, "Not inverted!");
  }

  layer_add_child(window_get_root_layer(window), text_layer_get_layer(text_layer));
}

Now the C code is complete!

PebbleKit JS Setup
The PebbleKit JS component of the app is the part responsible for loading the configuration page and sends the results of the user interaction to the watch to be processed as we just set up. This is done through the “showConfiguration” and “webviewclosed” events. Here is our initial JS code. Add a new JS file in CloudPebble or to the src/js/pebble-js-app.js if coding natively:

Pebble.addEventListener("ready",
  function(e) {
    console.log("PebbleKit JS ready!");
  }
);

So far, so simple. Next, we add an event listener for the “showConfiguration” event, triggered when a user chooses the Settings button in the Pebble app, like that shown below:

Screenshot_2014-05-24-15-04-23

The job of this event listener is to call Pebble.openURL(), a requirement of the system. This is when the configuration page is loaded (we will design this later). As stated in the introduction a good place to store this file is in your Public Dropbox folder. This way it is shown as a webpage and not as a download. Use mine for the moment, but if you want to make any changes you will need to change this to point to your own file:

Pebble.addEventListener("showConfiguration",
  function(e) {
    //Load the remote config page
    Pebble.openURL("https://dl.dropboxusercontent.com/u/10824180/pebble%20config%20pages/sdktut9-config.html");
  }
);

When the user has chosen their options and closed the page, the “webviewclosed” event is fired. We will register another event listener to handle this. The data returned will be encoded in the URL as a JSON dictionary containing one element: “invert” which will have a value of either “on” or “off” depending on what the user chose. This is then assembled into an AppMessage and sent to the watch, which then sets and saves as appropriate:

Pebble.addEventListener("webviewclosed",
  function(e) {
    //Get JSON dictionary
    var configuration = JSON.parse(decodeURIComponent(e.response));
    console.log("Configuration window returned: " + JSON.stringify(configuration));

    //Send to Pebble, persist there
    Pebble.sendAppMessage(
      {"KEY_INVERT": configuration.invert},
      function(e) {
        console.log("Sending settings data...");
      },
      function(e) {
        console.log("Settings feedback failed!");
      }
    );
  }
);

That concludes the PebbleKit JS setup. Now for the last part – HTML!

Configuration HTML Page Setup
The final piece of the puzzle is the part the user will actually see and takes the form of a HTML page consisting of form elements such as checkboxes, selectors and buttons. We will just use one selector and one button to let the user choose if they want the watchapp to be inverted or not. Here’s the layout code:

<!DOCTYPE html>
<html>
  <head>
    <title>SDKTut9 Configuration</title>
  </head>
  <body>
    <h1>Pebble Config Tutorial</h1>
    <p>Choose watchapp settings</p>

    <p>Invert watchapp:
    <select id="invert_select">
      <option value="off">Off</option>
      <option value="on">On</option>
    </select>
    </p>

    <p>
    <button id="save_button">Save</button>
    </p>
  </body>
</html>

With this done we add a script to add a click listener to the button and a function to assemble the JSON option dictionary. This dictionary is then encoded into the URL and handed to the PebbleKit JS code to be sent to the watch in the “webviewclosed” event. Insert this into the HTML page:

<script>
  //Setup to allow easy adding more options later
  function saveOptions() {
    var invertSelect = document.getElementById("invert_select");

    var options = {
      "invert": invertSelect.options[invertSelect.selectedIndex].value
    }
    
    return options;
  };

  var submitButton = document.getElementById("save_button");
  submitButton.addEventListener("click", 
    function() {
      console.log("Submit");

      var options = saveOptions();
      var location = "pebblejs://close#" + encodeURIComponent(JSON.stringify(options));
      
      document.location = location;
    }, 
  false);
</script>

That completes the page that will get the user’s option choices and also the app itself! Compile the app and install on your watch. By choosing either ‘On’ or ‘Off’ on the configuration page you should be able to toggle the colour used in the watchapp. This should look like that shown below:

invert-notinvert

Conclusion

So, that’s the process I’ve adopted to set up app configuration. You can expand it by adding more AppMessage keys and more elements in the HTML page. Make sure to add the fields to the JSON object constructed in saveOptions() though.

As usual, the full code is available on GitHub.

32 comments
  1. ClementG said:

    Hello Chris, thanks a lot for these tutorials. Love them !
    I am having trouble setting up animations in my own code, maybe you can help me… Let me know if you are available and I’ll give you the details.
    Thank you, best regards

    ClementG

    • bonsitm said:

      Hi ClementG, thanks for the kind words. What is the problem? Send me a link to the problem section of code or email me and we can discuss more fully!

      • ClementG said:

        Thanks for the quick answer but I actually find it out by myself. No need to bother you anymore 😉
        Keep it up with these tutos, they are really helpful and teaching.

        ClementG

      • bonsitm said:

        Great to hear! More to come 🙂

  2. minils said:

    I want to save and transmit larger texts (more than 256 bytes which is the PERSIST_DATA_MAX_LENGTH). How can I save sth on the phone and load it to the watch when needed?
    Thanks 😉

  3. Matthias said:

    Thanks for the tutorials! Especially this one because I’ve been trying to figure out how to make configuration with pebble possible (newby at javascript :p), but i’ve got one question.. is it possible to send a dictionary from your pebble to your html so you can check/uncheck the boxes correctly? Because if you, for example, check a checkbox saying you want your watchface to show seconds and you go into configuration a second time, the ‘show seconds’ checkbox is unchecked again.. I would like to resolve this problem, so if you have time you’re help would be much appreciated !
    Matthias

    • bonsitm said:

      Hi Matthias, thanks for the kind words!

      I think you will be able to send data to the HTML page by appending it in encoded JSON form in the URL used for Pebble.openURL(). Similar in fashion to how data is passed back the other way. Don’t forget you can have an inline processing this all in JS as in the .js file itself!

      Hope this helps.

      • Matthias said:

        Thanks for the quick reply! I figured you could use the URL to transfer data, but do you just paste it behind the url?

      • bonsitm said:

        If you have any special characters (like space becomes %20), then you must use encodeURLComponent() to get your string to append.

  4. josefos said:

    First of all thanks for your valuable tutorials!
    I have a question…
    I have made an app configuration with your tutorial and all works very well, but I have to “reload” the watchface in the Pebble to see the changes. The reason is because I load a lot of bitmaps in main_window_load that changes the watchface interface. I would like to know if it is possible to “reload” the watchface when I receive the event in in_recv_handler when a setting has changed.
    Thanks in advance!

    • bonsitm said:

      Hi, this should be possible if you use gbitmap_destroy to free the memory for the unused images first, then load the ones you want for the user’s preference.

  5. Hi, thanks a lot for the tutorial! I was so desperately looking for a good one. I was trying to add a string check and reload the page if it’s not correct. Would it be possible? I have an authorisation code to be checked and give an error message if wrong, then reload the configuration page. Any help or reference would be highly appreciated.

    • bonsitm said:

      I’ve not got a lot of expertise in HTML or forms beyond that shown here, but it may be possible using the navigation method used for submitting the form, but changing the address.

  6. Jonathan said:

    Hi there, I wonder if you could help with me adding another drop box to the HTML? I’ve added another select ID with the options for the drop box, but I’m struggling to insert into the script the On/Off option from this second drop box? Please could you advise? Whatever I seem to do it is not collected and sent back to the pebble app?

    • bonsitm said:

      This is done using the .value accessor towards the bottom of the page. Add your selector in the manner shown in getOptions.

      • Jonathan said:

        I have this and it doesn’t seem to work?

        Invert Watch Face Colours:

        Off
        On

        Show ºF:

        Off
        On

        Save

        //Setup to allow easy adding more options later
        function saveOptions() {
        var invertSelect = document.getElementById(“invert_select”);
        var fSelect = document.getElementById(“f_select”);
        var options = {
        “invert”: invertSelect.options[invertSelect.selectedIndex].value
        “fselect”: fSelect.options[fSelect.selectedIndex].value
        }

        return options;
        };

      • bonsitm said:

        Can you send me a link to your project source code so I can look in full?

      • bsavarin said:

        Hello, I’ve used your tutorial to add inverter settings to my watchface and it’s worked perfectly. However, I would like to add more options such as hide seconds, change date format and C or F switches by amending the codes for inverter and I’m getting an app error. Would you be able to point me in the right direction please?

        I can send you a zip file with the codes in a more secure format.

        Thanks in advance.

      • bonsitm said:

        I’m afraid I don’t have the time to do one-on-one reviews at the moment, but if you post your compiler error here I can shed some light. How much experience do you have with C?

      • bsavarin said:

        I have absolutely no experience with C, but I’m learning a bit while working on this project. The log is below:-

        [ERROR] ault_handling.c:77: App fault! {1b792909-692a-42c4-879b-e592be729e00} PC: 0x804caac LR: 0x525
        [PHONE] pebble-app.js:?: {‘runhost client uuid’ = 00000000-0000-0000-0000-000000000000}:{‘webapp uuid’ = 1b792909-692a-42c4-879b-e592be729e00}: ++_JS_LIFECYCLE_++:LAUNCHING
        [PHONE] pebble-app.js:?: Moto_360_Illuminator_Pro__1.0/pebble-js-app.js:4 PebbleKit JS ready!
        [PHONE] pebble-app.js:?: {‘runhost client uuid’ = dc7d621d-68d1-4bd5-9266-6d65fe66c9e3}:{‘webapp uuid’ = 1b792909-692a-42c4-879b-e592be729e00}: ++_JS_LIFECYCLE_++:READY-RUNNING
        [PHONE] pebble-app.js:?: {‘runhost client uuid’ = dc7d621d-68d1-4bd5-9266-6d65fe66c9e3}:{‘webapp uuid’ = 1b792909-692a-42c4-879b-e592be729e00}: ++_JS_LIFECYCLE_++:KILLED
        [ERROR] ault_handling.c:77: App fault! {1b792909-692a-42c4-879b-e592be729e00} PC: 0x804caac LR: 0x525

        (then the error messages loop until I leave the watchface).

        Thanks in advance for your help.

      • bonsitm said:

        Pebble is a good way to learn. It looks like there may be a null pointer somewhere (something like a TextLayer *s_some_layer_name;) that you have not created (text_layer_create() in this example case).

        Starting at the beginning of ‘main()’, use APP_LOG(APP_LOG_LEVEL_DEBUG, “GOT HERE”); to see how far execution gets before crashing. This should systematically lead you to the problem.

      • bsavarin said:

        I’ve checked all my text layers and I’ve created all of them. I’ve added the APP_LOG and recompiled, but I see no change in the logs. I placed the code within the int main(void), would that be correct?

  7. Jonathan said:

    Chris can you give me some options on sharing my code with you. I currently have it sitting in my public folder on dropbox, didn’t want to share that on here however. Thanks, Jon.

    • Chris said:

      Did you ever resolve this? I cant get the html page to ‘save’ if i have more than one option returned

      • Yes it was a problem with editing the html in TextEdit on my Mac. I have moved to Komodo Edit and now it works. It was a problem with several characters, mainly the ‘ symbol.

  8. Christian said:

    I’m very new to the Pebble and C, so thank you for your tutorials!
    I’m trying to add a second option to my watchface, but I get errors in “in_recv_handler” when compiling. Am I correct when I think that your code only reads the first dictionary entry? Should I make it iterate the Tuple t to read more entries?

    • bonsitm said:

      Correct! You can use while(t) and assign t inside the while loop with dict_read_next() to get all data

  9. yoshiboarder said:

    Thank you so much! I have a question.. Can I make an setting page via iOS SDK?

Leave a comment