Daten über serielle einlesen und auf Datei speichern

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
mega-hz
User
Beiträge: 14
Registriert: Sonntag 24. September 2017, 13:48

Hallo,

Ich habe eine kleine Haussteuerung mit mehreren Arduinos und CAN Bus Knoten am laufen.
Nun möchte ich per CAN-RS232 Daten per Python auf meinem (debian) Server regelmäßig einlesen und in einer Datei speichern.

Folgende kleine Routine habe ich mir zusammengesucht: (noch nicht getestet)

Code: Alles auswählen

import serial   #Serielle Schnittstelle klarmachen

file_out = open("rs232_daten.txt","w")

ser = serial.Serial('/dev/tty0', 19200)        #Serielle Schnittstelle öffnen
ser.flushInput()                                         #Input Puffer leeren

while True:                                               #abfrage schleife
    if (ser.inWaiting() >0):                            # wenn was drinne ist (imPuffer) dann...
        line = ser.readline()                            # lese die Zeichen
        print line                                           #Stelle das ausgelesene sichtbar da.
        file_out.write(line)

ser.close()                                                 #schliesse serielle Schnittstelle
file_out.close()
Nun möchte ich, daß die Datei nicht immer überschrieben sondern angehängt wird, wie macht man das?

Ziel ist es, später mit einem ChartViewer oder auch Excel die Daten als Statistik anzeigen lassen zu können.
Auch soll als Datensatz-Anfang das Datum und die Uhrzeit drinstehen, als Datensatz-Ende dann ein definiertes Zeichen, z.B. #
Sollte man die serielle vielleicht nur einmal öffnen und dann nur einlesen?
Daten kommen im 30Sekunden Takt.

Gruß,
Wolfram.
Benutzeravatar
noisefloor
User
Beiträge: 3854
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

der Modus zum anhängen ist `a` (statt `w`) - das ist aber auch in der Doku ziemlich einfach zu finden.

Bzgl. der Dateistruktur klingt es so, als solltest du besser direkt eine CSV-Datei schreiben, keine nackte Text-Datei. Bzw. wenn du jeden Datensatz in eine neue Zeile schreibst, dann kannst du dir auch so komische Sachen wie `#` am Zeilenende sparen.

Beim aktuellen Code wird übrigens weder die serielle Schnittstelle noch die Datei geschlossen, weil der Code nach der while-Schleife nicht ausgeführt wird. Hier solltest du besser das with-Statement nutzen - dann werden Datei und Schnittstelle automatisch geschlossen, wenn das Programm beendet wird.

Gruß, noisefloor
__deets__
User
Beiträge: 14525
Registriert: Mittwoch 14. Oktober 2015, 14:29

Statt in der dauerschleife zu Pollen solltest du einfach direkt readline aufrufen. Das blockiert cpu schonend bis die Daten da sind.

Und was die Speicherung angeht würde ich dir einen Blick auf Rdd Tool empfehlen. Damit kannst du dann gleich deine Visualisierung bauen. Wenn es unbedingt selbstgebaut sein muss, nimm besser SQLite. Darauf kannst du bequem konkurrierend zugreifen, und auch zb aufräumen.
mega-hz
User
Beiträge: 14
Registriert: Sonntag 24. September 2017, 13:48

ich habe nun erstmal das mit der Datei hinbekommen:

Code: Alles auswählen

import serial
from datetime import datetime
from random import uniform
from random import randint
from pprint import pprint


Data = []


pprint(Data)
print('--------------------------------')


with open('homesys.csv', 'a') as File:
    File.write(datetime.now().strftime('%d-%m-%Y-%H-%M-%S'))
    File.write(",wert1,")
    File.write("wert2")

Data = []

with open('homesys.csv', 'r') as File:
    Data = File.read()

pprint(Data)
Da ich aber ziemlicher Anfänger in Python bin, bitte ich um Eure Hilfe..

funktioniert das mit dem with open auch genauso mit der seriellen?
Ist die Schreibweise die gleiche?

