Summary:

Save your current moment with a single-click smartwatch application, so you can revisit it later. The idea is to discourage the knee-jerk reaction of whipping out a phone while adventuring in the world by saving the moment for later, so we can get back to now.

Materials:

materials-8

  1. Pebble Watch (any series)
  2. Computer with Pebble SDK
  3. Google Account

Process:

First, we'll need to create Google scripts and document to handle location saving and analysis. Then we'll need to install and build an application for a Pebble smartwatch that sends our coordinates to the script.

Prerequisites:

To build applications for the Pebble you'll need the SDK installed. Follow this guide to install the SDK on your development platform. You'll also need to have a Google account, for working with Google scripts and Google sheets.

Moments?

As I envisioned it, a Moment would be comprised of a saved date, time, and location. This combination of information would enable you to later explore the space in which you wanted to save the Moment.

Storing Moments:

Much like the Day 7: Open Data Logger project, this project will leverage Google's scripts API to save data in a Google sheets spreadsheet.

Like the other project, this requires logging in to sheets.google.com, and creating a new Sheets document to store Moments. Logically, I'll name this document and it's first sheet 'Moments'. I'll also give the sheet some headers for Date/Time, Latitude, Longitude, and some URLs that will later contain links to our saved Moment.

After extracting the document id from the URL (in the form https://docs.google.com/spreadsheets/d/{document_id}/edit) we'll create a new script for the sheet (Tools > Script editor).

Like before we'll need two functions. The first, doGet, is caled by default when an HTTPS GET request is sent to the script. The second function, saveData will be responsible for making the Sheets API calls to save our information to the document we created.

Our doGet function is short and sweet. It cleans up the request in case we have an invalid request. Then it extracts the URL parameters and passes them to our saveData function. All of this is wrapped in some error-catching code.

function doGet(e){
  try {
    // Safety values
    if (e == null){
      e={};
      e.parameters = {lat:"0",lon:"0"};
    }
        
    var lat = e.parameters.lat;
    var lon = e.parameters.lon;
        
    // save the data to spreadsheet
    saveData(lat, lon);
        
  } catch(error) { 
    Logger.log(error);    
  }  
}

The saveData function calls the SpreadsheetApp function to open the document we've created to save data. Then, using the Google Maps URL Reference we can create some request URLs to collect data about the location that's being saved. In this example, we generate a Maps URL with a pin on the location, and also a URL searching for nearby coffee shops. Then using the SpreadsheetApp we can save the Date, Lat/Lon and our URLs to the Sheets document.

function saveData(lat, lon) {
  try {
    // Get the request data and time
    var dateTime = new Date();
        
    var ss = SpreadsheetApp.openByUrl("https://docs.google.com/spreadsheets/d/{document_id}/edit");
    var dataLoggerSheet = ss.getSheetByName("Moments");
        
    // Get last edited row from DataLogger sheet
    var row = dataLoggerSheet.getLastRow() + 1;
    
    // Get a Google Maps URL and location-based searches from the Lat/Lon
    var mapsURL = "https://www.google.com/maps/search/?api=1&query="+lat+","+lon;
    var coffeNearURL = "https://www.google.com/maps/search/Coffee/@"+lat+","+lon+",15z";
    
    // DateTime/ Lat/ Lon/ Coffee
    dataLoggerSheet.getRange("A" + row).setValue(dateTime); // dateTime
    dataLoggerSheet.getRange("B" + row).setValue(lat); // Latitude
    dataLoggerSheet.getRange("C" + row).setValue(lon); // Longitude
    dataLoggerSheet.getRange("D" + row).setValue(mapsURL); // Maps URL
    dataLoggerSheet.getRange("E" + row).setValue(coffeNearURL); // Nearby coffee shops
  }
 
  catch(error) {
    Logger.log(JSON.stringify(error));
  }
}

To make this endpoint accessible to the smartwatch application we'll publish this script as a web app (Publish > Deploy as web app...). Make sure to set the app permissions to "anyone, even anonymous". Then save the app url that's generated.

publish_script

We can make requests to this endpoint by issuing a GET request to https://script.google.com/macros/s/{script_id}/exec?lat={latitude}&lon={longitude}

Creating and Testing a Pebble Project:

We'll need to leverage the Pebble SDK to create a template Application, so that we have a meaningful starting point for development. This is a relatively painless process, and the steps are detailed in the script below.

# Create a new Pebble Project
pebble new-project Moment

# Enter the project directory
cd Moment/

# Build the source
pebble build

# Install on an emulator for our build:
# Classic, Steel: Aplite
# Time, Time Steel: Basalt
# Time Round: Chalk
# Pebble 2: Diorite
# Pebble Time 2: Emery
pebble install --emulator chalk


# Open Log stream (Ctrl+C to close)
pebble logs

Running the install command will boot the QEMU-based Pebble emulation environment, where you can verify functionality of your application, test services, and capture debug outputs.

pebble_emulator

A Simple Watch Application Flow:

Now that we've got a suitable test environment, we're ready to make our first application changes. Most of this code is based on the Pebble one-click action example from the pebble developers page. It might be beneficial to run this example before proceeding, to build familiarity with the Pebble application environment. The next sections will detail important aspects of our pebble application, but they will not serve as a comprehensive introduction to Pebble application structure.

Basic Logistics:

We'll first need to edit our package.json file to include information about the structure of our project, as well as to add some message keys for passing information between the application's C++ and JS code.

{
  "name": "Save Moment",
  "author": "Your Name",
  "version": "1.0.0",
  "keywords": ["gps"],
  "private": true,
  "dependencies": {},
  "pebble": {
    "displayName": "Save Moment",
    "uuid": "1234-generated-uuid-1234",
    "sdkVersion": "3",
    "enableMultiJS": true,
    "targetPlatforms": [
      "aplite",
      "basalt",
      "chalk",
      "diorite"
    ],
    "watchapp": {
      "watchface": false
    },
    "messageKeys": [
      "APP_READY",
      "REQ_SUCCESS",
      "GPS_SUCCESS",
      "SCRIPT_ID"
    ],
    "resources": {
      "media": []
    }
  }
}

In Moment.c we'll need to define some static values, as well as global assets, for later reference in our code. We'll also import the Pebble library for all the standard functions we'll need.


// ==== GUI Assets ============================================================
static Window *s_window;
static TextLayer *s_text_layer;
static AppTimer *s_exit_timer;

// ==== Keys ==================================================================
#define GOOGLE_SHEET_ID "{script_id}"

// ==== Strings ===============================================================
#define MESSAGE_SAVE_ERROR "Moment could not be saved"
#define MESSAGE_SAVE_SUCCESS "Moment saved!"
#define MESSAGE_INSTALLED "Installed!"
#define MESSAGE_USER_LAUNCHED "Saving moment"
#define MESSAGE_GPS_SAVING "Saving moment..."
#define MESSAGE_GPS_SAVED "Moment saved!"

Main Application Structure:

The main Pebble application has three phases: Init, Application Loop, and Deinit. Much like Arduino code or stateful C code we'll need to separate our logic into these three phases. The init phase sets up GUI assets, registers system calls with callback functions, and initializes services or variables we'll need. While the application remains open on the Pebble device, the application loop phase is in effect. During this time we handle any timers or system calls issued during init (or reissued during application loop). When the application is closed by the user (or a close event issued by the application) the deinit process is executed.

int main(void) {
	prv_init();
	app_event_loop();
	prv_deinit();
}

Initialization:

We'll declare some functions that set up the application for the GUI, and our functions.

static void prv_init(void) {
	prv_init_app_message();

	// Create the application window and set the background color to Kelly Green
	s_window = window_create();
  	window_set_background_color(s_window, PBL_IF_COLOR_ELSE(GColorKellyGreen, GColorWhite));

	// Bind our onload and unload handlers
	window_set_window_handlers(s_window, (WindowHandlers) {
		.load = prv_window_load,
		.unload = prv_window_unload,
	});
	
	// Animate application open/close
	const bool animated = true;
	
	// Add the window to the stack
	window_stack_push(s_window, animated);
}

static void prv_init_app_message() {
	/* Initialize AppMessage - allowing us to communicate with
	 * PebbleJS and the system. We'll need to define a hander 
	 * for receving messages, prv_inbox_received_handler, which 
	 * we'll put in the Application Loop logic.
	 */
	app_message_register_inbox_received(prv_inbox_received_handler);
	app_message_open(256,256);
}

static void prv_window_load(Window *window) {
	// Get GUI base elements
	Layer *window_layer = window_get_root_layer(window);
	GRect bounds = layer_get_bounds(window_layer);

	// Create a text object for application messages.
	s_text_layer = text_layer_create(GRect(0, 72, bounds.size.w, 20));
	// Clear background
	text_layer_set_background_color(s_text_layer, GColorClear);
	// Center the text
	text_layer_set_text_alignment(s_text_layer, GTextAlignmentCenter);
	// Add it to the GUI parent element
	layer_add_child(window_layer, text_layer_get_layer(s_text_layer));
}

Application Logic:

First we'll need to define the handler for receiving application messages. If the message indicates the application was launched we'll need to check if it was just installed or if it was run after installation. If it was launched by the user we'll want to send our coordinates to our google script. If it was installed we'll need to display an appropriate message. Otherwise, if the message is some kind of status message we'll update the text field in the GUI to display an appropriate status message. We'll also set a timer to exit the application so that regardless of the state of the application we exit after a certain time (we are a single-click app after all).

static void prv_inbox_received_handler(DictionaryIterator *iter, void *context) {
	// Look for APP_READY message
	Tuple *ready_tuple = dict_find(iter, MESSAGE_KEY_APP_READY);
	if (ready_tuple) {
	
		// If it's a user launch then call our code
		if(launch_reason() == APP_LAUNCH_USER 
		|| launch_reason() == APP_LAUNCH_QUICK_LAUNCH) {
			text_layer_set_text(s_text_layer, MESSAGE_USER_LAUNCHED);
			prv_send_google_scripts_loc_save_message(GOOGLE_SHEET_ID);
		} else {
			// Must've been an installation
			text_layer_set_text(s_text_layer, MESSAGE_INSTALLED);
			s_exit_timer = app_timer_register(5000, prv_exit_application, NULL);
		}
		return;
	}
	
	// Look for 'GPS_SUCCESS' message
	ready_tuple = dict_find(iter, MESSAGE_KEY_GPS_SUCCESS);
	if (ready_tuple) {
		// If it was, display as such and set a 4 second exit timer
		text_layer_set_text(s_text_layer, MESSAGE_GPS_SAVING);
		s_exit_timer = app_timer_register(4000, prv_exit_application, NULL);
		return;
	}
	
	// Look for 'REQ_SUCCESS' message
	ready_tuple = dict_find(iter, MESSAGE_KEY_REQ_SUCCESS);
	if (ready_tuple) {
			// If it was, display as such and reset our exit timer to be 1 second.
			text_layer_set_text(s_text_layer, MESSAGE_GPS_SAVED);
			s_exit_timer = app_timer_register(1000, prv_exit_application, NULL);
		return;
	}
	
}

Since we're not masochists we'll not rely on the C++ Pebble code to send our HTTPS requests. Instead, if we want to send our location to our Google script we'll send a message to the javascript code of our application over the application message outbox.

static void prv_send_google_scripts_loc_save_message(const char *script_id) {
	// Open the outbox and check the result
	DictionaryIterator *out;
	AppMessageResult result = app_message_outbox_begin(&out);
	
	if (result != APP_MSG_OK) {
		// Some error occured - the user doesn't care why, so
		// display a generic error
		text_layer_set_text(s_text_layer, MESSAGE_SAVE_ERROR);
	}

	/* Write our script_id to the message buffer. This could be handled
	 * a number of ways, including by sending a generic 'SEND_LOCATION' key
	 * to the js code. For no good reason I've chosen to keep the Scripts key in 
	 * the C++ code and send it over the message buffer, but it could just as
	*/ easily be kept in JS.
	dict_write_cstring(out, MESSAGE_KEY_SCRIPT_ID, script_id);

	result = app_message_outbox_send();
	if (result != APP_MSG_OK) {
		// Some error occured - the user doesn't care why, so
		// display a generic error
		text_layer_set_text(s_text_layer, MESSAGE_SAVE_ERROR);
	}
}

Deinitialization:

We'll declare all the cleanup functions we need to ensure we don't leave junk hanging around in the system.

static void prv_exit_application(void *data) {
	// App can exit to return directly to their default watchface
	exit_reason_set(APP_EXIT_ACTION_PERFORMED_SUCCESSFULLY);

	// Exit the application by unloading the only window
	const bool animated = true;
	window_stack_remove(s_window, animated);
}

static void prv_window_unload(Window *window) {
	// Clean up our text layer
	text_layer_destroy(s_text_layer);
}

static void prv_deinit(void) {
	window_destroy(s_window);
}

Javascript - HTTPS and Location Services:

At this point we've successfully loaded the application and handled the GUI. But we won't see anything on screen if we run the application (except a green background). This is because we wait for an APP_READY message from the application buffer before we issue any commands other than setup. We'll need to create some javascript code in our project to handle this, as well as sending our location to the correct script.

Making a JS Project:

First we'll need our project to support javascript. We could have used the --javascript flag when creating our project, and the Pebble SDK would have taken care of this. But where's the fun in that. Instead we'll make the correct files and directories, and edit them ourselves.

# Make a js directory and file - default location that Pebble
# applications look for javascript
mkdir -p src/pkjs

# Create our js file
touch src/pkjs/index.js

Getting Ready:

In the javascript code we'll create a handler for the pebble system ready call, that's triggered when the application javascript component has been launched and is ready to execute. In our case we'll let the main application know we're all good to go, which will trigger the main application code we wrote to handle this.

// Let the app know we're ready to go
Pebble.addEventListener('ready', function(e) {
	Pebble.sendAppMessage({'APP_READY': true});
});

Listening for Messages:

We'll also need to listen for messages from the main application. To do that we'll define another handler, this time for the pebble system appmessage call. If it contains a key we recognize, SCRIPT_ID, we can send the location to that script.

// If we get a message
Pebble.addEventListener('appmessage', function(dict) {
	if(dict.payload['SCRIPT_ID']) {
		getLocationQueryString(dict.payload['SCRIPT_ID']);
	}
});

Sending HTTPS:

To make our lives easier we'll define some convenience functions to handle HTTPS GET requests. One, xhrGetWrapper, will provide a wrapper for GET requests. The other, googleScriptsRequest, will build an appropriate URL from a script id and query string, and send it.

function googleScriptsRequest(script_id, query_string) {
	var url = 'https://script.google.com/macros/s/' + script_id + '/exec' + query_string;
	console.log("URL: "+url);

	xhrGetWrapper(url, function(req) {
	if(req.status == 200) {
		Pebble.sendAppMessage({'REQ_SUCCESS':1});
	} else {
		Pebble.sendAppMessage({'REQ_SUCCESS':0});
	}
	});
}

function xhrGetWrapper(url, callback) {
	var xhr = new XMLHttpRequest();
	xhr.onload = function () {
		callback(xhr);
	};
	xhr.open('get', url);
	xhr.send();
}

Geolocation:

To actually get GPS coordinates for our script we'll need to use the JavaScript GeoLocation API. Because the function calls rely on callbacks we'll issue a request for a location, and then send a response based on the callback.

function getLocationQueryString(script_id) {
	// Options for the geolocation navigator
	var geolocationOptions = {
		enableHighAccuracy: true,
		timeout: 3000,
		maximumAge: 0
	};
	
	navigator.geolocation.getCurrentPosition(function(pos) {
		// onSuccess callback
		var crd = pos.coords;
		query = "?lat=" + crd.latitude + "&lon=" + crd.longitude;
		// Keep the GUI up to date
		Pebble.sendAppMessage({'GPS_SUCCESS':1});
		googleScriptsRequest(script_id, query)
	}, function(error) {
		// onError callback
		query = "?lat=0&lon=0";
		Pebble.sendAppMessage({'GPS_SUCCESS':0});
		googleScriptsRequest(script_id, query)
	}, geolocationOptions);

Putting It All Together:

After saving everything we can run a project build, and install it on the emulator to test our application. If everything goes well we should be able to see after installation that our application has been installed.

installed

If we launch the quick menu we can see our application now appears.

app

Clicking on the application will launch it, and we'll see our status messages update as we get GPS coordinates and send them to our script.

saved

If everything works we'll see our current location, as well as some useful URLs logged in our Google Sheets document. Success!

new_entry

Code:

Full example code for today can be found here.

Future Work:

The first step for continued work with this idea would be the creation of more advanced "what's around me" data collection. An example analytic activity could be aggregating a list of all the cool shops that were open near me at the time a Moment was saved.

It would also be cool to create some sort of visualization for this project that displayed all saved Moments and metadata, any perform meta-analysis on this data.