Python-Skript für GPIOs parallel laufen lassen

Python auf Einplatinencomputer wie Raspberry Pi, Banana Pi / Python für Micro-Controller
Antworten
dirk1312
User
Beiträge: 19
Registriert: Dienstag 1. November 2016, 18:16

Hallo zusammen,

ich möchte die fünf Magnetventile meiner Gartenbewässerung über ein Python-Skript steuern (geöffnet werden sie, wenn Spannung anliegt, liegt keine an, werden sie geschlossen). Die Magnetventile werden über die GPIOs gesteuert, in dem ich je Ventil ein Relais schließen und öffnen können muss, so dass die Ventile ihre 24 V (über einen Trafo) Spannung beziehen.

Testweise habe ich bisher immer nur ein Magnetventil geöffnet, dessen GPIO hart kodiert im Skript auf LOW und nach x Minuten wieder auf HIGH gesetzt wurde.

Da die Bewässerung fertig gebaut ist würde ich nun auch gerne die elektrische Steuerung der Ventile verwenden und hierfür einige Szenarien durchspielen um u.a. zu schauen, wieviele Kreisläufe ich parallel laufen lassen kann, wie lange ich jeden bewässern möchte,... bevor ich es komplett automatisieren und z.B. per Cronjob morgens um 5 Uhr die Bewässerung starten lasse.

Hierfür müsste ich mein Skript nun umbauen / erweitern, dass es als Argument beim Aufruf sowohl den jeweiligen GPIO, als auch dessen Zustand (High|Low) entgegen nimmt. Grundsätzlich kein Problem. Allerdings will ich ja nicht alle Ventile gleichzeitig öffnen bzw. schließen. D.h., es kann sein, dass ich GPIO 2 und 3 unmittelbar nacheinander auf LOW setze, aber GPIO 3 nach 30 Minuten auf HIGH und GPIO 2 erst nach 50 Minuten auf HIGH setze.

Mein bisheriges Skript sieht so aus:

Code: Alles auswählen

#!/usr/bin/python

import RPi.GPIO as GPIO
import time

try:
    GPIO.setmode(GPIO.BCM)

    pinList = [2, 3, 4, 17, 27]

    for i in pinList: 
        GPIO.setup(i, GPIO.OUT) 
        GPIO.output(i, GPIO.HIGH)

    GPIO.output(2, GPIO.LOW)
    time.sleep(0.5 * 60)  # One minute
    GPIO.output(2, GPIO.HIGH)
    
    GPIO.output(3, GPIO.LOW)
    time.sleep(0.5 * 60)  # One minute
    GPIO.output(3, GPIO.HIGH)
except KeyboardInterrupt:
    # here you put any code you want to run before the program
    # exits when you press CTRL+C
    print("\n", 'Fehler')  # print value of counter
except:
    print("Other error or exception occurred!")
finally:
    GPIO.cleanup()  # this ensures a clean exit
    print('QUIT')
Ich müsste das Skript ja nun so umbauen, dass zum einen die Argumente ausgelesen werden und dann deren Zustand / Status geprüft wird. Entspricht er noch nicht dem, auf den ich ihn laut Argument setzen will, setze ich ihn. Und dann ist das Skript zu Ende. Ich müsste dann aber auch die for-Schleife mit der GPIO-Initialisierung und das GPIO.cleanup() rausschmeißen?! Befürchte dann aber, dass mir irgendwann das ganze um die Ohren fliegt, weil die GPIOs in einen undefinierten Zustand kommen könnten.

Könnt ihr mir diese Sorge nehmen oder wie würdet ihr das lösen? So lange ich die Ventile manuell steuere kann ich ja auch noch händisch eingreifen. Wenn es aber irgendwann automatisiert läuft und nach dem 50.Start auf einmal die Ventile nicht mehr schließt, weil sich der Raspi verrannt hat, komme ich Abends heim und habe nen Schwimmteich... ;) Ich würde das ganze halt möglichst stabil realisieren, so dass ich jeden GPIO ohne Probleme separat an- und wieder abschalten kann.

Vielen Dank und viele Grüße

