SerialPlotter.py und Jitter bei der Datenaufzeichnung.

mit matplotlib, NumPy, pandas, SciPy, SymPy und weiteren mathematischen Programmbibliotheken.
Antworten
RIN67630
User
Beiträge: 91
Registriert: Sonntag 29. April 2018, 08:07

Guten Tag,
wir sind zwei Personen, einen Engländer und ich, die an einem Open-Source Projekt arbeiten.
Es geht darum auf einem Raspberry Pi die Daten die über eine serielle Schnittstelle von einem Arduino (oder etwas anderes) im Sekundentakt eintreffen
1) Als Matplotlib animiertes Plot darzustellen
2) In eine tägliche Log-Datei abzuspeichern.
3) Auf Anforderung die historische Daten aus der Log-Datei zurück zu lesen und wiederum als Plot darstellen zu können.

dafür haben wir zwei Programme erstellt:
1) SerialPlot.py, das die zwei ersten Aufgaben erledigt.
2) LogPlot.py, das die letzte Aufgabe erledigt.

Unser gemeinsames Projekt ist hier bei GitHub gespeichert:
https://github.com/Gordon999/Pi-Arduino_Ser_Plotter

Im Großen und Ganzen funktioniert das, allerdings möchten wir noch ein Problem lösen:
Während der Aufzeichnung der Log-Datei, zeigt die logische Folge der Zeitstempeln starke Unregelmäßigkeiten.
Ich bin der Ursache über mehrere Versuche auf die Schliche gekommen: Es gibt dafür zwei Ursachen.
a) der Takt des Arduinos weicht vom Takt das Raspberry Pi ab: Das ist akzeptabel und verantwortlich für etwa eine regelmäßige Sekundenabweichung alle x Minuten (von Arduino zu Arduino anders)
b) das Aufzeichnen eines Matplotlib-Frame belastet einen Kern des Prozessors zu 90% teilweise länger als 1 s. Während das Aufzeichnen des Frames (im default aus Performancegründen nur alle 10 s) erfolgen die Unregelmäßigkeiten im Zeitstempel.

Kann unser Programm SerialPlotter.Py, das das Plotten und den Rest (Lesen und Loggen) mit Threading versucht gleichzeitig zu erledigen und es offenbar nicht schafft, diesbezüglich optimiert werden?
Ich wäre für konstruktive Hinweisen zur Sache sehr dankbar.
Laszlo

Hinweise:
a) Änderungen auf der Arduino Seite sind (zumindest für meinem Projekt) kaum realisierbar, es gibt schon eine Landschaft von ca. 600 Arduinos, die kaum angepasst werden können.
b) Falls jemand es auch live sehen wollte, gibt es auf unser GitHub Projekt ein Arduino-Sketch Arduino_Data_Simulator.ino, das Gauss-Events alle 2 Minuten generiert, ohne dafür mehr als einen nackten Arduino Nano oder ähnliches zu brauchen.

Danke an allen und bleibt gesund!
Laszlo
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Also 600 Arduinos koennen nicht geupdated werden, aber die Computer an denen die mit einer seriellen Verbindung haengen, bei denen geht das?

Wie dem auch sei: ich habe an anderer Stelle schon erwaehnt, dass matplotlib fuer diese Aufgabe nicht gedacht ist. Andere Frameworks sind fuer Echtzeit-Darstellung besser, und besser heisst hier vor allem schneller. Womit die Probleme schonmal verringert werden.

Und natuerlich kann man auch an eine Trennung von einlesen und verarbeiten umsetzen. Ich wuerde mit einem Thread anfangen, weil das Problem IO-Lastig ist. Wenn es noch genauer sein muss, dann mulitprocessing und einen dezidierten Reader, der NICHTS anderes machn, und die Daten dann per Queue an andere Teile weiterreicht.
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

In SerialPlotter.py werden immer noch globale Variablen verwendet. `animate` muß alles, was es brauchst über Argumente bekommen, auf oberster Ebene sollten nur Konstanten und Definitionen stehen.
Alles unter if-name-main sollte in eine Funktion main wandern und diese Funktion sollte in mehrere Funktionen aufgeteilt werden.
Diese Prüfungen, ob eine korrekte Zeile vom Arduino gelesen wurde, ist sowas von kaputt. Mit der String-Darstellung von Objekten (datetime) sollte man nicht so arbeiten (11:19), sondern Datumsformatierung benutzen.
Dieses Locking ist immer noch kaputt. Wenn man mit mehreren Threads arbeitet, dann lernt erstmal, wie man richtige Kommunikation über verschiedene Kanäle macht. Das was fast immer richtig funktioniert sind Queues.

