1
0
Fork 0

New heart rate recorder app

master
sebi 2020-03-09 13:24:38 +01:00
parent 0ebc1c3a50
commit 47575f0f80
8 changed files with 320 additions and 0 deletions

View File

@ -261,6 +261,20 @@
{"name":"gpsrec.wid.js","url":"widget.js"}
]
},
{ "id": "heart",
"name": "Heart Rate Recorder",
"icon": "app.png",
"version":"0.01",
"interface": "interface.html",
"description": "Application that allows you to record your heart rate. Can run in background",
"tags": "tool,health,widget",
"storage": [
{"name":"heart.app.js","url":"app.js"},
{"name":"heart.json","url":"app-settings.json","evaluate":true},
{"name":"heart.img","url":"app-icon.js","evaluate":true},
{"name":"heart.wid.js","url":"widget.js"}
]
},
{ "id": "slevel",
"name": "Spirit Level",
"icon": "spiritlevel.png",

2
apps/heart/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New App!

1
apps/heart/app-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwhC/AH4AWzIAByAHDhIICCpINDDAgIIFpAADBBQuKE4QIIFxgAKC7g9HABSbIBQQXWGxgXEKQxOMC5AhBC66WMC5AuBJ5h3ICoI3LeAwKBBAICBD4TmHC48ACgQCCfxC/HAgYXDL44vFA4YRDAoiOIHAgXFYRAXFBwwIIOw4OGIxKmIC5ylHGAoXIXpBIGLxxIIIx6IJFxwwNCxQwLFxYwLCxgwJFxowJCxwwHFx4wHCyAwFFyIwFCyQYDCygA/AH4AFA"))

View File

@ -0,0 +1,4 @@
{
"isRecording":false,
"fileNbr":0
}

100
apps/heart/app.js Normal file
View File

@ -0,0 +1,100 @@
Bangle.loadWidgets();
Bangle.drawWidgets();
var settings = require("Storage").readJSON("heart.json",1)||{};
function getFileNbr(n) {
return ".heart"+n.toString(36);
}
function updateSettings() {
require("Storage").write("heart.json", settings);
if (WIDGETS["heart"])
WIDGETS["heart"].reload();
}
function showMainMenu() {
const mainMenu = {
'': { 'title': 'Heart Recorder' },
'RECORD': {
value: !!settings.isRecording,
format: v=>v?"On":"Off",
onchange: v => {
settings.isRecording = v;
updateSettings();
}
},
'File Number': {
value: settings.fileNbr|0,
min: 0,
max: 35,
step: 1,
onchange: v => {
settings.isRecording = false;
settings.fileNbr = v;
updateSettings();
}
},
'View Records': viewRecords,
'< Back': ()=>{load();}
};
return E.showMenu(mainMenu);
}
function viewRecords() {
const menu = {
'': { 'title': 'Heart Records' }
};
var found = false;
for (var n=0;n<36;n++) {
var f = require("Storage").open(getFileNbr(n),"r");
if (f.readLine()!==undefined) {
menu["Record "+n] = viewRecord.bind(null,n);
found = true;
}
}
if (!found)
menu["No Records Found"] = function(){};
menu['< Back'] = showMainMenu;
return E.showMenu(menu);
}
function viewRecord(n) {
const menu = {
'': { 'title': 'Heart Record '+n }
};
var heartCount = 0;
var heartTime;
var f = require("Storage").open(getFileNbr(n),"r");
var l = f.readLine();
if (l!==undefined) {
var c = l.split(",");
heartTime = new Date(c[0]*1000);
}
while (l!==undefined) {
heartCount++;
// TODO: min/max/average of heart rate?
l = f.readLine();
}
if (heartTime)
menu[" "+heartTime.toString().substr(4,17)] = function(){};
menu[heartCount+" records"] = function(){};
// TODO: option to draw it? Just scan through, project using min/max
menu['Erase'] = function() {
E.showPrompt("Delete Record?").then(function(v) {
if (v) {
settings.isRecording = false;
updateSettings();
var f = require("Storage").open(getFileNbr(n),"r");
f.erase();
viewRecords();
} else
viewRecord(n);
});
};
menu['< Back'] = viewRecords;
print(menu);
return E.showMenu(menu);
}
showMainMenu();

BIN
apps/heart/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 883 B

149
apps/heart/interface.html Normal file
View File

@ -0,0 +1,149 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
</head>
<body>
<div id="records"></div>
<div class="modal active" id="status-modal">
<div class="modal-overlay"></div>
<div class="modal-container">
<div class="modal-header">
<div class="modal-name h5">Please wait</div>
</div>
<div class="modal-body">
<div class="content">
Loading...
</div>
</div>
</div>
</div>
<script src="../../lib/interface.js"></script>
<script>
var domRecords = document.getElementById("records");
var domModal = document.getElementById("status-modal");
function showModal(name) {
domModal.querySelector(".content").innerHTML = name;
domModal.classList.add("active");
}
function hideModal(name) {
domModal.classList.remove("active");
}
function saveRecord(record,name) {
var csv = `${record.map(rec=>[rec.time, rec.bpm, rec.confidence].join(",")).join("\n")}`;
var a = document.createElement("a"),
file = new Blob([csv], {type: "Comma-separated value file"});
var url = URL.createObjectURL(file);
a.href = url;
a.download = name+".csv";
document.body.appendChild(a);
a.click();
setTimeout(function() {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 0);
}
function recordLineToObject(l, hasRecordNbr) {
var t = l.trim().split(",");
var n = hasRecordNbr?1:0;
var o = {
time: parseInt(t[n+0]),
bpm: parseFloat(t[n+1]),
confidence: parseFloat(t[n+2]),
};
if (hasRecordNbr)
o.number = t[0];
return o;
}
function downloadRecord(recordNbr, callback) {
showModal("Downloading heart rate record...");
Puck.write(`\x10(function() {
var f = require("Storage").open(".heart${recordNbr.toString(36)}","r");
var l = f.readLine();
while (l!==undefined) { Bluetooth.print(l); l = f.readLine(); }
})()\n`,recordList=>{
hideModal();
var record = recordList.trim().split("\n").map(l=>recordLineToObject(l,false));
callback(record);
});
}
function getRecordList() {
showModal("Loading heart rate records...");
domRecords.innerHTML = "";
Puck.write(`\x10(function() {
for (var n=0;n<36;n++) {
var f = require("Storage").open(".heart"+n.toString(36),"r");
var l = f.readLine();
if (l!==undefined)
Bluetooth.println(n+","+l.trim());
}
})()\n`,recordList=>{
var recordLines = recordList.trim().split("\n");
var html = `<div class="container">
<div class="columns">\n`;
recordLines.forEach(l => {
var record = recordLineToObject(l, true /*has record number*/);
html += `
<div class="column col-12">
<div class="card-header">
<div class="card-title h5">Heart Rate Record ${record.number}</div>
<div class="card-subtitle text-gray">${(new Date(record.time*1000)).toString().substr(0,24)}</div>
</div>
<div class="card-body"></div>
<div class="card-footer">
<button class="btn btn-primary" recordNbr="${record.number}" task="download">Download</button>
<button class="btn btn-default" recordNbr="${record.number}" task="delete">Delete</button>
</div>
</div>
`;
});
if (recordLines.length==0) {
html += `
<div class="column col-12">
<div class="card-header">
<div class="card-title h5">No record</div>
<div class="card-subtitle text-gray">No heart rate record found</div>
</div>
</div>
`;
}
html += `
</div>
</div>`;
domRecords.innerHTML = html;
hideModal();
var buttons = domRecords.querySelectorAll("button");
for (var i=0;i<buttons.length;i++) {
buttons[i].addEventListener("click",event => {
var button = event.currentTarget;
var recordNbr = button.getAttribute("recordNbr");
var task = button.getAttribute("task");
if (task=="delete") {
showModal("Deleting record...");
Puck.write(`\x10require("Storage").open(".heart${recordNbr.toString(36)}","r").erase()\n`,()=>{
hideModal();
getRecordList();
});
}
if (task=="download") {
downloadRecord(recordNbr, record => saveRecord(record, `HeartRateRecord${recordNbr}`));
}
});
}
})
}
function onInit() {
getRecordList();
}
</script>
</body>
</html>

50
apps/heart/widget.js Normal file
View File

@ -0,0 +1,50 @@
(() => {
var settings = {};
var hrmToggle = true; // toggles once for each reading
var recFile; // file for heart rate recording
// draw your widget
function draw() {
if (!settings.isRecording) return;
g.reset();
g.setFontAlign(0,0);
g.clearRect(this.x,this.y,this.x+23,this.y+23);
g.setColor(hrmToggle?"#ff0000":"#ff8000");
g.fillCircle(this.x+6,this.y+6,4); // draw heart left circle
g.fillCircle(this.x+16,this.y+6,4); // draw heart right circle
g.fillPoly([this.x+2,this.y+8,this.x+20,this.y+8,this.x+11,this.y+18]); // draw heart bottom triangle
g.setColor(-1); // change color back to be nice to other apps
}
function onHRM(hrm) {
hrmToggle = !hrmToggle;
WIDGETS["heart"].draw();
if (recFile) recFile.write([getTime().toFixed(0),hrm.bpm,hrm.confidence].join(",")+"\n");
}
// Called by the heart app to reload settings and decide what's
function reload() {
settings = require("Storage").readJSON("heart.json",1)||{};
settings.fileNbr |= 0;
Bangle.removeListener('HRM',onHRM);
if (settings.isRecording) {
WIDGETS["heart"].width = 24;
Bangle.on('HRM',onHRM);
Bangle.setHRMPower(1);
var n = settings.fileNbr.toString(36);
recFile = require("Storage").open(".heart"+n,"a");
} else {
WIDGETS["heart"].width = 0;
Bangle.setHRMPower(0);
recFile = undefined;
}
}
// add the widget
WIDGETS["heart"]={area:"tl",width:24,draw:draw,reload:function() {
reload();
Bangle.drawWidgets(); // relayout all widgets
}};
// load settings, set correct widget width
reload();
})()