Programm priorisieren

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
m4tthi45
User
Beiträge: 2
Registriert: Montag 3. August 2020, 11:36

Hallo liebe Community,
ich habe zurzeit ein Programm laufen, dass Messwerte aufnimmt und auch abspeichert. Mein Problem ist, dass teilweise zwischen den Zeilen kleinere Zeitsprünge (~0,1) Sekunden. Da meine Messwerte normalerweiser mit einer Frequenz von 300 Hz aufgenommen werden ist dies schon ein Problem.
Zwischen den schreiben der Zeilen habe ich folgende Zeile stehen, damit das Abspeichern der Zeiten in regelmäßigen Abständen passiert:

Code: Alles auswählen

sleep((1/Datenrate)-((time.time() - starttime) % (1/dataRate)))
Ich gehe davon aus, dass diese Zeitsprünge passieren, weil zwischendurch etwas vom Betriebssystem priorisiert wird und dadurch auf den nächsten Zyklus gewartet wird. Weshalb diese Zeitsprünge jedoch so groß sind kann ich mir nicht erklären.
Deshalb wollte ich fragen ob es einen Weg gibt, dem Programm eine höhere Priorität zuzuordnen. Das Programm wird automatisch mittels Timer Unit beim Starten des RPi gestartet.

Weiß jemand von euch was ich dagegen machen kann? Hier noch mein ganzes Programm:

Code: Alles auswählen

#Einbinden der genutzten Bibliotheken
import serial, sys, struct, fcntl, errno, traceback
import struct
import RPi.GPIO as GPIO
import csv                                                                              # Anpassung DP
from time import strftime, sleep
from os import system
import time
from datetime import datetime
import referenz2
 
