mirror of https://github.com/espruino/BangleApps
new 'health' app for 2v11, improved bangle2 launcher
parent
541dbb4a9b
commit
be34e2b00d
19
apps.json
19
apps.json
|
@ -13,6 +13,21 @@
|
||||||
],
|
],
|
||||||
"sortorder" : -10
|
"sortorder" : -10
|
||||||
},
|
},
|
||||||
|
{ "id": "health",
|
||||||
|
"name": "Health Tracking",
|
||||||
|
"tags": "tool,system,b2",
|
||||||
|
"icon": "app.png",
|
||||||
|
"version":"0.01",
|
||||||
|
"description": "Logs health data and provides an app to view it (BETA - requires firmware 2v11)",
|
||||||
|
"readme": "README.md",
|
||||||
|
"storage": [
|
||||||
|
{"name":"health.app.js","url":"app.js"},
|
||||||
|
{"name":"health.img","url":"app-icon.js","evaluate":true},
|
||||||
|
{"name":"health.boot.js","url":"boot.js"},
|
||||||
|
{"name":"health","url":"lib.js"}
|
||||||
|
],
|
||||||
|
"sortorder" : -10
|
||||||
|
},
|
||||||
{ "id": "moonphase",
|
{ "id": "moonphase",
|
||||||
"name": "Moonphase",
|
"name": "Moonphase",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
|
@ -55,7 +70,7 @@
|
||||||
"name": "Launcher (Bangle.js 2)",
|
"name": "Launcher (Bangle.js 2)",
|
||||||
"shortName":"Launcher",
|
"shortName":"Launcher",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"version":"0.02",
|
"version":"0.03",
|
||||||
"description": "This is needed by Bangle.js 2.0 to display a menu allowing you to choose your own applications. It will not work on Bangle.js 1.0.",
|
"description": "This is needed by Bangle.js 2.0 to display a menu allowing you to choose your own applications. It will not work on Bangle.js 1.0.",
|
||||||
"tags": "tool,system,launcher,b2,bno1",
|
"tags": "tool,system,launcher,b2,bno1",
|
||||||
"type":"launch",
|
"type":"launch",
|
||||||
|
@ -3578,7 +3593,7 @@
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"version":"0.01",
|
"version":"0.01",
|
||||||
"description": "Replace Bangle.js 2's menus with a version that contains smaller text",
|
"description": "Replace Bangle.js 2's menus with a version that contains smaller text",
|
||||||
"tags": "b2,bno1",
|
"tags": "b2,bno1,system",
|
||||||
"type": "boot",
|
"type": "boot",
|
||||||
"storage": [
|
"storage": [
|
||||||
{"name":"menusmall.boot.js","url":"boot.js"}
|
{"name":"menusmall.boot.js","url":"boot.js"}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
0.01: New App!
|
|
@ -0,0 +1,38 @@
|
||||||
|
# Health Tracking
|
||||||
|
|
||||||
|
Logs health data to a file every 10 minutes, and provides an app to view it
|
||||||
|
|
||||||
|
**BETA - requires firmware 2v11**
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Once installed, health data is logged automatically.
|
||||||
|
|
||||||
|
To view data, run the `Health` app from your watch.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
Stores:
|
||||||
|
|
||||||
|
* Heart rate (TODO)
|
||||||
|
* Step count
|
||||||
|
* Movement
|
||||||
|
|
||||||
|
## Technical Info
|
||||||
|
|
||||||
|
Once installed, the `health.boot.js` hooks onto the `Bangle.health` event and
|
||||||
|
writes data to a binary file (one per month).
|
||||||
|
|
||||||
|
A library (that can be used with `require("health").readXYZ` can then be used
|
||||||
|
to grab historical health info.
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
* **Extend file format to include combined data for each day (to make graphs faster)**
|
||||||
|
* `interface` page for desktop to allow data to be viewed and exported in common formats
|
||||||
|
* More features in app:
|
||||||
|
* Step counting goal (ensure pedometers use this)
|
||||||
|
* Calendar view showing steps per day
|
||||||
|
* Yearly view
|
||||||
|
* Heart rate 'zone' graph
|
||||||
|
* .. other
|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEw4UA///8H5AYM7/5L/ACsBqtQAgMFqtABYcVqtVAgIDBqgLDAwITBDYNVrQiEAANQEQNVtWAFIYfCE4Xq0AuEAAdX1W0BZFe1XUHQgADvWrJogAE9WtBYl66ouD2oLEtQGBFwQQBBYgeBFwYjFA4QuCBYgfCFwYLCL4IICFwacCPwetEwYLCR4QJBFwbFCU4QhBFwbMDNAYuCHQQwFFwowFFwowFFwwwEFwzNGFwjxFFwowEFw7aFBQwwDFwwwEFwwwEFw4wDBRAwBFxAwCFxAwCFxIA/AB4A="))
|
|
@ -0,0 +1,60 @@
|
||||||
|
function menuMain() {
|
||||||
|
E.showMenu({
|
||||||
|
"":{title:"Health Tracking"},
|
||||||
|
"< Back":()=>load(),
|
||||||
|
"Step Counting":()=>menuStepCount(),
|
||||||
|
"Movement":()=>menuMovement()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function menuStepCount() {
|
||||||
|
E.showMenu({
|
||||||
|
"":{title:"Step Counting"},
|
||||||
|
"per hour":()=>stepsPerHour()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function menuMovement() {
|
||||||
|
E.showMenu({
|
||||||
|
"":{title:"Movement"},
|
||||||
|
"per hour":()=>movementPerHour()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function stepsPerHour() {
|
||||||
|
E.showMessage("Loading...");
|
||||||
|
var data = new Uint16Array(24);
|
||||||
|
require("health").readDay(new Date(), h=>data[h.hr]+=h.steps);
|
||||||
|
g.clear(1);
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
g.reset();
|
||||||
|
require("graph").drawBar(g, data, {
|
||||||
|
y:24,
|
||||||
|
miny: 0,
|
||||||
|
axes : true,
|
||||||
|
gridx : 6,
|
||||||
|
gridy : 500
|
||||||
|
});
|
||||||
|
Bangle.setUI("updown", ()=>menuStepCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
function movementPerHour() {
|
||||||
|
E.showMessage("Loading...");
|
||||||
|
var data = new Uint16Array(24);
|
||||||
|
require("health").readDay(new Date(), h=>data[h.hr]+=h.movement);
|
||||||
|
g.clear(1);
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
g.reset();
|
||||||
|
require("graph").drawLine(g, data, {
|
||||||
|
y:24,
|
||||||
|
miny: 0,
|
||||||
|
axes : true,
|
||||||
|
gridx : 6,
|
||||||
|
ylabel : null
|
||||||
|
});
|
||||||
|
Bangle.setUI("updown", ()=>menuStepCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
Bangle.loadWidgets();
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
menuMain();
|
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1,38 @@
|
||||||
|
Bangle.on("health", health => {
|
||||||
|
// ensure we write health info for *last* block
|
||||||
|
var d = new Date(Date.now() - 590000);
|
||||||
|
|
||||||
|
const DB_RECORD_LEN = 4;
|
||||||
|
const DB_RECORDS_PER_HR = 6;
|
||||||
|
const DB_RECORDS_PER_DAY = DB_RECORDS_PER_HR*24;
|
||||||
|
const DB_RECORDS_PER_MONTH = DB_RECORDS_PER_DAY*31;
|
||||||
|
const DB_HEADER_LEN = 8;
|
||||||
|
const DB_FILE_LEN = DB_HEADER_LEN + DB_RECORDS_PER_MONTH*DB_RECORD_LEN;
|
||||||
|
|
||||||
|
function getRecordFN(d) {
|
||||||
|
return "health-"+d.getFullYear()+"-"+d.getMonth()+".raw";
|
||||||
|
}
|
||||||
|
function getRecordIdx(d) {
|
||||||
|
return (DB_RECORDS_PER_DAY*(d.getDate()-1)) +
|
||||||
|
(DB_RECORDS_PER_HR*d.getHours()) +
|
||||||
|
(0|(d.getMinutes()*DB_RECORDS_PER_HR/60));
|
||||||
|
}
|
||||||
|
|
||||||
|
var rec = getRecordIdx(d);
|
||||||
|
var fn = getRecordFN(d);
|
||||||
|
var f = require("Storage").read(fn);
|
||||||
|
if (f) {
|
||||||
|
var dt = f.substr(DB_HEADER_LEN+(rec*DB_RECORD_LEN), DB_RECORD_LEN);
|
||||||
|
if (dt!="\xFF\xFF\xFF\xFF") {
|
||||||
|
print("HEALTH ERR: Already written!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
require("Storage").write(fn, "HEALTH1\0", 0, DB_FILE_LEN); // header
|
||||||
|
}
|
||||||
|
var recordData = String.fromCharCode(
|
||||||
|
health.steps>>8,health.steps&255, // 16 bit steps
|
||||||
|
health.bpm, // 8 bit bpm
|
||||||
|
Math.min(health.movement / 8, 255)); // movement
|
||||||
|
require("Storage").write(fn, recordData, DB_HEADER_LEN+(rec*DB_RECORD_LEN), DB_FILE_LEN);
|
||||||
|
});
|
|
@ -0,0 +1,61 @@
|
||||||
|
const DB_RECORD_LEN = 4;
|
||||||
|
const DB_RECORDS_PER_HR = 6;
|
||||||
|
const DB_RECORDS_PER_DAY = DB_RECORDS_PER_HR*24;
|
||||||
|
const DB_RECORDS_PER_MONTH = DB_RECORDS_PER_DAY*31;
|
||||||
|
const DB_HEADER_LEN = 8;
|
||||||
|
const DB_FILE_LEN = DB_HEADER_LEN + DB_RECORDS_PER_MONTH*DB_RECORD_LEN;
|
||||||
|
|
||||||
|
function getRecordFN(d) {
|
||||||
|
return "health-"+d.getFullYear()+"-"+d.getMonth()+".raw";
|
||||||
|
}
|
||||||
|
function getRecordIdx(d) {
|
||||||
|
return (DB_RECORDS_PER_DAY*(d.getDate()-1)) +
|
||||||
|
(DB_RECORDS_PER_HR*d.getHours()) +
|
||||||
|
(0|(d.getMinutes()*DB_RECORDS_PER_HR/60));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read all records from the given month
|
||||||
|
exports.readAllRecords = function(d, cb) {
|
||||||
|
var rec = getRecordIdx(d);
|
||||||
|
var fn = getRecordFN(d);
|
||||||
|
var f = require("Storage").read(fn);
|
||||||
|
var idx = DB_HEADER_LEN;
|
||||||
|
for (var day=0;day<31;day++) {
|
||||||
|
for (var hr=0;hr<24;hr++) {
|
||||||
|
for (var m=0;m<DB_RECORDS_PER_HR;m++) {
|
||||||
|
var h = f.substr(idx, DB_RECORD_LEN);
|
||||||
|
if (h!="\xFF\xFF\xFF\xFF") {
|
||||||
|
cb({
|
||||||
|
day:day+1, hr : hr, min:m*10,
|
||||||
|
steps : (h.charCodeAt(0)<<8) | h.charCodeAt(1),
|
||||||
|
bpm : h.charCodeAt(2),
|
||||||
|
movement : h.charCodeAt(3)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
idx += 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read all records from the given month
|
||||||
|
exports.readDay = function(d, cb) {
|
||||||
|
var rec = getRecordIdx(d);
|
||||||
|
var fn = getRecordFN(d);
|
||||||
|
var f = require("Storage").read(fn);
|
||||||
|
var idx = DB_HEADER_LEN + (DB_RECORD_LEN*DB_RECORDS_PER_DAY*(d.getDate()-1));
|
||||||
|
for (var hr=0;hr<24;hr++) {
|
||||||
|
for (var m=0;m<DB_RECORDS_PER_HR;m++) {
|
||||||
|
var h = f.substr(idx, DB_RECORD_LEN);
|
||||||
|
if (h!="\xFF\xFF\xFF\xFF") {
|
||||||
|
cb({
|
||||||
|
hr : hr, min:m*10,
|
||||||
|
steps : (h.charCodeAt(0)<<8) | h.charCodeAt(1),
|
||||||
|
bpm : h.charCodeAt(2),
|
||||||
|
movement : h.charCodeAt(3)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
idx += 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,2 +1,3 @@
|
||||||
0.01: New App!
|
0.01: New App!
|
||||||
0.02: Fix occasional missed image when scrolling up
|
0.02: Fix occasional missed image when scrolling up
|
||||||
|
0.03: Text wrapping, better font
|
||||||
|
|
|
@ -14,17 +14,23 @@ var w = g.getWidth();
|
||||||
var h = g.getHeight();
|
var h = g.getHeight();
|
||||||
var n = Math.ceil((h-24)/APPH);
|
var n = Math.ceil((h-24)/APPH);
|
||||||
var menuScrollMax = APPH*apps.length - (h-24);
|
var menuScrollMax = APPH*apps.length - (h-24);
|
||||||
|
// FIXME: not needed after 2v11
|
||||||
|
var font = g.getFonts().includes("12x20") ? "12x20" : "6x8:2";
|
||||||
|
|
||||||
apps.forEach(app=>{
|
apps.forEach(app=>{
|
||||||
if (app.icon)
|
if (app.icon)
|
||||||
app.icon = s.read(app.icon); // should just be a link to a memory area
|
app.icon = s.read(app.icon); // should just be a link to a memory area
|
||||||
});
|
});
|
||||||
|
if (g.wrapString) { // FIXME: check not needed after 2v11
|
||||||
|
g.setFont(font);
|
||||||
|
apps.forEach(app=>app.name = g.wrapString(app.name, g.getWidth()-64).join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
function drawApp(i) {
|
function drawApp(i) {
|
||||||
var y = 24+i*APPH-menuScroll;
|
var y = 24+i*APPH-menuScroll;
|
||||||
var app = apps[i];
|
var app = apps[i];
|
||||||
if (!app || y<-APPH || y>=g.getHeight()) return;
|
if (!app || y<-APPH || y>=g.getHeight()) return;
|
||||||
g.setFont("6x8",2).setFontAlign(-1,0).drawString(app.name,64,y+32);
|
g.setFont(font).setFontAlign(-1,0).drawString(app.name,64,y+32);
|
||||||
if (app.icon) try {g.drawImage(app.icon,8,y+8);} catch(e){}
|
if (app.icon) try {g.drawImage(app.icon,8,y+8);} catch(e){}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue