Skript zur Ladesteuerung einer Batterie

Python auf Einplatinencomputer wie Raspberry Pi, Banana Pi / Python für Micro-Controller
Antworten
Minzent
User
Beiträge: 16
Registriert: Dienstag 11. September 2018, 15:09

Guten Tag,

in den letzten zwei Monaten habe ich von 0 angefangen mich mit dem Thema Raspberry Pi´s, Elektronik und Programmierung auseinander zu setzen. Daher bitte ich um Verzeihung für meinen "unschönen" Code...

Ich habe ein Skript für eine Ladesteuerung einer Batterie geschrieben (bitte keine DIskussion über den Sinn dahinter... es ist für mich erstmal ein Weg in ein Thema reinzukommen), das grundsätzliche Prinzip sieht folgendermaßen aus: Ich habe eine Ladeplatine, die ich mit einem Raspberry Pi via CAN anspreche. Je nach Nachricht, die ich der Ladeplatine schicke, setzt diese etwa eine Spannung an um die Batterie zu laden, sendet einen Heartbeat (wichtig, weil sonst die Ladeplatine nach einer gewissen Zeit "aus geht" wenn kein Heartbeatrequest gesendet wird), sendet die auf der Platine gemessene Spannung oder beendet den Ladevorgang.

Der Sinn des Skripts ist folgender: Zunächst wird ein Can-Bus aufgebaut. Dann soll eine Excel-Datei erstellt werden, die nachfolgend die Platinenspannung sekündlich loggt. Schön wäre, wenn die Datei je nachdem, wann ich das Skript ausführe, folgende Kennzeichnung hat: Platinenspannung_TTMMJJJJ und wenn ich das Skript mehrmals am selben Tag ausführe nicht überschreibt, sondern dann einfach etwa noch eine (2) ans Ende schreibt.
Das Skript soll dann sekündlich die Platinenspannung abfragen und ausgeben. Da die Antwort der Platine bei Spannungsabfrage in Hex kommt, muss diese noch konvertiert werden. Gleichzeitig soll alle 10 Sekunden ein Heartbeatrequest an die Platine gesendet werden, damit diese nicht einfach "ausgeht". Sobald die Platinenspannung 3 mal einen bestimmten Wert erreicht hat (etwa 41000 mV), soll die Nachricht zum beenden des Ladevorgangs gesendet & die Excel-Datei geschlossen werden sowie der Heartbeat-Task beendet werden.

Im Folgenden seht ihr meinen derzeitigen Code:

Code: Alles auswählen

import can
import time
import os
import sys
import queue
import colorama
from colorama import Fore
from threading import Thread
import threading
import matplotlib
import matplotlib.pyplot as plt
import numpy as np


# Bus-channel aufbauen

try:
    bus = can.interface.Bus(channel='can0', bustype='socketcan_native')
except OSError:
    print('Cannot find PiCAN board.')
    exit()


threadtasks=True



Stopp_Charge		= 0x00200039
Start_Charge		= 0x00200038
Heartbeat_Request	= 0x0FE00000   #Can-Nachrichten für die Ladeplatine
Voltage_Request		= 0x00200040
Voltage_Reply		= 0x00002006


outfile = open('Platinenspannung_Datum.csv','w', newline='')

def start_charge():
    msg = can.Message(arbitration_id=Start_Charge,data=[],extended_id=True)
    bus.send(msg)
    print(Fore.BLUE + "\n" "Charging initiated" "\n")
    print(Fore.RESET)
start_charge()


def stopp_charge():
        msg = can.Message(arbitration_id=Stopp_Charge,data=[],extended_id=True)
        bus.send(msg)
        print("Charging stopped. Battery full. Stop program")
        global threadtasks
        threadtasks=False
        sys.exit(0)
        




def heartbeat_task(): # heartbeat thread
    global threadtasks
    while threadtasks:
        time.sleep(600)
        msg = can.Message(arbitration_id=Heartbeat_Request,data=[],extended_id=True)
        bus.send(msg)
        print(Fore.YELLOW + "\n" "Heartbeatrequest sent" "\n")
        print(Fore.RESET)


def can_rx_task():	# Receive thread
    global threadtasks
    while threadtasks:
        message = bus.recv()
        if message.arbitration_id == Voltage_Reply: 
            q.put(message)			# Put message into queue


