(feat) added RTU gateway
Signed-off-by: Eduard Iten <eduard@iten.pro>
This commit is contained in:
parent
54e991294b
commit
6cb17be451
|
|
@ -0,0 +1 @@
|
|||
pyserial-asyncio>=0.6
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
import asyncio
|
||||
import argparse
|
||||
import serial
|
||||
import serial_asyncio
|
||||
from datetime import datetime
|
||||
|
||||
# Globale Variable für den seriellen Writer, um Zugriff im TCP-Handler zu ermöglichen
|
||||
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}")
|
||||
|
||||
async def handle_tcp_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter, verbose: bool):
|
||||
"""Bearbeitet eine einzelne TCP-Client-Verbindung."""
|
||||
global serial_writer
|
||||
peername = writer.get_extra_info('peername')
|
||||
log_message(f"✅ Client verbunden: {peername}")
|
||||
|
||||
if not serial_writer:
|
||||
log_message("❌ Fehler: Serielle Verbindung ist nicht aktiv. Client wird getrennt.")
|
||||
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
|
||||
|
||||
# Anfrage an das serielle Gerät weiterleiten
|
||||
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
|
||||
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} <timeout>")
|
||||
# Kein Timeout an den Client senden, das Protokoll selbst behandelt dies
|
||||
|
||||
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_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(
|
||||
url=comport, baudrate=baudrate, rtscts=False, dsrdtr=False
|
||||
)
|
||||
serial_writer = writer # Globale Variable aktualisieren
|
||||
log_message(f"✅ Serielle Verbindung zu {comport} wiederhergestellt.")
|
||||
return writer # Erfolgreich, gib den neuen Writer zurück
|
||||
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
|
||||
|
||||
async def main(args):
|
||||
"""Hauptfunktion zum Starten des Servers und der seriellen Verbindung."""
|
||||
global 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("---------------------------------")
|
||||
|
||||
# Initiale serielle Verbindung herstellen
|
||||
try:
|
||||
_, 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)
|
||||
|
||||
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():
|
||||
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 __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("👋 Programm wird durch Benutzer beendet.")
|
||||
except Exception as e:
|
||||
log_message(f"💥 Unerwarteter Fehler im Hauptprogramm: {e}")
|
||||
Loading…
Reference in New Issue