Wie sehen denn die Zeilen aus, die vom Arduino kommen? Es scheint drei verschiedene Formate zu geben. Solche Nachrichten sind am besten immer mit einer fixen Zeichenzahl kodiert, dann kann man auch einfach fehlerhafte Zeilen erkennen. Eine Zeile sollte immer eine Anfangskennung haben, die sonst nicht auftritt. Z.B. gleich den linetype s,h, oder d als erstes Zeichen jeder Zeile.

Und zum Abschluß noch der Lese-Thread für die Serielle Schnittstelle:

Code: Alles auswählen

import serial
import datetime
import threading
from queue import Queue, Empty

SERIAL_PORT = '/dev/ttyUSB1'

def reading_thread(queue):
    with serial.Serial(port=SERIAL_PORT) as lines
        start_time = datetime.datetime.now()
        for index, line in enumerate(lines):
            timestamp = start_time + datetime.timedelta(seconds=index)
            queue.put((timestamp, line))
            
def main():
    queue = Queue()
    reader = threading.Thread(target=reading_thread, args=(queue,))
    reader.daemon = True
    reader.start()
    while True:
        timestamp, line = queue.get()
        print(timestamp, line)
        
if __name__ == '__main__':
    main()
Das kann man jetzt natürlich noch dynamisch ausbauen, dass die wahre Tickrate ermittelt wird und nicht fest 1 Sekunde pro Zeile angenommen wird.
RIN67630
User
Beiträge: 91
Registriert: Sonntag 29. April 2018, 08:07

__deets__ hat geschrieben: Montag 23. März 2020, 09:19 ...Andere Frameworks sind fuer Echtzeit-Darstellung besser, und besser heisst hier vor allem schneller...
Soll ich dabei verstehen, dass du erneut Bokeh empfiehlst?
https://github.com/sebnil/Realtime-Seri ... sing-Bokeh
"getting it to work but performance of bokeh is too slow. not sure..." stimmt mich diesbezüglich nicht besonders optimistisch. Der Autor hat das auch nicht weiterverfolgt...

Falls es nicht Bokeh sein sollte, könntest du bitte präzisieren?
Vielleicht darf ich auch noch erwähnen, dass das ganze auf einen Raspberry Pi ablaufen soll und viele der schnellen Framworks auf einer Hardwareunterstützung wie zum Beispiel OpenGL basieren, die es bisher Standardweise auf dem Raspberry Pi noch nicht gibt.
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

@Sirius3: Wieso nimmst du den timestamp nicht einfach jedes mal? Wir wissen ja schon, dass es eine Drift gibt.

Ja, ich empfehle erneut Bokeh. Oder die Qt charting Loesung. matplotlib ist immer noch die langsamste aller dieser Ansaetze. Denn es braucht immer ALLE Daten, und baut immer ALLES neu auf. Bokeh zB tut das nicht, sondern uebertraegt nur ein diff.

Auf meinem Desktop kann ich mit Bokeh ein paar Tausend Eintraege pro Sekunde visualisieren. Auf einem PI ist das notwendigerweise weniger, aber auf der anderen Seite habt ihr auch ein geringeres Problem. Und das etwas vor 5 Jahren mal so war... tja. Da hat sich ja auch ein bisschen was getan.
RIN67630
User
Beiträge: 91
Registriert: Sonntag 29. April 2018, 08:07

Sirius3 hat geschrieben: Montag 23. März 2020, 09:42
Wie sehen denn die Zeilen aus, die vom Arduino kommen?
Es scheint drei verschiedene Formate zu geben. Solche Nachrichten sind am besten immer mit einer fixen Zeichenzahl kodiert, dann kann man auch einfach fehlerhafte Zeilen erkennen. Eine Zeile sollte immer eine Anfangskennung haben, die sonst nicht auftritt. Z.B. gleich den linetype s,h, oder d als erstes Zeichen jeder Zeile.

