Implementierung von Klassen für Sensor und OLED; Probleme mit der colloctions.deque() Funktion

Python auf Einplatinencomputer wie Raspberry Pi, Banana Pi / Python für Micro-Controller
Antworten
DoGro
User
Beiträge: 15
Registriert: Montag 23. Mai 2022, 07:18

Guten Tag,

bei diesem Post viewtopic.php?t=56975 habe ich von euch schon super Hilfe bekommen und konnte dann die Sensordaten erfassen und filtern. Ich habe sie in eine Text - Datei geschrieben und dann mit Matplotlib dargestellt.

Bild

Nun bin ich dabei das Programm in separate Klassen zu überführen. Leider bin ich mir bei den Klassen auch noch nicht sicher, wann etwas in der __init__ als self. deklariert werden muss und wann man etwas vor der __init__ schon definieren kann.

Kurze Info ich habe ein Raspberry Pi Pico mit einem Oled-Display und einem MX30102-Sensor.
Das Display und den Sensor hätte ich gerne in separaten Dateien und rufe diese dann aus dem Hauptprogramm auf.

Code: Alles auswählen

from machine import Pin
from display import Display
from sensor import Sensor
import time


#Hauptprogramm
def main():
    try:
        print('Start')
        oled = Display()
        iR_Daten = Sensor()
        iR_Daten.sensor_lesen()
    finally:
        print('Fertig')


if __name__ == "__main__":
    while True:
        main()
        time.sleep_ms(5000)
Datei Sensor:

Code: Alles auswählen

"""Es wir der MAX30102 verwendet zum Messen der SpO2 und HR"""

from MAX30102 import MAX30102
from machine import Pin, I2C
from time import sleep_ms, ticks_ms, ticks_diff
from collections import deque


class Sensor:
    
    def __init__(self):
        #Konfiguration des Sensors
        self.i2c_kanal_0 = I2C(0, sda=Pin(16), scl=Pin(17))
        self.sensor = MAX30102(self.i2c_kanal_0)
        self.sensor.setup_sensor() # Es werden Standardmäßig beide LEDs angesprochen.
        
        #Speicherliste für die Messdaten
        self.ir_werte = [] 
        self.ir_gefiltert = []
        
        #Liste für Herzratenberechnung
        self.fallende_w = []
        self.zeiten = deque((self.fallende_w), 5)
        

    def sensor_lesen(self): 
        start_zeit = ticks_ms()
        vergangene_zeit = 0
        while vergangene_zeit < 5000: #Zeitdauer der Messung in Millisekunden
            self.sensor.check()# muss in einer While-Schleife bleiben
            # Im Slot 0 werden die Daten des IR_self.sensors abgefragt.
            # Im Slot 1 werden die LED_Daten hinterlegt
            if self.sensor.slot_data_available(0) == 1:
                self.ir_werte.append(self.sensor.get_slot_data(0))
            vergangene_zeit = ticks_diff(ticks_ms(), start_zeit)
        self.sensor.shutdown(True)
        herz_rate_erkennen()
        

    def iir_filter(self, ir_werte_uebergabe):
        alpha = 0.95
        aktuelles_w = ir_werte_uebergabe[0] / (1 - alpha)
        for signal in ir_werte_uebergabe[1:]:
            vorheriges_w = aktuelles_w
            #Entfernen des Offsets der Werte mithilfe des Filters IIR
            aktuelles_w = signal + alpha * vorheriges_w
            gefiltertes_signal = aktuelles_w - vorheriges_w
            self.ir_gefiltert.append(float(gefiltertes_signal))
        print('in Funktion gefiltert - ' + str(self.ir_gefiltert))
        return gefiltertes_signal # Ersten 50 Werte werden für das Einschwingen der Funktion benötigt, und daher gelöscht
    
    
    def herz_rate_erkennen(self):
        #IIR-Filter auf Daten anwenden
        gefiltertes_signal = self.iir_filter(self.ir_werte)
        
        if (len(self.fallende_w) == 0):
            self.fallende_w.append(gefiltertes_signal)
        else:
        # Zum finden von den fallenden Werte mit Nulldurchgang
            if self.fallende_w[0] > 0 and \
               self.fallende_w[-1] < 0.0 and \
               self.fallende_w[0] - self.fallende_w[-1] > 200:
                self.herz_rate_rechner()
            self.fallende_w.clear()
    
    
    def herz_rate_rechner(self):
        self.zeiten.append(ticks_ms())
        if len(self.zeiten) < 2:
            return
        bpm = 60000 / self.zeiten[-1] - self.zeiten[0] * (len(self.zeiten) - 1)
        bpm = int(bpm)

