Skip to content

Commit c593e9c

Browse files
committed
feat: more robust connect and initalisation
1 parent 6487c8e commit c593e9c

3 files changed

Lines changed: 185 additions & 24 deletions

File tree

main.py

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ def initialize_logging(log_level_str: str):
2626
logging.StreamHandler(sys.stdout)
2727
]
2828
)
29+
# Setze den Level auch auf den Root-Logger, falls basicConfig ihn nicht korrekt gesetzt hat (z.B. bei wiederholtem Aufruf)
30+
logging.getLogger().setLevel(level)
2931

3032
# Initialisiere das Logging mit dem LOG_LEVEL aus der Umgebungsvariable (falls vorhanden)
3133
initialize_logging(os.environ.get("LOG_LEVEL", "INFO"))
@@ -86,6 +88,8 @@ def main():
8688
# Logging Einstellung
8789
parser.add_argument("--log-level", default=DEFAULT_LOG_LEVEL, choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], help=f"Logging Level. Standard: {DEFAULT_LOG_LEVEL}")
8890

91+
parser.add_argument("--timeout", type=int, default=None, help="Beendet das Programm nach N Sekunden (optional)")
92+
8993
args = parser.parse_args()
9094

9195
# Logging Level anpassen (aus CLI oder ENV Default)
@@ -129,28 +133,28 @@ def signal_handler(sig, frame):
129133
try:
130134
logger.info("Verbinde zum Signalduino...")
131135
controller.connect()
132-
logger.info("Verbunden! Drücke Ctrl+C zum Beenden.")
136+
logger.info("Verbunden! Starte Initialisierung...")
137+
138+
# Starte Initialisierung, welche die Versionsabfrage inkl. Retry-Logik durchführt
139+
controller.initialize()
140+
logger.info("Initialisierung abgeschlossen! Drücke Ctrl+C zum Beenden.")
133141

134-
# Sende Versionsabfrage zum Test
135-
logger.info("Sende Versionsabfrage (V)...")
136-
# Perl regex: 'V\s.*SIGNAL(?:duino|ESP|STM).*(?:\s\d\d:\d\d:\d\d)'
137-
version_pattern = re.compile(
138-
r"V\s.*SIGNAL(?:duino|ESP|STM).*", re.IGNORECASE
139-
)
140-
version = controller.send_command(
141-
"V",
142-
expect_response=True,
143-
timeout=15.0, # Erhöhe den Timeout für den initialen V-Befehl
144-
response_pattern=version_pattern,
145-
)
146-
if version:
147-
logger.info(f"Signalduino Version: {version.strip()}")
148-
else:
149-
logger.warning("Keine Antwort auf Versionsabfrage erhalten.")
150-
151142
# Hauptschleife
152-
while True:
153-
time.sleep(1)
143+
if args.timeout is not None:
144+
logger.info(f"Programm wird nach {args.timeout} Sekunden beendet.")
145+
start_time = time.time()
146+
# Der `while` Block mit `time.sleep(0.1)` wird verwendet, um auf das Timeout zu warten,
147+
# während das Controller-Thread im Hintergrund Nachrichten verarbeitet.
148+
while (time.time() - start_time) < args.timeout:
149+
time.sleep(0.1)
150+
# Timeout erreicht, Controller trennen (signal_handler wird nicht aufgerufen)
151+
logger.info("Timeout erreicht. Programm wird beendet.")
152+
controller.disconnect()
153+
sys.exit(0)
154+
else:
155+
# Endlosschleife, wenn kein Timeout gesetzt ist
156+
while True:
157+
time.sleep(1)
154158

155159
except Exception as e:
156160
logger.error(f"Ein Fehler ist aufgetreten: {e}", exc_info=True)

signalduino/controller.py

Lines changed: 105 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
from datetime import datetime, timedelta, timezone
77
from typing import Any, Callable, List, Literal, Optional, Pattern
88

