Introduction

As a side-effect of being involved with the development and testing of the new Pebble timeline experience, I’ve been eager to try my hand at exploiting this new mechanism to bring timely updates to users of BBC News (which as with most of my apps, began as an app for personal use) with timeline pins.

The Concept

As you may know from reading the Pebble Developers timeline guides, Pebble watchapps can now incorporate a purely web-based element to display data from web data sources (such as the BBC News feeds) on the timeline. This is ideal for chronological events such as news stories, as each pin is shown on the timeline according to date.

Pins can also include notifications that appear when they are first created or updated, keeping the user informed as details change. This functionality is used to show a notification each time a new news story is available. The pin itself includes only the icon (standard timeline pin icon) and the title, which is the main title of the story. In addition, timeline pins can have actions associated with them, enabling the user to launch the associated watchapp (whose API key is used to push the pins) and pass a single integer as a context argument. My idea was to use this action ability to enable the pin to open the BBC News watchapp to show the full story details, since a very long timeline pin body was a poor scrolling experience.

The Problem

The fundamental question was this; How does one tell the watchapp which story to show from the pin using a single integer? The first guess was to specify the launchCode (the argument) as the position in the array of downloaded news stories. This would work for a very recent pin only, as the order of news stories in the feed changes frequently as new stories are added and removed from the headlines. After an hour or two, the order would change and an existing pin would possess a launchCode pointing to the same array location, but no longer guaranteed to be the same story.

Implementation

The solution I ended up choosing came from Wristponder, which uses a checksum of the persisted list of responses on the Android and Pebble sides, prompting a re-download to the watch if the two do not match (i.e.: The user has modified their list of responses). Thus the procedure to open a news story from its pin is this:

1. Backend server running on DigitalOcean downloads the RSS feed from the BBC every half an hour, using a function to create a list of story objects from the XML;

var parseFeed = function(responseText) {
  var items = [];
  var longestTitle = 0;
  var longestDesc = 0;
  while(responseText.indexOf('<title>') > 0 && items.length < MAX_ITEMS) {
    //Title
    var title = responseText.substring(responseText.indexOf('<title>') + '<title>'.length);
    title = title.substring(0, title.indexOf('</title>'));
    responseText = responseText.substring(responseText.indexOf('</title>') + '</title>'.length);

    //Desc
    var desc = responseText.substring(responseText.indexOf('<description>') + '<description>'.length);
    desc = desc.substring(0, desc.indexOf('</description>'));

    // Date
    var date = responseText.substring(responseText.indexOf('<pubDate>') + '<pubDate>'.length);
    date = date.substring(0, date.indexOf('</pubDate>'));

    //Add
    var s = { 'title': title, 'description': desc, 'date': date };
    items.push(s);

    // Metrics
    Log('Story '+ items.length + ': ' + s.title + ' // ' + s.description);
    Log('(' + s.title.length + 'x' + s.description.length + ')');
    if(s.title.length > longestTitle) {
      longestTitle = s.title.length;
    }
    if(s.description.length > longestDesc) {
      longestDesc = s.description.length;
    }

    // Next
    responseText = responseText.substring(responseText.indexOf('</description>') + '</description>'.length);
  }

  Log('parseFeed(): Extracted ' + items.length + ' items.');
  Log('parseFeed(): Longest title/description: ' + longestTitle + '/' + longestDesc);
  return items;
};

2. Backend uses story titles, publish dates and checksums of the titles to push a pin for each story. The id field of each pin is a prefix followed by the Unix timestamp of the pubDate RSS field (as an easy solution of a fairly unique number from each story). The checksum is generated by simply adding all the character codes in each title, then specifying this number as the ‘Open Story’ pin action’s lanchCode;

var pin = {
  'id': 'bbcnews-story-' + pubDate.unix(),
  'time': pubDate.toDate(),
  'layout': {
    'type': 'genericPin',
    'tinyIcon': 'system://images/TIMELINE_PIN_TINY',
    'title': gStories[i].title,
    'subtitle': 'BBC News Headline'
  },
  'createNotification': {
    'layout': {
    'type': 'genericPin',
    'tinyIcon': 'system://images/TIMELINE_PIN_TINY',
    'title': gStories[i].title,
    'subtitle': 'BBC News Headline'
    }
  },
  'actions': [
    {
      'title': 'Open Story',
      'type': 'openWatchApp',
      'launchCode': checksum(gStories[i].title)
    }
  ]
};

