health 0.31: Add support for new health format (storing more data)
parent
9caa8e372b
commit
4315fcc409
|
|
@ -41,3 +41,4 @@
|
|||
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
|
||||
|
|
@ -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.40",
|
||||
"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",
|
||||
|
|
|
|||
|
|
@ -32,3 +32,4 @@
|
|||
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)
|
||||
|
|
@ -42,6 +42,40 @@ 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:
|
||||
|
||||
```
|
||||
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:
|
||||
|
||||
```
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
@ -120,3 +53,63 @@ function 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));
|
||||
}
|
||||
|
||||
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",
|
||||
|
|
|
|||
Loading…
Reference in New Issue