Friday, November 28, 2014

Pebble Watchface: Parsing JSON Weather

For my Watchface, I wanted to see current conditions (temperature / wind speed) and the forecast conditions (will it rain today or tomorrow?). There are times when the current conditions are clear skies but the forecast for later that day is rain; I wanted to be able to see both pieces of information at the same time.

The weather information on the watch is retrieved from openweathermap.org in the JSON format. I make 2 calls for weather, one for the current conditions and one for forecast.

The URL for current conditions:
  var url = "http://api.openweathermap.org/data/2.5/weather?lat=" +
      pos.coords.latitude + "&lon=" + pos.coords.longitude;

The URL for forecast conditions:
  var forecasturl = "http://api.openweathermap.org/data/2.5/forecast/daily?lat=" +
      pos.coords.latitude + "&lon=" + pos.coords.longitude;

Open Weather Map returns the weather data in JSON format.

For an example, we'll pull the weather from Traverse City, Michigan (N44.7681 W86.6222). Winter time in Michigan should have quite interesting weather.

Current Conditions URL:
http://api.openweathermap.org/data/2.5/weather?lat=44.7681&lon=-85.6222
(You can type this URL in your web browser to see the output.)

Current Conditions Raw Output:
{"coord":{"lon":-85.62,"lat":44.77},"sys":{"type":1,"id":1459,"message":0.0399,"country":"US","sunrise":1417265884,"sunset":1417298637},"weather":[{"id":600,"main":"Snow","description":"light snow","icon":"13n"}],"base":"cmc stations","main":{"temp":268.55,"pressure":1017,"humidity":79,"temp_min":267.15,"temp_max":269.15},"wind":{"speed":1.5,"deg":0},"clouds":{"all":90},"dt":1417225020,"id":5012495,"name":"Traverse City","cod":200}

Forecast Conditions URL:
http://api.openweathermap.org/data/2.5/forecast/daily?lat=44.7681&lon=-85.6222
(You can type this URL in your web browser to see the output.)

Forecast Conditions Raw Output:
{"cod":"200","message":0.0066,"city":{"id":5012495,"name":"Traverse City","coord":{"lon":-85.620628,"lat":44.763062},"country":"US","population":0},"cnt":7,"list":[{"dt":1417194000,"temp":{"day":269.15,"min":268.92,"max":270.14,"night":270.14,"eve":269.15,"morn":269.15},"pressure":1004.17,"humidity":100,"weather":[{"id":601,"main":"Snow","description":"snow","icon":"13d"}],"speed":7.88,"deg":170,"clouds":92,"snow":2},{"dt":1417280400,"temp":{"day":278.83,"min":275.01,"max":279.23,"night":278.33,"eve":278.59,"morn":275.01},"pressure":992.92,"humidity":90,"weather":[{"id":600,"main":"Snow","description":"light snow","icon":"13d"}],"speed":7.13,"deg":195,"clouds":48,"snow":0.5},{"dt":1417366800,"temp":{"day":279.25,"min":272.88,"max":279.25,"night":272.88,"eve":275.6,"morn":276.1},"pressure":993.15,"humidity":94,"weather":[{"id":800,"main":"Clear","description":"sky is clear","icon":"02d"}],"speed":7.06,"deg":232,"clouds":8},{"dt":1417453200,"temp":{"day":269.83,"min":267.55,"max":270.65,"night":267.55,"eve":269,"morn":270.65},"pressure":1020.68,"humidity":0,"weather":[{"id":600,"main":"Snow","description":"light snow","icon":"13d"}],"speed":11.08,"deg":308,"clouds":69,"snow":0.82},{"dt":1417539600,"temp":{"day":271.5,"min":268.24,"max":273.72,"night":273.72,"eve":272.95,"morn":268.24},"pressure":1020.55,"humidity":0,"weather":[{"id":600,"main":"Snow","description":"light snow","icon":"13d"}],"speed":8.29,"deg":178,"clouds":0,"snow":0.26},{"dt":1417626000,"temp":{"day":279.39,"min":271.41,"max":279.39,"night":271.41,"eve":276.42,"morn":276.33},"pressure":986.23,"humidity":0,"weather":[{"id":600,"main":"Snow","description":"light snow","icon":"13d"}],"speed":12.83,"deg":208,"clouds":93,"rain":3.44,"snow":0.22},{"dt":1417712400,"temp":{"day":272.6,"min":271.5,"max":272.6,"night":272.49,"eve":272.56,"morn":271.5},"pressure":1003.13,"humidity":0,"weather":[{"id":601,"main":"Snow","description":"snow","icon":"13d"}],"speed":13.59,"deg":285,"clouds":47,"snow":4.52}]}

I found a handy parser online where you could paste JSON data and it shows it to you in an organized fashion: http://json.parser.online.fr/

Paste the raw output above into that parser to see the results.

Parsed Current Conditions:
{
  • "coord":{
    • "lon":-85.62,
    • "lat":44.77
    }
    ,
  • "sys":{
    • "type":1,
    • "id":1459,
    • "message":0.0399,
    • "country":"US",
    • "sunrise":1417265884,
    • "sunset":1417298637
    }
    ,
  • "weather":[
    1. {
      • "id":600,
      • "main":"Snow",
      • "description":"light snow",
      • "icon":"13n"
      }
    ]
    ,
  • "base":"cmc stations",
  • "main":{
    • "temp":268.55,
    • "pressure":1017,
    • "humidity":79,
    • "temp_min":267.15,
    • "temp_max":269.15
    }
    ,
  • "wind":{
    • "speed":1.5,
    • "deg":0
    }
    ,
  • "clouds":{
    • "all":90
    }
    ,
  • "dt":1417225020,
  • "id":5012495,
  • "name":"Traverse City",
  • "cod":200
}

Parsed Forecast Conditions:
{
  • "cod":"200",
  • "message":0.0066,
  • "city":{
    • "id":5012495,
    • "name":"Traverse City",
    • "coord":{
      • "lon":-85.620628,
      • "lat":44.763062
      }
      ,
    • "country":"US",
    • "population":0
    }
    ,
  • "cnt":7,
  • "list":[
    1. {
      • "dt":1417194000,
      • "temp":{
        • "day":269.15,
        • "min":268.92,
        • "max":270.14,
        • "night":270.14,
        • "eve":269.15,
        • "morn":269.15
        }
        ,
      • "pressure":1004.17,
      • "humidity":100,
      • "weather":[
        1. {
          • "id":601,
          • "main":"Snow",
          • "description":"snow",
          • "icon":"13d"
          }
        ]
        ,
      • "speed":7.88,
      • "deg":170,
      • "clouds":92,
      • "snow":2
      }
      ,
    2. {
      • "dt":1417280400,
      • "temp":{
        • "day":278.83,
        • "min":275.01,
        • "max":279.23,
        • "night":278.33,
        • "eve":278.59,
        • "morn":275.01
        }
        ,
      • "pressure":992.92,
      • "humidity":90,
      • "weather":[
        1. {
          • "id":600,
          • "main":"Snow",
          • "description":"light snow",
          • "icon":"13d"
          }
        ]
        ,
      • "speed":7.13,
      • "deg":195,
      • "clouds":48,
      • "snow":0.5
      }
      ,
    3. {
      • "dt":1417366800,
      • "temp":{
        • "day":279.25,
        • "min":272.88,
        • "max":279.25,
        • "night":272.88,
        • "eve":275.6,
        • "morn":276.1
        }
        ,
      • "pressure":993.15,
      • "humidity":94,
      • "weather":[
        1. {
          • "id":800,
          • "main":"Clear",
          • "description":"sky is clear",
          • "icon":"02d"
          }
        ]
        ,
      • "speed":7.06,
      • "deg":232,
      • "clouds":8
      }
      ,
    4. {
      • "dt":1417453200,
      • "temp":{
        • "day":269.83,
        • "min":267.55,
        • "max":270.65,
        • "night":267.55,
        • "eve":269,
        • "morn":270.65
        }
        ,
      • "pressure":1020.68,
      • "humidity":0,
      • "weather":[
        1. {
          • "id":600,
          • "main":"Snow",
          • "description":"light snow",
          • "icon":"13d"
          }
        ]
        ,
      • "speed":11.08,
      • "deg":308,
      • "clouds":69,
      • "snow":0.82
      }
      ,
    5. {
      • "dt":1417539600,
      • "temp":{
        • "day":271.5,
        • "min":268.24,
        • "max":273.72,
        • "night":273.72,
        • "eve":272.95,
        • "morn":268.24
        }
        ,
      • "pressure":1020.55,
      • "humidity":0,
      • "weather":[
        1. {
          • "id":600,
          • "main":"Snow",
          • "description":"light snow",
          • "icon":"13d"
          }
        ]
        ,
      • "speed":8.29,
      • "deg":178,
      • "clouds":0,
      • "snow":0.26
      }
      ,
    6. {
      • "dt":1417626000,
      • "temp":{
        • "day":279.39,
        • "min":271.41,
        • "max":279.39,
        • "night":271.41,
        • "eve":276.42,
        • "morn":276.33
        }
        ,
      • "pressure":986.23,
      • "humidity":0,
      • "weather":[
        1. {
          • "id":600,
          • "main":"Snow",
          • "description":"light snow",
          • "icon":"13d"
          }
        ]
        ,
      • "speed":12.83,
      • "deg":208,
      • "clouds":93,
      • "rain":3.44,
      • "snow":0.22
      }
      ,
    7. {
      • "dt":1417712400,
      • "temp":{
        • "day":272.6,
        • "min":271.5,
        • "max":272.6,
        • "night":272.49,
        • "eve":272.56,
        • "morn":271.5
        }
        ,
      • "pressure":1003.13,
      • "humidity":0,
      • "weather":[
        1. {
          • "id":601,
          • "main":"Snow",
          • "description":"snow",
          • "icon":"13d"
          }
        ]
        ,
      • "speed":13.59,
      • "deg":285,
      • "clouds":47,
      • "snow":4.52
      }
    ]
}

With the data parsed using the handy online tool, it is very easy to visualize how it is structured and how we can read the data inside.

