Logger für Solarladeregler

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
Antworten
tnn85
User
Beiträge: 3
Registriert: Samstag 2. August 2014, 14:14

Ich habe vor, für einen Solarladeregler der Marke epsolar die aktuellen Messdaten, welche über eine Fernbedienungsschnittstelle auf Basis von RS232 übertragen werden, mit Hilfe eines Raspberry Pis zu loggen. Durch die native Unterstützung von Python habe ich nun mein erstes Projekt versucht mit Python zu realisieren. Der skriptbasierte Ansatz machte es mir natürlich leicht schnell eine Lösung zusammenzuschustern. Leider habe ich nun das Problem, dass das Skript nicht sonderlich robust zu sein scheint - sprich irgendwann hört er einfach auf zu loggen. Die erzeugte Datei soll eigentlich nur ein CSV Format haben, damit ich die einzelnen Messwerte auf dem PC weiterverarbeiten kann. Mein Problem ist jetzt, dass ich den Grund für das Anhalten des Loggens nicht erkenne, da ich dachte, dass er mir wenigstens in meinen Ausnahmebehandlungen (catch-Blöcke) einen Eintrag macht, was das Problem ist.

Der Programmablauf ist folgender:
1. Initialisieren der Logdatei, der seriellen Schnittstelle und einem zyklischen Timer
2. Senden einer Requestbotschaft über RS232 an den Laderegler (zyklisch alle 10 Sekunden für 24h)
3. Empfangen der Antwort und Schreiben der Werte in eine Zeile der CSV-Datei (Logdatei) / (genau wie 2. auch zyklisch in gleicher Funktion)
4. Schließen der Logdatei

Code: Alles auswählen

#!/usr/bin/env python3

import serial
import repeat_timer
import csv
import time
import sys
import random
import array
import socket

#Example for response: eb90eb90eb9000a0180102030405060708090a0b0c0d0e0f101112131415161718ffff7f

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udphost = "xxx.no-ip.biz"
udpport = 50000

log_filename = "Log_Solarlogger_" + time.strftime("Date_%d_%m_%y_Time_%H_%M_%S_number")+ str(random.randint(1, 10000)) + ".csv"
timer_period = 10
period_counter = 0
time_to_complete = 86400

if sys.platform == "linux2":
    directory = "//home//pi//"
    import RPi.GPIO as GPIO
    GPIO.setmode(GPIO.BOARD)
    GPIO.setup(12, GPIO.OUT)
    GPIO.output(12, GPIO.HIGH)
if sys.platform == "win32":
    directory = "C://Python34//"
    
request_cmd = array.array('B',[0xEB,0x90,0xEB,0x90,0xEB,0x90,0x01,0xA0,0x01,0x03,0xbd,0xbb,0x7f]).tostring()
response_no_of_bytes = 36

try:
    csv_file = open(directory + log_filename,'w', newline='')
    csv_handler = csv.writer(csv_file)
except:
    print ("Error: Can not open file for write access. File may be open")
    print (sys.exc_info())
    csv_file.close()
    exit(-1)

if sys.platform == "linux2":
    si = serial.Serial()
    si.port = "/dev/ttyAMA0"
if sys.platform == "win32":
    si = serial.Win32Serial() 
    si.port = 2 
si.baudrate = 9600
si.dsrdtr = False 
si.parity = 'N'
si.timeout = 5

