mqtt nachrichten empfangen, auswerten und mit RS485 Schnittstelle versenden

Python auf Einplatinencomputer wie Raspberry Pi, Banana Pi / Python für Micro-Controller
Antworten
Tola-Emma
User
Beiträge: 8
Registriert: Freitag 21. Februar 2025, 17:17

Hallo ihr Lieben,
entweder bin ich schon zu alt oder zu dumm. Nomalerweise programmiere ich S7 / Omron Steuerungen. Nun dachte ich ich probiere mich mal an Python heran. Aber ich scheitere schon an kleinigkeiten.
Ich versuche Gerade mqtt Nachrichten zu empangen ( was auch funktioniert) und diese mit IF in einer Funktion zu vergleichen, damit ich dieses Ergebnis per RS485 Schnittstelle versenden kann. Zur Zeit bekomme ich aber das Ergebnis aus meinen Funktion (mqtt_auswahl) nicht weiter verabeitet.

Code: Alles auswählen

import paho.mqtt.client as mqtt


def mqtt_verbindung(client, userdata, flags, rc):  # Verbindung zu mqtt Nachrichten
    client.subscribe('/RS485/empfang/daten/Tasten/Innenlicht')
    client.subscribe('/RS485/empfang/daten/Tasten/Aussenlicht')
    client.subscribe('/RS485/empfang/daten/Tasten/Wasserpumpe')

def mqtt_nachricht(client, userdata, message): # gibt die mqtt Nachricht als String heraus
    msg = str(message.payload.decode("utf-8")) 
    print("message received: ", msg) 
    print("message topic: ", message.topic) 
    return message.topic, msg

def mqtt_auswahl(mqtt_nachricht):    # vergleicht mqtt Nachricht auf wahr und gibt den RS485 String heraus

    if message.topic == '/RS485/empfang/daten/Tasten/Innenlicht' and msg == 'true':
        antwort = str(b'\xff\x01\x00\x00\x00')
    elif message.topic == '/RS485/empfang/daten/Tasten/Aussenlicht' and msg == 'true':
        antwort = str(b'\xff\x02\x00\x00\x01')
    elif message.topic == '/RS485/empfang/daten/Tasten/Wasserpumpe' and msg == 'true':
        antwort = str(b'\xff\x04\x00\x00\x03')
    else:
        antwort = ''
    return antwort

print(mqtt_auswahl) 

BROKER_ADDRESS = "localhost"
client = mqtt.Client()
client.on_connect = mqtt_verbindung
client.on_message = mqtt_nachricht
client.connect(BROKER_ADDRESS)


client.loop_start()
Als Antwort wird mir Folgengendes ausgegeben:

Code: Alles auswählen

<function mqtt_auswahl at 0x76059b20>
/home/pi/mu_code/mqtt_empfangen2.py:33: DeprecationWarning: Callback API version 1 is deprecated, update to latest version
  client = mqtt.Client()
>>> message received:  true
message topic:  /RS485/empfang/daten/Tasten/Innenlicht
Eigendlich hatte ich gedacht das mir bei "print(mptt_auswahl) der HexString angezeigt wird, bzw ich dieses weiterverwenden kann.

Lieben Gruß
Tola-Emma
Benutzeravatar
__blackjack__
User
Beiträge: 13998
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Tola-Emma: Wenn man eine Funktion mit `print()` ausgibt, wird die Funktion ausgegeben. Also das Funktionsobjekt selbst. Was in diesem Fall eine Zeichenkettendarstellung der Funktion ist, also die `repr()`-Form von einem definierten Objekt: Der Typ, der Name unter dem es definiert wurde, und eine eindeutige ID, damit man auch Objekte mit dem gleichen Definitionsnamen noch auseinanderhalten kann, in ”spitzen Llammern”. Sieht man ja in der Ausgabe.

Und dieses `print()` passiert ganz am Anfang des Hauptprogramms, noch bevor das `Client()`-Objekt erstellt wurde. Ein `print()` führt nicht auf magische Weise irgendwann später mal zu ausgaben. Das führt genau dann zu einer Ausgabe wenn es ausgeführt wird. Und kann zu dem Zeitpunkt nichts ausgeben was es zu dem Zeitpunkt noch gar nicht gibt.

