mirror of https://github.com/espruino/BangleApps
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"}
|
||||
]
|
||||
},
|
||||
{ "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",
|
||||
|
|
|
@ -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