Heart Guard: Revolutionizing Health Monitoring with Wireless Data Acquisition¶
Introduction for the project¶
Health care today, is one of the biggest challenges that are facing humanity nowadays, with the rapid increase of the population, the limited resources and the man power that are effecting the majority of the nations around the world in facing the large number of patients, limited hospitals capacity, that limits the regular citizen chance of meeting a doctor or getting treated, checked for free or with a low cost (supported by government), That’s where the use of technology come to save the world by providing tools and ideas to solve those problems, in this project we are going to provide an engineering solution that carry the purpose of minimizing the physical visits to a treatment center, for the patients with chronic disease by using a monitoring device that can detect any abnormal changes of sugar level, body temperature, hear rate, blood pressure, oxygen levels for the patient that is sent to the doctor monitoring devise behind a screen that will warn the doctor for any changes that will allow the doctor to handle multiple patients without physical attendance that will lower the pressure on the limited man power and resources around the world.
keywords¶
1.remote health monetoring. 2.disease prediction. 3.rapid monetoring. 4.biomedical. 5.vital signs. 6.sensors. 7.medical information. 8.telemedicine wearables. 9.feedback for comparison.
The Journey to build (Heart Guard)¶
Task I¶
Task I was done by Mohammed Karim and Ali Karim
We began by using different types of microcontrollers, such as the MKR1010, and connected a MAX30100 sensor to understand its functionality. Utilizing the MAX30100 library, we had the code ready to measure heart rate and SpO2, but it required some adjustments to work properly.
Ali and Mohammed collaborated to create and test a code for the MAX30100 sensor. After integrating their adjustments, they aimed to fine-tune the code for accurate heart rate and SpO2 measurements.
Test Code:
#include <Wire.h>
#include "MAX30100_PulseOximeter.h"
#define BLYNK_PRINT Serial
char auth[] = "G9nCH4W7AWUwviplzwpGLleH0fOdXF9_";
char ssid[] = "justdoelectronics";
char pass[] = "123456789";
PulseOximeter pox;
uint8_t DHTPin = 18;
float bodytemperature;
float BPM, SpO2;
uint32_t tsLastReport = 0;
void onBeatDetected() {
Serial.println("Beat Detected!");
}
void setup() {
Serial.begin(9600);
pinMode(19, OUTPUT);
pinMode(DHTPin, INPUT);
Serial.print("Initializing Pulse Oximeter..");
if (!pox.begin()) {
Serial.println("FAILED");
for (;;)
;
} else {
Serial.println("SUCCESS");
pox.setOnBeatDetectedCallback(onBeatDetected);
}
pox.setIRLedCurrent(MAX30100_LED_CURR_7_6MA);
}
void loop() {
pox.update();
BPM = pox.getHeartRate();
SpO2 = pox.getSpO2();
Serial.print("Heart rate:");
Serial.print(BPM);
Serial.print(" bpm / SpO2:");
Serial.print(SpO2);
Serial.println(" %");
tsLastReport = millis();
}
The result:
This is the final code developed for the Arduino environment using the ESP32 S3 LCD Touch 1.28 microcontroller. The code utilizes the MAX30105 library to interface with the MAX30102 sensor, incorporating necessary adjustments for accurate heart rate and SpO2 readings. It processes infrared (IR) signals to detect heartbeats, calculates beats per minute (BPM), and averages the readings for smoother results.
#include <Wire.h>
#include "MAX30105.h"
#include "heartRate.h"
// Create an instance of the MAX30105 class to interact with the sensor
MAX30105 particleSensor;
// Define the size of the rates array for averaging BPM; can be adjusted for smoother results
const byte RATE_SIZE = 4; // Increase this for more averaging. 4 is a good starting point.
byte rates[RATE_SIZE]; // Array to store heart rate readings for averaging
byte rateSpot = 0; // Index for inserting the next heart rate reading into the array
long lastBeat = 0; // Timestamp of the last detected beat, used to calculate BPM
float beatsPerMinute; // Calculated heart rate in beats per minute
int beatAvg; // Average heart rate after processing multiple readings
void setup() {
Serial.begin(115200); // Start serial communication at 115200 baud rate
Serial.println("Initializing...");
// Attempt to initialize the MAX30105 sensor. Check for a successful connection and report.
if (!particleSensor.begin(Wire, I2C_SPEED_FAST)) { // Start communication using fast I2C speed
Serial.println("MAX30102 was not found. Please check wiring/power. ");
while (1); // Infinite loop to halt further execution if sensor is not found
}
Serial.println("Place your index finger on the sensor with steady pressure.");
particleSensor.setup(); // Configure sensor with default settings for heart rate monitoring
particleSensor.setPulseAmplitudeRed(0x0A); // Set the red LED pulse amplitude (intensity) to a low value as an indicator
particleSensor.setPulseAmplitudeGreen(0); // Turn off the green LED as it's not used here
}
void loop() {
long irValue = particleSensor.getIR(); // Read the infrared value from the sensor
if (checkForBeat(irValue) == true) { // Check if a heart beat is detected
long delta = millis() - lastBeat; // Calculate the time between the current and last beat
lastBeat = millis(); // Update lastBeat to the current time
beatsPerMinute = 60 / (delta / 1000.0); // Calculate BPM
// Ensure BPM is within a reasonable range before updating the rates array
if (beatsPerMinute < 255 && beatsPerMinute > 20) {
rates[rateSpot++] = (byte)beatsPerMinute; // Store this reading in the rates array
rateSpot %= RATE_SIZE; // Wrap the rateSpot index to keep it within the bounds of the rates array
// Compute the average of stored heart rates to smooth out the BPM
beatAvg = 0;
for (byte x = 0 ; x < RATE_SIZE ; x++)
beatAvg += rates[x];
beatAvg /= RATE_SIZE;
}
}
// Output the current IR value, BPM, and averaged BPM to the serial monitor
Serial.print("IR=");
Serial.print(irValue);
Serial.print(", BPM=");
Serial.print(beatsPerMinute);
Serial.print(", Avg BPM=");
Serial.print(beatAvg);
// Check if the sensor reading suggests that no finger is placed on the sensor
if (irValue < 50000)
Serial.print(" No finger?");
Serial.println();
}
After downloading the required libraries, the code was working properly in Arduino IDE C language…
Task II¶
Task II was done by Mohammed Karim & Ali Karim
We initially experimented with the MKR1010, but it was only a starting point. For the actual project, we used the ESP32 S3 LCD Touch 1.28, a clock-shaped microcontroller that supports MicroPython. Thonny was used as the coding environment for this setup.
This code was developed for the MicroPython environment, incorporating essential adjustments and optimizations to ensure accurate and reliable readings from the MAX30102 sensor when used with the ESP32 S3 LCD Touch 1.28 microcontroller.
from machine import Pin, SPI, SoftI2C
from utime import ticks_diff, ticks_us, ticks_ms, sleep
import gc9a01py as gc9a01
from max30102 import MAX30102, MAX30105_PULSE_AMP_MEDIUM
from fonts.romfonts import vga2_bold_16x32 as font4
error = True
class HeartRateMonitor:
"""A simple heart rate monitor that uses a moving window to smooth the signal and find peaks."""
def _init_(self, sample_rate=100, window_size=10, smoothing_window=5):
self.sample_rate = sample_rate
self.window_size = window_size
self.smoothing_window = smoothing_window
self.samples = []
self.timestamps = []
self.filtered_samples = []
def add_sample(self, sample):
"""Add a new sample to the monitor."""
timestamp = ticks_ms()
self.samples.append(sample)
self.timestamps.append(timestamp)
# Apply smoothing
if len(self.samples) >= self.smoothing_window:
smoothed_sample = (
sum(self.samples[-self.smoothing_window :]) / self.smoothing_window
)
self.filtered_samples.append(smoothed_sample)
else:
self.filtered_samples.append(sample)
# Maintain the size of samples and timestamps
if len(self.samples) > self.window_size:
self.samples.pop(0)
self.timestamps.pop(0)
self.filtered_samples.pop(0)
def find_peaks(self):
"""Find peaks in the filtered samples."""
peaks = []
if len(self.filtered_samples) < 3: # Need at least three samples to find a peak
return peaks
# Calculate dynamic threshold based on the min and max of the recent window of filtered samples
recent_samples = self.filtered_samples[-self.window_size :]
min_val = min(recent_samples)
max_val = max(recent_samples)
threshold = (
min_val + (max_val - min_val) * 0.5
) # 50% between min and max as a threshold
for i in range(1, len(self.filtered_samples) - 1):
if (
self.filtered_samples[i] > threshold
and self.filtered_samples[i - 1] < self.filtered_samples[i]
and self.filtered_samples[i] > self.filtered_samples[i + 1]
):
peak_time = self.timestamps[i]
peaks.append((peak_time, self.filtered_samples[i]))
return peaks
def calculate_heart_rate(self):
"""Calculate the heart rate in beats per minute (BPM)."""
peaks = self.find_peaks()
if len(peaks) < 2:
return None # Not enough peaks to calculate heart rate
# Calculate the average interval between peaks in milliseconds
intervals = []
for i in range(1, len(peaks)):
interval = ticks_diff(peaks[i][0], peaks[i - 1][0])
intervals.append(interval)
average_interval = sum(intervals) / len(intervals)
# Convert intervals to heart rate in beats per minute (BPM)
heart_rate = (
60000 / average_interval
) # 60 seconds per minute * 1000 ms per second
return heart_rate
def setup_display():
"""Initialize the GC9A01 display."""
spi = SPI(2, baudrate=80000000, polarity=0, sck=Pin(10), mosi=Pin(11))
tft = gc9a01.GC9A01(
spi, dc=Pin(8, Pin.OUT), cs=Pin(9, Pin.OUT),
reset=Pin(14, Pin.OUT), backlight=Pin(2, Pin.OUT),
rotation=0
)
return tft
def update_display(tft, heart_rate):
"""Update the display with the current heart rate."""
tft.fill(gc9a01.BLACK)
line = tft.height // 2
col = tft.width // 3
if heart_rate is not None:
display_text = "HR: {:.0f} BPM".format(heart_rate)
else:
display_text = "No HR Data"
tft.text(font4, display_text, col, line, gc9a01.WHITE, gc9a01.RED)
if error==True:
tft.text(font4, "warning", col,0, gc9a01.WHITE, gc9a01.RED)
def main():
# Initialize I2C and sensor
i2c = SoftI2C(
sda=Pin(15), # Here, use your I2C SDA pin
scl=Pin(16), # Here, use your I2C SCL pin
freq=400000,
)
sensor = MAX30102(i2c=i2c)
if sensor.i2c_address not in i2c.scan():
print("Sensor not found.")
return
elif not (sensor.check_part_id()):
print("I2C device ID not corresponding to MAX30102 or MAX30105.")
return
else:
print("Sensor connected and recognized.")
print("Setting up sensor with default configuration.", "\n")
sensor.setup_sensor()
sensor_sample_rate = 400
sensor.set_sample_rate(sensor_sample_rate)
sensor_fifo_average = 8
sensor.set_fifo_average(sensor_fifo_average)
sensor.set_active_leds_amplitude(MAX30105_PULSE_AMP_MEDIUM)
actual_acquisition_rate = int(sensor_sample_rate / sensor_fifo_average)
sleep(1)
print(
"Starting data acquisition from RED & IR registers...",
"press Ctrl+C to stop.",
"\n",
)
sleep(1)
# Initialize heart rate monitor
hr_monitor = HeartRateMonitor(
sample_rate=actual_acquisition_rate,
window_size=int(actual_acquisition_rate * 3),
)
# Initialize display
tft = setup_display()
hr_compute_interval = 2 # seconds
ref_time = ticks_ms() # Reference time
while True:
sensor.check()
if sensor.available():
red_reading = sensor.pop_red_from_storage()
ir_reading = sensor.pop_ir_from_storage()
hr_monitor.add_sample(ir_reading)
if ticks_diff(ticks_ms(), ref_time) / 1000 > hr_compute_interval:
heart_rate = hr_monitor.calculate_heart_rate()
update_display(tft, heart_rate)
ref_time = ticks_ms()
if _name_ == "_main_":
main()
Task III¶
Task III was done by Ahmed Mahdi & Ali Karim.
my teammate Ali and I designed a custom smartwatch frame using Fusion 360. This design accommodates the ESP32 microcontroller, a 1.28-inch display, a rechargeable battery, and the MAX30102 sensor, ensuring a compact and efficient layout for the final product.
Frame Design¶
Here are the steps for Creating the Smartwatch Frame:
-
Determine Component Dimensions: Accurately measure the dimensions of the ESP32 microcontroller and the 1.28-inch display to ensure proper fit within the frame.
-
Incorporate a Power Source: Integrate a rechargeable battery into the design, considering the placement and space required for efficient power management.
-
Integrate Health Monitoring Sensor: Include the MAX30102 sensor in the frame design, ensuring that the sensor’s positioning allows for optimal functionality and contact with the user’s skin.
Procedure¶
After designing the frame for the smartwatch, the silicone lace was designed using Autodesk Fusion.
Procedure of designing the strap¶
-
Measure the distance between the tabs on the frame and draw a line slightly shorter than the measured distance, as shown in the figure, to create the lace.
-
Draw two straight lines starting from the end of the previously created line.
-
Create a slot for the MAX30100 sensor by drawing a rectangle slightly wider than the previously drawn lines. Use the “trim” option to remove any unwanted lines. Then, press the letter “P” on the keyboard and select the lines to project.
Next, the laser cutting machine was used to cut the required quantity of the designed pieces.
After cutting, the pieces were joined together using glue.
Finally, the silicone mixture was carefully poured into the molds and set aside to cure, allowing the silicone to solidify and accurately form the desired shape.
Task IV¶
Task IV was done by Mahdi Albasri & Ali Karim
This task is composed of creating an application that connects the microcontroller to the phone with bluetooth and transmitting sensor readings to the application.
The bluetooth communication¶
With the help of my friend Ali, we have successfully done data transfer and receive using an ESP32 as a sample Code:
########## DIGITAL MANUFACTURING ##########
# PIKACHU Project
# Authors: Miguel Angel Guzman
# Kadriye Nur Bakirci
###########################################
########## IMPORT REQUIRED LIBRARIES ##########
import bluetooth
from ble_uart_peripheral import BLEUART
from machine import Pin, ADC
import time
# Set up ADC for reading sensor value
sensor_pin = 34 # GPIO pin connected to the sensor
adc = ADC(Pin(sensor_pin)) # Initialize ADC on the sensor pin
adc.atten(ADC.ATTN_0DB) # Set attenuation to 0 dB (adjust if needed)
# Define a threshold for detecting if the sensor is connected
DISCONNECT_THRESHOLD = 10 # This is an example value; adjust based on your sensor
# Create BLE object
ble = bluetooth.BLE()
# Open UART session for BLE
uart = BLEUART(ble)
# Define ISR for UART input on BLE connection
def on_rx(data):
# Read UART string, AppInventor sends raw bytes
uart_in = data # read the message received from the Smartphone via Bluetooth
try:
message = uart_in.decode('utf-8') # decode bytes to string
except Exception as e:
print("Error decoding message:", e)
return
print("UART IN: ", message) # display the message received from the Smartphone on the Thonny console
# Example: Process message commands if needed
if message == 'READ_SENSOR':
send_sensor_value()
def send_sensor_value():
# Read the sensor value
sensor_value = adc.read() # Read the analog value from the sensor
# Check if the sensor value indicates a disconnection
if sensor_value < DISCONNECT_THRESHOLD:
# Send "NOT AVAILABLE" if the sensor value is below the threshold
uart.write('SENSOR_VALUE:NOT AVAILABLE\n')
else:
# Send the sensor value over UART
uart.write(f'SENSOR_VALUE:{sensor_value}\n') # Send the data in a readable format
# Map ISR to UART read interrupt
uart.irq(handler=on_rx)
# Main loop
while True:
# Periodically read and send sensor data
time.sleep(5) # Adjust the interval as needed
send_sensor_value()
The result:
Creating the application using MIT app inventor¶
MIT App Inventor is a visual programming environment that allows individuals, even those without extensive programming experience, to create mobile applications for Android devices. It’s particularly well-suited for creating simple and prototype-level applications that involve interactions with hardware devices like the ESP32
Here are the steps to create the application:
-
Create a new project in MIT App Inventor
-
Add the BluetoothClient and BluetoothLE components to your project
-
Declare the required permissions for your android device in the AndroidManifest.xml, for my app I need ACCESS_FINE_LOCATION, BLUETOOTH_SCAN, BLUETOOTH_CONNECT
-
Put the necessary blocks to connect the smartphone to your device
-
Design a simple interface with a label to replace the text with sensor data
-
Use programming blocks to send Bluetooth commands to the device when its connected to the smartphone
-
Load the MicroPython code onto your device.
-
Install the MIT App Inventor app on your smartphone and install the app you created.
-
Pair and connect your smartphone with the device through Bluetooth.
-
Open the app and connect the smartphone to your device and see the value changing This is how the application looks like Below is the block construction for MIT app inventor
We encountered issues with the Bluetooth connection due to differences in UUID configuration between the ESP32 and the smartwatch. Fortunately, we were able to resolve the problem.
This is the code used to connect through bluetooth and get sensor values:
from machine import Pin, SPI, SoftI2C
from utime import ticks_diff, ticks_us, ticks_ms, sleep
from ubluetooth import BLE, UUID, FLAG_READ, FLAG_WRITE, FLAG_NOTIFY
import gc9a01py as gc9a01
from max30102 import MAX30102, MAX30105_PULSE_AMP_MEDIUM
from fonts.romfonts import vga2_bold_16x32 as font4
error = True
# Global variables for BLE characteristic handles and connection status
char_handles = {}
connected = False
ble = BLE()
class HeartRateMonitor:
"""A simple heart rate monitor that uses a moving window to smooth the signal and find peaks."""
def __init__(self, sample_rate=100, window_size=10, smoothing_window=5):
self.sample_rate = sample_rate
self.window_size = window_size
self.smoothing_window = smoothing_window
self.samples = []
self.timestamps = []
self.filtered_samples = []
def add_sample(self, sample):
"""Add a new sample to the monitor."""
timestamp = ticks_ms()
self.samples.append(sample)
self.timestamps.append(timestamp)
# Apply smoothing
if len(self.samples) >= self.smoothing_window:
smoothed_sample = (
sum(self.samples[-self.smoothing_window:]) / self.smoothing_window
)
self.filtered_samples.append(smoothed_sample)
else:
self.filtered_samples.append(sample)
# Maintain the size of samples and timestamps
if len(self.samples) > self.window_size:
self.samples.pop(0)
self.timestamps.pop(0)
self.filtered_samples.pop(0)
def find_peaks(self):
"""Find peaks in the filtered samples."""
peaks = []
if len(self.filtered_samples) < 3:
return peaks
# Calculate dynamic threshold based on the min and max of the recent window of filtered samples
recent_samples = self.filtered_samples[-self.window_size:]
min_val = min(recent_samples)
max_val = max(recent_samples)
threshold = (min_val + (max_val - min_val) * 0.5)
for i in range(1, len(self.filtered_samples) - 1):
if (
self.filtered_samples[i] > threshold
and self.filtered_samples[i - 1] < self.filtered_samples[i]
and self.filtered_samples[i] > self.filtered_samples[i + 1]
):
peak_time = self.timestamps[i]
peaks.append((peak_time, self.filtered_samples[i]))
return peaks
def calculate_heart_rate(self):
"""Calculate the heart rate in beats per minute (BPM)."""
peaks = self.find_peaks()
if len(peaks) < 2:
return None # Not enough peaks to calculate heart rate
# Calculate the average interval between peaks in milliseconds
intervals = []
for i in range(1, len(peaks)):
interval = ticks_diff(peaks[i][0], peaks[i - 1][0])
intervals.append(interval)
average_interval = sum(intervals) / len(intervals)
# Convert intervals to heart rate in beats per minute (BPM)
heart_rate = 60000 / average_interval
return heart_rate
def setup_display():
"""Initialize the GC9A01 display."""
spi = SPI(2, baudrate=80000000, polarity=0, sck=Pin(10), mosi=Pin(11))
tft = gc9a01.GC9A01(
spi, dc=Pin(8, Pin.OUT), cs=Pin(9, Pin.OUT),
reset=Pin(14, Pin.OUT), backlight=Pin(2, Pin.OUT),
rotation=0
)
return tft
def update_display(tft, heart_rate):
"""Update the display with the current heart rate."""
tft.fill(gc9a01.BLACK)
line = tft.height // 2
col = tft.width // 3
if heart_rate is not None:
display_text = "HR: {:.0f} BPM".format(heart_rate)
else:
display_text = "No HR Data"
tft.text(font4, display_text, col, line, gc9a01.WHITE, gc9a01.RED)
if error:
tft.text(font4, "warning", col, 0, gc9a01.WHITE, gc9a01.RED)
# Callback function to handle BLE events
def bt_callback(event, data):
global connected
if event == 1: # Connection event
print("Connected")
connected = True
elif event == 2: # Disconnection event
print("Disconnected")
connected = False
start_advertising()
# Function to set up BLE service and multiple characteristics
def setup_ble_service():
global char_handles
ble.active(True)
ble.irq(bt_callback)
# Define a service UUID
service_uuid = UUID('19b10000-e8f2-537e-4f6c-d104768a1214')
# Define characteristics UUIDs and properties
char_uuids = [
UUID('19b10001-e8f2-537e-4f6c-d104768a1214'),
UUID('19b10002-e8f2-537e-4f6c-d104768a1214'),
UUID('19b10003-e8f2-537e-4f6c-d104768a1214')
]
char_properties = FLAG_READ | FLAG_WRITE | FLAG_NOTIFY
# Create the characteristics
chars = [(char_uuid, char_properties) for char_uuid in char_uuids]
# Register the service with its characteristics
service = (service_uuid, chars)
handles = ble.gatts_register_services((service,))
# Store the characteristic handles
char_handles['char1'] = handles[0][0]
char_handles['char2'] = handles[0][1]
char_handles['char3'] = handles[0][2]
# Function to start BLE advertising
def start_advertising():
adv_name = 'ESP32-BLE'
adv_data = bytearray(b'\x02\x01\x06' + bytes([len(adv_name) + 1, 0x09]) + adv_name.encode())
ble.gap_advertise(100, adv_data)
print("Advertising as", adv_name)
# Function to send data through a specified characteristic
def send_data(char_key, data):
global char_handles, connected
if connected and char_key in char_handles:
try:
handle = char_handles[char_key]
data_bytes = data.encode('utf-8')
ble.gatts_notify(0, handle, data_bytes)
print(f"Sent data to {char_key}: {data}")
except OSError as e:
print(f"Failed to send data: {e}")
def main():
# Setup Bluetooth and start advertising
setup_ble_service()
start_advertising()
# Initialize I2C and sensor
i2c = SoftI2C(sda=Pin(15), scl=Pin(16), freq=400000)
sensor = MAX30102(i2c=i2c)
if sensor.i2c_address not in i2c.scan() or not sensor.check_part_id():
print("Sensor not found or not recognized.")
return
sensor.setup_sensor()
sensor.set_sample_rate(400)
sensor.set_fifo_average(8)
sensor.set_active_leds_amplitude(MAX30105_PULSE_AMP_MEDIUM)
# Initialize heart rate monitor and display
hr_monitor = HeartRateMonitor(sample_rate=50, window_size=150)
tft = setup_display()
ref_time = ticks_ms()
while True:
sensor.check()
if sensor.available():
ir_reading = sensor.pop_ir_from_storage()
hr_monitor.add_sample(ir_reading)
if ticks_diff(ticks_ms(), ref_time) / 1000 > 2:
heart_rate = hr_monitor.calculate_heart_rate()
update_display(tft, heart_rate)
if heart_rate:
send_data('char3', f"HR: {int(heart_rate)} BPM") # Send to char1
ref_time = ticks_ms()
if __name__ == "__main__":
main()
Final Product¶
In the end after Design is printed using 3D printer, and the strap was created using moulding and casting, and the application is finished, we assemble the final product and install the app and test if both the sensor and the app work.
-
Final Product assembled
-
test the sensor and see if its working
- check the app and see if sensor data is being transmitted
This project is to show the world that we can adjust monitoring devices and can use iot to collect data so the doctors can check for the patient states without the need of the patient attendance, this can minimize the large numbers of patients in health centers globally and can create new job opportunities by allowing simple manufacturing in hospitals to at least use the simple moulding and casting techniques to create suitable straps for the patients and connect other devices such as scales and glucose monitoring devices to one application that the doctor is following large numbers of multiple patients data every day.
References¶
Below are detailed pages showcasing the work of all project members