def request_data(req,response_no_of_bytes):
    global s, udphost, udpport, si, time_start, csv_handler, csv_file, period_counter
    try:
        period_counter += 1
        if (period_counter%2==0):
            GPIO.output(12, GPIO.LOW)
        else:
            GPIO.output(12, GPIO.HIGH)
        si.write(req)
        time_since_start = time.time() - time_start
        print("Request from logger @ " + str(time_since_start) + "s")
        resp = si.read(response_no_of_bytes)
        si.flush()
        time_since_start = time.time() - time_start
        if (len(resp)== response_no_of_bytes):
            print("Response from charger @ " + str(time_since_start) + "s, Response Length: " + str(len(resp)) + ", Response: " + str(resp))
            try:
               s.sendto(resp, (udphost, udpport))
            except socket.error:
               print("No network connection")
            csv_handler.writerow([time.asctime(),time_since_start,resp[6],resp[7],resp[8],(resp[9] + resp[10]*256)/100,(resp[11] + resp[12]*256)/100,(resp[15] + resp[16]*256)/100,(resp[17] + resp[18]*256)/100,(resp[19] + resp[20]*256)/100,resp[21],resp[22],resp[23],resp[25],resp[26],resp[27],resp[28],resp[29]-30,(resp[30] + resp[31]*256)/100])
        else:
            print("No response from charger @ "+ str(time_since_start)  +"s, Response length: " + str(len(resp)) + " !")
            try:
               s.sendto(b"hallo", (udphost, udpport))
            except socket.error:
               print("No network connection")
            csv_handler.writerow([time.asctime(),time_since_start,"","","","","","","","","","","","","","","","",""])
    except:
        print("Error @ " + str(time_since_start) + "s ,Reason: " + sys.exc_info())
        csv_handler.writerow(["Error @ "+ str(time_since_start) + "s", "Period  " + str(period_counter)])
        csv_file.close()
        
try:
    si.open()
    print("Serial port " + si.name + " opened")
except serial.serialutil.SerialException:
    print ("Error: Can not open serial port. Serial port may be open already")
    exit(-2)
except:
    print(sys.exc_info())
    exit(-2)
    
try:
    if (si.isOpen()):
        print("Starting cyclic request of data")
        csv_handler.writerow(["Logging start: " + time.strftime("%H %M %S")," Logging period: " + str(timer_period)," Logging time: " + str(time_to_complete)])
        csv_handler.writerow(["Time","Time since Start","ID","Command","Number of Bytes","Battery Voltage","PV Voltage","Load Current","Overdischarge Voltage","Full Voltage","Load detected?","Overloaded?","Short circuit?","Overcharged?","Battery voltage too low?","Batter voltage full?","Charging?","Temperature","Charging current"])
        print("Cyclic timer started (period = "+ str(timer_period) + "s). Logging for " + str(time_to_complete) +"s ...")   
        time_start = time.time()
        cyclic_timer = repeat_timer.RepeatedTimer(timer_period,True,request_data,request_cmd,response_no_of_bytes)
        
    time.sleep(time_to_complete)
    cyclic_timer.stop()
    time.sleep(1) #wait for cyclic timer thread to stop
    print("Cyclic timer stopped. Logging finished")
    print("Finished cyclic request of data")
except:
    print("Error 1")
    print(sys.exc_info())
    
finally:
    if (si.isOpen()):
        print("Closed serial port")
        si.close()
    if (csv_file.closed == False):
        print("Closing file")
        csv_file.close()
    if sys.platform == "linux2":
        GPIO.output(12, GPIO.LOW)
        GPIO.cleanup()
print("End")
exit(0)
So sieht die von Python erzeugte Logdatei aus:
Logging start: 20 04 24, Logging period: 10, Logging time: 86400
Time,Time since Start,ID,Command,Number of Bytes,Battery Voltage,PV Voltage,Load Current,Overdischarge Voltage,Full Voltage,Load detected?,Overloaded?,Short circuit?,Overcharged?,Battery voltage too low?,Batter voltage full?,Charging?,Temperature,Charging current
Thu Jul 17 20:04:24 2014,0.05801510810852051,0,160,24,12.7,18.17,0.0,11.22,14.35,1,0,0,0,0,0,1,31,0.29

... alle 10 sekunden ein Eintrag
Thu Jul 17 22:03:26 2014,7142.438544988632,0,160,24,12.59,1.87,0.03,11.17,14.46,1,0,0,0,0,0,0,28,0.0
das war der letzte Eintrag