Und zum Abschluß noch der Lese-Thread für die Serielle Schnittstelle:...
Die Zeile, die vom Arduino kommen, sind auf GitHub in der Datei 20-03-10.log exemplarisch beigefügt. Dass die Daten nicht so optimal ankommen, wie man sich das wünschen könnte, ist leider eine Gegebenheit.
Es gibt eigentlich nur zwei echte Formate:
h) die Headerzeile, die die Beschreibung der Daten enthält (die wird extrem selten einmal nach dem Restart des Arduinos ausgegeben, im Moment machen wir noch nichts daraus)
d) die Datenzeile (die die Daten im Gleitpunkt-Format enthält, aber für mich de facto nur im nn.nn Format abliefert, getrennt durch Leerzeichen. Das Programm sollte allerdings nicht darauf bestehen, andere Formate müssen auch funktionieren.
das dritte wären fehlerhaften oder unvollständige Zeilen...
Ich kann nicht erkennen, was an der Identifizierungsroutine kaputt sein sollte.

Das Format ist das Format des eingebauten SerialPlotter des Arduino IDE.

Danke für deine Code Zeilen Lese-Thread für die Serielle Schnittstelle, das muss ich mir mal in Ruhe anschauen, und weil das Material zur Queues einstudieren. Mühsam ernährt sich das Eichhörnchen.
RIN67630
User
Beiträge: 91
Registriert: Sonntag 29. April 2018, 08:07

__deets__ hat geschrieben: Montag 23. März 2020, 10:29 ...Auf meinem Desktop kann ich mit Bokeh ein paar Tausend Eintraege pro Sekunde visualisieren...
"Einträge" oder Frames?
Ein Frame besteht bei uns aus 360*5 Werte = 1800 Werte + die Interpolation zwischen den Punkten. Da alles nach links ohne Hardwareunterstützung gerückt werden muss, kommt schon einiges zusammen.
RIN67630
User
Beiträge: 91
Registriert: Sonntag 29. April 2018, 08:07

Sirius3 hat geschrieben: Montag 23. März 2020, 09:42
Und zum Abschluß noch der Lese-Thread für die Serielle Schnittstelle:
Das kann man jetzt natürlich noch dynamisch ausbauen, dass die wahre Tickrate ermittelt wird und nicht fest 1 Sekunde pro Zeile angenommen wird.
Danke. Zuerst den Flüchtigkeitsfehler korrigiert und nach "with serial.Serial(port=SERIAL_PORT) as lines" noch ein ":" hinzugefügt.
Dann doch gleich dynamisch...

Code: Alles auswählen

import serial
import datetime
import threading
from queue import Queue, Empty

SERIAL_PORT = '/dev/ttyUSB1'

#serial_line = [str]

def reading_thread(queue):
    with serial.Serial(port=SERIAL_PORT) as lines:
        #start_time = datetime.datetime.now()
        for index, line in enumerate(lines):
            #timestamp = start_time + datetime.timedelta(seconds=index)
            timestamp = datetime.datetime.now()
            queue.put((timestamp, line))
            
def main():
    queue = Queue()
    reader = threading.Thread(target=reading_thread, args=(queue,))
    reader.daemon = True
    reader.start()
    while True:
        timestamp, line = queue.get()
        print(timestamp, line)
        
if __name__ == '__main__':
    main()
Zuletzt geändert von RIN67630 am Montag 23. März 2020, 11:48, insgesamt 1-mal geändert.
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

@__deets__: der Takt, mit denen die Daten kommen, wird vom Arduino vorgegeben. Wie schon gesagt, braucht das wahrscheinlich noch eine Offset- und Steigungskorrektur. Damit ist man unabhängig, zu welchem Zeitpunkt die Daten tatsächlich gelesen werden.
RIN67630
User
Beiträge: 91
Registriert: Sonntag 29. April 2018, 08:07

Sirius3 hat geschrieben: Montag 23. März 2020, 11:46 @__deets__: der Takt, mit denen die Daten kommen, wird vom Arduino vorgegeben. Wie schon gesagt, braucht das wahrscheinlich noch eine Offset- und Steigungskorrektur. Damit ist man unabhängig, zu welchem Zeitpunkt die Daten tatsächlich gelesen werden.
Der Drift ist normal und die dadurch verursachten Sekundensprünge müssen sogar beibehalten werden. Es gilt aber die Daten möglichst unabhängig von der Belastung durch Matplotlib einzulesen...
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Sirius3 hat geschrieben: Montag 23. März 2020, 11:46 @__deets__: der Takt, mit denen die Daten kommen, wird vom Arduino vorgegeben. Wie schon gesagt, braucht das wahrscheinlich noch eine Offset- und Steigungskorrektur. Damit ist man unabhängig, zu welchem Zeitpunkt die Daten tatsächlich gelesen werden.
Klar, aber wenn alles gut laeuft, bekommst du mit der direkten Ermittlung eines Zeitstempels einen guten wert. Klar ist dein Ansatz besser, wenn es mal nicht gut laeuft, aber nur zu Beginn. Ich wuerde eher ersteres machen. Mit Drift-Kompensation wird's natuerlich dann eh egal irgendwann.
RIN67630
User
Beiträge: 91
Registriert: Sonntag 29. April 2018, 08:07

Das ist OK und soll nicht kompensiert werden:

2020-03-23 12:29:44.960677 b'34.53 34.12 34.39 32.51 37.56\r\n'
2020-03-23 12:29:45.964318 b'34.53 34.02 34.16 31.66 39.06\r\n'
2020-03-23 12:29:46.968133 b'34.53 34.66 34.46 31.87 39.60\r\n'
2020-03-23 12:29:47.971770 b'34.53 34.66 34.58 31.87 37.77\r\n'
2020-03-23 12:29:48.975348 b'34.53 35.95 35.40 34.12 38.84\r\n'
2020-03-23 12:29:49.979028 b'34.53 37.45 36.64 33.16 42.71\r\n'
2020-03-23 12:29:50.982670 b'34.53 36.38 36.48 32.94 40.45\r\n'
2020-03-23 12:29:51.986406 b'34.53 36.59 36.55 34.98 38.63\r\n'
2020-03-23 12:29:52.989921 b'34.53 35.52 35.93 32.19 38.84\r\n'
2020-03-23 12:29:53.993665 b'34.53 34.02 34.78 32.19 37.77\r\n'
2020-03-23 12:29:54.997257 b'34.53 34.23 34.45 31.87 37.34\r\n'
2020-03-23 12:29:56.000648 b'34.53 34.45 34.45 32.30 38.31\r\n'

2020-03-23 12:29:57.004311 b'34.53 34.34 34.38 31.87 37.34\r\n'
2020-03-23 12:29:58.007950 b'34.53 34.45 34.42 32.41 38.63\r\n'
2020-03-23 12:29:59.011582 b'34.53 34.55 34.50 32.19 38.09\r\n'
2020-03-23 12:30:00.015255 b'34.53 34.45 34.47 31.66 38.63\r\n'
2020-03-23 12:30:01.018960 b'34.53 34.34 34.39 31.66 37.77\r\n'
2020-03-23 12:30:02.022707 b'34.53 35.09 34.81 32.19 38.31\r\n'
2020-03-23 12:30:03.026511 b'34.53 34.12 34.40 31.87 38.84\r\n'
RIN67630
User
Beiträge: 91
Registriert: Sonntag 29. April 2018, 08:07

...eigentlich wenn ich diese Werte mit nur einen angepassten Timestamp (einmal mit nur die Uhrzeit damit ein Mensch es auch lesen kann, und zusätzlich als Epoch, um maschinell schneller suchen zu können) direkt im Log abspeichere, wäre der Log quasi-perfekt.

Aber welche Möglichkeit hätte ich dann, im zweiten (nice) Python Skript den Log in zeitnahe Matplotlib Darstellung mit animate zurück zu lesen, ohne in Konflikt mit der Speicherung zu kommen?
Benutzeravatar
__blackjack__
User
Beiträge: 13069
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@RIN67630: Ich würde mit für die Live-Darstellung ja von der Vorstellung verabschieden das aus einer Text-Logdatei zu lesen. Dafür gibt es spezialisierte Datenbanken wie RRDTool, InfluxDB, …
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Was an Sirius3 prinzipiellem Vorgehen als ersten Versuch passt dir nicht? Und die weitergehenden Alternativen habe ich doch auch schon benannt. Schon probiert? (ich rede nicht vom visualisieren).
RIN67630
User
Beiträge: 91
Registriert: Sonntag 29. April 2018, 08:07

__deets__ hat geschrieben: Montag 23. März 2020, 13:17 Was an Sirius3 prinzipiellem Vorgehen als ersten Versuch passt dir nicht? Und die weitergehenden Alternativen habe ich doch auch schon benannt. Schon probiert? (ich rede nicht vom visualisieren).
Ich finde die Vorgehensweise mit Pipes toll.
Allerdings muss man dabei die zwei Prozesse der Generierung der Log-Datei und der Plot-Darstellung im gleichen Pythonskript komplett voneinander trennen und klar priorisieren, damit die Erstellung der Log-Datei nicht von eine langsame Plotdatei impaktiert wird.
Das kann ich zur Zeit nicht. Andererseits wird der Prozess der Logdateigenerierung ständig 24/24/365 laufen.
Die Visualisierung erfolgt ja nicht ständig, sondern meistens nur wenn ein Mensch sich dafür interessiert.
Liefe das über einen separaten Prozess, der nur bei Bedarf aufgerufen wird, wäre das sicherlich wesentlich günstiger. Dann ist es auch völlig Schnuppe wie schnell oder wie langsam das stattfindet.
__blackjack__ hat geschrieben: Montag 23. März 2020, 13:05 @RIN67630: Ich würde mit für die Live-Darstellung ja von der Vorstellung verabschieden das aus einer Text-Logdatei zu lesen. Dafür gibt es spezialisierte Datenbanken wie RRDTool, InfluxDB, …
Ich kann mich immer mehr mit der Idee der Datenbank anfreunden. Sie würde sicherlich auch mit der große Menge Daten, die so über ein Jahr anfällt, erheblich besser wirtschaften.
Ich muss mich dann nur von der Vorstellung, Daten in eine direkt lesbare Form abzuspeichern verabschieden und dann ein weiteres Programm, das den SQL Report auch für Laien unterstützt, schreiben.
Hast du eine Erfahrung, welche Datenbank hier auf einem Raspberry Pi für diese Aufgabe am geeignetesten wäre? Oder soll ich mal auf dem Form Raspberry Pi nachfragen?
Wir hätten wahrscheinlich im Hauptspeicher etwa 100MB dafür übrig, gegebenenfalls mehr, wenn man einige der vorhandenen Raspberry Pi gegen neueren Modellen austauscht.
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich denke du bist da schon einen Schritt zu weit. Die Loesung von Sirius3 stellt durch multi-threading doch schonmal eine Entkopplung vom Rendering dar. Ist da der Jitter trotzdem zu gross?
RIN67630
User
Beiträge: 91
Registriert: Sonntag 29. April 2018, 08:07

__deets__ hat geschrieben: Montag 23. März 2020, 16:31 Ich denke du bist da schon einen Schritt zu weit. Die Loesung von Sirius3 stellt durch multi-threading doch schonmal eine Entkopplung vom Rendering dar. Ist da der Jitter trotzdem zu gross?
So schnell bin ich nicht, dass ich ohne langes Nachschlagen, Suchen, Testen das umschreiben könnte.

Ich stelle allerdings fest dass die jetzige Lösung mehr Code zur Beseitigung von Nebeneffekten enthält, als operatives.

Mir gefällt einer Datenbank basierte Lösung von Stunde zu Stunde besser.
Ich bin mir ziemlich sicher, dass wir damit eine viel klarer und effizientere Lösung erreichen können.
a) ein Skript zum auffüllen der Datenbank, der ständig läuft
b) ein Skript zum auslesen der Datenbank und plotten der Ergebnisse nach Datum/Uhrzeit
c) ein Skript zum auslesen der Datenbank und laufendes Plotten der Ergebnisse in quasi-Echtzeit
d) ein Skript zum Abrufen eines Reports nach Gusto.

