Mit ModbusTCP & Python Prozessdaten aus der LOGO!8 auslesen

Siemens Logo wird für ModbusTCP konfiguriert

In dem vorigen Beitrag Mit Python Prozessdaten aus der LOGO!8 auslesen (Snap7) haben wir uns angeschaut, wie wir mit der Bibliothek Snap7 und dem zugehörigen Python Wrapper auf die Prozessdaten der Logo über das S7 Protokoll zugreifen können. Die Logo wurde dabei als Server konfiguriert und unser Python Skript auf dem PC hat als Client Daten gelesen und geschrieben.

In diesem Beitrag werden wir auch auf die internen Daten der Logo zugreifen, jedoch über ModbusTCP. Wir richten die Logo als ModbusTCP Server ein und greifen - auch hier - als Client über ein Python Skript darauf zu. Als Python Bibliothek verwenden wir pyModbusTCP.

1. Logo8 als ModbusTCP Server einrichten

Als Ausgangspunkt dient ein Netzwerkprojekt, bei dem alle Komponenten (hier: Logo8 und TDE Display) bereits angelegt wurden und vernetzt sind. Um nun den ModbusTCP Server der Logo einzurichten klickt man (mit der rechten Maustaste) auf einen freien blauen Slot unterhalb der Logo: Add server connection... => Modbus Connection

Als nächstes öffnet sich ein neues Fenster, welches die aktuelle Konfiguration grafisch darstellt. Die Logo wird als ModbusTCP Server mit der bereits eingestellten IP Adresse gezeigt, ihr gegenüber der ModbusTCP Client (unser Python Skript). Der Server-Port den die Logo verwendet ist fix im Bereich 502...509. Entscheidet man sich für den ersten blauen Slot, erhält man den Port 502. Da ich in diesem Beispiel den zweiten Slot für den Server ausgewählt habe, erhalte ich den Port 503.

Möchte man den Zugriff auf die Logo auf einen ModbusTCP Client beschränken, kann man dessen IP Adresse in der Konfiguration der Logo mitgeben. In diesem Beispiel legen wir uns nicht fest, sondern erlauben "jeden" Client. Dies wird mit einem Klick auf die Checkbox "Accept all connection request in server side" bestätigt. Sobald wir das Fenster nun schließen, sehen wir an dem vorig ausgewählten Slot ein "gelbes Fähnchen". Dies repräsentiert den ModbusTCP Server.

Sobald man nun das Projekt auf die Logo überträgt, wird vor dem Öffnen des ModbusTCP Ports gewarnt (je nach o.g. Konfiguration 502...509). Dies muss letztendlich bestätigt werden, damit der Port und somit der ModbusTCP Sever auf der Logo erreichbar ist. Die Konfiguration von ModbusTCP ist damit abgeschlossen.

2. Anmerkungen zur IT-Security

Logo Soft Comfort warnt zuverlässig beim öffnen von Ports in der Logo und betont dabei auch das einhergehende Sicherheitsrisiko! Trotzdem möchte ich hier nochmal hervorheben, dass auf die Absicherung der Netzsegmente – in denen SPS oder andere kritische Steuerungen angebunden werden – auf keinen Fall verzichtet werden darf. Ein umfangreiches IT-Security Konzept sowie dessen Umsetzung ist elementar für den Betrieb von SPS Steuerungen im produktiven Umfeld.

Um nun auf die Register der Logo zuzugreifen, benötigen wir noch die Übersicht über die verwendeten Register und Coils sowie deren Adressen. Diese können wir aus Logo Soft Comfort nehmen, indem wir in die Einstellungen der Logo schauen und in dem Fenster den Menüpunkt Modbus address space auswählen. Ich habe weiter unten ein Logo8 ModbusTCP Registermapping Cheat Sheet zum Download zur Verfügung gestellt, welches die Adressierung ausführlich in tabellarischer Form zeigt und beim Programmieren gut zur Hilfe genommen werden kann.

4. Auf ModbusTCP Register mit Python zugreifen

Als erstes installieren wir die ModbusTCP-Python-Bibliothek, um als Client auf die Logo zugreifen zu können. Die Bibliothek läuft auf Windows, Linux und macOS, auch auf ARM basierten Linux Devices (z.B. Raspberry Pi).

pip install pyModbusTCP