def can_tx_task():	# Transmit thread
    global threadtasks
    while threadtasks:
        msg = can.Message(arbitration_id=Voltage_Request,data=[],extended_id=True)
        bus.send(msg) #Sent a Voltage request
        time.sleep(1)
						
heart = Thread(target = heartbeat_task)
heart.start()						
q = queue.Queue()
rx = Thread(target = can_rx_task)
rx.start()
tx = Thread(target = can_tx_task)
tx.start()


try:
    while True:
            while(q.empty() == True):	# Wait until there is a message
                pass
            message = q.get()

            s=''
            for i in range(message.dlc ):            
                s +=  '{0:x} '.format(message.data[i])	                		
			
            x = s.split() #Konvertierung in DEZ
            y = x[0]
            z = x[1]

            if len(y) == 1:
                y = '0' + y
            volt = int(z+y, 16)

            if volt > 41000:
                print(Fore.RED + "\n"'OVERHEATING')
                print(Fore.RESET)
                outfile.close()
                stopp_charge()
                
            else:
                print(volt, 'mV')
                print(volt, file = outfile) # Save data to file



	
except KeyboardInterrupt:
    print(Fore.RED + '\n\r keyboard interrupted\n')
    print(Fore.RESET)
    outfile.close()		# Close logger file
    stopp_charge()
		
Mein Problem ist zunächst das Erstellen einer immer neuen Excel Datei für jedes Ausführen des Skripts. Weiterhin habe ich das Problem, dass der Heartbeat-Task erst beendet wird, wenn der Task aufgewacht ist und ein letztes mal gesendet hat. Vorher wird das Programm nicht beendet. Ich möchte aber eigentlich gerne den Task beenden, sobald mein Ladevorgang gestoppt wird.... sonst muss ich im blödesten Fall 9:59 min warten. Außerdem tue ich mich mit dem bis 3 laufenden Counter schwer und weiß nicht genau, wie diese Stelle aussehen soll. Am Anfang seht ihr, dass ich matplotlib importe, das liegt daran, dass ich damit gerne die Excel direkt plotten möchte. Aber dafür muss ich mich noch etwas mit matplotlib auseinandersetzen. Und die grundsätzliche Frage wäre, ob das Skript so überhaupt "gut" ist. Es wirkt für mich so dahingeschustert. Ich lese mich zwar gerade in das schreiben von Klassen ein, weiß aber noch nicht, wie ich dieses Konzept in meinem Skript anwenden kann.

Ich bin für jede Hilfe wirklich dankbar :oops:
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Vergiss sofort, dass es Threads gibt. Sie sind zum einen fuer deine Aufgabe ueberhaupt nicht notwendig, und zum zweiten eine der schwierigsten Dinge, die man beim Programmieren machen kann. Denn sie RICHTIG zu machen faellt eigentlich jedem sauschwer, und es koennen sich sehr leicht subtile Bugs einschleichen, denen man nur auesserst schwer auf die Schliche kommt.

Dein Problem laesst sich problemlos sequentiell abarbeiten, denn alles was dazu notwendig ist, ist das regelmaessig einlesen der Daten von der Platine, und wenn du feststellst, dass genug Zeit vergangen ist, schickst du eben einen keep-alive. Und damit verschwinden deine ganzen Task-Koordinierungsprobleme.

Ich wuerde dir desweiteren raten, statt einer CSV-Datei eine SQLite Datenbank zu schreiben. Damit hast du keinen Brass mit der Erzeugung von Dateien, sondern speicherst einfach nur jede Messung. Darauf basierend kannst du dir dann problemlos ein zweites Skript schreiben, welches zu Zwecken des Reporting einfach eine CSV-Datei aus den Eintraegen fuer einen Tag macht.
Minzent
User
Beiträge: 16
Registriert: Dienstag 11. September 2018, 15:09

