Zum Inhalt

Lerneinheit Pulsoxymeter

Wir möchten ein Pulsoxymeter an der Platine anschließen, mit welchem am Ende der Blutsauerstoff, der Puls und die Herzfrequenzvariabilität gemessen werden kann.

Aufgabe

Schließe zuerst die Platine am Computer und den Fingerclip an der Platine an. Öffne danach mithilfe des Mu Editors die sensor.py Datei auf deinem Raspberry, indem du auf Laden klickst und die Datei auswählst. Wähle den Modus RP2040 aus.

Jetzt hast du den Quelltext (der Code für das Programm) geöffnet. Damit du den Puls messen kannst, müssen noch ein paar Lücken ausgefüllt werden. Der Quelltext ist im folgenden in kleine Einheiten eingeteilt. Zu jeder Einheit gibt es einen Text, welcher den Quellcode erklärt. Manchmal ist im Quellcode ein rotes Fragezeichen eingebaut, dieses soll mit deiner Antwort auf die Frage ersetzt werden. Wenn du mal nicht weiter weist kannst du dir einen Tipp anschauen. Außerdem gibt es Abschnitte, welche für Experten sind und mit einer Glühbirne gekennzeichnet sind. Diese kannst du dir bei Interesse anschauen. Viel Spaß!

Funktion des Pulsoxymeters

Prinzip der reflektiven Oxymetrie.

Damit du dir vorstellen kannst, wie ein Pulsoxymeter funktioniert, erstmal eine kurze Erklärung dazu. Schaue dir dazu die Abbildung an. Ein Pulsoxymeter hat eine rote und infrarote LED. Es wird also rotes und infrarotes Licht ausgesendet. Infrarotes Licht ist für das menschliche Auge nicht sichtbar, das rote Licht kannst du sehen. Die beiden LEDs werden abwechselnd ganz schnell an und ausgeschaltet. Dabei reflektiert ein Teil des Lichts in deinem Finger. Genau dieses Licht wird mit einem Photodetektor gemessen. Ein Photodetekor wandelt das ankommende Licht in ein elektrisches Signal um. Das Signal verändert sich durch den Pulsschlag in deinem Finger. Das infrarote und rote Licht wird für die Berechnung der Blutsauerstoffsättigung benötigt. Dort machen wir uns zunutze, dass sich das Licht bei den roten Blutkörperchen mit Sauerstoff anders als ohne verhält.

Der Raspberry Pi Pico

Vielleicht kennst du Computer als Geräte, auf denen du YouTube Videos schauen kannst, Spiele spielen kannst oder mit anderen Menschen kommunizeren kannst. Die allermeisten Computer sind aber ein wenig kleiner. Es handelt sich dabei um Mikro-Computer (Oder auch Mikro-Controller). Diese verleihen all den Maschinen, die wir im Alltag benutzen ihre Intelligenz. Dazu zählen beispielsweise Waschmaschinen, Kühlschränke und sogar die Kaffemaschine! Im Grunde genommen sind Mikro-Controller überall verbaut, allein in einem modernen Auto stecken Hunderte drin.
Üblicherweise sind diese Mikro-Controller versteckt und nicht vom Anwender programmierbar. Der Raspberry-Pi Pico hingegen lässt sich leicht über eine USB-Schnittstelle von einem Computer programmieren. Auch verfügt der Pico über jede Menge Anschlüsse, mit denen man mit der Welt kommunizieren kann.

CircuitPython

CircuitPython ist eine auf Python aufbauende Programmiersprache, die man nutzen kann, um den Raspberry Pi Pico zu programmieren. Micro-Python unterscheided sich von Python hauptsächlich darin, dass es über verschiedene Bibliotheken verfügt, mit deren Hilfe sich die elektronische Hardware des Raspberry Pi-Picos ansteuern lässt.

Importieren der Module und Bibliotheken

Zuerst werden die für das Pulsoxymeter benötigten Module und Bibliotheken importiert. Bibliotheken und Module gestalten das verfasste Programm übersichtlicher. Mit diesen können Funktionen und Klassen direkt aufgerufen werden, welche bestimmte Aufgaben in einem Programm erfüllen. Ansonsten würde das Programm sehr lange und unübersichtlich werden, wenn in diesem alle Funktionen in einem Skript definiert werden müssten.

Experte

Was ist der Unterschied zwischen Bibliotheken und Modulen? Module sind eher kleinere Funktionseinheiten im Gegensatz zu Bibliotheken. Dabei sind in einem Modul vorallem Funktionen enthalten, welche einem bestimmten Zweck dienen, während in Bibliotheken eine Sammlung von Klassen und Funktionen vorhanden sein können, um eine ganze Aufgabe zu lösen. Eine Bibliothek ist also meistens umfangreicher, während Module kleinere Funktionen enthalten 1.

Zunächst werden die Module importiert, welche für CircuitPython entwickelt wurden. Das Modul displayio stellt eine Verbindung zum Display her und wird verwendet, um Inhalte auf dem Display anzuzeigen und busio stellt eine Verbindung mit dem SPI für das Display her. SPI bedeutet Serial Peripheral Interface und ist ein Bus-System. Dies wird später genauer erklärt. Mithilfe des board Moduls kann auf die Pins des Board zugegriffen werden. Das time Modul wird benötigt um später die Zeit zu messen, wann ein Herzschlag stattfindet. Durch terminalio kann ein Text auf einem Display angezeigt werden. Aus dem math Modul wird sqrt, also die Wurzel für die Herzfrequenzvariabilitätsmessung benötigt 2.

# Circuitpython Module
import displayio
import busio
import board
import time
import terminalio
from math import sqrt

Als nächstes werden noch ein paar weitere Bibliotheken importiert. Adafruit_ticks wird für die Berechnung der zeitlichen Differenz von zwei Herzschlägen benötigt und für einen timer in Millisekunden. Die Bibliothek adafruit_st7789 wird für das Display gebraucht. Die adafruit_display_text Bibliothek wird für die Anzeige eines Textes auf dem Display benötigt. Damit später das Signal des Pulses auf dem Bildschirm angezeigt wird, wird adafruit_display_shapes.sparkline verwendet 34.

# Circuitpython Bibliotheken
from adafruit_ticks import ticks_diff, ticks_ms
from adafruit_st7789 import ST7789
from adafruit_display_text import label
from adafruit_display_shapes.sparkline import Sparkline

Des Weiteren wird die Bibliothek MAX30103 verwendet, damit direkt auf den Sensor des Pulsoxymeters zugegriffen werden kann. Dadurch können direkt die Werte des Signals, also die Werte des roten und infraroten Lichts ausgegeben werden.

# Bibliothek des Sensors MAX30102
from pcb.MAX30102 import MAX30102, MAX30105_PULSE_AMP_MEDIUM, FIR_FILTER_COEFFS

