Skip to content

Commit bf06965

Browse files
committed
feat: create runable program
1 parent b4fec74 commit bf06965

7 files changed

Lines changed: 431 additions & 13 deletions

File tree

main.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import argparse
2+
import logging
3+
import signal
4+
import sys
5+
import time
6+
import os
7+
import re
8+
from typing import Optional
9+
10+
from signalduino.constants import SDUINO_CMD_TIMEOUT
11+
from signalduino.controller import SignalduinoController
12+
from signalduino.transport import SerialTransport, TCPTransport
13+
from signalduino.types import DecodedMessage
14+
15+
# Konfiguration des Loggings
16+
logging.basicConfig(
17+
level=logging.INFO,
18+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
19+
handlers=[
20+
logging.StreamHandler(sys.stdout)
21+
]
22+
)
23+
24+
logger = logging.getLogger("main")
25+
26+
def message_callback(message: DecodedMessage):
27+
"""Callback-Funktion, die aufgerufen wird, wenn eine Nachricht dekodiert wurde."""
28+
print("\n" + "="*50)
29+
print(f"NEUE NACHRICHT EMPFANGEN (Protokoll-ID: {message.protocol_id})")
30+
model = message.metadata.get("model", "Unbekannt")
31+
print(f"Modell: {model}")
32+
print(f"Payload: {message.payload}")
33+
print("-" * 20)
34+
print("Alle Felder:")
35+
# Zeige Metadaten an
36+
for key, value in message.metadata.items():
37+
print(f" {key}: {value}")
38+
39+
# Zeige RawFrame-Infos an, falls vorhanden
40+
if message.raw:
41+
print(" Raw Frame Info:")
42+
print(f" Line: {message.raw.line}")
43+
print(f" Timestamp: {message.raw.timestamp}")
44+
if message.raw.rssi:
45+
print(f" RSSI: {message.raw.rssi}")
46+
print("="*50 + "\n")
47+
48+
def main():
49+
parser = argparse.ArgumentParser(description="Signalduino Python Controller")
50+
51+
# Verbindungseinstellungen
52+
group = parser.add_mutually_exclusive_group(required=True)
53+
group.add_argument("--serial", help="Serieller Port (z.B. /dev/ttyUSB0)")
54+
group.add_argument("--tcp", help="TCP Host (z.B. 192.168.1.10)")
55+
56+
parser.add_argument("--baud", type=int, default=57600, help="Baudrate für serielle Verbindung (Standard: 57600)")
57+
parser.add_argument("--port", type=int, default=23, help="Port für TCP Verbindung (Standard: 23)")
58+
parser.add_argument("--debug", action="store_true", help="Debug-Logging aktivieren")
59+
60+
# MQTT Einstellungen (optional via CLI, sonst via ENV)
61+
parser.add_argument("--mqtt-host", help="MQTT Broker Host")
62+
parser.add_argument("--mqtt-port", type=int, help="MQTT Broker Port")
63+
parser.add_argument("--mqtt-username", help="MQTT Broker Benutzername")
64+
parser.add_argument("--mqtt-password", help="MQTT Broker Passwort")
65+
66+
args = parser.parse_args()
67+
68+
# Logging Level anpassen
69+
if args.debug:
70+
logging.getLogger().setLevel(logging.DEBUG)
71+
logger.debug("Debug-Modus aktiviert")
72+
73+
# MQTT Umgebungsvariablen setzen, falls über CLI übergeben
74+
if args.mqtt_host:
75+
os.environ["MQTT_HOST"] = args.mqtt_host
76+
if args.mqtt_port:
77+
os.environ["MQTT_PORT"] = str(args.mqtt_port)
78+
if args.mqtt_username:
79+
os.environ["MQTT_USERNAME"] = args.mqtt_username
80+
if args.mqtt_password:
81+
os.environ["MQTT_PASSWORD"] = args.mqtt_password
82+
83+
# Transport initialisieren
84+
transport = None
85+
if args.serial:
86+
logger.info(f"Initialisiere serielle Verbindung auf {args.serial} mit {args.baud} Baud...")
87+
transport = SerialTransport(port=args.serial, baudrate=args.baud)
88+
elif args.tcp:
89+
logger.info(f"Initialisiere TCP Verbindung zu {args.tcp}:{args.port}...")
90+
transport = TCPTransport(host=args.tcp, port=args.port)
91+
92+
if not transport:
93+
logger.error("Kein gültiger Transport konfiguriert.")
94+
sys.exit(1)
95+
96+
# Controller initialisieren
97+
controller = SignalduinoController(
98+
transport=transport,
99+
message_callback=message_callback,
100+
logger=logger
101+
)
102+
103+
# Graceful Shutdown Handler
104+
def signal_handler(sig, frame):
105+
logger.info("Programm wird beendet...")
106+
controller.disconnect()
107+
sys.exit(0)
108+
109+
signal.signal(signal.SIGINT, signal_handler)
110+
signal.signal(signal.SIGTERM, signal_handler)
111+
112+
# Starten
113+
try:
114+
logger.info("Verbinde zum Signalduino...")
115+
controller.connect()
116+
logger.info("Verbunden! Drücke Ctrl+C zum Beenden.")
117+
118+
# Sende Versionsabfrage zum Test
119+
logger.info("Sende Versionsabfrage (V)...")
120+
# Perl regex: 'V\s.*SIGNAL(?:duino|ESP|STM).*(?:\s\d\d:\d\d:\d\d)'
121+
version_pattern = re.compile(
122+
r"V\s.*SIGNAL(?:duino|ESP|STM).*", re.IGNORECASE
123+
)
124+
version = controller.send_command(
125+
"V",
126+
expect_response=True,
127+
timeout=SDUINO_CMD_TIMEOUT,
128+
response_pattern=version_pattern,
129+
)
130+
if version:
131+
logger.info(f"Signalduino Version: {version.strip()}")
132+
else:
133+
logger.warning("Keine Antwort auf Versionsabfrage erhalten.")
134+
135+
# Hauptschleife
136+
while True:
137+
time.sleep(1)
138+
139+
except Exception as e:
140+
logger.error(f"Ein Fehler ist aufgetreten: {e}", exc_info=True)
141+
controller.disconnect()
142+
sys.exit(1)
143+
144+
if __name__ == "__main__":
145+
main()