Dirk
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Wenn du das moeglichst stabil haben willst, solltest du auf einen PI verzichten. Das ist nicht boese gemeint. Nimm stattdessen einen leichtgewichtigeren Microcontroller, der sofort da ist, wenn man ihn einschaltet, und ausserdem vieeeeeel robuster vor sich hinwerkelt. Wenn du also ernsthaft auf einen Pool verzichten willst, dann solltest du das Pferd wechseln.

Fuer die mit WLAN ausgestatteten ESP8266 bzw. den moderneren ESP32 gibt es mit MicroPython auch eine Python-artige Sprache, die es dir erlaubt, aehnlich simpel mit dem System zu arbeiten. Und kostet ~10 Euro.

Das prinzipielle Vorgehen unabhaengig von der verwandten Hardware.

Code: Alles auswählen

ZEITRAEUME = {
     PUMPE_A: [(5uhr, 6uhr)],
     PUMPE_B: [(4uhr, 10uhr), (14uhr, 15uhr)]
}

init_gpios()
while True: # endlos schleifen
       t = jetzt()
       for pin in MEINE_PINS:
             for start, ende in ZEITRAEUME[pin]:
                     setze_pin(pin, start <= t <= ende) # an wenn im Zeitraum, sonst aus
Sirius3
User
Beiträge: 17710
Registriert: Sonntag 21. Oktober 2012, 17:20

@dirk1312: warum ergibt bei Dir 0.5*60 Sekunden eine Minute? Dein Exception-Handling ist fehlerhaft, da Du die Fehlerinformation einfach wegschmeißt. Module werden wie Variablen komplett klein geschrieben; nur Konstanten komplett GROSS.

Statt das ganze über einen Cronjob zu steuern, kannst Du die Zeitsteuerung auch direkt in Dein Programm einbauen. So werden auch alle Ausgänge bei einem Programmabsturz automatisch zurückgesetzt.

Ganz simple, könnte das so aussehen:

Code: Alles auswählen

#!/usr/bin/python

import RPi.GPIO as gpio
import time
from datetime import datetime as DateTime

PINS = [2, 3, 4, 17, 27]
SWITCH = [
    '05:30': (2, gpio.HIGH),
    '05:30': (3, gpio.HIGH),
    '05:45': (2, gpio.LOW),
    '06:30': (3, gpio.LOW),
]

def run_loop():
    while True:
        now = "{:%H:%M}".format(DateTime.now())
        for point, args in SWITCH:
            if now == point:
                gpio.output(*args)
        time.sleep(10)

def main():
    try:
        gpio.setmode(gpio.BCM)
        gpio.setup(PINS, gpio.OUT, initial=gpio.HIGH) 
        run_loop()
    except KeyboardInterrupt:
        pass
    finally:
        gpio.cleanup()  # this ensures a clean exit
        print('QUIT')
        
if __name__ == '__main__':
    main()
dirk1312
User
Beiträge: 19
Registriert: Dienstag 1. November 2016, 18:16

Hallo ihr beiden,

vielen Dank für eure Hinweise!

@__deets__: Danke für den Hinweis mit dem Mikrocontroller. Kannst du denn etwas mit LAN empfehlen? Ich dachte, ein Arduino hätte das, aber der hat laut Suchmaschine auch keines... Zu Testzwecken (dem manuellen Öffnen der Ventile) würde ich aber erstmal den Pi nehmen - damit ich keine 1.000 Baustellen aufmache. ;)

@Sirius3: Danke! Das Exception-Handling werde ich von dir übernehmen. Die Zeiten habe ich testhalber angepasst, damit ich keine Minute warten muss und nur den Kommentar unangepasst gelassen. O:-)
Die Dauer-Schleife hatte ich schon bei meinen Luftfeuchtigkeitsmessungen im Einsatz. Ich würde aber gerne jedes einzelne Ventil per Argument an- und abschalten können. "Irgendwann" ist dann eine Anbindung durch OpenHab2 geplant und da übernimmt das OpenHab2 die Zeitsteuerung,...

Vielen Dank und viele Grüße