Die Bibliothek heartrate ist für die Erkennung eines Herzschlages zuständig. Damit die gemessenen reflektierten Werte des infraroten und roten Lichts gespeichert werden, wird die CircularBuffer Bibliothek verwendet. Diese erstellt eine Liste mit einer bestimmten Länge. In einer Liste werden die Werte gespeichert. Wenn die Liste die maximale Länge erreicht hat, wird das älteste Element gelöscht und das Neue hinzugefügt. Aus der Utilities Bibliothek wird die Funktion remove_dc, welche den Gleichanteil eines Signals entfernt verwendet und der fir_lowpass glättet das Signal. Das wird später nochmal genauer erklärt 5.

# Bibliothek für die Erkennung eines Herzschlags
from lib.heartrate import HeartRate
# Bibliothek zum speichern von Werten in einer Liste
import lib.CircularBuffer as CircularBuffer
# Bibliothek zum entfernen des Gleichanteils im Signal und ein Tiefpassfilter
from lib.Utilities import remove_dc, fir_lowpass

Die Länge der Liste wird hier auf 100 festgelegt.

# Die Länge der Liste
CIRCULAR_BUFFER_SIZE=100

Initialisierung des Pulsoxymeters

Zuerst wird die Klasse Pulsoxymeter erstellt, in welcher einige Methoden definiert werden. Methoden erfüllen bestimmte Aufgaben (Funktionen) in einem Programm, welche das Verhalten von Objekten beschreiben 6. Damit du dir in etwa vorstellen kannst, was überhaupt Klassen und Objekte sind, gibt es eine kurze Erklärung dazu.

Was sind Klassen und Objekte?

Klassen und Objekte werden verwendet, um ein Programm übersichtlicher zu gestalten. Eine Klasse repräsentiert alle Objekte einer Art und definiert diese mithilfe von Parametern und Methoden. Ein Beispiel für eine Klasse kann zum Beispiel eine Person sein. Objekte dazu wären bestimmte Personen, wobei jede Person Eigenschaften besitzt. Diese Variablen, wie zum Beispiel der Name und das Gewicht der Person werden Instanzvariablen genannt 7.

In der Klasse Pulsoxymeter, werden als erstes einige Variablen und Startwerte initialisiert. Das self wird in Funktionen innerhalb von Klassen benötigt. Dadurch kann auf Methoden und Instanzvariablen der Klasse zugegriffen werden 8. Zuerst wird ein Objekt der Klasse MAX30102 erzeugt, mit der Bezeichnung sensor. Dieses Objekt stellt eine Verbindung mit der I2C Schnittstelle des Sensors MAX30102 her.

Experte

Was ist eine I2C Schnittstelle? Dies ist eine Abkürzung für Inter-Integrated Circuits. Es ist ein Datenbus, mit welchem die Daten unseres Sensors für die Blutsauerstoffsättigung und den Puls übertragen werden. Dabei basiert dieser auf zwei Leitungen. Der SCL und SDA Leitung. Die SCL (serial clock) Leitung ist für das Taktsignal verantwortlich. SDA (serial data) steht für die serielle Datenübertragung. Die Datenübertragung funktioniert nach einem Master Slave Prinzip. Dabei ist in unserem Versuch der Mikrocontroller der Master und der Sensor MAX30102 der Slave. Wenn Daten von dem Sensor verlangt werden teilt dies der Master dem Slave durch eine Startsequenz mit. Wie in der Abbildung zu sehen gibt es einen Low und einen High Zustand, dies entspricht einer 1 oder 0. Zu Beginn, wenn noch keine Daten verlangt wurden, sind SDA und SCL beide auf High.

Die I2C Kommunikation
Die I2C Kommunikation

Bei der Startsequenz wird SDA auf Low gesetzt. Danach sendet der Master die Adresse des Slaves, damit dieser mit dem richtigen Sensor kommuniziert. Danach wird durch ein Bit festgelegt, in welche Richtung die Daten übertragen werden sollen. Daraufhin werden die Daten auf die SDA Leitung gelegt, während der SCL den Takt vorgibt. Dabei handelt es sich um Datenpakete von 8 Bit. Es werden solange weitere Datenpakete versendet, bis eine stopp Bedingung erfolgt. Dabei sind SCL und SDA beide wieder auf High gesetzt 9.

Mit der Methode setup_sensor() werden dem Sensor bestimmte Werte für Parameter übergeben. Dies ist sozusagen die Grundeinstellung des Sensor. Die Werte der Parameter sind in der MAX30102 Bibliothek festgelegt.

Einstellungen des Sensors MAX30102

Ein kleiner Einblick, was alles an dem Sensor in der MAX30102 eingestellt wird. Die LED_Power ist die Helligkeit der LED. Diese ist auf mittel eingestellt, da dort die besten Ergebnisse erzielt wurden. Da zum Beispiel bei einer geringen Helligkeit zu wenig Licht am Photodetektor gelangen könnte und somit zu wenige Daten gesammelt werden. Die SAMPLE_RATE wird auf 1600 eingestellt. Das bedeutet, dass in einer Sekunde 1600 mal die LEDs an- und ausgeschaltet werden. Je höher die Abtastrate ist, desto mehr Daten können gesammelt werden und umso genauer ist der Signalverlauf am Ende. Dabei sollte auch beachtet werden, dass dies mehr Rechenleistung benötigt. Das SAMPLE_AVG ist auf 4 eingestellt 10. Dazu müssen wir uns erstmal anschauen was ein FIFO ist.

Was ist ein FIFO?

FIFO (First In First Out) ist eine Datenstruktur, bei welcher Daten gespeichert werden. Dabei ist zu beachten, dass die Daten, die zuerst hinzugefügt wurden, zuerst gelöscht werden. Dies siehst du in der Abbildung. Man kann sich dies wie ein Speicher vorstellen, welcher solange mit Daten gefüllt wird bis dieser voll ist. Daraufhin wird immer das älteste Element gelöscht, wenn ein Neues hinzugefügt wird 11.

Prinzip von der FIFO Datenstruktur.
Prinzip von der FIFO Datenstruktur.

Das SAMPLE_AVG bedeutet sample averaging. Es werden immer vier aufeinanderfolgende Werte gemittelt. Da es immer kleine Abweichungen bei Messungen gibt, soll diese Mittelung zu große Messabweichungen verringern. Als nächstes werden noch zwei Listen erstellt, in welcher die Werte von dem roten und infraroten AC-Signal gespeichert werden. Mit red_w und ir_w werden die beiden Parameter initialisiert, welche später den vorherigen Wert des roten und infraroten Signlas speichern. Dieser wird für die Berechnung des AC- und DC-Wertes benötigt. Was das überhaupt ist erfährst du im nächsten Abschnitt. Als letztes wird ein Objekt der Klasse HeartRate() erstellt, welches später für die Detektion der Herzschläge verwendet wird.

