Skip to content

Commit b212b90

Browse files
committed
feat: threads changeed with asyncio
1 parent 53030a8 commit b212b90

File tree

7 files changed

+739
-623
lines changed

7 files changed

+739
-623
lines changed

main.py

Lines changed: 84 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,16 @@
22
import logging
33
import signal
44
import sys
5-
import time
65
import os
7-
import re
8-
from typing import Optional
6+
from typing import Optional, Awaitable
7+
import asyncio # NEU: Für asynchrone Logik
98
from dotenv import load_dotenv
109

1110
from signalduino.constants import SDUINO_CMD_TIMEOUT
1211
from signalduino.controller import SignalduinoController
13-
from signalduino.exceptions import SignalduinoConnectionError
12+
from signalduino.exceptions import SignalduinoConnectionError, SignalduinoCommandTimeout
1413
from signalduino.transport import SerialTransport, TCPTransport
15-
from signalduino.types import DecodedMessage
14+
from signalduino.types import DecodedMessage, RawFrame # NEU: RawFrame
1615

1716
# Konfiguration des Loggings
1817
def initialize_logging(log_level_str: str):
@@ -35,7 +34,8 @@ def initialize_logging(log_level_str: str):
3534

3635
logger = logging.getLogger("main")
3736

38-
def message_callback(message: DecodedMessage):
37+
# NEU: Callback ist jetzt async
38+
async def message_callback(message: DecodedMessage):
3939
"""Callback-Funktion, die aufgerufen wird, wenn eine Nachricht dekodiert wurde."""
4040
model = message.metadata.get("model", "Unknown")
4141
logger.info(
@@ -44,9 +44,65 @@ def message_callback(message: DecodedMessage):
4444
f"payload={message.payload}"
4545
)
4646
logger.debug(f"Full Metadata: {message.metadata}")
47-
if message.raw:
48-
logger.debug(f"Raw Frame: {message.raw}")
47+
# NEU: Überprüfe, ob RawFrame vorhanden ist und das Attribut 'line' hat
48+
if message.raw and isinstance(message.raw, RawFrame):
49+
logger.debug(f"Raw Frame: {message.raw.line}")
4950

51+
52+
# NEU: Die asynchrone Hauptlogik, die von asyncio.run() aufgerufen wird
53+
async def _async_run(args: argparse.Namespace):
54+
55+
# Transport initialisieren
56+
transport = None
57+
if args.serial:
58+
logger.info(f"Initialisiere serielle Verbindung auf {args.serial} mit {args.baud} Baud...")
59+
transport = SerialTransport(port=args.serial, baudrate=args.baud)
60+
elif args.tcp:
61+
logger.info(f"Initialisiere TCP Verbindung zu {args.tcp}:{args.port}...")
62+
transport = TCPTransport(host=args.tcp, port=args.port)
63+
64+
# Wenn weder --serial noch --tcp (oder deren ENV-Defaults) gesetzt sind
65+
if not transport:
66+
logger.error("Kein gültiger Transport konfiguriert. Bitte geben Sie --serial oder --tcp an oder setzen Sie SIGNALDUINO_SERIAL_PORT / SIGNALDUINO_TCP_HOST in der Umgebung.")
67+
sys.exit(1)
68+
69+
# Controller initialisieren
70+
controller = SignalduinoController(
71+
transport=transport,
72+
message_callback=message_callback,
73+
logger=logger
74+
)
75+
76+
# Starten
77+
try:
78+
logger.info("Verbinde zum Signalduino...")
79+
# NEU: Verwende async with Block
80+
async with controller:
81+
logger.info("Verbunden! Starte Initialisierung und Hauptschleife...")
82+
83+
# Starte die Hauptschleife, warte auf deren Beendigung oder ein Timeout
84+
await controller.run(timeout=args.timeout)
85+
86+
logger.info("Hauptschleife beendet.")
87+
88+
except SignalduinoConnectionError as e:
89+
# Wird ausgelöst, wenn die Verbindung beim Start fehlschlägt
90+
logger.error(f"Verbindungsfehler: {e}")
91+
logger.error("Das Programm wird beendet.")
92+
sys.exit(1)
93+
94+
except asyncio.CancelledError:
95+
# Wird bei SIGINT/SIGTERM durch loop.stop() ausgelöst
96+
logger.info("Asynchrone Hauptschleife abgebrochen.")
97+
sys.exit(0) # Erfolgreiches Beenden
98+
99+
except Exception as e:
100+
# Wird ausgelöst, wenn ein unerwarteter Fehler auftritt (z.B. im Controller)
101+
logger.error(f"Ein unerwarteter Fehler ist aufgetreten: {e}", exc_info=True)
102+
sys.exit(1)
103+
104+
105+
# Die synchrone Hauptfunktion
50106
def main():
51107
# .env-Datei laden. Umgebungsvariablen werden gesetzt, aber CLI-Argumente überschreiben diese.
52108
load_dotenv()
@@ -89,7 +145,8 @@ def main():
89145
# Logging Einstellung
90146
parser.add_argument("--log-level", default=DEFAULT_LOG_LEVEL, choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], help=f"Logging Level. Standard: {DEFAULT_LOG_LEVEL}")
91147