3. These pins are filtered for duplicates and staleness by the Pebble timeline public API, then pushed through the Pebble mobile app to the user’s watch. This is demanded by a subscription to the ‘headlines’ topic that all users are subscribed to when they first launch the BBC News watchapp (this will be optional in the first release).

4. The user selects a BBC News story from their timeline and chooses the ‘Open Story’ pin action. This opens the watchapp with the launch_get_args() value set as the checksum stored in the pin when it was originally generated. If the launchCode is not zero, a pin was specified;

#ifdef PBL_PLATFORM_APLITE

// Timeline not available, give me ALL the stories!
comm_request(COMM_MODE_LIST, 0);

#elif PBL_PLATFORM_BASALT

// Is launchCode specified from a pin?
int launch_code = launch_get_args();

if(launch_code == 0) {
  // Get all stories
  comm_request(COMM_MODE_LIST, 0);

  stories_window_set_desc_text("Updating...");
} else if(launch_reason() == APP_LAUNCH_TIMELINE_ACTION) {
  // Check this checksum, JS!
  comm_request(COMM_MODE_PIN, launch_code);

  stories_window_set_desc_text("Getting pin story...");
}

#endif

5. The watchapp sends the checksum for the pin in question to PebbleKit JS, which downloads the latest feed from the BBC News site and checks the query checksum against checksums of all the stories downloaded. First, the checksum is obtained from the watch’s AppMessage;

if(hasKey(dict, 'KEY_ACTION')) {
  gLaunchCode = getValue(dict, 'KEY_ACTION');
  Log('TIMELINE PIN LAUNCH CODE: ' + gLaunchCode + '\n\n\n');

  // Download stories, and match the titles to the pin
  download(persistRead('category', 'headlines'), findPinWithHash);
}

Next, the matching story is found;

function findPinWithHash(responseText) {
  //Strip metadata
  var spool = responseText.substring(responseText.indexOf('<item>') + '<item>'.length);
  gQuantity = 30; // Get all

  var stories = parseFeed(spool);
  Log('Finding title with launchCode=' + gLaunchCode + ' in list of ' + stories.length + ' stories');

  var found = false;
  for(var i = 0; i < stories.length; i += 1) {
    var check = checksum(stories[i].title);
    if('' + check == '' + gLaunchCode) {
      Log('Found! check=' + check + ', gLaunchCode=' + gLaunchCode);

      // Send to phone
      var dict = {
        'KEY_ACTION': 0,
        'KEY_TITLE': stories[i].title,
        'KEY_DESCRIPTION': stories[i].description
      };
      Pebble.sendAppMessage(dict, function() {
        Log('Sent pin data to watch!');
        found = true;
      });
    }
  }

  // Not found?
  if(found == false) {
    var dict = {
      'KEY_FAILED': 1
    };
    Pebble.sendAppMessage(dict, function() {
      Log('Informed Pebble of failure to find story.');
    },
    function(err) {
      Log('Failed to inform of failure!');
    });
  }
}

6. If the story is still relatively recent (this can vary) and a checksum match is found, the story’s full title and body are sent to the watchapp for display to the user. The RSS feed also contains links to thumbnails already formatted to 144 pixels in width, which is ideally placed to be a possible future feature.

Results

The resulting flow looks something like the image below, which is still a work in progress:

pin

This new layout will be available soon (once bugs are worked out) for Aplite users of the existing BBC News Headlines watchapp, in color on Basalt for those with Pebble Time watches (admittedly few right now!). Stay tuned!

Long time no blog! There hasn’t been much time for experimentation (besides creating a couple of color apps for Pebble Time) in the months leading up the Pebble Time Kickstarter. Totally worth the effort though, after seeing the response to the new material on the Pebble Developers site.

This weekend, however, I found some time to bring to reality an idea I have had since I first learned of the ability to make color apps for Pebble. Being a big fan of PebbleKit Android (used in Dashboard, Wristponder, Watch Trigger etc) to remotely control/access the connected phone. The idea is this: create an app for Android that registers as a receptor of opening image files. This app then resizes and reduces the color palette of the file before piping it to an automatically opening watchapp for viewing. This stemmed partly from a desire to avoid constantly changing resource files and recompiling a simple app for viewing PNG files every time I wanted to see how a new image looked on Pebble Time.

