var Clay = require('pebble-clay');
var clayConfig = require('./config');
var clay = new Clay(clayConfig, null, { autoHandleEvents: false });
var messageKeys = require('message_keys');
var message;
var gpx_to_strava
var gpx_to_web
var locate_me
var locationInterval;
var instantLocationInterval;
// TODO to remove for security
var client_id = "94880";
var client_secret = "08dc170f0fe38f39dd327bea82a28db4400e6f00";
var firstlocationOptions = {
'enableHighAccuracy': true, // default = false (quick and dirty mode), can be true (more accurate but need more power and time)
'timeout': 60000, //60s timeout to get a first good signal
'maximumAge': 0 // no cache
};
var locationOptions = {
'enableHighAccuracy': true, // default = false (quick and dirty mode), can be true (more accurate but need more power and time)
'timeout': 5000, //5s timeout to get a good signal
'maximumAge': 0 // no cache
};
Pebble.addEventListener('showConfiguration', function (e) {
clay.config = clayConfig;
console.log("Clay config is showing...")
Pebble.openURL(clay.generateUrl());
});
Pebble.addEventListener('webviewclosed', function (t) {
if (!t || t.response) {
console.log("Clay config is submitted : " + t.response)
try {
if (data = JSON.parse(t.response), data.code && data.scope == "read,activity:write") {
// data looks like this :
// {"code":"db896b06f89804997a8088320fba755e6299c0d6","scope":"read,activity:write","state":"bike_companion"}
// so need to parse it correctly
//var grantcode = JSON.parse(data.strava.value)
if (data.state == "bike_companion" && data.scope == "read,activity:write") {
getTokens(data.code);
} else {
console.log("Error on response returned : scope is " + grantcode.scope + " and state is " + grantcode.state);
}
} else {
clay.getSettings(t.response);
console.log("Clay settings in Localstorage looks like " + localStorage.getItem("clay-settings"));
}
} catch (t) {
console.log("Oauth parsing error, continue on saving clay settings");
clay.getSettings(t.response);
console.log("Clay settings in Localstorage looks like " + localStorage.getItem("clay-settings"));
//set strava to false
}
// dict is not used as intended to send settings to watch
/* assess if needed to send setting to watch as already in phone (done with "getsettings")
m.live_mmt_enable = dict[p.live_mmt_enable]
m.live_mmt_login = dict[p.live_mmt_login]
m.live_mmt_password = dict[p.live_mmt_password]
m.live_jayps_enable = dict[p.live_jayps_enable]
m.live_jayps_login = dict[p.live_jayps_login]
m.live_jayps_password = dict[p.live_jayps_password]
m.strava_automatic_upload = dict[p.strava_automatic_upload]
i = dict[p.debug]
console.log("debug:" + i)
g.setDebug(i)
h.setDebug(i)
b.setDebug(i)
y.setDebug(i)
m.setDebug(i)
m.save()
// Assess if needed to send settings values to watch side
/*
Pebble.sendAppMessage(dict, function(e) {
console.log('Sent config data to Pebble');
}, function(e) {
console.log('Failed to send config data!');
console.log(JSON.stringify(e));
});*/
}
});
// Store location in Pebble app local storage
//
function storeLocation(position) {
var latitude = position.coords.latitude;
var longitude = position.coords.longitude;
var timestamp = position.timestamp;
localStorage.setItem("latitude", latitude);
localStorage.setItem("longitude", longitude);
localStorage.setItem("timestamp", timestamp);
// console.log("Stored location " + position.coords.latitude + ',' + position.coords.longitude);
}
// Get location from Pebble app local storage
//
function getLocation() {
if (localStorage.getItem("latitude") || localStorage.getItem("longitude") || localStorage.getItem("timestamp")) {
var la = localStorage.getItem("latitude");
var lo = localStorage.getItem("longitude");
var ti = localStorage.getItem("timestamp");
var co = { "latitude": la, "longitude": lo };
var pos = { "coords": co, "timestamp": ti };
// console.log("Stored location " + pos.co.la + ',' + pos.co.lo);
return pos;
} else {
return null;
}
}
// Get max speed of the run
//
function getMaxSpeed(lastSpeed) {
oldmax = localStorage.getItem("maxSpeed") || -1;
if (oldmax < lastSpeed) {
maxSpeed = lastSpeed
} else if (oldmax > lastSpeed) {
maxSpeed = oldmax
} else {
maxSpeed = oldmax
}
localStorage.setItem("maxSpeed", maxSpeed);
return maxSpeed
}
// split float number into an array of int (null returned instead of 0 for decimal)
//
function splitFloatNumber(num) {
const intStr = num.toString().split('.')[0];
const decimalStr = num.toString().split('.')[1];
return [Number(intStr), Number(decimalStr)];
}
// Build GPX headers
//
function GPXHeadersBuilder(timestamp, name, type) {
var headers = '<?xml version="1.0" encoding="UTF-8"?><gpx creator="Pebble with barometer" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd" version="1.1" xmlns="http://www.topografix.com/GPX/1/1"><metadata><time>' + timestamp + '</time></metadata><trk><name>' + name + '</name><type>' + type + '</type><trkseg>';
var ret = localStorage.setItem("GPX", headers);
return true;
}
// Build GPX track point
//
function GPXtrkptBuilder(lat, lon, ele, timestamp) {
var GPX = localStorage.getItem("GPX");
var trkpt = '<trkpt lat="' + lat + '" lon="' + lon + '"><ele>' + ele + '</ele><time>' + timestamp + '</time></trkpt>';
localStorage.setItem("GPX", GPX + trkpt);
return true;
}
// Build GPX footer
//
function GPXfooterBuilder() {
var GPX = localStorage.getItem("GPX");
var footer = '</trkseg></trk></gpx>';
var ret = localStorage.setItem("GPX", GPX + footer);
//console.log("GPX closed : " + GPX + footer);
return ret;
}
//------------------------------------------
// OAUTH functions
//------------------------------------------
function getTokens(code) {
// call to strava api to get tokens in exchange of temp code
// need to use strava.jonget.fr to proxy request and hide secret
var url = "https://www.strava.com/oauth/token?client_id=" + client_id + "&client_secret=" + client_secret + "&code=" + code + "&grant_type=authorization_code";
var xhr = new XMLHttpRequest();
xhr.timeout = 10000; // time in milliseconds
xhr.open("POST", url, false);
xhr.onload = function () {
//console.log('------xhr onloaded')
if (xhr.readyState === 4) {
console.log('------xhr request returned :', xhr.responseText);
response_json = JSON.parse(xhr.responseText);
var tokenjson = {
access_token: response_json.access_token,
refresh_token: response_json.refresh_token,
expiry: response_json.expires_at
};
localStorage.setItem("strava_tokens", JSON.stringify(tokenjson));
}
};
xhr.send();
}
function refreshTokens(refresh_token) {
// call to strava api to get tokens in exchange of refresh code
// need to use strava.jonget.fr to proxy request and hide secret
var url = "https://www.strava.com/oauth/token?client_id=" + client_id + "&client_secret=" + client_secret + "&refresh_token=" + refresh_token + "&grant_type=refresh_token";
var xhr = new XMLHttpRequest();
xhr.timeout = 10000; // time in milliseconds
xhr.open("POST", url, false);
xhr.onload = function () {
//console.log('------xhr onloaded')
if (xhr.readyState === 4) {
//console.log('------xhr request returned with ' + xhr.status);
response_json = JSON.parse(xhr.txt);
var tokenjson = {
access_token: response_json.access_token,
refresh_token: response_json.refresh_token,
expiry: response_json.expires_at
};
localStorage.setItem("strava_tokens", JSON.stringify(tokenjson));
}
};
xhr.send();
}
// Send GPX to Strava profile
function SendToStrava() {
console.log('--- GPX upload to strava');
var gpxfile = localStorage.getItem("GPX");
var tokens = localStorage.getItem("strava_tokens");
var bearer = JSON.parse(tokens).access_token;
params = {
url: "https://www.strava.com/api/v3/uploads",
method: "POST",
data: { description: "desc", data_type: "gpx" },
files: { file: gpxfile },
authorization: "Bearer " + bearer,
callback: function (e) {
var message = "";
// what is 'r'
// what is 'e'
// what is 'o'
// what is 'z'
if (console.log(e.status + " - " + e.txt), 201 == e.status) {
message = "Your activity has been created";
localStorage.setItem("strava_uploaded", true);
} else if (400 == e.status) {
message = "An error has occurred. If you've already uploaded the current activity, please delete it in Strava.";
} else if (401 == e.status) {
message = "Error - Unauthorized. Please check your credentials in the settings.", o.setAccessToken("");
} else {
try {
response_json = JSON.parse(e.txt)
response_json.error ? (console.log("error:" + response_json.error), message = response_json.error, o.setAccessToken("")) : response_json.status && (console.log("status:" + response_json.status), z = response_json.status)
} catch (e) {
console.log("Error log, " + e)
}
}
message && Pebble.showSimpleNotificationOnPebble("Ventoo SE - Strava", message)
}
}
var XHR = new XMLHttpRequest;
var n = this;
console.log(params.url);
XHR.open(params.method, params.url, !0);
var body = "";
var boundary = Math.random().toString().substring(2);
XHR.setRequestHeader("content-type", "multipart/form-data; charset=utf-8; boundary=" + boundary)
XHR.setRequestHeader("Authorization", params.authorization);
for (var i in params.data) body += "--" + boundary + '\r\nContent-Disposition: form-data; name="' + i + '"\r\n\r\n' + params.data[i] + "\r\n";
for (var i in params.files) body += "--" + boundary + '\r\nContent-Disposition: form-data; name="' + i + '" ; filename=test.gpx\r\n\r\n' + params.files[i] + "\r\n";
body += "--" + boundary + "--\r\n"
XHR.onreadystatechange = function () {
// what is 'n'
try {
4 == XHR.readyState && (n.status = XHR.status, n.txt = XHR.responseText, n.xml = XHR.responseXML, params.callback && params.callback(n))
} catch (e) {
console.error("Error2 loading, ", e)
}
}
XHR.send(body)
}
// Send GPX to web server (need configuration on serverside)
// TODO : secure it ?
function PostToWeb() {
console.log('--- GPX upload to custom web server');
var GPX = localStorage.getItem("GPX");
var url = JSON.parse(localStorage.getItem('clay-settings')).gpx_web_url + "?name=pebblegpx&type=application/gpx+xml";
var xhr = new XMLHttpRequest();
xhr.timeout = 10000; // time in milliseconds
xhr.open("POST", url, false);
//console.log('------ CSV / xhr opened')
xhr.onload = function () {
//console.log('------xhr onloaded')
if (xhr.readyState === 4) {
//console.log('------xhr request returned with ' + xhr.status);
//console.log(this.responseText);
localStorage.setItem("custom_uploaded", true);
if (xhr.status == 200) {
//console.log('--> HTTP 200');
return true;
} else {
//console.log('--> HTTP ' + xhr.status);
return false;
}
}
};
//send CSV in body
xhr.send(GPX);
}
// Send location to web server for instant location (no live tracking)
// TODO : secure it ?
function instantLocationUpdate(pos) {
console.log('--- Instant location update');
// console.log(" location is " + new_pos.coords.latitude + ',' + new_pos.coords.longitude + ' , acc. ' + new_pos.coords.accuracy);
var url = JSON.parse(localStorage.getItem('clay-settings')).ping_location_url + "?lat=" + pos.coords.latitude + "&long=" + pos.coords.longitude + "&acc=" + pos.coords.accuracy + "×tamp=" + pos.timestamp;
var xhr = new XMLHttpRequest();
xhr.timeout = 10000; // time in milliseconds
xhr.open("POST", url);
//console.log('------ instant / xhr opened')
xhr.onload = function () {
//console.log('------xhr onloaded')
if (xhr.readyState === 4) {
//console.log('------xhr request returned with ' + xhr.status);
//console.log(this.responseText);
if (xhr.status == 200) {
//console.log('--> HTTP 200');
return true;
} else {
//console.log('--> HTTP ' + xhr.status);
return false;
}
}
};
//send without body
xhr.send();
}
// called in case of successful geoloc gathering and sends the coordinate to watch
//
function locationSuccess(new_pos) {
console.log('--- locationSuccess');
// console.log(" location is " + new_pos.coords.latitude + ',' + new_pos.coords.longitude + ' , acc. ' + new_pos.coords.accuracy);
var prev_pos = getLocation();
storeLocation(new_pos);
if (prev_pos === null) {
console.log('--- start building gpx');
// Start the GPX file
GPXHeadersBuilder(new Date(new_pos.timestamp).toISOString(), "test", "18");
} else {
//clear watch of new position to avoid overlap
//navigator.geolocation.clearWatch(geoloc_id);
//console.log('--- watch geoloc cleared');
//var speed = speed_from_distance_and_time(prev_pos, new_pos);
//get speed from geoloc API isntead of calculate it
// speed is initially in m/s, get it at km/h
if (new_pos.coords.speed === null) {
var speed = 0;
} else {
var speed = new_pos.coords.speed * 3.6;
}
// Prepare display on watch
// now it's only raw data
// init strings
var latitudeString = "";
var longitudeString = "";
var accuracyString = "";
var altitudeString = "";
var speedString = "";
//formating for precision and max size
latitudeString = new_pos.coords.latitude.toString().substring(0, 12);
longitudeString = new_pos.coords.longitude.toString().substring(0, 12);
accuracyString = new_pos.coords.accuracy.toString().substring(0, 4);
//console.log("split num : " + new_pos.coords.altitude);
altitudeString = splitFloatNumber(new_pos.coords.altitude)[0].toString().substring(0, 5);
timestampISO = new Date(new_pos.timestamp).toISOString();
//console.log("split num : " + speed);
if (isNaN(speed)) {
speedString = "---";
} else {
speedString = splitFloatNumber(speed)[0].toString().substring(0, 3) + "." + splitFloatNumber(speed)[1].toString().substring(0, 1);
if (speedString == "0.N") {
speedString = "0.0";
}
//console.log("split num : " + getMaxSpeed(speed));
maxSpeedString = splitFloatNumber(getMaxSpeed(speed))[0].toString().substring(0, 3);
}
//add a new datapoint to GPX file
GPXtrkptBuilder(latitudeString, longitudeString, altitudeString, timestampISO);
// Build message
message = "OK";
var dict = {
'accuracy': accuracyString,
'altitude': altitudeString,
'speed': speedString,
'max_speed': maxSpeedString,
'status': message
};
// Send the message
Pebble.sendAppMessage(dict, function () {
console.log('Message sent successfully: ' + JSON.stringify(dict));
}, function (e) {
console.log('Message (' + JSON.stringify(dict) + ') failed: ' + JSON.stringify(e));
});
}
}
function locationError(err) {
console.warn('location error (' + err.code + '): ' + err.message);
}
function get_coordinate() {
console.log('--- Starting regular getCurrentPosition loop using setInterval at 1 sec');
locationInterval = setInterval(function () {
navigator.geolocation.getCurrentPosition(locationSuccess, locationError, locationOptions);
}, 1000);
if (locate_me) {
instantLocationInterval = setInterval(function () {
navigator.geolocation.getCurrentPosition(instantLocationUpdate, locationError, locationOptions);
}, 60000);
}
}
function init() {
// local storage init
try {
//console.log("Clay settings = " + localStorage.getItem('clay-settings'));
gpx_to_strava = JSON.parse(localStorage.getItem('clay-settings')).strava_enabled;
gpx_to_web = JSON.parse(localStorage.getItem('clay-settings')).gpx_web_enabled;
locate_me = JSON.parse(localStorage.getItem('clay-settings')).ping_location_enabled;
console.log("Locate_me = " + locate_me);
console.log("Strava = " + gpx_to_strava);
console.log("Custom web = " + gpx_to_web);
var ce = gpx_to_web;
var cu = localStorage.getItem("custom_uploaded");
var se = gpx_to_strava;
var su = localStorage.getItem("strava_uploaded");
//checking token freshness (expiry >4h)
var delay = (Date.now() + 14400000) / 1000
if (se) {
if (JSON.parse(localStorage.getItem("strava_tokens")).expiry < delay) {
console.log("Strava oAuth token expiring or expired, refreshing it")
refreshTokens(JSON.parse(localStorage.getItem("strava_tokens")).refresh_token);
} else {
console.log("token (" + JSON.parse(localStorage.getItem("strava_tokens")).access_token + ")valid for 4h min, continuing")
}
}
} catch (e) { }
if ((se && !su) || (ce && !cu)) {
//posting any missed XHR from previous ride session
if (ce) {
PostToWeb();
}
if (se) {
SendToStrava();
}
} else {
localStorage.setItem("GPX", "");
localStorage.setItem("maxSpeed", "");
localStorage.setItem("latitude", "");
localStorage.setItem("longitude", "");
localStorage.setItem("timestamp", "");
localStorage.setItem("custom_uploaded", false);
localStorage.setItem("strava_uploaded", false);
}
// clear any other var to do
clearInterval(locationInterval);
clearInterval(instantLocationInterval);
navigator.geolocation.getCurrentPosition(null, locationError, firstlocationOptions);
}
// Get JS readiness events
Pebble.addEventListener('ready', function (e) {
console.log('PebbleKit JS is ready');
// Update Watch on this
Pebble.sendAppMessage({ 'JSReady': 1 });
init();
});
// Get AppMessage events
Pebble.addEventListener('appmessage', function (e) {
// Get the dictionary from the message
var dict = e.payload;
//console.log(dict[0].toString());
switch (dict[0]) {
case 'get':
get_coordinate();
break;
case 'exit':
clearInterval(instantLocationInterval);
clearInterval(locationInterval);
gpx_to_strava = JSON.parse(localStorage.getItem('clay-settings')).strava_enabled;
gpx_to_web = JSON.parse(localStorage.getItem('clay-settings')).gpx_web_enabled;
locate_me = JSON.parse(localStorage.getItem('clay-settings')).ping_location_enabled;
//End GPX file
GPXfooterBuilder();
if (gpx_to_strava) {
//send to strava through API
SendToStrava();
}
if (gpx_to_web) {
// send CSV to web server through API
PostToWeb();
}
if (locate_me) {
var prev_pos = getLocation();
instantLocationUpdate(prev_pos);
}
// Send the message
var dict = {
'status': "EXIT"
};
Pebble.sendAppMessage(dict, function () {
console.log('Message sent successfully: ' + JSON.stringify(dict));
}, function (e) {
console.log('Message (' + JSON.stringify(dict) + ') failed: ' + JSON.stringify(e));
});
break;
default:
console.log('Sorry. I don\'t understand your request :' + dict[0]);
}
});