92-
parser.add_argument("--timeout", type=int, default=None, help="Beendet das Programm nach N Sekunden (optional)")
148+
# Timeout ist jetzt float
149+
parser.add_argument("--timeout", type=float, default=None, help="Beendet das Programm nach N Sekunden (optional)")
93150

94151
args = parser.parse_args()
95152

@@ -98,82 +155,31 @@ def main():
98155
initialize_logging(args.log_level)
99156
logger.debug(f"Logging Level auf {args.log_level.upper()} angepasst.")
100157

101-
# Manuelle Zuweisung von MQTT ENV Variablen ist nicht mehr nötig, da argparse sie für die gesamte Laufzeit setzt
102-
103-
# Transport initialisieren
104-
transport = None
105-
if args.serial:
106-
logger.info(f"Initialisiere serielle Verbindung auf {args.serial} mit {args.baud} Baud...")
107-
transport = SerialTransport(port=args.serial, baudrate=args.baud)
108-
elif args.tcp:
109-
logger.info(f"Initialisiere TCP Verbindung zu {args.tcp}:{args.port}...")
110-
transport = TCPTransport(host=args.tcp, port=args.port)
111-
112-
# Wenn weder --serial noch --tcp (oder deren ENV-Defaults) gesetzt sind
113-
if not transport:
114-
logger.error("Kein gültiger Transport konfiguriert. Bitte geben Sie --serial oder --tcp an oder setzen Sie SIGNALDUINO_SERIAL_PORT / SIGNALDUINO_TCP_HOST in der Umgebung.")
115-
sys.exit(1)
116-
117-
# Controller initialisieren
118-
controller = SignalduinoController(
119-
transport=transport,
120-
message_callback=message_callback,
121-
logger=logger
122-
)
123-
124-
# Graceful Shutdown Handler
158+
# Signal-Handler zum Beenden des asyncio-Loops
125159
def signal_handler(sig, frame):
126160
logger.info("Programm wird beendet...")
127-
controller.disconnect()
128-
sys.exit(0)
161+
# Stoppe den Event Loop anstatt nur sys.exit zu machen
162+
try:
163+
loop = asyncio.get_running_loop()
164+
loop.call_soon_threadsafe(loop.stop)
165+
except RuntimeError:
166+
# Loop läuft nicht, z.B. bei schnellem Beenden
167+
sys.exit(0)
129168

130169
signal.signal(signal.SIGINT, signal_handler)
131170
signal.signal(signal.SIGTERM, signal_handler)
132-
133-
# Starten
171+
172+
# Starte die asynchrone Hauptlogik
134173
try:
135-
logger.info("Verbinde zum Signalduino...")
136-
controller.connect()
137-
logger.info("Verbunden! Starte Initialisierung...")
138-
139-
# Starte Initialisierung, welche die Versionsabfrage inkl. Retry-Logik durchführt
140-
controller.initialize()
141-
logger.info("Initialisierung abgeschlossen! Drücke Ctrl+C zum Beenden.")
142-
143-
# Hauptschleife
144-
if args.timeout is not None:
145-
logger.info(f"Programm wird nach {args.timeout} Sekunden beendet.")
146-
start_time = time.time()
147-
# Der `while` Block mit `time.sleep(0.1)` wird verwendet, um auf das Timeout zu warten,
148-
# während das Controller-Thread im Hintergrund Nachrichten verarbeitet.
149-
while (time.time() - start_time) < args.timeout:
150-
time.sleep(0.1)
151-
# Timeout erreicht, Controller trennen (signal_handler wird nicht aufgerufen)
152-
logger.info("Timeout erreicht. Programm wird beendet.")
153-
controller.disconnect()
154-
sys.exit(0)
155-
else:
156-
# Endlosschleife, wenn kein Timeout gesetzt ist
157-
while True:
158-
time.sleep(1)
159-
if not controller.is_running:
160-
logger.error("Controller threads are dead. Exiting...")
161-
break
162-
163-
controller.disconnect()
164-
sys.exit(1)
165-
166-
except SignalduinoConnectionError as e:
167-
# Wird ausgelöst, wenn die Verbindung beim Start fehlschlägt (z.B. falscher Port, Gerät nicht angeschlossen)
168-
logger.error(f"Verbindungsfehler: {e}")
169-
logger.error("Das Programm wird beendet.")
170-
controller.disconnect()
171-
sys.exit(1)
172-
174+
asyncio.run(_async_run(args))
175+
except KeyboardInterrupt:
176+
# Fängt den KeyboardInterrupt ab, der nach loop.stop() auftreten kann
177+
logger.info("Programm beendet durch KeyboardInterrupt.")
173178
except Exception as e:
174-
logger.error(f"Ein unerwarteter Fehler ist aufgetreten: {e}", exc_info=True)
175-
controller.disconnect()
176-
sys.exit(1)
179+
# Diese Exception wird von _async_run ausgelöst, wenn dort sys.exit(1) aufgerufen wird.
180+
if not isinstance(e, SystemExit):
181+
logger.critical("Ein kritischer, ungefangener Fehler ist aufgetreten: %s", e, exc_info=True)
182+
sys.exit(1)
177183

178184
if __name__ == "__main__":
179185
main()

requirements.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
pyserial
22
requests
33
paho-mqtt
4-
python-dotenv
4+
python-dotenv
5+
asyncio-mqtt
6+
pyserial-asyncio

0 commit comments

Comments
 (0)