diff --git a/apps/android/ChangeLog b/apps/android/ChangeLog index f1107fc84..9ac11d75b 100644 --- a/apps/android/ChangeLog +++ b/apps/android/ChangeLog @@ -39,4 +39,6 @@ Allow `calendar-` to take an array of items to remove 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 \ No newline at end of file +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/boot.js b/apps/android/boot.js index 18297d84f..37550cdd0 100644 --- a/apps/android/boot.js +++ b/apps/android/boot.js @@ -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 => { 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 584c071cf..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.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", diff --git a/apps/compass/compass-icon.js b/apps/compass/compass-icon.js index 6a09df608..63a12aeee 100644 --- a/apps/compass/compass-icon.js +++ b/apps/compass/compass-icon.js @@ -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")) \ No newline at end of file diff --git a/apps/dejivaisu/ChangeLog b/apps/dejivaisu/ChangeLog new file mode 100644 index 000000000..62e2d0c20 --- /dev/null +++ b/apps/dejivaisu/ChangeLog @@ -0,0 +1 @@ +0.1: New App! \ No newline at end of file diff --git a/apps/dejivaisu/app-icon.js b/apps/dejivaisu/app-icon.js new file mode 100644 index 000000000..aa3f5f2b1 --- /dev/null +++ b/apps/dejivaisu/app-icon.js @@ -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==")) \ No newline at end of file diff --git a/apps/dejivaisu/app.js b/apps/dejivaisu/app.js new file mode 100644 index 000000000..30193f805 --- /dev/null +++ b/apps/dejivaisu/app.js @@ -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(); +} \ No newline at end of file diff --git a/apps/dejivaisu/app.png b/apps/dejivaisu/app.png new file mode 100644 index 000000000..c9bc5cbc2 Binary files /dev/null and b/apps/dejivaisu/app.png differ diff --git a/apps/dejivaisu/metadata.json b/apps/dejivaisu/metadata.json new file mode 100644 index 000000000..4095b3335 --- /dev/null +++ b/apps/dejivaisu/metadata.json @@ -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"}] +} diff --git a/apps/dejivaisu/screenshot.png b/apps/dejivaisu/screenshot.png new file mode 100644 index 000000000..7f465fa6e Binary files /dev/null and b/apps/dejivaisu/screenshot.png differ diff --git a/apps/dejivaisu/settings.js b/apps/dejivaisu/settings.js new file mode 100644 index 000000000..b808e47bd --- /dev/null +++ b/apps/dejivaisu/settings.js @@ -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() + }); +}) \ No newline at end of file diff --git a/apps/dejivaisu/settings.json b/apps/dejivaisu/settings.json new file mode 100644 index 000000000..09c433461 --- /dev/null +++ b/apps/dejivaisu/settings.json @@ -0,0 +1,4 @@ +{ + "showMascot": true, + "showDJSeconds": true +} diff --git a/apps/health/ChangeLog b/apps/health/ChangeLog index 1d108ece4..a4c3c49b9 100644 --- a/apps/health/ChangeLog +++ b/apps/health/ChangeLog @@ -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 \ No newline at end of file diff --git a/apps/health/README.md b/apps/health/README.md index ba6204670..a73985988 100644 --- a/apps/health/README.md +++ b/apps/health/README.md @@ -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 diff --git a/apps/health/app.js b/apps/health/app.js index 17c5e1ceb..6a461da2b 100644 --- a/apps/health/app.js +++ b/apps/health/app.js @@ -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); +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 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 } - setButton(back, mult); - barChart(/*LANG*/"HOUR", data, mult); -} - -function stepsPerDay(back, mult) { +*/ +function showGraph(options) { E.showMessage(/*LANG*/"Loading..."); - current_selection = "stepsPerDay"; - var data = new Uint16Array(32); - require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.steps); - // Include data for today - if (data[(new Date()).getDate()] === 0) { - data[(new Date()).getDate()] = Bangle.getHealthStatus("day").steps; + 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); + 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 + var day = (new Date()).getDate(); + if (data[day] === 0) { + data[day] = Bangle.getHealthStatus("day")[options.field]; + if (!options.ignoreZero || data[day]) cnt[day]++; + } + } 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); } - if (mult !== undefined) { - // Calculate distance from steps - data.forEach((d, i) => data[i] = d*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 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 @@ + +