# Es wird eine Klasse Pulsoxymeter erstellt die einige Funktionen definiert
class Pulsoxymeter:
    # Variablen werden initialisiert
    def __init__(self):
        # Verbindet den Sensor mit der I2C Schnittstelle
        self.sensor = MAX30102()

        # Sensor konfigurieren
        self.sensor.setup_sensor(LED_POWER=MAX30105_PULSE_AMP_MEDIUM)
        # Stellt die Abtastrate auf 1600 ein
        self.sensor.set_sample_rate(1600)

        # Im Fifo werden jeweils 4 Werte gemittelt
        self.sensor.set_fifo_average(4)

        # Initialisiert 2 Listen mit einer Länge von 100
        self.red_buffer = CircularBuffer.CircularBuffer(CIRCULAR_BUFFER_SIZE)
        self.ir_buffer = CircularBuffer.CircularBuffer(CIRCULAR_BUFFER_SIZE)

        # Initialisiert die beiden Parameter, diese speichern den vorherigen roten/infraroten Wert, wird für die Berechnung des DC/AC Wertes benötigt
        self.red_w = 0
        self.ir_w = 0

        # Ruft die Klasse HeartRate auf, welche die Peaks eines Signals detektiert
        self.heart_rate = HeartRate()

AC- und DC-Wert

Was bedeutet AC und DC?

Wie zu Beginn beschrieben, wird ein Teil des Lichts am Finger reflektiert. In dem Finger sind Arterien vorhanden, die sich bei einem Pulsschlag weiten und danach wieder verengen. Dadurch ist die Stärke der Reflektion an den Arterien immer unterschiedlich. Ein Teil des Lichts wird aber auch schon zum Beispiel am Gewebe, an den Knochen und den Muskeln reflektiert. Dieser Teil bleibt während den Pulsschlägen immer konstant, da diese sich nicht mit dem Herzschlag ändern. Das bedeutet, wir haben einen reflektierten Teil der sich während der Messung ändert und einen Teil der konstant bleibt 12. In der Elektrotechnik wird ein Strom der sich nicht ändert, also konstant bleibt, mit Direct Current (DC) bezeichnet. Ein Strom der sich während der Zeit ändert wird Alternating Current (AC) bezeichnet.

Da der Photodetektor das gesamte Signal misst, müssen wir das Signal in einen AC- und DC- Anteil aufteilen. Dazu wird die Methode red_value und ir_value definiert. Zuerst wird das sensor Objekt aufgerufen und mit get_red() wird die Methode aufgerufen, welche einen Wert des roten Lichts zurück gibt. Dadurch wird der Wert auf unserer Variable red gespeichert. Der Wert entspricht in diesem Fall dem reflektierten roten Licht an unserem Finger, welches der Photodetektor misst. Der nächste Schritt ist, dass auf der Variablen red der Wert in ein AC- und DC- Anteil aufgeteilt wird. Dazu verwenden wir die Funktion remove_dc(). Als nächstes soll der DC-Wert berechnet werden.

Aufgabe

Wie kann der DC-Wert berechnet werden? Schreibe in die red_value() und ir_value() Methode bei red_dc und ir_dc anstelle des Fragezeichens die passenden Variablen. Dabei benötigst du nur Variablen die innerhalb dieser Methode erstellt wurden. Schaue dir dazu die Abbildung an.

Damit die Methode einen Wert zurück gibt, wird als letztes der return Befehl gegeben. Diese gibt den AC- und den DC-Wert aus.

    # Diese Methode gibt den Wert des roten Lichts als AC- und DC-Wert zurück
    def red_value(self):
        red = self.sensor.get_red()
        red_ac, self.red_w = remove_dc(red, self.red_w, alpha=0.95)
        red_dc = ? - ?
        return red_ac, red_dc

    # Diese Methode gibt den Wert des infraroten Lichts als AC- und DC-Wert zurück
    def ir_value(self):
        ir = self.sensor.get_ir()
        ir_ac, self.ir_w = remove_dc(ir, self.ir_w, alpha=0.95)
        ir_dc = ? - ?
        return ir_ac, ir_dc

Image title
AC- und DC-Anteil eines gemessenen Signals mit dem Pulsoxymeter.

1. Tipp

Du musst den Wert des gesamten Signals mit dem AC-Anteil subtrahieren.

2. Tipp

Verwende die Variable red und red_ac bzw. ir und ir_ac.

Initialisierung der Startsequenz

Bevor irgendwelche Werte für die Blutsauerstoffsättigung und den Puls berechnet werden, sollen erstmal 100 Werte des roten und infraroten Lichts gesammelt werden. Dadurch können direkt durchschnittliche Werte berechnet werden, mit einer guten Näherung des aktuellen Pulses und der Blutsauerstoffsättigung. Wenn erst wenige Werte vorhanden sind, wären dies sehr ungenaue Werte, da es immer Messabweichungen gibt. In der Methode setup wird als erstes eine Variable i erstellt, die als ein Zähler fungiert. Die while-Schleife läuft also solange bis i so groß wie die Länge der red_buffer und ir_buffer Listen ist. Innerhalb der Schleife werden die zuvor definierten Methoden aufgerufen. Dadurch erhalten wir den aktuellen Wert von dem roten und infraroten Licht. Der AC-Wert des von dem Photodetektor gemessenen Lichts wird in der jeweils dazugehörigen Liste hinzugefügt. Danach wird der Wert des Zählers i um eins erhöht. Dieses Vorgehen wird solange wiederholt bis die Bedingung der while-Schleife nicht mehr erfüllt ist. Dann gibt die Methode mit dem print Befehl aus, dass alle Werte in der Liste gespeichert sind.

    # In dieser Methode werden die ersten 100 AC-Werte des roten/infraroten Lichts gespeichert
    def setup(self):
        print('Initialsierung der Startsequenz:')
        i = 0
        # Die while Schleife wird bis zum erreichen der Listenlänge wiederholt
        while i < CIRCULAR_BUFFER_SIZE:
            red_ac, red_dc = self.red_value()
            ir_ac, ir_dc = self.ir_value()
            # Der AC-Wert wird der Liste hinzugefügt
            self.red_buffer.append(red_ac)
            self.ir_buffer.append(ir_ac)
            i += 1
        print('Die ersten {} Werte sind in der Liste gespeichert'.format(CIRCULAR_BUFFER_SIZE))

Initialisierung der Variablen und des Displays