For example on the forecast:
     "list":[

  1. {
    • "dt":1417194000,
    • "temp":{
      • "min":268.92,
      • "max":270.14,
      • },
    • "weather":[
      1. {
        • "id":601,
        • "main":"Snow",
        • "description":"snow",
        • "icon":"13d"
        }
      ]
      ,

To retrieve the dt value for the first day, our JavaScript code will be: json.list[0].dt
To retrieve it for the second day: json.list[1].dt

Min/max temperature is in a group within the primary list.
To retrieve the minimum temperature for the first day: json.list[0].temp.min
To retrieve it for the second day: json.list[1].temp.min

Finally the weather grouping is also an array (note the []s); it's just an array of 1 entry. To get a value from there, we give an index.
To retrieve the "main" weather description for the first day: json.list[0].weather[0].main
To retrieve it for the second day: json.list[1].weather[0].main

Current Weather Conditions

Let's look at the simpler case first, the Current Weather conditions.

The JavaScript code for the query: (Some data has been commented out because I'm not using it in the watch face; however, it may be of value to you.)
  // Construct URL
  var url = "http://api.openweathermap.org/data/2.5/weather?lat=" +
      pos.coords.latitude + "&lon=" + pos.coords.longitude;

  // Send request to OpenWeatherMap
  xhrRequest(url, 'GET', 
    function(responseText) {
      // responseText contains a JSON object with weather info
      var json = JSON.parse(responseText);

      // Temperature in Kelvin requires adjustment
      var temperature = Math.round(json.main.temp - 273.15);

      // Conditions
      //var conditions = json.weather[0].main;      
      
      // Temperature Min
      //var temperatureMin = Math.round(json.main.temp_min - 273.15);
      
      // Temperature Min
      //var temperatureMax = Math.round(json.main.temp_max - 273.15);
      
      // Wind Speed
      var windSpeed = Math.round(json.wind.speed);
      
      // Wind Direction
      var windDirection = Math.round(json.wind.deg);
      
      // Humidity
      //var humidity = Math.round(json.main.humidity);
      
      // Description
      var description = json.weather[0].description;      
      
      // Assemble dictionary using our keys
      var dictionary = {
        "KEY_TEMPERATURE": temperature,
        "KEY_WIND_SPEED": windSpeed,
        "KEY_WIND_DIRECTION": windDirection,
        "KEY_DESCRIPTION": description
      };
//        "KEY_TEMP_MIN": temperatureMin,
//        "KEY_TEMP_MAX": temperatureMax,
//        "KEY_CONDITIONS": conditions,
//        "KEY_HUMIDITY": humidity,

      // Send to Pebble
      Pebble.sendAppMessage(dictionary,
        function(e) {
          //console.log("Weather info sent to Pebble successfully WX!");
        },
        function(e) {
          //console.log("Error sending weather info to Pebble WX!");
        }
      );
    }      
  );

What happens is the JavaScript code is sending out a query for the weather conditions and getting the response in the JSON format. The JavaScript code then pulls out the values it cares about, associates them with keys I have defined, and builds a dictionary array that is sent to the Pebble C code.

As you can see, JSON returns a wealth of data, but I'm only using the current temperature, wind speed, wind direction, and description.

After some research, I found out the current low and high temperatures returned from the current conditions query aren't actually from forecast data; they're formulated by other conditions throughout the day. The values are not reliable and give poor results. To get good values for the min and max temperature of the day, you must make a separate query for the forecast conditions.

The "description" from JSON is far more descriptive than "main". In this example, the "main" is "Snow" but the description is "light snow". I have room on my text layer so I want to be as descriptive as I can so I went with the "description".

I'm using CloudPebble for my watch face development. Under the Settings section of CloudPebble, I have Message Keys that correspond with the variable keys you see above:
KEY_TEMPERATURE = 0
KEY_CONDITIONS = 1
KEY_TEMP_MIN = 2
KEY_TEMP_MAX = 3
KEY_WIND_SPEED = 4
KEY_WIND_DIRECTION = 5
KEY_HUMIDITY = 6
KEY_DESCRIPTION = 7

The corresponding keys are also #defined in the C code:
#define KEY_TEMPERATURE 0
#define KEY_CONDITIONS 1
#define KEY_TEMP_MIN 2
#define KEY_TEMP_MAX 3
#define KEY_WIND_SPEED 4
#define KEY_WIND_DIRECTION 5
#define KEY_HUMIDITY 6
#define KEY_DESCRIPTION 7

There are existing tutorials on how to setup inbox callbacks with the JavaScript code, so I'll mostly gloss over those details.

In the init(), the inbox_received_callback has been setup to receive the call from the JavaScript code:
  app_message_register_inbox_received(inbox_received_callback);

The inbox_received_callback itself:
static void inbox_received_callback(DictionaryIterator *iterator, void *context)
{
  ...
  // Read first item  Tuple *t = dict_read_first(iterator);

  // For all items
  while(t != NULL)
  {
    // Which key was received?
    switch(t->key)
    {
      case KEY_TEMPERATURE:
        currentTemperature_c = t->value->int32;
        break;
//      case KEY_CONDITIONS:
//        // Current conditions (abbreviated).
//        break;
//      case KEY_TEMP_MIN:
//        // Not reliably low temperature.
//        break;
//      case KEY_TEMP_MAX:
//        // Not reliably high temperature.
//        break;
      case KEY_WIND_SPEED:
        // Reported in meters per second, convert to knots.
        currentWindSpeed_metersPerSecond = t->value->int32;
        break;
      case KEY_WIND_DIRECTION:
        currentWindDirection_deg = t->value->int32;
        break;
//      case KEY_HUMIDITY:
//        break;
      case KEY_DESCRIPTION:
        // Similar to conditions, but far more descriptive.
        strncpy(currentConditions, t->value->cstring, 32);
        break;
      ...
      default:
        APP_LOG(APP_LOG_LEVEL_ERROR, "Key %d not recognized!", (int)t->key);
        break;
    }

    // Look for next item
    t = dict_read_next(iterator);
  }
  ...
  update_weather();
}

In the above code, you can see how the keys that were in the JavaScript code, included in the CloudPebble Settings, and #defined at the top of the C code, are now fully retrieved.

currentTemperature_c, currentWindSpeed_metersPerSecond,currentWindDirection_deg, and currentConditions are defined as global variables at the top of the C code:
static int currentTemperature_c;
static char currentConditions[32];
static int currentWindDirection_deg;
static int currentWindSpeed_metersPerSecond;

The update_weather() function takes those values and updates the text layers on the watch face.

Forecast Weather Conditions

Now for the trickier one, the forecast conditions. The forecast includes 7 days worth of weather data. You can't assume the first weather condition is today, the second weather condition is tomorrow, etc. You must read the date to determine which day the forecast belongs to. Sometimes the first entry of the retrieved data is not in fact today's forecast, it is yesterday's! To be able to get today's and tomorrow's forecast, I have to pull 3 values from the forecast JSON.

Each day's conditions includes a "dt" value which is the date/time of that forecast in UNIX format. We'll be reading that "dt" value to determine what day it belongs to.

For reference, I found a handy UNIX time calculator here: http://www.onlineconversion.com/unix_time.htm Just give it in the "dt" value and it will reply with a more human-friendly date and time.

The JavaScript code:
  // Construct URL
  var forecasturl = "http://api.openweathermap.org/data/2.5/forecast/daily?lat=" +
      pos.coords.latitude + "&lon=" + pos.coords.longitude;

  // Send request to OpenWeatherMap
  xhrRequest(forecasturl, 'GET', 
    function(responseForecastText) {
      // responseText contains a JSON object with weather info
      var json = JSON.parse(responseForecastText);

      var day1Time = json.list[0].dt;
      
      // Conditions
      var day1Conditions = json.list[0].weather[0].main;      
      
      // Temperature in Kelvin requires adjustment
      var day1TemperatureMin = Math.round(json.list[0].temp.min - 273.15);

      // Temperature in Kelvin requires adjustment
      var day1TemperatureMax = Math.round(json.list[0].temp.max - 273.15);

      var day2Time = json.list[1].dt;
      
      // Conditions
      var day2Conditions = json.list[1].weather[0].main;      
           
      // Temperature in Kelvin requires adjustment
      var day2TemperatureMin = Math.round(json.list[1].temp.min - 273.15);

      // Temperature in Kelvin requires adjustment
      var day2TemperatureMax = Math.round(json.list[1].temp.max - 273.15);

      var day3Time = json.list[2].dt;
      
      // Conditions
      var day3Conditions = json.list[2].weather[0].main;      
           
      // Temperature in Kelvin requires adjustment
      var day3TemperatureMin = Math.round(json.list[2].temp.min - 273.15);

      // Temperature in Kelvin requires adjustment
      var day3TemperatureMax = Math.round(json.list[2].temp.max - 273.15);

      // Assemble dictionary using our keys
      var dictionary = {
        "KEY_DAY1_TIME": day1Time,
        "KEY_DAY1_CONDITIONS": day1Conditions,
        "KEY_DAY1_TEMP_MIN": day1TemperatureMin,
        "KEY_DAY1_TEMP_MAX": day1TemperatureMax,
        "KEY_DAY2_TIME": day2Time,
        "KEY_DAY2_CONDITIONS": day2Conditions,
        "KEY_DAY2_TEMP_MIN": day2TemperatureMin,
        "KEY_DAY2_TEMP_MAX": day2TemperatureMax,
        "KEY_DAY3_TIME": day3Time,
        "KEY_DAY3_CONDITIONS": day3Conditions,
        "KEY_DAY3_TEMP_MIN": day3TemperatureMin,
        "KEY_DAY3_TEMP_MAX": day3TemperatureMax
      };

      // Send to Pebble
      Pebble.sendAppMessage(dictionary,
        function(e) {
          //console.log("Weather info sent to Pebble successfully WX!");
        },
        function(e) {
          //console.log("Error sending weather info to Pebble WX!");
        }
      );
    }      
  );

The keys for CloudPebble:
KEY_DAY1_CONDITIONS = 8
KEY_DAY1_TEMP_MIN = 9
KEY_DAY1_TEMP_MAX = 10
KEY_DAY1_TIME = 11
KEY_DAY2_CONDITIONS = 12
KEY_DAY2_TEMP_MIN = 13
KEY_DAY2_TEMP_MAX = 14
KEY_DAY2_TIME = 15
KEY_DAY3_CONDITIONS = 16
KEY_DAY3_TEMP_MIN = 17
KEY_DAY3_TEMP_MAX = 18
KEY_DAY3_TIME = 19

The corresponding keys #defined in the C code:
#define KEY_DAY1_CONDITIONS 8
#define KEY_DAY1_TEMP_MIN 9
#define KEY_DAY1_TEMP_MAX 10
#define KEY_DAY1_TIME 11
#define KEY_DAY2_CONDITIONS 12
#define KEY_DAY2_TEMP_MIN 13
#define KEY_DAY2_TEMP_MAX 14
#define KEY_DAY2_TIME 15
#define KEY_DAY3_CONDITIONS 16
#define KEY_DAY3_TEMP_MIN 17
#define KEY_DAY3_TEMP_MAX 18
#define KEY_DAY3_TIME 19

And now the C code:
static void inbox_received_callback(DictionaryIterator *iterator, void *context)
{
  ...
  // Read first item
  Tuple *t = dict_read_first(iterator);

  int day1Date = 0;
  int day2Date = 0;
  //int day3Date = 0;
  int day1LowTemperature_c = 0;
  int day2LowTemperature_c = 0;
  int day3LowTemperature_c = 0;
  int day1HighTemperature_c = 0;
  int day2HighTemperature_c = 0;
  int day3HighTemperature_c = 0;
  char day1Conditions[32];
  char day2Conditions[32];
  char day3Conditions[32];

  // For all items
  while(t != NULL)
  {
    // Which key was received?
    switch(t->key)
    {
      ...
      case KEY_DAY1_TIME:
        // Usually today's date, but in the morning it's yesterday's!
        day1Date = t->value->int32;
        break;
      case KEY_DAY1_CONDITIONS:
        // Today's condition (abbreviated).
        //snprintf(day1_conditions_buffer, sizeof(day1_conditions_buffer), "%s", t->value->cstring);
        strncpy(day1Conditions, t->value->cstring, 32);
        break;
      case KEY_DAY1_TEMP_MIN:
        //currentLowTemperature_c = t->value->int32;
        day1LowTemperature_c = t->value->int32;
        break;
      case KEY_DAY1_TEMP_MAX:
        //currentHighTemperature_c = t->value->int32;
        day1HighTemperature_c = t->value->int32;
        break;
      case KEY_DAY2_TIME:
        // Usually it's tomorrow's date, but in the morning it's today's!.
        //forecastDate = t->value->int32;
        day2Date = t->value->int32;
        break;
      case KEY_DAY2_CONDITIONS:
        // Forecast condition (abbreviated).
        //strncpy(forecastConditions, t->value->cstring, 32);
        strncpy(day2Conditions, t->value->cstring, 32);
        break;
      case KEY_DAY2_TEMP_MIN:
        //forecastLowTemperature_c = t->value->int32;
        day2LowTemperature_c = t->value->int32;
        break;
      case KEY_DAY2_TEMP_MAX:
        //forecastHighTemperature_c = t->value->int32;
        day2HighTemperature_c = t->value->int32;
        break;
      case KEY_DAY3_TIME:
        // Usually it's in two days, but in the morning it's tomorrow's!
        //day3Date = t->value->int32;
        break;
      case KEY_DAY3_CONDITIONS:
        // Forecast condition (abbreviated).
        strncpy(day3Conditions, t->value->cstring, 32);
        break;
      case KEY_DAY3_TEMP_MIN:
        //day3LowTemperature_c = t->value->int32;
        day3LowTemperature_c = t->value->int32;
        break;
      case KEY_DAY3_TEMP_MAX:
        day3HighTemperature_c = t->value->int32;
        break;
        ...
      default:
        APP_LOG(APP_LOG_LEVEL_ERROR, "Key %d not recognized!", (int)t->key);
        break;
    }

    // Look for next item
    t = dict_read_next(iterator);
  }

  if (day1Date > 0)
  {
    // Forecast Response
    time_t currentTime = time(NULL);
    struct tm *currentCalendarTime = localtime(&currentTime);
    int dayOfMonthCurrent = currentCalendarTime->tm_mday;
    
    time_t day1Date_t = day1Date;
    struct tm *day1CalendarTime = localtime(&day1Date_t);
    int dayOfMonth1 = day1CalendarTime->tm_mday;
    
    time_t day2Date_t = day2Date;
    struct tm *day2CalendarTime = localtime(&day2Date_t);
    int dayOfMonth2 = day2CalendarTime->tm_mday;
    
    if (dayOfMonthCurrent == dayOfMonth1)
    {
      // Day 1 is Today's Date
      currentDate = day1Date;
      strncpy(currentDayForecastConditions, day1Conditions, 32);
      currentLowTemperature_c = day1LowTemperature_c;
      currentHighTemperature_c = day1HighTemperature_c;
      
      // So Day 2 will be the forecast.
      strncpy(forecastConditions, day2Conditions, 32);
      forecastLowTemperature_c = day2LowTemperature_c;
      forecastHighTemperature_c = day2HighTemperature_c;
    }
    else if (dayOfMonthCurrent == dayOfMonth2)
    {
      // Day 2 is Today's Date 
      currentDate = day2Date;
      strncpy(currentDayForecastConditions, day2Conditions, 32);
      currentLowTemperature_c = day2LowTemperature_c;
      currentHighTemperature_c = day2HighTemperature_c;

      // So Day 3 will be the forecast.
      strncpy(forecastConditions, day3Conditions, 32);
      forecastLowTemperature_c = day3LowTemperature_c;
      forecastHighTemperature_c = day3HighTemperature_c;
    }
  } // (day1Date > 0)

  ...
  update_weather();
}

The key piece above is determining if the first forecast condition is today's date or yesterday's.

I get the current day of the month:
    time_t currentTime = time(NULL);
    struct tm *currentCalendarTime = localtime(&currentTime);
    int dayOfMonthCurrent = currentCalendarTime->tm_mday;

the first forecast day of the month:    
    time_t day1Date_t = day1Date;
    struct tm *day1CalendarTime = localtime(&day1Date_t);
    int dayOfMonth1 = day1CalendarTime->tm_mday;

and the second forecast day of the month:
    time_t day2Date_t = day2Date;
    struct tm *day2CalendarTime = localtime(&day2Date_t);
    int dayOfMonth2 = day2CalendarTime->tm_mday;

If my current day of the month equals the first forecast day:
    if (dayOfMonthCurrent == dayOfMonth1)
    {
Then I know the first value from the JSON data is today's forecast (and the second value is tomorrow's):
      // Day 1 is Today's Date
      currentDate = day1Date;
      strncpy(currentDayForecastConditions, day1Conditions, 32);
      currentLowTemperature_c = day1LowTemperature_c;
      currentHighTemperature_c = day1HighTemperature_c;
      
      // So Day 2 will be the forecast.
      strncpy(forecastConditions, day2Conditions, 32);
      forecastLowTemperature_c = day2LowTemperature_c;
      forecastHighTemperature_c = day2HighTemperature_c;

Else if my current day of the month equals the second forecast day:
    else if (dayOfMonthCurrent == dayOfMonth2)
    {
Then I know the second value from the JSON data is today's forecast (and the third value is tomorrow's):
      // Day 2 is Today's Date 
      currentDate = day2Date;
      strncpy(currentDayForecastConditions, day2Conditions, 32);
      currentLowTemperature_c = day2LowTemperature_c;
      currentHighTemperature_c = day2HighTemperature_c;

      // So Day 3 will be the forecast.
      strncpy(forecastConditions, day3Conditions, 32);
      forecastLowTemperature_c = day3LowTemperature_c;
      forecastHighTemperature_c = day3HighTemperature_c;

Please refer to the first Pebble Watchface post for the full source code. Hopefully this has aided you in parsing JSON / weather data!

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!


Thursday, November 27, 2014

Pebble Watchface: Eliminating Floating Point Math

The Pebble watch hardware can't actually do floating point calculations (numbers with decimals). Those calculations are emulated in software. As a result, if you do even a single floating point operation in your Pebble app, the compiler automatically includes additional library code to be able to process them.

I was doing floating point calculations for my temperature and wind speed conversions; the size of my Pebble app was: 11531 bytes
When I changed those to be integer calculations only, the size of my app reduced to: 8955 bytes

So I got an immediate savings of 2576 bytes of space by eliminating floating point operations. (That's essentially a rounding error for a desktop computer, but it's significant for an embedded device / smart watch.)

My original conversion code:

static float getPreferedWindSpeed(float windSpeed_metersPerSecond)
{
  switch(windSpeedUnits)
  {
    case WINDSPEED_UNITS_KNOTS:
      return (windSpeed_metersPerSecond * 1.94384);
    case WINDSPEED_UNITS_MPH:
      return (windSpeed_metersPerSecond * 2.23694);
    case WINDSPEED_UNITS_KPH:
      return (windSpeed_metersPerSecond * 3.6);
  }
  return windSpeed_metersPerSecond;
}


To convert those floating point multiplications to integer calculations, I need to use integer numbers only and perform 2 calculations: first multiply the value then divide the magnitude.

So for MPH conversion, instead of * 2.23694, I'll split that into: * 223694 / 100000

It's important my first calculation is the multiplication, as doing the divide first will round out the number to 0.

My new conversion code:

static int getPreferedWindSpeed(int windSpeed_metersPerSecond)
{
  switch(windSpeedUnits)
  {
    case WINDSPEED_UNITS_KNOTS:
      return (windSpeed_metersPerSecond * 194384 / 100000);
    case WINDSPEED_UNITS_MPH:
      return (windSpeed_metersPerSecond * 223694 / 100000);
    case WINDSPEED_UNITS_KPH:
      return (windSpeed_metersPerSecond * 36 / 10);
  }
  return windSpeed_metersPerSecond;
}

A fairly trivial code change! (At least for wind speeds.) Once I did this for all calculations the size of my compiled watch face automatically went down.

One thing to keep in mind is that the maximum size of a Signed Integer is limited to 2^31 - 1 or 2147483647. We don't want to exceed that in our calculations.

Let's look at the worst case conversion which happens when getting MPH. To get the maximum wind speed we can safely calculate, we'll take the maximum size of an integer and divide that by the multiplication operation: 2147483647 / 223694 = 9600

As long as my wind speed is below 9600 meters/second, I'm going to remain within the limits of an Integer value. And considering that's faster than a spacecraft in low Earth orbit, I think I'm ok.

Say like you needed to calculate larger numbers - you can decrease the precision by an order of magnitude to get an order of magnitude increase in maximum size. For example use: 22369 / 10000, which would allow 96000 meters/second.

If you needed to do floating point division, then just multiply by the magnitude first (100000) then divide by the value (223694).

Side note, be sure to NOT include any decimals; do not do 100000.0! Otherwise, the compiler will interpret that as a float and will load the floating point library.

Saturday, November 22, 2014

Pebble Watchface

UPDATE: Complete source code is now available at:
http://kamoly.space/projects/pebble/all_info_as_text_watch_face/

About a month ago I purchased a Pebble smart watch. A few reasons I went with the Pebble:
1. Highly rated! It doesn't have color, a touchscreen, or sound, but all the reviews said what it does it does very very well.
2. Price just dropped to $99. It was hard for me to justify spending $150 on what may just be a new engineering toy, but I can do $100 for a new engineering toy (that also shows the time!).
3. Awesome software development kit! They have a web based programming environment called Cloud Pebble. You don't have to install anything on your computer. It's all done via a web browser that updates directly to your watch with a single click.
4. Pebble App Store where you can browse for apps and post your own.

They have many great watch faces that people have made available for download. Some were very close to what I wanted but not quite perfect. I basically wanted to combine 3 different watch faces for my ideal one. With the great SDK, I went to work to do that.

My watch face is available in the app store; it's called...

All Info As Text


I wanted a few key features:
- Seconds (Many watch faces don't include seconds because it decreases battery life.)
- Current Weather Conditions
- Today's & Tomorrow's Forecast
- 2 Week Calendar
- Indication of no data connection. (Different from no Bluetooth Link; you can have Bluetooth but no Internet.)
- Remember settings when data was unavailable. (If I navigate off the app then back on, I wanted it to remember the last weather conditions it had just in case I no longer had an Internet connection).

I received some requests to support a different calendar format (MTWTFSS rather than SMTWTFS), different temperature/windspeed units, and showing the week number. I created a configuration page to support those options.


In the next few posts I'll describe the methodology behind the watch face and how specific features were implemented.

Pebble discussion board for the app:
http://forums.getpebble.com/discussion/17838/watch-face-all-info-as-text

The complete source code is available on GitHub:
https://github.com/clintka/AllInfoAsText.git

The configuration page (that is opened in the Pebble phone app):
http://clintka.github.io/AllInfoAsText/index.html

I've also posted the complete source code below for convenience:

Cloud Pebble Message Keys

KEY_TEMPERATURE 0
KEY_CONDITIONS 1
KEY_TEMP_MIN 2
KEY_TEMP_MAX 3
KEY_WIND_SPEED 4
KEY_WIND_DIRECTION 5
KEY_HUMIDITY 6
KEY_DESCRIPTION 7
KEY_DAY1_CONDITIONS 8
KEY_DAY1_TEMP_MIN 9
KEY_DAY1_TEMP_MAX 10
KEY_DAY1_TIME 11
KEY_DAY2_CONDITIONS 12
KEY_DAY2_TEMP_MIN 13
KEY_DAY2_TEMP_MAX 14
KEY_DAY2_TIME 15
KEY_DAY3_CONDITIONS 16
KEY_DAY3_TEMP_MIN 17
KEY_DAY3_TEMP_MAX 18
KEY_DAY3_TIME 19
CONFIG_KEY_TEMPERATURE_UNITS 50
CONFIG_KEY_WINDSPEED_UNITS 51
CONFIG_KEY_WEEKNUMBER_ENABLED 52
CONFIG_KEY_MONDAY_FIRST 53

C Code

main.c

#include <pebble.h>

// Keys to link Javascript code to C code.
#define KEY_TEMPERATURE 0
#define KEY_CONDITIONS 1
#define KEY_TEMP_MIN 2
#define KEY_TEMP_MAX 3
#define KEY_WIND_SPEED 4
#define KEY_WIND_DIRECTION 5
#define KEY_HUMIDITY 6
#define KEY_DESCRIPTION 7
#define KEY_DAY1_CONDITIONS 8
#define KEY_DAY1_TEMP_MIN 9
#define KEY_DAY1_TEMP_MAX 10
#define KEY_DAY1_TIME 11
#define KEY_DAY2_CONDITIONS 12
#define KEY_DAY2_TEMP_MIN 13
#define KEY_DAY2_TEMP_MAX 14
#define KEY_DAY2_TIME 15
#define KEY_DAY3_CONDITIONS 16
#define KEY_DAY3_TEMP_MIN 17
#define KEY_DAY3_TEMP_MAX 18
#define KEY_DAY3_TIME 19

// Keys for configuration.
#define CONFIG_KEY_TEMPERATURE_UNITS 50
#define CONFIG_KEY_WINDSPEED_UNITS 51
#define CONFIG_KEY_WEEKNUMBER_ENABLED 52
#define CONFIG_KEY_MONDAY_FIRST 53

// Keys to reference persistent storage.
#define STORAGE_KEY_CURRENT_TEMPERATURE_C 100
#define STORAGE_KEY_CURRENT_CONDITIONS 101
#define STORAGE_KEY_CURRENT_LOW_C 102
#define STORAGE_KEY_CURRENT_HIGH_C 103
#define STORAGE_KEY_CURRENT_WIND_DIR_DEG 104
#define STORAGE_KEY_CURRENT_WIND_SPD_METERSPERSECOND 105
#define STORAGE_KEY_CURRENT_DAY 106
#define STORAGE_KEY_FORECAST_LOW_C 107
#define STORAGE_KEY_FORECAST_HIGH_C 108
#define STORAGE_KEY_FORECAST_CONDITIONS 109
#define STORAGE_KEY_CURRENT_DAY_FORECAST_CONDITIONS 110
#define STORAGE_KEY_TEMPERATURE_UNITS 111
#define STORAGE_KEY_WINDSPEED_UNITS 112
#define STORAGE_KEY_WEEKNUMBER_ENABLED 113
#define STORAGE_KEY_MONDAY_FIRST 114

// Durations for updates and time outs. Set as desired.
#define NUMBER_OF_SECONDS_BETWEEN_WEATHER_UPDATES 1800
#define NUMBER_OF_SECONDS_UNTIL_DATA_CONSIDERED_LOST 60
#define NUMBER_OF_SECONDS_TO_SHOW_SECONDS_AFTER_TAP 180

// Constants for Settings
#define TEMPERATURE_UNITS_F 0
#define TEMPERATURE_UNITS_C 1
#define WINDSPEED_UNITS_KNOTS 0
#define WINDSPEED_UNITS_MPH 1
#define WINDSPEED_UNITS_KPH 2
#define FALSE 0
#define TRUE 1

// Persistent storage variables (must be global).
static int currentTemperature_c;
static char currentConditions[32];
static char currentDayForecastConditions[32];
static int currentLowTemperature_c;
static int currentHighTemperature_c;
static int currentWindDirection_deg;
static int currentWindSpeed_metersPerSecond;
static int currentDate;
static int forecastLowTemperature_c;
static int forecastHighTemperature_c;
static char forecastConditions[32];
static int temperatureUnits; // 0 = F, 1 = C
static int windSpeedUnits; // 0 = KNOTS, 1 = MPH, 2 = KPH
static int weekNumberEnabled; // 0 = FALSE, 1 = TRUE
static int mondayFirst; // 0 = FALSE, 1 = TRUE

// Status variables.
bool isShowingSeconds = false;
bool connectedToBluetooth = false;
bool connectedToData = false;
time_t timeOfLastDataResponse = 0;
time_t timeOfLastDataRequest = 0;
time_t timeOfLastTap = 0;
int lastCalendarDateUpdatedTo = -1;

// Watch layers.
static Window *s_main_window;
static TextLayer *s_battery_layer;
static TextLayer *s_linkStatus_layer;
static TextLayer *s_time_layer;
static TextLayer *s_time_am_pm_layer;
static TextLayer *s_time_seconds_layer;
static TextLayer *s_date_layer;
static TextLayer *s_weather_current_layer;
static TextLayer *s_weather_label1_layer;
static TextLayer *s_weather_forecast1_layer;
static TextLayer *s_weather_label2_layer;
static TextLayer *s_weather_forecast2_layer;
static TextLayer *s_calendarDay_layer[14];

static int getFahrenheitFromCelsius(int temp_celsius)
{
  // Use only whole numbers in math operations. The Pebble watch
  // hardware does not support floats, and floating operations
  // are expensive to emulate (processing and memory).
  return ((temp_celsius * 9 / 5) + 32);
}

static int getPreferedWindSpeed(int windSpeed_metersPerSecond)
{
  // Use only whole numbers in math operations. The Pebble watch
  // hardware does not support floats, and floating operations
  // are expensive to emulate (processing and memory).
  switch(windSpeedUnits)
  {
    case WINDSPEED_UNITS_KNOTS:
      return (windSpeed_metersPerSecond * 194384 / 100000); // * 1.94384
    case WINDSPEED_UNITS_MPH:
      return (windSpeed_metersPerSecond * 223694 / 100000); // * 2.23694
    case WINDSPEED_UNITS_KPH:
      return (windSpeed_metersPerSecond * 36 / 10); // * 3.6
  }

  // Invalid windspeed units.
  return windSpeed_metersPerSecond;
}

static void update_link_label()
{
  static char bluetoothBuffer[8];

  if (connectedToBluetooth)
  {
    if (connectedToData)
    {
      // Connection is good!
      bluetoothBuffer[0] = 0;
    }
    else
    {
      // Bluetooth Connection, but failed to get Data.
      strncpy(bluetoothBuffer, "No Data", 7);
    }
  }
  else
  {
    // No Bluetooth Connection
    strncpy(bluetoothBuffer, "No Link", 7);
  }
  text_layer_set_text(s_linkStatus_layer, bluetoothBuffer);
}

static void update_time(struct tm *tick_time)
{
  static char timeBuffer[6]; // = "24:00";
  static char timeAmPmBuffer[3]; // = "am";
  static char timeSecondsBuffer[3];

  // Update the Time
  if (clock_is_24h_style())
  {
    strftime(timeBuffer, sizeof("00:00"), "%H:%M", tick_time);
    text_layer_set_text(s_time_layer, timeBuffer);

    // If using 24 hours, then don't draw AM/PM and seconds.
    // (At 2400 time, it will run across the AM/PM and seconds).
    timeSecondsBuffer[0] = 0;
    text_layer_set_text(s_time_seconds_layer, timeSecondsBuffer);
    timeAmPmBuffer[0] = 0;
    text_layer_set_text(s_time_am_pm_layer, timeAmPmBuffer);
  }
  else
  {
    strftime(timeBuffer, sizeof("00:00"), "%l:%M", tick_time);
    text_layer_set_text(s_time_layer, timeBuffer);

    // Update the seconds field.
    if (weekNumberEnabled == TRUE)
    {
      // Show Week Number in place of the seconds field.
      if (mondayFirst == TRUE)
      {
        strftime(timeSecondsBuffer, sizeof("00"), "%W", tick_time);
        text_layer_set_text(s_time_seconds_layer, timeSecondsBuffer);
      }
      else
      {
        strftime(timeSecondsBuffer, sizeof("00"), "%U", tick_time);
        text_layer_set_text(s_time_seconds_layer, timeSecondsBuffer);
      }
    }
    else 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);
    }
 
    if (tick_time->tm_hour < 12)
    {
      strncpy(timeAmPmBuffer, "AM", 2);
    }
    else
    {
      strncpy(timeAmPmBuffer, "PM", 2);
    }
    text_layer_set_text(s_time_am_pm_layer, timeAmPmBuffer);
  }
}

static void update_date(struct tm *tick_time)
{
  static char dateBuffer[20];
  static char calendarDayBuffer[14][3];

  // Keep track of the last date we updated to so we only have to
  // update when it changes.
  lastCalendarDateUpdatedTo = tick_time->tm_mday;

  // Update the Date
  strftime(dateBuffer, sizeof(dateBuffer), "%A, %b %e", tick_time);
  text_layer_set_text(s_date_layer, dateBuffer);

  // Update the Calendar
  char dayOfWeekString[2];
  strftime(dayOfWeekString, sizeof(dayOfWeekString), "%w", tick_time);
  int dayOfWeek = atoi(dayOfWeekString);
  if (mondayFirst == TRUE)
  {
    // %w reports Sunday first, subtract by 1 to have Monday first.
    dayOfWeek = dayOfWeek - 1;
  }

  // Subtract back to Sunday of this week.
  time_t calendarDate = time(NULL) - (86400 * dayOfWeek);

  if ((mondayFirst == TRUE) && (dayOfWeek == -1))
  {
    // If configured for Monday first and we're on Sunday, then
    // we need to wrap around to the end of the week for it to be
    // displayed properly.
    calendarDate -= (86400 * 7);
    dayOfWeek = 6;
  }

  struct tm *calendarDate_time = localtime(&calendarDate);

  // Update labels for the next 2 weeks.
  for (int dayLoop = 0; dayLoop < 14; dayLoop++)
  {
    // If we're on our current day of the week, then bold it.
    if (dayLoop == dayOfWeek)
    {
      text_layer_set_font(s_calendarDay_layer[dayLoop], fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD));
    }
    else
    {
      text_layer_set_font(s_calendarDay_layer[dayLoop], fonts_get_system_font(FONT_KEY_GOTHIC_18));
    }
    strftime(calendarDayBuffer[dayLoop], 3, "%e", calendarDate_time);
    text_layer_set_text(s_calendarDay_layer[dayLoop], calendarDayBuffer[dayLoop]);

    calendarDate += 86400;
    calendarDate_time = localtime(&calendarDate);
  }
}

static void update_weather()
{
  static char current_weather_layer_buffer[64];
  static char day1_label_layer_buffer[8];
  static char day1_layer_buffer[64];
  static char day2_label_layer_buffer[8];
  static char day2_layer_buffer[64];

  // Update Current Weather Condition
  char currentTemperatureString[10];
  switch(temperatureUnits)
  {
    case TEMPERATURE_UNITS_F:
      snprintf(currentTemperatureString, sizeof(currentTemperatureString), "%dF",
          getFahrenheitFromCelsius(currentTemperature_c));  
      break;
    case TEMPERATURE_UNITS_C:
      snprintf(currentTemperatureString, sizeof(currentTemperatureString), "%dC",
          currentTemperature_c);  
      break;
  }
  char windDirectionString[3];
  if (currentWindDirection_deg >= 337.5 || currentWindDirection_deg <= 22.5)
  {
    strcpy(windDirectionString, "N");  
  }
  else if (currentWindDirection_deg < 67.5) // currentWindDirection_deg > 22.5 &&
  {
    strcpy(windDirectionString, "NE");  
  }
  else if (currentWindDirection_deg <= 112.5) // currentWindDirection_deg >= 67.5 &&
  {
    strcpy(windDirectionString, "E");
  }
  else if (currentWindDirection_deg < 157.5) // currentWindDirection_deg > 112.5 &&
  {
    strcpy(windDirectionString, "SE");
  }
  else if (currentWindDirection_deg <= 202.5) // currentWindDirection_deg >= 157.5 &&
  {
    strcpy(windDirectionString, "S");
  }
  else if (currentWindDirection_deg < 247.5) // currentWindDirection_deg > 202.5 &&
  {
    strcpy(windDirectionString, "SW");
  }
  else if (currentWindDirection_deg <= 292.5) // currentWindDirection_deg >= 247.5 &&
  {
    strcpy(windDirectionString, "W");
  }
  else if (currentWindDirection_deg < 337.5) // currentWindDirection_deg > 292.5 &&
  {
    strcpy(windDirectionString, "NW");
  }
  snprintf(current_weather_layer_buffer, sizeof(current_weather_layer_buffer), "%s %d%s %s",
          currentTemperatureString,
          getPreferedWindSpeed(currentWindSpeed_metersPerSecond),
          windDirectionString,
          currentConditions);
  text_layer_set_text(s_weather_current_layer, current_weather_layer_buffer);

  // Update Labels for which Forecast Day
  if (currentDate > 0)
  {
    time_t currentDate_t = currentDate;
    struct tm *currentCalendarTime = localtime(&currentDate_t);
    strftime(day1_label_layer_buffer, sizeof(day1_label_layer_buffer), "%a", currentCalendarTime);
    // Cut off the 3rd letter to show a 2 character day abbreviation. Just as clear and saves space.
    day1_label_layer_buffer[2] = 0;
    text_layer_set_text(s_weather_label1_layer, day1_label_layer_buffer);

    time_t forecastDate_t = currentDate_t + 86400;
    struct tm *forecastCalendarTime = localtime(&forecastDate_t);
    strftime(day2_label_layer_buffer, sizeof(day2_label_layer_buffer), "%a", forecastCalendarTime);
    // Cut off the 3rd letter to show a 2 character day abbreviation. Just as clear and saves space.
    day2_label_layer_buffer[2] = 0;
    text_layer_set_text(s_weather_label2_layer, day2_label_layer_buffer);
  }

  // Update Today's Weather Condition
  switch(temperatureUnits)
  {
    case TEMPERATURE_UNITS_F:
      snprintf(day1_layer_buffer, sizeof(day1_layer_buffer), "%d/%dF %s",
              getFahrenheitFromCelsius(currentLowTemperature_c), getFahrenheitFromCelsius(currentHighTemperature_c),
              currentDayForecastConditions);
      break;
    case TEMPERATURE_UNITS_C:
      snprintf(day1_layer_buffer, sizeof(day1_layer_buffer), "%d/%dC %s",
              currentLowTemperature_c, currentHighTemperature_c,
              currentDayForecastConditions);
      break;
  }
  text_layer_set_text(s_weather_forecast1_layer, day1_layer_buffer);

  // Update Tomorrow's Weather Condition
  switch(temperatureUnits)
  {
    case TEMPERATURE_UNITS_F:
      snprintf(day2_layer_buffer, sizeof(day2_layer_buffer), "%d/%dF %s",
               getFahrenheitFromCelsius(forecastLowTemperature_c), getFahrenheitFromCelsius(forecastHighTemperature_c),
               forecastConditions);
      break;
    case TEMPERATURE_UNITS_C:
      snprintf(day2_layer_buffer, sizeof(day2_layer_buffer), "%d/%dC %s",
               forecastLowTemperature_c, forecastHighTemperature_c,
               forecastConditions);
      break;
  }
  text_layer_set_text(s_weather_forecast2_layer, day2_layer_buffer);
}

static void update_battery_state(BatteryChargeState charge_state)
{
  static char batteryBuffer[8];
  uint8_t raw_percent = charge_state.charge_percent;

  snprintf(batteryBuffer, sizeof(batteryBuffer), " %i%%", (int)raw_percent);
  if (charge_state.is_charging)
  {
    strcat(batteryBuffer, "+");
  }
  //else if (charge_state.is_plugged)
  //{
  //  strcat(batteryBuffer, "*");
  //}
  text_layer_set_text(s_battery_layer, batteryBuffer);
}

static void update_bluetooth_state(bool bluetoothConnected)
{
  connectedToBluetooth = bluetoothConnected;
  update_link_label();
}

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();
}