"with open('tty0 ... " ?

Und wie bekomme ich ein LF/CR beim File hin?

Rdd Tool schau ich mir morgen mal an.
__deets__
User
Beiträge: 14525
Registriert: Mittwoch 14. Oktober 2015, 14:29

Für die serielle Schnittstelle sollte

Code: Alles auswählen

with serial.Serial(...) as conn:
     .....
funktionieren.

Und CR/LF ist ein String mit „\r\n“ darin. CR ist aber ungewöhnlich unter UNIX.
mega-hz
User
Beiträge: 14
Registriert: Sonntag 24. September 2017, 13:48

danke sehr, das hat mich etwas weitergebracht!

Mein Listing sieht nun so aus:

Code: Alles auswählen

import serial
from datetime import datetime


Data = []


print(Data)
print('SERIELL: --------------------------------')

with serial.Serial('/dev/ttyUSB0', 19200) as ser:
    ser.flushInput()
    line = ser.readline()
    print line

with open('homesys.csv', 'a') as File:
    File.write(datetime.now().strftime('%d-%m-%Y-%H-%M-%S'))
    File.write(",")
    File.write(line)

Data = []

with open('homesys.csv', 'r') as File:
    Data = File.read()

print('File: --------------------------------')
print(Data)


an der seriellen /dev/ttyUSB0 hängt nun ein Arduino dran der alle 3 Sekunden Zufallsdaten schickt

Code: Alles auswählen

void setup()
{
  Serial.begin(19200);

}
  void loop()
  {
    Serial.print("Test ");
    Serial.print(random(0, 255));
    Serial.print(",");
    Serial.print(random(0, 255));
    Serial.print(",");
    Serial.print(random(0, 255));
    Serial.print(",");
    Serial.print(random(0, 255));
    Serial.print(",");
    Serial.print(random(0, 255));
    Serial.print(",");
    Serial.print(random(0, 255));
    Serial.print(",");
    Serial.print(random(0, 255));
    Serial.print(",");
    Serial.print(random(0, 255));

    Serial.println();

    delay(3000);

  }
Nun soll das Python-Script aber immer einlesen, wie mach ich das am Besten?
Und was mir noch aufgefallen ist: ich bekomme ständig Fehlermeldungen wegen einem /xc3 Zeichens, wenn ich mit # Remarks einfügen möchte!

Wenn nun sagen wir mal alle 5 Sekunden 128 Bytes über die Serielle kommen plus dem Timestamp = 148 Bytes dann hätte ich ja
148 * 12 = 1776 Bytes / Minute, * 60 = 106560 Bytes / Std., * 24 = 2557440 Bytes / Tag, * 365 = 933465600 Bytes / Jahr bzw. 890 MByte.

Geht das dann überhaupt noch mit dem fileopen und append?

Oder kann das RDD diese Daten verarbeiten? (Hab nur mal grob drübergeschaut, sieht exakt aus, als wäre das das richtige!)

Kann jemand der sich mit RDD-aufsetzen und einrichten auskennt da helfen?

Vielen Dank für die wertvollen Tips!

EDIT: ich sehe gerade, daß ich immer die gleichen Daten bekomme obwohl der Arduino Zufallswerte schickt!?!

Code: Alles auswählen