Zu 99 % der Zeit wird nur a) benötigt und das entlastet das System.
Ich denke die Skripts werden dabei als separate Aufgaben wesentlich schlanker und einfacher, wahrscheinlich kommt man dabei völlig ohne Threading durch.

Ich möchte jetzt nur keinen Fehler bei der Auswahl der Datenbank machen, wobei das auch nicht schlimm wäre, Datenbanken sie ja austauschbar.
Die Datenbank sollte allerdings auf die Besonderheiten vom Raspberry Pi zugeschnitten werden: Begrenzte Ressourcen und Speicherung auf SD Karte.

Last but not least: b) und c) könnte man auf einen vernetztes PC, mit wesentlich mehr power ablaufen lassen.
Gegebenenfalls auch die ganz große Cloud Lösung, wo eine zentrale Datenbank alle Daten der 600 Stationen vernetzt verwaltet, einsetzen.
Da wird einiges möglich...
Die MQTT Lösungen auf dem Markt sind meistens eher für Clients mit weniger Datenmengen ausgelegt, das hatte ich auch schon in Betracht genommen.
Benutzeravatar
__blackjack__
User
Beiträge: 13069
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@RIN67630: SQL gibt's bei den von mir genannten Datenbanken nicht. Die sind auf Zeitreihen und Darstellungen spezialisiert. Man könnte natürlich auch eine SQL-Datenbank nehmen.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
RIN67630
User
Beiträge: 91
Registriert: Sonntag 29. April 2018, 08:07

Danke.
InfluxDB und Grafana scheint es für den Raspberry Pi zu geben.
Es sieht so aus, als wäre Grafana ein ziemliches Tausendsassa. Wie sich das auf dem schwachbrüstige Pi macht?
Ob Telegraf eine serielle Schnittstelle nach dem Arduino Serial Plotter standard erfassen kann, müsste ich noch schauen.
Wenn es Lösungen ohne Python gibt, bin ich auch nicht böse...
Antworten