static void create_calendar_layers()
{
  int calendarX = 0;
  int calendarY = 132;
  for (int dayLoop = 0; dayLoop < 14; dayLoop++)
  {
    if (mondayFirst)
    {
      // Monday through Sunday Layout
      // Have a gap between Friday/Saturday
      if (dayLoop == 5 || dayLoop == 12)
      {
        calendarX = 104;
      }
      else if (dayLoop == 6 || dayLoop == 13)
      {
        calendarX = 124;
      }
      else if (dayLoop < 5)
      {
        calendarX = dayLoop * 20;
      }
      else
      {
        calendarX = (dayLoop - 7) * 20;
      }
    }
    else
    {
      // Sunday through Saturday Layout
      // Have a gap between Sunday/Monday and Friday/Saturday
      if (dayLoop == 0 || dayLoop == 7)
      {
        calendarX = 0;
      }
      else if (dayLoop == 6 || dayLoop == 13)
      {
        calendarX = 124;
      }
      else if (dayLoop < 7)
      {
        calendarX = 2 + dayLoop * 20;
      }
      else
      {
        calendarX = 2 + (dayLoop - 7) * 20;
      }
    }

    if (dayLoop < 7)
    {
      calendarY = 132;
    }
    else
    {
      calendarY = 150;
    }
 
    s_calendarDay_layer[dayLoop] = text_layer_create(GRect(calendarX, calendarY, 20, 20));
    text_layer_set_background_color(s_calendarDay_layer[dayLoop], GColorBlack);
    text_layer_set_text_color(s_calendarDay_layer[dayLoop], GColorWhite);
    text_layer_set_font(s_calendarDay_layer[dayLoop], fonts_get_system_font(FONT_KEY_GOTHIC_18)); // FONT_KEY_GOTHIC_18
    text_layer_set_text_alignment(s_calendarDay_layer[dayLoop], GTextAlignmentCenter);
    text_layer_set_text(s_calendarDay_layer[dayLoop], "-");
    layer_add_child(window_get_root_layer(s_main_window), text_layer_get_layer(s_calendarDay_layer[dayLoop]));
    calendarX += 22;
  }
}

