diff --git a/software/tools/rtu_gateway/rtu_gateway.py b/software/tools/rtu_gateway/rtu_gateway.py old mode 100644 new mode 100755 index ed16da1..782a375 --- a/software/tools/rtu_gateway/rtu_gateway.py +++ b/software/tools/rtu_gateway/rtu_gateway.py @@ -4,63 +4,78 @@ import serial import serial_asyncio from datetime import datetime -# Globale Variable für den seriellen Writer, um Zugriff im TCP-Handler zu ermöglichen +# Globale Variablen für den seriellen Reader und Writer +serial_reader = None serial_writer = None def log_message(message: str): """Gibt eine formatierte Log-Nachricht mit Zeitstempel aus.""" print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {message}") +def log_verbose_request(req: bytes): + """Gibt eine detaillierte, formatierte Log-Nachricht für eine Anfrage aus.""" + if len(req) < 6: return + log_message(f"VERBOSE: --> REQ from HA: {req.hex(' ')}") + slave_id = req[0] + func_code = req[1] + addr = int.from_bytes(req[2:4], 'big') + + # Für Lese-/Schreibbefehle + if func_code in [1, 2, 3, 4, 5, 6, 15, 16] and len(req) >= 6: + count_or_data = int.from_bytes(req[4:6], 'big') + log_message(f"VERBOSE: Parsed: slave={slave_id}, func={func_code}, addr={addr}, count/val={count_or_data}") + else: + log_message(f"VERBOSE: Parsed: slave={slave_id}, func={func_code}") + + +def log_verbose_response(res: bytes): + """Gibt eine detaillierte, formatierte Log-Nachricht für eine Antwort aus.""" + if len(res) < 5: return + log_message(f"VERBOSE: <-- RES from DEV: {res.hex(' ')}") + slave_id = res[0] + func_code = res[1] + + if func_code < 0x80: # Keine Fehler-Antwort + byte_count = res[2] + data = res[3:-2].hex(' ') + log_message(f"VERBOSE: Parsed: slave={slave_id}, func={func_code}, bytes={byte_count}, data=[{data}]") + else: # Fehler-Antwort + error_code = res[2] + log_message(f"VERBOSE: ERROR: slave={slave_id}, func={func_code}, err_code={error_code}") + + async def handle_tcp_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter, verbose: bool): """Bearbeitet eine einzelne TCP-Client-Verbindung.""" - global serial_writer + global serial_reader, serial_writer peername = writer.get_extra_info('peername') log_message(f"✅ Client verbunden: {peername}") - if not serial_writer: + if not serial_writer or not serial_reader: log_message("❌ Fehler: Serielle Verbindung ist nicht aktiv. Client wird getrennt.") - writer.close() - await writer.wait_closed() + writer.close(); await writer.wait_closed() return try: while True: - # Daten vom TCP-Client (Home Assistant) lesen tcp_request = await reader.read(256) - if not tcp_request: - # Verbindung vom Client geschlossen - break + if not tcp_request: break - # Anfrage an das serielle Gerät weiterleiten + if verbose: log_verbose_request(tcp_request) + + # *** HINZUGEFÜGTE ÄNDERUNG: Eine kleine Pause vor dem Senden *** + # Dies gibt empfindlichen Geräten oder langsamen Bussen Zeit. + await asyncio.sleep(0.05) # 50ms Verzögerung + serial_writer.write(tcp_request) await serial_writer.drain() try: - # Auf Antwort vom seriellen Gerät warten (mit Timeout) - serial_response = await asyncio.wait_for(serial_writer.protocol.transport.serial.read_async(256), timeout=2.0) - - # Antwort an den TCP-Client senden + serial_response = await asyncio.wait_for(serial_reader.read(256), timeout=2.0) + if verbose: log_verbose_response(serial_response) writer.write(serial_response) await writer.drain() - - if verbose: - # Zerlegen der Modbus-Antwort für das Logging - dev_id = tcp_request[0] - func_code = tcp_request[1] - # Einfache Register-Extraktion (variiert je nach Funktion) - # Dies ist eine Annahme für gängige Lesefunktionen - if func_code in [3, 4]: - reg_addr = int.from_bytes(tcp_request[2:4], 'big') - data_hex = serial_response[3:-2].hex() # Daten ohne ID, Func, Count und CRC - log_message(f"VERBOSE: id {dev_id:03d}, reg 0x{reg_addr:04x}, data 0x{data_hex}") - else: - log_message(f"VERBOSE: id {dev_id:03d}, data {serial_response.hex()}") - except asyncio.TimeoutError: - if verbose: - reg_addr = int.from_bytes(tcp_request[2:4], 'big') - log_message(f"VERBOSE: reg 0x{reg_addr:04x} ") - # Kein Timeout an den Client senden, das Protokoll selbst behandelt dies + log_message("VERBOSE: <-- Timeout from DEV") except asyncio.CancelledError: log_message("TCP-Handler wurde abgebrochen.") @@ -68,36 +83,34 @@ async def handle_tcp_client(reader: asyncio.StreamReader, writer: asyncio.Stream log_message(f"TCP-Verbindungsfehler: {e}") finally: log_message(f"🔌 Client getrennt: {peername}") - writer.close() - await writer.wait_closed() + writer.close(); await writer.wait_closed() async def serial_reconnector(comport, baudrate): """Versucht, die serielle Verbindung wiederherzustellen.""" - global serial_writer + global serial_reader, serial_writer for attempt in range(1, 6): log_message(f"🚨 Serielle Verbindung verloren! Versuch {attempt}/5 in 5 Sekunden...") await asyncio.sleep(5) try: - # Erneuter Verbindungsversuch - _, writer = await serial_asyncio.open_serial_connection( + reader_obj, writer_obj = await serial_asyncio.open_serial_connection( url=comport, baudrate=baudrate, rtscts=False, dsrdtr=False ) - serial_writer = writer # Globale Variable aktualisieren + serial_reader, serial_writer = reader_obj, writer_obj log_message(f"✅ Serielle Verbindung zu {comport} wiederhergestellt.") - return writer # Erfolgreich, gib den neuen Writer zurück + return True except (serial.SerialException, FileNotFoundError) as e: log_message(f"❌ Wiederverbindung fehlgeschlagen: {e}") log_message("💥 Konnte serielle Verbindung nach 5 Versuchen nicht wiederherstellen. Programm wird beendet.") - # Sauberes Beenden des gesamten Programms - loop = asyncio.get_running_loop() - loop.stop() - return None + try: + asyncio.get_running_loop().stop() + except RuntimeError: pass + return False async def main(args): """Hauptfunktion zum Starten des Servers und der seriellen Verbindung.""" - global serial_writer + global serial_reader, serial_writer log_message("--- Modbus RTU zu TCP Gateway ---") log_message(f"Serieller Port: {args.comport}") log_message(f"Baudrate: {args.baudrate}") @@ -105,35 +118,33 @@ async def main(args): log_message(f"Verbose Modus: {'Aktiv' if args.verbose else 'Inaktiv'}") log_message("---------------------------------") - # Initiale serielle Verbindung herstellen try: - _, serial_writer = await serial_asyncio.open_serial_connection( + serial_reader, serial_writer = await serial_asyncio.open_serial_connection( url=args.comport, baudrate=args.baudrate, rtscts=False, dsrdtr=False ) log_message(f"✅ Serielle Verbindung zu {args.comport} erfolgreich hergestellt.") except (serial.SerialException, FileNotFoundError) as e: log_message(f"❌ Kritischer Fehler bei der initialen Verbindung: {e}") - serial_writer = await serial_reconnector(args.comport, args.baudrate) - if not serial_writer: - return # Beenden, wenn die Wiederverbindung fehlschlägt - - # TCP-Server starten - server_handler = lambda r, w: handle_tcp_client(r, w, args.verbose) - server = await asyncio.start_server(server_handler, '0.0.0.0', args.tcpport) + if not await serial_reconnector(args.comport, args.baudrate): return + server = await asyncio.start_server( + lambda r, w: handle_tcp_client(r, w, args.verbose), '0.0.0.0', args.tcpport + ) addr = server.sockets[0].getsockname() log_message(f"👂 Server lauscht auf {addr}") async with server: - # Überwache die serielle Verbindung im Hintergrund while True: - if not serial_writer or not serial_writer.protocol.transport.is_open(): + try: + if serial_reader and hasattr(serial_reader, '_transport') and serial_reader._transport: + _ = serial_reader._transport.serial.cts + else: + raise serial.SerialException("Transport nicht verfügbar.") + except (serial.SerialException, AttributeError, BrokenPipeError, TypeError): log_message("Serielle Verbindung unterbrochen. Starte Wiederverbindungs-Logik...") - serial_writer = await serial_reconnector(args.comport, args.baudrate) - if not serial_writer: - server.close() # Server stoppen, wenn seriell nicht mehr geht - break # Schleife und Programm beenden - await asyncio.sleep(1) # Kurze Pause, um die CPU zu schonen + if not await serial_reconnector(args.comport, args.baudrate): + server.close(); break + await asyncio.sleep(2) if __name__ == "__main__": parser = argparse.ArgumentParser(description="Modbus RTU zu TCP Gateway für Home Assistant.") @@ -147,6 +158,6 @@ if __name__ == "__main__": try: asyncio.run(main(args)) except KeyboardInterrupt: - log_message("👋 Programm wird durch Benutzer beendet.") + log_message("\n👋 Programm wird durch Benutzer beendet.") except Exception as e: log_message(f"💥 Unerwarteter Fehler im Hauptprogramm: {e}")