conson-xp 1.43.0__py3-none-any.whl → 1.44.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {conson_xp-1.43.0.dist-info → conson_xp-1.44.0.dist-info}/METADATA +1 -1
- {conson_xp-1.43.0.dist-info → conson_xp-1.44.0.dist-info}/RECORD +7 -7
- xp/__init__.py +1 -1
- xp/services/conbus/actiontable/actiontable_download_service.py +219 -161
- {conson_xp-1.43.0.dist-info → conson_xp-1.44.0.dist-info}/WHEEL +0 -0
- {conson_xp-1.43.0.dist-info → conson_xp-1.44.0.dist-info}/entry_points.txt +0 -0
- {conson_xp-1.43.0.dist-info → conson_xp-1.44.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
conson_xp-1.
|
|
2
|
-
conson_xp-1.
|
|
3
|
-
conson_xp-1.
|
|
4
|
-
conson_xp-1.
|
|
5
|
-
xp/__init__.py,sha256=
|
|
1
|
+
conson_xp-1.44.0.dist-info/METADATA,sha256=_XnV63YzozzZlbnPRlvU5adBIY6s7pZq0yZR6xNaVNc,11403
|
|
2
|
+
conson_xp-1.44.0.dist-info/WHEEL,sha256=tsUv_t7BDeJeRHaSrczbGeuK-TtDpGsWi_JfpzD255I,90
|
|
3
|
+
conson_xp-1.44.0.dist-info/entry_points.txt,sha256=1OcdIcDM1hz3ljCXgybaPUh1IOFEwkaTgLIW9u9zGeg,50
|
|
4
|
+
conson_xp-1.44.0.dist-info/licenses/LICENSE,sha256=rxj6woMM-r3YCyGq_UHFtbh7kHTAJgrccH6O-33IDE4,1419
|
|
5
|
+
xp/__init__.py,sha256=_M4wR8pgjr7q-i0BO0eRQ8Tx1cbV_kmviwFhqlqRnYQ,181
|
|
6
6
|
xp/cli/__init__.py,sha256=QjnKB1KaI2aIyKlzrnvCwfbBuUj8HNgwNMvNJVQofbI,81
|
|
7
7
|
xp/cli/__main__.py,sha256=l2iKwMdat5rTGd3JWs-uGksnYYDDffp_Npz05QdKEeU,117
|
|
8
8
|
xp/cli/commands/__init__.py,sha256=G7A1KFRSV0CEeDTqr_khu-K9_sc01CTI2KSfkFcaBRM,4949
|
|
@@ -121,7 +121,7 @@ xp/services/actiontable/msactiontable_xp24_serializer.py,sha256=ku84HCCjYDM9XpRK
|
|
|
121
121
|
xp/services/actiontable/msactiontable_xp33_serializer.py,sha256=MFjEwSsIa8l3RJt-ig828E6kyiFYYXMHFKe4Q0A3NvA,8781
|
|
122
122
|
xp/services/conbus/__init__.py,sha256=Hi35sMKu9o6LpYoi2tQDaQoMb8M5sOt_-LUTxxaCU_0,28
|
|
123
123
|
xp/services/conbus/actiontable/__init__.py,sha256=oD6vRk_Ye-eZ9s_hldAgtRJFu4mfAnODqpkJUGHHszk,40
|
|
124
|
-
xp/services/conbus/actiontable/actiontable_download_service.py,sha256=
|
|
124
|
+
xp/services/conbus/actiontable/actiontable_download_service.py,sha256=RMGC3WxlMQDJkkXy3oKmqkORrXNwQyjDkyfEGWX3JJE,21294
|
|
125
125
|
xp/services/conbus/actiontable/actiontable_list_service.py,sha256=BYNpxdhdVvn5Z3XXvUYyiGGZ3fOoKR1F0boYDfAQ80c,2894
|
|
126
126
|
xp/services/conbus/actiontable/actiontable_show_service.py,sha256=Z11bVYstnIhahFKTL8WqgTmK8rMK6kvismdy7679ojY,3013
|
|
127
127
|
xp/services/conbus/actiontable/actiontable_upload_service.py,sha256=HTWJqJTIDFsp5v0SBx1Wt5gUeso6w03N6bKbtsbHtJY,9705
|
|
@@ -207,4 +207,4 @@ xp/utils/logging.py,sha256=rZDXwlBrYK8A6MPq5StsMNpgsRowzJXM6fvROPwJdGM,3750
|
|
|
207
207
|
xp/utils/serialization.py,sha256=RWHHk86feaB4ZP7rjE4qOWK0900yg2joUBDkP76gfOY,4618
|
|
208
208
|
xp/utils/state_machine.py,sha256=Oe2sLwCh9z_vr1tF6X0ZRGTeuckRQAGzmef7xc9CNdc,2413
|
|
209
209
|
xp/utils/time_utils.py,sha256=dEyViDlAG9GWU-J3D_YVa-sGma6yiyyMTgN4h2x3PY4,3781
|
|
210
|
-
conson_xp-1.
|
|
210
|
+
conson_xp-1.44.0.dist-info/RECORD,,
|
xp/__init__.py
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
from dataclasses import asdict
|
|
5
|
-
from
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any, Optional
|
|
6
7
|
|
|
7
8
|
from psygnal import SignalInstance
|
|
8
9
|
from statemachine import State, StateMachine
|
|
@@ -17,87 +18,130 @@ from xp.services.actiontable.actiontable_serializer import ActionTableSerializer
|
|
|
17
18
|
from xp.services.protocol.conbus_event_protocol import ConbusEventProtocol
|
|
18
19
|
from xp.services.telegram.telegram_service import TelegramService
|
|
19
20
|
|
|
21
|
+
# Constants
|
|
22
|
+
NO_ERROR_CODE = "00"
|
|
23
|
+
CHUNK_HEADER_LENGTH = 2 # data_value format: 2-char counter + actiontable chunk
|
|
24
|
+
MAX_ERROR_RETRIES = 3 # Max retries for error_status_received before giving up
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Phase(Enum):
|
|
28
|
+
"""Download workflow phases.
|
|
29
|
+
|
|
30
|
+
The download workflow consists of three sequential phases:
|
|
31
|
+
- INIT: Drain pending telegrams, query error status → proceed to DOWNLOAD
|
|
32
|
+
- DOWNLOAD: Request actiontable, receive chunks with ACK, until EOF
|
|
33
|
+
- CLEANUP: Drain pending telegrams, query error status → proceed to COMPLETED
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
INIT: Initial phase - drain pending telegrams and query error status.
|
|
37
|
+
DOWNLOAD: Download phase - request actiontable and receive chunks.
|
|
38
|
+
CLEANUP: Cleanup phase - drain remaining telegrams and verify status.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
INIT = "init"
|
|
42
|
+
DOWNLOAD = "download"
|
|
43
|
+
CLEANUP = "cleanup"
|
|
44
|
+
|
|
20
45
|
|
|
21
46
|
class ActionTableDownloadService(StateMachine):
|
|
22
|
-
"""
|
|
47
|
+
"""Service for downloading action tables from Conbus modules via TCP.
|
|
48
|
+
|
|
49
|
+
Inherits from StateMachine - the service IS the state machine. Uses guard
|
|
50
|
+
conditions to share states between INIT and CLEANUP phases.
|
|
51
|
+
see: Download-ActionTable-Workflow.dot
|
|
52
|
+
|
|
53
|
+
States (9 total):
|
|
54
|
+
idle -> receiving -> resetting -> waiting_ok -> requesting
|
|
55
|
+
-> waiting_data <-> receiving_chunk -> processing_eof -> completed
|
|
56
|
+
|
|
57
|
+
Phases - INIT and CLEANUP share the same states (receiving, resetting, waiting_ok):
|
|
58
|
+
|
|
59
|
+
INIT phase (drain → reset → wait_ok):
|
|
60
|
+
idle -> receiving -> resetting -> waiting_ok --(guard: is_init_phase)--> requesting
|
|
23
61
|
|
|
24
|
-
|
|
62
|
+
DOWNLOAD phase (request → receive chunks → EOF):
|
|
63
|
+
requesting -> waiting_data <-> receiving_chunk -> processing_eof
|
|
25
64
|
|
|
26
|
-
|
|
27
|
-
|
|
65
|
+
CLEANUP phase (drain → reset → wait_ok):
|
|
66
|
+
processing_eof -> receiving -> resetting -> waiting_ok --(guard: is_cleanup_phase)--> completed
|
|
67
|
+
|
|
68
|
+
The drain/reset/wait_ok cycle:
|
|
69
|
+
1. Drain pending telegrams (receiving state discards telegram without reading them).
|
|
70
|
+
There may be a lot of telegram. We are listening and receiving until no more
|
|
71
|
+
telegram arrive and timeout occurs.
|
|
72
|
+
2. Timeout triggers error status query (resetting)
|
|
73
|
+
3. Wait for response (waiting_ok)
|
|
74
|
+
4. On no error: guard determines target (requesting or completed)
|
|
75
|
+
On error: retry from drain step
|
|
28
76
|
|
|
29
77
|
Attributes:
|
|
30
|
-
on_progress: Signal emitted with
|
|
31
|
-
on_error: Signal emitted with error message string
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
78
|
+
on_progress: Signal emitted with "." for each chunk received.
|
|
79
|
+
on_error: Signal emitted with error message string.
|
|
80
|
+
on_actiontable_received: Signal emitted with (ActionTable, dict, list).
|
|
81
|
+
on_finish: Signal emitted when download and cleanup completed.
|
|
82
|
+
idle: Initial state - waiting for connection.
|
|
83
|
+
receiving: Drain telegrams state (INIT or CLEANUP phase).
|
|
84
|
+
resetting: Query error status state.
|
|
85
|
+
waiting_ok: Await error status response state.
|
|
86
|
+
requesting: DOWNLOAD phase - send download request state.
|
|
87
|
+
waiting_data: DOWNLOAD phase - await chunks state.
|
|
88
|
+
receiving_chunk: DOWNLOAD phase - process chunk state.
|
|
89
|
+
processing_eof: DOWNLOAD phase - deserialize result state.
|
|
90
|
+
completed: Final state - download finished.
|
|
42
91
|
do_connect: Transition from idle to receiving.
|
|
43
|
-
|
|
92
|
+
filter_telegram: Self-transition in receiving to drain telegrams.
|
|
93
|
+
do_timeout: Transition on timeout events.
|
|
44
94
|
send_error_status: Transition from resetting to waiting_ok.
|
|
45
|
-
error_status_received: Transition
|
|
46
|
-
no_error_status_received: Transition
|
|
95
|
+
error_status_received: Transition when error status is received.
|
|
96
|
+
no_error_status_received: Transition when no error status received.
|
|
47
97
|
send_download: Transition from requesting to waiting_data.
|
|
48
98
|
receive_chunk: Transition from waiting_data to receiving_chunk.
|
|
49
99
|
send_ack: Transition from receiving_chunk to waiting_data.
|
|
50
100
|
receive_eof: Transition from waiting_data to processing_eof.
|
|
51
|
-
do_finish: Transition from processing_eof to receiving.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
send_error_status2: Error status query transition for finalization phase.
|
|
59
|
-
error_status_received2: Error received transition for finalization phase.
|
|
60
|
-
no_error_status_received2: No error received transition to completed state.
|
|
101
|
+
do_finish: Transition from processing_eof to receiving (cleanup).
|
|
102
|
+
|
|
103
|
+
Example:
|
|
104
|
+
>>> with download_service as service:
|
|
105
|
+
... service.configure(serial_number="12345678")
|
|
106
|
+
... service.on_actiontable_received.connect(handle_result)
|
|
107
|
+
... service.start_reactor()
|
|
61
108
|
"""
|
|
62
109
|
|
|
63
|
-
# States
|
|
110
|
+
# States - unified for INIT and CLEANUP phases using guards
|
|
64
111
|
idle = State(initial=True)
|
|
65
|
-
receiving = State()
|
|
66
|
-
resetting = State()
|
|
67
|
-
waiting_ok = State()
|
|
68
|
-
|
|
69
|
-
requesting = State()
|
|
70
|
-
waiting_data = State()
|
|
71
|
-
receiving_chunk = State()
|
|
72
|
-
processing_eof = State()
|
|
112
|
+
receiving = State() # Drain telegrams (INIT or CLEANUP phase)
|
|
113
|
+
resetting = State() # Query error status
|
|
114
|
+
waiting_ok = State() # Await error status response
|
|
73
115
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
116
|
+
requesting = State() # DOWNLOAD phase: send download request
|
|
117
|
+
waiting_data = State() # DOWNLOAD phase: await chunks
|
|
118
|
+
receiving_chunk = State() # DOWNLOAD phase: process chunk
|
|
119
|
+
processing_eof = State() # DOWNLOAD phase: deserialize result
|
|
77
120
|
|
|
78
121
|
completed = State(final=True)
|
|
79
122
|
|
|
80
|
-
# Phase
|
|
123
|
+
# Phase transitions - shared states with guards for phase-dependent routing
|
|
81
124
|
do_connect = idle.to(receiving)
|
|
82
|
-
filter_telegram = receiving.to(receiving) # Self-transition
|
|
125
|
+
filter_telegram = receiving.to(receiving) # Self-transition: drain to /dev/null
|
|
83
126
|
do_timeout = receiving.to(resetting) | waiting_ok.to(receiving)
|
|
84
127
|
send_error_status = resetting.to(waiting_ok)
|
|
85
|
-
error_status_received = waiting_ok.to(
|
|
86
|
-
|
|
128
|
+
error_status_received = waiting_ok.to(
|
|
129
|
+
receiving, cond="can_retry"
|
|
130
|
+
) # Retry if under limit
|
|
87
131
|
|
|
88
|
-
#
|
|
132
|
+
# Conditional transitions based on phase
|
|
133
|
+
no_error_status_received = waiting_ok.to(
|
|
134
|
+
requesting, cond="is_init_phase"
|
|
135
|
+
) | waiting_ok.to(completed, cond="is_cleanup_phase")
|
|
136
|
+
|
|
137
|
+
# DOWNLOAD phase transitions
|
|
89
138
|
send_download = requesting.to(waiting_data)
|
|
90
139
|
receive_chunk = waiting_data.to(receiving_chunk)
|
|
91
140
|
send_ack = receiving_chunk.to(waiting_data)
|
|
92
141
|
receive_eof = waiting_data.to(processing_eof)
|
|
93
142
|
|
|
94
|
-
#
|
|
95
|
-
do_finish = processing_eof.to(
|
|
96
|
-
filter_telegram2 = receiving2.to(receiving2) # Self-transition for filtering
|
|
97
|
-
do_timeout2 = receiving2.to(resetting2) | waiting_ok2.to(receiving2)
|
|
98
|
-
send_error_status2 = resetting2.to(waiting_ok2)
|
|
99
|
-
error_status_received2 = waiting_ok2.to(receiving2)
|
|
100
|
-
no_error_status_received2 = waiting_ok2.to(completed)
|
|
143
|
+
# Return to drain/reset cycle for CLEANUP phase
|
|
144
|
+
do_finish = processing_eof.to(receiving)
|
|
101
145
|
|
|
102
146
|
def __init__(
|
|
103
147
|
self,
|
|
@@ -118,42 +162,63 @@ class ActionTableDownloadService(StateMachine):
|
|
|
118
162
|
self.serial_number: str = ""
|
|
119
163
|
self.actiontable_data: list[str] = []
|
|
120
164
|
self.logger = logging.getLogger(__name__)
|
|
165
|
+
self._phase: Phase = Phase.INIT
|
|
166
|
+
self._error_retry_count: int = 0
|
|
167
|
+
self._signals_connected: bool = False
|
|
121
168
|
|
|
122
169
|
# Signals (instance attributes to avoid conflict with statemachine)
|
|
123
170
|
self.on_progress: SignalInstance = SignalInstance((str,))
|
|
124
171
|
self.on_error: SignalInstance = SignalInstance((str,))
|
|
125
172
|
self.on_finish: SignalInstance = SignalInstance()
|
|
126
173
|
self.on_actiontable_received: SignalInstance = SignalInstance(
|
|
127
|
-
(ActionTable,
|
|
174
|
+
(ActionTable, dict[str, Any], list[str])
|
|
128
175
|
)
|
|
129
176
|
|
|
177
|
+
# Initialize state machine first (before connecting signals)
|
|
178
|
+
super().__init__(allow_event_without_transition=True)
|
|
179
|
+
|
|
130
180
|
# Connect protocol signals
|
|
131
|
-
self.
|
|
132
|
-
self.conbus_protocol.on_telegram_sent.connect(self._on_telegram_sent)
|
|
133
|
-
self.conbus_protocol.on_telegram_received.connect(self._on_telegram_received)
|
|
134
|
-
self.conbus_protocol.on_timeout.connect(self._on_timeout)
|
|
135
|
-
self.conbus_protocol.on_failed.connect(self._on_failed)
|
|
181
|
+
self._connect_signals()
|
|
136
182
|
|
|
137
|
-
|
|
138
|
-
|
|
183
|
+
# Guard conditions for phase-dependent transitions
|
|
184
|
+
|
|
185
|
+
def is_init_phase(self) -> bool:
|
|
186
|
+
"""Guard: check if currently in INIT phase.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
True if in INIT phase, False otherwise.
|
|
190
|
+
"""
|
|
191
|
+
return self._phase == Phase.INIT
|
|
192
|
+
|
|
193
|
+
def is_cleanup_phase(self) -> bool:
|
|
194
|
+
"""Guard: check if currently in CLEANUP phase.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
True if in CLEANUP phase, False otherwise.
|
|
198
|
+
"""
|
|
199
|
+
return self._phase == Phase.CLEANUP
|
|
200
|
+
|
|
201
|
+
def can_retry(self) -> bool:
|
|
202
|
+
"""Guard: check if retry is allowed (under max limit).
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
True if retry count is below MAX_ERROR_RETRIES, False otherwise.
|
|
206
|
+
"""
|
|
207
|
+
return self._error_retry_count < MAX_ERROR_RETRIES
|
|
139
208
|
|
|
140
209
|
# State machine lifecycle hooks
|
|
210
|
+
# Note: receiving state is used to drain pending telegrams from the connection
|
|
211
|
+
# pipe. Any telegram received in this state is intentionally discarded (sent
|
|
212
|
+
# to /dev/null) to ensure a clean state before processing.
|
|
141
213
|
|
|
142
214
|
def on_enter_receiving(self) -> None:
|
|
143
|
-
"""Enter receiving state -
|
|
144
|
-
self.logger.debug("Entering RECEIVING state
|
|
145
|
-
self.conbus_protocol.wait()
|
|
146
|
-
|
|
147
|
-
def on_enter_receiving2(self) -> None:
|
|
148
|
-
"""Enter receiving state - listening for telegrams."""
|
|
149
|
-
self.logger.debug("Entering RECEIVING2 state - waiting for telegrams")
|
|
215
|
+
"""Enter receiving state - drain pending telegrams."""
|
|
216
|
+
self.logger.debug(f"Entering RECEIVING state (phase={self._phase.value})")
|
|
150
217
|
self.conbus_protocol.wait()
|
|
151
218
|
|
|
152
219
|
def on_enter_resetting(self) -> None:
|
|
153
220
|
"""Enter resetting state - query error status."""
|
|
154
|
-
self.logger.debug("Entering RESETTING state
|
|
155
|
-
|
|
156
|
-
# query_datapoint_module_error_code
|
|
221
|
+
self.logger.debug(f"Entering RESETTING state (phase={self._phase.value})")
|
|
157
222
|
self.conbus_protocol.send_telegram(
|
|
158
223
|
telegram_type=TelegramType.SYSTEM,
|
|
159
224
|
serial_number=self.serial_number,
|
|
@@ -162,37 +227,20 @@ class ActionTableDownloadService(StateMachine):
|
|
|
162
227
|
)
|
|
163
228
|
self.send_error_status()
|
|
164
229
|
|
|
165
|
-
def on_enter_resetting2(self) -> None:
|
|
166
|
-
"""Enter resetting state - query error status."""
|
|
167
|
-
self.logger.debug("Entering RESETTING2 state - querying error status")
|
|
168
|
-
|
|
169
|
-
# query_datapoint_module_error_code
|
|
170
|
-
self.conbus_protocol.send_telegram(
|
|
171
|
-
telegram_type=TelegramType.SYSTEM,
|
|
172
|
-
serial_number=self.serial_number,
|
|
173
|
-
system_function=SystemFunction.READ_DATAPOINT,
|
|
174
|
-
data_value=DataPointType.MODULE_ERROR_CODE.value,
|
|
175
|
-
)
|
|
176
|
-
self.send_error_status2()
|
|
177
|
-
|
|
178
230
|
def on_enter_waiting_ok(self) -> None:
|
|
179
|
-
"""Enter waiting_ok state - awaiting
|
|
180
|
-
self.logger.debug("Entering WAITING_OK state
|
|
181
|
-
self.conbus_protocol.wait()
|
|
182
|
-
|
|
183
|
-
def on_enter_waiting_ok2(self) -> None:
|
|
184
|
-
"""Enter waiting_ok state - awaiting ERROR/NO_ERROR."""
|
|
185
|
-
self.logger.debug("Entering WAITING_OK state - awaiting ERROR/NO_ERROR")
|
|
231
|
+
"""Enter waiting_ok state - awaiting error status response."""
|
|
232
|
+
self.logger.debug(f"Entering WAITING_OK state (phase={self._phase.value})")
|
|
186
233
|
self.conbus_protocol.wait()
|
|
187
234
|
|
|
188
235
|
def on_enter_requesting(self) -> None:
|
|
189
236
|
"""Enter requesting state - send download request."""
|
|
237
|
+
self._phase = Phase.DOWNLOAD
|
|
190
238
|
self.logger.debug("Entering REQUESTING state - sending download request")
|
|
191
239
|
self.conbus_protocol.send_telegram(
|
|
192
240
|
telegram_type=TelegramType.SYSTEM,
|
|
193
241
|
serial_number=self.serial_number,
|
|
194
242
|
system_function=SystemFunction.DOWNLOAD_ACTIONTABLE,
|
|
195
|
-
data_value=
|
|
243
|
+
data_value=NO_ERROR_CODE,
|
|
196
244
|
)
|
|
197
245
|
self.send_download()
|
|
198
246
|
|
|
@@ -208,12 +256,12 @@ class ActionTableDownloadService(StateMachine):
|
|
|
208
256
|
telegram_type=TelegramType.SYSTEM,
|
|
209
257
|
serial_number=self.serial_number,
|
|
210
258
|
system_function=SystemFunction.ACK,
|
|
211
|
-
data_value=
|
|
259
|
+
data_value=NO_ERROR_CODE,
|
|
212
260
|
)
|
|
213
261
|
self.send_ack()
|
|
214
262
|
|
|
215
263
|
def on_enter_processing_eof(self) -> None:
|
|
216
|
-
"""Enter processing_eof state - deserialize and emit result."""
|
|
264
|
+
"""Enter processing_eof state - deserialize and emit result, then cleanup."""
|
|
217
265
|
self.logger.debug("Entering PROCESSING_EOF state - deserializing")
|
|
218
266
|
all_data = "".join(self.actiontable_data)
|
|
219
267
|
actiontable = self.serializer.from_encoded_string(all_data)
|
|
@@ -222,6 +270,8 @@ class ActionTableDownloadService(StateMachine):
|
|
|
222
270
|
self.on_actiontable_received.emit(
|
|
223
271
|
actiontable, actiontable_dict, actiontable_short
|
|
224
272
|
)
|
|
273
|
+
# Switch to CLEANUP phase before returning to receiving state
|
|
274
|
+
self._phase = Phase.CLEANUP
|
|
225
275
|
self.do_finish()
|
|
226
276
|
|
|
227
277
|
def on_enter_completed(self) -> None:
|
|
@@ -259,44 +309,28 @@ class ActionTableDownloadService(StateMachine):
|
|
|
259
309
|
)
|
|
260
310
|
return
|
|
261
311
|
|
|
262
|
-
if
|
|
263
|
-
|
|
264
|
-
self.no_error_status_received()
|
|
312
|
+
if not self.waiting_ok.is_active:
|
|
313
|
+
return
|
|
265
314
|
|
|
266
|
-
|
|
315
|
+
is_no_error = reply_telegram.data_value == NO_ERROR_CODE
|
|
316
|
+
if is_no_error:
|
|
317
|
+
self._error_retry_count = 0 # Reset on success
|
|
318
|
+
self.no_error_status_received() # Guards determine target state
|
|
319
|
+
else:
|
|
320
|
+
self._error_retry_count += 1
|
|
321
|
+
self.logger.debug(
|
|
322
|
+
f"Error status received, retry {self._error_retry_count}/{MAX_ERROR_RETRIES}"
|
|
323
|
+
)
|
|
324
|
+
# Guard can_retry blocks transition if max retries exceeded
|
|
325
|
+
self.error_status_received()
|
|
326
|
+
# Check if guard blocked the transition (still in waiting_ok)
|
|
267
327
|
if self.waiting_ok.is_active:
|
|
268
|
-
self.
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if reply_telegram.data_value != "00":
|
|
275
|
-
if self.waiting_ok2.is_active:
|
|
276
|
-
self.error_status_received2()
|
|
277
|
-
|
|
278
|
-
def _on_ack_received(self, _reply_telegram: ReplyTelegram) -> None:
|
|
279
|
-
"""Handle ACK telegram received.
|
|
280
|
-
|
|
281
|
-
Args:
|
|
282
|
-
_reply_telegram: The parsed reply telegram (unused).
|
|
283
|
-
"""
|
|
284
|
-
self.logger.debug(f"Received ACK in {self.current_state}")
|
|
285
|
-
if self.waiting_ok.is_active:
|
|
286
|
-
self.ack_received()
|
|
287
|
-
|
|
288
|
-
if self.waiting_ok2.is_active:
|
|
289
|
-
self.ack_received2()
|
|
290
|
-
|
|
291
|
-
def _on_nack_received(self, _reply_telegram: ReplyTelegram) -> None:
|
|
292
|
-
"""Handle NAK telegram received.
|
|
293
|
-
|
|
294
|
-
Args:
|
|
295
|
-
_reply_telegram: The parsed reply telegram (unused).
|
|
296
|
-
"""
|
|
297
|
-
self.logger.debug(f"Received NAK in {self.current_state}")
|
|
298
|
-
if self.waiting_ok.is_active:
|
|
299
|
-
self.nak_received()
|
|
328
|
+
self.logger.error(
|
|
329
|
+
f"Max error retries ({MAX_ERROR_RETRIES}) exceeded, giving up"
|
|
330
|
+
)
|
|
331
|
+
self.on_error.emit(
|
|
332
|
+
f"Module error persists after {MAX_ERROR_RETRIES} retries"
|
|
333
|
+
)
|
|
300
334
|
|
|
301
335
|
def _on_actiontable_chunk_received(self, reply_telegram: ReplyTelegram) -> None:
|
|
302
336
|
"""Handle actiontable chunk telegram received.
|
|
@@ -306,7 +340,7 @@ class ActionTableDownloadService(StateMachine):
|
|
|
306
340
|
"""
|
|
307
341
|
self.logger.debug(f"Received actiontable chunk in {self.current_state}")
|
|
308
342
|
if self.waiting_data.is_active:
|
|
309
|
-
data_part = reply_telegram.data_value[
|
|
343
|
+
data_part = reply_telegram.data_value[CHUNK_HEADER_LENGTH:]
|
|
310
344
|
self.actiontable_data.append(data_part)
|
|
311
345
|
self.on_progress.emit(".")
|
|
312
346
|
self.receive_chunk()
|
|
@@ -327,7 +361,10 @@ class ActionTableDownloadService(StateMachine):
|
|
|
327
361
|
Args:
|
|
328
362
|
telegram_received: The telegram received event.
|
|
329
363
|
"""
|
|
330
|
-
self.logger.debug(f"Received{telegram_received} in {self.current_state}")
|
|
364
|
+
self.logger.debug(f"Received {telegram_received} in {self.current_state}")
|
|
365
|
+
|
|
366
|
+
# In receiving state, drain pending telegrams from pipe (discard to /dev/null).
|
|
367
|
+
# This ensures clean state before processing by clearing any stale messages.
|
|
331
368
|
if self.receiving.is_active:
|
|
332
369
|
self.filter_telegram()
|
|
333
370
|
return
|
|
@@ -336,11 +373,13 @@ class ActionTableDownloadService(StateMachine):
|
|
|
336
373
|
if not telegram_received.checksum_valid:
|
|
337
374
|
self.logger.debug("Filtered: invalid checksum")
|
|
338
375
|
return
|
|
376
|
+
|
|
339
377
|
if telegram_received.telegram_type != TelegramType.REPLY.value:
|
|
340
378
|
self.logger.debug(
|
|
341
379
|
f"Filtered: not a reply (got {telegram_received.telegram_type})"
|
|
342
380
|
)
|
|
343
381
|
return
|
|
382
|
+
|
|
344
383
|
if telegram_received.serial_number != self.serial_number:
|
|
345
384
|
self.logger.debug(
|
|
346
385
|
f"Filtered: wrong serial {telegram_received.serial_number} != {self.serial_number}"
|
|
@@ -355,14 +394,6 @@ class ActionTableDownloadService(StateMachine):
|
|
|
355
394
|
self._on_read_datapoint_received(reply_telegram)
|
|
356
395
|
return
|
|
357
396
|
|
|
358
|
-
if reply_telegram.system_function == SystemFunction.ACK:
|
|
359
|
-
self._on_ack_received(reply_telegram)
|
|
360
|
-
return
|
|
361
|
-
|
|
362
|
-
if reply_telegram.system_function == SystemFunction.NAK:
|
|
363
|
-
self._on_nack_received(reply_telegram)
|
|
364
|
-
return
|
|
365
|
-
|
|
366
397
|
if reply_telegram.system_function == SystemFunction.ACTIONTABLE:
|
|
367
398
|
self._on_actiontable_chunk_received(reply_telegram)
|
|
368
399
|
return
|
|
@@ -373,15 +404,14 @@ class ActionTableDownloadService(StateMachine):
|
|
|
373
404
|
|
|
374
405
|
def _on_timeout(self) -> None:
|
|
375
406
|
"""Handle timeout event."""
|
|
376
|
-
self.logger.debug("Timeout occurred")
|
|
407
|
+
self.logger.debug(f"Timeout occurred (phase={self._phase.value})")
|
|
377
408
|
if self.receiving.is_active:
|
|
378
409
|
self.do_timeout() # receiving -> resetting
|
|
379
410
|
elif self.waiting_ok.is_active:
|
|
380
|
-
self.do_timeout() # waiting_ok -> receiving
|
|
381
|
-
elif self.
|
|
382
|
-
self.
|
|
383
|
-
|
|
384
|
-
self.do_timeout2() # waiting_ok2 -> receiving2
|
|
411
|
+
self.do_timeout() # waiting_ok -> receiving (retry)
|
|
412
|
+
elif self.waiting_data.is_active:
|
|
413
|
+
self.logger.error("Timeout waiting for actiontable data")
|
|
414
|
+
self.on_error.emit("Timeout waiting for actiontable data")
|
|
385
415
|
else:
|
|
386
416
|
self.logger.debug("Timeout in non-recoverable state")
|
|
387
417
|
self.on_error.emit("Timeout")
|
|
@@ -397,18 +427,26 @@ class ActionTableDownloadService(StateMachine):
|
|
|
397
427
|
|
|
398
428
|
# Public API
|
|
399
429
|
|
|
400
|
-
def
|
|
430
|
+
def configure(
|
|
401
431
|
self,
|
|
402
432
|
serial_number: str,
|
|
403
433
|
timeout_seconds: Optional[float] = 2.0,
|
|
404
434
|
) -> None:
|
|
405
|
-
"""
|
|
435
|
+
"""Configure download parameters before starting.
|
|
436
|
+
|
|
437
|
+
Sets the target module serial number and timeout. Call this before
|
|
438
|
+
start_reactor() to configure the download target.
|
|
406
439
|
|
|
407
440
|
Args:
|
|
408
|
-
serial_number: Module serial number.
|
|
409
|
-
timeout_seconds:
|
|
441
|
+
serial_number: Module serial number to download from.
|
|
442
|
+
timeout_seconds: Timeout in seconds for each operation (default 2.0).
|
|
443
|
+
|
|
444
|
+
Raises:
|
|
445
|
+
RuntimeError: If called while download is in progress.
|
|
410
446
|
"""
|
|
411
|
-
self.
|
|
447
|
+
if not self.idle.is_active:
|
|
448
|
+
raise RuntimeError("Cannot configure while download in progress")
|
|
449
|
+
self.logger.info("Configuring actiontable download")
|
|
412
450
|
self.serial_number = serial_number
|
|
413
451
|
if timeout_seconds:
|
|
414
452
|
self.conbus_protocol.timeout_seconds = timeout_seconds
|
|
@@ -429,17 +467,42 @@ class ActionTableDownloadService(StateMachine):
|
|
|
429
467
|
"""Stop the reactor."""
|
|
430
468
|
self.conbus_protocol.stop_reactor()
|
|
431
469
|
|
|
470
|
+
def _connect_signals(self) -> None:
|
|
471
|
+
"""Connect protocol signals to handlers (idempotent)."""
|
|
472
|
+
if self._signals_connected:
|
|
473
|
+
return
|
|
474
|
+
self.conbus_protocol.on_connection_made.connect(self._on_connection_made)
|
|
475
|
+
self.conbus_protocol.on_telegram_sent.connect(self._on_telegram_sent)
|
|
476
|
+
self.conbus_protocol.on_telegram_received.connect(self._on_telegram_received)
|
|
477
|
+
self.conbus_protocol.on_timeout.connect(self._on_timeout)
|
|
478
|
+
self.conbus_protocol.on_failed.connect(self._on_failed)
|
|
479
|
+
self._signals_connected = True
|
|
480
|
+
|
|
481
|
+
def _disconnect_signals(self) -> None:
|
|
482
|
+
"""Disconnect protocol signals from handlers (idempotent)."""
|
|
483
|
+
if not self._signals_connected:
|
|
484
|
+
return
|
|
485
|
+
self.conbus_protocol.on_connection_made.disconnect(self._on_connection_made)
|
|
486
|
+
self.conbus_protocol.on_telegram_sent.disconnect(self._on_telegram_sent)
|
|
487
|
+
self.conbus_protocol.on_telegram_received.disconnect(self._on_telegram_received)
|
|
488
|
+
self.conbus_protocol.on_timeout.disconnect(self._on_timeout)
|
|
489
|
+
self.conbus_protocol.on_failed.disconnect(self._on_failed)
|
|
490
|
+
self._signals_connected = False
|
|
491
|
+
|
|
432
492
|
def __enter__(self) -> "ActionTableDownloadService":
|
|
433
|
-
"""Enter context manager - reset state
|
|
493
|
+
"""Enter context manager - reset state and reconnect signals.
|
|
434
494
|
|
|
435
495
|
Returns:
|
|
436
496
|
Self for context manager protocol.
|
|
437
497
|
"""
|
|
438
498
|
# Reset state for singleton reuse
|
|
439
499
|
self.actiontable_data = []
|
|
440
|
-
self.
|
|
500
|
+
self._phase = Phase.INIT
|
|
501
|
+
self._error_retry_count = 0
|
|
441
502
|
# Reset state machine to idle
|
|
442
503
|
self._reset_state()
|
|
504
|
+
# Reconnect signals (in case previously disconnected)
|
|
505
|
+
self._connect_signals()
|
|
443
506
|
return self
|
|
444
507
|
|
|
445
508
|
def _reset_state(self) -> None:
|
|
@@ -447,21 +510,16 @@ class ActionTableDownloadService(StateMachine):
|
|
|
447
510
|
# python-statemachine uses model.state to track current state
|
|
448
511
|
# Set it directly to the initial state id
|
|
449
512
|
self.model.state = self.idle.id
|
|
450
|
-
self._download_complete = False
|
|
451
513
|
|
|
452
514
|
def __exit__(
|
|
453
515
|
self, _exc_type: Optional[type], _exc_val: Optional[Exception], _exc_tb: Any
|
|
454
516
|
) -> None:
|
|
455
517
|
"""Exit context manager and disconnect signals."""
|
|
456
|
-
|
|
457
|
-
self.conbus_protocol.on_connection_made.disconnect(self._on_connection_made)
|
|
458
|
-
self.conbus_protocol.on_telegram_sent.disconnect(self._on_telegram_sent)
|
|
459
|
-
self.conbus_protocol.on_telegram_received.disconnect(self._on_telegram_received)
|
|
460
|
-
self.conbus_protocol.on_timeout.disconnect(self._on_timeout)
|
|
461
|
-
self.conbus_protocol.on_failed.disconnect(self._on_failed)
|
|
518
|
+
self._disconnect_signals()
|
|
462
519
|
# Disconnect service signals
|
|
463
520
|
self.on_progress.disconnect()
|
|
464
521
|
self.on_error.disconnect()
|
|
522
|
+
self.on_actiontable_received.disconnect()
|
|
465
523
|
self.on_finish.disconnect()
|
|
466
524
|
# Stop reactor
|
|
467
525
|
self.stop_reactor()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|