BangleApps/apps/clock_info/lib.js

365 lines
14 KiB
JavaScript

/* See the README for more info... */
let storage = require("Storage");
let stepGoal = undefined;
// Load step goal from health app and pedometer widget
let d = storage.readJSON("health.json", true) || {};
stepGoal = d.stepGoal;
if (stepGoal == undefined) {
d = storage.readJSON("wpedom.json", true) || {};
stepGoal = d != undefined && d.settings != undefined ? d.settings.goal : 10000;
}
// Load the settings, with defaults
exports.loadSettings = function() {
return Object.assign({
hrmOn : 0, // 0(Always), 1(Tap)
defocusOnLock : true,
maxAltitude : 3000,
apps : {}
},
require("Storage").readJSON("clock_info.json",1)||{}
);
};
exports.load = function() {
var settings = exports.loadSettings();
delete settings.apps; // keep just the basic settings in memory
// info used for drawing...
var hrm = 0;
var alt = "--";
// callbacks (needed for easy removal of listeners)
function batteryUpdateHandler() { bangleItems[0].emit("redraw"); }
function stepUpdateHandler() { bangleItems[1].emit("redraw"); }
function hrmUpdateHandler(e) {
if (e && e.confidence>60) hrm = Math.round(e.bpm);
bangleItems[2].emit("redraw");
}
function altUpdateHandler() {
try {
Bangle.getPressure().then(data=>{
if (!data) return;
alt = Math.round(data.altitude) + "m";
bangleItems[3].emit("redraw");
});
} catch (e) {
print("Caught "+e+"\n in function altUpdateHandler in module clock_info");
bangleItems[3].emit('redraw');}
}
// actual menu
var menu = [{
name: "Bangle",
img: atob("GBiBAAD+AAH+AAH+AAH+AAH/AAOHAAYBgAwAwBgwYBgwYBgwIBAwOBAwOBgYIBgMYBgAYAwAwAYBgAOHAAH/AAH+AAH+AAH+AAD+AA=="),
items: [
{ name : "Battery",
hasRange : true,
get : () => { let v = E.getBattery();
var img;
if (!Bangle.isCharging()) {
var s=24,g = Graphics.createArrayBuffer(24,24,1,{msb:true});
g.fillRect(0,6,s-3,18).clearRect(2,8,s-5,16).fillRect(s-2,10,s,15).fillRect(3,9,3+v*(s-9)/100,15);
img = g.asImage("string");
} else img=atob("GBiBAAABgAADwAAHwAAPgACfAAHOAAPkBgHwDwP4Hwf8Pg/+fB//OD//kD//wD//4D//8D//4B//QB/+AD/8AH/4APnwAHAAACAAAA==");
return {
text : v + "%", v : v, min:0, max:100, img : img
}
},
show : function() { this.interval = setInterval(()=>this.emit('redraw'), 60000); Bangle.on("charging", batteryUpdateHandler); batteryUpdateHandler(); },
hide : function() { clearInterval(this.interval); delete this.interval; Bangle.removeListener("charging", batteryUpdateHandler); },
},
{ name : "Steps",
hasRange : true,
get : () => { let v = Bangle.getHealthStatus("day").steps; return {
text : v, v : v, min : 0, max : stepGoal,
img : atob("GBiBAAcAAA+AAA/AAA/AAB/AAB/gAA/g4A/h8A/j8A/D8A/D+AfH+AAH8AHn8APj8APj8AHj4AHg4AADAAAHwAAHwAAHgAAHgAADAA==")
}},
show : function() { Bangle.on("step", stepUpdateHandler); stepUpdateHandler(); },
hide : function() { Bangle.removeListener("step", stepUpdateHandler); },
},
{ name : "HRM",
hasRange : true,
get : () => { return {
text : (hrm||"--") + " bpm", v : hrm, min : 40, max : 200,
img : atob("GBiBAAAAAAAAAAAAAAAAAAAAAADAAADAAAHAAAHjAAHjgAPngH9n/n82/gA+AAA8AAA8AAAcAAAYAAAYAAAAAAAAAAAAAAAAAAAAAA==")
}},
run : function() {
Bangle.setHRMPower(1,"clkinfo");
if (settings.hrmOn==1/*Tap*/) {
/* turn off after 1 minute. If Health HRM monitoring is
enabled we will still get HRM events every so often */
this.timeout = setTimeout(function() {
this.timeout = undefined;
Bangle.setHRMPower(0,"clkinfo");
}, 60000);
}
},
show : function() {
Bangle.on("HRM", hrmUpdateHandler);
hrm = Math.round(Bangle.getHealthStatus().bpm||Bangle.getHealthStatus("last").bpm); hrmUpdateHandler();
this.run(); // start HRM
},
hide : function() {
Bangle.setHRMPower(0,"clkinfo");
Bangle.removeListener("HRM", hrmUpdateHandler);
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = undefined;
}
hrm = 0;
},
}
],
}];
var bangleItems = menu[0].items;
if (Bangle.getPressure){ // Altimeter may not exist
bangleItems.push({ name : "Altitude",
hasRange : true,
get : () => ({
text : alt, v : parseInt(alt),
min : 0, max : settings.maxAltitude,
img : atob("GBiBAAAAAAAAAAAAAAAAAAAAAAACAAAGAAAPAAEZgAOwwAPwQAZgYAwAMBgAGBAACDAADGAABv///////wAAAAAAAAAAAAAAAAAAAA==")
}),
show : function() { this.interval = setInterval(altUpdateHandler, 60000); alt = "--"; altUpdateHandler(); },
hide : function() { clearInterval(this.interval); delete this.interval; },
});
}
// In case there exists already a menu object b with the same name as the next
// object a, we append the items. Otherwise we add the new object a to the list.
require("Storage").list(/clkinfo.js$/).forEach(fn => {
try{
var a = eval(require("Storage").read(fn))();
var b = menu.find(x => x.name === a.name)
if(b) b.items = b.items.concat(a.items);
else menu = menu.concat(a);
} catch(e){
console.log("Could not load clock info "+E.toJS(fn))
}
});
// return it all!
return menu;
};
/** Adds an interactive menu that could be used on a clock face by swiping.
Simply supply the menu data (from .load) and a function to draw the clock info.
options = {
app : "str", // optional: app ID used when saving clock_info positions
// if defined, your app will remember its own positions,
// otherwise all apps share the same ones
x : 20, y: 20, w: 80, h:80, // dimensions of area used for clock_info
draw : (itm, info, options) // draw function
}
For example:
let clockInfoItems = require("clock_info").load();
let clockInfoMenu = require("clock_info").addInteractive(clockInfoItems, {
x : 20, y: 20, w: 80, h:80, // dimensions of area used for clock_info
draw : (itm, info, options) => {
g.reset().clearRect(options.x, options.y, options.x+options.w-2, options.y+options.h-1);
if (options.focus) g.drawRect(options.x, options.y, options.x+options.w-2, options.y+options.h-1); // show if focused
var midx = options.x+options.w/2;
if (info.img) g.drawImage(info.img, midx-12,options.y+4);
g.setFont("6x8:2").setFontAlign(0,1).drawString(info.text, midx,options.y+44);
}
});
// then when clock 'unloads':
clockInfoMenu.remove();
delete clockInfoMenu;
Then if you need to unload the clock info so it no longer
uses memory or responds to swipes, you can call clockInfoMenu.remove()
and delete clockInfoMenu
clockInfoMenu is the 'options' parameter, with the following added:
* `index` : int - which instance number are we? Starts at 0
* `menuA` : int - index in 'menu' of showing clockInfo item
* `menuB` : int - index in 'menu[menuA].items' of showing clockInfo item
* `remove` : function - remove this clockInfo item
* `redraw` : function - force a redraw
* `focus` : function - bool to show if menu is focused or not
You can have more than one clock_info at once as well, for instance:
let clockInfoDraw = (itm, info, options) => {
g.reset().setBgColor(options.bg).setColor(options.fg).clearRect(options.x, options.y, options.x+options.w-2, options.y+options.h-1);
if (options.focus) g.drawRect(options.x, options.y, options.x+options.w-2, options.y+options.h-1)
var midx = options.x+options.w/2;
if (info.img) g.drawImage(info.img, midx-12,options.y);
g.setFont("6x15").setFontAlign(0,1).drawString(info.text, midx,options.y+41);
};
let clockInfoItems = require("clock_info").load();
let clockInfoMenu = require("clock_info").addInteractive(clockInfoItems, { x:126, y:24, w:50, h:40, draw : clockInfoDraw, bg : g.theme.bg, fg : g.theme.fg });
let clockInfoMenu2 = require("clock_info").addInteractive(clockInfoItems, { x:0, y:120, w:50, h:40, draw : clockInfoDraw, bg : bgColor, fg : g.theme.bg});
*/
exports.addInteractive = function(menu, options) {
if (!menu.length || !menu[0].items.length) return; // no infos - can't load a clock_info
if ("function" == typeof options) options = {draw:options}; // backwards compatibility
options.index = 0|exports.loadCount;
exports.loadCount = options.index+1;
options.focus = options.index==0 && options.x===undefined; // focus if we're the first one loaded and no position has been defined
const appName = (options.app||"default")+":"+options.index;
// load the currently showing clock_infos
let settings = exports.loadSettings()
if (settings.apps[appName]) {
let a = settings.apps[appName].a|0;
let b = settings.apps[appName].b|0;
if (menu[a] && menu[a].items[b]) { // all ok
options.menuA = a;
options.menuB = b;
}
}
if (options.menuA===undefined) options.menuA = 0;
if (options.menuB===undefined) options.menuB = Math.min(exports.loadCount, menu[options.menuA].items.length)-1;
function drawItem(itm) {
options.draw(itm, itm.get(), options);
}
function menuShowItem(itm) {
options.redrawHandler = ()=>drawItem(itm);
itm.on('redraw', options.redrawHandler);
itm.uses = (0|itm.uses)+1;
if (itm.uses==1) itm.show();
itm.emit("redraw");
}
function menuHideItem(itm) {
itm.removeListener('redraw',options.redrawHandler);
delete options.redrawHandler;
itm.uses--;
if (!itm.uses)
itm.hide();
}
// handling for swipe between menu items
function swipeHandler(lr,ud){
if (!options.focus) return; // ignore if we're not focussed
var oldMenuItem;
if (ud) {
if (menu[options.menuA].items.length==1) return; // 1 item - can't move
oldMenuItem = menu[options.menuA].items[options.menuB];
options.menuB += ud;
if (options.menuB<0) options.menuB = menu[options.menuA].items.length-1;
if (options.menuB>=menu[options.menuA].items.length) options.menuB = 0;
} else if (lr) {
if (menu.length==1) return; // 1 item - can't move
oldMenuItem = menu[options.menuA].items[options.menuB];
do {
options.menuA += lr;
if (options.menuA<0) options.menuA = menu.length-1;
if (options.menuA>=menu.length) options.menuA = 0;
options.menuB = 0;
//get the next one if the menu is empty
//can happen for dynamic ones (alarms, events)
//in the worst case we come back to 0
} while(menu[options.menuA].items.length==0);
}
if (oldMenuItem) {
menuHideItem(oldMenuItem);
oldMenuItem.removeAllListeners("draw");
menuShowItem(menu[options.menuA].items[options.menuB]);
}
// save the currently showing clock_info
let settings = exports.loadSettings();
settings.apps[appName] = {a:options.menuA,b:options.menuB};
require("Storage").writeJSON("clock_info.json",settings);
}
Bangle.on("swipe",swipeHandler);
let touchHandler, lockHandler;
if (options.x!==undefined && options.y!==undefined && options.w && options.h) {
touchHandler = function(_,e) {
if (e.x<options.x || e.y<options.y ||
e.x>(options.x+options.w) || e.y>(options.y+options.h)) {
if (options.focus) {
options.focus=false;
delete Bangle.CLKINFO_FOCUS;
options.redraw();
}
return; // outside area
}
if (!options.focus) {
options.focus=true; // if not focussed, set focus
Bangle.CLKINFO_FOCUS=true;
options.redraw();
} else if (menu[options.menuA].items[options.menuB].run) {
Bangle.buzz(100, 0.7);
menu[options.menuA].items[options.menuB].run(); // allow tap on an item to run it (eg home assistant)
} else {
options.focus=true;
Bangle.CLKINFO_FOCUS=true;
}
};
Bangle.on("touch",touchHandler);
if (settings.defocusOnLock) {
lockHandler = function() {
if(options.focus) {
options.focus=false;
delete Bangle.CLKINFO_FOCUS;
options.redraw();
}
};
Bangle.on("lock", lockHandler);
}
}
// draw the first item
menuShowItem(menu[options.menuA].items[options.menuB]);
// return an object with info that can be used to remove the info
options.remove = function() {
Bangle.removeListener("swipe",swipeHandler);
if (touchHandler) Bangle.removeListener("touch",touchHandler);
if (lockHandler) Bangle.removeListener("lock", lockHandler);
delete Bangle.CLKINFO_FOCUS;
menuHideItem(menu[options.menuA].items[options.menuB]);
exports.loadCount--;
};
options.redraw = function() {
drawItem(menu[options.menuA].items[options.menuB]);
};
options.setItem = function (menuA, menuB) {
if (!menu[menuA] || !menu[menuA].items[menuB] || (options.menuA == menuA && options.menuB == menuB)) {
// menuA or menuB did not exist or did not change
return false;
}
const oldMenuItem = menu[options.menuA].items[options.menuB];
if (oldMenuItem) {
menuHideItem(oldMenuItem);
oldMenuItem.removeAllListeners("draw");
}
options.menuA = menuA;
options.menuB = menuB;
menuShowItem(menu[options.menuA].items[options.menuB]);
return true;
}
delete settings; // don't keep settings in RAM - save space
return options;
};
// Code for testing (plots all elements from first list)
/*
g.clear();
var menu = exports.load(); // or require("clock_info").load()
var items = menu[0].items;
items.forEach((itm,i) => {
var y = i*24;
console.log("Starting", itm.name);
function draw() {
var info = itm.get();
g.reset().setFont("6x8:2").setFontAlign(-1,0);
g.clearRect(0,y,g.getWidth(),y+23);
g.drawImage(info.img, 0,y);
g.drawString(info.text, 48,y+12);
}
itm.on('redraw', draw); // ensures we redraw when we need to
itm.show();
draw();
});
*/