Jetzt können wir auch schon mit unserem Python Skript beginnen, auf die Register zuzugreifen. Wichtig: Die Logo implementiert den ModbusTCP +1 Register Offset. Für unser Python Skript bedeutet das, dass wir die Modbus Adressen nicht 1:1 übernehmen können, sondern in unserem Skript von der im LSC genannten Adresse immer 1 subtrahieren müssen. Dies können wir aber durch eine Funktion konsistent in unserem Client Skript abbilden:

def lg8add(logo_modbustcp_address: int) -> int:
    """
    Logo8 uses modbusTCP +1 address offset
    to stay conform with their documentation, subtract 1 from each given address
    """
    return logo_modbustcp_address-1

In dem folgenden Beispiel verbinden wir uns auf die Logo, lesen die digitalen Eingänge 1...3 aus, setzen die digitalen Ausgänge 1 & 2 auf True, lesen anschließend ihren Status und lesen außerdem den Zustand der analogen Eingänge 1...4 aus. In dem LSC Screenshot weiter oben sind die digitalen Eingänge/Ausgänge gar nicht "programmiert", trotzdem kann man auf diese über ModbusTCP zugreifen.

Zur Veranschaulichung und Verwendung der Python Library lesen wir die analogen Eingänge dreifach, siehe dazu den LSC Screenshot (oben):

  1. über den direkten Zugriff auf den AI (analogen Input)
  2. über den VM-Parameter Zugriff des einprogrammierten Verstärkers
  3. über analoge Merker (AM)

Um auf die VM-Parameter zuzugreifen, müssen diese vorher über Variable Memory Configuration festgelegt werden. Die Einstellung ist im LSC unter Tools => Parameter VM Mapping... zu finden.

from pyModbusTCP.client import ModbusClient

def lg8add(logo_modbustcp_address: int) -> int:
    """
    Logo8 uses modbusTCP +1 address offset
    to stay conform with their documentation, subtract 1 from each given address
    """
    return logo_modbustcp_address-1


lg8 = ModbusClient(host="1.1.2.1", port=503, auto_open=True, auto_close=True)
# ->  logo 8 read digital inputs <- #
di1 = lg8.read_discrete_inputs(lg8add(1), 1) # read DI 1
di2 = lg8.read_discrete_inputs(lg8add(2), 1) # read DI 2
di3 = lg8.read_discrete_inputs(lg8add(3), 1) # read DI 3

# -> logo 8 write digital outputs <- #
lg8.write_single_coil(lg8add(8193), True) # write DO 1
lg8.write_single_coil(lg8add(8194), True) # write DO 1

# -> logo 8 read digital outputs <- #
do1 = lg8.read_coils(lg8add(8193), 1) # read DO 1 state
do2 = lg8.read_coils(lg8add(8194), 1) # read DO 2 state

# -> logo 8 read analog inputs <- #
ai1 = lg8.read_input_registers(lg8add(1), 1) # read AI 1 directly
ai1_vmmapping = lg8.read_holding_registers(lg8add(1), 1) # read AI 1 using parameter VM mapping - VW 0
am1 = lg8.read_holding_registers(lg8add(529), 1) # read AI 1 using AM1

ai2 = lg8.read_input_registers(lg8add(2), 1) # read AI 2 directly
ai2_vmmapping = lg8.read_holding_registers(lg8add(2), 1) # read AI 2 using parameter VM mapping - VW 2
am2 = lg8.read_holding_registers(lg8add(530), 1) # read AI 2 using AM2

ai3 = lg8.read_input_registers(lg8add(3), 1) # read AI 3 directly
ai3_vmmapping = lg8.read_holding_registers(lg8add(3), 1) # read AI 3 using parameter VM mapping - VW 4
am3 = lg8.read_holding_registers(lg8add(531), 1) # read AI 3 using AM3

ai4 = lg8.read_input_registers(lg8add(4), 1) # read AI 4 directly
ai4_vmmapping = lg8.read_holding_registers(lg8add(4), 1) # read AI 4 using parameter VM mapping - VW 6
am4 = lg8.read_holding_registers(lg8add(532), 1) # read AI 4 using AM4

print("DI 1: ", str(di1))
print("DI 2: ", str(di2))
print("DI 3: ", str(di3))

print("DO 1: ", str(do1))
print("DO 2: ", str(do2))

print("AI 1: ", str(ai1), str(ai1_vmmapping), str(am1))
print("AI 2: ", str(ai2), str(ai2_vmmapping), str(am2))
print("AI 3: ", str(ai3), str(ai3_vmmapping), str(am3))
print("AI 4: ", str(ai4), str(ai4_vmmapping), str(am4))

5. Downloads