static void destroy_calendar_layers()
{
  for (int dayLoop = 0; dayLoop < 14; dayLoop++)
  {
    text_layer_destroy(s_calendarDay_layer[dayLoop]);
  }
}

static void main_window_load(Window *window)
{
  // Recover saved weather conditions and configuration options.
  // We will re-query weather, but this is important if we don't
  // have a data connection when the app reopens.
  if (persist_exists(STORAGE_KEY_CURRENT_TEMPERATURE_C))
  {
    currentTemperature_c = persist_read_int(STORAGE_KEY_CURRENT_TEMPERATURE_C);
  }
  else
  {
    currentTemperature_c = 0;
  }

  if (persist_exists(STORAGE_KEY_CURRENT_CONDITIONS))
  {
    persist_read_string(STORAGE_KEY_CURRENT_CONDITIONS, currentConditions, 32);
  }
  else
  {
    currentConditions[0] = 0;
  }

  if (persist_exists(STORAGE_KEY_CURRENT_DAY_FORECAST_CONDITIONS))
  {
    persist_read_string(STORAGE_KEY_CURRENT_DAY_FORECAST_CONDITIONS, currentDayForecastConditions, 32);
  }
  else
  {
    currentDayForecastConditions[0] = 0;
  }

  if (persist_exists(STORAGE_KEY_CURRENT_LOW_C))
  {
    currentLowTemperature_c = persist_read_int(STORAGE_KEY_CURRENT_LOW_C);
  }
  else
  {
    currentLowTemperature_c = 0;
  }

  if (persist_exists(STORAGE_KEY_CURRENT_HIGH_C))
  {
    currentHighTemperature_c = persist_read_int(STORAGE_KEY_CURRENT_HIGH_C);
  }
  else
  {
    currentHighTemperature_c = 0;
  }

  if (persist_exists(STORAGE_KEY_CURRENT_WIND_DIR_DEG))
  {
    currentWindDirection_deg = persist_read_int(STORAGE_KEY_CURRENT_WIND_DIR_DEG);
  }
  else
  {
    currentWindDirection_deg = 0;
  }

  if (persist_exists(STORAGE_KEY_CURRENT_WIND_SPD_METERSPERSECOND))
  {
    currentWindSpeed_metersPerSecond = persist_read_int(STORAGE_KEY_CURRENT_WIND_SPD_METERSPERSECOND);
  }
  else
  {
    currentWindSpeed_metersPerSecond = 0;
  }

  if (persist_exists(STORAGE_KEY_CURRENT_DAY))
  {
    currentDate = persist_read_int(STORAGE_KEY_CURRENT_DAY);
  }
  else
  {
    currentDate = 0;
  }

  if (persist_exists(STORAGE_KEY_FORECAST_LOW_C))
  {
    forecastLowTemperature_c = persist_read_int(STORAGE_KEY_FORECAST_LOW_C);
  }
  else
  {
    forecastLowTemperature_c = 0;
  }

  if (persist_exists(STORAGE_KEY_FORECAST_HIGH_C))
  {
    forecastHighTemperature_c = persist_read_int(STORAGE_KEY_FORECAST_HIGH_C);
  }
  else
  {
    forecastHighTemperature_c = 0;
  }

  if (persist_exists(STORAGE_KEY_FORECAST_CONDITIONS))
  {
    persist_read_string(STORAGE_KEY_FORECAST_CONDITIONS, forecastConditions, 32);
  }
  else
  {
    forecastConditions[0] = 0;
  }

  if (persist_exists(STORAGE_KEY_TEMPERATURE_UNITS))
  {
    temperatureUnits = persist_read_int(STORAGE_KEY_TEMPERATURE_UNITS);
  }
  else
  {
    temperatureUnits = TEMPERATURE_UNITS_F;
  }

  if (persist_exists(STORAGE_KEY_WINDSPEED_UNITS))
  {
    windSpeedUnits = persist_read_int(STORAGE_KEY_WINDSPEED_UNITS);
  }
  else
  {
    windSpeedUnits = WINDSPEED_UNITS_KNOTS;
  }

  if (persist_exists(STORAGE_KEY_WEEKNUMBER_ENABLED))
  {
    weekNumberEnabled = persist_read_int(STORAGE_KEY_WEEKNUMBER_ENABLED);
  }
  else
  {
    weekNumberEnabled = FALSE;
  }

  if (persist_exists(STORAGE_KEY_MONDAY_FIRST))
  {
    mondayFirst = persist_read_int(STORAGE_KEY_MONDAY_FIRST);
  }
  else
  {
    mondayFirst = FALSE;
  }

  // 144 wide
  // GRect: x position, y position, x size, y size

  // Create Battery TextLayer
  s_battery_layer = text_layer_create(GRect(0, 0, 50, 16));
  text_layer_set_background_color(s_battery_layer, GColorBlack);
  text_layer_set_text_color(s_battery_layer, GColorWhite);
  text_layer_set_font(s_battery_layer, fonts_get_system_font(FONT_KEY_GOTHIC_14_BOLD));
  text_layer_set_text_alignment(s_battery_layer, GTextAlignmentLeft);
  layer_add_child(window_get_root_layer(window), text_layer_get_layer(s_battery_layer));
 
  // Create Link Status TextLayer
  s_linkStatus_layer = text_layer_create(GRect(50, 0, 94, 16));
  text_layer_set_background_color(s_linkStatus_layer, GColorBlack);
  text_layer_set_text_color(s_linkStatus_layer, GColorWhite);
  text_layer_set_font(s_linkStatus_layer, fonts_get_system_font(FONT_KEY_GOTHIC_14_BOLD));
  text_layer_set_text_alignment(s_linkStatus_layer, GTextAlignmentLeft);
  layer_add_child(window_get_root_layer(window), text_layer_get_layer(s_linkStatus_layer));

  // Create Time TextLayer
  if (clock_is_24h_style())
  {
    s_time_layer = text_layer_create(GRect(0, 10, 125, 50));
  }
  else
  {
    s_time_layer = text_layer_create(GRect(0, 10, 118, 50));
  }
  text_layer_set_background_color(s_time_layer, GColorClear);
  text_layer_set_text_color(s_time_layer, GColorBlack);
  text_layer_set_font(s_time_layer, fonts_get_system_font(FONT_KEY_BITHAM_42_BOLD));
  text_layer_set_text_alignment(s_time_layer, GTextAlignmentRight);
  layer_add_child(window_get_root_layer(window), text_layer_get_layer(s_time_layer));

  // Create AM/PM TextLayer
  s_time_am_pm_layer = text_layer_create(GRect(120, 16, 24, 18));
  text_layer_set_background_color(s_time_am_pm_layer, GColorClear);
  text_layer_set_text_color(s_time_am_pm_layer, GColorBlack);
  text_layer_set_font(s_time_am_pm_layer, fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD));
  text_layer_set_text_alignment(s_time_am_pm_layer, GTextAlignmentLeft);
  layer_add_child(window_get_root_layer(window), text_layer_get_layer(s_time_am_pm_layer));

  // Create Seconds TextLayer
  s_time_seconds_layer = text_layer_create(GRect(120, 33, 24, 18));
  text_layer_set_background_color(s_time_seconds_layer, GColorClear);
  text_layer_set_text_color(s_time_seconds_layer, GColorBlack);
  text_layer_set_font(s_time_seconds_layer, fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD));
  text_layer_set_text_alignment(s_time_seconds_layer, GTextAlignmentLeft);
  layer_add_child(window_get_root_layer(window), text_layer_get_layer(s_time_seconds_layer));

  // Create Date TextLayer
  s_date_layer = text_layer_create(GRect(0, 48, 144, 28)); // 0, 43, 144, 28
  text_layer_set_background_color(s_date_layer, GColorClear);
  text_layer_set_text_color(s_date_layer, GColorBlack);
  text_layer_set_font(s_date_layer, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD));
  text_layer_set_text_alignment(s_date_layer, GTextAlignmentCenter);
  layer_add_child(window_get_root_layer(window), text_layer_get_layer(s_date_layer));

  // Create Current Weather Layer
  s_weather_current_layer = text_layer_create(GRect(2, 73, 142, 20)); // 0, 65, 144, 20
  text_layer_set_background_color(s_weather_current_layer, GColorClear);
  text_layer_set_text_color(s_weather_current_layer, GColorBlack);
  text_layer_set_font(s_weather_current_layer, fonts_get_system_font(FONT_KEY_GOTHIC_18));
  text_layer_set_text_alignment(s_weather_current_layer, GTextAlignmentLeft);
  text_layer_set_text(s_weather_current_layer, "");
  layer_add_child(window_get_root_layer(window), text_layer_get_layer(s_weather_current_layer));

  // Create Today's Forecast Label Layer
  s_weather_label1_layer = text_layer_create(GRect(2, 90, 25, 20)); // 0, 113, 35, 20
  text_layer_set_background_color(s_weather_label1_layer, GColorClear);
  text_layer_set_text_color(s_weather_label1_layer, GColorBlack);
  text_layer_set_font(s_weather_label1_layer, fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD));
  text_layer_set_text_alignment(s_weather_label1_layer, GTextAlignmentLeft);
  text_layer_set_text(s_weather_label1_layer, "");
  layer_add_child(window_get_root_layer(window), text_layer_get_layer(s_weather_label1_layer));

  // Create Today's Forecast Layer
  s_weather_forecast1_layer = text_layer_create(GRect(25, 90, 119, 20)); // 0, 81, 144, 20
  text_layer_set_background_color(s_weather_forecast1_layer, GColorClear);
  text_layer_set_text_color(s_weather_forecast1_layer, GColorBlack);
  text_layer_set_font(s_weather_forecast1_layer, fonts_get_system_font(FONT_KEY_GOTHIC_18));
  text_layer_set_text_alignment(s_weather_forecast1_layer, GTextAlignmentLeft);
  text_layer_set_text(s_weather_forecast1_layer, "");
  layer_add_child(window_get_root_layer(window), text_layer_get_layer(s_weather_forecast1_layer));

  // Create Tomorrow's Forecast Label Layer
  s_weather_label2_layer = text_layer_create(GRect(2, 107, 25, 20)); // 0, 113, 35, 20
  text_layer_set_background_color(s_weather_label2_layer, GColorClear);
  text_layer_set_text_color(s_weather_label2_layer, GColorBlack);
  text_layer_set_font(s_weather_label2_layer, fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD));
  text_layer_set_text_alignment(s_weather_label2_layer, GTextAlignmentLeft);
  text_layer_set_text(s_weather_label2_layer, "");
  layer_add_child(window_get_root_layer(window), text_layer_get_layer(s_weather_label2_layer));

  // Create Tomorrow's Forecast Layer
  s_weather_forecast2_layer = text_layer_create(GRect(25, 107, 119, 20)); // 35, 113, 109, 20
  text_layer_set_background_color(s_weather_forecast2_layer, GColorClear);
  text_layer_set_text_color(s_weather_forecast2_layer, GColorBlack);
  text_layer_set_font(s_weather_forecast2_layer, fonts_get_system_font(FONT_KEY_GOTHIC_18));
  text_layer_set_text_alignment(s_weather_forecast2_layer, GTextAlignmentLeft);
  text_layer_set_text(s_weather_forecast2_layer, "");
  layer_add_child(window_get_root_layer(window), text_layer_get_layer(s_weather_forecast2_layer));

  // Create Calendar Layers
  create_calendar_layers();

  // Do an immediate update for all the layers.
  time_t currentTime = time(NULL);
  struct tm *tick_time = localtime(&currentTime);
  update_time(tick_time);
  update_date(tick_time);
  update_battery_state(battery_state_service_peek());
  update_bluetooth_state(bluetooth_connection_service_peek());
  update_weather();

  // Cannot do a request_weather here, crashes the Pebble Watch.
}

