Zugriff auf BNI-Daten über Modbus TCP mit der Programmiersprache Python
1. Einführung
Der Zweck dieses technischen Anwendungshinweises ist es, die Schritte zu erklären, die notwendig sind, um auf die Balluff IO-Link-Master-Konfiguration und ihre Prozesseingangs- und -ausgangsdaten mit der Programmiersprache Python über das Modbus/TCP-Feldbusprotokoll zuzugreifen.
Ein Überblick über die verfügbaren Daten findet sich auf Seite 75 des IO-Link-Master-Konfigurationshandbuchs in Tabelle 6-2 - siehe unten.

2. Einrichtung
Dieses Kapitel beschreibt sowohl die Hardware- als auch die Software-Einrichtung.
2.1. Hardware
Dieser technische Anwendungshinweis bezieht sich auf die IO-Link-Master-Familie der Black- und Silver-Line von Balluff mit einer Firmware-Revision 1.2.0 oder höher, die Multi-Protokoll-Kommunikation einschließlich Modbus/TCP unterstützt.
In den in dieser technischen Applikationsschrift gezeigten Beispielen wurde folgende Hardware verwendet:
- IO-Link-Master
- BNI00L3 p/n BNI XG3-508-0B5-R067
- HW rev. 1.0.1
- FW rev. 1.2.0
- IO-Link Master IP-Adresse: 192.168.1.1
- BNI00L3 p/n BNI XG3-508-0B5-R067
- An den IO-Link-Master angeschlossene IO-Link-Geräte
- Port 1 - Balluff Condition Monitoring Sensor
- BCM0003 p/n BCM R16E-004-CI02-01,5-S4
- Prozessdatenprofil - Schwingungsgeschwindigkeit - 1
- Anschluss 2 - Balluff Smart Light
- BNI0085 p/n BNI IOL-802-102-Z037
- 5-Segment-Smart-Light mit Buzzer
- Port 1 - Balluff Condition Monitoring Sensor
2.2. Software
Verwendete IDE (Integrierte Entwicklungsumgebung):
PyCharm 2025.2
Build #PY-252.23892.439, erstellt am 4. August 2025
Quellcode-Revision: e7a5644c801f1
Lizenziert für Testbenutzer
Abonnement ist bis zum 10. September 2025 aktiv.
Laufzeitversion: 21.0.7+6-b1038.58 amd64 (JCEF 122.1.9)
VM: OpenJDK 64-Bit Server VM von JetBrains s.r.o.
Toolkit: sun.awt.windows.WToolkit
Windows 10.0
GC: G1 Young Generation, G1 Concurrent GC, G1 Old Generation
Speicher: 2048M
Cores: 4
Registry:
ide.experimental.ui=true
Hinweis:
Es können auch andere IDEs verwendet werden.
3. Testen der Kommunikation mit einem IO-Link-Master und Lesen von Identifikationsdaten
In diesem Beispielcode werden die Modbus-Kommunikation mit dem IO-Link-Master unter Verwendung der "Client"-Funktion geöffnet, Identifikationsdaten im Holdingregister ab Adresse 10 und einer Länge von 4 Wörtern gelesen und in ASCII konvertiert.

Beispiel-Code:
import struct
from pyModbusTCP.client import ModbusClient
client = ModbusClient(host="192.168.1.1", port=502, auto_open=True)
regs = client.read_holding_registers(10, 4)
product_order_code = b''.join(struct.pack('<H', r) for r in regs).decode('ascii').strip('\x00')
print(client.host, client.port,client.unit_id, client.open())
print("Raw_register_data:", regs)
print("Product order code:", product_order_code)
Ergebnis:
92.168.1.1 502 1 True
Raw_register_data: [20034, 12361, 19504, 51]
Produkt-Bestellcode: BNI00L3
4. Lesen der Richtung des IO-Link-Ports
In diesem Kapitel wird die Richtung von IO-Link Port 1 gelesen.
Der Beispiel-Code kann modifiziert werden, um die Richtung anderer Ports zu lesen.
Weitere Informationen sind im IO-Link Konfigurationshandbuch zu finden:

Beispiel-Code:
from pyModbusTCP.client import ModbusClient
# Modbus-Client erstellen
client = ModbusClient(host="192.168.1.1", port=502, auto_open=True)
# 1 Register ab 3001 lesen
regs = client.read_holding_registers(9101, 1)
if regs: # prüfen, ob Lesen erfolgreich war
reg_val = regs[0] # den ersten (und einzigen) Registerwert ermitteln
# Low-Byte (Bits 0-7) extrahieren
byte0 = reg_val & 0xFF
# High-Byte (Bits 8-15) extrahieren
byte1 = (reg_val >> 8) & 0xFF
print(f "Register: 0x{reg_val:04X}")
print(f "Byte 0 (low) : 0x{byte0:02X} ({byte0})")
print(f "Byte 1 (high): 0x{byte1:02X} ({byte1})")
else:
print("❌ Failed to read register")
if byte1 == 0x00:
print("port 1 - Deactivated")
else:
# code if condition1 is False
client.close()
if byte1 == 0x01:
print("port 1 - IOL Manual")
else:
# code if condition1 is False
client.close()
if byte1 == 0x02:
print("port 1 - IOL Auto")
else:
# code if condition1 is False
client.close()
if byte1 == 0x03:
print("port 1 - Digital IN")
else:
# code if condition1 is False
client.close()
if byte1 == 0x04:
print("port 1 - Digital OUT")
else:
client.close()
Ergebnis:
Register: 0x0201
Byte 0 (niedrig) : 0x01 (1)
Byte 1 (high): 0x02 (2)
Anschluss 1 - IOL Auto
5. Einstellung der Richtung des IO-Link-Ports
In diesem Kapitel wird die Richtung von IO-Link Port 1 gesetzt (geschreiben) .
Der Beispiel-Code kann modifiziert werden, um die Richtung der anderen Ports einzustellen (zu schreiben).
Beispiel-Code:
from pyModbusTCP.client import ModbusClient
client = ModbusClient(host="192.168.1.1", port=502, auto_open=True)
# Schritt 1: Lesen des aktuellen Wertes von Register 9101 - IOL Port 1 Konfiguration
regs = client.read_holding_registers(9101, 1)
if regs:
current_val = regs[0]
print(f "Aktuelles Register 9101: {current_val} (0x{current_val:04X})")
# Schritt 2: Ersetze NUR das hohe Byte durch eine dezimale 2
low_byte = current_val & 0xFF # Behalte das niedrige Byte
high_byte = 2 # 0=deaktiviert, 1=IOL Manual, 2=IOL Autostart, 3=Digital Input, 4=Digital Output
new_val = (high_byte << 8) | low_byte # Kombinieren
print(f "Neuer zu schreibender Wert: {neues_wert} (0x{neues_wert:04X})")
# Schritt 3: Zurückschreiben mit Write Multiple Registers
success = client.write_multiple_registers(9101, [new_val])
print("✅ Write successful" if success else "❌ Write failed")
else:
print("❌ Failed to read register 9101")
Ergebnis:
Aktuelles Register 9101: 513 (0x0201)
Neuer zu schreibender Wert: 513 (0x0201)
✅ Schreiben erfolgreich
6. Lesen von IO-Link-Prozessdaten
In diesem Kapitel werden Prozessdaten von Port 1 des IO-Link-Masters gelesen. Dies kann auf andere IO-Link-Master-Ports angewendet werden, indem die Adresse des Startregisters angepasst wird.

Als Referenz:
Die Adresse des Startregisters von Port 1 ist 1100 und die Datenlänge beträgt 34 Worte oder 64 Bytes, die 32 Bytes der Eingangsprozessdaten und 32 Bytes der Ausgangsprozessdaten umfassen.
Im nachstehenden Beispiel-Code werden 32 Byte Eingangsprozessdaten von einem BCM0003 Balluff Condition Monitoring Sensor gelesen, die in 8 Slots zu je 4 Byte (2 Worte) unterteilt sind. Anschließend wird ein Byte-Swap durchgeführt, wandeln die Daten in einen REAL-Wert um und runden sie auf eine bestimmte Anzahl von Dezimalstellen.
Beispiel-Code:
import struct
from pyModbusTCP.client import ModbusClient
client = ModbusClient(host='192.168.1.1', port=502, unit_id=255, auto_open=True)
#**************************************************************************************
def read_float32(client, address, unit=255):
regs = client.read_holding_registers(address, 2)
if not regs:
return None
swapped = [regs[1], regs[0]]
raw_bytes = struct.pack('<HH', swapped[0], swapped[1])
return struct.unpack('>f', raw_bytes)[0]
#**************************************************************************************
v_rms_x = read_float32(client, 1100)
v_rms_y = read_float32(client, 1102)
v_rms_z = read_float32(client, 1104)
v_peak_x = read_float32(client, 1106)
v_peak_y = read_float32(client, 1108)
v_peak_z = read_float32(client, 1110)
bcm_temp = read_float32(client, 1112)
v_rms_x = round(v_rms_x, 4)
v_rms_y = round(v_rms_y, 4)
v_rms_z = round(v_rms_z, 4)
v_peak_x = round(v_peak_x, 4)
v_peak_y = round(v_peak_y, 4)
v_peak_z = round(v_peak_z, 4)
bcm_temp = round(bcm_temp, 2)
print("BCM vRMS X:", v_rms_x)
print("BCM vRMS Y:", v_rms_y)
print("BCM vRMS Z:", v_rms_z)
print("BCM vPeak X:", v_peak_x)
print("BCM vPeak Y:", v_peak_y)
print("BCM vPeak Z:", v_peak_z)
print("BCM Temp:", bcm_temp)
Ergebnis:
BCM vRMS X: 0.0398
BCM vRMS Y: 0.038
BCM vRMS Z: 0.0582
BCM vPeak X: 0.1225
BCM vPeak Y: 0.1395
BCM vPeak Z: 0.1735
BCM Temp: 39.87
7. Schreiben von IO-Link-Prozessdaten
In diesem Kapitel werden IO-Link-Prozessdaten geschrieben, um die Funktion einer 5-Segment-Smart-Light zu steuern, die an Port 2 des IO-Link-Masters angeschlossen ist.
Der Beispiel-Code kann auf andere IO-Link-Geräte angewendet werden.
Hinweis:
Register 1n17 (d.h. 1217 für Port 2) muss auf 1 gesetzt werden, um IO-Link-Prozessdaten zu validieren.

