Ich wollte den Modbus-Verkehr auf einer RS485-Schnittstelle (Modbus RTU) mitlesen und hatte es erst mit dem Picoscope versucht, aber durch die Interpretation der Signale in SW auf dem PC (so zumindest meine Theorie) sind da immer wieder Botschaften verloren gegangen und ich konnte keinen vollständigen Trace erzeugen.
Dann habe ich versucht einen KI-Chatbot um HIlfe zu fragen wie ich am einfachsten unter Linux einen solchen Modbus-trace erstellen kann, nachdem ich einen RS485-USB-Konverter zwischen den zu sniffenden Bus und meinen Laptop gepackt habe. Nach etwas hin und her kam der Vorschlag mit socat die Daten einzulesen und in einem Python-Skript zu interpretieren.
Gesagt getan und ich hatte ein Vibe-coded Skript modbus_parser.py in das sich der socat output reinpipen ließ, aber es kamen ähnliche Ergebnisse wie beim Versuch mit dem SW-Oszi. Also nochmal nachgehakt und viele Runden gedreht. Kurz bevor ich genervt aufgab um am nächsten Tag den Parser klassisch zu programmieren und mich ernsthaft mit der Modbus-Spec auseinander zu setzen hatte ich dann doch etwas, das zumindest in meinem Fall einen hilfreichen Output geliefert hatte. Aufzurufen mit
socat -u /dev/ttyUSB0,b9600,raw,echo=0,clocal=0,hupcl=0,crtscts=0 STDOUT 2>/dev/null | python3 -u ./modbus_parser.py
liefer das folgende Skript einen Output mit interpretierten Modbus-Daten:
#!/usr/bin/env python3
import sys
def parse_packet(packet):
if len(packet) < 6:
return
slave_id = packet[0]
func_code = packet[1]
# --- FUNCTION 03: READ HOLDING REGISTERS ---
if func_code == 3:
# Request: Slave(1) + Func(1) + StartAddr(2) + Count(2) + CRC(2) = 8 Bytes
if len(packet) == 8:
start_addr = int.from_bytes(packet[2:4], byteorder='big')
quantity = int.from_bytes(packet[4:6], byteorder='big')
print(f"[REQ] Slave {slave_id} | Read {quantity} Reg ab Addr: {start_addr} (0x{packet[2:4].hex()})")
# Response: Slave(1) + Func(1) + ByteCount(1) + Data(N) + CRC(2)
elif len(packet) >= 5 and len(packet) == 3 + packet[2] + 2:
byte_count = packet[2]
data_bytes = packet[3:3+byte_count]
registers = []
for i in range(0, len(data_bytes), 2):
reg_val = int.from_bytes(data_bytes[i:i+2], byteorder='big', signed=True)
registers.append(reg_val)
print(f"[RES] Slave {slave_id} | Antwort -> Werte (Dez): {registers}")
# --- FUNCTION 06: WRITE SINGLE REGISTER ---
elif func_code == 6:
if len(packet) == 8:
reg_addr = int.from_bytes(packet[2:4], byteorder='big')
reg_val = int.from_bytes(packet[4:6], byteorder='big', signed=True)
print(f"[SET] Slave {slave_id} | Schreibe Reg {reg_addr} (0x{packet[2:4].hex()}) -> Wert: {reg_val}")
# Live-Streaming der rohen Binärbytes
buffer = bytearray()
while True:
byte = sys.stdin.buffer.read(1)
if not byte:
break
buffer.extend(byte)
# Versuche Pakete im Puffer zu finden
while len(buffer) >= 6:
# Falls Müll im Puffer ist, wegschmeißen bis ein valider Header (Slave 1, Funktion 3 oder 6) kommt
# print(f"{buffer[0]}")
if buffer[0] != 1 or buffer[1] not in [3, 6]:
print(f"drop {buffer[0]}")
buffer.pop(0)
continue
func_code = buffer[1]
expected_len = 0
if func_code == 3:
if len(buffer) >= 3 and buffer[2] < 30: # Response mit Byte-Count
expected_len = 3 + buffer[2] + 2
else:
expected_len = 8 # Vermutlich ein Request
elif func_code == 6:
expected_len = 8
if expected_len and len(buffer) >= expected_len:
packet = buffer[:expected_len]
del buffer[:expected_len]
parse_packet(packet)
else:
break
und die Moral von der Geschicht:
Traue der KI lieber nicht! Dass ich es später wieder find, dokumentiere ich’s im Blog geschwind!