Als erstes werden einige Variablen initialisiert, welche in dieser Methode benötigt werden. Dabei steht bpm für beats per minute, diese zeigt somit später den aktuellen Puls an. Die Variable time_last_beat ist für die Speicherung des vorherigen Pulses zuständig und delta ist die zeitliche Differenz zwischen dem aktuellen Herzschlag und dem vorherigen. Dann wird noch eine neue Liste erstellt, in welcher die Puls Werte gespeichert werden. Unter spo2 speichern wir die Blutsauerstoffsättigungswerte. Die Herzfrequenzvariabilität wird unter hfv gespeichert. Als nächstes wird wieder eine neue Liste erstellt, in welcher die Herzfrequenzvariabilitätswerte gespeichert werden. Der Parameter N ist auf 10 eingestellt, welches später die Listen Länge begrenzen wird.

Bevor wir mit den gemessenen Werten des Sensors Berechnungen vornehmen, rufen wir unsere Methode setup() auf, damit die ersten Daten gesammelt werden.

    # Mit dieser Methode wird der Puls, Blutsauerstoff, Herzfrequenzvariabilität berechnet und das Display konfiguriert
    def run_sensor(self):

        # Initialisierung der Variablen

        # Beats per minute
        bpm = 0
        # Für die Speicherung des Zeitpunktes des letzten Herzschlags
        time_last_beat = 0
        # Zeitdifferenz zwischen 2 Herzschlägen
        delta = 0
        # Neue Liste, speichert die bpm Werte
        bpms = []

        # Blutsauerstoff Wert
        spo2 = 0

        # Herzfrequenzvariabilität
        hfv = 0
        # Neue Liste, speichert die hfv Werte
        HFVs = []
        # Listen länge
        N = 10

        # Paramter für die Display Aktualisierung
        c = 0

        # Ruft die Methode setup auf
        self.setup()

Display Einstellungen

Die Werte von Puls, Blutsauerstoffsättigung und Herzfrequenzvariabilität möchten wir am Ende auf dem Display ausgeben. Dazu muss erstmal eine Verbindung zum Display hergestellt werden. Mit release_displays() wird das Display freigegeben. Wenn zum Beispiel das Display kurz vorher schon mal anderweitig benutzt wurde und die Pins belegt sind, werden mit diesem Befehl die Pins wieder freigegeben. Danach stellen wir mit SPI eine Verbindung zum Display her.

Experte

Was ist SPI? SPI bedeutet Serial Peripheral Interface. Es ist genauso wie bei I2C ein Master Slave Prinzip. Der Unterschied ist, dass es eine Dateneingangs- und eine Datenausgangsleitung gibt. Bei I2C gibt es nur eine Datenleitung. Die Taktleitung (clock) gibt vor in welcher Geschwindigkeit die Daten gesendet beziehungsweise empfangen werden. MOSI bedeutet Master Output Slave In. Die Daten werden also von unserem Mikrocontroller an das Display gesendet. MISO bedeutet genau das Umgekehrte, Master Input Slave Output. Dies ist auf None eingestellt, da keine Daten vom Display zum Mikrocontroller gesendet werden müssen 13.

Aufgabe

Schau auf die Rückseite der Platine. Dort ist ein Spickzettel für die Pinbelegung. Suche nach den beiden passenden Pins. Die Taktleitung wird mit SCK abgekürzt und beachte, dass die Verbindung für das OLED Display ist. Schreibe die gefundenen Zahlen jeweils direkt hinter GP, anstelle des Fragezeichens.

1. Tipp

Schaue welche Zahl bei Display_SCK steht und schreibe diese bei clock hin.

2. Tipp

Die Zahl für MOSI steht direkt unter Display_SCK.

        # Display Konfiguration
        displayio.release_displays()
        spi = busio.SPI(clock=board.GP?, MOSI=board.GP?, MISO=None)

Damit das SPI überhaupt verwendet werden kann muss es mit dem unlock() Befehl entsperrt werden. Als nächstes wird das Display mit allen Pins verbunden. Die baudrate gibt an, wie viele Signaländerungen pro Sekunde an das Display übergeben werden können. Für das Display benötigen wir eine Initialisierungssequenz, welche in der Bibliothek ST7789 enthalten ist. Des Weiteren muss dort die Anzahl der Pixel in Breite und Höhe eingegeben werden. Danach wird ein Index der ersten möglichen Zeile und die Rotation des Displays festgelegt. Dabei kann in 90 Grad Schritten das Display gedreht werden. Die blacklight_pin ist für die Hintergrundbeleuchtung des Displays zuständig 4. Damit später die Werte auf dem Display angezeigt werden, erstellen wir erstmal eine Gruppe. In der Gruppe werden sozusagen alle Texte gesammelt und mit dem Befehl show() angezeigt 14.

Aufgabe

Trage in die Lücken anstelle des Fragezeichens, die Höhe und Breite des Displays in Pixel ein. Schaue dir dazu genau das Display auf der Platine an. Dort steht die Anzahl der Pixel.

        spi.unlock()
        display_bus = displayio.FourWire(spi, reset=board.GP13, command=board.GP12, chip_select=board.GP11, baudrate=62500000)
        display = ST7789(display_bus, width=?, height=?, rowstart=80, rotation=180, backlight_pin=board.GP10)

        # Display
        splash = displayio.Group()
        display.root_group=splash

Um Bilder auf dem Display anzuzeigen werden diese als Bitmap gespeichert. Eine Bitmap_datei unterteilt das Bild in Pixel, wobei jedes Pixel eine Farbe repräsentiert. Das Herzsymbol wird unter bitmap gespeichert. Als nächstes wird das Bild als Tile Grid gespeichert, damit es im nächsten Schritt auf dem Bildschirm angezeigt werden kann. Danach wird die Position mit x und y festgelegt 15.

        # Herz Symbol
        # Symbol wird als Bitmap gespeichert
        bitmap = displayio.OnDiskBitmap("/heart.bmp")

        # Tile grid wird erstellt, damit Symbol angezeigt wird
        tile_grid = displayio.TileGrid(bitmap, pixel_shader=bitmap.pixel_shader)
        splash.append(tile_grid)
        # Position des Symbols
        tile_grid.x=150
        tile_grid.y=140

Als nächstes wird mit Label der Text für das SpO2 erstellt. Mit terminalio.FONT wird die Schriftart festgelegt. Danach wird der Text festgelegt und die Farbe. Die Farbe wird als Hexadezimalsystem angegeben.

Was ist das Hexadezimalsystem?

Dezimalzahl Dualzahl Hexadezimalzahl
0 0000 0
1 0001 1
2 0010 2
3 0011 3
4 0100 4
5 0101 5
6 0110 6
7 0111 7
8 1000 8
9 1001 9
10 1010 A
11 1011 B
12 1100 C
13 1101 D
14 1110 E
15 1111 F

