import asyncio import argparse import serial import serial_asyncio from datetime import datetime # 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_reader, serial_writer peername = writer.get_extra_info('peername') log_message(f"✅ Client verbunden: {peername}") 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() return try: while True: tcp_request = await reader.read(256) if not tcp_request: break 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: 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() except asyncio.TimeoutError: log_message("VERBOSE: <-- Timeout from DEV") except asyncio.CancelledError: log_message("TCP-Handler wurde abgebrochen.") except Exception as e: log_message(f"TCP-Verbindungsfehler: {e}") finally: log_message(f"🔌 Client getrennt: {peername}") writer.close(); await writer.wait_closed() async def serial_reconnector(comport, baudrate): """Versucht, die serielle Verbindung wiederherzustellen.""" 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: reader_obj, writer_obj = await serial_asyncio.open_serial_connection( url=comport, baudrate=baudrate, rtscts=False, dsrdtr=False ) serial_reader, serial_writer = reader_obj, writer_obj log_message(f"✅ Serielle Verbindung zu {comport} wiederhergestellt.") 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.") 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_reader, serial_writer log_message("--- Modbus RTU zu TCP Gateway ---") log_message(f"Serieller Port: {args.comport}") log_message(f"Baudrate: {args.baudrate}") log_message(f"TCP Port: {args.tcpport}") log_message(f"Verbose Modus: {'Aktiv' if args.verbose else 'Inaktiv'}") log_message("---------------------------------") try: 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}") 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: while True: 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...") 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.") parser.add_argument("comport", help="Der serielle Port (z.B. /dev/ttyUSB0 oder COM3)") parser.add_argument("-b", "--baudrate", type=int, default=19200, help="Baudrate der seriellen Verbindung (Standard: 19200)") parser.add_argument("-p", "--tcpport", type=int, default=502, help="TCP-Port, auf dem der Server lauscht (Standard: 502)") parser.add_argument("-v", "--verbose", action="store_true", help="Verbose-Modus aktivieren für detaillierte Logs") args = parser.parse_args() try: asyncio.run(main(args)) except KeyboardInterrupt: log_message("\n👋 Programm wird durch Benutzer beendet.") except Exception as e: log_message(f"💥 Unerwarteter Fehler im Hauptprogramm: {e}")