In this tutorial, I will walk you through the process of creating a simple ECG application using the DFRobot Gravity MAX30102 PPG Heart Rate and Oximeter Sensor connected to a DFRobot Unihiker via I2C. Using Python and the Tkinter library, we will visualize real-time heart rate and oxygen saturation data on an interactive interface.
Tutorial Objective
In this tutorial, you'll learn:
1. How to interface the MAX30102 sensor with Unihiker via I2C.
2. How to use Python to read sensor data.
3. Techniques for visualizing ECG-like heart rate and oxygen data using Python's Tkinter.
Create a new folder on your computer, for example with the name: Electrocardiogram and in it a new file called: main.py.
Copy and save now the following provided Python code into main.py:
from signal import signal, SIGINT
from time import sleep
from tkinter import Tk, Canvas, Label, Frame
from types import FrameType
from typing import Optional
from pinpong.board import Board
from pinpong.libs.dfrobot_max30102 import DFRobot_BloodOxygen_S
SCREEN_WIDTH: int = 240
SCREEN_HEIGHT: int = 320
CANVAS_HEIGHT: int = SCREEN_HEIGHT // 2
CANVAS_MARGIN: int = 10
BG_COLOR: str = "black"
HEADLINE_COLOR: str = "white"
HEART_RATE_COLOR: str = "azure"
OXYGEN_COLOR: str = "yellow"
SCALE_COLOR: str = "gray"
HEADLINE_FONT: tuple = ("Helvetica", 20, "bold")
VALUE_FONT: tuple = ("Helvetica", 12)
def signal_handler(sig: int, frame: Optional[FrameType]) -> None:
"""
Handles operating system signals to safely stop sensor operations.
:param sig: The signal number received from the operating system.
:type sig: int
:param frame: The current stack frame when the signal was triggered.
:type frame: Optional[FrameType]
:return: None
"""
global sensor
_ = sig
_ = frame
print('[INFO] Stopping sensor...')
sensor.sensor_end_collect()
exit(0)
class App(Tk):
def __init__(self, width: int, height: int, canvas_height: int, canvas_margin: int):
"""
Class constructor
:param width: Width of the application window in pixels.
:type width: int
:param height: Height of the application window in pixels.
:type height: int
:param canvas_height: Height of the data drawing canvas.
:type canvas_height: int
:param canvas_margin: Margin between canvas and window borders.
:type canvas_margin: int
"""
super().__init__()
self._width = int(width)
self._height = int(height)
self._ecg_height = int(canvas_height)
self._ecg_margin = int(canvas_margin)
self._ecg_width = self._width - 2 * self._ecg_margin
self._data_points_hr = []
self._data_points_oxy = []
self._max_points = self._ecg_width
self.geometry(f"{self._width}x{self._height}+0+0")
self.resizable(width=False, height=False)
self.config(bg=BG_COLOR)
self._title = Label(self, text="HealthHiker", font=HEADLINE_FONT, bg=BG_COLOR, fg=HEADLINE_COLOR)
self._title.pack(pady=15)
self._ecg = Canvas(self, width=self._ecg_width, height=self._ecg_height, bg=BG_COLOR, highlightthickness=1)
self._ecg.pack(padx=self._ecg_margin)
self._data = Frame(self, bg=BG_COLOR)
self._data.pack(fill="x", pady=10)
self._hr = Label(self._data, text="0 bpm", font=VALUE_FONT, bg=BG_COLOR, fg=HEART_RATE_COLOR)
self._hr.pack(side="left", padx=20)
self._oxy = Label(self._data, text="0 %", font=VALUE_FONT, bg=BG_COLOR, fg=OXYGEN_COLOR)
self._oxy.pack(side="right", padx=20)
def update_ecg(self, heart: int, oxygen: int) -> None:
"""
Updates the ECG display with the latest heart rate and oxygen saturation data.
:param heart: Current heart rate in beats per minute (bpm).
:type heart: int
:param oxygen: Current oxygen saturation percentage (%).
:type oxygen: int
:returns: None
"""
scaled_hr = self._ecg_height - int((heart / 200) * self._ecg_height)
scaled_oxy = self._ecg_height - int((oxygen / 100) * self._ecg_height) + 10
self._data_points_hr.append(scaled_hr)
self._data_points_oxy.append(scaled_oxy)
if len(self._data_points_hr) > self._max_points:
self._data_points_hr.pop(0)
if len(self._data_points_oxy) > self._max_points:
self._data_points_oxy.pop(0)
self._ecg.delete("all")
for i in range(1, len(self._data_points_hr)):
self._ecg.create_line(i - 1, self._data_points_hr[i - 1], i, self._data_points_hr[i],
fill=HEART_RATE_COLOR, width=2)
self._ecg.create_line(i - 1, self._data_points_oxy[i - 1], i, self._data_points_oxy[i],
fill=OXYGEN_COLOR, width=2)
for y in range(0, self._ecg_height + 1, 10):
self._ecg.create_line(self._ecg_width - 5, y, self._ecg_width, y, fill=SCALE_COLOR, width=1)
for y in range(0, self._ecg_height + 1, 10):
self._ecg.create_line(0, y, 5, y, fill=SCALE_COLOR, width=1)
self._hr.config(text=f"{heart} bpm")
self._oxy.config(text=f"{oxygen} %")
if __name__ == '__main__':
Board("UNIHIKER").begin()
sensor = DFRobot_BloodOxygen_S()
while not sensor.begin():
print('[INFO] Initializing sensor...')
sleep(1)
else:
print('[INFO] Sensor initialized...')
sensor.sensor_start_collect()
sleep(1)
signal(SIGINT, signal_handler)
app = App(width=SCREEN_WIDTH, height=SCREEN_HEIGHT, canvas_height=CANVAS_HEIGHT, canvas_margin=CANVAS_MARGIN)
print('[INFO] Press Ctrl + c to stop...')
print('[INFO] Please wait for data to be collected...')
while True:
sensor.get_heartbeat_SPO2()
heart_rate = sensor.heartbeat
oxygen_level = sensor.SPO2
if heart_rate == -1 or oxygen_level == -1:
continue
app.update_ecg(heart=heart_rate, oxygen=oxygen_level)
app.update()
sleep(.25)
Short code explanation:
The Python code contains a few imports, which bring in the necessary libraries and modules. Next, constants are defined. These contain values for: screen dimensions, canvas height, and margins. As well as values for colors and fonts.
The signal_handler function ensures a graceful shutdown when the program is interrupted (e.g. with Ctrl + c).
The App(Tk) class defines the GUI application using Tkinter. Inside this class is a method update_ecg. Via this method the GUI is updated with real-time heart rate and oxygen data.
After the if condition, the objects are initialized and the while loop reads data from the sensor (and sends values to the class).
Upload the project to Unihiker via SCP or SMB. The online documents will help you! If you have not changed the initial SSH configuration the user is: root and password is: dfrobot.
Here the SCP example:
# upload via SCP
$ scp -r Electrocardiogram/ 10.1.2.3:/root/
Now you can start the application via the touch screen or SSH.
# ssh from local to unihiker
$ ssh 10.1.2.3
# change directory
$ cd /root/Electrocardiogram/
# execute Python script
$ python3 main.py
The application will start and after a few seconds when you place your finger on the sensor, the ECG interface will appear on the Unihiker screen.
Please ignore my excessively high heart rate values, these should be around 80 for you.
1. Data Logging: Save real-time heart rate and oxygen data to a file for later analysis.
2. Health Insights: Implement algorithms to detect irregular heartbeats or anomalies.
3. More sensors: Add other sensors to measure values.
With this step-by-step guide, you now have a functional ECG application and plenty of room for enhancements. Explore further, experiment, and bring your health monitoring ideas to life!