initial commit
|
@ -0,0 +1,574 @@
|
||||||
|
import document from "document";
|
||||||
|
import clock from "clock";
|
||||||
|
import * as messaging from "messaging";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import { me } from "appbit";
|
||||||
|
import { me as device } from "device";
|
||||||
|
import { Accelerometer } from "accelerometer";
|
||||||
|
import { HeartRateSensor } from "heart-rate";
|
||||||
|
import { today } from "user-activity";
|
||||||
|
import { goals } from "user-activity";
|
||||||
|
import { user } from "user-profile";
|
||||||
|
import { vibration } from "haptics";
|
||||||
|
import { display } from "display";
|
||||||
|
import { preferences } from "user-settings";
|
||||||
|
|
||||||
|
if (!device.screen) device.screen = { width: 348, height: 250 };
|
||||||
|
|
||||||
|
import * as util from "../common/utils";
|
||||||
|
|
||||||
|
const DEBUG = false;
|
||||||
|
const COLORDEBUG = false;
|
||||||
|
|
||||||
|
let accel = new Accelerometer({ frequency: 4 });
|
||||||
|
let hrm = new HeartRateSensor;
|
||||||
|
|
||||||
|
let touchArea = document.getElementById('touchArea');
|
||||||
|
let overlay = document.getElementById('overlay');
|
||||||
|
let mainHealthText = document.getElementById('mainHealthText');
|
||||||
|
let timeElements = document.getElementsByClassName('time');
|
||||||
|
let healthTextInstances = document.getElementsByClassName('healthTextInstance');
|
||||||
|
let timeElementsNum = timeElements.length;
|
||||||
|
|
||||||
|
let averageAccelZ = 10.0;
|
||||||
|
let lastAccelZ = 10.0;
|
||||||
|
let hasOverlay = false;
|
||||||
|
let overlayTimer;
|
||||||
|
let sublineTimer;
|
||||||
|
let colorTimer;
|
||||||
|
let saveTimer;
|
||||||
|
let hrTimer;
|
||||||
|
|
||||||
|
let currentColor;
|
||||||
|
|
||||||
|
let hiddenSubline = false;
|
||||||
|
|
||||||
|
const smoothingFactor = 0.2;
|
||||||
|
|
||||||
|
const colorGreen = [0.525,1.0,0.0];
|
||||||
|
const colorYellow = [0.3,1.0,0.0];
|
||||||
|
const colorOrange = [0.213,1.0,0.0];
|
||||||
|
const colorRed = [0.108,1.0,0.5];
|
||||||
|
const colorNeutral = [0.632,0.8,0.7];
|
||||||
|
|
||||||
|
const placeholdertext = '· · · ';
|
||||||
|
let hrLabel = placeholdertext;
|
||||||
|
|
||||||
|
const sublines = ['-', 'date', 'steps', 'cal', 'dist', 'elev', 'actmin', 'hr'];
|
||||||
|
|
||||||
|
const defaults = {leadingZero: false, secondary: 'steps', tap: 'overlay', colors: 'manual', customcolor:['0.632','0.8','0.7'], dateformat: 'md/', colorgoal: 'steps'};
|
||||||
|
|
||||||
|
let userSettings;
|
||||||
|
|
||||||
|
try {
|
||||||
|
//console.log("Reading settings");
|
||||||
|
userSettings = fs.readFileSync("echocentric_settings.json", "json");
|
||||||
|
} catch (e) {
|
||||||
|
//console.log("No settings found. Using default settings");
|
||||||
|
userSettings = defaults;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeSettings() {
|
||||||
|
//console.log("Writing settings");
|
||||||
|
fs.writeFileSync("echocentric_settings.json", userSettings, "json");
|
||||||
|
}
|
||||||
|
|
||||||
|
me.onunload = () => {
|
||||||
|
//console.log("Unloading. Good Bye");
|
||||||
|
writeSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
messaging.peerSocket.onmessage = e => {
|
||||||
|
//console.log("Message received -> "+e.data.key+": "+e.data.newValue);
|
||||||
|
display.poke();
|
||||||
|
vibration.start("bump");
|
||||||
|
let doClockUpdate = false;
|
||||||
|
let doSublineChange = false;
|
||||||
|
let doSublineUpdate = false;
|
||||||
|
let doColorUpdate = false;
|
||||||
|
switch (e.data.key) {
|
||||||
|
case 'leadingZero':
|
||||||
|
userSettings.leadingZero = (e.data.newValue == "true");
|
||||||
|
doClockUpdate = true;
|
||||||
|
break;
|
||||||
|
case 'secondary':
|
||||||
|
let oldval = userSettings.secondary;
|
||||||
|
let newval = JSON.parse(e.data.newValue).values[0].value;
|
||||||
|
if (oldval != newval) {
|
||||||
|
userSettings.secondary = newval;
|
||||||
|
doSublineChange = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'tap':
|
||||||
|
userSettings.tap = JSON.parse(e.data.newValue).values[0].value;
|
||||||
|
break;
|
||||||
|
case 'colors':
|
||||||
|
userSettings.colors = JSON.parse(e.data.newValue).values[0].value;
|
||||||
|
doColorUpdate = true;
|
||||||
|
break;
|
||||||
|
case 'customcolor':
|
||||||
|
userSettings.customcolor = e.data.newValue.replace(/["']/g, "").split(',');
|
||||||
|
doColorUpdate = true;
|
||||||
|
break;
|
||||||
|
case 'colorgoal':
|
||||||
|
userSettings.colorgoal = JSON.parse(e.data.newValue).values[0].value;
|
||||||
|
doColorUpdate = true;
|
||||||
|
if (userSettings.colorgoal === 'hr') {
|
||||||
|
doClockUpdate = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'dateformat':
|
||||||
|
userSettings.dateformat = JSON.parse(e.data.newValue).values[0].value;
|
||||||
|
doSublineUpdate = true;
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
clearTimeout(saveTimer);
|
||||||
|
saveTimer = setTimeout(() => {
|
||||||
|
writeSettings();
|
||||||
|
}, 8000);
|
||||||
|
|
||||||
|
if (doColorUpdate) { updateColors(false); }
|
||||||
|
if (doClockUpdate) { updateClock(); }
|
||||||
|
if (doSublineChange) { changeSubline(); }
|
||||||
|
if (doSublineUpdate) { updateSubline(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
messaging.peerSocket.onopen = () => {
|
||||||
|
//console.log("Clockface opened socket");
|
||||||
|
};
|
||||||
|
|
||||||
|
messaging.peerSocket.close = () => {
|
||||||
|
//console.log("Clockface closed socket");
|
||||||
|
};
|
||||||
|
|
||||||
|
messaging.peerSocket.onerror = (err) => {
|
||||||
|
//console.log("Clockface socket error: " + err.code + " - " + err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendSetting() {
|
||||||
|
if (messaging.peerSocket.readyState === messaging.peerSocket.OPEN) {
|
||||||
|
messaging.peerSocket.send(userSettings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
display.onchange = () => {
|
||||||
|
if (display.on) {
|
||||||
|
accel.start();
|
||||||
|
averageAccelZ = lastAccelZ = Math.max(accel.z,5);
|
||||||
|
callback();
|
||||||
|
if (hiddenSubline === false) {
|
||||||
|
mainHealthText.animate('enable');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
accel.stop();
|
||||||
|
if (hiddenSubline === false) {
|
||||||
|
mainHealthText.animate('disable');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateClock() {
|
||||||
|
let currDate = new Date();
|
||||||
|
let hours = currDate.getHours();
|
||||||
|
let displayHours = hours;
|
||||||
|
if (preferences.clockDisplay === '12h') {
|
||||||
|
displayHours = displayHours % 12;
|
||||||
|
displayHours = displayHours ? displayHours : 12;
|
||||||
|
}
|
||||||
|
if (userSettings.leadingZero === true) {
|
||||||
|
displayHours = util.zeroPad(displayHours);
|
||||||
|
}
|
||||||
|
let mins = util.zeroPad(currDate.getMinutes());
|
||||||
|
let timeString = `${displayHours}:${mins}`;
|
||||||
|
for(var i = 0; i < timeElementsNum; i++) {
|
||||||
|
timeElements[i].text = timeString;
|
||||||
|
}
|
||||||
|
if (userSettings.colors === 'random') {
|
||||||
|
updateColors(false);
|
||||||
|
}
|
||||||
|
updateSubline();
|
||||||
|
switch (userSettings.secondary) {
|
||||||
|
case 'steps':
|
||||||
|
case 'cal':
|
||||||
|
case 'dist':
|
||||||
|
case 'elev':
|
||||||
|
case 'actmin':
|
||||||
|
case 'hr':
|
||||||
|
clearInterval(sublineTimer);
|
||||||
|
sublineTimer = setInterval(() => {
|
||||||
|
updateSubline();
|
||||||
|
},2000);
|
||||||
|
}
|
||||||
|
if (userSettings.colorgoal === 'hr') {
|
||||||
|
clearInterval(colorTimer);
|
||||||
|
colorTimer = setInterval(() => {
|
||||||
|
updateColors(false);
|
||||||
|
},2000);
|
||||||
|
}
|
||||||
|
if (userSettings.colors === 'auto') {
|
||||||
|
updateColors(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSubline() {
|
||||||
|
if (userSettings.secondary === 'hr') {
|
||||||
|
mainHealthText.getElementById('healthTextLabel').text = hrLabel;
|
||||||
|
hrm.start();
|
||||||
|
} else {
|
||||||
|
let sublineText = getActivityText(userSettings.secondary);
|
||||||
|
if (mainHealthText.getElementById('healthTextLabel').text !== sublineText)
|
||||||
|
mainHealthText.getElementById('healthTextLabel').text = sublineText;
|
||||||
|
}
|
||||||
|
if (display.on === false) {
|
||||||
|
clearInterval(sublineTimer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActivityNumber(activity) {
|
||||||
|
let values = {raw: 0, display: 0};
|
||||||
|
|
||||||
|
switch (activity) {
|
||||||
|
case 'steps':
|
||||||
|
values.raw = (today.adjusted.steps || 0);
|
||||||
|
values.display = util.addCommas(values.raw);
|
||||||
|
break;
|
||||||
|
case 'cal':
|
||||||
|
values.raw = (today.adjusted.calories || 0);
|
||||||
|
values.display = util.addCommas(values.raw);
|
||||||
|
break;
|
||||||
|
case 'dist':
|
||||||
|
values.raw = (today.adjusted.distance || 0);
|
||||||
|
values.display = (Math.round(values.raw/10)/100 || 0);
|
||||||
|
break;
|
||||||
|
case 'elev':
|
||||||
|
values.raw = values.display = (today.adjusted.elevationGain || 0);
|
||||||
|
break;
|
||||||
|
case 'actmin':
|
||||||
|
values.raw = (today.adjusted.activeMinutes || 0);
|
||||||
|
values.display = util.addCommas(values.raw);
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActivityText(activity) {
|
||||||
|
let label = placeholdertext;
|
||||||
|
switch (activity) {
|
||||||
|
case 'hr':
|
||||||
|
label = hrLabel;
|
||||||
|
break;
|
||||||
|
case 'steps':
|
||||||
|
case 'cal':
|
||||||
|
case 'actmin':
|
||||||
|
case 'dist':
|
||||||
|
case 'elev':
|
||||||
|
label = getActivityNumber(activity).display;
|
||||||
|
break;
|
||||||
|
case 'date':
|
||||||
|
let currDate = new Date();
|
||||||
|
let day = currDate.getDate();
|
||||||
|
let month = currDate.getMonth()+1;
|
||||||
|
|
||||||
|
let zeroPadded = false;
|
||||||
|
let ddmm = false;
|
||||||
|
let delimiter = '/';
|
||||||
|
let endsWithDelimiter = false;
|
||||||
|
|
||||||
|
if (userSettings.dateformat[0] === 'd') {
|
||||||
|
ddmm = true;
|
||||||
|
}
|
||||||
|
delimiter = userSettings.dateformat[2];
|
||||||
|
endsWithDelimiter = ((delimiter === '.') ? true : false);
|
||||||
|
|
||||||
|
if (zeroPadded) {
|
||||||
|
day = util.zeroPad(day);
|
||||||
|
month = util.zeroPad(month);
|
||||||
|
}
|
||||||
|
if (ddmm) {
|
||||||
|
label = day+delimiter+month+(endsWithDelimiter ? delimiter : '');
|
||||||
|
} else {
|
||||||
|
label = month+delimiter+day+(endsWithDelimiter ? delimiter : '');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
function activityGoalColor(activity, heartRate) {
|
||||||
|
let level = colorNeutral;
|
||||||
|
if (heartRate !== false) {
|
||||||
|
let hrZone = (user.heartRateZone(heartRate) || 'none');
|
||||||
|
switch (hrZone) {
|
||||||
|
case 'out-of-range':
|
||||||
|
case 'below-custom':
|
||||||
|
level = colorGreen;
|
||||||
|
break;
|
||||||
|
case 'fat-burn':
|
||||||
|
case 'custom':
|
||||||
|
level = colorYellow;
|
||||||
|
break;
|
||||||
|
case 'cardio':
|
||||||
|
level = colorOrange;
|
||||||
|
break;
|
||||||
|
case 'above-custom':
|
||||||
|
case 'peak':
|
||||||
|
level = colorRed;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return level;
|
||||||
|
} else {
|
||||||
|
if (activity === 'hr') {
|
||||||
|
hrm.start();
|
||||||
|
} else {
|
||||||
|
let goal = 0;
|
||||||
|
switch (activity) {
|
||||||
|
case 'steps':
|
||||||
|
goal = (goals.steps || 0);
|
||||||
|
break;
|
||||||
|
case 'cal':
|
||||||
|
goal = (goals.calories || 0);
|
||||||
|
break;
|
||||||
|
case 'dist':
|
||||||
|
goal = (goals.distance || 0);
|
||||||
|
break;
|
||||||
|
case 'elev':
|
||||||
|
goal = (Math.floor(goals.elevationGain/10) || 0);
|
||||||
|
break;
|
||||||
|
case 'actmin':
|
||||||
|
goal = (goals.activeMinutes || 0);
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
let current = getActivityNumber(activity).raw;
|
||||||
|
//console.log('Goal for '+activity+' is '+current+' of '+goal+' = '+(current/goal));
|
||||||
|
if (goal > 0) {
|
||||||
|
let goalPercentage = current/goal;
|
||||||
|
if (goalPercentage > 1) {
|
||||||
|
level = colorGreen;
|
||||||
|
} else if (goalPercentage > 0.666) {
|
||||||
|
level = util.interpolateColors(goalPercentage, 0.666, 1, colorYellow, colorGreen);
|
||||||
|
} else if (goalPercentage > 0.333) {
|
||||||
|
level = util.interpolateColors(goalPercentage, 0.333, 0.666, colorOrange, colorYellow);
|
||||||
|
} else {
|
||||||
|
level = util.interpolateColors(goalPercentage, 0, 0.333, colorRed, colorOrange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return level;
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeSubline() {
|
||||||
|
if (hiddenSubline === false) {
|
||||||
|
mainHealthText.animate('disable');
|
||||||
|
}
|
||||||
|
let href = '';
|
||||||
|
switch (userSettings.secondary) {
|
||||||
|
case 'date':
|
||||||
|
case 'steps':
|
||||||
|
case 'cal':
|
||||||
|
case 'dist':
|
||||||
|
case 'elev':
|
||||||
|
case 'actmin':
|
||||||
|
href = 'img/icon-'+userSettings.secondary+'.png';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
setTimeout(() => {
|
||||||
|
updateSubline();
|
||||||
|
if (userSettings.secondary === 'hr') { hrm.start(); }
|
||||||
|
mainHealthText.getElementById('healthTextIcon').href = href;
|
||||||
|
if (userSettings.secondary === 'hr') {
|
||||||
|
mainHealthText.getElementById("healthTextHrIcon").style.display="inline";
|
||||||
|
} else {
|
||||||
|
mainHealthText.getElementById("healthTextHrIcon").style.display="none";
|
||||||
|
}
|
||||||
|
if (userSettings.secondary !== '-') {
|
||||||
|
mainHealthText.animate('enable');
|
||||||
|
hiddenSubline = false;
|
||||||
|
} else {
|
||||||
|
hiddenSubline = true;
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateColors(color) {
|
||||||
|
let newColor = false;
|
||||||
|
if (color !== false) {
|
||||||
|
newColor = color;
|
||||||
|
} else {
|
||||||
|
switch (userSettings.colors) {
|
||||||
|
case 'auto':
|
||||||
|
let activityColor = activityGoalColor(userSettings.colorgoal, false);
|
||||||
|
if (userSettings.colorgoal !== 'hr') {
|
||||||
|
newColor = activityColor;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'manual':
|
||||||
|
newColor = userSettings.customcolor;
|
||||||
|
break;
|
||||||
|
case 'random':
|
||||||
|
default:
|
||||||
|
newColor = [Math.random(),1,0.5];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (COLORDEBUG) {
|
||||||
|
newColor = [Math.random(),1,0];
|
||||||
|
}
|
||||||
|
//console.log('Color comparison '+newColor+' == '+currentColor+': '+(newColor == currentColor));
|
||||||
|
if (newColor !== false && newColor != currentColor) {
|
||||||
|
//console.log('Color was changed');
|
||||||
|
currentColor = newColor;
|
||||||
|
for(var i = 0; i < timeElementsNum-1; i++) {
|
||||||
|
let fraction = i/timeElementsNum;
|
||||||
|
timeElements[i].style.fill = util.hslToHex(newColor[0]-fraction*0.3, newColor[1]-fraction*newColor[2], fraction*0.74+0.15);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//console.log('Color is unchanged');
|
||||||
|
}
|
||||||
|
if (display.on === false) {
|
||||||
|
clearInterval(colorTimer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPositions(offset) {
|
||||||
|
for(var i = 0; i < timeElementsNum; i++) {
|
||||||
|
let ypos = ((1-(offset-9))*5*(timeElementsNum-0.5-i))+20;
|
||||||
|
timeElements[i].y = ypos;
|
||||||
|
if (i == timeElementsNum-1) {
|
||||||
|
mainHealthText.y = ypos+160+((device.screen.height-250)/2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hrm.onreading = () => {
|
||||||
|
//console.log("Reading heart rate");
|
||||||
|
if (display.on) {
|
||||||
|
let heartRate = (hrm.heartRate || 0);
|
||||||
|
hrLabel = heartRate;
|
||||||
|
document.getElementById('hrText').getElementById('healthTextLabel').text = heartRate;
|
||||||
|
if (userSettings.secondary == 'hr') {
|
||||||
|
mainHealthText.getElementById('healthTextLabel').text = heartRate;
|
||||||
|
}
|
||||||
|
if (userSettings.colorgoal === 'hr') {
|
||||||
|
let color = activityGoalColor('hr', heartRate);
|
||||||
|
//console.log('got HR color from hr '+heartRate+': '+color);
|
||||||
|
updateColors(color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clearTimeout(hrTimer);
|
||||||
|
hrTimer = setTimeout(() => {
|
||||||
|
hrLabel = placeholdertext;
|
||||||
|
}, 1500);
|
||||||
|
hrm.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateOverlay() {
|
||||||
|
document.getElementById('hrText').getElementById('healthTextLabel').text = hrLabel;
|
||||||
|
hrm.start();
|
||||||
|
document.getElementById('stepsText').getElementById('healthTextLabel').text = getActivityText('steps');
|
||||||
|
document.getElementById('calText').getElementById('healthTextLabel').text = getActivityText('cal');
|
||||||
|
document.getElementById('distText').getElementById('healthTextLabel').text = getActivityText('dist');
|
||||||
|
document.getElementById('elevText').getElementById('healthTextLabel').text = getActivityText('elev');
|
||||||
|
document.getElementById('actminText').getElementById('healthTextLabel').text = getActivityText('actmin');
|
||||||
|
}
|
||||||
|
|
||||||
|
touchArea.onclick = (e) => {
|
||||||
|
if (COLORDEBUG) {
|
||||||
|
updateColors(false);
|
||||||
|
} else {
|
||||||
|
switch (userSettings.tap) {
|
||||||
|
case 'overlay':
|
||||||
|
if (hasOverlay == false) {
|
||||||
|
vibration.start("bump");
|
||||||
|
addOverlay();
|
||||||
|
} else {
|
||||||
|
removeOverlay();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'cycle':
|
||||||
|
vibration.start("bump");
|
||||||
|
let next = sublines[0];
|
||||||
|
let index = sublines.indexOf(userSettings.secondary);
|
||||||
|
if(index >= 0 && index < sublines.length - 1) {
|
||||||
|
next = sublines[index + 1];
|
||||||
|
}
|
||||||
|
userSettings.secondary = next;
|
||||||
|
changeSubline();
|
||||||
|
sendSetting();
|
||||||
|
clearTimeout(saveTimer);
|
||||||
|
saveTimer = setTimeout(() => {
|
||||||
|
writeSettings();
|
||||||
|
}, 8000);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addOverlay() {
|
||||||
|
updateOverlay();
|
||||||
|
overlay.style.display = 'inline';
|
||||||
|
hasOverlay = true;
|
||||||
|
overlayTimer = setTimeout(() => {
|
||||||
|
removeOverlay();
|
||||||
|
},6000);
|
||||||
|
document.getElementById('overlayShadeInstance').animate('enable');
|
||||||
|
for(var i = 0; i < healthTextInstances.length; i++) {
|
||||||
|
let index = i;
|
||||||
|
setTimeout(() => {
|
||||||
|
healthTextInstances[index].animate('enable');
|
||||||
|
}, 200+index*100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeOverlay() {
|
||||||
|
clearTimeout(overlayTimer);
|
||||||
|
document.getElementById('overlayShadeInstance').animate('disable');
|
||||||
|
for(var i = 0; i < healthTextInstances.length; i++) {
|
||||||
|
healthTextInstances[i].animate('disable');
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
overlay.style.display = 'none';
|
||||||
|
}, 100);
|
||||||
|
hasOverlay = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
accel.onreading = () => {
|
||||||
|
//console.log(accel.z);
|
||||||
|
if (hasOverlay == false) {
|
||||||
|
lastAccelZ = Math.max(accel.z,5);
|
||||||
|
}
|
||||||
|
//accel.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
function callback(timestamp) {
|
||||||
|
if (hasOverlay == false) {
|
||||||
|
averageAccelZ = (lastAccelZ*smoothingFactor)+(averageAccelZ*(1.0-smoothingFactor));
|
||||||
|
setPositions(averageAccelZ);
|
||||||
|
requestAnimationFrame(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateColors(false);
|
||||||
|
|
||||||
|
changeSubline();
|
||||||
|
|
||||||
|
clock.granularity = "minutes";
|
||||||
|
clock.ontick = () => updateClock();
|
||||||
|
|
||||||
|
document.getElementById("hrText").getElementById("healthTextHrIcon").style.display="inline";
|
||||||
|
|
||||||
|
updateClock();
|
||||||
|
updateOverlay();
|
||||||
|
|
||||||
|
requestAnimationFrame(callback);
|
||||||
|
accel.start();
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
display.autoOff = false;
|
||||||
|
display.on = true;
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
function map(value, sourceBottom, sourceTop, targetBottom, targetTop) {
|
||||||
|
let valueRatio = (value-sourceBottom)/(sourceTop-sourceBottom);
|
||||||
|
return ((targetTop-targetBottom)*valueRatio)+targetBottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function interpolateColors(value, sourceBottom, sourceTop, color1, color2) {
|
||||||
|
let newColor = [
|
||||||
|
map(value, sourceBottom, sourceTop, color1[0], color2[0]),
|
||||||
|
map(value, sourceBottom, sourceTop, color1[1], color2[1]),
|
||||||
|
map(value, sourceBottom, sourceTop, color1[2], color2[2])
|
||||||
|
];
|
||||||
|
return newColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addCommas(nStr) {
|
||||||
|
if (nStr < 1000) { return nStr; }
|
||||||
|
nStr += '';
|
||||||
|
let x = nStr.split('.');
|
||||||
|
let x1 = x[0];
|
||||||
|
let x2 = x.length > 1 ? '.' + x[1] : '';
|
||||||
|
let rgx = /(\d+)(\d{3})/;
|
||||||
|
while (rgx.test(x1)) {
|
||||||
|
x1 = x1.replace(rgx, '$1' + ',' + '$2');
|
||||||
|
}
|
||||||
|
return x1 + x2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add zero in front of numbers < 10
|
||||||
|
export function zeroPad(i) {
|
||||||
|
if (i < 10) {
|
||||||
|
i = "0" + i;
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an HSL color value to RGB. Conversion formula
|
||||||
|
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
|
||||||
|
* Assumes h, s, and l are contained in the set [0, 1] and
|
||||||
|
* returns r, g, and b in the set [0, 255].
|
||||||
|
*
|
||||||
|
* @param {number} h The hue
|
||||||
|
* @param {number} s The saturation
|
||||||
|
* @param {number} l The lightness
|
||||||
|
* @return {Array} The RGB representation
|
||||||
|
*/
|
||||||
|
function hslToRgb(h, s, l) {
|
||||||
|
var r, g, b;
|
||||||
|
|
||||||
|
if (s == 0) {
|
||||||
|
r = g = b = l; // achromatic
|
||||||
|
} else {
|
||||||
|
var hue2rgb = function hue2rgb (p, q, t) {
|
||||||
|
if(t < 0) t += 1;
|
||||||
|
if(t > 1) t -= 1;
|
||||||
|
if(t < 1/6) return p + (q - p) * 6 * t;
|
||||||
|
if(t < 1/2) return q;
|
||||||
|
if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
||||||
|
var p = 2 * l - q;
|
||||||
|
r = hue2rgb(p, q, h + 1/3);
|
||||||
|
g = hue2rgb(p, q, h);
|
||||||
|
b = hue2rgb(p, q, h - 1/3);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function rgbToHex(r, g, b) {
|
||||||
|
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hslToHex(h, s, l) {
|
||||||
|
let rgb = hslToRgb(h, s, l);
|
||||||
|
return rgbToHex(rgb[0],rgb[1],rgb[2]);
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
import * as messaging from "messaging";
|
||||||
|
import { settingsStorage } from "settings";
|
||||||
|
|
||||||
|
//console.log("Companion Started");
|
||||||
|
|
||||||
|
//settingsStorage.setItem('colors', Json.stringify({}));
|
||||||
|
//settingsStorage.setItem('secondary', 'cal');
|
||||||
|
|
||||||
|
// Message socket opens
|
||||||
|
messaging.peerSocket.onopen = () => {
|
||||||
|
//console.log("Companion opened socket");
|
||||||
|
//restoreSettings();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Message socket closes
|
||||||
|
messaging.peerSocket.close = () => {
|
||||||
|
//console.log("Companion closed Socket");
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
messaging.peerSocket.onmessage = (evt) => {
|
||||||
|
// Output the message to the console
|
||||||
|
console.log(JSON.stringify(evt.data));
|
||||||
|
Object.keys(evt.data).forEach((k) => {
|
||||||
|
console.log(k + ' - ' + evt.data[k]);
|
||||||
|
//settingsStorage.setItem(k, evt.data[k]);
|
||||||
|
});
|
||||||
|
//settingsStorage.setItem('secondary', 'cal');
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
// A user changes settings
|
||||||
|
settingsStorage.onchange = evt => {
|
||||||
|
let data = {
|
||||||
|
key: evt.key,
|
||||||
|
newValue: evt.newValue
|
||||||
|
};
|
||||||
|
switch (evt.key) {
|
||||||
|
case 'button':
|
||||||
|
// perform magic here
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
sendVal(data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Restore any previously saved settings and send to the device
|
||||||
|
function restoreSettings() {
|
||||||
|
for (let index = 0; index < settingsStorage.length; index++) {
|
||||||
|
let key = settingsStorage.key(index);
|
||||||
|
if (key) {
|
||||||
|
let data = {
|
||||||
|
key: key,
|
||||||
|
newValue: settingsStorage.getItem(key)
|
||||||
|
};
|
||||||
|
sendVal(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send data to device using Messaging API
|
||||||
|
function sendVal(data) {
|
||||||
|
if (messaging.peerSocket.readyState === messaging.peerSocket.OPEN) {
|
||||||
|
messaging.peerSocket.send(data);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"fitbit": {
|
||||||
|
"appUUID": "753385ec-70bd-4560-baf0-5538105d67e9",
|
||||||
|
"appType": "clockface",
|
||||||
|
"appDisplayName": "Echocentric",
|
||||||
|
"iconFile": "resources/icon.png",
|
||||||
|
"wipeColor": "#009688",
|
||||||
|
"requestedPermissions": [
|
||||||
|
"access_activity",
|
||||||
|
"access_heart_rate",
|
||||||
|
"access_user_profile"
|
||||||
|
],
|
||||||
|
"buildTargets": [
|
||||||
|
"higgs",
|
||||||
|
"meson"
|
||||||
|
],
|
||||||
|
"i18n": {
|
||||||
|
"en": {
|
||||||
|
"name": "Echocentric"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@fitbit/sdk": "~3.0.0"
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 342 B |
After Width: | Height: | Size: 413 B |
After Width: | Height: | Size: 574 B |
After Width: | Height: | Size: 367 B |
After Width: | Height: | Size: 258 B |
After Width: | Height: | Size: 446 B |
|
@ -0,0 +1,50 @@
|
||||||
|
<svg viewport-fill="black">
|
||||||
|
|
||||||
|
<g transform="translate(100%-20,50%)">
|
||||||
|
<!--<g transform="rotate(12)">
|
||||||
|
<text class="time" />
|
||||||
|
</g>-->
|
||||||
|
<g transform="rotate(8)">
|
||||||
|
<text class="time" />
|
||||||
|
</g>
|
||||||
|
<g transform="rotate(4)">
|
||||||
|
<text class="time" />
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<text class="time" />
|
||||||
|
</g>
|
||||||
|
<g transform="rotate(-4)">
|
||||||
|
<text class="time" id="mainTime" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<use href="#healthText" x="100%-70" y="185" id="mainHealthText">
|
||||||
|
<set href="healthTextIcon" attributeName="href" to="img/icon-steps.png" />
|
||||||
|
</use>
|
||||||
|
|
||||||
|
<g id="overlay">
|
||||||
|
|
||||||
|
<use href="#overlayShade" id="overlayShadeInstance" />
|
||||||
|
|
||||||
|
<use href="#healthText" x="35%" y="20%" class="healthTextInstance" id="hrText">
|
||||||
|
</use>
|
||||||
|
<use href="#healthText" x="35%" y="50%" class="healthTextInstance" id="stepsText">
|
||||||
|
<set href="healthTextIcon" attributeName="href" to="img/icon-steps.png" />
|
||||||
|
</use>
|
||||||
|
<use href="#healthText" x="35%" y="80%" class="healthTextInstance" id="calText">
|
||||||
|
<set href="healthTextIcon" attributeName="href" to="img/icon-cal.png" />
|
||||||
|
</use>
|
||||||
|
<use href="#healthText" x="80%" y="20%" class="healthTextInstance" id="distText">
|
||||||
|
<set href="healthTextIcon" attributeName="href" to="img/icon-dist.png" />
|
||||||
|
</use>
|
||||||
|
<use href="#healthText" x="80%" y="50%" class="healthTextInstance" id="elevText">
|
||||||
|
<set href="healthTextIcon" attributeName="href" to="img/icon-elev.png" />
|
||||||
|
</use>
|
||||||
|
<use href="#healthText" x="80%" y="80%" class="healthTextInstance" id="actminText">
|
||||||
|
<set href="healthTextIcon" attributeName="href" to="img/icon-actmin.png" />
|
||||||
|
</use>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<rect x="0" y="0" width="100%" height="100%" opacity="0" id="touchArea" pointer-events="visible" />
|
||||||
|
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
|
@ -0,0 +1,33 @@
|
||||||
|
.time {
|
||||||
|
font-size: 140;
|
||||||
|
font-family: Fabrikat-Bold;
|
||||||
|
text-length: 5;
|
||||||
|
text-anchor: end;
|
||||||
|
x: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mainTime
|
||||||
|
{
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#overlay {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#overlayShadeColor {
|
||||||
|
fill: #009688;
|
||||||
|
fill: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.healthTextLabel {
|
||||||
|
font-size: 35;
|
||||||
|
font-family: Seville-Bold-Condensed;
|
||||||
|
text-length: 8;
|
||||||
|
text-anchor: end;
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#healthTextHrIcon {
|
||||||
|
display: none;
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
.time {
|
||||||
|
font-size: 120;
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
<svg>
|
||||||
|
<defs>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="styles-common.css" />
|
||||||
|
<link rel="stylesheet" href="styles.css" />
|
||||||
|
<link rel="import" href="/mnt/sysassets/widgets_common.gui" />
|
||||||
|
|
||||||
|
<symbol id="healthText">
|
||||||
|
|
||||||
|
<g opacity="0">
|
||||||
|
|
||||||
|
<g transform="rotate(-4)">
|
||||||
|
<text x="0" y="14" class="healthTextLabel" id="healthTextLabel" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<image x="15" y="-14" width="30" height="30" href="" fill="white" id="healthTextIcon" />
|
||||||
|
|
||||||
|
<g transform="scale(1),translate(26, 3)" id="healthTextHrIcon">
|
||||||
|
<animateTransform attributeType="scale" begin="load" from="1.1" to="0.95" dur="0.9" final="restore" repeatCount="indefinite" easing="ease-out"/>
|
||||||
|
<svg width="27" height="27">
|
||||||
|
<circle cx="-24%" cy="-24%" r="26%" fill="white" />
|
||||||
|
<circle cx="24%" cy="-24%" r="26%" fill="white" />
|
||||||
|
<g transform="translate(0, 40%), rotate(-135)">
|
||||||
|
<rect x="0" y="0" width="62%" height="38%" fill="white" />
|
||||||
|
<rect x="0" y="0" width="38%" height="62%" fill="white" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<animate attributeName="opacity" begin="enable" from="0" to="1" dur="0.25" final="freeze" />
|
||||||
|
<animateTransform attributeType="translate" begin="enable" from="0, -10" to="0, 0" dur="0.25" final="freeze" easing="ease-out" />
|
||||||
|
|
||||||
|
<animate attributeName="opacity" begin="disable" from="1" to="0" dur="0.1" final="freeze" />
|
||||||
|
<animateTransform attributeType="translate" begin="disable" from="0, 0" to="0, 10" dur="0.1" final="freeze" easing="ease-out" />
|
||||||
|
|
||||||
|
</g>
|
||||||
|
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="overlayShade">
|
||||||
|
<rect id="overlayShadeColor" width="100%" height="100%" opacity="0.85" />
|
||||||
|
<animate attributeName="opacity" begin="enable" from="0" to="1" dur="0.25" final="freeze" />
|
||||||
|
<animate attributeName="opacity" begin="disable" from="1" to="0" dur="0.1" final="freeze" />
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
|
@ -0,0 +1,110 @@
|
||||||
|
function Echocentric(props) {
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<Section
|
||||||
|
title={<Text bold align="center">Time Settings</Text>}>
|
||||||
|
<Toggle
|
||||||
|
settingsKey="leadingZero"
|
||||||
|
label={`Hour has leading Zero`}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label={`Colors`}
|
||||||
|
settingsKey="colors"
|
||||||
|
options={[
|
||||||
|
{name:"Custom color", value:"manual"},
|
||||||
|
{name:"Random colors", value:"random"},
|
||||||
|
{name:"Based on activity goal", value:"auto"}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
{ (props.settings.colors && JSON.parse(props.settings.colors).values[0].value === 'manual' || false) &&
|
||||||
|
<ColorSelect
|
||||||
|
settingsKey="customcolor"
|
||||||
|
centered={true}
|
||||||
|
colors={[
|
||||||
|
{color: '#00b0e7', value: ['0.702','1.0','0.4']}, // hue, saturation, sat modifier
|
||||||
|
{color: '#2084c0', value: ['0.729','1.0','0.5']},
|
||||||
|
{color: '#2040c0', value: ['0.799','1.0','0.5']},
|
||||||
|
{color: '#5700ad', value: ['0.883','1.0','0.0']},
|
||||||
|
{color: '#685098', value: ['0.898','0.5','0.3']},
|
||||||
|
{color: '#8820c0', value: ['0.933','1.0','0.5']},
|
||||||
|
{color: '#c020c0', value: ['0.987','1.0','0.5']},
|
||||||
|
{color: '#806480', value: ['0.011','0.3','0.3']},
|
||||||
|
{color: '#e800d0', value: ['0.013','1.0','0.0']},
|
||||||
|
{color: '#a04890', value: ['0.016','0.8','0.7']},
|
||||||
|
{color: '#985070', value: ['0.085','0.5','0.3']},
|
||||||
|
{color: '#c02058', value: ['0.108','1.0','0.5']},
|
||||||
|
{color: '#e80040', value: ['0.113','1.0','0.0']},
|
||||||
|
{color: '#e83800', value: ['0.203','1.0','0.0']},
|
||||||
|
{color: '#986850', value: ['0.216','0.5','0.3']},
|
||||||
|
{color: '#e89c01', value: ['0.275','1.0','0.0']},
|
||||||
|
{color: '#c8e802', value: ['0.348','1.0','0.0']},
|
||||||
|
{color: '#788f4b', value: ['0.372','0.5','0.3']},
|
||||||
|
{color: '#42bb1b', value: ['0.444','1.0','0.5']},
|
||||||
|
{color: '#00e12d', value: ['0.525','1.0','0.0']},
|
||||||
|
{color: '#01e890', value: ['0.600','1.0','0.0']},
|
||||||
|
{color: '#608078', value: ['0.613','0.3','0.3']},
|
||||||
|
{color: '#48a090', value: ['0.632','0.8','0.7']},
|
||||||
|
{color: '#508c98', value: ['0.689','0.5','0.3']}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{ (props.settings.colors && JSON.parse(props.settings.colors).values[0].value === 'auto' || false) &&
|
||||||
|
<Select
|
||||||
|
label={`Activity for goal color`}
|
||||||
|
settingsKey="colorgoal"
|
||||||
|
options={[
|
||||||
|
{name:"Steps", value:"steps"},
|
||||||
|
{name:"Calories", value:"cal"},
|
||||||
|
{name:"Distance", value:"dist"},
|
||||||
|
{name:"Floors", value:"elev"},
|
||||||
|
{name:"Active Minutes", value:"actmin"},
|
||||||
|
{name:"Heart Rate", value:"hr"}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{ (props.settings.colors && JSON.parse(props.settings.colors).values[0].value === 'auto' || false) &&
|
||||||
|
<Text>You will see different colors depending on your activity. Red means you still have a long way to your goal. On your way there you will see orange and yellow, until finally when you've reached your goal you will see green. With heart rate the colors will work the other way around. Green means you are below your Fat Burn zone, your Fat Burn zone is yellow, Cardio is orange and Peak is red.</Text>
|
||||||
|
}
|
||||||
|
</Section>
|
||||||
|
<Section
|
||||||
|
title={<Text bold align="center">Secondary Information</Text>}>
|
||||||
|
<Select
|
||||||
|
label={`Second line`}
|
||||||
|
settingsKey="secondary"
|
||||||
|
options={[
|
||||||
|
{name:"nothing", value:"-"},
|
||||||
|
{name:"Date", value:"date"},
|
||||||
|
{name:"Steps", value:"steps"},
|
||||||
|
{name:"Calories", value:"cal"},
|
||||||
|
{name:"Distance", value:"dist"},
|
||||||
|
{name:"Floors", value:"elev"},
|
||||||
|
{name:"Active Minutes", value:"actmin"},
|
||||||
|
{name:"Heart Rate", value:"hr"}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label={`Date format`}
|
||||||
|
settingsKey="dateformat"
|
||||||
|
options={[
|
||||||
|
{name:"MM/DD", value:"md/"},
|
||||||
|
{name:"MM-DD", value:"md-"},
|
||||||
|
{name:"DD.MM.", value:"dm."},
|
||||||
|
{name:"DD/MM", value:"dm/"},
|
||||||
|
{name:"DD-MM", value:"dm-"}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label={`Tap action`}
|
||||||
|
settingsKey="tap"
|
||||||
|
options={[
|
||||||
|
{name:"nothing", value:"-"},
|
||||||
|
{name:"Activity overlay", value:"overlay"},
|
||||||
|
{name:"Changes secondary info", value:"cycle"}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
registerSettingsPage(Echocentric);
|