Die Funktion `mqtt_auswahl()` wird ja auch überhaupt nicht aufgerufen, weder direkt, noch wird sie irgendwo als Rückruffunktion übergeben. Also kann die auch niemals ausgeführt werden.

Die Funktion bekommt ein Argument übergeben, welches überhaupt nicht verwendet wird, verwendet selbst aber zwei Namen `message` und `msg` die überhaupt nicht definiert sind. `message` und `msg` sind als Namen auch nicht so wirklich sinnvoll, weil man als Leser durcheinander kommen kann was denn der Unterschied zwischen der Nachricht und der Nachricht ist.

Wo wir gerade bei Namen sind: Funktionen und Methoden werden üblicherweise nach der Tätigkeit benannt die sie durchführen. Damit der Leser weiss was da passiert, und damit sie leicht von eher passiven Werten unterscheiden kann. `mqtt_nachricht` wäre ein passender Name für ein Objekt, das eine MQTT-Nachricht repräsentiert. Weniger für eine Funktion die etwas mit so einer Nachricht anstellt. `mqtt_verbindung` wäre ein passender Name für eine Verbindung. Statt einer Tätigkeit gibt es bei Rückruffunktionen noch das übliche Muster `on_<ereignis>` für Reaktionen auf bestimmte Ereignisse. Das `Client`-Objekt macht es mit `on_connect()` und `on_message()` vor.

Bei `mqtt_nachricht()` macht das ``return`` keinen Sinn. Die Funktion wird vom `Client`-Objekt aus aufgerufen wenn eine neue Nachricht kommt. Und das `Client`-Objekt erwartet von dieser Rückruffunktion keine Werte als Ergebnis. Damit passiert also nichts.

Sämtliche Aufrufe von `str()` sind falsch. Bei einer Zeichenkette ist der Aufruf von `str()` einfach nur sinnlos — das ist ja bereits eine Zeichenkette. Die Zeichenkettendarstellung von literalen `bytes`-Objekten macht keinen Sinn, da hätte man dann ja gleiche eine Zeichenkette mit dem Inhalt schreiben können. Was aber in aller Regel ebenfalls keinen Sinn macht.

Man sollte keine Daten wiederholen. Die Topics sollten *einmal* als Konstanten definiert werden, statt mehrfach als laaange Zeichenkettenliterale im Code zu stehen, wo schon beim schreiben die Gefahr besteht, dass man sich vertippt, und dann jedes mal wenn man da etwas dran ändern will oder muss.

Wenn man in allen ``if``/``elif``-Answeisungen immer die gleiche Teilbedingung prüft, sollte man die dort herausziehen und einmal vorher prüfen. So eine Abbildung von einem Wert auf einen anderen Wert löst man eher mit einem Wörterbuch anstelle eines ``if``/``elif``-Konstrukts.