25-09-2017-11-37-19,Test 232,19,158,38,175,197,114,68
25-09-2017-11-52-35,Test 232,19,158,38,175,197,114,68
25-09-2017-11-52-38,Test 232,19,158,38,175,197,114,68
25-09-2017-11-52-45,Test 232,19,158,38,175,197,114,68
25-09-2017-11-52-48,Test 232,19,158,38,175,197,114,68
25-09-2017-11-52-51,Test 232,19,158,38,175,197,114,68
25-09-2017-11-52-54,Test 232,19,158,38,175,197,114,68
25-09-2017-11-52-57,Test 232,19,158,38,175,197,114,68
25-09-2017-11-54-14,Test 232,19,158,38,175,197,114,68
25-09-2017-11-54-55,Test 188,109,120,80,102,47,102,1Test 232,19,158,38,175,197,114,68
25-09-2017-11-54-58,Test 232,19,158,38,175,197,114,68
25-09-2017-11-55-01,Test 232,19,158,38,175,197,114,68
25-09-2017-11-55-05,Test 232,19,158,38,175,197,114,68
25-09-2017-11-55-43,Test 188,109,120,80,102,47,102,1Test 232,19,158,38,175,197,114,68
25-09-2017-11-55-46,Test 232,19,158,38,175,197,114,68
25-09-2017-11-55-49,Test 232,19,158,38,175,197,114,68
25-09-2017-11-55-52,Test 232,19,158,38,175,197,114,68
25-09-2017-11-56-31,Test 188,109,120,80,102,47,102,1Test 232,19,158,38,175,197,114,68
25-09-2017-11-56-35,Test 232,19,158,38,175,197,114,68
25-09-2017-11-58-23,Test 232,19,158,38,175,197,114,68
25-09-2017-11-58-48,Test 232,19,158,38,175,197,114,68
25-09-2017-11-58-53,Test 232,19,158,38,175,197,114,68
25-09-2017-11-59-24,Test 232,19,158,38,175,197,114,68
25-09-2017-12-00-18,Test 232,19,158,38,175,197,114,68
25-09-2017-12-00-22,Test 232,19,158,38,175,197,114,68
Woran liegt das denn, ich leere doch die serielle mit ser.flushInput ... ?
mega-hz
User
Beiträge: 14
Registriert: Sonntag 24. September 2017, 13:48

So, nun funktioniert das einlesen:

Code: Alles auswählen

#!/usr/bin/env python
# coding: utf8

from __future__ import absolute_import, division, print_function
import serial
from datetime import datetime


Data = []

with open('homesys.csv', 'r') as File:
    Data = File.read()

    print('File: --------------------------------')
    print(Data)


def main():
    with open('homesys.csv', 'a') as File:
        with serial.Serial('/dev/ttyUSB0', 19200) as lines:
            for i, line in enumerate(lines):
                File.write(datetime.now().strftime('%d-%m-%Y-%H-%M-%S'))
                File.write(",")
                File.write(line)

                print('Datensatz ', i, ':', datetime.now().strftime('%d-%m-%Y-%H-%M-%S'), line)


if __name__ == '__main__':
    main()
hatte die oberen beiden Zeilen vergessen, daher die Fehlermeldungen beim # :D

Die Frage ist nun, ob das Programm immer so laufen kann oder gibt es Probleme nach ner Zeit?
__deets__
User
Beiträge: 14525
Registriert: Mittwoch 14. Oktober 2015, 14:29

Du kannst deine with-Statements auch mit einer while-Schleife kombinieren (Achtung, Pseudocode):

Code: Alles auswählen

with foo as bar:
       while True:
              line = bar.readline()
Und ~1GB pro Jahr ist ja nun Kleckerkram, selbst fuer einen PI. Der schreibt ~5-20MB pro Sekunde raus. Also ein halbes Petabyte im Jahr...

Warum du immer dasselbe bekommst kann ich aus deinem Code nicht ablesen, der liest ja eh immer nur eine Zeile, und entspricht damit auch nicht deiner Ausgabe :K
mega-hz
User
Beiträge: 14
Registriert: Sonntag 24. September 2017, 13:48

das Problem mit den immer gleichen Daten ist weg.

die Ausgabe sieht nun so aus:

Code: Alles auswählen

File einlesen und darstellen:

25-09-2017-13-15-01,Test 188,109,120,80,102,47,102,143
25-09-2017-13-15-04,Test 142,79,160,52,3,124,114,32
25-09-2017-13-15-07,Test 70,18,189,123,116,190,247,56
25-09-2017-13-15-10,Test 17,157,230,3,139,79,204,66
25-09-2017-13-15-13,Test 22,167,208,141,155,125,158,16
25-09-2017-13-15-16,Test 54,157,56,53,118,49,163,35
25-09-2017-13-15-19,Test 84,116,30,193,22,211,24,89
25-09-2017-13-15-22,Test 251,223,42,123,228,43,13,211
25-09-2017-13-15-25,Test 160,178,15,154,233,126,200,14
25-09-2017-13-15-28,Test 30,19,234,106,32,185,15,104
25-09-2017-13-15-31,Test 6,228,183,173,125,202,177,131
25-09-2017-13-15-34,Test 16,162,158,159,216,57,71,143
25-09-2017-13-15-37,Test 122,143,112,87,189,144,239,236
25-09-2017-13-15-40,Test 95,180,30,98,232,214,53,197

Ankommende Daten anzeigen und in File speichern:

Datensatz  0 : 25-09-2017-13-15-45 Test 135,82,39,148,205,228,83,23

Datensatz  1 : 25-09-2017-13-15-48 Test 188,109,120,80,102,47,102,143

Datensatz  2 : 25-09-2017-13-15-51 Test 142,79,160,52,3,124,114,32

Datensatz  3 : 25-09-2017-13-15-54 Test 70,18,189,123,116,190,247,56

Datensatz  4 : 25-09-2017-13-15-57 Test 17,157,230,3,139,79,204,66

Datensatz  5 : 25-09-2017-13-16-00 Test 22,167,208,141,155,125,158,16

Datensatz  6 : 25-09-2017-13-16-03 Test 54,157,56,53,118,49,163,35
OK, nun wirds wohl erstmal Zeit für das RDD...
mega-hz
User
Beiträge: 14
Registriert: Sonntag 24. September 2017, 13:48

Habe gerade festgestellt, daß RRD-Tool bereits installiert ist!
Liegt wohl am OMV welches auf dem Rechner auch läuft.

Wie kann man nun die ausgelesenen Daten mit RRD sichtbar machen?
Kann mir da jemand weiterhelfen?
mega-hz
User
Beiträge: 14
Registriert: Sonntag 24. September 2017, 13:48

Moin moin,

also erstmal möchte ich mich für die Tips von Euch bedanken!
Nach gefühlten 20 Tabs im Firefox und ca. 20Std. konnte ich selber mich mit RRD einarbeiten und bin nun recht begeistert!
Mein Arduino, der die CAN-Bus Daten in serielle Daten für den Server umwandelt, liefert zwar noch immer Zufallswerte, aber das Format
ist schonmal korrekt.
Diese Python Routine liest und filtert nun die ankommenden Daten und macht selbständig gleich ein RRD-Update anstatt auf Platte zu speichern:

Code: Alles auswählen

#!/usr/bin/python -u

import serial
import sys
import os
import rrdtool

# serial port of CAN-SER Arduino
port = '/dev/ttyUSB0'

# MAIN
def main():
  # open serial line
  ser = serial.Serial(port, 19200)
  if not ser.isOpen():
    print "Unable to open serial port %s" % port
    sys.exit(1)

  while(1==1):
    # read line from CAN-SER Arduino
    line = ser.readline()
    line = line.strip()
    
    data = line.split(',')
    if (len(data) == 16 and data[0] == '$' and data[15] == '#'):
      print "Homesys-Elektrik Daten erkannt"
      print line
      # data is valid 
      # re-format data into an update string for rrdtool
      for i, val in enumerate(data):
        data[i] = ('U' if val == '' else val.replace(',', '.'))
      update = 'N:' + ':'.join(data[1:15])
      print update
      # insert data into database
      rrdtool.update(
        "%s/homesys-elektrik.rrd" % (os.path.dirname(os.path.abspath(__file__))),
        update)

  ser.close()

if __name__ == '__main__':
  main()
Da später noch andere Daten wie z.B. Temperaturen und IOs dazukommen, ist die Filterung auf das Anfangs- und Ende Zeichens $ / #
ganz praktisch.