static void main_window_unload(Window *window)
{
  // Record current weather conditions so they can be restored
  // immediately when the app reopens. Very important if we don't
  // have a data connection when the app reopens.
  persist_write_int(STORAGE_KEY_CURRENT_TEMPERATURE_C, currentTemperature_c);
  persist_write_string(STORAGE_KEY_CURRENT_CONDITIONS, currentConditions);
  persist_write_string(STORAGE_KEY_CURRENT_DAY_FORECAST_CONDITIONS, currentDayForecastConditions);
  persist_write_int(STORAGE_KEY_CURRENT_LOW_C, currentLowTemperature_c);
  persist_write_int(STORAGE_KEY_CURRENT_HIGH_C, currentHighTemperature_c);
  persist_write_int(STORAGE_KEY_CURRENT_WIND_DIR_DEG, currentWindDirection_deg);
  persist_write_int(STORAGE_KEY_CURRENT_WIND_SPD_METERSPERSECOND, currentWindSpeed_metersPerSecond);
  persist_write_int(STORAGE_KEY_CURRENT_DAY, currentDate);
  persist_write_int(STORAGE_KEY_FORECAST_LOW_C, forecastLowTemperature_c);
  persist_write_int(STORAGE_KEY_FORECAST_HIGH_C, forecastHighTemperature_c);
  persist_write_string(STORAGE_KEY_FORECAST_CONDITIONS, forecastConditions);

  // Record configuration settings.
  persist_write_int(STORAGE_KEY_TEMPERATURE_UNITS, temperatureUnits);
  persist_write_int(STORAGE_KEY_WINDSPEED_UNITS, windSpeedUnits);
  persist_write_int(STORAGE_KEY_WEEKNUMBER_ENABLED, weekNumberEnabled);
  persist_write_int(STORAGE_KEY_MONDAY_FIRST, mondayFirst);

  // Destroy Layers
  text_layer_destroy(s_battery_layer);
  text_layer_destroy(s_linkStatus_layer);
  text_layer_destroy(s_time_layer);
  text_layer_destroy(s_time_am_pm_layer);
  text_layer_destroy(s_time_seconds_layer);
  text_layer_destroy(s_date_layer);
  text_layer_destroy(s_weather_current_layer);
  text_layer_destroy(s_weather_label1_layer);
  text_layer_destroy(s_weather_forecast1_layer);
  text_layer_destroy(s_weather_label2_layer);
  text_layer_destroy(s_weather_forecast2_layer);
  destroy_calendar_layers();
}

