Watch App Development Blog - Week 4 *cough* 5
February 08, 2015 -I’m blogging my progress in developing an Apple Watch App. Read the previous instalments here.
So, um, I missed a week. Sorry about that. Sad face.
Step 4: The Pebble
We don’t have access to any Apple Watch hardware yet, so it’s difficult to get a good feel for how you will use your app in context. I was given a Pebble for Christmas though, and thought it may be worthwhile getting the core information displayed on a Pebble app so I can experience and analyse the usage flow behind a watch app with this data.
Pebble: the Palm Pilot of Smartwatches
First, my impressions of the pebble:
- Battery life is not too bad, I get about a week of normal usage. It doesn't compare favourably to several years of battery life on a traditional watch (like my trusty Tag 2000), but it should soundly spank most newer-generation smart watches, including the Apple Watch, based on the rumours to date.
- The screen is awful if you’re used to a high-resolution smartphone display. The B&W 144 x 168 display looks like 90s tech.
- The Watch itself is plasticky (lasted a whole day before it copped a small but visible scratch on the face), and too large and ungainly for most wrists.
- The app/watchface marketplace is fairly limited & most of the apps have a ‘hobbyist’ feel to them. I don’t necessarily mean that in a negative manner - it’s great to see the enthusiasm, but without a good mechanism to monetise apps there’s not the same level of investment & innovation that I see in the iOS App Store or the Play Store.
- The hardware is fairly slow and anaemic.
- Development for the pebble is difficult. The Pebble C API is very low level, requires a lot of careful memory management, can only run on the device, and can’t be debugged. They’ve released a JavaScript API to try to make the experience a bit better, though I haven’t tried it.
- If you’re using your pebble with an iPhone, the experience is less than seamless due to Apple’s bluetooth accessory restrictions. Some functionality (e.g. network access) stops working if the Pebble iOS app has been terminated from the background. Third party iOS apps have a single Pebble connection to share, and communication can only be initiated from the phone.
To me, the product is very reminiscent of the early Palm Pilots - clunky B&W screen, an awkward developer experience, small hobbyist developer community etc. The future’s yet to be written, but the Pebble will need to undergo radical and ruthless improvement to keep pace with the latest smartwatches.
The Pebble App
The general concept behind the watch app is a simple display showing the departure times for the closest station. This should provide the ‘in context’ component of the most important watch app functionality. The initial UI design (pictured) includes the nearest station, and the destination, pattern, and minutes remaining for the next four departures from the station.
The simplest way to manage communication between the phone app & the watch app is the AppSync API. The general semantics of the API involve syncing a dictionary of shared data between the phone & watch; it also makes data storage on the watch more convenient. The downside is that this requires a specific key for each individual data element synced to the watch - i.e. specific numbered departures rather than a variable array of scheduled trains.
With that in mind, the keys were defined thus:
#define KEY_STATION 0
#define KEY_DEST_1 1
#define KEY_TIME_1 2
#define KEY_DEST_2 3
#define KEY_TIME_2 4
#define KEY_DEST_3 5
#define KEY_TIME_3 6
#define KEY_DEST_4 7
#define KEY_TIME_4 8
The main issue I ran into with AppSync was a storage limitation. The sample code includes a 30 byte sync buffer which is insufficient for most data sync requirements, but it wasn’t immediately obvious that’s what the error DICT_NOT_ENOUGH_STORAGE
was referring to. Upping the buffer to 128 bytes solved the issue. That should be enough for anybody.
Once the data was synced, I update the UI using the following function:
static void drawText() {
const Tuple *tuple;
if ((tuple = app_sync_get(&s_sync, KEY_STATION))) {
if (tuple->value->cstring[0] == 0) {
text_layer_set_text(station_text_layer, "Waiting for data");
} else {
text_layer_set_text(station_text_layer, tuple->value->cstring);
}
}
for (int i = 0; i < 4; i++) {
if ((tuple = app_sync_get(&s_sync, i * 2 + 1))) {
if (tuple->value->cstring[0] == 0) {
text_layer_set_text(dest_text_layers[i], "");
} else {
text_layer_set_text(dest_text_layers[i], tuple->value->cstring);
}
}
if ((tuple = app_sync_get(&s_sync, i * 2 + 2))) {
if (!tuple->value->int32) {
text_layer_set_text(time_text_layers[i], "");
} else {
time_t departure_time = tuple->value->int32;
time_t current_time = time(NULL);
int minutes = (departure_time - current_time)/60;
char time_str[5];
snprintf(time_str, 5, "%dm", minutes);
text_layer_set_text(time_text_layers[i], time_str);
}
}
}
}
...where dest_text_layers
and time_text_layers
are four element arrays containing references to the text layers on the watch UI.
Can you spot the bug? If you haven’t done much work with embedded systems it’s not obvious. Critically, the documentation for text_layer_set_text
says:
The string is not copied, so its buffer most likely cannot be stack allocated, but is recommended to be a buffer that is long-lived, at least as long as the TextLayer is part of a visible Layer hierarchy.
time_str
is not copied when passed to text_layer_set_text
; the effect being that it goes out of scope and is never displayed on the watch face. The solution is a set of string buffers referenced statically - I used a static char pointer array, and malloc
ed/free
d the buffers in window_load
/window_unload
.
// at the top of the file
#define TIME_LABEL_LENGTH 5
static char *time_strings[4];
// in the window_load() function
for (int i = 0; i < 4; i++) {
time_strings[i] = malloc(sizeof(char[TIME_LABEL_LENGTH]));
}
// drawtext() changes to:
snprintf(time_strings[i], TIME_LABEL_LENGTH, "%dm", minutes);
text_layer_set_text(time_text_layers[i], time_strings[i]);
The iOS App
Pebble integration doesn’t require a a substantial amount of code - drag in the frameworks and pull a PBWatch
reference from PBPebbleCentral.defaultCentral().lastConnectedWatch()
. Because I want to be able to show the number of minutes until a train leaves, I changed the earlier code from a HH:MM string to a ZonedDate
/NSDate
in the Haskell & Swift code. I then implemented Pebble communication using the following (dest/pattern is abbreviated to economise on transfer bandwidth & Pebble display size):
func updatePebble(station: Station, _ times : [Departure]) {
var pebbleUpdate : [NSNumber: AnyObject] = [
KeyStation : station.name,
]
for i: Int in 0..<4 {
let destKey = NSNumber(int: Int32(i * 2 + 1))
let timeKey = NSNumber(int: Int32(i * 2 + 2))
pebbleUpdate[destKey] = times[i].shortDescription
pebbleUpdate[timeKey] = NSNumber(int32: Int32(times[i].time.timeIntervalSince1970))
}
self.watch?.appMessagesPushUpdate(pebbleUpdate, withUUID: appUUID, onSent: { (w, Dict, e) in
if let error = e {
println("Error sending update to pebble: \(error.localizedDescription)")
} else {
println("Sent update to pebble!")
}
})
}
There was a problem though: the Pebble showed around -470 minutes for each train (i.e. 8 hours out - suspicious, as local time is +8:00). Turns out Pebble has no concept of timezone at all. The docs spin this as: “Note that the epoch is adjusted for Timezones and Daylight Savings.” Not sure that qualifies as epoch time, but it was clear the conversion is meant to happen on the phone. The following code sorted the issue:
let adjustedEpoch = Int(times[i].time.timeIntervalSince1970) + NSTimeZone.localTimeZone().secondsFromGMT
pebbleUpdate[timeKey] = NSNumber(int32: Int32(adjustedEpoch))
Glorious 1 bit UI
The result (I have a promising future as a watch model). I’ll give it a good workout near the train station over the next week.
As always, the code is available here, here and here.
A last note: The fact that I’m on week 5 of my watch app development journey and am yet to touch WatchKit is not lost on me. I should hopefully start hitting WatchKit code this week. With a bit of luck.