Hallo deets und Danke für deine Antwort!
__deets__ hat geschrieben: Montag 22. Oktober 2018, 17:18 Dein Problem laesst sich problemlos sequentiell abarbeiten
Da es so ist, dass ich die Platinenspannung nur als Antwort bekomme, wenn ich die Frage schicke (die CAN-Nachricht "Voltage_request") bin ich diesen Weg gemäß dieser Anleitung gegangen und habe mit Threads gearbeitet. Wie stelle ich mir ein sequentielles Arbeiten vor? Ich möchte ja so vorgehen: voltage_request sekündlich senden, Antwort in mV konvertieren und ausgeben und dies solange wiederholen bis die Antwort >4100 mV ist, dann den Ladevorgang durch senden der stopp_charge Nachricht beenden. gleichzeitig aber eben den keep_alive alle 10 Minuten durchführen. Wenn ich mit einer while-true schleife arbeite habe ich es eben nicht geschafft den keep alive mit einzuarbeiten.
__deets__ hat geschrieben: Montag 22. Oktober 2018, 17:18 Ich wuerde dir desweiteren raten, statt einer CSV-Datei eine SQLite Datenbank zu schreiben
Das werde ich probieren!

Danke nochmal bis hierher
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Aber was hindert dich denn daran, erst eine Frage zu schicken, und danach eine Antwort einzulesen? Das passiert doch eh im Wechsel? Und für das keepalive mus du dir doch nur merken, wann du das letzte geschickt hast, und dann ein neues schicken, wenn genug Zeit verflossen ist. Und dir dann wieder die Zeit merken. Auch das geschieht als Teil einer großen Schleife.

Selbst WENN man einen Thread bräuchte, weil es Nachrichten geben könnte, die asynchron zueinander reinkommen, dann brauchst du dafür genau EINEN Hintergrund-Thread. Da ist dein Beispiel leider kompletter Mist, und das ist nicht der einzige Teil, der zum wegwerfen ist. Auch das einlesen neuer Daten aus der Queue ist kompletter Schrott. Queue.get WARTET auf die nächste Nachricht. Davor eine Schleife zu packen, die die ganze Zeit nur CPU-Zyklen verbrät, um zu prüfen, ob was in der Queue ist, ist also komplett wertlos, aber teuer. Das ganze Ding solltest du komplett aus deinem Gedächtnis verbannen, weil es wirklich nix richtig macht.

Und das du einen DRITTEN Thread eingeführt hat’s für deinen keepalive, ohne dabei aber die dafür vorgesehene message Queue zu benutzen, und stattdessen direkt den Bus zu beschreiben - das wartet nur darauf, einen katastrophalen Crash zu produzieren. Das ist nur durch Zufall bis dato glatt gegangen.

Also nochmal: schmeiß das ganze Thread geraffelt raus. Mach das einfach sequentiell. Schreiben, lesen, warten. Schreiben, lesen, warten. Wahrscheinlich kannst du sogar immer ein keepalive schicken, oder wird der bockig wenn das öfter kommt? Kann ich mir nicht vorstellen. Das setzt ja auch intern nur einen Timer zurück.
Sirius3
User
Beiträge: 17710
Registriert: Sonntag 21. Oktober 2012, 17:20

@Minzent: ein paar Anmerkungen zum Code

Du solltest keine Tabs und Spaces mischen, auch wenn Tabs nur vor Kommentaren stehen, kann es schnell passieren, dass das mal vor eine normale Zeile rutscht. Stell Deinen Editor so ein, dass er beim Drücken der Tab-Taste Spaces ausgibt.

Zeile 3,7,...: viele Importe werden nicht gebraucht. Aufräumen!
Zeile 21: exit sollte sys.exit sein
Zeile 28ff: Konstanten werden per Konvention KOMPLETT_GROSS geschrieben
Zeile 35: Variablen erst definieren, wenn sie gebraucht werden, hier, open immer mit dem with-Statement benutzen, dann kommt man auch gar nicht in die Verlegenheit, die Datei zu früh zu öffnen.
Zeile 37, 45, 57, ...: Funktionen sollten alles was sie brauchen über ihre Argumente bekommen.
Zeile 42: das mischen von Definitionen und ausführbarem Code ist sehr unübersichtlich. Diese Zeile kann viel zu leicht übersehen werden. Am besten gar keinen ausführbaren Code auf oberster Ebene, sondern in einer Funktion "main" zusammenfassen.
Zeile 49: keine globalen Variablen benutzen, erst recht nicht mit Threads. Hier willst Du eigentlich ein Event benutzen, oder noch besser daemonisierte Threads. Die wichtige Frage bei Threads ist sowieso, ob das Bus-Objekt threading unterstützt. Die Dokumentation läßt das erahnen, klar formuliert ist das aber nicht.
Zeile 51: nicht irgendwo innerhalb des Programms mit exit beenden. Das Programm sollte natürlich dadurch enden, dass die Funktion main verlassen wird.
Zeile 93f: keine Busy-Loops! `get` blockiert automatisch.
Zeile 99: statt Strings mit + zusammenzustückeln, join benutzen
Zeile 101: oder eben nicht, wenn Du dann sowieso wieder splittest
Zeile 106: dafür kennt format führende 0.
Zeile 107: und dafür kann man einfach Mathematik benutzen.
Zeile 113: da gehört eigentlich ein break hin
Zeiel 125: wird nur bei Ctrl+C ausgeführt, nicht bei anderen Fehlern!