// tick_handler may be called once per second or once per minute
// depending on if the watch has been tapped and if we're showing
// the 12 HR clock.
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 5
      // 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);

  // 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();
  }

  // Update the weather every 30 minutes.
  if (difftime(time(NULL), timeOfLastDataRequest) > NUMBER_OF_SECONDS_BETWEEN_WEATHER_UPDATES)
  {
    request_weather();
  }

  // If we haven't received our weather request within 1 minute,
  // assume that we have lost our data connection.
  if (connectedToData &&
      (difftime(timeOfLastDataResponse, timeOfLastDataRequest) > NUMBER_OF_SECONDS_UNTIL_DATA_CONSIDERED_LOST))
  {
    connectedToData = false;
    update_link_label();
  }
}

static void inbox_received_callback(DictionaryIterator *iterator, void *context)
{
  // We received data, update the time stamp / link label.
  timeOfLastDataResponse = time(NULL);
  connectedToData = true;
  update_link_label();

  // Read first item
  Tuple *t = dict_read_first(iterator);

  int day1Date = 0;
  int day2Date = 0;
  //int day3Date = 0;
  int day1LowTemperature_c = 0;
  int day2LowTemperature_c = 0;
  int day3LowTemperature_c = 0;
  int day1HighTemperature_c = 0;
  int day2HighTemperature_c = 0;
  int day3HighTemperature_c = 0;
  char day1Conditions[32];
  char day2Conditions[32];
  char day3Conditions[32];

  bool recreateCalendarLayers = false;

  // For all items
  while(t != NULL)
  {
    // Which key was received?
    switch(t->key)
    {
      case KEY_TEMPERATURE:
        currentTemperature_c = t->value->int32;
        break;
//      case KEY_CONDITIONS:
//        // Current conditions (abbreviated).
//        snprintf(conditions_buffer, sizeof(conditions_buffer), "%s", t->value->cstring);
//        break;
//      case KEY_TEMP_MIN:
//        // Not reliably low temperature.
//        snprintf(temperatureMin_buffer, sizeof(temperatureMin_buffer), "%d", getFahrenheitFromCelsius(t->value->int32));
//        break;
//      case KEY_TEMP_MAX:
//        // Not reliably high temperature.
//        snprintf(temperatureMax_buffer, sizeof(temperatureMax_buffer), "%dF", getFahrenheitFromCelsius(t->value->int32));
//        break;
      case KEY_WIND_SPEED:
        // Reported in meters per second, convert to knots.
        currentWindSpeed_metersPerSecond = t->value->int32;
        break;
      case KEY_WIND_DIRECTION:
        currentWindDirection_deg = t->value->int32;
        break;
//      case KEY_HUMIDITY:
//        snprintf(humidity_buffer, sizeof(humidity_buffer), "%d%%", (int)t->value->int32);
//        break;
      case KEY_DESCRIPTION:
        // Similar to conditions, but far more descriptive.
        strncpy(currentConditions, t->value->cstring, 32);
        break;
      case KEY_DAY1_TIME:
        // Usually today's date, but in the morning it's yesterday's!
        day1Date = t->value->int32;
        break;
      case KEY_DAY1_CONDITIONS:
        // Today's condition (abbreviated).
        //snprintf(day1_conditions_buffer, sizeof(day1_conditions_buffer), "%s", t->value->cstring);
        strncpy(day1Conditions, t->value->cstring, 32);
        break;
      case KEY_DAY1_TEMP_MIN:
        //currentLowTemperature_c = t->value->int32;
        day1LowTemperature_c = t->value->int32;
        break;
      case KEY_DAY1_TEMP_MAX:
        //currentHighTemperature_c = t->value->int32;
        day1HighTemperature_c = t->value->int32;
        break;
      case KEY_DAY2_TIME:
        // Usually it's tomorrow's date, but in the morning it's today's!.
        //forecastDate = t->value->int32;
        day2Date = t->value->int32;
        break;
      case KEY_DAY2_CONDITIONS:
        // Forecast condition (abbreviated).
        //strncpy(forecastConditions, t->value->cstring, 32);
        strncpy(day2Conditions, t->value->cstring, 32);
        break;
      case KEY_DAY2_TEMP_MIN:
        //forecastLowTemperature_c = t->value->int32;
        day2LowTemperature_c = t->value->int32;
        break;
      case KEY_DAY2_TEMP_MAX:
        //forecastHighTemperature_c = t->value->int32;
        day2HighTemperature_c = t->value->int32;
        break;
      case KEY_DAY3_TIME:
        // Usually it's in two days, but in the morning it's tomorrow's!
        //day3Date = t->value->int32;
        break;
      case KEY_DAY3_CONDITIONS:
        // Forecast condition (abbreviated).
        strncpy(day3Conditions, t->value->cstring, 32);
        break;
      case KEY_DAY3_TEMP_MIN:
        //day3LowTemperature_c = t->value->int32;
        day3LowTemperature_c = t->value->int32;
        break;
      case KEY_DAY3_TEMP_MAX:
        day3HighTemperature_c = t->value->int32;
        break;
      case CONFIG_KEY_TEMPERATURE_UNITS:
        if (strcmp(t->value->cstring, "F") == 0)
        {
          temperatureUnits = TEMPERATURE_UNITS_F;
        }
        else if (strcmp(t->value->cstring, "C") == 0)
        {
          temperatureUnits = TEMPERATURE_UNITS_C;
        }
        break;
      case CONFIG_KEY_WINDSPEED_UNITS:
        if (strcmp(t->value->cstring, "KNOTS") == 0)
        {
          windSpeedUnits = WINDSPEED_UNITS_KNOTS;
        }
        else if (strcmp(t->value->cstring, "MPH") == 0)
        {
          windSpeedUnits = WINDSPEED_UNITS_MPH;
        }
        else if (strcmp(t->value->cstring, "KPH") == 0)
        {
          windSpeedUnits = WINDSPEED_UNITS_KPH;
        }
        break;
      case CONFIG_KEY_WEEKNUMBER_ENABLED:
        if (strcmp(t->value->cstring, "DISABLED") == 0)
        {
          weekNumberEnabled = FALSE;
        }
        else if (strcmp(t->value->cstring, "ENABLED") == 0)
        {
          weekNumberEnabled = TRUE;
        }
        break;
      case CONFIG_KEY_MONDAY_FIRST:
        if (strcmp(t->value->cstring, "DISABLED") == 0)
        {
          if (mondayFirst == TRUE)
          {
            // Setting changed, flag to recreate the calendar layers.
            recreateCalendarLayers = true;
          }
          mondayFirst = FALSE;
        }
        else if (strcmp(t->value->cstring, "ENABLED") == 0)
        {
          if (mondayFirst == FALSE)
          {
            // Setting changed, flag to recreate the calendar layers.
            recreateCalendarLayers = true;
          }
          mondayFirst = TRUE;
        }
        break;
      default:
        APP_LOG(APP_LOG_LEVEL_ERROR, "Key %d not recognized!", (int)t->key);
        break;
    }

    // Look for next item
    t = dict_read_next(iterator);
  }

  if (day1Date > 0)
  {
    // Forecast Response
    time_t currentTime = time(NULL);
    struct tm *currentCalendarTime = localtime(&currentTime);
    int dayOfMonthCurrent = currentCalendarTime->tm_mday;
 
    time_t day1Date_t = day1Date;
    struct tm *day1CalendarTime = localtime(&day1Date_t);
    int dayOfMonth1 = day1CalendarTime->tm_mday;
 
    time_t day2Date_t = day2Date;
    struct tm *day2CalendarTime = localtime(&day2Date_t);
    int dayOfMonth2 = day2CalendarTime->tm_mday;
 
    if (dayOfMonthCurrent == dayOfMonth1)
    {
      // Day 1 is Today's Date
      currentDate = day1Date;
   
      strncpy(currentDayForecastConditions, day1Conditions, 32);
      currentLowTemperature_c = day1LowTemperature_c;
      currentHighTemperature_c = day1HighTemperature_c;
   
      // So Day 2 will be the forecast.
      strncpy(forecastConditions, day2Conditions, 32);
      forecastLowTemperature_c = day2LowTemperature_c;
      forecastHighTemperature_c = day2HighTemperature_c;
    }
    else if (dayOfMonthCurrent == dayOfMonth2)
    {
      // Day 2 is Today's Date
      currentDate = day2Date;
   
      strncpy(currentDayForecastConditions, day2Conditions, 32);
      currentLowTemperature_c = day2LowTemperature_c;
      currentHighTemperature_c = day2HighTemperature_c;

      // So Day 3 will be the forecast.
      strncpy(forecastConditions, day3Conditions, 32);
      forecastLowTemperature_c = day3LowTemperature_c;
      forecastHighTemperature_c = day3HighTemperature_c;
    }
  } // (day1Date > 0)

  if (recreateCalendarLayers)
  {
    destroy_calendar_layers();
    create_calendar_layers();
    time_t currentTime = time(NULL);
    struct tm *tick_time = localtime(&currentTime);
    update_date(tick_time);
  }

  update_weather();
}