Beispiel-Code:
def reverse_bits(word):
"""
Kehrt die Reihenfolge der Bits in einem 16-Bit-Wort um.
Beispiel: 0b0000000000000001 -> 0b1000000000000000
"""
if not (0 <= word <= 0xFFFF):
raise ValueError("Word must be a 16-bit integer (0-65535)")
reversed_word = 0
for i in range(16):
if word & (1 << i):
reversed_word |= (1 << (15 - i))
return reversed_word
#*********************************************************************
def bits_to_word(bits):
"""
Konvertiert eine Liste von 16 Bits (0 oder 1) in eine 16-Bit Ganzzahl (Wort).
bits[0] ist das MSB, bits[15] ist das LSB.
"""
if len(bits) != 16:
raise ValueError("Genau 16 Bits sind erforderlich")
if any(bit not in (0, 1) for bit in bits):
raise ValueError("Bits müssen 0 oder 1 sein")
word = 0
for i, bit in enumerate(bits):
word |= (bit << (15 - i)) # Schiebe Bit in Position
return word
#*********************************************************************
SL_byte0_und_1 = [1, #Seg 1 Farbbit1
0, #Seg 1 Farbbit2
0, #Seg 1 Farbbit3
0, #Seg 1 blink
1, #Seg 2 Farbbit1
1, #Seg 2 Farbbit2
0, #Seg 2 Farbbit3
0, #Seg 2 blink
1, #Seg 3 Farbbit1
1, #Seg 3 Farbbit2
1, #Seg 3 Farbbit3
0, #Seg 3 blink
0, #Seg 4 Farbbit1
0, #Seg 4 Farbbit2
1, #Seg 4 Farbbit3
0, #Seg 4 blink
]
#*****************************************
SL_byte1_und_2 = [1, #Seg 5 color bit1
0, #Seg 5 color bit2
0, #Seg 5 color bit3
0, #Seg 1 blink
0, #Buzzer type bit 0
0, #Buzzer type bit 1
0, #nicht verwendet für SL
0, #Buzzer state
1, #Segment mode
0, #Level mode
0, #Runlight mode
0, #Flexible mode
0, #Nicht verwendet für SL
0, #Nicht sicher für SL
0, #Sync start
0, #Sync-Impuls
]
Register_1 = bits_to_word(SL_byte0_und_1)
Register_1_reversed = reverse_bits(Register_1)
Register_2 = bits_to_word(SL_byte1_und_2)
Register_2_reversed = reverse_bits(Register_2)
from pyModbusTCP.client import ModbusClient
# Modbus-Client erstellen
client = ModbusClient(host="192.168.1.1", port=502, auto_open=True)
# HINWEIS: 1n01 Port n IOL Prozessdateneingang gültig
# 1n01...1n16 16 Worte (32 Bytes) IOL Input Prozessdaten
# 1n17 Port n IOL Prozessdatenausgang gültig
# 1n18...1n33 16 Worte (32 Bytes) IOL Output Prozessdaten
IOL_Process_data_output_valid = 1 #0=ungültig und 1=gültig
# Schreiben von 16 (32 Bytes) Registern beginnend bei Adresse 1200 IOL Port 2
client.write_multiple_registers(1217, [IOL_Process_data_output_valid,Register_1_reversed,Register_2_reversed,5,1])
# Lesen von 16 (32 Byte) Registern ab Adresse 1200 IOL Port 2
registers = client.read_holding_registers(1217, 5)
# Werte prüfen und anzeigen
if registers:
for i, value in enumerate(registers):
print(f "Register {1217 + i}: {value:016b}")
else:
print("❌ Fehler beim Lesen von Registern")
#*********************************************************************************************************