Das ergibt ungefähr das:

Code: Alles auswählen

import can
import time
import queue
from colorama import Fore
from threading import Thread

STOP_CHARGE = 0x00200039
START_CHARGE = 0x00200038
HEARTBEAT_REQUEST = 0x0FE00000   #Can-Nachrichten für die Ladeplatine
VOLTAGE_REQUEST = 0x00200040
VOLTAGE_REPLY = 0x00002006

def send_message(bus, id):
    msg = can.Message(arbitration_id=id, data=[], extended_id=True)
    bus.send(msg)

def start_charge(bus):
    send_message(bus, START_CHARGE)
    print(Fore.BLUE + "\n" "Charging initiated" "\n")
    print(Fore.RESET)

def stopp_charge(bus):
    send_message(bus, STOP_CHARGE)
    print("Charging stopped. Battery full. Stop program")

def heartbeat_task(bus): # heartbeat thread
    while True:
        time.sleep(600)
        send_message(bus, HEARTBEAT_REQUEST)
        print(Fore.YELLOW + "\n" "Heartbeatrequest sent" "\n")
        print(Fore.RESET)

def can_rx_task(bus, queue): # Receive thread
    while True:
        message = bus.recv()
        if message.arbitration_id == VOLTAGE_REPLY: 
            queue.put(message) # Put message into queue

def can_tx_task(bus): # Transmit thread
    while True:
        send_message(bus, VOLTAGE_REQUEST)
        time.sleep(1)

def main():
    # Bus-channel aufbauen
    try:
        bus = can.interface.Bus(channel='can0', bustype='socketcan_native')
    except OSError:
        print('Cannot find PiCAN board.')
        return
    
    msg_queue = queue.Queue()
    heart = Thread(target=heartbeat_task, args=(bus, ))
    heart.deamon = True
    heart.start()
    rx = Thread(target=can_rx_task, args=(bus, msg_queue))
    rx.deamon = True
    rx.start()
    tx = Thread(target=can_tx_task, args=(bus, ))
    tx.deamon = True
    tx.start()

    try:
        start_charge(bus)
        with open('Platinenspannung_Datum.csv','w', newline='') as outfile:
            while True:
                message = msg_queue.get()
                volt = message.data[0] + message.data[1] * 256
                print(volt, 'mV')
                print(volt, file=outfile) # Save data to file
                if volt > 41000:
                    break
            print(Fore.RED + "\n"'OVERHEATING')
            print(Fore.RESET)
    except KeyboardInterrupt:
        print(Fore.RED + '\n\r keyboard interrupted\n')
        print(Fore.RESET)
    finally:
        stopp_charge()

if __name__ == '__main__':
    main()
Jetzt gibt es aber `send_periodic` was das Programm deutlich vereinfacht:

Code: Alles auswählen

import can
from colorama import Fore

STOP_CHARGE = 0x00200039
START_CHARGE = 0x00200038
HEARTBEAT_REQUEST = 0x0FE00000   #Can-Nachrichten für die Ladeplatine
VOLTAGE_REQUEST = 0x00200040
VOLTAGE_REPLY = 0x00002006

def send_message(bus, id, periodic=None):
    msg = can.Message(arbitration_id=id, data=[], extended_id=True)
    if periodic is None:
        bus.send(msg)
    else:
        bus.send_periodic(msg, periodic)

def start_charge(bus):
    send_message(bus, START_CHARGE)
    print(Fore.BLUE + "\n" "Charging initiated" "\n")
    print(Fore.RESET)