Zwischenstand (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
import paho.mqtt.client as mqtt

BROKER_ADDRESS = "localhost"

BASE_TOPIC = "/RS485/empfang/daten/Tasten/"
INNER_LIGHT_TOPIC = BASE_TOPIC + "Innenlicht"
OUTER_LIGHT_TOPIC = BASE_TOPIC + "Aussenlicht"
WATERPUMP_TOPIC = BASE_TOPIC + "Wasserpumpe"

TOPIC_TO_ANSWER = {
    INNER_LIGHT_TOPIC: b"\xff\x01\x00\x00\x00",
    OUTER_LIGHT_TOPIC: b"\xff\x02\x00\x00\x01",
    WATERPUMP_TOPIC: b"\xff\x04\x00\x00\x03",
}


def get_answer(message):
    if message.payload == b"true":
        return TOPIC_TO_ANSWER.get(message.topic, b"")

    return b""


def on_connect(client, _userdata, _flags, _rc):
    client.subscribe(INNER_LIGHT_TOPIC)
    client.subscribe(OUTER_LIGHT_TOPIC)
    client.subscribe(WATERPUMP_TOPIC)


def on_message(_client, _userdata, message):
    print("message received:", message.payload.decode("utf-8"))
    print("message topic:", message.topic)
    answer = get_answer(message)
    print("answer:", answer)


def main():
    client = mqtt.Client()
    client.on_connect = on_connect
    client.on_message = on_message
    client.connect(BROKER_ADDRESS)
    client.loop_start()


if __name__ == "__main__":
    main()
Die Warnung mit der API sollte man auch ernst nehmen. Ich habe anscheinend eine neuere Version von `paho-mqtt` auf dem Rechner: bei mir kommt statt der Warnung bereits ein Fehler an der Stelle.
“The best book on programming for the layman is »Alice in Wonderland«; but that's because it's the best book on anything for the layman.” — Alan J. Perlis
Tola-Emma
User
Beiträge: 8
Registriert: Freitag 21. Februar 2025, 17:17

@__blackjack__: Als erstes wollte ich mal wieder ein großes Lob aussprechen, das Du dir soviel Zeit nimmst, einen "alten Anfänger" zu unterstützen und damit mir etwas beibringst.
Könntes Du mir noch erzählen wie ich das Ergebnis von "get_answer" von "bytes" in" bytearray" umwandeln kann, da meine RS485 Schnittstelle diese verlangt.
Lieben Gruß
Tola-Emma
Tola-Emma
User
Beiträge: 8
Registriert: Freitag 21. Februar 2025, 17:17

Nachtrag zur oben gestellten Frage:

Code: Alles auswählen

#!/usr/bin/env python3
import paho.mqtt.client as mqtt
import serial
import time
from RPi import GPIO


EN_485 = 4
BROKER_ADDRESS = "localhost"


BASE_TOPIC = "/RS485/empfang/daten/Tasten/"
INNER_LIGHT_TOPIC = BASE_TOPIC + "Innenlicht"
OUTER_LIGHT_TOPIC = BASE_TOPIC + "Aussenlicht"
WATERPUMP_TOPIC = BASE_TOPIC + "Wasserpumpe"

TOPIC_TO_ANSWER = {
    INNER_LIGHT_TOPIC: b"\xff\x01\x00\x00\x00",
    OUTER_LIGHT_TOPIC: b"\xff\x02\x00\x00\x01",
    WATERPUMP_TOPIC: b"\xff\x04\x00\x00\x03",
}


def get_answer(message):
    if message.payload == b"true":
        return TOPIC_TO_ANSWER.get(message.topic, b"")

    return b""


def on_connect(client, _userdata, _flags, _rc):
    client.subscribe(INNER_LIGHT_TOPIC)
    client.subscribe(OUTER_LIGHT_TOPIC)
    client.subscribe(WATERPUMP_TOPIC)


def on_message(_client, _userdata, message):
    answer = get_answer(message)
    

def main():
    try:
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(EN_485, GPIO.OUT, initial=GPIO.HIGH)

        with serial.Serial("/dev/ttyS0", 38400, rtscts=True) as connection:
            while True:
                GPIO.output(EN_485, GPIO.HIGH)
                time.sleep(1)
                connection.write(get_answer)
                connection.flush()
                time.sleep(1)
                GPIO.output(EN_485, GPIO.LOW)
                data = connection.read(20)
                
        client = mqtt.Client()
        client.on_connect = on_connect
        client.on_message = on_message
        client.connect(BROKER_ADDRESS)
        client.loop_start()
    finally:
        GPIO.cleanup()    

if __name__ == "__main__":
    main()
Und als Fehlermeldung kommt dieses:

Code: Alles auswählen

Traceback (most recent call last):
  File "/home/pi/mu_code/mqtt_empfangen4.py", line 65, in <module>
    main()
  File "/home/pi/mu_code/mqtt_empfangen4.py", line 50, in main
    connection.write(get_answer)
  File "/usr/lib/python3/dist-packages/serial/serialposix.py", line 598, in write
    d = to_bytes(data)
  File "/usr/lib/python3/dist-packages/serial/serialutil.py", line 68, in to_bytes
    return bytes(bytearray(seq))
TypeError: cannot convert 'function' object to bytearray
>>> 
Im nächsten Schritt wollte ich das Sendesignal zur RS485 schnittstelle in einem Loop einbinden( Die andere Seite der RS485 Schnittstelle verlangt als Ping den Code" \xff\x00\x00\x00\xff") und wenn ein Signal(z.b. Innenlicht)vom mqtt erfolgt, soll diese dazwischen gesetzt werden.

Lieben Gruß
Tola-Emma
Benutzeravatar
__blackjack__
User
Beiträge: 13998
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Tola-Emma: Du hast komische Vorstellungen davon in welcher Reihenfolge Code ausgeführt wird und das irgendwie magisch Sachen gemacht werden die da nicht stehen. Das Problem ist nicht `bytes` vs. `byteaeeay` — die sind in diesem Fall problemlos austauschbar.

Du versuchst die *Funktion* über die serielle Schnittstelle zu senden. Nicht deren *Rückgabewert* wenn man sie denn *aufrufen* würde. Dazu braucht man die Nachricht als Argument. An der Stelle wo das senden über die serielle Schnittstelle steht, gibt es aber noch gar keine Nachricht. Diese Endlosschleife verhindert sogar, dass die Nachricht jemals empfangen werden kann, weil *nach* dieser Schleife überhaupt erst das `Client`-Objekt erstellt wird.

Und die `on_message()` und die `get_answer()`-Funktionen kann man sich auch in der Form sparen wenn mit `answer` dann überhaupt gar nichts gemacht wird. In der `on_message()` müsste der Code stehen der etwas mit der Nachricht macht.
“The best book on programming for the layman is »Alice in Wonderland«; but that's because it's the best book on anything for the layman.” — Alan J. Perlis
Tola-Emma
User
Beiträge: 8
Registriert: Freitag 21. Februar 2025, 17:17

@__blackjack__: Ich habe mir das alles etwas einfacher vorgestellt, in der S7 Welt hat man zwar auch ein Hauptprogramm (OB) und Unterprogramme (FB/FC). diese werden von oben nach unten abgearbeitet. Bei Python habe ich da so meine Probleme mit den "def Funktionen". Ist die "main Funktion " wie da Hauptprogram und die andern Funktionen wie die Unterprogramme?
Ich habe für meine Pythonanwendung einzelne Programme( RS485-Senden, RS485-Empfang, mqtt-Senden, mqtt-Empfang), nun versuche ich diese zusammenzufügen.
Mein Probleme habe ich mit den "def Funktionen" und wie ich die Variablen herausbekomme und an andere Stelle wieder benutzen kann.

Damit Du verstehtst was ich vorhabe, hier der Ablaufplan:

1. meine Raspberry sendet alle Sekunde auf der RS485 Schnittstelle einen String "\xff\x00\x00\x00\xff" zur Wohnmobilsteuerung

2. daraufhin bekomme ich einen Anwortstring(Beispiel) aus der Wohnmobilsteuerung "\xff\x00\x00\x00 \xff\x00\x20\x00\x10\x00\x00\x00\x00\x8e\xf\x01\x00\x00\x00\xbe ",
dort sind alle Information über Batteriespannungen, Wassertankinhalt, Information ob Licht/Pumpe an sind,... enthalten

4. Dieser Antwortstring wird auf Checksumme überprüft und in die einzelnen Informationen aufgeteilt und per mqtt versendet und im Display abgerufen

5. Wenn ich nun am Display (Node-Red Aufbau) einen Dashboard-Button (zb. INNENLICHT) betätige sollte der String für INNENLICHT "\xff\x01\x00\x00\x00" unter (Punkt 1) zwischengeschoben werden und die Wohnmobilsteuerung schaltet das Innenlicht ein und gibt dieses wieder per Antwortstring aus der Steuerung herraus.

Vieleicht kannst du mir mit deinem riesen Fachwissen dabei helfen diese umzusetzen.

Lieben Gruß
Tola-Emma
Benutzeravatar
noisefloor
User
Beiträge: 4172
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,
Bei Python habe ich da so meine Probleme mit den "def Funktionen". Ist die "main Funktion " wie da Hauptprogram und die andern Funktionen wie die Unterprogramme?
Von Prinzip ja. Aber: es ist grundsätzlich ein Fehler, wenn man irgendwie versucht, das Konzept von Programmiersprache $FOO zwanghaft auf Sprache $BAR zu übertragen. Das geht in der Regel schief, weil man sich dann eben _nicht_ auf Sprache $BAR einlässt und damit _nicht_ deren volles potential ausschöpft.

`def` definiert eine Funktion in Python. "Unterprogramme" bzw. den Begriff "Unterprogramme" gibt es nicht in Python. Wenn du die Funktion aufrufst, wird deren Inhalt ausgeführt, Funktionen kann man Werte mitgeben. Ob die zwingend oder optional sind legst du beim Anlegen der Funktion fest. Funktionen können via `return` Werte zurückgeben.

Wenn du ein Python-Programm ausführst, wird _nur_ der Code ausgeführt, der auf oberster Ebene steht. Das ist quasi das, was du als "Hauptprogramm" bezeichnest, auch wenn es diesen Begriff in der Form in Python nicht wirklich gibt.

Das sind aber alles ziemliche Basics. Die musst du verstehen, sonst wird das mit dir und Python nicht. Falls du es noch nicht gemacht hast: unbedingt das Python-Tutorial durcharbeiten (alternativ: deutsche Übersetzung), danach sollte das alles klarer sein.

Gruß, noisefloor
Benutzeravatar
__blackjack__
User
Beiträge: 13998
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Tola-Emma: Auch bei Python wird der Code grundsätzlich von oben nach unten abgearbeitet. Darum bin ich ja so verwirrt warum Du erwartest, dass wenn man am Anfang ``print(eine_funktion)`` ausführt, das nicht genau *dann* *einmal* einsgeführt wird, sondern später wenn Nachrichten empfangen werden, und dann auch noch für jede Nachricht. Und das die Argumente da irgendwie auf magische Weise ihren Weg in die Funktion finden, obwohl die Funktion gar nicht aufgerufen wurde, mit konkreten Werten. Da müsste Python schon ziemlich viel hellseherische Fähigkeiten haben, um das zu machen was Du willst, ohne dass Du das im Code auch sagst.

Funktionen sind in der Tat etwas ähnliches zu dem was andere Programmiersprachen als Unterprogramme bezeichnen. Je nach Programmiersprache ist die Semantik aber leicht unterschiedlich, beispielsweise was die Sichtbarkeit von Variablen angeht. Klassische BASIC-Dialekte haben beispielsweise oft nur einen Namensraum den sich der komplette Code des Programms teilt. Funktionen sind in sich geschlossen, dass heisst beispielsweise lokale Namen sind nur innerhalb der Funktion bekannt. Funktionen haben einen Rückgabewert, mit dem der Aufrufer etwas machen kann, sofern der Aufrufer denn so etwas erwartet. Was bei Rückruffunktionen oft nicht der Fall ist, und falls doch, dann nur etwas allgemeines was in der Dokumentation zum Rahmenwerk steht. Aber eine MQTT-Bibliothek hat ja beispielsweise keine Ahnung das man vielleicht etwas über eine davon völlig unabhängige serielle Schnittstelle senden möchte.

Man kann in der Regel nicht einzelne Programme so einfach irgendwie nach Schema F zu einem Programm zusammenfassen. Man muss da dann schon *ein* Programm schreiben, mit dem passenden Programmablauf.

Variablen bekommt man aus Funktionen nicht heraus. Man kann Werte als Rückgabewerte haben. Sofern der Aufrufer das erwartet und etwas damit anfangen kann. Das ist wie gesagt bei den Rückruffunktionen nicht der Fall. Wenn Du etwas versenden willst was in einer MQTT-Nachricht steht, dann muss das in der Funktion passieren, die aufgerufen wird, wenn eine Nachricht ankommt. Entweder direkt, oder in einer Funktion die indirekt von dort aufgerufen wird.

Nach Deinen 5 Punkten willst Du ein Programm bei dem zwei Sachen gleichzeitig passieren, das heisst Du bastelst Dir das entweder mit `Client.loop()`, was laut Dokumentation nicht mehr empfohlen wird, oder man verwendet Threads was komplexer ist. In dem Fall würde ich den Client in einem Thread starten, und eventuell auch die sekündliche Kommunikation um den Status abzufragen, und das dann über eine Queue in einer eigenen Hauptschleife im Hauptthread zusammenführen.
“The best book on programming for the layman is »Alice in Wonderland«; but that's because it's the best book on anything for the layman.” — Alan J. Perlis
Benutzeravatar
DeaD_EyE
User
Beiträge: 1219
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

Der OB1 ist sozusagen das Programm, welches durch den Python-Interpreter ausgeführt wird.
Der Unterschied ist, dass Python-Programm keine eingebaute Endlosschleife hat.
Wenn das Programm zu Ende ist und keine weiteren Statements mehr folgen, wird der Interpreter beendet.

Bei threading ist es komplizierter. Erst wenn alle nicht-Daemon-Threads fertig sind und auch im MainThread nichts mehr abzuarbeiten ist, wird das Programm beendet.
paho.mqtt nutzt soweit ich weiß einen Daemon-Thread. D.h. ohne Hauptschleife endet das Programm einfach.

Bei paho.mqtt hat man aber auch die Möglichkeit ohne Threads zu arbeiten, in dem man seine eigene Endlosschleife erstellt und die Update-Funktion des Clients aufruft. Der Client sendet z.B. regelmäßig einen Ping, damit die andere Seite weiß, dass er noch da ist.

Ein FC ist eine Funktion ohne Gedächtnis und ähnelt Funktionen in Python (def).
Ein FB ist ein Funktionsbaustein mit Gedächtnis und ist bei Python mit einer Klasse (class) vergleichbar, wobei die Klasse in Python viel mehr Möglichkeiten bietet.

Anweisungen werden von oben nach unten abgearbeitet. Zuerst wird alles in Python-Bytecode übersetzt, dann wird der Python-Bytecode durch den Python-Interpreter ausgeführt. Dieser Prozess ist vergleichbar, wenn man seinen KOP/FPU/SCL übersetzt. Bei der S7-1x00 ist nicht mehr AWL, sondern irgendeine proprietäre Sprache von Siemens, die unter anderem darauf ausgerichtet ist, Datenstrukturen zu optimieren. Deswegen funktioniert auch der Zugriff mit Snap7 auf optimierte Datenbausteine nicht.

Dann musst du noch zwischen Funktionsdefinition und Funktionsaufruf unterscheiden. Definierte Funktionen muss man aufrufen, damit die Funktion ausgeführt wird.
Die Funktionsdefinition ist mit dem Programmordner vergleichbar. FCs und FBs, die dort enthalten sind, können durch andere Bausteine aufgerufen werden.

Welche Datenstruktur sendest du? Das mit den Topics ist zu kryptisch.
Übers Netzwerk kann man nur ganze Bytes senden. D.h. wenn du nur einen Status (bool) abrufen willst, wird ein Byte an Nutzdaten übertragen.
Eine SPS, die z.B. 32 Eingänge hat, kann die 4 Bytes problemlos übers Netzwerk senden.
sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
Tola-Emma
User
Beiträge: 8
Registriert: Freitag 21. Februar 2025, 17:17

@__Blackjack__: ich habe gerade gelernt ( Komentar von @DeaD_EyE) das Python keine Endlosschleife hat, wie bei der S7 / Omron Programmieung und deshalb komme ich mit den Variablen auch nicht so klar, weil das Programm unten endet und nicht wieder oben anfängt.
Ich glaube mit der Threads-Programmierung komme ich noch nicht klar (so weit bin ich noch nicht). Habe gerade eine Anfrage in unserer IT Abteilung von der Arbeit nachgefragt ob jemand Python programmieren kann.
Tola-Emma
User
Beiträge: 8
Registriert: Freitag 21. Februar 2025, 17:17

@DeaD_EyE: Danke für den Hinweis das Python keine Endlosschleife wie bei der S7 / Omron Programmierung besitzt, den das war mir nicht klar.
Die Daten die ich per mqtt an Node-Red sende sind in Bytes aufgeteilt und wenn ich den Sting der RS485 Schnittstelle simuliere kommen alle Werte auch an.
Antworten