This pebble watchapp is checking for your location and is displaying it on the screen. What it does at the moment:
- Get's your phone location
- Does a request to google maps API that's determining your location based on long / lat
- Displays a formatted address including: Road name, City name, ZIP Code, Country
- gestures controls for refreshing location, etc
- implementing viber (vibration)
- implementing icons and menus
- implementing compass
- a counter to see how many times you've gone into some locations
- a "save location" function to the locations you've been and want to save
This Pebble application has for the moment only two files:
main_activity.c
- which handles the main activity and main windowAPI_calls.js
- which makes the API calls and get's the phone location.
The main_activity.c
includes the pebble.h
header and defines the KEYS for our dictionary that we are using to transfer the data from the phone to Pebble. In the following, I will describe this file and it's content.
First of all, we are defining 3 elements that we are using within the application. Those are one Window
object and two TextLayer
objects. Each of them, are defined as pointers of types Window
and TextLayer
:
static Window *main_window; // Create a new window layer
static TextLayer *text_layer; // Create a new text layer
static TextLayer *api_data_text; // Create a new text layer
As every C
program, the Pebble WhatchApp has a main()
function that is called when the program is executed. Our main()
function consists into 3 function calls that are going to init our variables, get the application into it's running loop while the application is running and another function that destroys our used variables to free up memory. The main()
function is defined as follows:
int main(void){
init(); // Init everything
app_event_loop(); // Loop everything (run app and take actions)
deinit(); // When app is leaved, de-init everything to free memory
}
In our application, on the init()
function, I am creating a new Window
object and the two TextLayer
objects, and configure them with their default values, and also I am adding some of their properties. Also, in the same init()
function, I am initialising all the required event listeners for the AppMessage
API from Pebble, to listen for any events that are sending or receiving data. The function definition, along with the description of the code is the following:
// Init function
static void init(){
// Create new window
main_window = window_create(); // Instantiates the *window (pointer) with a new Window object
// Set the handlers
window_set_window_handlers(main_window, (WindowHandlers){
.load = main_window_load, // Set the handler for window load
.unload = main_window_unload // Set the handler for window unload
});
// Push the window to the stack
window_stack_push(main_window, true); // Push the window animated
// Init the callbacks for AppMessage API
app_message_register_inbox_received(inbox_received_callback); // Init callback
app_message_register_inbox_dropped(inbox_dropped_callback); // Init callback
app_message_register_outbox_failed(outbox_failed_callback); // Init callback
app_message_register_outbox_sent(outbox_sent_callback); // Init callback
// Start app message
app_message_open(app_message_inbox_size_maximum(), app_message_outbox_size_maximum());
}
Instead of instantiating all the elements that are used within the Window
object, I have attached handlers for when the Window
loads (is constructed) or unloads (is destructed). This means, that when the window is loaded, I am instantiating all the TextLayer
objects along with their properties. The handler for instantiating the Window
is called main_window_load
and it's defined as follows:
static void main_window_load(Window *window){ // Get our pointer to the Window object
// Create a new text layer and add it's properties
text_layer = text_layer_create(GRect(0, 0, 144, 28)); // Create a new rectangle shape that starts at 0,0 and has the width of 144PX and height of 28PX
text_layer_set_background_color(text_layer, GColorBlack); // Set the background color to Black
text_layer_set_text_color(text_layer, GColorWhite); // Set the text color to White
text_layer_set_text_alignment(text_layer, GTextAlignmentCenter); // Set the text alignment to center
text_layer_set_text(text_layer, "You're at"); // This is the default value for the string contained in this TextLayer
// Create a new text layer and add it's properties
api_data_text = text_layer_create(GRect(0, 28, 144, 140));
text_layer_set_background_color(api_data_text, GColorBlack);
text_layer_set_text_color(api_data_text, GColorWhite);
text_layer_set_text_alignment(api_data_text, GTextAlignmentCenter);
text_layer_set_text(api_data_text, "Loading...");
// Add this text layer to the window as a child
// As we have the pointer to the window, we can get it's root layer with window_get_root_layer(pointer)
// and we are using layer_add_child to add a new child into that window's layer and display it
layer_add_child(window_get_root_layer(window), text_layer_get_layer(text_layer));
// Add the api text layer to the window
layer_add_child(window_get_root_layer(window), text_layer_get_layer(api_data_text));
}
The prototype of those functions related to the UI can be found within the Pebble documentation at Official Pebble Documentation > PEBBLE C > UI. Every other details about what's happening in that block, can be readed and understood by the comments on each line.
When a Window
object is destroyed (when the application closed), our handler for destroying the Window
object is going to destroy those objects to free up the memory used by the application on the Pebble OS. Thus meaning we need to remove the two layers we've added within this window, in it's window unload handler, as follows:
static void main_window_unload(Window *window){
// Destroy the text layer to free up space
text_layer_destroy(text_layer);
// Destroy the text layer to free up space
text_layer_destroy(api_data_text);
}
The above code clears the memory that was allocated on Pebble.
Eearlier in this description, I've told you that the Pebble WhatchApp is getting the Phone's location and does an AJAX request to get the location name based on the latitude and longitude determined by the phone. In our init()
function we had the following code that was subscribing some events related to AppMessage
API:
// Init the callbacks for AppMessage API
app_message_register_inbox_received(inbox_received_callback); // Listen for information received from the phone
app_message_register_inbox_dropped(inbox_dropped_callback); // Listen for information received from the phone but discarded
app_message_register_outbox_failed(outbox_failed_callback); // Listen for events that send information to the phone but were failed
app_message_register_outbox_sent(outbox_sent_callback); // Listen for events that send information to the phone with success
// Start app message
app_message_open(app_message_inbox_size_maximum(), app_message_outbox_size_maximum());
The app_message_open
actually opens the connection and attached all those event listeners. At the moment of calling those function, every event triggered is handled by one of the 4 event listeners we have instantiated later on. The only event listener implemented at the moment is the app_message_register_inbox_received()
and it's handled by the function called inbox_received_callback
, defined and explained as follows:
static void inbox_received_callback(DictionaryIterator *iterator, void *context){
Tuple *t = dict_read_first(iterator); // Get first dictionary value
// Create a new buffer for the location string
static char location_buffer[200]; // Buffer length should be enough to catch the entire string
// Iterate troughout the dictionary values
while(t != NULL){
switch(t->key){ // Switch the key that we receive
case KEY_APISTRING: // If we identify the key
// Include the information from the key as a STRING into our buffer
snprintf(location_buffer, sizeof(location_buffer), "%s", t->value->cstring);
break;
default: // The default case means we haven't indentified the string
APP_LOG(APP_LOG_LEVEL_WARNING, "Key %d is not recognized", (int)t->key); // Log the error
break;
}
// Read the next key in the dictionary if available
t = dict_read_next(iterator);
}
// Add the location_buffer to the clock watchface
APP_LOG(APP_LOG_LEVEL_DEBUG, "The value of location is: %s", location_buffer);
// Update the text layer so we can see the address at where we are
text_layer_set_text(api_data_text, location_buffer);
}
This handler get's the dictionary key defined by #define KEY_APISTRING 0
onto our application. It get's the dictionary from the Phone, searches for our defined keys, and when it finds KEY_APISTRING
into the dictionary, it loads it's content as a string within our buffer var created in this function. After the dictionary is read entirely and the while loop finishes, then we are updating the required TextLayer
with the new content (the address at where we are located).
All of this happens within the app_event_loop()
call from our main()
function. When we leave the application, the deinit()
function is called. In here, we remove the Window
object that we have created in the earlier steps:
static void deinit(){
// Destroy the main window to free memory on the Pebble watch
window_destroy(main_window);
}
Now, how does the phone send us all this information, and how everything is executed between the Pebble WhatchApp and the Phone?
The JavaScript file API_calls.js
handles all those steps and comunicates with our c
app. First, usng the Pebble
object from the PebbleJS KIT
, we are setting up an event listener for when the connection between the watch and the phone is ready:
Pebble.addEventListener('ready', function(ev){ // Initialise the AppMessage event listener for "ready" state
getLocation(); // When we are ready, request the location
});
When this event is ready, we are calling a function named geoLocation()
that get's our location from the phone as follows:
function getLocation(){
navigator.geolocation.getCurrentPosition( // Get the location from the phone
locationSuccessCallback, // Set callback for success
locationErrorCallback, // Set callback for errors
{timeout: 15000, maximumAge: 60000} // Set timeout and maximum age for this request
);
}
To make the function more readable, and easy to maintain, instead of using inline functions for the callbacks of navigator.geolocation.getCurrentPosition
I have created some handler functions that are taking care of the required callbacks: locationSuccessCallback
and locationErrorCallback
- their names are very intuitive and I am pretty sure you know what they are doing. Focusing on the locationSuccessCallback
(which means we have the location!) we are doing some more processing in this function like: getting the result from this callback, making an ajax call to Google Maps API to get our location name. The function is defined as follows:
function locationSuccessCallback(pos){
// Build the long/lat coords
var lat = pos.coords.latitude;
var lng = pos.coords.longitude;
// Get the location name
var current_address;
var req_url = "http://maps.googleapis.com/maps/api/geocode/json?latlng="+lat+","+lng+"&sensor=false";
// Return the JSON data
var request = new XMLHttpRequest(); // Create new AJAX request
request.open('GET', req_url, true); // Open connection to the req_url
request.onload = function(e){ // Check the AJAX STATE
if (request.readyState == 4 && request.status == 200) { // IF ajax finished and response is OK (200)
var response = JSON.parse(request.responseText); // Parse the response from the API
// Build the current address
current_address = response.results[0].formatted_address; // Get the current address
var dictionary = { // Create a dictionary that we send to the pebble app
"KEY_APISTRING": current_address
};
// Initialise a request to the pebble application
// and send our dictionary (the propper data)
Pebble.sendAppMessage(dictionary, function(){console.log("Location sent");}, function(){console.log("failed");});
}
};
// Finish tue ajax request
request.send(null);
}
The callback sends us a parameter with the longitude and latitude. We are using those two coordinates to make a request to Google Maps API via an Ajax call. When the call succeeds, we parse the response and create a new object (dictionary) where we put our KEY defined in the C app as being the key of the object, and the parsed content from google as being the VALUE of that key in the object. Then, we push this information to the watch by calling:
Pebble.sendAppMessage(dictionary, function(){console.log("Location sent");}, function(){console.log("failed");});
This function call, sends our dictionary to our C
app handler:
static void inbox_received_callback(DictionaryIterator *iterator, void *context)
This handler has been explained on top: Pushes the new value of the key to the TextLayer and displays it on the watch screen.