Es ist ein Zahlensystem, welches auf der Basis 16 beruht. In der Tabelle sind im Vergleich die Dual-/Binär-, Dezimal- und Hexadezimalzahlen abgebildet. Dort siehst du den Längenunterschied zwischen dem Zahlensystem nochmal genauer. Das Hexadezimalsystem ist viel kompakter. Wie in dem Beispiel zu sehen, stellt man dem Hexadezimalsystem ein 0x vorne weg. Dadurch kann das System von anderen Zahlensystemen unterschieden werden. Da danach zwei Hexadezimalzahlen folgen, besitzt die erste Zahl den Exponenten 1 und die letzte 0. Die Basis ist dabei 16. In der kannst du nachschauen, welche Dezimalzahl F bedeutet. Nach diesem Prinzip kann die Hexadezimalzahl in eine Dezimalzahl übergeführt werden.

\(\begin{aligned} 0xFF &= 15 \cdot 16^1 + 15 \cdot 16^0\\ &= 255 \label{HexaBeispiel}\end{aligned}\)

Jetzt kommen wir zurück zu der Darstellung der Farben. Dabei folgen nach den 0x sechs Hexadezimalzahlen in der Form 0xRRGGBB. Dabei steht R für rot, G für grün und B für blau. Dies erfolgt also nach dem RGB Schema.

RGB steht für rot, grün und blau. Diese drei Farben werden in unterschiedlichen Verhältnissen gemischt, um eine beliebige Farbe zu erhalten. Dabei besitzt jeder der drei Grundfarben ein Wert zwischen 0 und 255 16.

Image title
Darstellung der RGB Farben und die Entstehung von neuen Farben bei der Überlappung der Grundfarben [^RGB].

In der Tabelle sind in der ersten Spalte die verschiedenen Hexadezimalen Werte angegeben. In der zweiten Spalte steht welchem RGB-Anteil dies entspricht und in der letzten, welche Leuchtkraft.

Aufgabe

Suche dir eine Farbe für den Text aus, welche später auf dem Display der Platine zu sehen ist. Schaue dir dafür die Tabelle an und verwende den Hex-Wert. Ergänze dazu nach 0x die Farbe. Zum Beispiel steht 0xFF0000 für rot.

Hex-Wert Dezimalwert Leuchtkraft()
00 0 0
40 64 25
80 128 50
C0 192 75
FF 255 100
        # Display SpO2
        spo2_label=label.Label(terminalio.FONT, text="SpO2 ", color=0x??????)

Nachdem die Farbe für SpO2 festgelegt ist, wird nun die Position für den Text angegeben. Dabei haben wir ein Display mit 240x240 Pixel. Das Display ist wie ein Koordinatensystem aufgebaut 17. Wie in der Abbildung zu sehen, ist oben links im Display die Koordinate 0,0. Nach rechts reicht die x-Achse von dem Wert 0-239. Die y-Achse geht von der Koordinate 0,0 senkrecht nach unten mit den Werten 0-239.

Image title
Aufteilung des Displays in ein Koordinatensystem.

Als nächstes erfolgt mit spo2_label.scale die Skalierung der Schrift, sozusagen die Festlegung der Schriftgröße. Danach muss zum anzeigen der Schrift das label unserer Display Gruppe hinzugefügt werden. Dies wird mit append() vorgenommen.

        spo2_label.x = 10
        spo2_label.y = 20
        spo2_label.scale = 2
        # Label auf dem Display anzeigen
        splash.append(spo2_label)

Als nächstes soll der aktuelle Wert der Sauerstoffsättigung auf dem Display ausgegeben werden. Der Wert der spo2 Variablen wird weiter unten im Quellcode berechnet. In die geschweifte Klammer schreiben wir unsere Variable spo2 damit der aktuelle Wert der Sauerstoffsättigung angezeigt wird. Nach der Variablen folgt ein Prozentzeichen, da die Blutsauerstoffsättigung in Prozent angegeben wird. Danach folgt das gleiche Vorgehen wie oben, bei der Anzeige von SpO2.

Aufgabe

Welche Farbe möchtest du für den Wert der Blutsauerstoffsättigung? Schreibe diese anstelle der Fragezeichen, in Form des Hex-codes rein.

        # Display SpO2 Wert
        spo2_string = f"{spo2}%"
        spo2_value_label = label.Label(terminalio.FONT, text = spo2_string, color = 0x??????)

Aufgabe

Was ist die y-Position des spo2_value_label, wenn wir dieses in der gleichen Zeile wie den String SpO2 stehen haben wollen? Trage diesen Wert ein. Tipp: Wie vorher ist die Position 20.

        # Position des labels
        spo2_value_label.x = 100
        spo2_value_label.y = ?

Aufgabe

Welchen Wert hat der Skalierungsfaktor, wenn wir möchten das der Text die gleiche Größe, wie unser vorheriger Text hat? Trage diesen ein.

        spo2_value_label.scale = ?
Tipp

Wie vorher ist dieser 2.

Aufgabe

Was muss in der Klammer stehen? Wenn du es nicht mehr genau weißt schau dir den vorherigen Abschnitt an.

        # Label auf dem Display anzeigen
        splash.append(?)
Tipp

Schreibe in die Klammer spo2_value_label.

Das gleiche Vorgehen folgt nun für die Anzeige des Pulses und für die Herzfrequenzvariabilität.

        # Display BPM
        bpm_label=label.Label(terminalio.FONT, text="BPM ", color=0xFF0000)
        # Position des labels
        bpm_label.x = 10
        bpm_label.y = 60
        bpm_label.scale = 2
        # Label auf dem Display anzeigen
        splash.append(bpm_label)

        bpm_string = f"{bpm}"
        bpm_value_label = label.Label(terminalio.FONT, text = bpm_string, color = 0xFF0000)
        # Position des labels
        bpm_value_label.x = 100
        bpm_value_label.y = 60
        bpm_value_label.scale = 2
        # Label auf dem Display anzeigen
        splash.append(bpm_value_label)

        # Display Herzfrequenzvariabilität
        hfv_label=label.Label(terminalio.FONT, text="HFV ", color=0x00FF00)
        # Position des labels
        hfv_label.x = 10
        hfv_label.y = 100
        hfv_label.scale = 2
        # Label auf dem Display anzeigen
        splash.append(hfv_label)

        hfv_string = f"{hfv}ms"
        hfv_value_label = label.Label(terminalio.FONT, text = hfv_string, color = 0x00FF00)
        hfv_value_label.x = 100
        hfv_value_label.y = 100
        hfv_value_label.scale = 2
        # Label auf dem Display anzeigen
        splash.append(hfv_value_label)

While-Schleife

Als nächstes folgt eine While-Schleife. Die Schleife wird, wenn das Programm läuft und kein Fehler auftritt, ständig wiederholt. Als erstes möchten wir immer den aktuellen Wert des roten und infraroten Lichts erhalten.

Aufgabe

Rufe die passende Methode auf. Einmal für das rote und einmal für das infrarote Licht.

        while True:
            # Gibt die aktuellen Werte des roten/infraroten Lichts an
            red_ac, red_dc = self.?
            ir_ac, ir_dc = self.?