The implementation was pretty straightforward: Make a simple Android app that included an Intent filter for image files, and a watchapp that simply accepted data packets and stored them in the image data allocated for a GBitmap of the right size. The difficulty came in the signaling between the two. I’m no stranger to establishing communication schemes between Android and Pebble apps, but I’m only human. For some reason I was trying to use a mix of PebbleDataReceivers and PebbleAckReceivers. The former is for processing messages from the watch, and the latter is for reacting to the event that the watch acknowledges a message from the phone. By sending the next data packet in an ‘ACK’ handler, you can ensure maximum transmission speed as no time is wasted between the watch processing one packet and being ready for the next. This is the same method that apps like Wristponder use for transferring lots of data (try 30 canned responses!). If you’re not careful, mixing these two modes (one of which can be considered manual, the other semi-automatic when set up in a continuous data transmission loop) can result in puzzling behavior that is difficult to debug.

Once debugged, however, the result is an app that sends a complete uncompressed image (save that for another day) 24k image to the watch in about 11 – 16 seconds (approx 2 kB/s) and then displays it on the watch. In the meantime, both the Android and Pebble apps show the progress of the transfer:

Screenshot_2015-03-08-18-41-59Screenshot_2015-03-08-18-19-10

transfer

 

 

 

 

 

 

 

 

 

 

 

The app sends a built-in image of a tree as a default option when it is launched by itself, or another image if presented one using ‘Open With’ actions, such as from file managers or emails. The result of sending the tree image is thus:

IMG_20150308_182032

For some reason I can’t quite fathom the color reduction process the app uses (admittedly brittle bit shifting) doesn’t handle all files as well, producing discolored results, which I will try and iron out with a better solution, perhaps in the Android SDK itself. When that day comes, I’ll polish both app components and hopefully make another Google Play app of it (free, of course, this doesn’t do anything particularly useful) and also a two-part library generalized to facilitating large data transfers between phone and watch.

Watch this space!

 

Dashboard for Pebble is now version 1.13. This version contains a new Materially design, as well as fixes to the Data and ringer toggles as a result of the changes in Android 5.0 Lollipop.

The major point to note is that the method reflection I was using to implement the Data relied on an internal API in the ConnectivityManager class (which has existed in Android since very early versions). This API has since been moved to a more system-exclusive location (the Telephony class) that cannot be invoked using method reflection and as such the feature stopped working on Android 5.

Being one of the main features of Dashboard (and half the sole purpose in the original Data Toggle watchapp), this outcome was unacceptable. After searching for an alternative and finding nothing but similarly disgruntled Android developers, I came across a widget developer named Cygery who had found a way to implement this behavior in Android 5, and after a brief email exchange I was informed of his method, which was quite ingenious.

As a result, full functionality has been restored on Android 5, but at a large price – the feature now requires root privileges to change that particular settings. This is obviously not ideal, but the only way I can see the feature working beyond Android 5. Users on KitKat and below remain unaffected and the app should work as it did for them. Therefore, the Dashboard Android app will request root when it starts, as well as post a notification if the Data toggle is used and root access is not given. Most SuperUser apps will allow this access to be given on a per-app basis, so please allow this if you are a Android 5 user and need to use the Data toggle.

Download

Get it on Google Play

Over the last week, there have been have been four version of Dashboard released (9, 10, 11 and now 12) following the inclusion of the Wakeup API. I used this shiny new firmware feature to let users of Dashboard schedule daily wakeups to issue an on/off command to the Dashboard Android app, at times they would use the toggles anyway.

pebble-screenshot_2014-11-08_21-37-47    pebble-screenshot_2014-11-08_21-38-04

pebble-screenshot_2014-11-08_21-38-11    pebble-screenshot_2014-11-08_21-38-21

A new ‘HOLD’ icon prompts access to the scheduling feature, including list of existing events and UI to create new ones.

A personal example of this is that every night at about midnight I turn off WiFi on my phone to save power through the night using Tasker. Now, I can remove the Tasker icon from the status bar and use Dashboard to carry out the action instead. Of course, Tasker didn’t require me to keep my Bluetooth on overnight, but it’s a small price to pay for automated control of Android radios!