static void inbox_dropped_callback(AppMessageResult reason, void *context)
{
  APP_LOG(APP_LOG_LEVEL_ERROR, "Message dropped!");
}

static void outbox_failed_callback(DictionaryIterator *iterator, AppMessageResult reason, void *context)
{
  APP_LOG(APP_LOG_LEVEL_ERROR, "Outbox send failed!");
}

static void outbox_sent_callback(DictionaryIterator *iterator, void *context)
{
  APP_LOG(APP_LOG_LEVEL_INFO, "Outbox send success!");
}

static void accel_tap_handler(AccelAxisType axis, int32_t direction)
{
  // In testing, showing seconds all the time resulted in a battery
  // life of barely 3 days. Not showing seconds (updating only once
  // per minute) resulted in a battery life of 5 days.
  // As a compromise, if our watch gets a tap then we'll show seconds
  // for 3 minutes. In the middle of the night we won't waste
  // processing on seconds but when we need seconds for exact timing
  // it's just a flick of a wrist to see them.

  timeOfLastTap = time(NULL);

  // The 24 HR clock never shows seconds.
  if ((weekNumberEnabled == FALSE) && !clock_is_24h_style())
  {
    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);

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

static void init(void)
{
  // Create main Window element and assign to pointer
  s_main_window = window_create();

  // Set handlers to manage the elements inside the Window
  window_set_window_handlers(s_main_window, (WindowHandlers) {
    .load = main_window_load,
    .unload = main_window_unload
    });

  // Show the window on the watch, with animated=true
  window_stack_push(s_main_window, true);

  // Initially only subscribe at MINUTE_UNIT. If the user is in
  // 12 HR mode and does a watch bump, then the timer will change
  // to SECOND_UNIT for a brief period.
  tick_timer_service_subscribe(MINUTE_UNIT, tick_handler);

  battery_state_service_subscribe(update_battery_state);
  bluetooth_connection_service_subscribe(update_bluetooth_state);
  accel_tap_service_subscribe(accel_tap_handler);

  app_message_register_inbox_received(inbox_received_callback);
  app_message_register_inbox_dropped(inbox_dropped_callback);
  app_message_register_outbox_failed(outbox_failed_callback);
  app_message_register_outbox_sent(outbox_sent_callback);

  app_message_open(app_message_inbox_size_maximum(), app_message_outbox_size_maximum());
}

void deinit(void)
{
  tick_timer_service_unsubscribe();
  battery_state_service_unsubscribe();
  bluetooth_connection_service_unsubscribe();
  accel_tap_service_unsubscribe();

  window_destroy(s_main_window);
}

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

JavaScript Code

weatherStream.js

var xhrRequest = function (url, type, callback) {
  var xhr = new XMLHttpRequest();
  xhr.onload = function () {
    callback(this.responseText);
  };
  xhr.open(type, url);
  xhr.send();
};

function locationSuccess(pos) {
  // Construct URL
  var url = "http://api.openweathermap.org/data/2.5/weather?lat=" +
      pos.coords.latitude + "&lon=" + pos.coords.longitude;

  // Send request to OpenWeatherMap
  xhrRequest(url, 'GET',
    function(responseText) {
      // responseText contains a JSON object with weather info
      var json = JSON.parse(responseText);

      // Temperature in Kelvin requires adjustment
      var temperature = Math.round(json.main.temp - 273.15);
      //console.log("WX Temperature is " + temperature);

      // Conditions
      //var conditions = json.weather[0].main;    
      //console.log("WX Conditions are " + conditions);
   
      // Temperature Min
      //var temperatureMin = Math.round(json.main.temp_min - 273.15);
      //console.log("WX Min Temperature is " + temperatureMin);
   
      // Temperature Min
      //var temperatureMax = Math.round(json.main.temp_max - 273.15);
      //console.log("WX Max Temperature is " + temperatureMax);
   
      // Wind Speed
      var windSpeed = Math.round(json.wind.speed);
      //console.log("WX Wind Speed is " + windSpeed);
   
      // Wind Direction
      var windDirection = Math.round(json.wind.deg);
      //console.log("WX Wind Direction is " + windDirection);
   
      // Humidity
      //var humidity = Math.round(json.main.humidity);
      //console.log("WX Humidity is " + humidity);
   
      // Description
      var description = json.weather[0].description;    
      //console.log("WX Description is " + description);
   
      // Assemble dictionary using our keys
      var dictionary = {
        "KEY_TEMPERATURE": temperature,
        "KEY_WIND_SPEED": windSpeed,
        "KEY_WIND_DIRECTION": windDirection,
        "KEY_DESCRIPTION": description
      };
//        "KEY_TEMP_MIN": temperatureMin,
//        "KEY_TEMP_MAX": temperatureMax,
//        "KEY_CONDITIONS": conditions,
//        "KEY_HUMIDITY": humidity,

      // Send to Pebble
      Pebble.sendAppMessage(dictionary,
        function(e) {
          //console.log("Weather info sent to Pebble successfully WX!");
        },
        function(e) {
          //console.log("Error sending weather info to Pebble WX!");
        }
      );
    }    
  );

  // Construct URL
  var forecasturl = "http://api.openweathermap.org/data/2.5/forecast/daily?lat=" +
      pos.coords.latitude + "&lon=" + pos.coords.longitude;

  // Send request to OpenWeatherMap
  xhrRequest(forecasturl, 'GET',
    function(responseForecastText) {
      // responseText contains a JSON object with weather info
      var json = JSON.parse(responseForecastText);

      var day1Time = json.list[0].dt;
      //console.log("Day 1 Time is " + day1Time);
   
      // Conditions
      var day1Conditions = json.list[0].weather[0].main;    
      //console.log("Day 1 Forecast is " + day1Conditions);
   
      // Temperature in Kelvin requires adjustment
      var day1TemperatureMin = Math.round(json.list[0].temp.min - 273.15);
      //console.log("Day 1 Min Temperature is " + day1TemperatureMin);

      // Temperature in Kelvin requires adjustment
      var day1TemperatureMax = Math.round(json.list[0].temp.max - 273.15);
      //console.log("Day 1 Max Temperature is " + day1TemperatureMax);

      var day2Time = json.list[1].dt;
      //console.log("Day 2 Time is " + day2Time);
   
      // Conditions
      var day2Conditions = json.list[1].weather[0].main;    
      //console.log("Day 2 Forecast is " + day2Conditions);
         
      // Temperature in Kelvin requires adjustment
      var day2TemperatureMin = Math.round(json.list[1].temp.min - 273.15);
      //console.log("Day 2 Min Temperature is " + day2TemperatureMin);

      // Temperature in Kelvin requires adjustment
      var day2TemperatureMax = Math.round(json.list[1].temp.max - 273.15);
      //console.log("Day 2 Max Temperature is " + day2TemperatureMax);

      var day3Time = json.list[2].dt;
      //console.log("Day 3 Time is " + day3Time);
   
      // Conditions
      var day3Conditions = json.list[2].weather[0].main;    
      //console.log("Day 3 Forecast is " + day3Conditions);
         
      // Temperature in Kelvin requires adjustment
      var day3TemperatureMin = Math.round(json.list[2].temp.min - 273.15);
      //console.log("Day 3 Min Temperature is " + day3TemperatureMin);

      // Temperature in Kelvin requires adjustment
      var day3TemperatureMax = Math.round(json.list[2].temp.max - 273.15);
      //console.log("Day 3 Max Temperature is " + day3TemperatureMax);

      // Assemble dictionary using our keys
      var dictionary = {
        "KEY_DAY1_TIME": day1Time,
        "KEY_DAY1_CONDITIONS": day1Conditions,
        "KEY_DAY1_TEMP_MIN": day1TemperatureMin,
        "KEY_DAY1_TEMP_MAX": day1TemperatureMax,
        "KEY_DAY2_TIME": day2Time,
        "KEY_DAY2_CONDITIONS": day2Conditions,
        "KEY_DAY2_TEMP_MIN": day2TemperatureMin,
        "KEY_DAY2_TEMP_MAX": day2TemperatureMax,
        "KEY_DAY3_TIME": day3Time,
        "KEY_DAY3_CONDITIONS": day3Conditions,
        "KEY_DAY3_TEMP_MIN": day3TemperatureMin,
        "KEY_DAY3_TEMP_MAX": day3TemperatureMax
      };

      // Send to Pebble
      Pebble.sendAppMessage(dictionary,
        function(e) {
          //console.log("Weather info sent to Pebble successfully WX!");
        },
        function(e) {
          //console.log("Error sending weather info to Pebble WX!");
        }
      );
    }    
  );
}

function locationError(err) {
  console.log("Error requesting location WX!");
}

function getWeather() {
  navigator.geolocation.getCurrentPosition(
    locationSuccess,
    locationError,
    {timeout: 15000, maximumAge: 60000}
  );
}

// Listen for when the watchface is opened
Pebble.addEventListener('ready',
  function(e) {
    //console.log("PebbleKit WX JS ready!");

    // Get the initial weather
    getWeather();
  }
);

// Listen for when an AppMessage is received
Pebble.addEventListener('appmessage',
  function(e) {
    //console.log("AppMessage WX received!");
    getWeather();
  }                  
);

Pebble.addEventListener("showConfiguration",
  function(e) {
    Pebble.openURL("http://clintka.github.io/AllInfoAsText/index.html");
  }
);

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

    // Assemble dictionary using our keys
    var dictionary = {
      "CONFIG_KEY_TEMPERATURE_UNITS": configuration.temperatureUnits,
      "CONFIG_KEY_WINDSPEED_UNITS": configuration.windspeedUnits,
      "CONFIG_KEY_WEEKNUMBER_ENABLED": configuration.weekNumberEnabled,
      "CONFIG_KEY_MONDAY_FIRST": configuration.mondayFirst
      };

      // Send to Pebble
      Pebble.sendAppMessage(dictionary,
        function(e) {
          //console.log("Config info sent to Pebble successfully!");
        },
        function(e) {
          //console.log("Error sending config info to Pebble!");
        }
      );

  }
);