def stop_charge(bus):
    send_message(bus, STOP_CHARGE)
    print("Charging stopped. Battery full. Stop program")

def heartbeat(bus):
    send_message(bus, HEARTBEAT_REQUEST, periodic=600)

def can_tx(bus): # Transmit thread
    send_message(bus, VOLTAGE_REQUEST, periodic=1)

def can_rx(bus): # Receive thread
    while True:
        message = bus.recv()
        if message.arbitration_id == VOLTAGE_REPLY: 
            yield message

def main():
    # Bus-channel aufbauen
    try:
        bus = can.interface.Bus(channel='can0', bustype='socketcan_native')
    except OSError:
        print('Cannot find PiCAN board.')
        return

    try:
        start_charge(bus)
        heartbeat(bus)
        can_tx(bus)
        with open('Platinenspannung_Datum.csv', 'w', newline='') as outfile:
            for message in can_rx(bus):
                volt = message.data[0] + message.data[1] * 256
                print(volt, 'mV')
                print(volt, file=outfile) # Save data to file
                if volt > 41000:
                    break
            print(Fore.RED + "\n"'OVERHEATING')
            print(Fore.RESET)
    except KeyboardInterrupt:
        print(Fore.RED + '\n\r keyboard interrupted\n')
        print(Fore.RESET)
    finally:
        stop_charge()

if __name__ == '__main__':
    main()
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

@Sirius3: schoen! Ich finde die Filterung in can_rx falsch, weil das generischer sein sollte. Aber sonst klasse.
Minzent
User
Beiträge: 16
Registriert: Dienstag 11. September 2018, 15:09

@Sirius3 vielen vielen Dank...

Habe mich gerade hingesetzt und deine Verbessung durchgearbeitet um nachzuvollziehen, was dort genau passiert. Mit Argumenten habe ich bisher noch nicht gearbeitet, aber ich habe es sehr gut verstanden und werde versuchen es künftig anzuwenden. Die Umsetzung mit periodic gefällt mir sehr gut, ich versuche ab jetzt nicht mehr mit threads zu arbeiten :)

Jetzt werde ich noch _deets_´s Vorschlag umsetzen und das Erstellen der csv-Datei ersetzen und mit SQLite arbeiten.

Ich möchte nochmal sagen, dass ich es echt klasse finde, dass ihr eure Zeit dafür opfert untalentierten Hobby-Programmierern wie mir weiterzuhelfen... Chapeau!
Minzent
User
Beiträge: 16
Registriert: Dienstag 11. September 2018, 15:09

Leider erhalte ich folgende Fehlermeldung:


AttributeError: 'SocketscanNative_Bus' object has no attribute 'send_periodic'

Ich stöbere gerade in dem Verzeichnis /hardbyte-Python-can.../can/interfaces rum und da gibt es definitiv eine class CyclicSendTask und ich finde auch die Klasse SocketscanNative. Dann bin ich allerdings mit meinem Latein am Ende.... :roll:
Sirius3
User
Beiträge: 17710
Registriert: Sonntag 21. Oktober 2012, 17:20

Du scheinst da eine sehr alte Version von python-can zu benutzen. Woher hast Du die?
Minzent
User
Beiträge: 16
Registriert: Dienstag 11. September 2018, 15:09

Ich hatte ein paar tutorials durchgearbeitet bezüglich PICAN2 + Raspberry Pi. Ich meine, dass ich mich ziemlich genau an diese Anleitung gehalten hatte. Ich habe auch schon ein update probiert, leider ohne Erfolg.
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Minzent: Das ist Version 1.4.1 von 2015: https://bitbucket.org/hardbyte/python-c ... e8c007e9aa (Vergleiche den ZIP-Dateinamen mit den ersten Ziffern des Commit-Hashes. Das Projekt ist mittlerweile auch von Bitbucket zu Github umgezogen. Und statt direkt bei irgendwelchen Repositoryhostern würde ich eher erst einmal im Python Package Index nachsehen.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Minzent
User
Beiträge: 16
Registriert: Dienstag 11. September 2018, 15:09

Erledigt. neuste Version geladen, vorher alles alte deinstalliert, jetzt funktionierts.

Hier die neuste Version: https://github.com/hardbyte/python-can/ ... ease-3.0.0

danke @_blackjack_
Antworten