New heart rate recorder app
parent
0ebc1c3a50
commit
47575f0f80
14
apps.json
14
apps.json
|
|
@ -261,6 +261,20 @@
|
||||||
{"name":"gpsrec.wid.js","url":"widget.js"}
|
{"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",
|
{ "id": "slevel",
|
||||||
"name": "Spirit Level",
|
"name": "Spirit Level",
|
||||||
"icon": "spiritlevel.png",
|
"icon": "spiritlevel.png",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
0.01: New App!
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEwwhC/AH4AWzIAByAHDhIICCpINDDAgIIFpAADBBQuKE4QIIFxgAKC7g9HABSbIBQQXWGxgXEKQxOMC5AhBC66WMC5AuBJ5h3ICoI3LeAwKBBAICBD4TmHC48ACgQCCfxC/HAgYXDL44vFA4YRDAoiOIHAgXFYRAXFBwwIIOw4OGIxKmIC5ylHGAoXIXpBIGLxxIIIx6IJFxwwNCxQwLFxYwLCxgwJFxowJCxwwHFx4wHCyAwFFyIwFCyQYDCygA/AH4AFA"))
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"isRecording":false,
|
||||||
|
"fileNbr":0
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 883 B |
|
|
@ -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>
|
||||||
|
|
@ -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();
|
||||||
|
})()
|
||||||
Loading…
Reference in New Issue