def f_MesswertEmpfangen():
    messwerte = ("leer",)
    MW_Erhalten = False
    ZeitpunktMWEnde = ("leer")
    # Auslesen des Praefix-Bytes
    praefix = verbindung.readline(1)
    praefixUSB = verbindungUSB.readline(1)
    praefix = praefix.encode("hex")
    praefixUSB = praefixUSB.encode("hex")
    #Auswertung der Messwerte
    if praefix == "aa":
        while True:
                praefixUSB = verbindungUSB.readline(1)
                praefixUSB = praefixUSB.encode("hex")
                if praefixUSB == "aa":              
                    identifier = verbindung.readline(1)
                    identifierUSB = verbindungUSB.readline(1)
                    identifier = identifier.encode("hex")
                    identifierUSB = identifierUSB.encode("hex")
                    break   
        if identifier == "15" and identifierUSB == "15":
            rest = verbindung.read(26)
            restUSB = verbindungUSB.read(26)
            rest = rest.encode("hex")
            restUSB = restUSB.encode("hex")
            Antwort = praefix + identifier + rest
            AntwortUSB = praefixUSB + identifierUSB + restUSB
            if len(Antwort) == 56 and len(AntwortUSB) == 56:
                ZeitpunktMWAnfang = (datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S.%f'))
                ch1 = byte_to_value(Antwort[6:14], "float")
                ch2 = byte_to_value(Antwort[14:22], "float")
                ch3 = byte_to_value(Antwort[22:30], "float")
                ch4 = byte_to_value(Antwort[30:38], "float")
                ch5 = byte_to_value(Antwort[38:46], "float")
                ch6 = byte_to_value(Antwort[46:54], "float")
                ch7 = byte_to_value(AntwortUSB[6:14], "float")
                ch8 = byte_to_value(AntwortUSB[14:22], "float")
                ch9 = byte_to_value(AntwortUSB[22:30], "float")
                ch10= byte_to_value(AntwortUSB[30:38], "float")
                ch11= byte_to_value(AntwortUSB[38:46], "float")
                ch12= byte_to_value(AntwortUSB[46:54], "float")
                messwerte = ((ZeitpunktMWAnfang, ch1,ch2,ch3,ch4,ch5,ch6,ch7,ch8,ch9,ch10,ch11,ch12),) 
                MW_Erhalten = True
                ZeitpunktMWEnde = (datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S.%f'))
    return MW_Erhalten, messwerte, ZeitpunktMWEnde       
 
def antwort_empfangen(verbindung):
    var_routine = True
    while (var_routine == True) :
        praefix = verbindung.readline(1)                                # lese praefix
        praefix = praefix.encode("hex")                                    # string to hexstring
        if praefix == "aa":
            identifier = verbindung.readline(1)                         # lese identifier
            identifier = identifier.encode("hex")                       # identifier: string to hexstring
            if (identifier != 0x15):                                    # Identifier vergleichen
                rest = verbindung.readline(int(identifier)-0x50+2)      # lese Rest des strings
                rest = rest.encode("hex")                               # Rest: string to hexstring
                Antwort = praefix + identifier + rest                   # string zusammensetzen
                var_routine = False
    return Antwort
 
def byte_to_value(daten, typ):
    if typ == "float":                          # uint 32 Byte
        Multiplikator = [268435456,16777216,1048576,65536,4096,256,16,1]
    MW_list = []
    for i in range(len(Multiplikator)):
        MW_list.append(int(daten[i:(i+1)], 16))
    MW = 0
    for k in range(len(Multiplikator)):
        MW += ( MW_list[k] * Multiplikator[k] )
    if typ == "float":
        MW = struct.pack("I", MW)
        MW = struct.unpack("f", MW)[0]
    return MW

if __name__ == '__main__':
    GPIO.setmode(GPIO.BOARD)  
    GPIO.setup(13, GPIO.IN, pull_up_down = GPIO.PUD_DOWN) 
    GPIO.setup(12, GPIO.OUT)
    GPIO.output(12,0)
    starttime = time.time()
    # Verbindung UART-Shield
    verbindung = serial.Serial('/dev/ttyAMA0', 230400, timeout=1)
    verbindung.isOpen()
    #Verbindung zu USB Schittstelle
    verbindungUSB = serial.Serial('/dev/ttyUSB0', 230400, timeout=1)
    verbindungUSB.isOpen()
    sleep(0.5)
    
    verbindung.write(chr(0xAA)+chr(0x90)+chr(0x23)+chr(0x85))   # stop transmission
    Antwort = antwort_empfangen(verbindung)
    print "serielle Verbindung aufgebaut"
    verbindungUSB.write(chr(0xAA)+chr(0x90)+chr(0x23)+chr(0x85))
    AntwortUSB = antwort_empfangen(verbindungUSB)
    print "USB Verbindung aufgebaut"
    sleep(0.5)
     
    dataRate = 333.0  #Datenrate des USV-Shields einstellen
    verbindung.write(referenz2.Befehl(verbindung,"WriteDataRate", [dataRate]))
    Antwort = antwort_empfangen(verbindung)
    verbindungUSB.write(referenz2.Befehl(verbindungUSB,"WriteDataRate", [dataRate]))
    AntwortUSB = antwort_empfangen(verbindungUSB)
    
    verbindung.write(chr(0xAA)+chr(0x90)+chr(0x78)+chr(0x85)) #Reset Device
    sleep(0.5)
    verbindungUSB.write(chr(0xAA)+chr(0x90)+chr(0x78)+chr(0x85))
    sleep(0.5)
    verbindung.write(chr(0xAA)+chr(0x90)+chr(0x23)+chr(0x85))
    Antwort = antwort_empfangen(verbindung)
    sleep(0.5)
    verbindungUSB.write(chr(0xAA)+chr(0x90)+chr(0x23)+chr(0x85))
    AntwortUSB = antwort_empfangen(verbindungUSB)
      
    verbindung.write(chr(0xAA)+chr(0x90)+chr(0x24)+chr(0x85))   # start transmission
    verbindungUSB.write(chr(0xAA)+chr(0x90)+chr(0x24)+chr(0x85))   
    print("Start der Messung:" + (datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')))
    print ("Messung gestartet")
    GPIO.output(12,1)  #Kontroll LED einschlten
    try:
        while True:
            dateiname = ("/media/pi/MP/MessungUSB/" +datetime.now().strftime('%Y%m%d')+"_meas_"+datetime.now().strftime('%H%M')  + ".csv")     
            system('touch '+ dateiname)  #Datei erstellen
            system('sudo rm ' + dateiname)    
            csv_file = open(dateiname,'a')    #Datei beschreiben
            Zaehler_Zeilen = 0        
            while Zaehler_Zeilen < 500000:   #Maximalzahl der Zeilen festlegen
                bool_MW_Erhalten = False
                bool_MW_Erhalten, float_Messwerte, string_ZeitpunktMWENDE = f_MesswertEmpfangen()
                if bool_MW_Erhalten == True:                          # wenn ein Messwert empfangen wurde wird eine Zeile in der .csv Datei beschrieben
                        for row in float_Messwerte[0]:
                           csv_file.write(str(row)+";")
                        csv_file.write(string_ZeitpunktMWENDE)
                        csv_file.write("\n")
                        sleep( (1/dataRate) - ((time.time() - starttime) % (1/dataRate)))
                if GPIO.input(13)==GPIO.HIGH:
                    raise Exception("Knopfdruck")  #Bei Knopfdruck Exception auswerfen und so Program beenden
                
    except KeyboardInterrupt:
        verbindung.close()  
        verbindungUSB.close() 
        GPIO.output(12,0)   #Kontroll LED ausschalten
        GPIO.cleanup() 
    print "\nProgrammende"

    #sleep(10.0)
    #system('sudo shutdown now') #RPi herunterfahren
Vielen Dank bereits im Vorraus
Mit freundlichen Grüßen
Matthias
Sirius3
User
Beiträge: 18272
Registriert: Sonntag 21. Oktober 2012, 17:20

Eingerückt wird immer mit 4 Leerzeichen pro Ebene, nicht mal 4 und mal 8. Python2 sollte man nicht mehr einsetzen, steige auf Python3 um. (Nebenbei ist in Python2 print keine Funktion, sollte also nicht als solche geschrieben werden)

Importe gehören jeweils in eine eigene Zeile. `sys`, `fcntl`, `errno` und `traceback` werden gar nicht benutzt. `struct` zweimal importiert.
Das `as` ist zum Umbenennen da, GPIO wird aber gar nicht umbenannt.
`csv` wird gar nicht benutzt, sollte aber wohl.
`strftime` wird nicht benutzt.
`system` sollte nicht benutzt werden, statt dessen subprocess.run.
Was steht denn in `referenz2` drin? Der Name ist jedenfalls nicht sehr aussagekräftig.

Funktionsnamen werden wie Variablennamen komplett klein geschrieben. Das unsinnige f_ steht wohl für Funktion, dass es sich um eine Funktion handelt, sollte aber schon aus dem Namen klar sein: messwerte_empfangen.
Fehler in der Funktion sollten nicht über einen boolschen Wert und weiteren unsinnigen Dummy-Werten gekennzeichnet werden, sondern per Exception.
Benutze keine Abkürzungen, wenn Du Megawatt meinst, dann schreib nicht MW.
Alles was eine Funktion braucht, sollte sie über ihre Argumente bekommen, verbindung und verbindungUSB kommen aus dem Nichts.
Es ist unsinnig, Bytewerte in Hex umzuwandeln, um sie später wieder Rückzuwandeln. Dieses ganze Lesen kommt mir auch schon sehr bekannt vor. Statt byte_to_value gibt es was einfaches mit struct.unpack_from.
Das Prefix-Byte wird zweimal von verbindungUSB gelesen. Das ist wohl ein Programmierfehler.
ZeitpunktMWAnfang und ZeitpunktMWEnde enthalten nur die Zeit der Umwandlung in Zahlen, das sollte hoffentlich ziemlich bei 0 Sekunden sein.

In `antwort_empfangen` wird eine unsinnige Variable var_routine definiert. Nicht nur der Name ist schrecklich (var für Variable? Ernsthaft?) sondern sie ist auch ganz unnötig, weil man am besten eine Endlosschleife schreibt, die per break verlassen wird.
Die if-Abfrage mit identifier ist immer wahr, weil ein HexString niemals gleich einer Zahl sein kann.
Ist hier readline wirklich sinnvoll? Glaube nicht.

In `byte_to_value` hast Du das Antipattern über einen Index zu iterieren, statt über die Elemente der Listen direkt (in Kombination mit zip). Aber die Funktion ist, wie oben schon geschrieben, eh überflüssig.

Alles was nach if __name__ == '__main__' kommt muß in eine Funktion, die üblicherweise main genannt wird, dann hat man auch nicht die Probleme, dass messwerte_empfangen einfach so Variablen benutzen kann.
Die isOpen-Aufrufe sind ohne effekt und können weg.
Die Verbindungen sollten immer aufgeräumt werden, am besten in Kombination mit dem with-Statement. GPIO.cleanup sollte auch immer aufgerufen werden, und zwar mit einem finally-Block.
Im Dateinamen wird zweimal die Aktuelle Uhrzeit ermittelt, das kann dazu führen, dass die Angaben bis zu einem Tag falsch sind. Ermittle nur einmal die Zeit. Warum wird einmal die Lokale und einmal UTC-Zeit genommen?
Eine Datei zu erstellen um sie gleich wieder zu löschen ist unsinnig, warum muß man mit sudo löschen?
Und wenn Du Datei gar nicht haben willst, warum öffnest Du sie dann zum Anhängen?
Die Zaehler_Zeilen-Schleife ist eine Endlosschleife, weil Zaehler_Zeilen nie verändert wird.
Zum Schreiben von csv-Dateien nimmt man das csv-Modul.

Es gibt viele Gründe, warum es zu Pausen kommen kann. Schreiben der Daten, Lesen der Daten von der seriellen Schnittstelle.

Ungetestet:

Code: Alles auswählen

import serial
import struct
from RPi import GPIO
import csv
from time import sleep
from datetime import datetime
import referenz2

DATA_RATE = 333.0  #Datenrate des USV-Shields einstellen
 
def messwert_empfangen(verbindung, verbindung_usb):
    # der Anfang wäre doch am Anfang besser
    zeitpunkt_anfang = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S.%f')
    praefix = verbindung.read(1)
    if praefix != b"\xaa":
        raise ValueError()
    while True:
        praefix_usb = verbindungUSB.read(1)
        if praefix_usb == b"\xaa":
            identifier = verbindung.read(1)
            identifier_usb = verbindung_usb.read(1)
            break   
    if identifier != b"\x15" or identifier_usb != b"\x15":
        raise ValueError()
    rest = verbindung.read(26)
    rest_usb = verbindung_usb.read(26)
    if len(rest) != 26 or len(rest_usb) != 26:
        raise ValueError()
    channels = struct.unpack_from(">6f", rest, 1)
    channels_usb = struct.unpack_from(">6f", rest_usb, 1)
    zeitpunkt_ende = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S.%f')
    return (zeitpunkt_anfang, *channels, *channels_usb, zeitpunkt_ende)
 
def antwort_empfangen(verbindung):
    while True:
        praefix = verbindung.read(1)
        if praefix == b"\xaa":
            identifier = verbindung.read(1)
            if identifier == b"\x15":
                break
    rest = verbindung.read(identifier[0] - 0x50 + 2)
    return praefix + identifier + rest

def main()
    try:
        GPIO.setmode(GPIO.BOARD)  
        GPIO.setup(13, GPIO.IN, pull_up_down = GPIO.PUD_DOWN) 
        GPIO.setup(12, GPIO.OUT)
        GPIO.output(12,0)
        starttime = time.time()
        # Verbindung UART-Shield
        verbindung = serial.Serial('/dev/ttyAMA0', 230400, timeout=1)
        #Verbindung zu USB Schittstelle
        verbindung_usb = serial.Serial('/dev/ttyUSB0', 230400, timeout=1)
        sleep(0.5)
        verbindung.write(b"\xAA\x90\x23\x85")   # stop transmission
        _ = antwort_empfangen(verbindung)
        print("serielle Verbindung aufgebaut")
        verbindung_usb.write(b"\xAA\x90\x23\x85")   # stop transmission
        _ = antwort_empfangen(verbindung_usb)
        print("USB Verbindung aufgebaut")
        sleep(0.5)
     
        # TODO: find better names for referenz2 and Befehl.
        verbindung.write(referenz2.Befehl(verbindung, "WriteDataRate", [DATA_RATE]))
        _ = antwort_empfangen(verbindung)
        verbindung_usb.write(referenz2.Befehl(verbindung_usb, "WriteDataRate", [DATA_RATE]))
        _ = antwort_empfangen(verbindung_usb)

        verbindung.write(b"\xAA\x90\x78\x85")   # Reset Device
        verbindung_usb.write(b"\xAA\x90\x78\x85")
        sleep(0.5)
        verbindung.write(b"\xAA\x90\x23\x85")   # stop transmission
        _ = antwort_empfangen(verbindung)
        verbindung_usb.write(b"\xAA\x90\x23\x85")   # stop transmission
        _ = antwort_empfangen(verbindung_usb)
    
        verbindung.write(b"\xAA\x90\x23\x85")   # start transmission
        verbindung_usb.write(b"\xAA\x90\x24\x85")
      
        print(f"Start der Messung: {datetime.now():%Y-%m-%d %H:%M:%S.%f}")
        print("Messung gestartet")
        GPIO.output(12,1)  #Kontroll LED einschlten
        while True:
            now = datetime.now()
            dateiname = f"/media/pi/MP/MessungUSB/{now:%Y%m%d}_meas_{now:%H%M}.csv"
            with open(dateiname, 'w') as csv_file:
                output = csv.writer(csv_file)
                while True:
                    try:
                        messwerte = messwert_empfangen(verbindung, verbindung_usb)
                    except ValueError:
                        # Messfehler ignorieren
                        pass
                    else:
                        output.write(messwerte)
                        sleep( (1/DATA_RATE) - ((time.time() - starttime) % (1/DATA_RATE)))
                # TODO: never reached
                if GPIO.input(13) == GPIO.HIGH:
                    # bei Knopfdruck main verlassen
                    return
    finally:
        verbindung.close()  
        verbindung_usb.close() 
        GPIO.output(12,0)   #Kontroll LED ausschalten
        GPIO.cleanup() 

if __name__ == '__main__':
    main()
Benutzeravatar
__blackjack__
User
Beiträge: 14052
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Sirius3 hat ja bereits die Verwendung von `readline()` hinterfragt. Ich würde sagen das ist nicht nur ziemlich schräg und nicht sinnvoll, sondern mindestens an einer Stelle wirklich falsch, weil es zu nicht komplett empfangenen Daten führt wenn innerhalb der Float-Daten zufällig ein Byte vorkommt, das ein Zeilenende markiert.

`readline()` liest nicht die angegebene Anzahl an Bytes, sondern *maximal* die angegebene Anzahl, falls vorher die *Textzeile* nicht zuende ist. Der Code liest aber gar keine Textzeilen sondern Binärdaten, und da kann das ”Zeilenende” in den Daten vorkommen und etwas anderes bedeuten.
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
nezzcarth
User
Beiträge: 1764
Registriert: Samstag 16. April 2011, 12:47

An der Uni bin ich damals Messgeräten begegnet, deren Auswertesoftware eben genau deswegen nicht unter Windows/Linux sondern "bare metal" bzw. unter irgendeinem DOS-artigen RTOS lief. Ein unmodifizierter Linuxkernel ist nicht für Echtzeitanforderungen ausgelegt. Mit dem Pi+Standard-Linux+Python bist du m.M.n. mehrere Level über dem, wo du so etwas im Details beeinflussen kannst. Entweder du lebst mit den leichten Schwankungen oder du musst einen ganz anderen Ansatz wählen.
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Auch wenn nezzcarth prinzipiell recht hat mit seiner Bemerkung zur echtzeitfähigkeit von Linux im allgemeinen, konkret ist das hier IMHO kein Problem. Denn wenn du weißt, dass du alle 1/300s ein Datum bekommst, bist du durch die Pufferung durch die serielle Schnittstelle hier auf der sicheren Seite. Einfach gemacht kannst du also pro gelesenem Datum einfach die Periode auf den Zeitstempel addieren, und alles ist gut. Sollte die Messung allerdings sehr lange gehen, bekommst du eine drift zwischen wallclock und Ereignissen. Wenn das ein Problem darstellt, musst du die Uhrzeit Filtern. Du nimmst dazu einen Zeitstempel wann immer du wirklich Daten bekommst, und bildest die Differenz zum letzten gelesenen Zeitpunkt. Die Zeitdeltas kannst du dann unterschiedlich aufwändig verarbeiten, zb einfach den Median nehmen, einen Durchschnitt bilden, einen IIR-Filter benutzen, einen alpha-beta-Filter, etc.
Antworten