Merge remote-tracking branch 'upstream/master'

master
Martin Zwigl 2025-03-15 23:19:43 +01:00
commit fd8b06b8c6
27 changed files with 627 additions and 240 deletions

View File

@ -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
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

View File

@ -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 => {

View File

@ -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++;
}

View File

@ -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",

View File

@ -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"))

1
apps/dejivaisu/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.1: New App!

View File

@ -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=="))

192
apps/dejivaisu/app.js Normal file
View File

@ -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();
}

BIN
apps/dejivaisu/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -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

View File

@ -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()
});
})

View File

@ -0,0 +1,4 @@
{
"showMascot": true,
"showDJSeconds": true
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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<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);
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 (bpm) health.bpmCnt++;
if (h.bpm) health.bpmCnt++;
}
recordPos -= DB_RECORD_LEN;
recordPos -= inf.r;
}
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() {
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);
}
}
}
require("Storage").write(fn, inf.encode(health), sumPos);
})});

View File

@ -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)}})})

View File

@ -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
@ -177,7 +179,7 @@ function viewMonthDataAsTable(filename, date, monthstr) {
Util.readStorage(
filename, data => {
Util.hideModal();
var htmlOverview = `<h1>` + monthstr + `</ h1>
<button class="btn btn-primary" id="backtomonth" style="float: right;margin-right: 5px;">Back</button>
<table class="table table-striped table-hover">
@ -215,7 +217,7 @@ function viewMonthDataAsGraph(filename, date, monthstr) {
Util.readStorage(
filename, data => {
Util.hideModal();
var html = `<h1>` + monthstr + `</ h1>
<button class="btn btn-primary" id="backtomonth" style="float: right;margin-right: 5px;">Back</button>
<h2>Steps</h2>
@ -228,10 +230,10 @@ function viewMonthDataAsGraph(filename, date, monthstr) {
domContent.querySelector("#backtomonth").addEventListener("click",event => {
getMonthList();
});
var labels = [];
var dataSteps = [], dataBPM = [], dataMovement = [];
var dailyData = getDailyData(data);
for (var i = 1; i < dailyData.length + 1; i++) {
var dayData = dailyData[i];
@ -242,7 +244,7 @@ function viewMonthDataAsGraph(filename, date, monthstr) {
dataMovement.push(dayData.movement);
}
}
new Chart(document.getElementById("chartSteps"), {
type: 'bar',
data: {
@ -261,7 +263,7 @@ function viewMonthDataAsGraph(filename, date, monthstr) {
}
}
});
new Chart(document.getElementById("chartBPM"), {
type: 'bar',
data: {
@ -280,7 +282,7 @@ function viewMonthDataAsGraph(filename, date, monthstr) {
}
}
});
new Chart(document.getElementById("chartMovement"), {
type: 'bar',
data: {

View File

@ -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;
}
}
}

View File

@ -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}}}}

View File

@ -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",

View File

@ -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)

View File

@ -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?

View File

@ -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",

View File

@ -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());

@ -1 +1 @@
Subproject commit a4e8ee137e720ba48d3ef758b5b2d12a8845c73e
Subproject commit d659cfa05e66c5c770659668e8eecaecd08f91bd