Merge remote-tracking branch 'upstream/master'
commit
fd8b06b8c6
|
|
@ -40,3 +40,5 @@
|
|||
0.37: Support Gadgetbridge canned responses
|
||||
0.38: Don't rewrite settings file on every boot!
|
||||
0.39: Move GB message handling into a library to reduce boot time from 40ms->13ms
|
||||
0.40: Ensure we send health 'activity' message to gadgetbridge (added 2v26)
|
||||
0.41: When using `actfetch`, fetch historical activity type too
|
||||
|
|
@ -32,7 +32,7 @@
|
|||
setInterval(sendBattery, 10*60*1000);
|
||||
// Health tracking - if 'realtime' data is sent with 'rt:1', but let's still send our activity log every 10 mins
|
||||
Bangle.on('health', h=>{
|
||||
require("android").gbSend({ t: "act", stp: h.steps, hrm: h.bpm, mov: h.movement });
|
||||
require("android").gbSend({ t: "act", stp: h.steps, hrm: h.bpm, mov: h.movement, act: h.activity }); // h.activity added in 2v26
|
||||
});
|
||||
// Music control
|
||||
Bangle.musicControl = cmd => {
|
||||
|
|
|
|||
|
|
@ -213,7 +213,8 @@ exports.gbHandler = (event) => {
|
|||
ts: sampleTs,
|
||||
stp: r.steps,
|
||||
hrm: r.bpm,
|
||||
mov: r.movement
|
||||
mov: r.movement,
|
||||
act: r.activity
|
||||
});
|
||||
actCount++;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "android",
|
||||
"name": "Android Integration",
|
||||
"shortName": "Android",
|
||||
"version": "0.39",
|
||||
"version": "0.41",
|
||||
"description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,system,messages,notifications,gadgetbridge",
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwghC/AE8IxAAEwAWVDB4WIDBwWJAAIWOwcz///mc4DBhFDwYVBAAYYDJJAWJDAoXKCw//+YXJIwWPCQk/Aof4JBAuHC4v/GBBdHC4nzMIZGHCAIOBC4vz75hDJAgXCCgS9CC4fdAYQXGIwsyCAPyl//nvdVQoXFRofzkYXCCwJGBSIgXFQ4kymcykfdIwZgDC5XzkUyCwJGDC6FNCwPTC5i9FmQXCMgLZFC48zLgMilUv/vdkUjBII9BC6HSC55HD1WiklDNIgXIBok61QYBkSBFC5kqCwMjC6RGB1RcCR4gXIx4MC+Wqkfyl70BEQf4C4+DIwYqBC4XzGAc4C4sISAfz0QDCFgUzRwmAC4wQB+QTCC4f/AYJeCC4hIEPQi9FIwwXDbIzVHC4xICSIYXGRoRGFGAgqFXgouGC4iqDLo4XIJAQYHCwZGHGAgYBXQUzCwYuIDAwAHCxRJEAAxFJDBgWNDBAWPAH4AYA="))
|
||||
require("heatshrink").decompress(atob("mEwwZC/AB8G7dt2AQMtu2CIICBCBUbCItsCJIODAQeACA82BYO////+wUCCA0BDoN/CAIAB/YRB4ARFhu274QDAAPt23YNA4QF//9Nw9t24RG/4RGgZWDAApcBsCMFCA///ySFjdvCA+f/4RF7YRH8gCBUgqMFAAUkSQYRL5MnCJ36pM/CI0G7YiFyQRD/9t2ARI8gRBAwYRJ/1m6VNCJ1//VPCJvyCIJgECJVPCYIRNK4KNCCJf8CIKVFCIahECIIQFWZPkyamFCJNkdgwRGt4JBKwoAB7YREjYRB/IQGCINsCAQRBtoPHXIIRFgdt34RH+3bsARDgFt24RH23bCAgRB2wQG/oRHhu274RF9u27ARFgIaBWIiMB23ACIsAmyGBLgRWBCIIQGUgQLBAQieDAAqSBCIiMEAAwRFCBQABgwRB2AQMAH4AB"))
|
||||
|
|
@ -0,0 +1 @@
|
|||
0.1: New App!
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEw4cBnnH///BIO6q2+++GoUwwmYmUkyVJAWspBhcSAgVKqOggEBA4VAwEAgnb9IRDqeQk3bvtAPAWbtv0gEP0QRCzmAFgQRDGoQEBugEB0nkUJkOxMk1IYCAAJWD7ASECIsDpILDyVgAgUB6MlhMkyEAjQxFpgEDnUoFoOQg2QgcAm3AhkAhMwCQdCNoU0kmbeYMYgKPBkwRDiQ1ByOhCIQABhuA4EELgsEwMJjmSnxrBuEGSQcgCQcNpHjxsl2XZkm44EAHAJDBtgRBtEBjlrsuS5dly4uBaoMEydtCwNog8Drcs21Zlmy8Eg3//0zdB2j0Bg3aOAQCCrgRDzFtl//pEAi1W7dt23btXIug1BmvAtf+y/9QQIRGnwyB0mSr+l/VdgPWCItIm/SQYMArt+y/r0GyCIvZg3brh6Brt1/QRIrIRBoARGywRF5IRJEYwRBI4IRCI4eSGo7FDNYdw2wRGgrFDhaPCgPSR40oYocNWYNLwCzG5TFDwEB+jOBYo/KYokAm//OIMCdItR3zFDNoMD9ADBrNlyXLsuywO1YoYACtACBhcs23LluUhuk6/8CAcAjomBgMk2Vbkmgts2ydgCIkNCIIIBI4MAjdN027CIQCCgeggFJ2AGBm3TpO17YGBg7+CgF0gUJPYNt03atOu7AMB/UpLgUOydp2matt07VtuyMBgPRkmuEgU6pk06VtmnbpM2BQMGxMkyoXBAAPpky0CyXtJoU+CIOS3YRCgbLBpMl7dsBAMB2i7CqdgggOBEYgMBRIP0CIVSpp0BNAIRC3dt2kbtsiCIVKcwoCFpAKJAVoA=="))
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
/* eslint-disable no-unused-vars */
|
||||
/* eslint-disable no-undef */
|
||||
|
||||
const storage = require('Storage');
|
||||
require("Font8x16").add(Graphics);
|
||||
|
||||
let appsettings = storage.readJSON('setting.json');
|
||||
|
||||
//MASCOT
|
||||
if (appsettings.showMascot) {
|
||||
var L1 = {
|
||||
width : 16, height : 16, bpp : 1,
|
||||
transparent : 0,
|
||||
palette : new Uint16Array([65535,0]),
|
||||
buffer : atob("AAAH4AgQcMiAaIDohgh4CEAQP4gpLylFGM40YlPVfn8=")
|
||||
};
|
||||
var L2 = {
|
||||
width : 16, height : 16, bpp : 1,
|
||||
transparent : 0,
|
||||
palette : new Uint16Array([65535,0]),
|
||||
buffer : atob("B+AIEHDIgGiA6IYIeAhA0D8QGMgpGDnHDF0zxlKqfv4=")
|
||||
};
|
||||
var R1 = {
|
||||
width : 16, height : 16, bpp : 1,
|
||||
transparent : 0,
|
||||
palette : new Uint16Array([65535,0]),
|
||||
buffer : atob("B+AIEBMOFgEXARBhEB4LAgj8ExgYlOOcujBjzFVKf34=")
|
||||
};
|
||||
var R2 = {
|
||||
width : 16, height : 16, bpp : 1,
|
||||
transparent : 0,
|
||||
palette : new Uint16Array([65535,0]),
|
||||
buffer : atob("AAAH4AgQEw4WARcBEGEQHggCEfz0lKKUcxhGLKvK/n4=")
|
||||
};
|
||||
|
||||
// Initial position and direction
|
||||
var x = 40;
|
||||
var y = 25;
|
||||
var direction = 1; // 1 for right, -1 for left
|
||||
var currentFrame = 0; // 0 for L1/R1, 1 for L2/R2
|
||||
var prevX = x; // Track the previous position of the sprite
|
||||
|
||||
function drawSprite() {
|
||||
g.clearRect(prevX, y, prevX + 32, y + 32);
|
||||
if (direction === 1) {
|
||||
g.drawImage(currentFrame === 0 ? R1 : R2, x, y, {scale:2});
|
||||
} else {
|
||||
g.drawImage(currentFrame === 0 ? L1 : L2, x, y, {scale:2});
|
||||
}
|
||||
prevX = x;
|
||||
}
|
||||
|
||||
function updatePosition() {
|
||||
if (Math.random() < 0.3) {
|
||||
direction = Math.random() < 0.5 ? -1 : 1;
|
||||
}
|
||||
|
||||
x += direction * 2;
|
||||
|
||||
if (x > g.getWidth() - 70) {
|
||||
x = g.getWidth() - 70;
|
||||
direction = -1;
|
||||
} else if (x < 0) {
|
||||
x = 0;
|
||||
direction = 1;
|
||||
}
|
||||
}
|
||||
|
||||
function alternateFrame() {
|
||||
currentFrame = 1 - currentFrame;
|
||||
}
|
||||
}
|
||||
|
||||
//BARS
|
||||
|
||||
if (appsettings.showDJSeconds) {
|
||||
let barCount = 0;
|
||||
let increasing = true;
|
||||
|
||||
function drawBars() {
|
||||
const barWidth = 5;
|
||||
const barSpacing = 3;
|
||||
const barHeight = 15;
|
||||
const startX = (g.getWidth() - (5 * barWidth + 4 * barSpacing)) / 2 -60;
|
||||
const startY = g.getHeight() / 2 + 30;
|
||||
|
||||
for (let i = 0; i < barCount; i++) {
|
||||
g.fillRect(
|
||||
startX + i * (barWidth + barSpacing),
|
||||
startY - barHeight / 2,
|
||||
startX + i * (barWidth + barSpacing) + barWidth,
|
||||
startY + barHeight / 2
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function updateBars() {
|
||||
if (increasing) {
|
||||
barCount++;
|
||||
if (barCount >= 5) {
|
||||
increasing = false;
|
||||
}
|
||||
} else {
|
||||
barCount--;
|
||||
if (barCount <= 0) {
|
||||
increasing = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//ACTUAL WATCH
|
||||
|
||||
{
|
||||
let drawTimeout;
|
||||
let queueMillis = 1000;
|
||||
let queueDraw = function() {
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = setTimeout(function() {
|
||||
drawTimeout = undefined;
|
||||
drawWatchface();
|
||||
}, queueMillis - (Date.now() % queueMillis));
|
||||
};
|
||||
let updateState = function() {
|
||||
if (Bangle.isLCDOn()) {
|
||||
if (Bangle.isLocked()){
|
||||
queueMillis = 60000;
|
||||
} else {
|
||||
queueMillis = 1000;
|
||||
}
|
||||
drawWatchface(); // draw immediately, queue redraw
|
||||
} else { // stop draw timer
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
function drawWatchface() {
|
||||
var date = new Date();
|
||||
var day = date.getDate();
|
||||
var month = date.getMonth() + 1; // Months are 0-indexed
|
||||
var year = date.getFullYear();
|
||||
var seconds = date.getSeconds();
|
||||
|
||||
g.reset().clearRect(Bangle.appRect);
|
||||
g.setFontAlign(0, 0);
|
||||
g.setFontVector(60);
|
||||
var timeString = require("locale").time(date, 1);
|
||||
var AMPM = require("locale").meridian(new Date()).toUpperCase();
|
||||
var timeWidth = g.stringWidth(timeString)/2;
|
||||
var jpclX = (g.getWidth() - timeWidth );
|
||||
var jpclY = g.getHeight() / 2;
|
||||
g.drawString(timeString, jpclX, jpclY);
|
||||
if (!Bangle.isLocked()) {
|
||||
if (appsettings.showMascot) {
|
||||
updatePosition();
|
||||
alternateFrame();
|
||||
drawSprite();
|
||||
}
|
||||
if (appsettings.showDJSeconds) {
|
||||
g.setFontVector(20);
|
||||
g.drawString(seconds.toString().padStart(2, '0'), jpclX + timeWidth / 2+25, jpclY + 33);
|
||||
updateBars();
|
||||
drawBars();
|
||||
}
|
||||
g.drawString(AMPM, jpclX+60, jpclY-38);
|
||||
}
|
||||
queueDraw();
|
||||
}
|
||||
|
||||
// Clear the screen once, at startup
|
||||
g.clear();
|
||||
// Set dynamic state and perform initial drawing
|
||||
updateState();
|
||||
// Register hooks for LCD on/off event and screen lock on/off event
|
||||
Bangle.on('lcdPower', updateState);
|
||||
Bangle.on('lock', updateState);
|
||||
Bangle.setUI({
|
||||
mode: "clock",
|
||||
remove: function() {
|
||||
// Called to unload all of the clock app
|
||||
Bangle.removeListener('lcdPower', updateState);
|
||||
Bangle.removeListener('lock', updateState);
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = undefined;
|
||||
}
|
||||
});
|
||||
// Load widgets
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"id": "dejivaisu",
|
||||
"name": "Dejivaisu",
|
||||
"version": "0.1",
|
||||
"description": "A clock loosely inspired by a certain digital device. Includes an (optional) animated mascot and a seconds animation.",
|
||||
"icon": "app.png",
|
||||
"screenshots": [{"url":"screenshot.png"}],
|
||||
"type": "clock",
|
||||
"tags": "clock",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"allow_emulator": true,
|
||||
"storage": [
|
||||
{
|
||||
"name": "dejivaisu.app.js",
|
||||
"url": "app.js"
|
||||
},
|
||||
{
|
||||
"name": "dejivaisu.img",
|
||||
"url": "app-icon.js",
|
||||
"evaluate": true
|
||||
},
|
||||
{ "name":"dejivaisu.settings.js",
|
||||
"url":"settings.js"
|
||||
}
|
||||
],
|
||||
"data": [{"name":"dejivaisu.json"}]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
|
|
@ -0,0 +1,28 @@
|
|||
(function back() {
|
||||
const storage = require('Storage');
|
||||
// Load existing settings or initialize defaults
|
||||
let settings = storage.readJSON('setting.json') || {};
|
||||
|
||||
function saveSettings() {
|
||||
storage.write('setting.json', settings);
|
||||
}
|
||||
|
||||
E.showMenu({
|
||||
'': { 'title': 'Dejivaisu Settings' },
|
||||
'Show Mascot': {
|
||||
value: settings.showMascot,
|
||||
onchange: v => {
|
||||
settings.showMascot = v;
|
||||
saveSettings();
|
||||
}
|
||||
},
|
||||
'Show Seconds': {
|
||||
value: settings.showDJSeconds,
|
||||
onchange: v => {
|
||||
settings.showDJSeconds = v;
|
||||
saveSettings();
|
||||
}
|
||||
},
|
||||
'< Back': () => load()
|
||||
});
|
||||
})
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"showMascot": true,
|
||||
"showDJSeconds": true
|
||||
}
|
||||
|
|
@ -32,3 +32,5 @@
|
|||
0.28: Calculate distance from steps if myprofile is installed and stride length is set
|
||||
0.29: Minor code improvements
|
||||
0.30: Minor code improvements
|
||||
0.31: Add support for new health format (storing more data)
|
||||
Added graphs for Temperature and Battery
|
||||
|
|
@ -42,11 +42,46 @@ and run `EspruinoDocs/bin/minify.js lib.js lib.min.js`
|
|||
|
||||
HRM data is stored as a number representing the best/average value from a 10 minute period.
|
||||
|
||||
## Usage in code
|
||||
|
||||
You can read a day's worth of health data using readDay, which will call the callback with each data packet:
|
||||
|
||||
```JS
|
||||
require("health").readDay(new Date(), print)
|
||||
// ... for each 10 min packet:
|
||||
{ "steps": 40, "bpmMin": 92, "bpmMax": 95, "movement": 488,
|
||||
"battery": 51, "isCharging": false, "temperature": 25.5, "altitude": 79,
|
||||
"activity": "UNKNOWN",
|
||||
"bpm": 93.5, "hr": 6, "min": 50 }
|
||||
```
|
||||
|
||||
Other functions are available too, and they all take a `Date` as an argument:
|
||||
|
||||
```JS
|
||||
// Read all records from the given month
|
||||
require("health").readAllRecords(d, cb)
|
||||
|
||||
// Read the entire database. There is no guarantee that the months are read in order.
|
||||
require("health").readFullDatabase(cb)
|
||||
|
||||
// Read all records per day, until the current time.
|
||||
// There may be some records for the day of the timestamp previous to the timestamp
|
||||
require("health").readAllRecordsSince(d, cb)
|
||||
|
||||
// Read daily summaries from the given month
|
||||
require("health").readDailySummaries(d, cb)
|
||||
|
||||
// Read all records from the given day
|
||||
require("health").readDay(d, cb)
|
||||
```
|
||||
|
||||
|
||||
## TODO
|
||||
|
||||
* `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)
|
||||
* Viewing stored altitude/bpm min/max graphs
|
||||
* Currently we only graph per hour but we have 10 min data - should it be shown?
|
||||
* Pie chart to show percent of time in each activity
|
||||
* Calendar view showing steps per day
|
||||
* Yearly view
|
||||
* Heart rate 'zone' graph
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ function menuMain() {
|
|||
/*LANG*/"Step Counting": () => menuStepCount(),
|
||||
/*LANG*/"Movement": () => menuMovement(),
|
||||
/*LANG*/"Heart Rate": () => menuHRM(),
|
||||
/*LANG*/"Battery": () => menuBattery(),
|
||||
/*LANG*/"Temperature": () => menuTemperature(),
|
||||
/*LANG*/"Settings": () => eval(require("Storage").read("health.settings.js"))(()=>{loadSettings();menuMain();})
|
||||
});
|
||||
}
|
||||
|
|
@ -16,8 +18,11 @@ function menuStepCount() {
|
|||
const menu = {
|
||||
"": { title:/*LANG*/"Steps" },
|
||||
/*LANG*/"< Back": () => menuMain(),
|
||||
/*LANG*/"per hour": () => stepsPerHour(menuStepCount),
|
||||
/*LANG*/"per day": () => stepsPerDay(menuStepCount)
|
||||
/*LANG*/"per hour": () => showGraph({id:"stepsPerHour",range:"hour",field:"steps", back:menuStepCount}),
|
||||
/*LANG*/"per day": () => {
|
||||
showGraph({id:"stepsPerHour",range:"day",field:"steps", back:menuStepCount})
|
||||
drawHorizontalLine(settings.stepGoal);
|
||||
}
|
||||
};
|
||||
if (myprofile.strideLength) {
|
||||
menu[/*LANG*/"distance"] = () => menuDistance();
|
||||
|
|
@ -31,8 +36,11 @@ function menuDistance() {
|
|||
E.showMenu({
|
||||
"": { title:/*LANG*/"Distance" },
|
||||
/*LANG*/"< Back": () => menuStepCount(),
|
||||
/*LANG*/"per hour": () => stepsPerHour(menuDistance, distMult),
|
||||
/*LANG*/"per day": () => stepsPerDay(menuDistance, distMult)
|
||||
/*LANG*/"per hour": () => showGraph({id:"stepsPerHour",range:"hour",field:"steps",mult:distMult, back:menuDistance}),
|
||||
/*LANG*/"per day": () => {
|
||||
showGraph({id:"stepsPerDay",range:"day",field:"steps",mult:distMult, back:menuDistance})
|
||||
drawHorizontalLine(settings.stepGoal * (distMult || 1));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -40,8 +48,8 @@ function menuMovement() {
|
|||
E.showMenu({
|
||||
"": { title:/*LANG*/"Movement" },
|
||||
/*LANG*/"< Back": () => menuMain(),
|
||||
/*LANG*/"per hour": () => movementPerHour(),
|
||||
/*LANG*/"per day": () => movementPerDay(),
|
||||
/*LANG*/"per hour": () => showGraph({id:"movementPerHour",range:"hour",field:"movement",average:true,back:menuMovement}),
|
||||
/*LANG*/"per day": () => showGraph({id:"movementPerDay",range:"day",field:"movement",average:true,back:menuMovement}),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -49,96 +57,76 @@ function menuHRM() {
|
|||
E.showMenu({
|
||||
"": { title:/*LANG*/"Heart Rate" },
|
||||
/*LANG*/"< Back": () => menuMain(),
|
||||
/*LANG*/"per hour": () => hrmPerHour(),
|
||||
/*LANG*/"per day": () => hrmPerDay(),
|
||||
/*LANG*/"per hour": () => showGraph({id:"hrmPerHour",range:"hour",field:"bpm",ignoreZero:true, average:true,back:menuHRM}),
|
||||
/*LANG*/"per day": () => showGraph({id:"hrmPerDay",range:"day",field:"bpm",ignoreZero:true, average:true,back:menuHRM}),
|
||||
});
|
||||
}
|
||||
|
||||
function stepsPerHour(back, mult) {
|
||||
E.showMessage(/*LANG*/"Loading...");
|
||||
current_selection = "stepsPerHour";
|
||||
var data = new Uint16Array(24);
|
||||
require("health").readDay(new Date(), h=>data[h.hr]+=h.steps);
|
||||
if (mult !== undefined) {
|
||||
// Calculate distance from steps
|
||||
data.forEach((d, i) => data[i] = d*mult+0.5);
|
||||
}
|
||||
setButton(back, mult);
|
||||
barChart(/*LANG*/"HOUR", data, mult);
|
||||
function menuBattery() {
|
||||
E.showMenu({
|
||||
"": { title:/*LANG*/"Battery" },
|
||||
/*LANG*/"< Back": () => menuMain(),
|
||||
/*LANG*/"per hour": () => showGraph({id:"batPerHour",range:"hour",field:"battery",average:true,back:menuBattery}),
|
||||
/*LANG*/"per day": () => showGraph({id:"batPerDay",range:"day",field:"battery",average:true,back:menuBattery}),
|
||||
});
|
||||
}
|
||||
|
||||
function stepsPerDay(back, mult) {
|
||||
function menuTemperature() {
|
||||
E.showMenu({
|
||||
"": { title:/*LANG*/"Temperature" },
|
||||
/*LANG*/"< Back": () => menuMain(),
|
||||
/*LANG*/"per hour": () => showGraph({id:"batPerHour",range:"hour",field:"temperature",average:true,back:menuTemperature}),
|
||||
/*LANG*/"per day": () => showGraph({id:"batPerDay",range:"day",field:"temperature",average:true,back:menuTemperature}),
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
Display a graph. Options are:
|
||||
|
||||
{
|
||||
range: "day"/"hour"
|
||||
id: "stepsPerHour" // id of graph
|
||||
field: "steps" // field name
|
||||
mult: 1.0, // optional multiplier
|
||||
ignoreZero: bool, // if set, ignore record that were 0 in average
|
||||
average: bool, // if set, average records (ignoring)
|
||||
back: fn() // callback for back button
|
||||
}
|
||||
*/
|
||||
function showGraph(options) {
|
||||
E.showMessage(/*LANG*/"Loading...");
|
||||
current_selection = "stepsPerDay";
|
||||
current_selection = options.id;
|
||||
var data,cnt,title;
|
||||
if (options.range=="hour") {
|
||||
title = /*LANG*/"HOUR";
|
||||
data = new Uint16Array(24);
|
||||
cnt = new Uint8Array(24);
|
||||
require("health").readDay(new Date(), h=>{
|
||||
data[h.hr]+=h[options.field];
|
||||
if (!options.ignoreZero || h[options.field]) cnt[h.hr]++;
|
||||
});
|
||||
} else if (options.range=="day") {
|
||||
title = /*LANG*/"DAY";
|
||||
var data = new Uint16Array(32);
|
||||
require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.steps);
|
||||
var cnt = new Uint8Array(32);
|
||||
require("health").readDailySummaries(new Date(), h=>{
|
||||
data[h.day]+=h[options.field];
|
||||
if (!options.ignoreZero || h[options.field]) cnt[h.day]++;
|
||||
});
|
||||
// Include data for today
|
||||
if (data[(new Date()).getDate()] === 0) {
|
||||
data[(new Date()).getDate()] = Bangle.getHealthStatus("day").steps;
|
||||
var day = (new Date()).getDate();
|
||||
if (data[day] === 0) {
|
||||
data[day] = Bangle.getHealthStatus("day")[options.field];
|
||||
if (!options.ignoreZero || data[day]) cnt[day]++;
|
||||
}
|
||||
if (mult !== undefined) {
|
||||
// Calculate distance from steps
|
||||
data.forEach((d, i) => data[i] = d*mult+0.5);
|
||||
} else throw new Error("Unknown range");
|
||||
if (options.average)
|
||||
data.forEach((d,i)=>data[i] = d/cnt[i]+0.5);
|
||||
if (options.mult !== undefined) { // Calculate distance from steps
|
||||
data.forEach((d, i) => data[i] = d*options.mult+0.5);
|
||||
}
|
||||
setButton(back, mult);
|
||||
barChart(/*LANG*/"DAY", data, mult);
|
||||
drawHorizontalLine(settings.stepGoal * (mult || 1));
|
||||
}
|
||||
|
||||
function hrmPerHour() {
|
||||
E.showMessage(/*LANG*/"Loading...");
|
||||
current_selection = "hrmPerHour";
|
||||
var data = new Uint16Array(24);
|
||||
var cnt = new Uint8Array(24);
|
||||
require("health").readDay(new Date(), h=>{
|
||||
data[h.hr]+=h.bpm;
|
||||
if (h.bpm) cnt[h.hr]++;
|
||||
});
|
||||
data.forEach((d,i)=>data[i] = d/cnt[i]+0.5);
|
||||
setButton(menuHRM);
|
||||
barChart(/*LANG*/"HOUR", data);
|
||||
}
|
||||
|
||||
function hrmPerDay() {
|
||||
E.showMessage(/*LANG*/"Loading...");
|
||||
current_selection = "hrmPerDay";
|
||||
var data = new Uint16Array(32);
|
||||
var cnt = new Uint8Array(32);
|
||||
require("health").readDailySummaries(new Date(), h=>{
|
||||
data[h.day]+=h.bpm;
|
||||
if (h.bpm) cnt[h.day]++;
|
||||
});
|
||||
data.forEach((d,i)=>data[i] = d/cnt[i]+0.5);
|
||||
setButton(menuHRM);
|
||||
barChart(/*LANG*/"DAY", data);
|
||||
}
|
||||
|
||||
function movementPerHour() {
|
||||
E.showMessage(/*LANG*/"Loading...");
|
||||
current_selection = "movementPerHour";
|
||||
var data = new Uint16Array(24);
|
||||
var cnt = new Uint8Array(24);
|
||||
require("health").readDay(new Date(), h=>{
|
||||
data[h.hr]+=h.movement;
|
||||
cnt[h.hr]++;
|
||||
});
|
||||
data.forEach((d,i)=>data[i] = d/cnt[i]+0.5);
|
||||
setButton(menuMovement);
|
||||
barChart(/*LANG*/"HOUR", data);
|
||||
}
|
||||
|
||||
function movementPerDay() {
|
||||
E.showMessage(/*LANG*/"Loading...");
|
||||
current_selection = "movementPerDay";
|
||||
var data = new Uint16Array(32);
|
||||
var cnt = new Uint8Array(32);
|
||||
require("health").readDailySummaries(new Date(), h=>{
|
||||
data[h.day]+=h.movement;
|
||||
cnt[h.day]++;
|
||||
});
|
||||
data.forEach((d,i)=>data[i] = d/cnt[i]+0.5);
|
||||
setButton(menuMovement);
|
||||
barChart(/*LANG*/"DAY", data);
|
||||
setButton(options.back, options.mult);
|
||||
barChart(title, data, options.mult);
|
||||
}
|
||||
|
||||
// Bar Chart Code
|
||||
|
|
|
|||
|
|
@ -26,84 +26,17 @@
|
|||
})();
|
||||
|
||||
Bangle.on("health", health => {
|
||||
(Bangle.getPressure?Bangle.getPressure():Promise.resolve({})).then(pressure => {
|
||||
Object.assign(health, pressure); // add temperature/pressure/altitude
|
||||
// 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 + 1/*summary*/;
|
||||
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;
|
||||
|
||||
if (health && health.steps > 0) {
|
||||
handleStepGoalNotification();
|
||||
}
|
||||
|
||||
function getRecordFN(d) {
|
||||
return "health-"+d.getFullYear()+"-"+(d.getMonth()+1)+".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));
|
||||
}
|
||||
function getRecordData(health) {
|
||||
return String.fromCharCode(
|
||||
health.steps>>8,health.steps&255, // 16 bit steps
|
||||
health.bpm, // 8 bit bpm
|
||||
Math.min(health.movement, 255)); // movement
|
||||
}
|
||||
|
||||
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 recordPos = DB_HEADER_LEN+(rec*DB_RECORD_LEN);
|
||||
|
||||
// scale down reported movement value in order to fit it within a
|
||||
// uint8 DB field
|
||||
health = Object.assign({}, health);
|
||||
health.movement /= 8;
|
||||
|
||||
require("Storage").write(fn, getRecordData(health), recordPos, DB_FILE_LEN);
|
||||
if (rec%DB_RECORDS_PER_DAY != DB_RECORDS_PER_DAY-2) return;
|
||||
// we're at the end of the day. Read in all of the data for the day and sum it up
|
||||
var sumPos = recordPos + DB_RECORD_LEN; // record after the current one is the sum
|
||||
if (f.substr(sumPos, DB_RECORD_LEN)!="\xFF\xFF\xFF\xFF") {
|
||||
print("HEALTH ERR: Daily summary already written!");
|
||||
return;
|
||||
}
|
||||
health = { steps:0, bpm:0, movement:0, movCnt:0, bpmCnt:0};
|
||||
var records = DB_RECORDS_PER_HR*24;
|
||||
for (var i=0;i<records;i++) {
|
||||
var dt = f.substr(recordPos, DB_RECORD_LEN);
|
||||
if (dt!="\xFF\xFF\xFF\xFF") {
|
||||
health.steps += (dt.charCodeAt(0)<<8)+dt.charCodeAt(1);
|
||||
var bpm = dt.charCodeAt(2);
|
||||
health.bpm += bpm;
|
||||
health.movement += dt.charCodeAt(3);
|
||||
health.movCnt++;
|
||||
if (bpm) health.bpmCnt++;
|
||||
}
|
||||
recordPos -= DB_RECORD_LEN;
|
||||
}
|
||||
if (health.bpmCnt)
|
||||
health.bpm /= health.bpmCnt;
|
||||
if (health.movCnt)
|
||||
health.movement /= health.movCnt;
|
||||
require("Storage").write(fn, getRecordData(health), sumPos, DB_FILE_LEN);
|
||||
});
|
||||
|
||||
function handleStepGoalNotification() {
|
||||
if (health && health.steps > 0) { // Show step goal notification
|
||||
var settings = require("Storage").readJSON("health.json",1)||{};
|
||||
const steps = Bangle.getHealthStatus("day").steps;
|
||||
if (settings.stepGoalNotification && settings.stepGoal > 0 && steps >= settings.stepGoal) {
|
||||
|
|
@ -119,4 +52,64 @@ function handleStepGoalNotification() {
|
|||
require("Storage").writeJSON("health.json", settings);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getRecordFN(d) {
|
||||
return "health-"+d.getFullYear()+"-"+(d.getMonth()+1)+".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 inf, f = require("Storage").read(fn);
|
||||
|
||||
if (f!==undefined) {
|
||||
inf = require("health").getDecoder(f);
|
||||
var dt = f.substr(DB_HEADER_LEN+(rec*inf.r), inf.r);
|
||||
if (dt!=inf.clr) {
|
||||
print("HEALTH ERR: Already written!");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
inf = require("health").getDecoder("HEALTH2");
|
||||
require("Storage").write(fn, "HEALTH2\0", 0, DB_HEADER_LEN + DB_RECORDS_PER_MONTH*inf.r); // header (and allocate full new file)
|
||||
}
|
||||
var recordPos = DB_HEADER_LEN+(rec*inf.r);
|
||||
|
||||
// scale down reported movement value in order to fit it within a
|
||||
// uint8 DB field
|
||||
health = Object.assign({}, health);
|
||||
health.movement /= 8;
|
||||
|
||||
require("Storage").write(fn, inf.encode(health), recordPos);
|
||||
if (rec%DB_RECORDS_PER_DAY != DB_RECORDS_PER_DAY-2) return;
|
||||
// we're at the end of the day. Read in all of the data for the day and sum it up
|
||||
var sumPos = recordPos + inf.r; // record after the current one is the sum
|
||||
if (f.substr(sumPos, inf.r)!=inf.clr) {
|
||||
print("HEALTH ERR: Daily summary already written!");
|
||||
return;
|
||||
}
|
||||
health = { steps:0, bpm:0, movement:0, movCnt:0, bpmCnt:0};
|
||||
var records = DB_RECORDS_PER_HR*24;
|
||||
for (var i=0;i<records;i++) {
|
||||
var dt = f.substr(recordPos, inf.r);
|
||||
if (dt!=inf.clr) {
|
||||
var h = inf.decode(dt);
|
||||
health.steps += h.steps
|
||||
health.bpm += h.bpm;
|
||||
health.movement += h.movement;
|
||||
health.movCnt++;
|
||||
if (h.bpm) health.bpmCnt++;
|
||||
}
|
||||
recordPos -= inf.r;
|
||||
}
|
||||
if (health.bpmCnt)
|
||||
health.bpm /= health.bpmCnt;
|
||||
if (health.movCnt)
|
||||
health.movement /= health.movCnt;
|
||||
require("Storage").write(fn, inf.encode(health), sumPos);
|
||||
})});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
function m(){var a=require("Storage").readJSON("health.json",1)||{},d=Bangle.getHealthStatus("day").steps;a.stepGoalNotification&&0<a.stepGoal&&d>=a.stepGoal&&(d=(new Date(Date.now())).toISOString().split("T")[0],!a.stepGoalNotificationDate||a.stepGoalNotificationDate<d)&&(Bangle.buzz(200,.5),require("notify").show({title:a.stepGoal+" steps",body:"You reached your step goal!",icon:atob("DAyBABmD6BaBMAsA8BCBCBCBCA8AAA==")}),a.stepGoalNotificationDate=d,require("Storage").writeJSON("health.json",
|
||||
a))}(function(){var a=0|(require("Storage").readJSON("health.json",1)||{}).hrm;if(1==a||2==a){function d(){Bangle.setHRMPower(1,"health");setTimeout(()=>Bangle.setHRMPower(0,"health"),6E4*a);if(1==a){function b(){Bangle.setHRMPower(1,"health");setTimeout(()=>{Bangle.setHRMPower(0,"health")},6E4)}setTimeout(b,2E5);setTimeout(b,4E5)}}Bangle.on("health",d);Bangle.on("HRM",b=>{90<b.confidence&&1>Math.abs(Bangle.getHealthStatus().bpm-b.bpm)&&Bangle.setHRMPower(0,"health")});90<Bangle.getHealthStatus().bpmConfidence||
|
||||
d()}else Bangle.setHRMPower(!!a,"health")})();Bangle.on("health",a=>{function d(c){return String.fromCharCode(c.steps>>8,c.steps&255,c.bpm,Math.min(c.movement,255))}var b=new Date(Date.now()-59E4);a&&0<a.steps&&m();var f=function(c){return 145*(c.getDate()-1)+6*c.getHours()+(0|6*c.getMinutes()/60)}(b);b=function(c){return"health-"+c.getFullYear()+"-"+(c.getMonth()+1)+".raw"}(b);var g=require("Storage").read(b);if(g){var e=g.substr(8+4*f,4);if("\xff\xff\xff\xff"!=e){print("HEALTH ERR: Already written!");
|
||||
return}}else require("Storage").write(b,"HEALTH1\x00",0,17988);var h=8+4*f;a=Object.assign({},a);a.movement/=8;require("Storage").write(b,d(a),h,17988);if(143==f%145)if(f=h+4,"\xff\xff\xff\xff"!=g.substr(f,4))print("HEALTH ERR: Daily summary already written!");else{a={steps:0,bpm:0,movement:0,movCnt:0,bpmCnt:0};for(var k=0;144>k;k++){e=g.substr(h,4);if("\xff\xff\xff\xff"!=e){a.steps+=(e.charCodeAt(0)<<8)+e.charCodeAt(1);var l=e.charCodeAt(2);a.bpm+=l;a.movement+=e.charCodeAt(3);a.movCnt++;
|
||||
l&&a.bpmCnt++}h-=4}a.bpmCnt&&(a.bpm/=a.bpmCnt);a.movCnt&&(a.movement/=a.movCnt);require("Storage").write(b,d(a),f,17988)}})
|
||||
(function(){var a=0|(require("Storage").readJSON("health.json",1)||{}).hrm;if(1==a||2==a){function c(){Bangle.setHRMPower(1,"health");setTimeout(()=>Bangle.setHRMPower(0,"health"),6E4*a);if(1==a){function b(){Bangle.setHRMPower(1,"health");setTimeout(()=>{Bangle.setHRMPower(0,"health")},6E4)}setTimeout(b,2E5);setTimeout(b,4E5)}}Bangle.on("health",c);Bangle.on("HRM",b=>{90<b.confidence&&1>Math.abs(Bangle.getHealthStatus().bpm-b.bpm)&&Bangle.setHRMPower(0,"health")});90<Bangle.getHealthStatus().bpmConfidence||
|
||||
c()}else Bangle.setHRMPower(!!a,"health")})();Bangle.on("health",a=>{(Bangle.getPressure?Bangle.getPressure():Promise.resolve({})).then(c=>{Object.assign(a,c);c=new Date(Date.now()-59E4);if(a&&0<a.steps){var b=require("Storage").readJSON("health.json",1)||{},d=Bangle.getHealthStatus("day").steps;b.stepGoalNotification&&0<b.stepGoal&&d>=b.stepGoal&&(d=(new Date(Date.now())).toISOString().split("T")[0],!b.stepGoalNotificationDate||b.stepGoalNotificationDate<d)&&(Bangle.buzz(200,.5),require("notify").show({title:b.stepGoal+
|
||||
" steps",body:"You reached your step goal!",icon:atob("DAyBABmD6BaBMAsA8BCBCBCBCA8AAA==")}),b.stepGoalNotificationDate=d,require("Storage").writeJSON("health.json",b))}var g=function(f){return 145*(f.getDate()-1)+6*f.getHours()+(0|6*f.getMinutes()/60)}(c);c=function(f){return"health-"+f.getFullYear()+"-"+(f.getMonth()+1)+".raw"}(c);d=require("Storage").read(c);if(void 0!==d){b=require("health").getDecoder(d);var e=d.substr(8+g*b.r,b.r);if(e!=b.clr){print("HEALTH ERR: Already written!");return}}else b=
|
||||
require("health").getDecoder("HEALTH2"),require("Storage").write(c,"HEALTH2\x00",0,8+4495*b.r);var h=8+g*b.r;a=Object.assign({},a);a.movement/=8;require("Storage").write(c,b.encode(a),h);if(143==g%145)if(g=h+b.r,d.substr(g,b.r)!=b.clr)print("HEALTH ERR: Daily summary already written!");else{a={steps:0,bpm:0,movement:0,movCnt:0,bpmCnt:0};for(var k=0;144>k;k++)e=d.substr(h,b.r),e!=b.clr&&(e=b.decode(e),a.steps+=e.steps,a.bpm+=e.bpm,a.movement+=e.movement,a.movCnt++,e.bpm&&a.bpmCnt++),h-=b.r;a.bpmCnt&&
|
||||
(a.bpm/=a.bpmCnt);a.movCnt&&(a.movement/=a.movCnt);require("Storage").write(c,b.encode(a),g)}})})
|
||||
|
|
@ -7,45 +7,49 @@
|
|||
|
||||
<script src="../../core/lib/interface.js"></script>
|
||||
<script type="module" src="chart.min.js"></script>
|
||||
<script>exports = {};</script>
|
||||
<script type="module" src="lib.js"></script>
|
||||
<script>
|
||||
const DB_RECORD_LEN = 4;
|
||||
const DB_RECORDS_PER_HR = 6;
|
||||
const DB_RECORDS_PER_DAY = DB_RECORDS_PER_HR*24 + 1/*summary*/;
|
||||
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;
|
||||
const DB_RECORDS_PER_HR = 6;
|
||||
|
||||
var domContent = document.getElementById("content");
|
||||
|
||||
function saveCSV(data, date, title) {
|
||||
// date = "2021-9"/ etc
|
||||
var csv = "Date,Time,Steps,Heartrate,Movement\n";
|
||||
var csv = "Date,Time,Steps,Heartrate,Movement,Heartrate(min),Heartrate(max),Temperature,Altitude,Activity,Battery\n";
|
||||
var f = data;
|
||||
|
||||
var inf = exports.getDecoder(data);
|
||||
var idx = DB_HEADER_LEN;
|
||||
for (var day=0;day<31;day++) {
|
||||
for (var hr=0;hr<24;hr++) { // actually 25, see below
|
||||
for (var m=0;m<DB_RECORDS_PER_HR;m++) {
|
||||
var h = f.substr(idx, DB_RECORD_LEN);
|
||||
if (h!="\xFF\xFF\xFF\xFF") {
|
||||
var h = {
|
||||
day:day+1, hr : hr, min:m*10,
|
||||
steps : (h.charCodeAt(0)<<8) | h.charCodeAt(1),
|
||||
bpm : h.charCodeAt(2),
|
||||
movement : h.charCodeAt(3)
|
||||
};
|
||||
var h = f.substr(idx, inf.r);
|
||||
if (h!=inf.clr) {
|
||||
var h = Object.assign({
|
||||
day : day + 1,
|
||||
hr : hr,
|
||||
min : m * 10
|
||||
}, inf.decode(h));
|
||||
csv += [
|
||||
date + "-" + h.day,
|
||||
h.hr+":"+h.min.toString().padStart(2,0),
|
||||
h.steps,
|
||||
h.bpm||"",
|
||||
h.movement
|
||||
h.movement,
|
||||
h.bpmMin||h.bpm||"",
|
||||
h.bpmMax||h.bpm||"",
|
||||
(h.temperature!==undefined)?h.temperature:"",
|
||||
(h.altitude!==undefined)?h.altitude:"",
|
||||
(h.activity!==undefined)?h.activity:"",
|
||||
(h.battery!==undefined)?h.battery:"",
|
||||
].join(",")+"\n";
|
||||
}
|
||||
idx += DB_RECORD_LEN;
|
||||
idx += inf.r;
|
||||
}
|
||||
}
|
||||
idx += DB_RECORD_LEN; // +1 because we have an extra record with totals for the end of the day
|
||||
idx += inf.r; // +1 because we have an extra record with totals for the end of the day
|
||||
}
|
||||
|
||||
Util.saveCSV(title, csv);
|
||||
|
|
@ -136,22 +140,20 @@ function getMonthList() {
|
|||
|
||||
function getDailyData(data) {
|
||||
var dailyData = [];
|
||||
var inf = exports.getDecoder(data);
|
||||
var idx = DB_HEADER_LEN;
|
||||
for (var day = 0; day < 31; day++) {
|
||||
var dayData = {steps: 0, bpm: 0, movement: 0};
|
||||
let bpmCnt = 0;
|
||||
for (var hr = 0; hr < 24; hr++) { // actually 25, see below
|
||||
for (var m = 0; m < DB_RECORDS_PER_HR; m++) {
|
||||
var h = data.substr(idx, DB_RECORD_LEN);
|
||||
if (h != "\xFF\xFF\xFF\xFF") {
|
||||
var h = {
|
||||
var h = data.substr(idx, inf.r);
|
||||
if (h != inf.clr) {
|
||||
var h = Object.assign({
|
||||
day : day + 1,
|
||||
hr : hr,
|
||||
min : m * 10,
|
||||
steps : (h.charCodeAt(0) << 8) | h.charCodeAt(1),
|
||||
bpm : h.charCodeAt(2),
|
||||
movement : h.charCodeAt(3)
|
||||
};
|
||||
min : m * 10
|
||||
}, inf.decode(h));
|
||||
dayData.steps += h.steps; // sum
|
||||
if (h.bpm > 0) {
|
||||
dayData.bpm = dayData.bpm + h.bpm;
|
||||
|
|
@ -159,10 +161,10 @@ function getDailyData(data) {
|
|||
}
|
||||
dayData.movement += h.movement; // sum
|
||||
}
|
||||
idx += DB_RECORD_LEN;
|
||||
idx += inf.r;
|
||||
}
|
||||
}
|
||||
idx += DB_RECORD_LEN; // +1 because we have an extra record with totals
|
||||
idx += inf.r; // +1 because we have an extra record with totals
|
||||
// for the end of the day
|
||||
if (bpmCnt > 0) {
|
||||
dayData.bpm = Math.round(dayData.bpm/bpmCnt); // average
|
||||
|
|
|
|||
|
|
@ -1,10 +1,31 @@
|
|||
const DB_RECORD_LEN = 4;
|
||||
const DB_RECORDS_PER_HR = 6;
|
||||
const DB_RECORDS_PER_DAY = DB_RECORDS_PER_HR*24 + 1/*summary*/;
|
||||
//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;
|
||||
|
||||
/*
|
||||
HEALTH1 (4 bytes):
|
||||
|
||||
uint16_t steps;
|
||||
uint8_t bpm;
|
||||
uint8_t movement;
|
||||
|
||||
HEALTH2: (10 bytes):
|
||||
|
||||
uint16_t steps;
|
||||
uint8_t bpmMin;
|
||||
uint8_t bpmMax;
|
||||
uint8_t movement;
|
||||
uint8_t battery; // %, +charging in top bit
|
||||
uint8_t temperature; // in 0.5 degree increments
|
||||
uint16_t altitude; // in metres, stored unsigned 0..8191, top 3 bits = activity type
|
||||
uint8_t to_be_decided;
|
||||
|
||||
*/
|
||||
|
||||
exports.ACTIVITY = ["UNKNOWN","NOT_WORN","WALKING","EXERCISE","LIGHT_SLEEP","DEEP_SLEEP"]; // activity type, stored in 3 bits
|
||||
|
||||
function getRecordFN(d) {
|
||||
return "health-"+d.getFullYear()+"-"+(d.getMonth()+1)+".raw";
|
||||
}
|
||||
|
|
@ -14,28 +35,76 @@ function getRecordIdx(d) {
|
|||
(0|(d.getMinutes()*DB_RECORDS_PER_HR/60));
|
||||
}
|
||||
|
||||
exports.getDecoder = function(fileContents) {
|
||||
var header = fileContents.substr(0,7);
|
||||
if (header=="HEALTH2") {
|
||||
return {
|
||||
r : 10, // record length
|
||||
clr : "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF",
|
||||
decode : h => { var v = {
|
||||
steps : (h.charCodeAt(0)<<8) | h.charCodeAt(1),
|
||||
bpmMin : h.charCodeAt(2),
|
||||
bpmMax : h.charCodeAt(3),
|
||||
movement : h.charCodeAt(4)*8,
|
||||
battery : h.charCodeAt(5)&127,
|
||||
isCharging : !!(h.charCodeAt(5)&128),
|
||||
temperature : h.charCodeAt(6)/2, // signed?
|
||||
altitude : ((h.charCodeAt(7)&31)<<8)|h.charCodeAt(8), // signed?
|
||||
activity : exports.ACTIVITY[h.charCodeAt(7)>>5]
|
||||
};
|
||||
if (v.temperature>80) v.temperature-=128;
|
||||
v.bpm = (v.bpmMin+v.bpmMax)/2;
|
||||
if (v.altitude > 7500) v.altitude-=8192;
|
||||
return v;
|
||||
},
|
||||
encode : health => {var alt=health.altitude&8191;return String.fromCharCode(
|
||||
health.steps>>8,health.steps&255, // 16 bit steps
|
||||
health.bpmMin || health.bpm, // 8 bit bpm
|
||||
health.bpmMax || health.bpm, // 8 bit bpm
|
||||
Math.min(health.movement, 255),
|
||||
E.getBattery()|(Bangle.isCharging()&&128),
|
||||
0|Math.round(health.temperature*2),
|
||||
(alt>>8)|(Math.max(0,exports.ACTIVITY.indexOf(health.activity))<<5),alt&255,
|
||||
0 // tbd
|
||||
)}
|
||||
};
|
||||
} else { // HEALTH1
|
||||
return {
|
||||
r : 4, // record length
|
||||
clr : "\xFF\xFF\xFF\xFF",
|
||||
decode : h => ({
|
||||
steps : (h.charCodeAt(0)<<8) | h.charCodeAt(1),
|
||||
bpm : h.charCodeAt(2),
|
||||
bpmMin : h.charCodeAt(2),
|
||||
bpmMax : h.charCodeAt(2),
|
||||
movement : h.charCodeAt(3)*8
|
||||
}),
|
||||
encode : health => String.fromCharCode(
|
||||
health.steps>>8,health.steps&255, // 16 bit steps
|
||||
health.bpm, // 8 bit bpm
|
||||
Math.min(health.movement, 255))
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Read all records from the given month
|
||||
exports.readAllRecords = function(d, cb) {
|
||||
var fn = getRecordFN(d);
|
||||
var f = require("Storage").read(fn);
|
||||
if (f===undefined) return;
|
||||
var idx = DB_HEADER_LEN;
|
||||
var inf = exports.getDecoder(f), date = {}, idx = DB_HEADER_LEN;
|
||||
for (var day=0;day<31;day++) {
|
||||
date.day=day+1;
|
||||
for (var hr=0;hr<24;hr++) { // actually 25, see below
|
||||
date.hr=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)*8
|
||||
});
|
||||
}
|
||||
idx += DB_RECORD_LEN;
|
||||
date.min=m*10;
|
||||
var h = f.substr(idx, inf.r);
|
||||
if (h!=inf.clr) cb(Object.assign(inf.decode(h), date));
|
||||
idx += inf.r;
|
||||
}
|
||||
}
|
||||
idx += DB_RECORD_LEN; // +1 because we have an extra record with totals for the end of the day
|
||||
idx += inf.r; // +1 because we have an extra record with totals for the end of the day
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -44,8 +113,8 @@ exports.readFullDatabase = function(cb) {
|
|||
require("Storage").list(/health-[0-9]+-[0-9]+.raw/).forEach(val => {
|
||||
console.log(val);
|
||||
var parts = val.split('-');
|
||||
var y = parseInt(parts[1]);
|
||||
var mo = parseInt(parts[2].replace('.raw', ''));
|
||||
var y = parseInt(parts[1],10);
|
||||
var mo = parseInt(parts[2].replace('.raw', ''),10);
|
||||
|
||||
exports.readAllRecords(new Date(y, mo, 1), (r) => {
|
||||
r.date = new Date(y, mo, r.day, r.hr, r.min);
|
||||
|
|
@ -74,18 +143,11 @@ exports.readDailySummaries = function(d, cb) {
|
|||
var fn = getRecordFN(d);
|
||||
var f = require("Storage").read(fn);
|
||||
if (f===undefined) return;
|
||||
var idx = DB_HEADER_LEN + (DB_RECORDS_PER_DAY-1)*DB_RECORD_LEN; // summary is at the end of each day
|
||||
var inf = exports.getDecoder(f), idx = DB_HEADER_LEN + (DB_RECORDS_PER_DAY-1)*inf.r; // summary is at the end of each day
|
||||
for (var day=0;day<31;day++) {
|
||||
var h = f.substr(idx, DB_RECORD_LEN);
|
||||
if (h!="\xFF\xFF\xFF\xFF") {
|
||||
cb({
|
||||
day:day+1,
|
||||
steps : (h.charCodeAt(0)<<8) | h.charCodeAt(1),
|
||||
bpm : h.charCodeAt(2),
|
||||
movement : h.charCodeAt(3)*8
|
||||
});
|
||||
}
|
||||
idx += DB_RECORDS_PER_DAY*DB_RECORD_LEN;
|
||||
var h = f.substr(idx, inf.r);
|
||||
if (h!=inf.clr) cb(Object.assign(inf.decode(h), {day:day+1}));
|
||||
idx += DB_RECORDS_PER_DAY*inf.r;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -95,19 +157,14 @@ exports.readDay = function(d, cb) {
|
|||
var fn = getRecordFN(d);
|
||||
var f = require("Storage").read(fn);
|
||||
if (f===undefined) return;
|
||||
var idx = DB_HEADER_LEN + (DB_RECORD_LEN*DB_RECORDS_PER_DAY*(d.getDate()-1));
|
||||
var inf = exports.getDecoder(f), date = {}, idx = DB_HEADER_LEN + (inf.r*DB_RECORDS_PER_DAY*(d.getDate()-1));
|
||||
for (var hr=0;hr<24;hr++) {
|
||||
date.hr = 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)*8
|
||||
});
|
||||
}
|
||||
idx += DB_RECORD_LEN;
|
||||
date.min = m*10;
|
||||
var h = f.substr(idx, inf.r);
|
||||
if (h!=inf.clr) cb(Object.assign(inf.decode(h), date));
|
||||
idx += inf.r;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
function h(a){return"health-"+a.getFullYear()+"-"+(a.getMonth()+1)+".raw"}function k(a){return 145*(a.getDate()-1)+6*a.getHours()+(0|6*a.getMinutes()/60)}exports.readAllRecords=function(a,e){a=h(a);a=require("Storage").read(a);if(void 0!==a)for(var d=8,b=0;31>b;b++){for(var c=0;24>c;c++)for(var f=0;6>f;f++){var g=a.substr(d,4);"\xff\xff\xff\xff"!=g&&e({day:b+1,hr:c,min:10*f,steps:g.charCodeAt(0)<<8|g.charCodeAt(1),bpm:g.charCodeAt(2),movement:8*g.charCodeAt(3)});d+=
|
||||
4}d+=4}};exports.readFullDatabase=function(a){require("Storage").list(/health-[0-9]+-[0-9]+.raw/).forEach(e=>{console.log(e);e=e.split("-");var d=parseInt(e[1]),b=parseInt(e[2].replace(".raw",""));exports.readAllRecords(new Date(d,b,1),c=>{c.date=new Date(d,b,c.day,c.hr,c.min);a(c)})})};exports.readAllRecordsSince=function(a,e){for(var d=(new Date).getTime();a.getTime()<=d;)exports.readDay(a,b=>{b.date=new Date(a.getFullYear(),a.getMonth(),a.getDate(),b.hr,b.min);e(b)}),a.setDate(a.getDate()+1)};
|
||||
exports.readDailySummaries=function(a,e){k(a);a=h(a);a=require("Storage").read(a);if(void 0!==a)for(var d=584,b=0;31>b;b++){var c=a.substr(d,4);"\xff\xff\xff\xff"!=c&&e({day:b+1,steps:c.charCodeAt(0)<<8|c.charCodeAt(1),bpm:c.charCodeAt(2),movement:8*c.charCodeAt(3)});d+=580}};exports.readDay=function(a,e){k(a);var d=h(a);d=require("Storage").read(d);if(void 0!==d){a=8+580*(a.getDate()-1);for(var b=0;24>b;b++)for(var c=0;6>c;c++){var f=d.substr(a,4);"\xff\xff\xff\xff"!=f&&e({hr:b,min:10*
|
||||
c,steps:f.charCodeAt(0)<<8|f.charCodeAt(1),bpm:f.charCodeAt(2),movement:8*f.charCodeAt(3)});a+=4}}}
|
||||
function k(b){return"health-"+b.getFullYear()+"-"+(b.getMonth()+1)+".raw"}function l(b){return 145*(b.getDate()-1)+6*b.getHours()+(0|6*b.getMinutes()/60)}exports.ACTIVITY="UNKNOWN NOT_WORN WALKING EXERCISE LIGHT_SLEEP DEEP_SLEEP".split(" ");exports.getDecoder=function(b){return"HEALTH2"==b.substr(0,7)?{r:10,clr:"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff",decode:a=>{a={steps:a.charCodeAt(0)<<8|a.charCodeAt(1),bpmMin:a.charCodeAt(2),bpmMax:a.charCodeAt(3),
|
||||
movement:8*a.charCodeAt(4),battery:a.charCodeAt(5)&127,isCharging:!!(a.charCodeAt(5)&128),temperature:a.charCodeAt(6)/2,altitude:(a.charCodeAt(7)&31)<<8|a.charCodeAt(8),activity:exports.ACTIVITY[a.charCodeAt(7)>>5]};80<a.temperature&&(a.temperature-=128);a.bpm=(a.bpmMin+a.bpmMax)/2;7500<a.altitude&&(a.altitude-=8192);return a},encode:a=>{var c=a.altitude&8191;return String.fromCharCode(a.steps>>8,a.steps&255,a.bpmMin||a.bpm,a.bpmMax||a.bpm,Math.min(a.movement,255),E.getBattery()|(Bangle.isCharging()&&
|
||||
128),0|Math.round(2*a.temperature),c>>8|Math.max(0,exports.ACTIVITY.indexOf(a.activity))<<5,c&255,0)}}:{r:4,clr:"\xff\xff\xff\xff",decode:a=>({steps:a.charCodeAt(0)<<8|a.charCodeAt(1),bpm:a.charCodeAt(2),bpmMin:a.charCodeAt(2),bpmMax:a.charCodeAt(2),movement:8*a.charCodeAt(3)}),encode:a=>String.fromCharCode(a.steps>>8,a.steps&255,a.bpm,Math.min(a.movement,255))}};exports.readAllRecords=function(b,a){b=k(b);b=require("Storage").read(b);if(void 0!==b)for(var c=exports.getDecoder(b),d={},e=8,
|
||||
f=0;31>f;f++){d.day=f+1;for(var g=0;24>g;g++){d.hr=g;for(var h=0;6>h;h++){d.min=10*h;var m=b.substr(e,c.r);m!=c.clr&&a(Object.assign(c.decode(m),d));e+=c.r}}e+=c.r}};exports.readFullDatabase=function(b){require("Storage").list(/health-[0-9]+-[0-9]+.raw/).forEach(a=>{console.log(a);a=a.split("-");var c=parseInt(a[1],10),d=parseInt(a[2].replace(".raw",""),10);exports.readAllRecords(new Date(c,d,1),e=>{e.date=new Date(c,d,e.day,e.hr,e.min);b(e)})})};exports.readAllRecordsSince=function(b,a){for(var c=
|
||||
(new Date).getTime();b.getTime()<=c;)exports.readDay(b,d=>{d.date=new Date(b.getFullYear(),b.getMonth(),b.getDate(),d.hr,d.min);a(d)}),b.setDate(b.getDate()+1)};exports.readDailySummaries=function(b,a){l(b);b=k(b);b=require("Storage").read(b);if(void 0!==b)for(var c=exports.getDecoder(b),d=8+144*c.r,e=0;31>e;e++){var f=b.substr(d,c.r);f!=c.clr&&a(Object.assign(c.decode(f),{day:e+1}));d+=145*c.r}};exports.readDay=function(b,a){l(b);var c=k(b);c=require("Storage").read(c);if(void 0!==c){var d=exports.getDecoder(c),
|
||||
e={};b=8+145*d.r*(b.getDate()-1);for(var f=0;24>f;f++){e.hr=f;for(var g=0;6>g;g++){e.min=10*g;var h=c.substr(b,d.r);h!=d.clr&&a(Object.assign(d.decode(h),e));b+=d.r}}}}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "health",
|
||||
"name": "Health Tracking",
|
||||
"shortName": "Health",
|
||||
"version": "0.30",
|
||||
"version": "0.31",
|
||||
"description": "Logs health data and provides an app to view it",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,system,health",
|
||||
|
|
|
|||
|
|
@ -84,3 +84,4 @@ of 'Select Clock'
|
|||
0.73: Fix `const` bug / work with fastload
|
||||
0.74: Add extra layer of checks before allowing a factory reset (fix #3476)
|
||||
0.75: Restore previous menu's scroll positions
|
||||
0.76: Add altitude calibration menu (and update README after menu changed)
|
||||
|
|
@ -1,20 +1,32 @@
|
|||
# Settings
|
||||
|
||||
This is Bangle.js's settings menu
|
||||
This is Bangle.js's main settings menu:
|
||||
|
||||
* **Apps** - Settings for installed apps/widgets
|
||||
* **System** - Settings related to themes, default apps, date & time, etc
|
||||
* **Bluetooth** - Bluetooth Settings menu - see below.
|
||||
* **Alerts** - Set how Bangle.js alerts you (including Quiet mode)
|
||||
* **Utils** - Utilities, including resetting settings
|
||||
|
||||
See below for options under each heading:
|
||||
|
||||
## System - System settings
|
||||
|
||||
* **Theme** Adjust the colour scheme
|
||||
* **LCD** Configure settings about the screen. How long it stays on, how bright it is, and when it turns on - see below.
|
||||
* **Locale** set time zone, the time format (12/24h, for supported clocks) and the first day of the week
|
||||
* **Clock** if you have more than one clock face, select the default one
|
||||
* **Launcher** if you have more than one app launcher, select the default one
|
||||
* **Date & Time** Configure the current time - Note that this can be done much more easily by choosing 'Set Time' from the App Loader
|
||||
* **Altitude** On Bangle.js 2, adjust the altitude (
|
||||
|
||||
## Alerts
|
||||
|
||||
* **App/Widget Settings** settings specific to installed applications
|
||||
* **BLE** Bluetooth Settings menu - see below.
|
||||
* **Beep** most Bangle.js do not have a speaker inside, but they can use the vibration motor to beep in different pitches. You can change the behaviour here to use a Piezo speaker if one is connected
|
||||
* **Vibration** enable/disable the vibration motor
|
||||
* **Quiet Mode** prevent notifications/alarms from vibrating/beeping/turning the screen on - see below
|
||||
* **Locale** set time zone, the time format (12/24h, for supported clocks) and the first day of the week
|
||||
* **Select Clock** if you have more than one clock face, select the default one
|
||||
* **Date & Time** Configure the current time - Note that this can be done much more easily by choosing 'Set Time' from the App Loader
|
||||
* **LCD** Configure settings about the screen. How long it stays on, how bright it is, and when it turns on - see below.
|
||||
* **Theme** Adjust the colour scheme
|
||||
* **Utils** Utilities - including resetting settings (see below)
|
||||
|
||||
## BLE - Bluetooth Settings
|
||||
## Bluetooth
|
||||
|
||||
* **Make Connectable** regardless of the current Bluetooth settings, makes Bangle.js so you can connect to it (while the window is up)
|
||||
* **BLE** is Bluetooth LE enabled and the watch connectable?
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "setting",
|
||||
"name": "Settings",
|
||||
"version": "0.75",
|
||||
"version": "0.76",
|
||||
"description": "A menu for setting up Bangle.js",
|
||||
"icon": "settings.png",
|
||||
"tags": "tool,system",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
{
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
||||
|
|
@ -110,7 +109,6 @@ function mainMenu() {
|
|||
}
|
||||
|
||||
function systemMenu() {
|
||||
|
||||
const mainmenu = {
|
||||
'': { 'title': /*LANG*/'System' },
|
||||
'< Back': ()=>popMenu(mainMenu()),
|
||||
|
|
@ -121,6 +119,7 @@ function systemMenu() {
|
|||
/*LANG*/'Launcher': ()=>pushMenu(launcherMenu()),
|
||||
/*LANG*/'Date & Time': ()=>pushMenu(setTimeMenu())
|
||||
};
|
||||
if (Bangle.getPressure) mainmenu[/*LANG*/"Altitude"] = ()=>pushMenu(showAltitude());
|
||||
|
||||
return mainmenu;
|
||||
}
|
||||
|
|
@ -1030,5 +1029,45 @@ function showTouchscreenCalibration() {
|
|||
showTapSpot();
|
||||
}
|
||||
|
||||
pushMenu(mainMenu());
|
||||
// Calibrate altitude - Bangle.js2 only
|
||||
function showAltitude() {
|
||||
function onPressure(pressure) {
|
||||
menuPressure.value = Math.round(pressure.pressure);
|
||||
menuAltitude.value = Math.round(pressure.altitude);
|
||||
m.draw();
|
||||
}
|
||||
Bangle.setBarometerPower(1,"settings");
|
||||
Bangle.on("pressure",onPressure);
|
||||
var seaLevelPressure = Bangle.getOptions().seaLevelPressure;
|
||||
if (!isFinite(seaLevelPressure)) seaLevelPressure=1013.25;
|
||||
var menuPressure = {value:"-"};
|
||||
var menuAltitude = {value:"-"};
|
||||
var m = E.showMenu({ "" : {title:/*LANG*/"Altitude",back:() => {
|
||||
Bangle.setBarometerPower(0,"settings");
|
||||
Bangle.removeListener("pressure",onPressure);
|
||||
settings.seaLevelPressure = seaLevelPressure;
|
||||
updateSettings();
|
||||
popMenu(systemMenu());
|
||||
}},
|
||||
/*LANG*/"Pressure (hPa)" : menuPressure,
|
||||
/*LANG*/"Altitude (m)" : menuAltitude,
|
||||
/*LANG*/"Adjust up" : function() {
|
||||
Bangle.buzz(80);
|
||||
seaLevelPressure++;
|
||||
Bangle.setOptions({seaLevelPressure:seaLevelPressure});
|
||||
},
|
||||
/*LANG*/"Adjust down" : function() {
|
||||
Bangle.buzz(80);
|
||||
seaLevelPressure--;
|
||||
Bangle.setOptions({seaLevelPressure:seaLevelPressure});
|
||||
},
|
||||
/*LANG*/"Set Default" : function() {
|
||||
Bangle.buzz();
|
||||
seaLevelPressure=1013.25;
|
||||
Bangle.setOptions({seaLevelPressure:seaLevelPressure});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Show the main menu
|
||||
pushMenu(mainMenu());
|
||||
2
webtools
2
webtools
|
|
@ -1 +1 @@
|
|||
Subproject commit a4e8ee137e720ba48d3ef758b5b2d12a8845c73e
|
||||
Subproject commit d659cfa05e66c5c770659668e8eecaecd08f91bd
|
||||
Loading…
Reference in New Issue