Und so sieht es aus wenn ich die Ausgabe in der Bash in Linux umleite auf eine Datei:
Serial port /dev/ttyAMA0 opened
Starting cyclic request of data
Cyclic timer started (period = 10s). Logging for 86400s ...
Request from logger @ 0.003033161163330078s
Response from charger @ 0.05801510810852051s, Response Length: 36, Response: b'\xeb\x90\xeb\x90\xeb\x90\x00\xa0\x18\xf6\x04\x19\x07\x00\x00\x00\x00b\x04\x9b\x05\x01\x00\x00/\x00\x00\x00\x01=\x1d\x00\x00\xf9i\x7f'
Request from logger @ 10.00679898262024s

... und so weiter....
Request from logger @ 7582.3808760643005s
Response from charger @ 7582.4358241558075s, Response Length: 36, Response: b'\xeb\x90\xeb\x90\xeb\x90\x00\xa0\x18\xe9\x04;\x00\x00\x00\x03\x00]\x04\xa6\x05\x01\x00\x00*\x00\x00\x00\x00:\x00\x00\x00D\xc8\x7f'

Das war die letzte Zeile
Zuletzt geändert von tnn85 am Samstag 2. August 2014, 16:32, insgesamt 1-mal geändert.
tnn85
User
Beiträge: 3
Registriert: Samstag 2. August 2014, 14:14

Ach ja, das Modul verwende ich noch:

Code: Alles auswählen

from threading import Timer

class RepeatedTimer(object):
    def __init__(self, interval, startwithfirst, function, *args, **kwargs):
        self._timer     = None
        self.function   = function
        self.interval   = interval
        self.startwithfirst    = startwithfirst
        self.args       = args
        self.kwargs     = kwargs
        self.is_running = False
        self.start()
        if (startwithfirst):
            self.function(*self.args, **self.kwargs)

    def _run(self):
        self.is_running = False
        self.start()
        self.function(*self.args, **self.kwargs)
        
    def start(self):
        if not self.is_running:
            self._timer = Timer(self.interval, self._run)
            self._timer.start()
            self.is_running = True

    def stop(self):
        self._timer.cancel()
        self.is_running = False

BlackJack

@tnn85: Das ist alles viel zu unübersichtlich. Ein Riesenhaufen Code auf Modulebene, Threads und ``global``-Variablen auf denen operiert wird, Plattformsonderbehandlungen über den Code verstreut aber auch unter Windows wird GPIO benutzt was dort aber gar nicht importiert wird, also zu einem `NameError` führen muss…

Ich würde sagen das sollte man erst einmal alles aufräumen bevor man in dem Wust einen Fehler sucht bei dem nicht unwahrscheinlich ist, dass er genau deswegen überhaupt vorhanden ist.

Den `RepeatedTimer` verstehe ich auch nicht so ganz. Hätte da eine Schleife mit `time.sleep()` nicht gereicht?
tnn85
User
Beiträge: 3
Registriert: Samstag 2. August 2014, 14:14

Du meinst ich soll mit mehr Modulen arbeiten?
Die globalen Variablen brauche ich ja. Sonst kann ich ja nicht auf den handle in der Funktion zugreifen (z.b für das file oder die serielle Schnittstelle), oder?
Deswegen hatte ich auch Probleme das zu modularisieren.

Ja, die Plattformsonderbehandlung muss eigentlich raus. Auf Windows wird das Skript sowieso nicht ausgeführt.
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@tnn85: keine Module, sondern Funktionen. Wobei jede Funktion eine Aufgabe hat, und nicht 25. Ich sehe GPIO, serielle Schnittstellen, csv_dateien, noch ein UDP-Paketchen, da blick ja niemand mehr durch. Wenn jede Funktion relativ einfach ist, kann man sie auch getrennt testen, und prüfen, ob sie auch das tut, was Du erwartest. Sobald Du globale Variablen benutzt, geht das nicht mehr.
Benutzeravatar
pillmuncher
User
Beiträge: 1482
Registriert: Samstag 21. März 2009, 22:59
Wohnort: Pfaffenwinkel

@tnn85: Was BlackJack und Sirius3 gesagt haben.

