This project aims to develop an affordable, battery-powered, machine learning-based portable device to detect Atrial Fibrillation (AFib) using Photoplethysmography (PPG). AFib, a common cardiac arrhythmia characterized by irregular and often rapid heartbeats, increases the risk of stroke and heart failure if undetected. PPG, a non-invasive optical technique, measures blood volume changes in peripheral tissues, typically via a wearable sensor on the wrist or finger, to derive heart rate and rhythm data. By leveraging machine learning (ML) algorithms, such as those implemented through Edge Impulse, the project seeks to analyze PPG signals in real-time to identify AFib patterns with high accuracy, enabling early detection and timely medical intervention.The system will process PPG data on an edge ML device, ensuring low power consumption, portability, and accessibility. Edge Impulse will be used to train, optimize, and deploy ML models tailored for resource-constrained devices, allowing for real-time AFib detection without reliance on cloud processing. The project targets applications in remote healthcare, wearable technology, and preventive cardiology, offering a cost-effective solution for continuous heart rhythm monitoring.
How Photoplethysmography (PPG) Sensor FunctionsA PPG sensor works by emitting light, typically red, infrared or green, into the skin and measuring the amount of light reflected or absorbed by blood vessels. As blood volume changes with each heartbeat, the sensor detects variations in light intensity, which are processed to calculate heart rate, blood oxygen levels, or other vital signs. The sensor usually consists of a light source (LED) and a photodetector (PD) that captures the reflected light.
We aimed for a cost-effective, portable device that’s easy to carry in a pocket. We plan to use an ESP32-based M5Stack M5StickC PLUS, which features a small display, buzzer, and buttons.
The M5Stack StickC Plus has headers on one side that allow for a variety of sensor hats. We will be using an M5Stack Heart Rate Hat that incorporates the Maxim MAX30102 sensor. The MAX30102 is an integrated pulse oximetry and heart-rate monitor module.
We need to sign up an account at the Edge Impulse Studio and create a new project for the data processing, model training, and deployment.
We are using the MIMIC PERform AF Dataset.
Citation: Charlton PH et al.Detecting beats in the photoplethysmogram: benchmarking open-source algorithms. Physiological Measurement 2022. DOI: 10.1088/1361-6579/ac826d
This dataset contains ECG and PPG recordings of 20-minutes duration, some of which were acquired during atrial fibrillation (AF), and the rest were acquired during normal sinus rhythm. The data were collected from 35 critically-ill adults ((19 in AF, 16 not in AF) during routine clinical care. Data were measured using a bedside monitor at 125 Hz. The datasets can be downloaded from the following link.
https://ppg-beats.readthedocs.io/en/latest/datasets/mimic_perform_af
We downloaded the AF subjects and non-AF subjects data in the CSV format.
Time,PPG,ECG
0,0.403921568627451,0.390625
0.008,0.407843137254902,0.390625
0.016,0.407843137254902,0.40625
0.024,0.407843137254902,0.40625
0.032,0.411764705882353,0.4375
0.04,0.411764705882353,0.421875
0.048,0.411764705882353,0.40625
0.056,0.415686274509804,0.40625
0.064,0.415686274509804,0.40625
0.072,0.415686274509804,0.40625
...
The following Python script extracts the PPG column from the CSV file and generates the Json files in the required Data Acquisition Format.
import numpy as np
import pandas as pd
import json
import hmac
import hashlib
import os
import time
import sys
HMAC_KEY = '<your hmac key>'
emptySignature = ''.join(['0'] * 64)
data = {
"protected": {
"ver": "v1",
"alg": "HS256",
"iat": None
},
"signature": emptySignature,
"payload": {
"device_name": "AF:55:A2:90:30:75",
"device_type": "ESP32",
"interval_ms": 8, # 125Hz
"sensors": [
{ "name": "ppg", "units": "n.u." },
],
"values": []
}
}
data_dir = sys.argv[1]
label = sys.argv[2]
for filename in os.listdir(data_dir):
if filename.endswith('.csv'):
data_file_path = os.path.join(data_dir, filename)
df = pd.read_csv(data_file_path)
df['PPG'] = df['PPG'].replace('NaN', np.nan)
if df['PPG'].isna().sum() > 0:
df['PPG'] = df['PPG'].interpolate()
print(f'Interpolate: {filename}')
ppg = df['PPG'].values
data["protected"]["iat"] = time.time()
values = np.expand_dims(ppg, axis=-1)
data["payload"]["values"] = values.tolist()
# encode in JSON
encoded = json.dumps(data)
# sign message
signature = hmac.new(bytes(HMAC_KEY, 'utf-8'), msg = encoded.encode('utf-8'), digestmod = hashlib.sha256).hexdigest()
# set the signature again in the message, and encode again
data['signature'] = signature
encoded = json.dumps(data, indent=4)
outfile = f'./data/{label}.{os.path.splitext(filename)[0]}.json'
print(outfile)
with open(outfile, 'w') as f:
f.write(encoded)
The Edge Impulse Data Acquisition format is an optimized format for time-series data. It allows cryptographic signing of the data when sampling is complete to prove the authenticity of the data. In this case, data is encoded using JSON and signed according to the JSON Web Signature specification. One of the generated JSON file sample is given below.
{
"protected": {
"ver": "v1",
"alg": "HS256",
"iat": 1751975220.5584269
},
"signature": "fdef5ac392e353e7fc9e01e9a90aa02dad75f2f83c005ca0ea5c63d1de5e7efb",
"payload": {
"device_name": "AF:55:A2:90:30:75",
"device_type": "ESP32",
"interval_ms": 8,
"sensors": [
{
"name": "ppg",
"units": "n.u."
}
],
"values": [
[
0.403921568627451
],
[
0.407843137254902
],
[
0.407843137254902
],
...(149,998 more data items)...
]
}
}
The data is uploaded to the Edge Impulse Studio using the Edge Impulse CLI that can be installed by following the instructions here. Execute the following command to upload the data to the Edge Impulse Studio. The datasets are automatically split into training and testing datasets.
$ edge-impulse-uploader --category split data/*.json
The uploaded datasets can be viewed at Data Acquisition page.
For the model developments, we need to design an impulse at the Impulse Design > Create Impulse page. For the Time Series data columns, we have chosen the Window size to 30, 000ms (30 seconds) and Window Increase size to 1000ms. The raw data were sampled at 125Hz but we have chosen 50Hz to downsample it that is enough to detect Atrial Fibrillation.
We have added the HR and HRV features processing block to process the PPG data to extract key metrics such as heart rate (HR) and heart rate variability (HRV). HR measures the number of beats per minute, while HRV measures the time variance between successive heartbeats, also known as the inter-beat interval (IBI).
The HR/HRV Signal Processing Block is available for evaluation, but an Enterprise plan is required for deployment on hardware for production or commercial purposes.
We haven't selected accelerometer options since the dataset lacks motion data. However, if we collect data ourselves, using an accelerometer to capture motion data would be beneficial, allowing the processing block to eliminate motion artifacts. And, finally we added a Classifier learning block the learns patterns from data, and can apply these to new data.
Navigate to the Impulse Design > HR/HRV page and configure the parameters as shown in the image below. Since accelerometer settings were not selected in the feature block, their parameters will be disregarded.
Then, click the Save Parameters button, which will redirect to a new page where you should click the Generate Feature button. Feature generation typically takes a few minutes to complete.
The HR/HRV feature generation block generates the features for both heart rate and HRV over the specified window size.
We can see the 2D visualization of the generated features in Feature Explorer.
To train the model, navigate to the Impulse Design > Classifier page. We chose the XGBoost Random Forest Classifier as the model architecture, a classical machine learning algorithm that combines the strengths of gradient boosting and random forest techniques for classification tasks. After several trials, we identified the optimal classifier settings, as shown below.
Click the Save & train button and wait for the training process to finish. The training output is displayed below. The unoptimized float32 model achieves 93.2% accuracy. The confusion matrix and aggregated metrics are shown below.
To evaluate the model’s performance on the test datasets, navigate to the Model testing page and click the Classify All button. The model shows slightly improved performance on unseen datasets with a 95.15% accuracy.
On the Deployment page, search for and select the HR Library (ESP32 Arduino). In the Model Optimizations section, choose the EON Compiler. Click the Build button to compile and download the Arduino library zip bundle.
Our objective was to develop an intuitive, portable medical device that requires no prior knowledge to operate effectively. We utilized the M5StickC Plus, incorporating its buzzer, buttons, and TFT display to enhance the user experience. The device can be powered on or off using the side button, and PPG data sampling is initiated by pressing the top button. Minimal on-screen messages guide the user, while the buzzer provides audible cues to indicate the start and end of the sampling process. PPG samples are visually displayed on the screen to confirm correct sampling, offering users a clear visual feedback mechanism. Additionally, a progress bar is shown during data collection to inform users of the duration they need to keep their finger on the sensor, ensuring a seamless and user-friendly experience.
The user interface is shown below.
To deploy the Edge Impulse precompiled HR Library (including SDK and the model) on the M5StickC Plus, follow these detailed steps for setting up the development environment and integrating the necessary libraries:
- Obtain the latest version of the Arduino Integrated Development Environment (IDE) from the official Arduino website (https://www.arduino.cc/en/software).
- After downloading the Arduino library zip bundle from the Edge Impulse Deployment page (as described previously), integrate it into the Arduino IDE. Open the Arduino IDE, navigate to the menu, and select Sketch > Include Library > Add.ZIP Library. Browse to the location of the downloaded Edge Impulse HR Library zip file and select it.
- This project uses the ESP32 Arduino core version 2.0.17, which is compatible with the Edge Impulse SDK and ensures successful compilation. To install the ESP32 core, go to Tools > Board > Boards Manager in the Arduino IDE. Search for “ESP32” and install version 2.0.17 of the ESP32 Arduino core by Espressif Systems.
- The MAX30102 Heart Rate Sensor requires the SparkFun MAX3010x Sensor Library. In the Arduino IDE, navigate to Tools > Manage Libraries (Library Manager). Search for “SparkFun MAX3010x” and install the library.
The full Arduino sketch, which can be compiled and uploaded to the M5StickC Plus, is provided below.
#include <M5StickCPlus.h>
#include <Wire.h>
#include <MAX30105.h>
#define EI_CLASSIFIER_HR_LIB 1
#include <arduino-hrv.h>
#define SCREEN_WIDTH 240
#define SCREEN_HEIGHT 135
#define GRAPH_HEIGHT 75 // Fits within SCREEN_HEIGHT (60 + 75 = 135)
#define GRAPH_WIDTH 240 // Full screen width
#define GRAPH_X 0 // No offset
#define GRAPH_Y 60 // Below text area
#define ADC_RANGE 4096
#define PROGRESS_BAR_HEIGHT 10 // Progress bar at top
MAX30105 particleSensor;
// Framebuffer for graph (240x75 pixels, 16-bit RGB565)
uint16_t framebuffer[GRAPH_WIDTH * GRAPH_HEIGHT]; // 36,000 bytes
// Rolling buffer settings for 30 seconds at 50 Hz
#define BUFFER_SIZE 1500 // 30 seconds * 50 Hz
uint32_t redBuffer[BUFFER_SIZE];
int bufferIndex = 0;
int samplesCollected = 0;
// Sampling settings
#define SAMPLE_RATE 50 // Effective rate (200 Hz / 4)
#define SAMPLE_INTERVAL (1000 / SAMPLE_RATE) // 20 ms
unsigned int display_update_interval = 100; // Update display every 100 ms (10 Hz)
// Track timing
unsigned long lastSampleTime = 0;
unsigned long lastDisplayUpdate = 0;
unsigned long lastTextUpdate = 0; // For progress bar
float features[BUFFER_SIZE];
bool doSampling = false;
long minRed = 100000; // Fixed initial min
long maxRed = 200000; // Fixed initial max
int raw_feature_get_data(size_t offset, size_t length, float *out_ptr) {
memcpy(out_ptr, features + offset, length * sizeof(float));
return 0;
}
void setup() {
M5.begin();
Serial.begin(115200);
pinMode(37, INPUT_PULLUP); // Button A for M5StickC Plus
M5.Lcd.setRotation(3); // Landscape orientation (240x135)
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setTextSize(3);
// Initialize I2C with delay
Wire.begin(0, 26); // SDA=0, SCL=26 for M5StickC Plus
delay(500); // Stabilize I2C bus
// Initialize MAX30102 with retry
int retryCount = 0;
while (!particleSensor.begin(Wire, I2C_SPEED_FAST) && retryCount < 3) {
Serial.println("MAX30102 not found, retrying...");
particleSensor.softReset(); // Reset MAX30102
delay(100); // Wait for reset
retryCount++;
}
if (retryCount >= 3) {
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setCursor(0, 0);
M5.Lcd.println("MAX30102 not found!");
Serial.println("MAX30102 failed to initialize!");
while (1);
}
// Configure MAX30102
byte ledBrightness = 0x1F; // Increased for stronger signal
byte sampleAverage = 4; // 4 samples per FIFO item
byte ledMode = 1; // Red only
int sampleRate = 200; // Effective sample rate = 50 Hz
int pulseWidth = 411;
int adcRange = ADC_RANGE; // 12-bit
particleSensor.setup(ledBrightness, sampleAverage, ledMode, sampleRate, pulseWidth, adcRange);
// Initialize buffers
for (int i = 0; i < BUFFER_SIZE; i++) {
redBuffer[i] = 0;
features[i] = 0.0;
}
for (int i = 0; i < GRAPH_WIDTH * GRAPH_HEIGHT; i++) {
framebuffer[i] = BLACK;
}
// Draw initial graph background and labels
M5.Lcd.fillRect(GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT, BLACK);
M5.Lcd.setCursor(10, 30);
M5.Lcd.setTextColor(YELLOW);
M5.Lcd.println("AFib Monitor");
M5.Lcd.setTextSize(2);
M5.Lcd.setTextColor(WHITE);
M5.Lcd.setCursor(20, 70);
M5.Lcd.println("Press the button");
M5.Lcd.setCursor(45, 90);
M5.Lcd.println("to continue");
M5.Beep.tone(4000, 500);
}
// Bresenham's line algorithm for framebuffer
void drawLineInFramebuffer(int x0, int y0, int x1, int y1, uint16_t color) {
int dx = abs(x1 - x0);
int dy = abs(y1 - y0);
int sx = x0 < x1 ? 1 : -1;
int sy = y0 < y1 ? 1 : -1;
int err = dx - dy;
while (true) {
if (x0 >= 0 && x0 < GRAPH_WIDTH && y0 >= 0 && y0 < GRAPH_HEIGHT) {
framebuffer[y0 * GRAPH_WIDTH + x0] = color;
}
if (x0 == x1 && y0 == y1) break;
int e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x0 += sx;
}
if (e2 < dx) {
err += dx;
y0 += sy;
}
}
}
void updateGraph() {
unsigned long start_ms = millis();
// Clear framebuffer
for (int i = 0; i < GRAPH_WIDTH * GRAPH_HEIGHT; i++) {
framebuffer[i] = BLACK;
}
// Skip graphing until 150 samples to avoid initial instability
if (samplesCollected < 150) return;
// Find min and max for displayed samples
uint32_t minVal = redBuffer[(bufferIndex - min(samplesCollected, GRAPH_WIDTH) + BUFFER_SIZE) % BUFFER_SIZE];
uint32_t maxVal = minVal;
for (int i = 0; i < min(samplesCollected, GRAPH_WIDTH); i++) {
int idx = (bufferIndex - min(samplesCollected, GRAPH_WIDTH) + i + BUFFER_SIZE) % BUFFER_SIZE;
uint32_t value = redBuffer[idx];
if (value < minVal) minVal = value;
if (value > maxVal) maxVal = value;
}
uint32_t range = maxVal - minVal;
uint32_t minRange = 300; // Fixed min range
if (range < minRange) range = minRange;
Serial.printf("min/max calc time %d ms\n", millis() - start_ms);
// Draw graph with connected lines in framebuffer
unsigned long draw_start_ms = millis();
for (int i = 0; i < min(samplesCollected, GRAPH_WIDTH - 1); i++) {
int idx1 = (bufferIndex - min(samplesCollected, GRAPH_WIDTH) + i + BUFFER_SIZE) % BUFFER_SIZE;
int idx2 = (bufferIndex - min(samplesCollected, GRAPH_WIDTH) + i + 1 + BUFFER_SIZE) % BUFFER_SIZE;
double scaled1 = static_cast<double>(redBuffer[idx1] - minVal) / range;
double scaled2 = static_cast<double>(redBuffer[idx2] - minVal) / range;
int y1 = GRAPH_HEIGHT - 1 - static_cast<int>(scaled1 * (GRAPH_HEIGHT - 1));
int y2 = GRAPH_HEIGHT - 1 - static_cast<int>(scaled2 * (GRAPH_HEIGHT - 1));
y1 = constrain(y1, 0, GRAPH_HEIGHT - 1);
y2 = constrain(y2, 0, GRAPH_HEIGHT - 1);
drawLineInFramebuffer(i, y1, i + 1, y2, RED);
}
Serial.printf("framebuffer draw time %d ms\n", millis() - draw_start_ms);
// Push framebuffer to display
unsigned long push_start_ms = millis();
M5.Lcd.setSwapBytes(true);
M5.Lcd.pushImage(GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT, framebuffer);
M5.Lcd.setSwapBytes(false);
Serial.printf("pushImage time %d ms\n", millis() - push_start_ms);
}
void inference() {
// Normalize input data
for (int i = 0; i < BUFFER_SIZE; i++) {
int idx = (bufferIndex - BUFFER_SIZE + i + BUFFER_SIZE) % BUFFER_SIZE;
if (maxRed > minRed) {
features[i] = (float)(redBuffer[idx] - minRed) / (maxRed - minRed);
} else {
features[i] = 0.0;
}
}
ei_impulse_result_t result = { 0 };
signal_t features_signal;
features_signal.total_length = sizeof(features) / sizeof(features[0]);
features_signal.get_data = &raw_feature_get_data;
EI_IMPULSE_ERROR res = run_classifier(&features_signal, &result, true);
if (res != EI_IMPULSE_OK) {
Serial.printf("ERR: Failed to run classifier (%d)\n", res);
return;
}
// Print inference results
Serial.printf("run_classifier returned: %d\r\n", res);
Serial.printf("Timing: DSP %d ms, inference %d ms\r\n", result.timing.dsp, result.timing.classification);
Serial.printf("Heart rate: %.2f BPM\n", result.hr_calcs.heart_rate);
int max_prob_idx = -1;
float max_prob = 0.0;
for (uint16_t i = 0; i < EI_CLASSIFIER_LABEL_COUNT; i++) {
Serial.printf(" %s: %.5f\r\n", ei_classifier_inferencing_categories[i], result.classification[i].value);
if (result.classification[i].value > max_prob) {
max_prob = result.classification[i].value;
max_prob_idx = i;
}
}
// Update LCD with inference result
M5.Lcd.fillRect(0, 0, SCREEN_WIDTH, 55, BLACK); // Clear text and progress bar area
M5.Lcd.setCursor(0, 10);
M5.Lcd.setTextColor(WHITE, BLACK);
Serial.printf("max_prob_idx = %d, max_prob = %0.2f\n", max_prob_idx, max_prob);
if (max_prob_idx == 0 && max_prob >= 0.85f) {
M5.Lcd.setTextColor(ORANGE, BLACK);
M5.Lcd.printf("AF:%.3f", max_prob);
} else if (max_prob_idx == 1 && max_prob >= 0.85f) {
M5.Lcd.setTextColor(GREEN, BLACK);
M5.Lcd.printf("Non-AF:%.3f", max_prob);
} else {
M5.Lcd.setTextColor(WHITE, BLACK);
M5.Lcd.println("Uncertain");
}
}
void loop() {
M5.update();
if (M5.BtnA.wasReleased()) {
doSampling = !doSampling;
if (doSampling) {
particleSensor.clearFIFO();
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setTextSize(2);
M5.Lcd.setTextColor(WHITE, BLACK);
M5.Lcd.setCursor(0, 10);
M5.Lcd.println("Place your index");
M5.Lcd.setCursor(0, 30);
M5.Lcd.println("finger on the sensor");
M5.Lcd.setCursor(0, 50);
M5.Lcd.println("and hold steady");
M5.Lcd.setCursor(0, 70);
M5.Lcd.println("for 30 seconds");
// Discard initial samples for 5 seconds
unsigned long warm_up_start = millis();
while (millis() - warm_up_start < 5000) {
particleSensor.check();
if (particleSensor.available()) {
particleSensor.getRed();
}
delay(5); // Check every 5 ms
}
bufferIndex = 0;
samplesCollected = 0;
minRed = 100000; // Fixed initial min
maxRed = 200000; // Fixed initial max
display_update_interval = 1000; // Initial delay for graph
M5.Lcd.fillScreen(BLACK);
// Draw progress bar border
M5.Lcd.drawRect(0, 0, SCREEN_WIDTH, PROGRESS_BAR_HEIGHT, WHITE);
particleSensor.setPulseAmplitudeRed(0x1F);
Serial.println("Sampling Started");
M5.Beep.tone(4000, 500);
} else {
particleSensor.setPulseAmplitudeRed(0x00);
Serial.println("Sampling Stopped");
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setCursor(10, 30);
M5.Lcd.setTextSize(3);
M5.Lcd.setTextColor(YELLOW);
M5.Lcd.println("AFib Monitor");
M5.Lcd.setTextSize(2);
M5.Lcd.setTextColor(WHITE);
M5.Lcd.setCursor(20, 70);
M5.Lcd.println("Press the button");
M5.Lcd.setCursor(45, 90);
M5.Lcd.println("to continue");
}
delay(100);
}
if (doSampling) {
unsigned long currentTime = millis();
// Sample at defined interval
if (currentTime - lastSampleTime >= SAMPLE_INTERVAL) {
particleSensor.check();
if (particleSensor.available()) {
uint32_t redValue = particleSensor.getRed();
// Optional finger detection (commented)
// bool isValid = (redValue >= 50000 && redValue <= 250000);
// if (isValid) {
// Diagnostic: Log sample timing and value
if (samplesCollected < 150) {
Serial.printf("Sample %d time delta: %d ms, value: %u\n",
samplesCollected, currentTime - lastSampleTime, redValue);
}
// Update min and max only after 150 samples
if (samplesCollected >= 150) {
if (redValue < minRed) minRed = redValue;
if (redValue > maxRed) maxRed = redValue;
}
redBuffer[bufferIndex] = redValue;
bufferIndex = (bufferIndex + 1) % BUFFER_SIZE;
samplesCollected = min(samplesCollected + 1, BUFFER_SIZE);
// } // End isValid
// Check if 30 seconds of data is collected
if (samplesCollected >= BUFFER_SIZE) {
inference();
M5.Beep.tone(4000, 500);
doSampling = false;
particleSensor.setPulseAmplitudeRed(0x00);
//samplesCollected = 0;
}
lastSampleTime = currentTime;
}
}
// Update display
if (currentTime - lastDisplayUpdate >= display_update_interval) {
M5.Lcd.setTextColor(WHITE, BLACK);
// Update progress bar every 1 second
if (currentTime - lastTextUpdate >= 1000) {
// Update progress bar
int progress_width = (int)(samplesCollected * SCREEN_WIDTH / BUFFER_SIZE);
M5.Lcd.fillRect(0, 0, SCREEN_WIDTH, PROGRESS_BAR_HEIGHT, BLACK); // Clear bar area
M5.Lcd.fillRect(0, 0, progress_width, PROGRESS_BAR_HEIGHT, WHITE); // Draw progress
M5.Lcd.drawRect(0, 0, SCREEN_WIDTH, PROGRESS_BAR_HEIGHT, WHITE); // Redraw border
lastTextUpdate = currentTime;
}
if (samplesCollected < 150) {
M5.Lcd.setTextSize(3);
M5.Lcd.setTextColor(WHITE, BLACK);
M5.Lcd.setCursor(0, 57); // Approximate center (240x135)
M5.Lcd.println("Sampling PPG");
} else {
// Clear center message when graphing starts
M5.Lcd.fillRect(0, 30, SCREEN_WIDTH, SCREEN_HEIGHT - 30, BLACK);
unsigned long start_ms = millis();
updateGraph();
Serial.printf("display update time %d ms\n", millis() - start_ms);
M5.Lcd.setTextSize(3); // Restore for other text
}
lastDisplayUpdate = currentTime;
display_update_interval = 100; // Subsequent updates at 100 ms
}
}
}
Live DemoWe evaluated the device on several individuals without a history of AFib. We plan to test it on someone with AFib soon and will provide updates here.
ConclusionThis project created a test version of a device that uses machine learning to detect AFib (a heart condition) with PPG signals. Using Edge Impulse, the device analyzes heart signals in real time with high accuracy, while being energy-efficient and portable. By processing data directly on the device, it works well in low-resource settings. This could be useful for remote healthcare, wearable devices, and preventing heart issues, providing an affordable way to catch AFib early. Future improvements could include making the model more accurate, testing it in more clinical settings, and connecting it to larger tele-health systems to better manage heart health.
DisclaimerThis ML-based device is not intended for medical use. It is designed for general informational, educational, or research purposes only and should not be used for diagnosing, treating, curing, or preventing any medical condition or disease.
Comments