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 locate_me var firstlocationInterval = false; var locationInterval = false; var instantLocationInterval = false; // TODO to move to C 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': 10000, //10s 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") { 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 if(data = JSON.parse(t.response), data.strava_auth_code){ console.log("Manual entry of the OAuth code : "+data.strava_auth_code['value']) getTokens(data.strava_auth_code['value']); }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); var claysettings = JSON.parse(localStorage.getItem('clay-settings')) claysettings.strava_enabled = false; localStorage.setItem("clay-settings", JSON.stringify("claysettings")); console.log("Clay settings in Localstorage looks like " + localStorage.getItem("clay-settings")); } } }); // Calculate the distance from 2 geoloc in degrees. // IMPORTANT : this is a calculation from 2D projection, altitude is not involved // function distance_on_geoid(lat1, lon1, lat2, lon2) { // Convert degrees to radians lat1 = lat1 * Math.PI / 180.0; lon1 = lon1 * Math.PI / 180.0; lat2 = lat2 * Math.PI / 180.0; lon2 = lon2 * Math.PI / 180.0; // radius of earth in metres r = 6378100; // P rho1 = r * Math.cos(lat1); z1 = r * Math.sin(lat1); x1 = rho1 * Math.cos(lon1); y1 = rho1 * Math.sin(lon1); // Q rho2 = r * Math.cos(lat2); z2 = r * Math.sin(lat2); x2 = rho2 * Math.cos(lon2); y2 = rho2 * Math.sin(lon2); // Dot product dot = (x1 * x2 + y1 * y2 + z1 * z2); cos_theta = dot / (r * r); theta = Math.acos(cos_theta); // Distance in Metres return r * theta; } // Adding leading characters to string for nice displays // function padStart(string, max_length, padding) { if (string.length > max_length) { return string; } else { var new_str = string; for (index = string.length; index < max_length; index++) { new_str = padding + new_str; } return new_str; } } // Store location in Pebble app local storage // function storeLocation(position, first) { var latitude = position.coords.latitude; var longitude = position.coords.longitude; var timestamp = position.timestamp; if (first == true) { localStorage.setItem("firstlatitude", latitude); localStorage.setItem("firstlongitude", longitude); localStorage.setItem("firsttimestamp", Date.now()); //console.log("-- First timestamp: " + localStorage.getItem("firsttimestamp")); localStorage.setItem("totalcoordinates", 0) } localStorage.setItem("lastlatitude", latitude); localStorage.setItem("lastlongitude", longitude); localStorage.setItem("lasttimestamp", timestamp); localStorage.setItem("totalcoordinates", parseInt(localStorage.getItem("totalcoordinates")) + 1) // console.log("Stored location " + position.coords.latitude + ',' + position.coords.longitude); } // Get location from Pebble app local storage // function getLocation(first) { if (first == false) { if (localStorage.getItem("lastlatitude") || localStorage.getItem("lastlongitude") || localStorage.getItem("lasttimestamp")) { var la = localStorage.getItem("lastlatitude"); var lo = localStorage.getItem("lastlongitude"); var ti = localStorage.getItem("lasttimestamp"); var co = { "latitude": la, "longitude": lo }; var pos = { "coords": co, "timestamp": ti }; return pos; } else { return null; } } else { if (localStorage.getItem("firstlatitude") || localStorage.getItem("firstlongitude") || localStorage.getItem("firsttimestamp")) { var la = localStorage.getItem("firstlatitude"); var lo = localStorage.getItem("firstlongitude"); var ti = localStorage.getItem("firsttimestamp"); var co = { "latitude": la, "longitude": lo }; var pos = { "coords": co, "timestamp": ti }; 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]; var decimalStr = num.toString().split('.')[1]; if (decimalStr === undefined) { decimalStr = 0 } else { decimalStr = decimalStr } return [Number(intStr), Number(decimalStr)]; } // Build GPX headers // function GPXHeadersBuilder(timestamp, name, type) { var headers = '' + name + '' + type + ''; localStorage.setItem("GPX", headers); return true; } // Build GPX track point // function GPXtrkptBuilder(lat, lon, ele, timestamp) { var GPX = localStorage.getItem("GPX"); var trkpt = '' + ele + ''; localStorage.setItem("GPX", GPX + trkpt); return true; } // Build GPX footer // function GPXfooterBuilder() { var GPX = localStorage.getItem("GPX"); var footer = ''; var ret = localStorage.setItem("GPX", GPX + footer); console.log("GPX closed : " + localStorage.getItem("GPX")); 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.send(); if (xhr.status === 200) { 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, delay: response_json.expires_in }; localStorage.setItem("strava_tokens", JSON.stringify(tokenjson)); } } 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.send(); //console.log('------Refresh token - xhr onloaded') if (xhr.status === 200) { console.log('------Refresh token - xhr request returned with ' + 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, delay: response_json.expires_in }; localStorage.setItem("strava_tokens", JSON.stringify(tokenjson)); } } // Send GPX to Strava profile function SendToStrava() { console.log('--- GPX upload to strava'); var gpxfile = localStorage.getItem("GPX"); var tokens = localStorage.getItem("strava_tokens"); //checking token expiry var date = (Date.now()) / 1000 if (JSON.parse(tokens).expiry < date) { console.log("Strava oAuth token expired, refreshing it") refreshTokens(JSON.parse(tokens).refresh_token); } else { console.log("token (" + JSON.parse(tokens).access_token + ") valid, continuing") } var bearer = JSON.parse(tokens).access_token; params = { url: "https://www.strava.com/api/v3/uploads", method: "POST", data: { description: "desc", data_type: "gpx", sport_type: "Ride" , activity_types: "Ride"}, files: { file: gpxfile }, authorization: "Bearer " + bearer, callback: function (e) { var message = ""; if (console.log(e.status + " - " + e.txt), 201 == e.status) { message = "S200"; localStorage.setItem("strava_uploaded", true); } else if (400 == e.status) { message = "S400"; } else if (401 == e.status) { //token expired, retrying message = "S401" SendToStrava() } else { try { response_json = JSON.parse(e.txt) response_json.error ? (console.log("error:" + response_json.error), message = response_json.error) : response_json.status && (console.log("status:" + response_json.status)) } catch (err) { console.log("Error log, " + err) } message = "S500"; } //message && Pebble.showSimpleNotificationOnPebble("Ventoo SE - Strava", message) // Build message var dict = { '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)); }); } } 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() { 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 GPX 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(" Instant location is " + pos.coords.latitude + ',' + pos.coords.longitude + ' , acc. ' + 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(false); var first_pos = getLocation(true); if (prev_pos === null) { console.log('--- start building gpx'); storeLocation(new_pos, true); localStorage.setItem("strava_uploaded", false); localStorage.setItem("custom_uploaded", false); // Start the GPX file GPXHeadersBuilder(new Date(new_pos.timestamp).toISOString(), "Pebble track", "18"); } else { storeLocation(new_pos, false); // Prepare display on watch // now it's only raw data // init strings var latitudeString = ""; var longitudeString = ""; var accuracyString = ""; var altitudeString = ""; var speedString = ""; var distanceString = ""; // 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; localStorage.setItem("speedsum", parseInt(localStorage.getItem("speedsum")) + speed); } // distance since beginning in m var dist = distance_on_geoid(prev_pos.coords.latitude, prev_pos.coords.longitude, new_pos.coords.latitude, new_pos.coords.longitude); var totaldist = parseInt(localStorage.getItem("dist")); if (!isNaN(dist)) { totaldist = totaldist + parseInt(dist); localStorage.setItem("dist", totaldist); }else{ console.log("[NaN] dist: "+dist) } distanceString = splitFloatNumber(totaldist / 1000)[0].toString() + "." + splitFloatNumber(totaldist / 1000)[1].toString().substring(0, 3); //console.log("total dist is now " + totaldist); // avg speed (also when not moving) since beginning var avgspeed = parseInt(localStorage.getItem("speedsum")) / parseInt(localStorage.getItem("totalcoordinates")); var avgSpeedString = splitFloatNumber(avgspeed)[0].toString() + "." + splitFloatNumber(avgspeed)[1].toString().substring(0, 1); console.log("speedsum=" + parseInt(localStorage.getItem("speedsum")) + " / totalcoordinates=" + parseInt(localStorage.getItem("totalcoordinates"))); console.log("--avgspeed=" + avgspeed + " / avgSpeedString=" + avgSpeedString) if (avgSpeedString == "0.N") { console.log("[NaN] avgspeed: "+avgspeed+" / speedsum: "+localStorage.getItem("speedsum")+" / totalcoordinates: "+localStorage.getItem("totalcoordinates")) avgSpeedString = "-.-"; } //console.log("avg speed is : " + avgSpeedString); // Duration var duration = new_pos.timestamp - first_pos.timestamp; const date = new Date(duration); durationString = padStart(date.getUTCHours().toString(), 2, "0") + ":" + padStart(date.getMinutes().toString(), 2, "0") + ":" + padStart(date.getSeconds().toString(), 2, "0"); console.log("durationString is : " + durationString); //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") { console.log("[NaN] speed: "+speed+" / speedString: "+speedString) 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 = "L200"; var dict = { 'accuracy': accuracyString, 'distance': distanceString, 'avg_speed': avgSpeedString, 'duration': durationString, '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 start_get_coordinate() { clearInterval(firstlocationInterval); firstlocationInterval = false; 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() { clearInterval(locationInterval); locationInterval = false; clearInterval(instantLocationInterval); instantLocationInterval = false; firstlocationInterval = setInterval(function() { navigator.geolocation.getCurrentPosition(null, locationError, firstlocationOptions); }, 1000); //console.log("Clay settings = " + localStorage.getItem('clay-settings')); var se = JSON.parse(localStorage.getItem('clay-settings')).strava_enabled; var su = ("true" === localStorage.getItem("strava_uploaded")); var ce = JSON.parse(localStorage.getItem('clay-settings')).gpx_web_enabled; var cu = ("true" === localStorage.getItem("custom_uploaded")); locate_me = JSON.parse(localStorage.getItem('clay-settings')).ping_location_enabled; console.log("Locate_me = " + locate_me); console.log("Strava = " + se + " (" + typeof se + ")/ uploaded = " + su + " (" + typeof su + ")"); console.log("Custom web = " + ce + " (" + typeof ce + ")/ uploaded = " + cu + " (" + typeof cu + ")"); if ((se && !su) || (ce && !cu)) { var GPX = localStorage.getItem("GPX"); console.log("last 6 char of GPX are " + GPX.substring(GPX.length - 6, GPX.length)) if (GPX.substring(GPX.length - 6, GPX.length) !== "") { console.log("WARNING - NO GPX FOOTER ") /*var footer = ''; localStorage.setItem("GPX", GPX + footer);*/ GPXfooterBuilder(); GPX = localStorage.getItem("GPX"); console.log("GPX FOOTER is now : " + GPX.substring(GPX.length - 6, GPX.length)) } if (ce) { console.log("GPX upload needed to custom server") PostToWeb(); } if (se) { console.log("GPX upload needed to Strava") SendToStrava(); } } else { console.log("clearing var") localStorage.setItem("maxSpeed", 0); localStorage.setItem("firstlatitude", ""); localStorage.setItem("firstlongitude", ""); localStorage.setItem("firsttimestamp", ""); localStorage.setItem("lastlatitude", ""); localStorage.setItem("lastlongitude", ""); localStorage.setItem("lasttimestamp", ""); localStorage.setItem("dist", 0) localStorage.setItem("speedsum", 0) } } // 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 'startstop': if (!locationInterval == false) { console.log("Stopping the track"); clearInterval(locationInterval); locationInterval = false; clearInterval(instantLocationInterval); instantLocationInterval = false; /*firstlocationInterval = setInterval(function () { navigator.geolocation.getCurrentPosition(null, locationError, firstlocationOptions); }, 1000);*/ } else { console.log("Starting the track"); start_get_coordinate(); } break; case 'send': if (locate_me) { var prev_pos = getLocation(false); instantLocationUpdate(prev_pos); } init(); break; default: console.log('Sorry. I don\'t understand your request :' + dict[0]); } });