1. Tipp

Wenn du es nicht mehr weißt schau dir die Methode setup() nochmal an.

2. Tipp

Füge in das erste Fragezeichen red_value() ein.

3. Tipp

Füge in das zweite Fragezeichen ir_value() ein.

Da wir für die spätere Berechnung auch vergangene Werte des roten und infraroten Lichts benötigen, werden mit append() die Werte in einer Liste gespeichert.

            # Der neue Wert wird in der Liste gespeichert
            self.red_buffer.append(red_ac)
            self.ir_buffer.append(ir_ac)

Damit das Pulsoxymeter nicht Werte berechnet, obwohl gar kein Finger im Pulsoxymeter ist, soll dies erkannt werden. Dafür verwenden wir eine if-Bedingung. Wenn der DC-Wert des roten Lichts über 15000 liegt, ist mit großer Wahrscheinlichkeit ein Finger erkannt worden.

            # Wenn der Wert von 15000 überschritten ist, dann ist mit großer Wahrscheinlichkeit ein Finger erkannt worden.
            # Die folgenden Funktionen werden dann ausgeführt.
            if red_dc>15000:

Da es immer kleine Messfehler und Ungenauigkeiten im Signalverlauf gibt, wird ein FIR-Tiefpassfilter verwendet. Einfach gesagt glättet dieser unser Signal.

                # Fir Tiefpassfilter
                red_fir = fir_lowpass(self.red_buffer.get_buffer(), FIR_FILTER_COEFFS)

Wir rufen nun die Methode check_beat() auf. Diese soll in unserem roten AC-Signal schauen, ob es ein beat, also ein Pulsschlag gibt. Mithilfe der if-Bedingung soll dies untersucht werden.

Aufgabe

Was muss dafür in der Klammer von check_beat stehen?

                # Wenn ein Herzschlag erkannt wurde, wird der Puls berechnet.
                if self.heart_rate.check_beat(?):
1. Tipp

Wir wollen das rote AC-Signal untersuchen, welches zuvor mit einem Filter geglättet wurde.

2. Tipp

Schreibe red_fir in die Klammer.

Der Teil in der if-Bedingung wird nur ausgeführt, wenn ein Puls erkannt wurde. Dabei sucht der Algorithmus nach den Spitzen in einem Signal.

Danach wird mit ticks_ms() ein timer gestartet. Unter time_last_beat wird der Zeitpunkt des vorherigen beats gespeichert. Bei delta wird also die Zeitdifferenz zwischen zwei detektierten beats berechnet, wie in zu sehen. Der Puls wird immer auf die Herzschläge pro Minute berechnet. Da eine Minute 60000 Millisekunde hat wird dies durch die Zeitdifferenz zwischen einem beat dividiert. Daraus ergibt sich ein ungefährer Puls.

Image title
Zwei aufeinanderfolgende Herzschläge, wobei die zeitliche Differenz delta entspricht [^EKG].
                    # Ein timer wird in ms gestartet
                    t = ticks_ms()
                    # Die zeitliche Differenz zwischen den letzten 2 Herzschlägen wird berechnet.
                    delta = ticks_diff(t, time_last_beat)
                    # Der Zeitpunkt des Herzschlags wird gespeichert, damit beim nächsten Durchlauf wieder die Zeit zwischen den letzten beiden Herzschlägen berechnet werden kann.
                    time_last_beat = t
                    # Der Puls wird berechnet.
                    bpm = 60000 / delta

Damit die peak Detektion etwas genauer wird, wird nochmals eine if-Bedingung eingefügt. Diese wird nur ausgeführt, wenn der Puls unter 250 liegt.

Experte

Der maximal Puls eines Menschen, welcher bei maximaler körperlicher Anstrengung erreicht werden kann, kann mit der Sally-Edwards-Formel berechnet werden 19.

\(\begin{aligned} M = 214 - 0,5 \cdot Lebensalter - 0,11 \cdot Gewicht (in\ Kg) \label{maxpulsM}\end{aligned}\)

\(\begin{aligned} F = 210 - 0,5 \cdot Lebensalter - 0,11 \cdot Gewicht (in\ Kg) \label{maxpulsF}\end{aligned}\)

Dabei steht M für die Berechnung des Maximalpulses für Männer und F für Frauen.

Da die beiden Gleichungen oben nur einer Näherung entsprechen, ist der maximale Puls auf 250 für den Pulsoxymeter festgelegt. Alles was darüber hinaus geht sind mit hoher Wahrscheinlichkeit Messfehler. Wenn der Puls über 250 ist, entspricht dies Kammerflattern und bei über 400 Schlägen pro Minute spricht man von Kammerflimmern 20. Da das Pulsoxymeter in unserem Versuch nicht darauf ausgelegt ist Kammerflattern und -flimmern erkennen zu müssen schließen wird dies aus.

Wenn also ein Puls von unter 250 gemessen wurde, wird dieser Wert der Liste bpms angehängt. Danach werden die letzten zehn Elemente in unserer Liste gespeichert. Der älteste Wert wird somit gelöscht.

Für die Berechnung der Herzfrequenzvariabilität, werden in der Liste HFVs die delta Werte gespeichert. Die Variable delta speichert den zeitlichen Abstand zwischen zwei Herzschlägen.

                    # Es werden die letzten 10 berechneten Pulse, gespeichert, wenn der Puls unter 250 liegt.
                    if (bpm < 250):
                        bpms.append(bpm)
                        bpms = bpms[-10:]
                        HFVs.append(delta)

Was ist die Herzfrequenzvariabilität?🫀

Wie du in der Abbildung sehen kannst, schwankt die Zeit zwischen zwei Herzschlägen. Genau diese zeitliche Abweichung stellt die Herzfrequenzvariabilität dar.

Image title
Darstellung der Herzfrequenzvariabilität [^HRVBild].

Die nächste if-Bedingung wird ausgeführt, wenn die Länge der HFVs Liste größer als N ist. Dadurch werden erstmal die Werte gesammelt, damit danach ein Mittelwert gebildet werden kann. Innerhalb der if-Bedingung werden die letzten N Elemente in der Liste gespeichert und das älteste Element gelöscht. Als nächstes wird die Herzfrequenzvariabilität berechnet. Dafür verwenden wir die folgende Formel 22.

\(HRV = \sqrt{\frac{1}{N-1}\sum_{i=1}^{N-1}(RR_{i+1}-RR_{i})^2} \label{HRV2}\)