sd_protocols/pattern_utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ def pattern_exists(search_pattern: List[float], pattern_list: Dict[str, float],
7171

7272
for pid, pval in pattern_list.items():
7373
gap = abs(pval - search_val)
74-
if gap <= tol:
75-
weighted_matches.append((gap, str(pid)))
74+
if gap <= 0.001 or gap <= tol: # The gap is likely 0.0 for exact match, add a small tolerance to guarantee it
75+
weighted_matches.append((gap, pid))
7676

7777
if not weighted_matches:
7878
# If any value has no candidates, the pattern cannot exist

signalduino/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
SDUINO_INIT_WAIT_XQ = 1.5
55
SDUINO_INIT_WAIT = 2.0
66
SDUINO_INIT_MAXRETRY = 3
7-
SDUINO_CMD_TIMEOUT = 10
7+
SDUINO_CMD_TIMEOUT = 10.0
88
SDUINO_KEEPALIVE_TIMEOUT = 60
99
SDUINO_KEEPALIVE_MAXRETRY = 3
1010
SDUINO_WRITEQUEUE_NEXT = 0.3

signalduino/controller.py

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
import queue
33
import re
44
import threading
5+
import os # NEU: Import für Umgebungsvariablen
56
from datetime import datetime, timedelta, timezone
6-
from typing import Any, Callable, List, Literal, Optional
7+
from typing import Any, Callable, List, Literal, Optional, Pattern
78

9+
from .constants import SDUINO_CMD_TIMEOUT
810
from .exceptions import SignalduinoCommandTimeout, SignalduinoConnectionError
11+
from .mqtt import MqttPublisher # NEU: MQTT-Import
912
from .parser import SignalParser
1013
from .transport import BaseTransport
1114
from .types import DecodedMessage, PendingResponse, QueuedCommand
@@ -26,6 +29,13 @@ def __init__(
2629
self.message_callback = message_callback
2730
self.logger = logger or logging.getLogger(__name__)
2831

32+
# NEU: MQTT Publisher initialisieren
33+
self.mqtt_publisher: Optional[MqttPublisher] = None
34+
if os.environ.get("MQTT_HOST"):
35+
# Nur initialisieren, wenn MQTT-Host konfiguriert ist
36+
self.mqtt_publisher = MqttPublisher(logger=self.logger)
37+
self.mqtt_publisher.register_command_callback(self._handle_mqtt_command)
38+
2939
self._reader_thread: Optional[threading.Thread] = None
3040
self._parser_thread: Optional[threading.Thread] = None
3141
self._writer_thread: Optional[threading.Thread] = None
@@ -68,6 +78,10 @@ def disconnect(self) -> None:
6878
self.logger.info("Disconnecting...")
6979
self._stop_event.set()
7080

81+
# NEU: MQTT Publisher stoppen
82+
if self.mqtt_publisher:
83+
self.mqtt_publisher.stop()
84+
7185
# Wake up threads that might be waiting on queues
7286
self._raw_message_queue.put("")
7387
self._write_queue.put(QueuedCommand("", 0))
@@ -113,6 +127,12 @@ def _parser_loop(self) -> None:
113127

114128
decoded_messages = self.parser.parse_line(raw_line)
115129
for message in decoded_messages:
130+
if self.mqtt_publisher:
131+
try:
132+
self.mqtt_publisher.publish(message)
133+
except Exception:
134+
self.logger.exception("Error in MQTT publish")
135+
116136
if self.message_callback:
117137
try:
118138
self.message_callback(message)
@@ -241,7 +261,11 @@ def send_message(self, message: str) -> None:
241261
self.send_command(message)
242262

243263
def send_command(
244-
self, payload: str, expect_response: bool = False, timeout: float = 2.0
264+
self,
265+
payload: str,
266+
expect_response: bool = False,
267+
timeout: float = 2.0,
268+
response_pattern: Optional[Pattern[str]] = None,
245269
) -> Optional[str]:
246270
"""Queues a command and optionally waits for a specific response."""
247271
if not self.transport.is_open:
@@ -256,11 +280,16 @@ def send_command(
256280
def on_response(response: str):
257281
response_queue.put(response)
258282

283+
if response_pattern is None:
284+
response_pattern = re.compile(
285+
f".*{re.escape(payload)}.*|.*OK.*", re.IGNORECASE
286+
)
287+
259288
command = QueuedCommand(
260289
payload=payload,
261290
timeout=timeout,
262291
expect_response=True,
263-
response_pattern=re.compile(f".*{re.escape(payload)}.*|.*OK.*", re.IGNORECASE),
292+
response_pattern=response_pattern,
264293
on_response=on_response,
265294
description=payload,
266295
)
@@ -270,4 +299,52 @@ def on_response(response: str):
270299
try:
271300
return response_queue.get(timeout=timeout)
272301
except queue.Empty:
273-
raise SignalduinoCommandTimeout(f"Command '{payload}' timed out")
302+
raise SignalduinoCommandTimeout(f"Command '{payload}' timed out")
303+
304+
def _handle_mqtt_command(self, command: str, payload: str) -> None:
305+
"""Handles commands received via MQTT."""
306+
self.logger.info("Handling MQTT command: %s (payload: %s)", command, payload)
307+
308+
if command == "version":
309+
try:
310+
# Send 'V' command and wait for response matching version pattern
311+
# Perl: 'V\s.*SIGNAL(?:duino|ESP|STM).*(?:\s\d\d:\d\d:\d\d)'
312+
version_pattern = re.compile(
313+
r"V\s.*SIGNAL(?:duino|ESP|STM).*", re.IGNORECASE
314+
)
315+
316+
try:
317+
response = self.send_command(
318+
payload="V",
319+
expect_response=True,
320+
timeout=SDUINO_CMD_TIMEOUT,
321+
response_pattern=version_pattern,
322+
)
323+
self.logger.info("Got version response: %s", response)
324+
# Publish result back to MQTT
325+
# Topic: signalduino/messages/result/version
326+
# We need access to the client to publish ad-hoc messages or add a method to publisher
327+
if (
328+
self.mqtt_publisher
329+
and self.mqtt_publisher.client.is_connected()
330+
):
331+
result_topic = (
332+
f"{self.mqtt_publisher.mqtt_topic}/result/{command}"
333+
)
334+
self.mqtt_publisher.client.publish(result_topic, response)
335+
336+
except SignalduinoCommandTimeout:
337+
self.logger.error("Timeout waiting for version response")
338+
if (
339+
self.mqtt_publisher
340+
and self.mqtt_publisher.client.is_connected()
341+
):
342+
result_topic = (
343+
f"{self.mqtt_publisher.mqtt_topic}/error/{command}"
344+
)
345+
self.mqtt_publisher.client.publish(result_topic, "Timeout")
346+
347+
except Exception as e:
348+
self.logger.error("Error executing version command: %s", e)
349+
else:
350+
self.logger.warning("Unknown MQTT command: %s", command)

signalduino/transport.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def is_open(self) -> bool:
6565
def write_line(self, data: str) -> None:
6666
if not self._serial or not self._serial.is_open:
6767
raise SignalduinoConnectionError("serial port is not open")
68-
payload = (data + "\n").encode("ascii", errors="ignore")
68+
payload = (data + "\n").encode("latin-1", errors="ignore")
6969
self._serial.write(payload)
7070

7171
def readline(self, timeout: Optional[float] = None) -> Optional[str]:
@@ -74,7 +74,7 @@ def readline(self, timeout: Optional[float] = None) -> Optional[str]:
7474
if timeout is not None:
7575
self._serial.timeout = timeout
7676
raw = self._serial.readline()
77-
return raw.decode("ascii", errors="ignore") if raw else None
77+
return raw.decode("latin-1", errors="ignore") if raw else None
7878

7979

8080
class TCPTransport(BaseTransport):
@@ -107,7 +107,7 @@ def is_open(self) -> bool:
107107
def write_line(self, data: str) -> None:
108108
if not self._sock:
109109
raise SignalduinoConnectionError("socket is not open")
110-
payload = (data + "\n").encode("ascii", errors="ignore")
110+
payload = (data + "\n").encode("latin-1", errors="ignore")
111111
self._sock.sendall(payload)
112112

113113
def readline(self, timeout: Optional[float] = None) -> Optional[str]:
@@ -119,9 +119,13 @@ def readline(self, timeout: Optional[float] = None) -> Optional[str]:
119119
while True:
120120
if b"\n" in self._buffer:
121121
line, _, self._buffer = self._buffer.partition(b"\n")
122-
return line.decode("ascii", errors="ignore")
122+
return line.decode("latin-1", errors="ignore")
123123

124-
chunk = self._sock.recv(4096)
125-
if not chunk:
124+
try:
125+
chunk = self._sock.recv(4096)
126+
except socket.timeout:
126127
return None
128+
129+
if not chunk:
130+
raise SignalduinoConnectionError("Remote closed connection")
127131
self._buffer.extend(chunk)

0 commit comments

Comments
 (0)