From 4315fcc4094f6707a4d545b66e9f809ea988ecc9 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Fri, 14 Mar 2025 10:35:20 +0000 Subject: [PATCH] health 0.31: Add support for new health format (storing more data) --- apps/android/ChangeLog | 1 + apps/android/lib.js | 3 +- apps/android/metadata.json | 2 +- apps/health/ChangeLog | 1 + apps/health/README.md | 34 ++++++++++ apps/health/boot.js | 87 +++++++++++------------- apps/health/boot.min.js | 10 +-- apps/health/interface.html | 70 ++++++++++---------- apps/health/lib.js | 131 ++++++++++++++++++++++++++----------- apps/health/lib.min.js | 10 +-- apps/health/metadata.json | 2 +- 11 files changed, 221 insertions(+), 130 deletions(-) diff --git a/apps/android/ChangeLog b/apps/android/ChangeLog index f616e0286..9ac11d75b 100644 --- a/apps/android/ChangeLog +++ b/apps/android/ChangeLog @@ -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 \ No newline at end of file diff --git a/apps/android/lib.js b/apps/android/lib.js index 038d154b3..c2b3722b3 100644 --- a/apps/android/lib.js +++ b/apps/android/lib.js @@ -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++; } diff --git a/apps/android/metadata.json b/apps/android/metadata.json index 2c38de5cf..2066a2133 100644 --- a/apps/android/metadata.json +++ b/apps/android/metadata.json @@ -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", diff --git a/apps/health/ChangeLog b/apps/health/ChangeLog index 1d108ece4..16eb89307 100644 --- a/apps/health/ChangeLog +++ b/apps/health/ChangeLog @@ -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) \ No newline at end of file diff --git a/apps/health/README.md b/apps/health/README.md index ba6204670..45ce07aa7 100644 --- a/apps/health/README.md +++ b/apps/health/README.md @@ -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 diff --git a/apps/health/boot.js b/apps/health/boot.js index 66b4acda6..d53c823dc 100644 --- a/apps/health/boot.js +++ b/apps/health/boot.js @@ -26,18 +26,32 @@ })(); 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(); + 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) { + const now = new Date(Date.now()).toISOString().split('T')[0]; // yyyy-mm-dd + if (!settings.stepGoalNotificationDate || settings.stepGoalNotificationDate < now) { // notification not yet shown today? + Bangle.buzz(200, 0.5); + require("notify").show({ + title : settings.stepGoal + /*LANG*/ " steps", + body : /*LANG*/ "You reached your step goal!", + icon : atob("DAyBABmD6BaBMAsA8BCBCBCBCA8AAA==") + }); + settings.stepGoalNotificationDate = now; + require("Storage").writeJSON("health.json", settings); + } + } } function getRecordFN(d) { @@ -48,75 +62,54 @@ Bangle.on("health", health => { (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") { + 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 { - require("Storage").write(fn, "HEALTH1\0", 0, DB_FILE_LEN); // header + 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*DB_RECORD_LEN); + 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, getRecordData(health), recordPos, DB_FILE_LEN); + 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 + DB_RECORD_LEN; // record after the current one is the sum - if (f.substr(sumPos, DB_RECORD_LEN)!="\xFF\xFF\xFF\xFF") { + 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 0 && steps >= settings.stepGoal) { - const now = new Date(Date.now()).toISOString().split('T')[0]; // yyyy-mm-dd - if (!settings.stepGoalNotificationDate || settings.stepGoalNotificationDate < now) { // notification not yet shown today? - Bangle.buzz(200, 0.5); - require("notify").show({ - title : settings.stepGoal + /*LANG*/ " steps", - body : /*LANG*/ "You reached your step goal!", - icon : atob("DAyBABmD6BaBMAsA8BCBCBCBCA8AAA==") - }); - settings.stepGoalNotificationDate = now; - require("Storage").writeJSON("health.json", settings); - } - } -} + require("Storage").write(fn, inf.encode(health), sumPos); +})}); diff --git a/apps/health/boot.min.js b/apps/health/boot.min.js index 0d1a80f4c..1a849fa94 100644 --- a/apps/health/boot.min.js +++ b/apps/health/boot.min.js @@ -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=(new Date(Date.now())).toISOString().split("T")[0],!a.stepGoalNotificationDate||a.stepGoalNotificationDateBangle.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=>{90Math.abs(Bangle.getHealthStatus().bpm-b.bpm)&&Bangle.setHRMPower(0,"health")});90{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&&0k;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)}}) \ No newline at end of file +(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=>{90Math.abs(Bangle.getHealthStatus().bpm-b.bpm)&&Bangle.setHRMPower(0,"health")});90{(Bangle.getPressure?Bangle.getPressure():Promise.resolve({})).then(c=>{Object.assign(a,c);c=new Date(Date.now()-59E4);if(a&&0=b.stepGoal&&(d=(new Date(Date.now())).toISOString().split("T")[0],!b.stepGoalNotificationDate||b.stepGoalNotificationDatek;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)}})}) \ No newline at end of file diff --git a/apps/health/interface.html b/apps/health/interface.html index 34d478473..bc254172e 100644 --- a/apps/health/interface.html +++ b/apps/health/interface.html @@ -7,45 +7,49 @@ + +