Web Page

index.html

<!DOCTYPE html>
<html>
  <head>
    <title>All Info As Text Config</title>
  </head>
  <body>
    <h1>All Info As Text Config</h1>
    <p>Choose settings:</p>

    <p>Please note: Opening this page always displays the default watch settings.</p>

    <p>Temperature Units:
    <select id="key_temperature_units_select">
      <option value="F" selected>F</option>
      <option value="C">C</option>
    </select>
    </p>

    <p>Windspeed Units:
    <select id="key_windspeed_units_select">
      <option value="KNOTS">Knots</option>
      <option value="MPH">MPH</option>
      <option value="KPH">KM/HR</option>
    </select>
    </p>

    <p>Week Number (replaces seconds):
    <select id="key_weeknumber_enabled_select">
      <option value="DISABLED">Disabled</option>
      <option value="ENABLED">Enabled</option>
    </select>
    </p>

    <p>First day of the week:
    <select id="key_mondayFirst_select">
      <option value="DISABLED">Sunday</option>
      <option value="ENABLED">Monday</option>
    </select>
    </p>

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

    <script>
//      function getQueryVariable(variable)
//      {
//             var query = window.location.search.substring(1);
//             var vars = query.split("&");
//             for (var i=0;i<vars.length;i++) {
//                     var pair = vars[i].split("=");
//                     if(pair[0] == variable){return pair[1];}
//             }
//             return(false);
//      }
      
      //Setup to allow easy adding more options later
      function saveOptions() {
        var temperatureUnits = document.getElementById("key_temperature_units_select");
        var windspeedUnits = document.getElementById("key_windspeed_units_select");
        var weekNumberEnabled = document.getElementById("key_weeknumber_enabled_select");
        var mondayFirst = document.getElementById("key_mondayFirst_select");
        var options = {
          "temperatureUnits": temperatureUnits.options[temperatureUnits.selectedIndex].value,
          "windspeedUnits": windspeedUnits.options[windspeedUnits.selectedIndex].value,
          "weekNumberEnabled": weekNumberEnabled.options[weekNumberEnabled.selectedIndex].value,
          "mondayFirst": mondayFirst.options[mondayFirst.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>
  </body>
</html>

The code within this post is released into the public domain. You're free to use it however you wish, but it is provided "as-is" without warranty of any kind. In no event shall the author be liable for any claims or damages in connection with this software.