Echtzeit-Herzschlag-LED-Visualisierung per EKG
Komponenten und Zubehör
| Komponente | Menge | Hinweis |
|---|---|---|
| uECG-Gerät | 1 | Tragbares EKG, Open Source, Arduino-kompatibler Digitalausgang |
| Arduino Nano R3 | 1 | Steuerung der LED-Ring |
| Adafruit NeoPixel Ring: WS2812 5050 RGB LED (16 LEDs) | 1 | Visualisierung |
| Li-Ion Akku 1000 mAh | 1 | Stromversorgung (direkt an 5V, nicht Vin!) |
Benötigte Werkzeuge
- Lötkolben (für tragbare Version verlötete Verbindungen empfohlen)
Projektbeschreibung
Seit Jahren wollte ich LEDs bauen, die im Rhythmus meines Herzschlags blinken – nicht nur im Stillstand, sondern auch bei Bewegung. Das war überraschend schwierig, ich scheiterte jahrelang. Nicht mehr!
Die ganze Signalverarbeitung übernimmt das uECG – ein kleines, tragbares, Open-Source-EKG-Gerät mit einem Arduino-freundlichen Digitalausgang (Pin geht bei jedem Herzschlag High/Low). Die Auswertung dieser digitalen Zustände ist weit einfacher als die Roh-EKG-Signalverarbeitung.
Update: Zweite Iteration des Projekts nutzt Funkübertragung statt Kabel.
1. Schaltplan
Da nur digitale Signale verarbeitet werden, ist der Aufbau simpel. Für den tragbaren Einsatz sind verlötete Verbindungen zuverlässiger (und kompakter). Für schnelle Tests geht auch Steckbrett.
- DI-Pin des LED-Rings → Arduino D11 (im Code konfigurierbar)
- DRV-Pin des uECG → Arduino D3 (im Code konfigurierbar)
- Akku + → Arduino 5V & LED-Ring 5V
- Akku – → Arduino GND, Ring GND, uECG GND
Wichtig: LiPo-Akku direkt an 5V anschließen – nicht an Vin! Der Spannungsregler am Vin-Pin verursacht Spannungsabfall, den wir uns nicht leisten können. Arduino ist stabil bis ~3,4 V. Voll geladener LiPo: 4,2 V, 3,4 V erst bei <15 % Restladung. Mit >200 mAh Akku gute Laufzeit. Akku per Stecker anschließen (zum Laden abnehmbar).
2. Code
Das Programm liest kontinuierlich Pin D3. Bei Flankenwechsel wird der Zeitpunkt in ein 20-Elemente-Array geschoben. Differenz zwischen erstem und letztem Element geteilt durch 20 = mittlere Zeit pro Schlag (ms). 60.000 ms / dieser Wert = BPM.
Array-Größe anpassbar: Weniger Elemente = schnellere Reaktion, aber instabiler. Mehr Elemente = stabiler, aber träger bei schnellen BPM-Änderungen.
BPM wird auf Farbe gemappt (blau → grün → gelb → rosa → rot bei steigendem Puls) und auf Anzahl leuchtender LEDs (80 BPM = 8 Segmente, 110 BPM = 11, Skalierung einstellbar).
#include <Adafruit_NeoPixel.h>
#ifdef __AVR__
#include <avr/power.h>
#endif
#define PIN 11 // DI-Pin LED-Ring
#define NUMPIXELS 16 // Anzahl LEDs im Ring
int in_pin = 3; // uECG Eingangspin
Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);
#define BEAT_HIST 20 // Anzahl Herzschläge für Mittelwert
long beats[BEAT_HIST];
void push_beat(long ms) {
for(int x=0; x<BEAT_HIST-1; x++) beats[x] = beats[x+1];
beats[BEAT_HIST-1] = ms;
}
int get_bpm() {
long dt = beats[BEAT_HIST-1] - beats[0];
return BEAT_HIST * 60000 / dt;
}
long last_pix_upd = 0;
int prev_in_state = 0;
void setup() {
pixels.begin();
pinMode(in_pin, INPUT);
digitalWrite(in_pin, 1); // Pull-Up aktivieren – KRITISCH, uECG hat keinen internen Pull-Up
}
void loop() {
long ms = millis();
int in_state = digitalRead(in_pin); // 1 = kein Schlag, 0 = Schlag
if (in_state == 1 && prev_in_state == 0) { // Flanke High→Low = Schlag
push_beat(ms);
}
prev_in_state = in_state;
if (ms - last_pix_upd > 10) { // LEDs nicht zu oft aktualisieren
last_pix_upd = ms;
int bpm = get_bpm();
int max_bright = 120; // Max-Helligkeit (max 255)
float dd = 20; // BPM-Schritte zwischen Farbtonwechseln
float t1 = 90, t2 = t1+dd, t3 = t2+dd, t4 = t3+dd;
int r, g, b;
// Farbmapping
if (bpm < t1) { r=0; g=0; b=max_bright; }
else if (bpm < t2) { r=0; g=max_bright*(bpm-t1)/dd; b=max_bright-g; }
else if (bpm < t3) { r=max_bright*(bpm-t2)/dd; g=max_bright-r; b=r/4; }
else if (bpm < t4) { r=max_bright; g=0; b=max_bright/2 - max_bright*(bpm-t3)/(2*dd); }
else { r=max_bright; g=0; b=0; }
// Zwischen Schlägen auf 1/4 Helligkeit dimmen (nur Schläge hervorgehoben)
if (in_state) { r*=0.25; g*=0.25; b*=0.25; }
int on_pixels = (bpm + 5) / 10; // LEDs an: 60 BPM→6, 120 BPM→12
for (int i=0; i<NUMPIXELS; i++) {
if (i < on_pixels) pixels.setPixelColor(i, pixels.Color(r,g,b));
else pixels.setPixelColor(i, pixels.Color(0,0,0));
}
pixels.show();
}
}
3. Aufbau als Wearable
Arduino passt fast perfekt in den LED-Ring. Akku daneben. Wichtig: uECG wird auf der Brust getragen – Kabel mit Steckern nötig! Erst uECG anbringen, dann Shirt mit Elektronik anziehen, dann Stecker verbinden. Sonst sehr fummelig.
Alles richtig verkabelt → nach ~30 Sek. startet Blinken mit BPM-Anzeige.
4. Praxistest
Getestet beim Gehen und Laufen. Problem beim Laufen: Akku hüpft auf EKG-Sensor → verfälscht Messung. Kabel zu kurz → zieht bei jedem Schritt am Sensor. Zuverlässige Erkennung nur beim Gehen/Stehen, nicht Laufen. Sensor selbst (mit anderem Shirt) zeigte beim Laufen korrekte BPM (per App geprüft).
LEDs auf der Brust sehen cool aus, sind aber unpraktisch – man schaut nicht auf die Brust für Puls. Nächste Iteration: Armband zur Anzeige.
Mehr zu uECG: Hackaday-Projektseite (Technikdetails, PCB-Designs, Logs).
Code
uECG_pixel_ring.ino
uECG_pixel_ring.ino (vollständig)
#include <Adafruit_NeoPixel.h>
#ifdef __AVR__
#include <avr/power.h>
#endif
#define PIN 11
#define NUMPIXELS 16
int in_pin = 3;
Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);
void setup() {
pixels.begin();
pinMode(in_pin, INPUT);
digitalWrite(in_pin, 1); // Pull-Up kritisch!
}
#define BEAT_HIST 20
long beats[BEAT_HIST];
void push_beat(long ms) {
for(int x=0; x<BEAT_HIST-1; x++) beats[x] = beats[x+1];
beats[BEAT_HIST-1] = ms;
}
int get_bpm() {
long dt = beats[BEAT_HIST-1] - beats[0];
return BEAT_HIST * 60000 / dt;
}
long last_pix_upd = 0;
int prev_in_state = 0;
void loop() {
long ms = millis();
int in_state = digitalRead(in_pin);
if (in_state == 1 && prev_in_state == 0) push_beat(ms);
prev_in_state = in_state;
if (ms - last_pix_upd > 10) {
int r,g,b; last_pix_upd = ms;
int bpm = get_bpm();
int max_bright = 120;
float dd = 20;
float t1=90, t2=t1+dd, t3=t2+dd, t4=t3+dd;
if (bpm < t1) { r=0; g=0; b=max_bright; }
else if (bpm < t2) { r=0; g=max_bright*(bpm-t1)/dd; b=max_bright-g; }
else if (bpm < t3) { r=max_bright*(bpm-t2)/dd; g=max_bright-r; b=r/4; }
else if (bpm < t4) { r=max_bright; g=0; b=max_bright/2 - max_bright*(bpm-t3)/(2*dd); }
else { r=max_bright; g=0; b=0; }
if (in_state) { r*=0.25; g*=0.25; b*=0.25; }
int on_pixels = (bpm+5)/10;
for(int i=0;i<NUMPIXELS;i++) {
if(i<on_pixels) pixels.setPixelColor(i, pixels.Color(r,g,b));
else pixels.setPixelColor(i, pixels.Color(0,0,0));
}
pixels.show();
}
}
Schaltplan
Herstellungsprozess
- Externer Defibrillator: Geschichte, Design und Fertigung
- Das Nudelholz: Von etruskischen Ursprüngen bis zur modernen Handwerkskunst
- Künstliches Herz: Technologie, Geschichte und Zukunftsperspektiven
- Künstliche Herzklappen: Typen, Geschichte und zukünftige Innovationen
- LED mit PIR-Bewegungsmelder am Raspberry Pi steuern
- Heimtemperatur mit Raspberry Pi Dashboard überwachen
- Fern-Herzfrequenz-Überwachung mit AWS IoT Alarmen
- Kontaktlose Herzschlagüberwachung via Smart Speaker: Ein revolutionäres System
- Graphen-basiertes elektronisches Tattoo ermöglicht kontinuierliche, präzise Herzüberwachung über lange Zeiträume
- Gelenkwelle / Knickgelenk (Knuckle Joint): Aufbau, Anwendungen & Eigenschaften