Merge pull request #3374 from ekwawu/master

master
Rob Pilling 2024-04-22 20:49:40 +01:00
commit 377b43619b
7 changed files with 248 additions and 0 deletions

1
apps/phystrax/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: New App.

8
apps/phystrax/README.md Normal file
View File

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

View File

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

157
apps/phystrax/app.js Normal file
View File

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

BIN
apps/phystrax/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

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

View File

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