Das anlegen einer RRD-Datenbank macht dieses Script:

Code: Alles auswählen

#!/usr/bin/python
import rrdtool
ret = rrdtool.create("homesys-elektrik.rrd", "--step", "60",
 "DS:PV1:GAUGE:1200:0:50",
 "DS:PV2:GAUGE:1200:0:50",
 "DS:WIND:GAUGE:1200:0:200",
 "DS:SYS-BAT:GAUGE:1200:0:30",
 "DS:BATT:GAUGE:1200:0:20",
 "DS:LADE-I:GAUGE:1200:0:30",
 "DS:LAST-I:GAUGE:1200:0:30",
 "DS:LADE-W:GAUGE:1200:0:500",
 "DS:LAST-W:GAUGE:1200:0:500",
 "DS:USV:GAUGE:1200:0:1",
 "DS:L1-W:GAUGE:1200:0:10000",
 "DS:L2-W:GAUGE:1200:0:10000",
 "DS:L3-W:GAUGE:1200:0:10000",
 "DS:N-W:GAUGE:1200:0:10000",
 "RRA:AVERAGE:0.5:1:36000",
 "RRA:MIN:0.5:18000:36000",
 "RRA:MAX:0.5:18000:36000",
 "RRA:AVERAGE:0.5:3600:36000")
und die generierung der Grafiken werden hier erzeugt (minütlich per cron):

Code: Alles auswählen

#!/usr/bin/env python
import rrdtool


for sched in ['hourly', 'daily' , 'weekly', 'monthly', 'yearly']:

    if sched == 'hourly':
        period = 'h'
    elif sched == 'weekly':
        period = 'w'
    elif sched == 'daily':
        period = 'd'
    elif sched == 'monthly':
        period = 'm'
    elif sched == 'yearly':
        period = 'y'
    ret = rrdtool.graph( "/home/hs-elektrik-%s.png" %(sched), "--start", "-1%s" %(period), "--vertical-label=Num",
         "-w 640",
		 "-h 240",
		 '--color', 'BACK#202040',
		 '--color', 'CANVAS#202040',
		 '--color', 'FONT#FFFFFF',
		 '--color', 'GRID#88FF88',
		 '--color', 'SHADEB#000000',
		 '--upper-limit', '30',
		 '--lower-limit', '0',
		 '--rigid',
		 "DEF:PV1=/home/homesys-elektrik.rrd:PV1:AVERAGE",
		 "LINE1:PV1#00FFFF:PV1\r",
		 "DEF:PV2=/home/homesys-elektrik.rrd:PV2:AVERAGE",
		 "LINE1:PV2#00AFFF:PV2\r",
         "DEF:WIND=/home/homesys-elektrik.rrd:WIND:AVERAGE",
		 "LINE1:WIND#888844:WIND\r",
		 "DEF:SYS-BAT=/home/homesys-elektrik.rrd:SYS-BAT:AVERAGE",
		 "LINE1:SYS-BAT#00FF80:SYS-BAT\r",
		 "DEF:BATT=/home/homesys-elektrik.rrd:BATT:AVERAGE",
		 "LINE1:BATT#88AA88:BATT\r",
		 "DEF:LADE-I=/home/homesys-elektrik.rrd:LADE-I:AVERAGE",
		 "LINE1:LADE-I#80FF00:LADE-I\r",
		 "DEF:LAST-I=/home/homesys-elektrik.rrd:LAST-I:AVERAGE",
		 "LINE1:LAST-I#FF8000:LAST-I\r",
		 "DEF:LADE-W=/home/homesys-elektrik.rrd:LADE-W:AVERAGE",
		 "LINE1:LADE-W#000000:LADE-W\r",
		 "DEF:LAST-W=/home/homesys-elektrik.rrd:LAST-W:AVERAGE",
		 "LINE1:LAST-W#FF6000:LAST-W\r",
		 "DEF:USV=/home/homesys-elektrik.rrd:USV:AVERAGE",
		 "LINE1:USV#000000:USV\r",
		 "DEF:L1-W=/home/homesys-elektrik.rrd:L1-W:AVERAGE",
		 "LINE1:L1-W#000000:L1-W\r",
		 "DEF:L2-W=/home/homesys-elektrik.rrd:L2-W:AVERAGE",
		 "LINE1:L2-W#000000:L2-W\r",
		 "DEF:L3-W=/home/homesys-elektrik.rrd:L3-W:AVERAGE",
		 "LINE1:L3-W#000000:L3-W\r",
		 "DEF:N-W=/home/homesys-elektrik.rrd:N-W:AVERAGE",
		 "LINE1:N-W#000000:N-W\r")