Dirk
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Fuer den Arduino gibt es LAN-shields, die muesstest du dann verwenden. Auf dem Arduino laeuft dann aber kein uPython. Wobei es jetzt natuerlich nur so mittel viel bringt, in einen uC zu investieren, wenn am Ende mit OpenHAB dann wieder ein komplexerer Rechner die Steuerung uebernimmt, und potentiell wieder Seelandschaften entstehen. Da kannst du denke ich auch bei Python bleiben. Aber kann OpenHAB nicht schon selbst einfach GPIOs schalten?
dirk1312
User
Beiträge: 19
Registriert: Dienstag 1. November 2016, 18:16

Vielen Dank für deine Antwort.

Ich hatte es eigentlich so geplant, dass ich mein Relais an dem die Ventile hängen an den Pi (oder einen uC) hänge und übers LAN lediglich per SSH-Verbindung lediglich die Schaltung aufgerufen wird. Grundsätzlich denke ich, dass OpenHAB2 stabiler laufen wird, da (bedingt durch die Masse an Benutzern) besser getestet, als es mein Skript könnte.

So wie ich es verstanden hatte kann OpenHAB2 das nicht, sondern nur durch Skript-Aufrufe. Würde ich dann ggf. aber auch nochmal schauen. Den Arduino mit C zu programmieren sollte bei so sequentiellen Progrämmchen auch kein Problem sein. Python ist ja auch nicht unbedingt meine Sprache, aber man findet ja viele Beispiele.

Viele Grüße

Dirk
dirk1312
User
Beiträge: 19
Registriert: Dienstag 1. November 2016, 18:16

Hallo nochmal,

ich habe testhalber mal mein Skript nach euren Anregungen umgebaut und versucht es so zu machen, dass ich nur einen GPIO setze und dann das Skript beende inkl. gpio.cleanup(). Das führt aber dazu, dass scheinbar nur minimalst das Relais schaltet: Ich höre weder das Klacken des Relais, noch sehe ich auch nur kurz das Lämpchen leuchten. Ich denke mal, es geht zuuu schnell.

Mit einem zweiten Skript, das eine Kopie des ersten ist, nur dass es den GPIO wieder auf HIGH setzt, hätte ich versucht es das Relais wieder öffnen zu können.

Lasse ich das gpio.cleanup() weg, so kommt immer die RuntimeWarning, dass "This channel is already in use...".

Code: Alles auswählen

#!/usr/bin/python
import RPi.GPIO as gpio
import time

PINS = [2, 3, 4, 17, 27, 22, 10, 9]

def main():
    try:
        gpio.setmode(gpio.BCM)
        gpio.setup(PINS, gpio.OUT, initial=gpio.HIGH) 
        gpio.output(2, gpio.LOW)
#        gpio.output(2, gpio.HIGH)
    except KeyboardInterrupt:
        pass
    finally:
        gpio.cleanup()  # this ensures a clean exit
        print('QUIT')
        
if __name__ == '__main__':
    main()
Gibt es keine Möglichkeit das so umzusetzen, dass ich den GPIO auf LOW setze, bis die nächste Anweisung kommt, die ihn wiederum auf HIGH setzt, außer per Dauerschleife und Zeitintervallen oder es per STRG+C abgebrochen wird?

Vielen Dank und viele Grüße

Dirk
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Das sind ja nicht unsere Anregungen. Du musst schon eine dauerhafte Schleife einführen. Sonst geht alles wieder auf Ursprung. Und wenn OpenHAB nur Skripte aufrufen kann, nützt dessen Stabilität dir ja nix. Denn da dein Skript auch auf Dauer laufen muss, und die OpenHAB Skripte dann nur damit kommunizieren, kann das krachen und OpenHAB hilft nichts.

Läuft das ganze auf einem Arduino mit zB Ethernet Shield oder serieller Verbindung mit OpenHAB kann der ja auch per Skript angesprochen werden.

Der gewichtige Unterschied: in dem kannst du dir merken wenn zB mehrere Stunden keine Kommunikation kam, und dann mal sicherheitshalber alles auf 0 stellen.
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