Dabei benötigen wir eine Summenformel, welche mithilfe der for-Schleife umgesetzt wird. Dabei stellt die for-Schleife sozusagen die Grenzen der Summenformel dar. Zuerst wird für i Null eingesetzt, danach eins und so weiter. Das range() erstellt eine Liste von Zahlen von 0 bis einschließlich N-1. In unserem Fall ist N-1 = 9. Es wird somit eine Liste mit 9 Elementen erstellt. Dies entspricht 0, 1, 2, 3, 4, 5, 6, 7, 8. Mithilfe der Summenformel wird ein Mittelwert über die letzten 10 HFVs Werte gebildet.

                        # Die Herzfrequenzvariabilität wird berechnet, wenn N Werte vorhanden sind.
                        if (len(HFVs))>N:
                            HFVs = HFVs[-N:]
                            hfv = round(sqrt(1/(N-1) * sum((HFVs[1+i]-HFVs[i])**2 for i in range(N-1))))

Für die berechneten Pulswerte wird ebenso ein Mittelwert gebildet. Da wir zehn Pulswerte gespeichert haben, addieren wir diese zehn Elemente auf und dividieren durch zehn.

                # Es wir der aktuelle Puls mit der Bildung eines Mittelwertes berechnet.
                bpm = int(sum(bpms)/10)

Jetzt kann als nächstes der Blutsauerstoff berechnet werden. Dort werden zuerst die 100 Werte des roten Lichts, welche in der Liste gespeichert sind mit sich selbst multipliziert, dann addiert und die Wurzel gezogen. Am Ende wird dies durch 100 dividiert, um wieder einen Mittelwert zu erhalten. Dadurch wird ein quadratisches Mittel berechnet, welche etwas größere Werte stärker gewichtet als kleine. In ist die allgemeine Formel für die Berechnung des quadratischen Mittels gegeben 23. Das Gleiche Verfahren wird danach für das infrarote Licht durchgeführt. Als nächstes wird mit der Variablen r_ratio das Modulationsverhältnis ermittelt. Dies ist ein Verhältnis zwischen dem infraroten AC- und DC-Wert und dem roten AC- und DC-Wert. Das Verhältnis wird durch die Blutsauerstoffsättigung beeinflusst.

\(QM = \sqrt{\frac{1}{N}\sum_{i=1}^{N}(x_{i})^2} \label{QuadratischesMittel}\)

                # Der Blutsauerstoffsättigungswert wird berechnet.
                red_rms = sqrt(sum(i * i for i in self.red_buffer.get_buffer()) / CIRCULAR_BUFFER_SIZE)
                ir_rms = sqrt(sum(i * i for i in self.ir_buffer.get_buffer()) / CIRCULAR_BUFFER_SIZE)
                r_ratio = (ir_rms / ir_dc) / (red_rms / red_dc) 

Eine Näherung für die Berechnung des Blutsauerstoffgehalts ist die Formel 24.

                spo2 = round(110 - 25 * r_ratio, 1)

Da es kleine Messfehler gibt durch zum Beispiel die Bewegung des Finger, kann es passieren, dass der Blutsauerstoffgehalt angeblich größer als 100% ist.

Aufgabe

Erstelle eine if-Bedingung, welche Werte über 99,9% als einen spo2 Wert von 99,9% anzeigt.

                # Wenn der spo2 Wert über 100% liegen sollte, wird dieser als 99,9% angegeben.
                ?
Tipp
                if spo2 > 99.9:
                    spo2 = 99.9

Danach möchten wir noch, dass unser Pulsoxymeter keine Werte ausgibt, wenn kein Finger vorhanden ist. Dabei wird der DC-Wert betrachtet, da dieser Wert durch die Reflexion an Haut, Gewebe und weiteren nicht pulsierenden Komponenten des Fingers einen relativ konstanten Wert besitzt.

Aufgabe

Welche Werte sollen die folgenden Variablen annehmen, wenn kein Finger erkannt wurde, also der DC-Wert kleiner ist als dein festgelegter Schwellenwert.

            # Wenn der DC-Wert des roten Lichts unter 15000 beträgt, werden die Variablen auf 0 gesetzt.
            if red_dc < 15000:
                bpm = ?
                spo2 = ?
                red_fir = ?
                hfv = ?
Tipp

Die Werte sollen 0 sein.

Der letzte Schritt in unserer while-Schleife ist die Aktualisierung der Werte auf dem Display. Damit nicht bei jedem while-Schleifen Durchlauf die Werte aktualisiert werden und Ressourcen gespart werden, wird eine weitere if-Bedingung eingeführt. Dabei wird bei jedem 50. Durchlauf der Wert auf dem Display aktualisiert. Dafür wird ein formatierter String verwendet. Das .1f bedeutet eine Nachkommastelle. Das f leitet sich von float ab, dies ist ein Datentyp, welcher Kommazahlen speichern kann. Mit der nächsten Zeile kann nun der aktuelle String angezeigt werden. Das gleiche Vorgehen folgt nun für den Puls und die Herzfrequenzvariabilität.

Aufgabe

Auf wie viel Kommastellen sollte der Puls gespeichert werden? Trage den Wert in das Fragezeichen ein.

            # Jeden 50. Durchlauf der while-Schleife werden die Werte auf dem Display aktualisiert.
            if c==50:
                c=0
                # Display SpO2 Wert
                spo2_string = f"{spo2:.1f}%"
                spo2_value_label.text = spo2_string

                # Display BPM Wert
                bpm_string = f"{bpm:.?f}"
                bpm_value_label.text = bpm_string

                # Display HFV Wert
                hfv_string = f"{hfv:.0f}ms"
                hfv_value_label.text = hfv_string
            c+=1
1. Tipp

Sollte ein Puls eine Kommastelle haben?

2. Tipp

Es ist eine 0.

Programm starten

Jetzt haben wir unser Programm fertig gestellt. Das Programm wird mit der Klasse und der Methode aufgerufen.

Aufgabe

Schreibe dies nun unten in den Quelltext, damit das Programm fertig gestellt wird.

Pulsoxymeter().run_sensor()
Starte nun das Programm, indem du im Mu Editor oben auf ausführen klickst.

Jetzt sollte alles funktionieren und deine Parameter sollten auf dem Display der Platine angezeigt werden. Wenn etwas nicht funktioniert, wird eine Fehlermeldung ausgegeben, in der steht wo der Fehler ist. Gehe zu der Aufgabe zurück und schaue sie dir nochmal an.

Achte bei der Messung darauf deinen Finger möglichst ruhig zu halten. Durch Bewegung werden die Messwerte verfälscht.

Zusatzaufgaben

Puls plotten

Wenn du schon den Puls, die Blutsauerstoffsättigung und die Herzfrequenzvariabilität auf dem Display siehst, kannst du dir als nächstes das AC-Signal anschauen. Damit du einen Plot auf dem PC-Bildschirm sehen kannst, wird der print-Befehl verwendet 25. Füge also

                print((red_fir,))

deinem Quelltext hinzu. Dabei sollte dies am besten am Ende der if-Bedingung

            if red_dc>15000:

stehen. Dadurch wird der Plot nur angezeigt, wenn ein Finger detektiert wird. Jetzt kannst du das Programm ausführen. Damit der Plot zu sehen ist, drücke oben bei dem Mu-Editor auf Plotter. Jetzt sollte sich neben dem REPL eine Plotter öffnen. Wenn du jetzt mit deinem Finger eine Messung durchführst ist das AC-Signal zu sehen. Dort siehst du eine Wellenform, welches dein Puls darstellt. Achte darauf deinen Finger ruhig zu halten. Danach kannst du dir anschauen, was passiert wenn du deinen Finger bewegst. Was hat dies für Folgen für deine Messung?

Plot auf dem Display

Alt Text
Als nächstes kannst du den Plot auch auf dem Display anzeigen lassen. Dabei wird die Höhe und die Breite in Pixel des zusehenden Graphen eingestellt. Mithilfe der Sparkline() Funktion wird ein Graph erstellt. Bei max_items, wird angegeben wie viele Werte in einem Graph angezeigt werden. Mit x und y wird die Position des Graphen auf dem Display festgelegt. Der folgende Quelltext wird vor die while-Schleife geschrieben.

Aufgabe

Damit der Graph angezeigt wird, muss etwas in der Klammer ergänzt werden. Ergänze diese Lücke.

        # BPM Plot
        chart_width = 50
        chart_height = 40
        sparkline1 = Sparkline(width=chart_width, height=chart_height, max_items=15, x=30, y=150)
        splash.append(?)
1. Tipp

Trage in die Klammer sparkline1 ein.

Damit der Plot von dem roten Licht erst angezeigt wird wenn es in einem relevanten Wertebereich liegt, wird der Bereich mithilfe der if-Bedingung begrenzt. Innerhalb der Schleife wird der Wert des roten Lichts auf eine Nachkommastelle gerundet. Mithilfe von auto_refresh = False wird das Display kurz ausgeschaltet und dann der neue Wert dem Plot hinzugefügt. Danach wird das Display wieder angeschaltet und der neue Wert wird mit auf dem Plot angezeigt 26.

                # Das Signal des roten Lichts wird geplottet, wenn der Wert zwischen -500 und +500 liegt. Damit nur relevante Daten angezeigt werden.
                if (-500<red_fir<500):
                    red_plot = round(red_fir,1)
                    display.auto_refresh = False
                    sparkline1.add_value(red_plot)
                    display.auto_refresh = True

Diesen Teil schreibst du innerhalb der folgenden if-Bedingung.

            if red_dc>15000:

Wenn alles funktioniert kannst du noch weitere Sachen mit dem Pulsoxymeter ausprobieren. Du kannst zum Beispiel verschiedene Farben ausprobieren, die Position des Textes ändern oder dir selbst etwas ausdenken.


  1. Sebastian Dörn. Python lernen in abgeschlossenen Lerneinheiten. Springer Vieweg, Wiesbaden, 2020. 

  2. Core modules. URL: https://docs.circuitpython.org/en/latest/shared-bindings/index.html (visited on 2023-09-28). 

  3. Circuitpython libraries. URL: https://circuitpython.org/libraries (visited on 2023-10-01). 

  4. Gut gesättigt! URL: https://become-an.engineer/2023/ies/ (visited on 2023-11-01). 

  5. Sam Koblenski. Everyday dsp for programmers: dc and impulsive noise removal. URL: http://sam-koblenski.blogspot.com/2015/11/everyday-dsp-for-programmers-dc-and.html (visited on 2023-08-29). 

  6. Funktionen und methoden bei variablen/strings. URL: https://www.python-lernen.de/unterschied-methoden-funktionen.htm (visited on 2023-10-25). 

    1. objekte und klassen. URL: https://pythonbuch.com/objekte.html (visited on 2023-10-18).

  7. Python self. URL: https://www.w3schools.com/python/gloss_python_self.asp (visited on 2023-11-02). 

  8. Enrique Fernandez. I2c-bus. URL: http://fmh-studios.de/theorie/informationstechnik/i2c-bus/ (visited on 2023-09-30). 

  9. Max30102. 2018. URL: https://www.analog.com/media/en/technical-documentation/data-sheets/MAX30102.pdf (visited on 2023-10-02). 

  10. First in - first out (fifo). URL: https://www.datenbanken-verstehen.de/lexikon/first-in-first-out/ (visited on 2023-10-18). 

  11. Eberhard Koch. Anästhesievorbereitung und perioperatives Monitoring. Thieme, München und Frankfurt am Main, 2015. 

  12. Spi protocol. URL: https://learn.adafruit.com/circuitpython-basics-i2c-and-spi/spi-devices (visited on 2023-10-01). 

  13. Group. URL: https://learn.adafruit.com/circuitpython-display-support-using-displayio/group (visited on 2023-10-15). 

  14. Display a bitmap. URL: https://learn.adafruit.com/circuitpython-display-support-using-displayio/display-a-bitmap (visited on 2023-09-25). 

  15. Über die farbmodelle rgb, cmyk und hex, ihre anwendung und umwandlung. URL: https://www.techsmith.de/blog/rgb-und-cmyk-unterschied/ (visited on 2023-10-20). 

  16. Grouplibrary overview. URL: https://learn.adafruit.com/circuitpython-display-support-using-displayio/library-overview (visited on 2023-10-15). 

  17. Elektrokardiografie (ekg). URL: https://www.kardionet.de/elektrokardiographie-ekg/ (visited on 2023-10-19). 

  18. Trainingspuls. URL: https://www.kardiologie-gamm.de/sportmedizin/trainingspuls/ (visited on 2023-10-12). 

  19. Nicole Menche. Biologie Anatomie Physiologie. Elsevier, München, 2016. 

  20. Alles, was du über die herzfrequenzvariabilität (hfv) wissen musst. URL: https://www.whoop.com/ae/de/thelocker/alles-was-du-uber-die-herzfrequenzvariabilitat-hfv-wissen-musst/ (visited on 2023-10-22). 

  21. Nicole Franke-Gricksch. Berechnung des hrv-werts rmssd. URL: https://hrv-herzratenvariabilität.de/2017/10/berechnung-des-hrv-werts-rmssd/ (visited on 2023-09-12). 

  22. Quadratisches mittel. URL: https://de.wikipedia.org/wiki/Quadratisches_Mittel (visited on 2023-10-21). 

  23. How to design peripheral oxygen saturation (spo2) and optical heart rate monitoring (ohrm) systems using the afe4403. URL: https://www.ti.com/lit/an/slaa655/slaa655.pdf (visited on 2023-09-09). 

  24. Plotting data with mu. URL: https://codewith.mu/en/tutorials/1.2/plotter (visited on 2023-09-04). 

  25. Sparkline simple test. URL: https://docs.circuitpython.org/projects/display-shapes/en/latest/examples.html#sparkline-simple-test (visited on 2023-09-20).