Das Ergebnis sieht dann so aus:

Bild

Ich werde die Elektrik-Bilder noch aufteilen müssen, damit die Skalen übersichtlich bleiben, z.Z. geht die Skala nur bis 30, aber wenn die Leistungsmessung von L1/L2/L3 dazukommen, sollte die Skala schon bis 5000 oder so gehen, dann wären die kleinen Werte fast unsichtbar...

Ein nettes Projekt!

Ich denke das Thema kann als elredigt angesehen werden!

Vielen Dank! :D
Sirius3
User
Beiträge: 17738
Registriert: Sonntag 21. Oktober 2012, 17:20

@mega-hz: zu Deinem ersten Code im letzten Posting, eingerückt wird immer mit 4 Leerzeichen pro Ebene, nicht 2. Das if mit `ser.isOpen` kann ersatzlos gestrichen werden, denn `Serial` öffnet immer den Port, `isOpen` liefert also immer True. `while(1==1):` ist eigenlich `while True:`. Die Klammern beim if in Zeile 25 gehören weg. Statt Elemente in einer Liste zu aktualisieren, erzeugt man einfach eine neue Liste, dann braucht man auch nicht umständlich mit Indizes zu hantieren. Das ser.close() in Zeile 39 wird nie erreicht. Besser Serial gleich mit dem with-Statement öffnen, dann wird (auch bei Exceptions) automatisch geschlossen. Die Kommentare sind größtenteils nutzlos, weil sie nur beschreiben, was im Code sowieso schon steht.

Code: Alles auswählen

#!/usr/bin/python -u
import serial
import os
import rrdtool

# serial port of CAN-SER Arduino
PORT = '/dev/ttyUSB0'

DATABASE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "homesys-elektrik.rrd")

def generate_update_string(data):
    data = ['N'] + ['U' if val == '' else val.replace(',', '.')
        for val in data[1:15]]
    return ':'.join(data)

def main():
    with serial.Serial(PORT, 19200) as ser:
        while True:
            line = ser.readline().strip()
            data = line.split(',')
            if len(data) == 16 and data[0] == '$' and data[15] == '#':
                # data is valid 
                print "Homesys-Elektrik Daten erkannt"
                print line
                update = generate_update_string(data)
                print update
                rrdtool.update(DATABASE, update)

if __name__ == '__main__':
    main()
mega-hz
User
Beiträge: 14
Registriert: Sonntag 24. September 2017, 13:48

Ok, das sieht kürzer und übersichtlicher aus!
Wusste nicht, das nach dem IF keine Klammern benötigt werden...
Das mit den 4 Leerzeichen, ok, Python hat so seine Eigenheiten, muss man sich dran gewöhnen.

Danke sehr für die hilfreichen Tips!
Benutzeravatar
noisefloor
User
Beiträge: 3854
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,
Das mit den 4 Leerzeichen, ok, Python hat so seine Eigenheiten, muss man sich dran gewöhnen.
Das ist keine "Eigenheit" (der Python Interpreter akzeptiert auch 2 oder 3 oder 5 oder ...), aber vier ist die Konvention und auch die klarer Empfehlung der PEP8, dem "Python Style Guide", dem alle Python-Programmierer folgen.

Gruß, noisefloor
Antworten