OpenHAB kann übrigens GPIOs: https://docs.openhab.org/addons/binding ... eadme.html

Die sind dann Switches. Ich würde dir raten deine Zeit darein zu investieren. Denn dein Python Skript muss ein Daemon sein, also dauerhaft laufen, und auf Netzwerkeingabn reagieren. Ein Beispiel zB hier:

https://github.com/openhab/openhab1-add ... ce/scripts

Und das anzupassen und aufzusetzen noch ein gutes Stück schwerer als dein akutes Problem.
dirk1312
User
Beiträge: 19
Registriert: Dienstag 1. November 2016, 18:16

Guten Morgen __deets__,

vielen Dank für deine Antworten.
__deets__ hat geschrieben: Mittwoch 16. Mai 2018, 23:38 Das sind ja nicht unsere Anregungen. Du musst schon eine dauerhafte Schleife einführen. Sonst geht alles wieder auf Ursprung.
Das alles wieder auf Anfang geht war ja meine Befürchtung und der Grund, warum ich dieses Thema hier gestartet hatte, um zu erfahren, ob es auch anders geht.
__deets__ hat geschrieben: Mittwoch 16. Mai 2018, 23:38 Und wenn OpenHAB nur Skripte aufrufen kann, nützt dessen Stabilität dir ja nix.
Das war mit dem Hintergrund gedacht, dass auf z.B. dem Pi nicht permanent eine Schleife läuft, sondern jede Ventilschaltung durch einen eigenen Skriptaufruf realisiert wird.
[/quote]
__deets__ hat geschrieben: Mittwoch 16. Mai 2018, 23:38 Läuft das ganze auf einem Arduino mit zB Ethernet Shield oder serieller Verbindung mit OpenHAB kann der ja auch per Skript angesprochen werden.
Achso, das ist dann wirklich rein Raspberry Pi bedingt, dass das nicht funktioniert. Ich wäre eher davon ausgegangen, dass das dann ein generelles "Problem" bzw. Art der Verwendung der GPIOs ist.
__deets__ hat geschrieben: Mittwoch 16. Mai 2018, 23:38 Der gewichtige Unterschied: in dem kannst du dir merken wenn zB mehrere Stunden keine Kommunikation kam, und dann mal sicherheitshalber alles auf 0 stellen.
Das ist ein guter Tipp. Danke!
__deets__ hat geschrieben: Mittwoch 16. Mai 2018, 23:46 OpenHAB kann übrigens GPIOs: https://docs.openhab.org/addons/binding ... eadme.html

Die sind dann Switches. Ich würde dir raten deine Zeit darein zu investieren. Denn dein Python Skript muss ein Daemon sein, also dauerhaft laufen, und auf Netzwerkeingabn reagieren. Ein Beispiel zB hier:

https://github.com/openhab/openhab1-add ... ce/scripts

Und das anzupassen und aufzusetzen noch ein gutes Stück schwerer als dein akutes Problem.
Vielen Dank! Dummerweise müsste dann OpenHAB auch auf dem Raspberry Pi (der in der Scheune hängt) laufen. Ich wollte den eigentlich im Keller, auf z.B. meinem Server laufen lassen. Remote den GPIO anzusprechen geht lauf OpenHAB community aber nicht - zumindest laut ein paar Forenbeiträgen, die ich gelesen habe.

Dann würde ich es aber für meine Tests doch mal so machen, dass ich doch zur Dauerschleife zurückkehre. Wenn das alles erfolgreich war, dann hole ich mir einen Adruino und schaue damit, wie und ob ich es in OpenHAB integrieren kann. Ansonsten würde eben erstmal weiter per Endlosschleife arbeiten.

Vielen Dank und viele Grüße

Dirk
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Für Arduino ist MQTT wohl ne gute Wahl. https://pubsubclient.knolleary.net/
dirk1312
User
Beiträge: 19
Registriert: Dienstag 1. November 2016, 18:16

Ich habe es jetzt so für meine Testzwecke umgesetzt:

Code: Alles auswählen

#!/usr/bin/python
import RPi.GPIO as gpio