When adding such a complex feature (Dashboard itself went from two main code files to eight and ~600 to ~1300 lines of code), bugs will occur. Some will be code-based, such as not handling setting two wakeups for the same time (which the system will not allow) and warning the user, and some are behavioral.

At the moment, the Pebble appstore will not always update the released watchapp when a new version is uploaded. This can make co-ordinating a release with Google Play Store very difficult. Users were prompted to ‘update watchapp from Android app’. What I intended was for them to use the ‘Install Watchapp’ button in the Dashboard Android app to get the bundled compatible version, but in reality they were unloading and reloading the watchapp from the Pebble app locker, which ended up with them still having the old version. And so the loop continued until some concerned users emailed me about it. In all cases I clarified the correct procedure and every case was fixed. So now that process is hopefully a bit more explicit!

For a couple of my existing watchapps and watchfaces I have implemented a smooth animation using an AppTimer. This involves something like the snippet below:

static void some_layer_update_proc(Layer *layer, GContext *ctx) {
  // Graphics calls

}

static void timer_handler(void *context) {
  // Update frame
  layer_mark_dirty(some_layer);

  // Finally schedule next frame
  app_timer_register(34, timer_handler, NULL);
}

...

static void start_animation() {
  // Schedule first frame to start loop
  layer_set_update_proc(some_layer, some_layer_update_proc);
  app_timer_register(34, timer_handler, NULL);
}

As you can see, after the first frame is scheduled with an AppTimer, the timer’s handler schedules the next, and so an infinite loop is born. After implementing this multiple times, it occurred to me that I could make this process easier to set up, even if it was just for myself.

The result of this is a new library called pge, which creates an object that handles this looping of game logic and rendering per-frame for you, similar to STL. It also handles button clicks with an easier abstraction for the developer. Here’s a quick example of usage, from the GitHub README file:

#include "pge.h"

static PGE *s_game;

void loop() {

}

void draw(GContext *ctx) {

}

void click(int button_id) {

}

...

s_game = pge_begin(s_window, loop, draw, click);

This will start a 30 FPS loop that calls the developer’s implementation of draw() and loop() every frame, and click() when a button is clicked. The developer can then check the button ID as usual using the Pebble SDK constants, such as BUTTON_ID_UP. The loop will end and the PGE can be destroyed as part of a normal Window‘s lifecycle:

static void main_window_unload(Window *window) {
  // Destroy all game resources
  pge_finish(s_game);
}

The GitHub repo also includes a sample app where I implemented a simple ‘game’ of controlling a ‘robot’, using select to start/stop the robot and the up and down buttons to rotate its direction of travel.

Robot game

I’m currently working on implementing an Entity object that can be added to a list for automatic looping and rendering by the PGE. This will eventually also allow collision, as well as couple of other useful features.

If you are thinking of creating such a game, this library can hopefully help get you started. Let me know when you end up creating! The repo can be found here.

A while ago I created a simple Alert Library, which provides functions to show and hide a set of layers to show an ‘alert window’ for a time that tells the user of an event without changing Window.

screenshot

Continuing this theme, today I created another similar library called the ToastLayer, which has two advantages over the Alert Library:

1. The toast notification animates up from the bottom, instead of covering the majority of the Window.

2. It is designed as an object, like any of the other Layers in the Pebble SDK. This means there can be more than one!

Feel free to use it to show notifications without changing Window! I will probably look into including this in some of my existing apps.

Not a lot has been happening on this blog for the last few weeks, and the reason for that is that I have been busy beginning my internship at Pebble! It’s been a great experience so far (We released SDK 2.5 today!) and I can’t wait to work on more awesome things to help make Pebble even more useful smartwatch.

That being said, I’ve been able to find time to maintain and update my public Pebble apps at weekends, and hope to continue this pattern whenever I can, because I have some ideas I want to implement even just for myself.

A result of this is a new application of my Spark Core driven LCD project in collaboration with a new colleague to display social media trends from WhatTheTrend on animated cards on Pebble, and as an added bonus show the same information on the LCD display.

After some teething issues, it was eventually presentable and works pretty well – but for some reason only on Wi-Fi. Here’s a photo of the whole thing in action. (Eduardo Sasha is imprinted on my brain now from all the testing…)

image

Stay tuned for coming updates to Dashboard and Wristponder, as well as bringing Watch Trigger up to date as well!

Follow

Get every new post delivered to your Inbox.

Join 261 other followers