Friday, November 28, 2014

Pebble Watchface: Saving Battery Life

The Pebble watch is advertised to have up to 7 day battery life, but it must be very aggressive at shutting off the CPU / other tricks to achieve that. If your watch face tries to do too much, it may cause your battery to be drained in just a couple days. (Leaving the backlight on causes the battery to drain in just a few hours!)

Seconds on Request

Many of the existing watch faces available did not show seconds, but I really wanted to see them. I did some testing and found out why:
If I did show seconds, then I barely got 3 days of battery life.
If I did not show seconds, then I was on track to get 5 days of battery life.

Seconds are useful some of the time, but it's probably not important to show them at 3 am when you're asleep.

Pebble watch faces don't allow any kind of button input, but the watch does have an accelerometer that both watch faces and apps can take advantage of. The accelerometer can be used for a variety of functions, including a watch "tap" (basically shaking the watch). The most common use for the "tap" is to momentarily turn on the backlight - allowing you to read the screen in the dark. It also seemed like the best (only?) way to have the user request seconds.

Please note: The following assumes you have some experience with C coding and the Pebble SDK / watch face generation. This is just a rough guide to reducing battery consumption.

The Pebble SDK has a subscriber call to register how often your app needs to be refreshed.

To update every minute:
tick_timer_service_subscribe(MINUTE_UNIT, tick_handler);

To update every second:
tick_timer_service_subscribe(SECOND_UNIT, tick_handler);

tick_handler is my function that is called at the given time unit.

The SDK also includes the subscriber for the watch accelerator tap:
accel_tap_service_subscribe(accel_tap_handler);

accel_tap_handler is my function that is called when a tap has occured.

Both of these subscribers should be placed in the watch face's init:
static void init(void)
{
  ...
  tick_timer_service_subscribe(MINUTE_UNIT, tick_handler);
  accel_tap_service_subscribe(accel_tap_handler);
  ...
}

And they should be unsubscribed in the deinit:
void deinit(void)
{
  ..
  tick_timer_service_unsubscribe();
  accel_tap_service_unsubscribe();
  ..
}

Both init() and deinit() need to be at the bottom of your .c file as they reference functions defined above them. (Alternatively, you could use function prototypes to place them anywhere.)

Now we need to have a couple variables to keep track if we're showing the seconds and for how long:
bool isShowingSeconds = false;
time_t timeOfLastTap = 0;

And a constant for how long to display seconds:
#define NUMBER_OF_SECONDS_TO_SHOW_SECONDS_AFTER_TAP 180

The variables and constants should be declared at the top of your .c file.

Now let's handle what happens when the tap event if fired (user shakes his or her watch):
static void accel_tap_handler(AccelAxisType axis, int32_t direction)
{
  // Save time of last tap.
  timeOfLastTap = time(NULL);
  
  if (!isShowingSeconds)
  {
    // We aren't showing seconds, let's show them and switch
    // to the second_unit timer subscription.
    isShowingSeconds = true;
      
    // Immediatley update the time so our tap looks very responsive.
    struct tm *tick_time = localtime(&timeOfLastTap);
    update_time(tick_time);

    // Resubscribe to the tick timer at every second.
    tick_timer_service_subscribe(SECOND_UNIT, tick_handler);
  }
}

When the event is fired, I save the time of last tap. If I'm not currently showing seconds (!isShowingSeconds), then I show seconds (isShowingSeconds = true) and resubscribe the tick_timer to the seconds unit so it is called every second. I also immediately update my time so the watch face looks responsive (rather than wait for the next second to update the watch face).

Now we need to add code to revert back to minutes:
static void tick_handler(struct tm *tick_time, TimeUnits units_changed)
{
  if (isShowingSeconds)
  {
    if (difftime(time(NULL), timeOfLastTap) > NUMBER_OF_SECONDS_TO_SHOW_SECONDS_AFTER_TAP)
    {
      // We are showing seconds, but it has been more than 3
      // minutes since our wrist was tapped. To save processing,
      // stop showing seconds (revert back to one minute updates).
      isShowingSeconds = false;
      tick_timer_service_subscribe(MINUTE_UNIT, tick_handler);
    }
  }

  // Update the time.
  update_time(tick_time);
  ...
}

Finally the update_time function should only display seconds if the timer interval is seconds:
static void update_time(struct tm *tick_time)
{
  ...
  static char timeSecondsBuffer[3];
  if (isShowingSeconds)
  {
    // isShowingSeconds is toggled by a watch bump to save battery.
    strftime(timeSecondsBuffer, sizeof("00"), "%S", tick_time);
    text_layer_set_text(s_time_seconds_layer, timeSecondsBuffer);
  }
  else
  {
    // Clear out the seconds field.
    timeSecondsBuffer[0] = 0;
    text_layer_set_text(s_time_seconds_layer, timeSecondsBuffer);
  }
  ...
}

According to the Pebble SDK, it is not necessary to unsubscribe tick_timer_service before subscribing to a different time interval. The previous interval is replaced.

That's it! You've just increased your watch battery life 66%!

Weather Every 30 Minutes

In addition to only using the SECOND_UNIT when absolutely necessary, it's a good idea to minimize how much code is executed in all parts of the watch face.

For example, I only update the weather once every 30 minutes.

Variable to keep track of the last time we updated:
time_t timeOfLastDataRequest = 0;

#define NUMBER_OF_SECONDS_BETWEEN_WEATHER_UPDATES 1800

Update that variable whenever the weather is updated:
static void request_weather()
{
  timeOfLastDataRequest = time(NULL);
  
  // Begin dictionary
  DictionaryIterator *iter;
  app_message_outbox_begin(&iter);

  // Add a key-value pair
  dict_write_uint8(iter, 0, 0);

  // Send the message!
  app_message_outbox_send();
}

Check if 30 minutes have elapsed in the timer event:
static void tick_handler(struct tm *tick_time, TimeUnits units_changed)
{
  ...
  // Update the weather every 30 minutes.
  if (difftime(time(NULL), timeOfLastDataRequest) > NUMBER_OF_SECONDS_BETWEEN_WEATHER_UPDATES)
  {
    request_weather();
  }
  ...
}

Calendar Once a Day

The calendar only needs to be updated when the day changes:

Variable to keep track of what date we're on:
int lastCalendarDateUpdatedTo = -1;

Update that variable whenever the calendar is updated:
static void update_date(struct tm *tick_time)
{
  ...
  // Keep track of the last date we updated to so we only have to
  // update when it changes.
  lastCalendarDateUpdatedTo = tick_time->tm_mday;
  ...
}

Check if the date has changed in the timer event:
static void tick_handler(struct tm *tick_time, TimeUnits units_changed)
{
  ...
  // Update the date only when the day changes.
  if (lastCalendarDateUpdatedTo != tick_time->tm_mday)
  {
    update_date(tick_time);

    // Also request the weather again so we get the next day's forecast.
    request_weather();
  }
  ...
}

Setting up the window layers and other supporting code has been omitted to keep this guide focused on just the power saving code. Please refer to the first post on my Pebble Watchface for the complete source code. I hope it helps!


No comments:

Post a Comment