pins = [2, 3, 4, 9, 10]
action_pin_mapper = {'a': 2, 'b': 3, 'c': 4, 'd': 9, 'e': 10, 'f': 2, 'g': 3, 'h': 4, 'i': 9, 'j': 10}

def open(pin):
    gpio.output(pin, gpio.LOW)
    return 'Opened Valve ' + str(pin)

def close(pin):
    gpio.output(pin, gpio.HIGH)
    return 'Opened Valve ' + str(pin)

def error(pin):
    print ('FEHLER: Keine erlaubte Aktion ausgewählt')

actions = {'a': open, 'b': open, 'c': open, 'd': open, 'e': open, 'f': close, 'g': close, 'h': close, 'i': close, 'j': close}

print(
'Buchstaben drücken folgende Aktion ausführen:\n' +
'a) Öffnen Mauer-Kreis\n' + 
'b) Öffnen Bohnen-Kreis\n' +
'c) Öffnen Apfelbaum-Kreis\n' +
'd) Öffnen Beet-Kreis\n' + 
'e) Öffnen Nussbaum-Kreis\n' +
'\n' +
'f) Schließen Mauer-Kreis\n' +
'g) Schließen Bohnen-Kreis\n' +
'h) Schließen Apfelbaum-Kreis\n' +
'i) Schließen Beet-Kreis\n' + 
'j) Schließen Nussbaum-Kreis'
)

def main(prompt):
    try:
        gpio.setmode(gpio.BCM)
        gpio.setup(pins, gpio.OUT, initial=gpio.HIGH) 

        while True:
            argument = raw_input(prompt)
            
            pin  = action_pin_mapper.get(argument, error)
            func = actions.get(argument, error)
            # Execute the function
            print func(pin)
    except KeyboardInterrupt:
        pass
    finally:
        gpio.cleanup()  # this ensures a clean exit
        print('QUIT')

if __name__ == '__main__':
    main('Action? ')
Das macht im Grunde ja genau das, was ich will: Ein Ventil wird geöffnet und es ist währenddessen auch möglich weitere zu öffnen, dieses zu schließen,... Sicher kann man es noch deutlich schöner machen, aber für meine Testzwecke reicht das völlig aus und so, wie ihr schon geschrieben habt, macht es hinsichtlich Stromverbrauch und Stabilität wohl mehr Sinn das für den produktiven Betrieb auf eine andere Plattform umzuziehen (und wohl das Skript nochmal ordentlich objektorientiert neu zu schreiben).
Sirius3
User
Beiträge: 17710
Registriert: Sonntag 21. Oktober 2012, 17:20

Mehr Objektorientierung ist bei Deinem Problem nicht nötig. Micro-Rechnern ist der Overhead für Objektorientierung auch zu groß. Statt zwei Wörterbücher action_pin_mapper und actions solltest Du nur eines haben, entweder mit Tupeln oder mit den Parametern schon per partial an die Funktionen gebunden. Statt Strings mit + und str zusammenzustückeln solltest Du .format benutzen. Der Rückgabewert von `close` ist falsch.
dirk1312
User
Beiträge: 19
Registriert: Dienstag 1. November 2016, 18:16

Guten Morgen,
Sirius3 hat geschrieben: Mittwoch 23. Mai 2018, 07:58 Mehr Objektorientierung ist bei Deinem Problem nicht nötig. Micro-Rechnern ist der Overhead für Objektorientierung auch zu groß.
danke für deine Hinweise. Dann werde ich das wohl sein lassen. ;)
Sirius3 hat geschrieben: Mittwoch 23. Mai 2018, 07:58 Statt zwei Wörterbücher action_pin_mapper und actions solltest Du nur eines haben, entweder mit Tupeln oder mit den Parametern schon per partial an die Funktionen gebunden.
Mit tuple kann ich noch was anfangen, was aber genau du mit "Parameter schon per partial an die Funktionen gebunden" meinst, aber nicht wirklich. So wie ich es verstehe wäre beim tuple der einzige Vorteil im Vergleich zu meinem assoziativen Array, dass eine eine unveränderbare Liste ist?! Ich hatte es zunächst mit ineinander geschachtelten Listen probiert:

Code: Alles auswählen

{'a': {'pin': 2, 'callback': open}, 'b': ...}
hatte dann aber Probleme mit dem Aufruf

Code: Alles auswählen

            config = actions.get(argument, error)
            func = config['callback']
            # Execute the function
            print func(pin)
Den genauen Fehler weiß ich aber nicht mehr aus dem Stehgreif.
Sirius3 hat geschrieben: Mittwoch 23. Mai 2018, 07:58 Statt Strings mit + und str zusammenzustückeln solltest Du .format benutzen. Der Rückgabewert von `close` ist falsch.
Danke, das mit dem .format werde ich noch umsetzen. Den Rückgabewert von close hatte ich auf dem Raspberry Pi geändert, nachdem ich mir erstmals die Meldungen durchgelesen habe. ;)

Vielen Dank und viele Grüße

Dirk
Sirius3
User
Beiträge: 17710
Registriert: Sonntag 21. Oktober 2012, 17:20

Ja, Wörterbücher gehen genauso, bei zwei Werten aber noch nicht zwingend:

Code: Alles auswählen

            config = actions.get(argument, error)
            func = config['callback']
            pin = config['pin']
            # Execute the function
            print func(pin)
oder mit `partial`:

Code: Alles auswählen

from functools import partial

ACTIONS = {'a': partial(open, 2), 'b': ...}

...

action = ACTIONS.get(argument, error)
print action()
BTW: pins und actions sind Konstanten, werden also per Konvention komplett GROSS geschrieben.
dirk1312
User
Beiträge: 19
Registriert: Dienstag 1. November 2016, 18:16

Sehr cool, danke! Deine Änderungsvorschläge mit Partials und die Namenskonvention habe ich vorgenommen. Jetzt klappt's auch mit der Fehler-Meldung ohne einen Warning zu erzeugen. So sieht das Skript jetzt aus:

Code: Alles auswählen

#!/usr/bin/python
import RPi.GPIO as gpio
from functools import partial

PINS     = [2, 3, 4, 9, 10]
CONTROLS = """Buchstaben drücken um Aktion auszuführen:
a) Öffnen Mauer-Kreis
b) Öffnen Bohnen-Kreis
c) Öffnen Apfelbaum-Kreis
d) Öffnen Beet-Kreis
e) Öffnen Nussbaum-Kreis

f) Schließen Mauer-Kreis
g) Schließen Bohnen-Kreis
h) Schließen Apfelbaum-Kreis
i) Schließen Beet-Kreis
j) Schließen Nussbaum-Kreis"""

def open(pin):
    gpio.output(pin, gpio.LOW)
    return 'Ventil an Pin ' + str(pin) + ' geöffnet'

def close(pin):
    gpio.output(pin, gpio.HIGH)
    return 'Ventil an Pin ' + str(pin) + ' geschlossen'
    
def help():
    return CONTROLS

def error():
    print ('FEHLER: Keine erlaubte Aktion ausgewählt.\n\n' + CONTROLS)

ACTIONS = {
    'a': partial(open, 2), 
    'b': partial(open, 3), 
    'c': partial(open, 4), 
    'd': partial(open, 9), 
    'e': partial(open, 10), 
    'f': partial(close, 2), 
    'g': partial(close, 3), 
    'h': partial(close, 4), 
    'i': partial(close, 9), 
    'j': partial(close, 10), 
    'help': help
}

print(CONTROLS)

def main(prompt):
    try:
        gpio.setmode(gpio.BCM)
        gpio.setup(PINS, gpio.OUT, initial=gpio.HIGH) 

        while True:
            argument = raw_input(prompt)

            func = ACTIONS.get(argument, error)
            # Execute the function
            print func()
    except KeyboardInterrupt:
        pass
    finally:
        gpio.cleanup()  # this ensures a clean exit
        print('QUIT')

if __name__ == '__main__':
    main('Action? ')
Vielen Dank nochmals und viele Grüße

Dirk
Antworten