initial commit
This commit is contained in:
574
app/index.js
Normal file
574
app/index.js
Normal file
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user