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
SerialPlotter.py und Jitter bei der Datenaufzeichnung.
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.
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.
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:
Das kann man jetzt natürlich noch dynamisch ausbauen, dass die wahre Tickrate ermittelt wird und nicht fest 1 Sekunde pro Zeile angenommen wird.
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()
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.
@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.
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.
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.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:...
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.
"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.
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.
@__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...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.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.
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'
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'
...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?
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?
- __blackjack__
- User
- Beiträge: 13919
- 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, …
“Java is a DSL to transform big Xml documents into long exception stack traces.”
— Scott Bellware
— Scott Bellware
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.
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.__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 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.
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.
- __blackjack__
- User
- Beiträge: 13919
- 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.
“Java is a DSL to transform big Xml documents into long exception stack traces.”
— Scott Bellware
— Scott Bellware
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...
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...