574 lines
16 KiB
JavaScript
574 lines
16 KiB
JavaScript
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;
|
|
} |