fitbit-echocentric/app/index.js

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;
}