9-
from .constants import SDUINO_CMD_TIMEOUT
9+
from .constants import (
10+
SDUINO_CMD_TIMEOUT,
11+
SDUINO_INIT_MAXRETRY,
12+
SDUINO_INIT_WAIT,
13+
SDUINO_INIT_WAIT_XQ,
14+
)
1015
from .exceptions import SignalduinoCommandTimeout, SignalduinoConnectionError
1116
from .mqtt import MqttPublisher # NEU: MQTT-Import
1217
from .parser import SignalParser
@@ -46,6 +51,9 @@ def __init__(
4651
self._pending_responses: List[PendingResponse] = []
4752
self._pending_responses_lock = threading.Lock()
4853

54+
self.init_retry_count = 0
55+
self.init_reset_flag = False
56+
4957
def connect(self) -> None:
5058
"""Opens the transport and starts the worker threads."""
5159
if self.transport.is_open:
@@ -96,6 +104,92 @@ def disconnect(self) -> None:
96104
self.transport.close()
97105
self.logger.info("Transport closed.")
98106

107+
def initialize(self) -> None:
108+
"""Starts the initialization process."""
109+
self.logger.info("Initializing device...")
110+
self.init_retry_count = 0
111+
self.init_reset_flag = False
112+
113+
# Schedule Disable Receiver (XQ) and wait briefly
114+
threading.Timer(SDUINO_INIT_WAIT_XQ, self._send_xq).start()
115+
116+
# Schedule StartInit (Get Version)
117+
threading.Timer(SDUINO_INIT_WAIT, self._start_init).start()
118+
119+
def _send_xq(self) -> None:
120+
try:
121+
self.logger.debug("Sending XQ to disable receiver during init")
122+
self.send_command("XQ", expect_response=False)
123+
except Exception as e:
124+
self.logger.warning("Failed to send XQ: %s", e)
125+
126+
def _start_init(self) -> None:
127+
self.logger.info("StartInit, get version, retry = %d", self.init_retry_count)
128+
129+
if self.init_retry_count == 0:
130+
# First attempt: XQ is sent via a separate timer in initialize(), no blocking wait here.
131+
pass
132+
133+
if self.init_retry_count >= SDUINO_INIT_MAXRETRY:
134+
if not self.init_reset_flag:
135+
self.logger.warning("StartInit, retry count reached. Resetting device.")
136+
self.init_reset_flag = True
137+
self._reset_device()
138+
else:
139+
self.logger.error("StartInit, retry count reached after reset. Closing device.")
140+
self.disconnect()
141+
return
142+
143+
response = None
144+
try:
145+
# Perl Regex: 'V\s.*SIGNAL(?:duino|ESP|STM).*(?:\s\d\d:\d\d:\d\d)'
146+
version_pattern = re.compile(r"V\s.*SIGNAL(?:duino|ESP|STM).*", re.IGNORECASE)
147+
# Use a short timeout here to speed up failed attempts
148+
response = self.send_command(
149+
"V",
150+
expect_response=True,
151+
timeout=2.0, # Shorter timeout for retries
152+
response_pattern=version_pattern,
153+
)
154+
except Exception as e:
155+
self.logger.debug("StartInit: Exception during version check: %s", e)
156+
157+
self._check_version_resp(response)
158+
159+
def _check_version_resp(self, msg: Optional[str]) -> None:
160+
if msg:
161+
self.logger.info("Initialized %s", msg.strip())
162+
self.init_reset_flag = False
163+
self.init_retry_count = 0
164+
165+
# Enable Receiver XE
166+
try:
167+
self.logger.info("Enabling receiver (XE)")
168+
self.send_command("XE", expect_response=False)
169+
except Exception as e:
170+
self.logger.warning("Failed to enable receiver: %s", e)
171+
172+
# Check for CC1101
173+
if "cc1101" in msg.lower():
174+
self.logger.info("CC1101 detected")
175+
# Here we could query ccconf and ccpatable like in Perl
176+
else:
177+
self.logger.warning("StartInit: No valid version response.")
178+
self.init_retry_count += 1
179+
# Retry initialization
180+
self._start_init()
181+
182+
def _reset_device(self) -> None:
183+
self.logger.info("Resetting device...")
184+
try:
185+
self.disconnect()
186+
# Wait briefly to ensure port is released/device resets
187+
threading.Event().wait(2.0)
188+
self.connect()
189+
self.initialize()
190+
except Exception as e:
191+
self.logger.error("Failed to reset device: %s", e)
192+
99193
def _reader_loop(self) -> None:
100194
"""Continuously reads from the transport and puts lines into a queue."""
101195
self.logger.debug("Reader loop started.")
@@ -123,10 +217,17 @@ def _parser_loop(self) -> None:
123217
if not raw_line or self._stop_event.is_set():
124218
continue
125219

126-
if self._handle_as_command_response(raw_line.strip()):
220+
line_data = raw_line.strip()
221+
222+
if self._handle_as_command_response(line_data):
223+
continue
224+
225+
if line_data.startswith("XQ") or line_data.startswith("XR"):
226+
# Abfangen der Receiver-Statusmeldungen XQ/XR (wie in Perl /^XQ/ und /^XR/)
227+
self.logger.debug("Found receiver status: %s", line_data)
127228
continue
128229

129-
decoded_messages = self.parser.parse_line(raw_line)
230+
decoded_messages = self.parser.parse_line(line_data)
130231
for message in decoded_messages:
131232
if self.mqtt_publisher:
132233
try:
@@ -228,7 +329,7 @@ def set_message_type_enabled(
228329
raise ValueError(f"Invalid message type: {message_type}")
229330

230331
verb = "E" if enabled else "D"
231-
noun = message_type # S, U, or C
332+
noun = message_type[-1] # S, U, or C
232333
command = f"C{verb}{noun}"
233334
self.send_command(command)
234335

tests/test_controller.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,60 @@ def readline_side_effect():
141141
try:
142142
callback_mock.assert_called_once_with(decoded_msg)
143143
finally:
144+
controller.disconnect()
145+
146+
147+
def test_initialize_retry_logic(mock_transport, mock_parser):
148+
"""Test the retry logic during initialization."""
149+
controller = SignalduinoController(transport=mock_transport, parser=mock_parser)
150+
controller.connect()
151+
152+
# Mock send_command to fail initially and then succeed
153+
call_count = 0
154+
155+
def side_effect(*args, **kwargs):
156+
nonlocal call_count
157+
call_count += 1
158+
payload = kwargs.get("payload") or args[0] if args else None
159+
160+
if payload == "XQ":
161+
return None
162+
if payload == "V":
163+
if call_count <= 2: # Fail first attempt (XQ is 1st call)
164+
raise SignalduinoCommandTimeout("Timeout")
165+
return "V 3.5.0-dev SIGNALduino"
166+
return None
167+
168+
controller.send_command = Mock(side_effect=side_effect)
169+
170+
# Use very short intervals for testing by patching the imported constants in the controller module
171+
import signalduino.controller
172+
173+
original_wait = signalduino.controller.SDUINO_INIT_WAIT
174+
original_wait_xq = signalduino.controller.SDUINO_INIT_WAIT_XQ
175+
176+
signalduino.controller.SDUINO_INIT_WAIT = 0.1
177+
signalduino.controller.SDUINO_INIT_WAIT_XQ = 0.05
178+
179+
try:
180+
controller.initialize()
181+
time.sleep(1.5) # Wait for timers and retries
182+
183+
# Verify calls:
184+
# 1. XQ
185+
# 2. V (fails)
186+
# 3. V (retry, succeeds)
187+
# 4. XE (enabled after success)
188+
189+
# Note: Depending on timing and implementation details, call count might vary slighty
190+
# but we expect at least XQ, failed V, successful V, XE.
191+
192+
calls = [c.kwargs.get('payload') or c.args[0] for c in controller.send_command.call_args_list]
193+
assert "XQ" in calls
194+
assert calls.count("V") >= 2
195+
assert "XE" in calls
196+
197+
finally:
198+
signalduino.controller.SDUINO_INIT_WAIT = original_wait
199+
signalduino.controller.SDUINO_INIT_WAIT_XQ = original_wait_xq
144200
controller.disconnect()

0 commit comments

Comments
 (0)