Hier ist ein gespeicherter Datensatz von meinem Sensor:
https://www.gronloh.net/wp-content/uplo ... _Daten.txt

Nun zu meinem Problem. :(
Ich bekomme einen Value Error bei der deque Funktion.
self.zeiten = deque((self.fallende_w), 5)

Folgende Internet Seiten habe ich mir angeguckt:
https://docs.micropython.org/en/latest/ ... tions.html
https://realpython.com/python-deque/

Egal wie ich die Funktion oder auch die Parameter deklariere immer bekomme ich einen Fehler.

Zweite Frage, habe ich bei den Klassen so erstmal alles richtig umgesetzt?

Die bpm Werte würde ich dann gerne abgreifen und als Text auf dem OLED darstellen.

Vielen Dank
Benutzeravatar
__blackjack__
User
Beiträge: 13116
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@DoGro: Eine Datei pro Klasse ist eher ungewöhnlich. Das ist Python und nicht Java.

Es wäre ja interessant was für einen Fehler Du wo genau bekommst.

Die Klasse `Sensor` sieht so nicht so wirklich sinnvoll aus. Wenn man ein Objekt erstellt und darauf dann nur eine Methode aufruft, ist das oft einfach eine Funktion die in eine Klasse gesteckt wurde.

Das `self.zeiten` über `self.fallende_w` initualisiert wird ist irreführend. Das ist ja an der Stelle immer eine leere Liste, also sollte man auch genau so eine benutzen, damit keiner denkt die beiden Werte wären irgendwie verknüpft.

Der Kommentar ``# Ersten 50 Werte werden für das Einschwingen der Funktion benötigt, und daher gelöscht`` passt irgendwie nicht zum Code. Ich sehe nicht wo da an der Stelle 50 Werte gelöscht werden.

Beim Namen `ir_werte_uebergabe` ist sowohl das `ir_` als auch das `_uebergabe` nicht sinnvoll. Der Filterfunktion ist es egal wo die Werte her kommen, und das die Werte übergeben worden sind, sieht man schon daran, dass sie in der Argumentliste stehen.

`ir_gefiltert` wird nur in `herzrate_erkennen()` verwendet, gehört also nicht zum Zustand.

Eigentlich machen all diese Listen keinen Sinn als Zustand so wie es jetzt abläuft, weil die einfach nur dazu missbraucht werden keine Funktionen mit ordentlicher Parameterübergabe zu schreiben. Beziehungsweise sollte davon vielleicht etwas Zustand sein, aber dann macht es keinen Sinn das jede Methode nur einmal aufgerufen wird. `self.fallende_w` kann selbst bei mehrfachen Aufrufen immer nur entweder gar kein oder genau ein Element enthalten. Das ist kein sinnvoller Einsatz für eine Liste. Oder der Code ist fehlerhaft. `herz_rate_rechner()` wird im Moment beispielsweise niemals erreicht.

Eine Klasse beschreibt ein ”Ding” und die Attribute woraus dieses ”Ding” besteht. Es ist ein „code smell“ wenn ein Attribut genau so heisst wie die Klasse, denn ein Sensor besteht aus einem Sensor und Messwerten, klingt schräg. Man kann natürlich einen Sensor aus anderen Sensoren bauen, dann sollte man an den beiden Namen aber erkennen können was da was ist. Also so etwas wie `HartbeatSensor` und `ir_sensor`.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
DoGro
User
Beiträge: 15
Registriert: Montag 23. Mai 2022, 07:18

Hallo __blackjack__,

die beiden Dateien habe ich erstellt um einfach erstmal eine übersichtliche Trennung zu erhalten. Und damit eine Datei nicht zu lang wird.
Die Klasse `Sensor` sieht so nicht so wirklich sinnvoll aus. Wenn man ein Objekt erstellt und darauf dann nur eine Methode aufruft, ist das oft einfach eine Funktion die in eine Klasse gesteckt wurde.
Da ich erstmal versuche meine Klasse überhaupt ans laufen zu bekommen wirkt es vermutlich genau so, wie du schreibst.
Das `self.zeiten` über `self.fallende_w` initualisiert wird ist irreführend. Das ist ja an der Stelle immer eine leere Liste, also sollte man auch genau so eine benutzen, damit keiner denkt die beiden Werte wären irgendwie verknüpft.
Ok, da ist ja noch mein Verständnis Problem.
Wäre es dann richtig die leere Liste vor der __init__ zu schreiben?

Und momentan kann ich an den Herzratenfunktionen nicht weiter arbeiten, weil ich die collections.deque() Funktion nicht zum laufen bekomme. :(

Fallende_w und zeiten, sollen 2 unabhängige Listen sein.

Code: Alles auswählen

class Sensor:

    #Liste für Herzratenberechnung
    fallende_w = []
    zeiten = deque([1, 2, 3, 4, 5], 5)
        
    def __init__(self):
        #Konfiguration des Sensors
        self.i2c_kanal_0 = I2C(0, sda=Pin(16), scl=Pin(17))
        self.sensor = MAX30102(self.i2c_kanal_0)
        self.sensor.setup_sensor() # Es werden Standardmäßig beide LEDs angesprochen.
        
        #Speicherliste für die Messdaten
        self.ir_werte = [] 
        self.ir_gefiltert = []
Hier habe ich tatsächlich bisher die Werte aus der Print Funktion kopiert und in meine matplotlib Funktion eingesetzt zum gucken, ob der Filter funktioniert. Da hatte ich die 50 ersten Werte dann raus gefiltert.
Verwendet wurden die noch gar nicht, da hast du recht.

Code: Alles auswählen

def iir_filter(self, ir_werte_uebergabe):
        alpha = 0.95
        aktuelles_w = ir_werte_uebergabe[0] / (1 - alpha)
        for signal in ir_werte_uebergabe[1:]:
            vorheriges_w = aktuelles_w
            #Entfernen des Offsets der Werte mithilfe des Filters IIR
            aktuelles_w = signal + alpha * vorheriges_w
            gefiltertes_signal = aktuelles_w - vorheriges_w
            self.ir_gefiltert.append(float(gefiltertes_signal))
        print('in Funktion gefiltert - ' + str(self.ir_gefiltert))
        return gefiltertes_signal[50:] # Ersten 50 Werte werden für das Einschwingen der Funktion benötigt, und daher gelöscht
Wegen der Benennung, die Klasse Sensor soll die 2 Methoden Herzrate berechnen und Sauerstoffsättigung berechnen enthalten.
Und ich habe die Dateien so benannt, weil es die beiden Bauteile betrifft.
Wäre es so besser?

Code: Alles auswählen

from machine import Pin
from display import oled_display
from sensor import MAX30102
Das mit der Benennung mache ich tatsächlich damit ich mit den Namen und wo welche Wert verwendet wird nicht durcheinander komme. Liegt vermutlich an meiner Unsicherheit.

Code: Alles auswählen

def iir_filter(self, ir_werte_uebergabe):
Vielen Dank schonmal
Benutzeravatar
__blackjack__
User
Beiträge: 13116
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@DoGro: Solange eine Datei nicht so ca. 1000 Zeilen lang ist, mache ich mir noch keine Gedanken über Übersicht. Das ist dann vielleicht eine Frage der Werkzeuge. Jeder vernünftige Editor der sich zum Programmieren eignet, bietet irgendeine Funktionalität um leicht in einer Quelltextdatei navigieren zu können. IDEs sowieso. Also zum Beispiel eine Baumdarstellung dessen was da in der Datei an globalen Namen, Funktionen, Klassen, und Methoden existiert. Oder „code folding“, so dass man alle Inhalte von den ``def``-Zeilen einklappen kann, an denen man gerade nicht arbeitet. Oder eine Miniaturübersicht über den kompletten Dateiinhalt. Oder sogar alle drei Sachen.

Auf der Klasse haben Variablen nichts zu suchen, damit wären die dann ja globaler Zustand.

``from sensor import MAX30102`` würde dann doch das `MAX30102` aus dem `MAX30102`-Modul aus dem `sensor`-Modul importieren‽ Und ist an der Stelle doch ein Detail was gar nicht interessieren sollte. Wenn es den Hardware-Sensor irgendwann einmal nicht mehr gibt oder es was besseres/günstigeres/… gibt, dann will man ja die Sensor-Klasse immer noch mit der gleichen Schnittstelle, dann auf einem anderen Hardware-Sensor aufbauen, ohne das man dafür dann irgendwelchen Code ändern müsste, der den verwendet. Alternative wäre dann den falschen Namen im Programm zu lassen. Was ja irgendwie auch keine Alternative ist.

Das mit den Namenszusätzen für Argumente damit man die als Argumente erkennt ist ein Problem. Eine Funktion oder Methode sollte nicht so viele verschiedene Namen verwenden und/oder so lang sein, dass das nötig wäre. Die Funktion `iir_filter()` würde ich auch einfach als Funktion schreiben, ohne irgendwelches Wissen darüber was da gefiltert wird, oder das es irgendwelche Klassen gibt. Am besten auch als Generatorfunktion, ohne irgendwelche Listen und Längen. Wenn man davon die ersten 50 Werte verwerfen will, kann das der Aufrufer machen. Beispielsweise mit `itertools.islice()`.

Code: Alles auswählen

def iir_filter(werte, alpha=0.95):
    werte = iter(werte)
    aktuelles_w = next(werte) / (1 - alpha)
    for signal in werte:
        vorheriges_w, aktuelles_w = aktuelles_w, signal + alpha * vorheriges_w
        yield aktuelles_w - vorheriges_w
Mir ist immer noch nicht ganz klar ob die Sensorklasse überhaupt eine Klasse ist. Im Moment sieht es eher nicht danach aus.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
DoGro
User
Beiträge: 15
Registriert: Montag 23. Mai 2022, 07:18

Hallo,

danke ich überdenke das nochmal mit der Klasse.

Könnte mir jemand helfen, warum ich das nicht zum laufen bekomme?

Und momentan kann ich an den Herzratenfunktionen nicht weiter arbeiten, weil ich die collections.deque() Funktion nicht zum laufen bekomme. :(

Ich bekomme immer einen Value error.

Viele Dank
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Bitte den echten Code und die vollständige, dazugehörige Fehlermeldung zeigen. Nicht etwas paraphrasieren. Dazu kann man keine Aussage treffen.
DoGro
User
Beiträge: 15
Registriert: Montag 23. Mai 2022, 07:18

Ok sorry.

Code: Alles auswählen

from machine import Pin
from display import Display
from sensor import Sensor

import time


#Hauptprogramm
def main():
    try:
        print('Start')
        #oled = Display()
        iR_Daten = Sensor()  #Line 13
        iR_Daten.sensor_lesen()
    finally:
        print('Fertig')



if __name__ == "__main__":
    main()   #Line 22
        

Code: Alles auswählen

"""Es wir der MAX30102 verwendet zum Messen der SpO2 und HR"""

from MAX30102 import MAX30102
from machine import Pin, I2C
from time import sleep_ms, ticks_ms, ticks_diff
from collections import deque


class Sensor:
    
    def __init__(self):
        #Konfiguration des Sensors
        self.i2c_kanal_0 = I2C(0, sda=Pin(16), scl=Pin(17))
        self.sensor = MAX30102(self.i2c_kanal_0)
        self.sensor.setup_sensor() # Es werden Standardmäßig beide LEDs angesprochen.
        
        #Speicherliste für die Messdaten
        self.ir_werte = [] 
        self.ir_gefiltert = []
        
        #Liste für Herzratenberechnung
        self.fallende_w = []
        self.zeiten = deque([0,1,2,3,4], 5)   #Line 23
        bpm = None
        

    def sensor_lesen(self): 
        start_zeit = ticks_ms()
        vergangene_zeit = 0
        while vergangene_zeit < 5000: #Zeitdauer der Messung in Millisekunden
            self.sensor.check()# muss in einer While-Schleife bleiben
            # Im Slot 0 werden die Daten des IR_self.sensors abgefragt.
            # Im Slot 1 werden die LED_Daten hinterlegt
            if self.sensor.slot_data_available(0) == 1:
                self.ir_werte.append(self.sensor.get_slot_data(0))
            vergangene_zeit = ticks_diff(ticks_ms(), start_zeit)
        self.sensor.shutdown(True)
        herz_rate_erkennen()
        
        

    def iir_filter(self, ir_werte):
        alpha = 0.95
        aktuelles_w = ir_werte[0] / (1 - alpha)
        for signal in ir_werte[1:]:
            vorheriges_w = aktuelles_w
            #Entfernen des Offsets der Werte mithilfe des Filters IIR
            aktuelles_w = signal + alpha * vorheriges_w
            gefiltertes_signal = aktuelles_w - vorheriges_w
            self.ir_gefiltert.append(float(gefiltertes_signal))
        print('in Funktion gefiltert - ' + str(self.ir_gefiltert))
        return gefiltertes_signal
    
    
    def herz_rate_erkennen(self):
        #IIR-Filter auf Daten anwenden
        gefiltertes_signal = self.iir_filter(self.ir_werte)
        
        if (len(self.fallende_w) == 0):
            self.fallende_w.append(gefiltertes_signal)
        else:
            # Zum finden von den fallenden Werte mit Nulldurchgang
            if self.fallende_w[0] > 0 and \
               self.fallende_w[-1] < 0.0 and \
               self.fallende_w[0] - self.fallende_w[-1] > 200:
                self.herz_rate_rechner()
            self.fallende_w.clear()
    
    def herz_rate_rechner(self):
        self.zeiten.append(ticks_ms())
        if len(self.zeiten) < 2:
            return
        # Aus den 5 Listenwerten soll der Mittelwert gebildet werden.
        bpm = 60000 / self.zeiten[-1] - self.zeiten[0] * (len(self.zeiten) - 1)
        bpm = int(bpm)
        return bpm
        
    
    def speichern():
        with open("self.iR_daten.txt", "w", encoding="ascii") as file, \
             open('filterd_signal.txt', 'w') as file1:
            #Da der erste Messwert immer viel zu niedrieg ist wird der nicht mit gespeichert
            for ir_wert in ir_werte[1:]: 
                file.write(str(ir_wert) + "\n")
            for ir_wert in ir_gefiltert: 
                file1.write(str(ir_wert) + "\n")

Fehlermeldung aus dem Hauptprogramm lautet:

Code: Alles auswählen

>>> %Run -c $EDITOR_CONTENT
Start
Fertig
Traceback (most recent call last):
  File "<stdin>", line 22, in <module>
  File "<stdin>", line 13, in main
  File "sensor.py", line 23, in __init__
ValueError: 
>>> 


Du verwendest für deinen Code Generatoren und yield. Damit habe ich noch nie gearbeitet. Ich verstehe das Konzept noch nicht.
Ich hoffe erstmal das mit dem deque() zu lösen.

Code: Alles auswählen

    def iir_filter(werte, alpha=0.95):
    werte = iter(werte)
    aktuelles_w = next(werte) / (1 - alpha)
    for signal in werte:
        vorheriges_w, aktuelles_w = aktuelles_w, signal + alpha * vorheriges_w
        yield aktuelles_w - vorheriges_w
        
Benutzeravatar
__blackjack__
User
Beiträge: 13116
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Na das ist ja super von den MicroPython-Leuten gelöst. 🙄 Zählt das als Notwehr wenn man die mal haut?

Bitte einmal in der Dokumentation von MicroPython zu `deque` nachlesen was man da als Argument(e) übergeben kann, beziehungsweise muss.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
DoGro
User
Beiträge: 15
Registriert: Montag 23. Mai 2022, 07:18

ok die Lösung:

man muss liste = deque((), maxlen=5)
erst mit einem Leeren Tuple initialisieren.
dann kann man die liste befüllen.
Jedoch wurde aus der deque funktion die Möglichkeit des Indexieren und des Iterieren raus genommen, dadurch kann ich die Funktion gar nicht gebrauchen.
:(
Also doch mit einer Liste gelöst und da einfach immer mit .pop(0) entfernen.

Schade.
Antworten