Als erstes würde ich mal alles weglassen, was nicht essentiell ist. Dabei komme ich (plus ein paar Vereinfachungen/Verbesserungen) ungefähr zu sowas:

Code: Alles auswählen

#!/usr/bin/env python3

import time
import array
import socket
import serial
import RPi.GPIO as GPIO
from itertools import cycle

def main():
    INTERVAL = 10
    TIME_TO_COMPLETE = 86400
    DESTINATION = "fqdn.domain.tld", 50000
    RESPONSE_LEN = 36
    REQUEST = array.array('B',
            [0xEB,0x90,0xEB,0x90,0xEB,0x90,0x01,0xA0,0x01,0x03,0xbd,0xbb,0x7f]
            ).tostring()
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    ser = serial.Serial(
        port="/dev/ttyAMA0",
        baudrate=9600,
        dsrdtr=False,
        parity='N',
        timeout=5)
    GPIO.setmode(GPIO.BOARD)
    GPIO.setup(12, GPIO.OUT)
    gpio_out = cycle((GPIO.HIGH, GPIO.LOW))
    try:
        for _ in range(0, TIME_TO_COMPLETE, INTERVAL):
            GPIO.output(12, next(gpio_out))
            ser.write(REQUEST)
            ser.flush()
            response = ser.read(RESPONSE_LEN)
            try:
                if len(response) == RESPONSE_LEN:
                    sock.sendto(response, DESTINATION)
                else:
                    sock.sendto(b'hallo', DESTINATION)
            except socket.error:
                pass
            time.sleep(INTERVAL)
    finally:
        GPIO.output(12, GPIO.LOW)
        GPIO.cleanup()

if __name__ == '__main__':
    main()
Das ist noch nicht so, wie ich es bauen würde, aber so kann man alle wichtigen Dinge erst mal sehen und analysieren.

Dass es keine Fehlerbehandlung mehr gibt ist Absicht. Auf diese Weise bekommt man auftretende Fehler zu sehen, wenn das Programm abschmiert, inklusive Traceback. Da allerdings in deinem Programm Socket-Fehler komplett ignoriert werden (bis auf einenkurze Ausgabe in die Shell), habe ich sie ebenfalls ignoriert.

Die Logging-Ausgaben kann man später einbauen. Sie sollten später zum Debuggen des laufenden Prozesses dienen, nicht zum Deguggen des Programms, während man es gerade erst schreibt. Der Grund dafür liegt darin, dass die Debug-Informationen, die man im Betrieb des Programms lesen möchte, solche sind, die einem sagen, ob alle angeschlossenen Geräte, mit denen man kommunizieren möchte, erreichbar sind und funktionieren. Während des Programmierens möchte man dagegen Debug-Ausgaben, die einem sagen, ob man irgendwo einen undefinierten Namen im Programm hat oder durch 0 dividiert, und so weiter. Letzteres macht man am besten mittels print(). Ersteres am besten mit dem Logging-Modul. IMO jedenfalls.

So, wie du den Timer verwendet hast, ist dein Programm auf nicht-triviale Weise nicht thread-safe. Es kann dabei dazu kommen, dass zwei Threads gleichzeitig versuchen, über denselben seriellen Port bzw. Socket zu kommunizieren. Ich habe den Timer durch eine for-Schleife und einen time.sleep() Aufruf ersetzt.

Ein Verbesserungsvorschlag: Die Kommunikation über den Socket würde ich evtl. in einen extra Thread auslagern und die response über eine Queue an ihn schicken. Als Faustregel sollte jede Kommunikation mit einem Gerät oder Port in je einem eigenen Thread laufen. Damit schaltet man die Möglichkeit weitgehend aus, dass verschiedene Dinge sich gegenseitig blockieren oder behindern. Außerdem bekommt man als Nebeneffekt eine größere Isolierung der einzelnen zu lösenden Probleme (jedenfalls sofern man nicht versucht, über globale Variablen zu kommunizieren, sondern zB. über die og. Queue).
In specifications, Murphy's Law supersedes Ohm's.
Antworten