Merge pull request #3374 from ekwawu/master
commit
377b43619b
|
|
@ -0,0 +1 @@
|
|||
0.01: New App.
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# Capstone Wearable
|
||||
Research has shown that active learning approaches in classrooms, which emphasize the construction of knowledge and reflection on the learning process, significantly enhance the student learning experience and lead to better success in the classroom. The Active Learning Initiative at the University of Georgia focuses on instructor development, student engagement, and classroom enhancement.
|
||||
|
||||
This project focuses specifically on classroom enhancement to create a device that will enhance the learning experience of students in physiology classes by allowing them to learn about and analyze their physiological patterns. The project must adhere to the constraints of a one-hundred dollar per watch budget, the ability to be scaled to a classroom of three-hundred twenty students, and provide data with at least 80% accuracy. By giving students access to their heart rate (HR) and heart rate variability (HRV) data, the goal is that students can better understand the function of the heart as well as the physiological conditions that can be affected by the heart. The original concept was to develop a wrist-wearable, screen-less device featuring sensors that detect a user’s heart rate by emitting LEDs that detect the expansion of blood vessels in the wrist and the quantity of light transmitted in the vascular bed to obtain heart rate and blood oxygen data and the heart rate variability calculated using an algorithm that takes the root-mean-square of interbeat intervals between successive heartbeats.
|
||||
|
||||
After prototyping this custom device and testing its efficacy, the accuracy of the data produced by the sensor was not within the accuracy criteria, and the decision was made to pivot to an existing on-market device coupled with custom software to meet the needs of the client. The analysis software created for the device allows the students to analyze their physiological data and permits for their instructor to access their data anonymously. The data, coupled with survey data entered by the students daily, provides insight into the effect of variables such as sleep, exercise, and caffeine intake to analyze the effects that these have on their physiological measurements. To evaluate the device's efficacy in achieving the goal of 80% accuracy, the wrist wearable device was tested on several different skin tones doing various activities and measuring data over varying intervals and then compared against the data of Apple Watches.
|
||||
|
||||
This project resulted in the creation of a custom data analysis software for the use of students on a Bangle.js smartwatch device that can be continually developed in the future to reach the goal of an inexpensive, easy-to-use device for students to use in active learning classrooms to enhance their understanding of physiology by analyzing their own physiological data in class.
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwZC/AH4ABgVJkmAA4cEyVJCIwIBkmSAwUBAoIIBCAgaCBYNIA4IFCFgwIDF4Q7CBAWQFg4CBkESCIg+DhIRFAQ9ACKYOLMRWf5IRQ/IRP/4RlzwRKyf5k4RC/xcFCISPBKwMn+YRB/4RFUImf/4RCEwIRIa4P/AAPz/YRDHwLXEgP//1P/+T/8vJQIZCCIkAn/yCIOSv8/2YQCCIOQCIY+CCIYmB8g1CCIkECIM8CII4CLIeACIcAMQd//mSvYRDCAkAiQRFWYcgCIsCCJIQFbQl/8jCGAAq2ByVPCIiwCAAq2BCILCECA6SEAQaMEAAqSCRhIAFCIoQKSQiMHQBJ6IQBB6IQBAQNNwRoLAH4AoA="))
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
let isMeasuring = false;
|
||||
let currentHR = null;
|
||||
let lcdTimeout;
|
||||
let logData = [];
|
||||
let bpmValues = [];
|
||||
|
||||
function startMeasure() {
|
||||
logData = [];
|
||||
isMeasuring = true;
|
||||
Bangle.setLCDTimeout(0);
|
||||
lcdTimeout = setTimeout(() => {
|
||||
Bangle.setLCDTimeout(50);
|
||||
}, 50000);
|
||||
|
||||
setTimeout(() => {
|
||||
Bangle.setHRMPower(1); // starts HRM
|
||||
Bangle.on('HRM', handleHeartRate);
|
||||
Bangle.buzz(200, 10); // Buzz to indicate measurement start
|
||||
drawScreen();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function stopMeasure() {
|
||||
isMeasuring = false;
|
||||
clearTimeout(lcdTimeout);
|
||||
Bangle.setLCDTimeout(10);
|
||||
Bangle.setHRMPower(0);
|
||||
Bangle.removeAllListeners('HRM'); //stop HRM
|
||||
saveDataToCSV(); // Save data to CSV when measurement stops
|
||||
Bangle.buzz(200, 10); // Buzz to indicate measurement stop
|
||||
drawScreen();
|
||||
}
|
||||
|
||||
function handleHeartRate(hrm) {
|
||||
if (isMeasuring && hrm.confidence > 90) {
|
||||
let date = new Date();
|
||||
let dateStr = require("locale").date(date);
|
||||
let timeStr = require("locale").time(date, 1);
|
||||
let seconds = date.getSeconds();
|
||||
let timestamp = `${dateStr} ${timeStr}:${seconds}`; // Concatenate date, time, and seconds
|
||||
currentHR = hrm.bpm;
|
||||
|
||||
logData.push({ timestamp: timestamp, heartRate: currentHR });
|
||||
bpmValues.push(currentHR); // Store heart rate for HRV calculation
|
||||
if (bpmValues.length > 30) bpmValues.shift(); // Keep last 30 heart rate values
|
||||
// Calculate and add SDNN (standard deviation of NN intervals) to the last log entry
|
||||
logData[logData.length - 1].hrv = calcSDNN();
|
||||
drawScreen();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function calcSDNN() {
|
||||
if (bpmValues.length < 5) return 0; // No calculation if insufficient data
|
||||
|
||||
// Calculate differences between adjacent heart rate values
|
||||
const differences = [];
|
||||
for (let i = 1; i < bpmValues.length; i++) {
|
||||
differences.push(Math.abs(bpmValues[i] - bpmValues[i - 1]));
|
||||
}
|
||||
|
||||
// Calculate mean difference
|
||||
const meanDifference = differences.reduce((acc, val) => acc + val, 0) / differences.length;
|
||||
|
||||
// Calculate squared differences from mean difference
|
||||
const squaredDifferences = differences.map(diff => Math.pow(diff - meanDifference, 2));
|
||||
|
||||
// Calculate mean squared difference
|
||||
const meanSquaredDifference = squaredDifferences.reduce((acc, val) => acc + val, 0) / squaredDifferences.length;
|
||||
|
||||
// Calculate SDNN (standard deviation of NN intervals)
|
||||
const sdnn = Math.sqrt(meanSquaredDifference);
|
||||
|
||||
return sdnn;
|
||||
}
|
||||
|
||||
function drawScreen(message) {
|
||||
g.clear(); // Clear the display
|
||||
|
||||
// Set the background color
|
||||
g.setColor('#95E7FF');
|
||||
|
||||
// Fill the entire display with the background color
|
||||
g.fillRect(0, 0, g.getWidth(), g.getHeight());
|
||||
|
||||
// Set font and alignment for drawing text
|
||||
g.setFontAlign(0, 0);
|
||||
g.setFont('Vector', 15);
|
||||
|
||||
// Draw the title
|
||||
g.setColor('#000000'); // Set text color to black
|
||||
g.drawString('Heart Rate Monitor', g.getWidth() / 2, 10);
|
||||
|
||||
if (isMeasuring) {
|
||||
// Draw measuring status
|
||||
g.setFont('6x8', 2);
|
||||
g.drawString('Measuring...', g.getWidth() / 2, g.getHeight() / 2 - 10);
|
||||
|
||||
// Draw current heart rate if available
|
||||
g.setFont('6x8', 4);
|
||||
if (currentHR !== null) {
|
||||
g.drawString(currentHR.toString(), g.getWidth() / 2, g.getHeight() / 2 + 20);
|
||||
g.setFont('6x8', 1.6);
|
||||
g.drawString(' BPM', g.getWidth() / 2 + 42, g.getHeight() / 2 + 20);
|
||||
}
|
||||
|
||||
// Draw instructions
|
||||
g.setFont('6x8', 1.5);
|
||||
g.drawString('Press button to stop', g.getWidth() / 2, g.getHeight() / 2 + 42);
|
||||
} else {
|
||||
// Draw last heart rate
|
||||
if (currentHR !== null && currentHR > 0) {
|
||||
g.setFont('Vector', 12);
|
||||
g.drawString('Last Heart Rate:', g.getWidth() / 2, g.getHeight() / 2 - 20);
|
||||
g.setFont('6x8', 4);
|
||||
g.drawString(currentHR.toString(), g.getWidth() / 2, g.getHeight() / 2 + 10);
|
||||
g.setFont('6x8', 1.6);
|
||||
g.drawString(' BPM', g.getWidth() / 2 + 42, g.getHeight() / 2 + 12);
|
||||
} else {
|
||||
g.setFont('6x8', 2);
|
||||
g.drawString('No data', g.getWidth() / 2, g.getHeight() / 2 + 5);
|
||||
g.setFont('6x8', 1);
|
||||
g.drawString(message || 'Press button to start', g.getWidth() / 2, g.getHeight() / 2 + 30);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the display
|
||||
g.flip();
|
||||
}
|
||||
|
||||
function saveDataToCSV() {
|
||||
let fileName = "phystrax_hrm.csv";
|
||||
let file = require("Storage").open(fileName, "a"); // Open the file for appending
|
||||
|
||||
// Check if the file is empty (i.e., newly created)
|
||||
if (file.getLength() === 0) {
|
||||
// Write the header if the file is empty
|
||||
file.write("Timestamp,Heart Rate(bpm),HRV(ms)\n");
|
||||
}
|
||||
|
||||
// Append the data
|
||||
logData.forEach(entry => {
|
||||
let scaledHRV = entry.hrv * 13.61;
|
||||
file.write(`${entry.timestamp},${entry.heartRate},${scaledHRV}\n`);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
setWatch(function() {
|
||||
if (!isMeasuring) {
|
||||
startMeasure();
|
||||
} else {
|
||||
stopMeasure();
|
||||
}
|
||||
}, BTN1, { repeat: true, edge: 'rising' });
|
||||
|
||||
drawScreen();
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
|
|
@ -0,0 +1,65 @@
|
|||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="../../css/spectre.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="data"></div>
|
||||
<button class="btn btn-default" id="btnSave">Save</button>
|
||||
<button class="btn btn-default" id="btnDelete">Delete</button>
|
||||
|
||||
<script src="../../core/lib/interface.js"></script>
|
||||
<script>
|
||||
var dataElement = document.getElementById("data");
|
||||
var csvData = "";
|
||||
|
||||
function getData() {
|
||||
// show loading window
|
||||
Util.showModal("Loading...");
|
||||
// get the data
|
||||
dataElement.innerHTML = "";
|
||||
Util.readStorageFile(`phystrax_hrm.csv`,data=>{
|
||||
csvData = data.trim();
|
||||
// remove window
|
||||
Util.hideModal();
|
||||
// If no data, report it and exit
|
||||
if (data.length==0) {
|
||||
dataElement.innerHTML = "<b>No data found</b>";
|
||||
return;
|
||||
}
|
||||
// Otherwise parse the data and output it as a table
|
||||
dataElement.innerHTML = `<table>
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<th>Heart Rate (bpm)</th>
|
||||
<th>HRV (ms)</th>
|
||||
</tr>`+data.trim().split("\n").map(l=>{
|
||||
l = l.split(",");
|
||||
return `<tr>
|
||||
<td>${l[0]}</td>
|
||||
<td>${l[1]}</td>
|
||||
<td>${l[2]}</td>
|
||||
</tr>`
|
||||
}).join("\n")+"</table>";
|
||||
});
|
||||
}
|
||||
|
||||
// You can call a utility function to save the data
|
||||
document.getElementById("btnSave").addEventListener("click", function() {
|
||||
Util.saveCSV("phystrax_hrm.csv", csvData);
|
||||
});
|
||||
// Or you can also delete the file
|
||||
document.getElementById("btnDelete").addEventListener("click", function() {
|
||||
Util.showModal("Deleting...");
|
||||
Util.eraseStorageFile("phystrax_hrm.csv", function() {
|
||||
Util.hideModal();
|
||||
getData();
|
||||
});
|
||||
});
|
||||
// Called when app starts
|
||||
function onInit() {
|
||||
getData();
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
{ "id": "phystrax",
|
||||
"name": "PhysTrax",
|
||||
"shortName":"PhysTrax",
|
||||
"icon": "app.png",
|
||||
"version":"0.01",
|
||||
"description": "Tracking physiological measurements to support active learning in classrooms",
|
||||
"tags": "health",
|
||||
"interface": "interface.html",
|
||||
"readme": "README.md",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"storage": [
|
||||
{"name":"phystrax.app.js","url":"app.js"},
|
||||
{"name":"phystrax.img","url":"app-icon.js","evaluate":true}
|
||||
],
|
||||
"data": [{"name":"phystrax_hrm.csv"}]
|
||||
}
|
||||
Loading…
Reference in New Issue