conson-xp 1.45.0__py3-none-any.whl → 1.46.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.45.0.dist-info → conson_xp-1.46.0.dist-info}/METADATA +1 -1
- {conson_xp-1.45.0.dist-info → conson_xp-1.46.0.dist-info}/RECORD +10 -9
- xp/__init__.py +1 -1
- xp/services/conbus/actiontable/actiontable_download_service.py +76 -256
- xp/services/conbus/actiontable/actiontable_download_state_machine.py +276 -0
- xp/services/protocol/conbus_event_protocol.py +98 -1
- xp/utils/dependencies.py +1 -1
- {conson_xp-1.45.0.dist-info → conson_xp-1.46.0.dist-info}/WHEEL +0 -0
- {conson_xp-1.45.0.dist-info → conson_xp-1.46.0.dist-info}/entry_points.txt +0 -0
- {conson_xp-1.45.0.dist-info → conson_xp-1.46.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.46.0.dist-info/METADATA,sha256=Xxip0IyUnxyoV7r0v9lNkFPQAlyokOY0YC5tkn62bnk,11403
|
|
2
|
+
conson_xp-1.46.0.dist-info/WHEEL,sha256=tsUv_t7BDeJeRHaSrczbGeuK-TtDpGsWi_JfpzD255I,90
|
|
3
|
+
conson_xp-1.46.0.dist-info/entry_points.txt,sha256=1OcdIcDM1hz3ljCXgybaPUh1IOFEwkaTgLIW9u9zGeg,50
|
|
4
|
+
conson_xp-1.46.0.dist-info/licenses/LICENSE,sha256=rxj6woMM-r3YCyGq_UHFtbh7kHTAJgrccH6O-33IDE4,1419
|
|
5
|
+
xp/__init__.py,sha256=kG_WD81lVigZqxRFHU7VU0M0OecuS4eeAibhjoDk7qo,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,8 @@ 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=SL7D4cUzCCwONVSddVQSOHmBHhIKaIyzP57xD7l7BUU,13350
|
|
125
|
+
xp/services/conbus/actiontable/actiontable_download_state_machine.py,sha256=RVQJQp9wJf3TD6SalriPmf2dI1RfBUEqx5Y1GSBiCbE,9956
|
|
125
126
|
xp/services/conbus/actiontable/actiontable_list_service.py,sha256=BYNpxdhdVvn5Z3XXvUYyiGGZ3fOoKR1F0boYDfAQ80c,2894
|
|
126
127
|
xp/services/conbus/actiontable/actiontable_show_service.py,sha256=Z11bVYstnIhahFKTL8WqgTmK8rMK6kvismdy7679ojY,3013
|
|
127
128
|
xp/services/conbus/actiontable/actiontable_upload_service.py,sha256=HTWJqJTIDFsp5v0SBx1Wt5gUeso6w03N6bKbtsbHtJY,9705
|
|
@@ -161,7 +162,7 @@ xp/services/homekit/homekit_service.py,sha256=0lW-hg40ETco3gDBEYkR_sX-UIYsLSKCD4
|
|
|
161
162
|
xp/services/log_file_service.py,sha256=fvPcZQj8XOKUA-4ep5R8n0PelvwvRlTLlVxvIWM5KR4,10506
|
|
162
163
|
xp/services/module_type_service.py,sha256=xWhr1EAZMykL5uNWHWdpa5T8yNruGKH43XRTOS8GwZg,7477
|
|
163
164
|
xp/services/protocol/__init__.py,sha256=qRufBmqRKGzpuzZ5bxBbmwf510TT00Ke8s5HcWGnqRY,818
|
|
164
|
-
xp/services/protocol/conbus_event_protocol.py,sha256=
|
|
165
|
+
xp/services/protocol/conbus_event_protocol.py,sha256=keuT_ZWXkps0zdfXMycGAqHtfIX9FsLfd9jhiY3lHPY,19274
|
|
165
166
|
xp/services/protocol/conbus_protocol.py,sha256=JO7yLkD_ohPT0ETjnAIx4CGpZyobf4LdbuozM_43btE,10276
|
|
166
167
|
xp/services/protocol/protocol_factory.py,sha256=PmjN9AtW9sxNo3voqUiNgQA-pTvX1RW4XXFlHKfFr5Q,2470
|
|
167
168
|
xp/services/protocol/telegram_protocol.py,sha256=Ki5DrXsKxiaqLcdP9WWUuhUI7cPu2DfwyZkh-Gv9Lb8,9496
|
|
@@ -201,10 +202,10 @@ xp/term/widgets/protocol_log.py,sha256=CJUpckWj7GC1kVqixDadteyGnI4aHyzd4kkH-pSbz
|
|
|
201
202
|
xp/term/widgets/status_footer.py,sha256=bxrcqKzJ9V0aPSn_WwraVpJz7NxBUh3yIjA3fwv5nVA,3256
|
|
202
203
|
xp/utils/__init__.py,sha256=_avMF_UOkfR3tNeDIPqQ5odmbq5raKkaq1rZ9Cn1CJs,332
|
|
203
204
|
xp/utils/checksum.py,sha256=HDpiQxmdIedbCbZ4o_Box0i_Zig417BtCV_46ZyhiTk,1711
|
|
204
|
-
xp/utils/dependencies.py,sha256=
|
|
205
|
+
xp/utils/dependencies.py,sha256=gjwuVFm_1KY3WkSPrtlvpJHpuoVDLwrqMXevblr1ixY,25379
|
|
205
206
|
xp/utils/event_helper.py,sha256=W-A_xmoXlpWZBbJH6qdaN50o3-XrwFsDgvAGMJDiAgo,1001
|
|
206
207
|
xp/utils/logging.py,sha256=rZDXwlBrYK8A6MPq5StsMNpgsRowzJXM6fvROPwJdGM,3750
|
|
207
208
|
xp/utils/serialization.py,sha256=RWHHk86feaB4ZP7rjE4qOWK0900yg2joUBDkP76gfOY,4618
|
|
208
209
|
xp/utils/state_machine.py,sha256=Oe2sLwCh9z_vr1tF6X0ZRGTeuckRQAGzmef7xc9CNdc,2413
|
|
209
210
|
xp/utils/time_utils.py,sha256=dEyViDlAG9GWU-J3D_YVa-sGma6yiyyMTgN4h2x3PY4,3781
|
|
210
|
-
conson_xp-1.
|
|
211
|
+
conson_xp-1.46.0.dist-info/RECORD,,
|
xp/__init__.py
CHANGED
|
@@ -2,103 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
from dataclasses import asdict
|
|
5
|
-
from enum import Enum
|
|
6
5
|
from typing import Any, Optional
|
|
7
6
|
|
|
8
7
|
from psygnal import SignalInstance
|
|
9
|
-
from statemachine import State, StateMachine
|
|
10
8
|
|
|
11
9
|
from xp.models.actiontable.actiontable import ActionTable
|
|
12
10
|
from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
|
|
13
11
|
from xp.models.telegram.datapoint_type import DataPointType
|
|
14
12
|
from xp.models.telegram.reply_telegram import ReplyTelegram
|
|
15
|
-
from xp.models.telegram.system_function import SystemFunction
|
|
16
|
-
from xp.models.telegram.telegram_type import TelegramType
|
|
17
13
|
from xp.services.actiontable.actiontable_serializer import ActionTableSerializer
|
|
18
|
-
from xp.services.
|
|
19
|
-
|
|
14
|
+
from xp.services.conbus.actiontable.actiontable_download_state_machine import (
|
|
15
|
+
MAX_ERROR_RETRIES,
|
|
16
|
+
ActionTableDownloadStateMachine,
|
|
17
|
+
)
|
|
18
|
+
from xp.services.protocol.conbus_event_protocol import (
|
|
19
|
+
NO_ERROR_CODE,
|
|
20
|
+
ConbusEventProtocol,
|
|
21
|
+
)
|
|
20
22
|
|
|
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
23
|
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
class ActionTableDownloadService(StateMachine):
|
|
24
|
+
class ActionTableDownloadService(ActionTableDownloadStateMachine):
|
|
47
25
|
"""Service for downloading action tables from Conbus modules via TCP.
|
|
48
26
|
|
|
49
|
-
Inherits from
|
|
50
|
-
|
|
51
|
-
see: Download-ActionTable-Workflow.dot
|
|
27
|
+
Inherits from ActionTableDownloadStateMachine and overrides on_enter_*
|
|
28
|
+
methods to add protocol-specific behavior.
|
|
52
29
|
|
|
53
|
-
|
|
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):
|
|
30
|
+
The workflow consists of three phases:
|
|
58
31
|
|
|
59
32
|
INIT phase (drain → reset → wait_ok):
|
|
60
|
-
|
|
33
|
+
Connection established, drain pending telegrams, query error status.
|
|
61
34
|
|
|
62
35
|
DOWNLOAD phase (request → receive chunks → EOF):
|
|
63
|
-
|
|
36
|
+
Request actiontable, receive and ACK chunks until EOF.
|
|
64
37
|
|
|
65
38
|
CLEANUP phase (drain → reset → wait_ok):
|
|
66
|
-
|
|
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
|
|
39
|
+
After EOF, drain remaining telegrams and verify final status.
|
|
76
40
|
|
|
77
41
|
Attributes:
|
|
78
42
|
on_progress: Signal emitted with "." for each chunk received.
|
|
79
43
|
on_error: Signal emitted with error message string.
|
|
80
44
|
on_actiontable_received: Signal emitted with (ActionTable, dict, list).
|
|
81
45
|
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.
|
|
91
|
-
do_connect: Transition from idle to receiving.
|
|
92
|
-
filter_telegram: Self-transition in receiving to drain telegrams.
|
|
93
|
-
do_timeout: Transition on timeout events.
|
|
94
|
-
send_error_status: Transition from resetting to waiting_ok.
|
|
95
|
-
error_status_received: Transition when error status is received.
|
|
96
|
-
no_error_status_received: Transition when no error status received.
|
|
97
|
-
send_download: Transition from requesting to waiting_data.
|
|
98
|
-
receive_chunk: Transition from waiting_data to receiving_chunk.
|
|
99
|
-
send_ack: Transition from receiving_chunk to waiting_data.
|
|
100
|
-
receive_eof: Transition from waiting_data to processing_eof.
|
|
101
|
-
do_finish: Transition from processing_eof to receiving (cleanup).
|
|
102
46
|
|
|
103
47
|
Example:
|
|
104
48
|
>>> with download_service as service:
|
|
@@ -107,66 +51,24 @@ class ActionTableDownloadService(StateMachine):
|
|
|
107
51
|
... service.start_reactor()
|
|
108
52
|
"""
|
|
109
53
|
|
|
110
|
-
# States - unified for INIT and CLEANUP phases using guards
|
|
111
|
-
idle = State(initial=True)
|
|
112
|
-
receiving = State() # Drain telegrams (INIT or CLEANUP phase)
|
|
113
|
-
resetting = State() # Query error status
|
|
114
|
-
waiting_ok = State() # Await error status response
|
|
115
|
-
|
|
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
|
|
120
|
-
|
|
121
|
-
completed = State(final=True)
|
|
122
|
-
|
|
123
|
-
# Phase transitions - shared states with guards for phase-dependent routing
|
|
124
|
-
do_connect = idle.to(receiving)
|
|
125
|
-
filter_telegram = receiving.to(receiving) # Self-transition: drain to /dev/null
|
|
126
|
-
do_timeout = receiving.to(resetting) | waiting_ok.to(receiving)
|
|
127
|
-
send_error_status = resetting.to(waiting_ok)
|
|
128
|
-
error_status_received = waiting_ok.to(
|
|
129
|
-
receiving, cond="can_retry"
|
|
130
|
-
) # Retry if under limit
|
|
131
|
-
|
|
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
|
|
138
|
-
send_download = requesting.to(waiting_data)
|
|
139
|
-
receive_chunk = waiting_data.to(receiving_chunk)
|
|
140
|
-
send_ack = receiving_chunk.to(waiting_data)
|
|
141
|
-
receive_eof = waiting_data.to(processing_eof)
|
|
142
|
-
|
|
143
|
-
# Return to drain/reset cycle for CLEANUP phase
|
|
144
|
-
do_finish = processing_eof.to(receiving)
|
|
145
|
-
|
|
146
54
|
def __init__(
|
|
147
55
|
self,
|
|
148
56
|
conbus_protocol: ConbusEventProtocol,
|
|
149
57
|
actiontable_serializer: ActionTableSerializer,
|
|
150
|
-
telegram_service: TelegramService,
|
|
151
58
|
) -> None:
|
|
152
59
|
"""Initialize the action table download service.
|
|
153
60
|
|
|
154
61
|
Args:
|
|
155
62
|
conbus_protocol: ConbusEventProtocol instance.
|
|
156
63
|
actiontable_serializer: Action table serializer.
|
|
157
|
-
telegram_service: Telegram service for parsing.
|
|
158
64
|
"""
|
|
159
65
|
self.conbus_protocol = conbus_protocol
|
|
160
66
|
self.serializer = actiontable_serializer
|
|
161
|
-
self.telegram_service = telegram_service
|
|
162
67
|
self.serial_number: str = ""
|
|
163
68
|
self.actiontable_data: list[str] = []
|
|
164
|
-
self.logger = logging.getLogger(__name__)
|
|
165
|
-
self._phase: Phase = Phase.INIT
|
|
166
|
-
self._error_retry_count: int = 0
|
|
167
69
|
self._signals_connected: bool = False
|
|
168
70
|
|
|
169
|
-
#
|
|
71
|
+
# Service signals
|
|
170
72
|
self.on_progress: SignalInstance = SignalInstance((str,))
|
|
171
73
|
self.on_error: SignalInstance = SignalInstance((str,))
|
|
172
74
|
self.on_finish: SignalInstance = SignalInstance()
|
|
@@ -174,74 +76,37 @@ class ActionTableDownloadService(StateMachine):
|
|
|
174
76
|
(ActionTable, dict[str, Any], list[str])
|
|
175
77
|
)
|
|
176
78
|
|
|
177
|
-
# Initialize state machine
|
|
178
|
-
super().__init__(
|
|
79
|
+
# Initialize state machine (must be last - triggers introspection)
|
|
80
|
+
super().__init__()
|
|
81
|
+
|
|
82
|
+
# Override logger for service-specific logging
|
|
83
|
+
self.logger = logging.getLogger(__name__)
|
|
179
84
|
|
|
180
85
|
# Connect protocol signals
|
|
181
86
|
self._connect_signals()
|
|
182
87
|
|
|
183
|
-
#
|
|
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
|
|
208
|
-
|
|
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.
|
|
88
|
+
# Override state entry hooks with protocol behavior
|
|
213
89
|
|
|
214
90
|
def on_enter_receiving(self) -> None:
|
|
215
|
-
"""Enter receiving state -
|
|
216
|
-
self.logger.debug(f"Entering RECEIVING state (phase={self.
|
|
91
|
+
"""Enter receiving state - wait for telegrams to drain."""
|
|
92
|
+
self.logger.debug(f"Entering RECEIVING state (phase={self.phase.value})")
|
|
217
93
|
self.conbus_protocol.wait()
|
|
218
94
|
|
|
219
95
|
def on_enter_resetting(self) -> None:
|
|
220
|
-
"""Enter resetting state -
|
|
221
|
-
self.logger.debug(f"Entering RESETTING state (phase={self.
|
|
222
|
-
self.conbus_protocol.
|
|
223
|
-
telegram_type=TelegramType.SYSTEM,
|
|
224
|
-
serial_number=self.serial_number,
|
|
225
|
-
system_function=SystemFunction.READ_DATAPOINT,
|
|
226
|
-
data_value=DataPointType.MODULE_ERROR_CODE.value,
|
|
227
|
-
)
|
|
96
|
+
"""Enter resetting state - send error status query."""
|
|
97
|
+
self.logger.debug(f"Entering RESETTING state (phase={self.phase.value})")
|
|
98
|
+
self.conbus_protocol.send_error_status_query(serial_number=self.serial_number)
|
|
228
99
|
self.send_error_status()
|
|
229
100
|
|
|
230
101
|
def on_enter_waiting_ok(self) -> None:
|
|
231
|
-
"""Enter waiting_ok state -
|
|
232
|
-
self.logger.debug(f"Entering WAITING_OK state (phase={self.
|
|
102
|
+
"""Enter waiting_ok state - wait for error status response."""
|
|
103
|
+
self.logger.debug(f"Entering WAITING_OK state (phase={self.phase.value})")
|
|
233
104
|
self.conbus_protocol.wait()
|
|
234
105
|
|
|
235
106
|
def on_enter_requesting(self) -> None:
|
|
236
107
|
"""Enter requesting state - send download request."""
|
|
237
|
-
self.
|
|
238
|
-
self.
|
|
239
|
-
self.conbus_protocol.send_telegram(
|
|
240
|
-
telegram_type=TelegramType.SYSTEM,
|
|
241
|
-
serial_number=self.serial_number,
|
|
242
|
-
system_function=SystemFunction.DOWNLOAD_ACTIONTABLE,
|
|
243
|
-
data_value=NO_ERROR_CODE,
|
|
244
|
-
)
|
|
108
|
+
self.enter_download_phase() # Sets phase to DOWNLOAD
|
|
109
|
+
self.conbus_protocol.send_download_request(serial_number=self.serial_number)
|
|
245
110
|
self.send_download()
|
|
246
111
|
|
|
247
112
|
def on_enter_waiting_data(self) -> None:
|
|
@@ -252,16 +117,11 @@ class ActionTableDownloadService(StateMachine):
|
|
|
252
117
|
def on_enter_receiving_chunk(self) -> None:
|
|
253
118
|
"""Enter receiving_chunk state - send ACK."""
|
|
254
119
|
self.logger.debug("Entering RECEIVING_CHUNK state - sending ACK")
|
|
255
|
-
self.conbus_protocol.
|
|
256
|
-
telegram_type=TelegramType.SYSTEM,
|
|
257
|
-
serial_number=self.serial_number,
|
|
258
|
-
system_function=SystemFunction.ACK,
|
|
259
|
-
data_value=NO_ERROR_CODE,
|
|
260
|
-
)
|
|
120
|
+
self.conbus_protocol.send_ack(serial_number=self.serial_number)
|
|
261
121
|
self.send_ack()
|
|
262
122
|
|
|
263
123
|
def on_enter_processing_eof(self) -> None:
|
|
264
|
-
"""Enter processing_eof state - deserialize and emit result
|
|
124
|
+
"""Enter processing_eof state - deserialize and emit result."""
|
|
265
125
|
self.logger.debug("Entering PROCESSING_EOF state - deserializing")
|
|
266
126
|
all_data = "".join(self.actiontable_data)
|
|
267
127
|
actiontable = self.serializer.from_encoded_string(all_data)
|
|
@@ -270,15 +130,19 @@ class ActionTableDownloadService(StateMachine):
|
|
|
270
130
|
self.on_actiontable_received.emit(
|
|
271
131
|
actiontable, actiontable_dict, actiontable_short
|
|
272
132
|
)
|
|
273
|
-
# Switch to CLEANUP phase
|
|
274
|
-
self.
|
|
275
|
-
self.do_finish()
|
|
133
|
+
# Switch to CLEANUP phase
|
|
134
|
+
self.start_cleanup_phase()
|
|
276
135
|
|
|
277
136
|
def on_enter_completed(self) -> None:
|
|
278
|
-
"""Enter completed state -
|
|
137
|
+
"""Enter completed state - emit finish signal."""
|
|
279
138
|
self.logger.debug("Entering COMPLETED state - download finished")
|
|
280
139
|
self.on_finish.emit()
|
|
281
140
|
|
|
141
|
+
def on_max_retries_exceeded(self) -> None:
|
|
142
|
+
"""Handle max retries exceeded - emit error signal."""
|
|
143
|
+
self.logger.error(f"Max error retries ({MAX_ERROR_RETRIES}) exceeded")
|
|
144
|
+
self.on_error.emit(f"Module error persists after {MAX_ERROR_RETRIES} retries")
|
|
145
|
+
|
|
282
146
|
# Protocol event handlers
|
|
283
147
|
|
|
284
148
|
def _on_connection_made(self) -> None:
|
|
@@ -287,14 +151,6 @@ class ActionTableDownloadService(StateMachine):
|
|
|
287
151
|
if self.idle.is_active:
|
|
288
152
|
self.do_connect()
|
|
289
153
|
|
|
290
|
-
def _on_telegram_sent(self, telegram_sent: str) -> None:
|
|
291
|
-
"""Handle telegram sent event.
|
|
292
|
-
|
|
293
|
-
Args:
|
|
294
|
-
telegram_sent: The telegram that was sent.
|
|
295
|
-
"""
|
|
296
|
-
self.logger.debug(f"Telegram sent: {telegram_sent}")
|
|
297
|
-
|
|
298
154
|
def _on_read_datapoint_received(self, reply_telegram: ReplyTelegram) -> None:
|
|
299
155
|
"""Handle READ_DATAPOINT response for error status check.
|
|
300
156
|
|
|
@@ -302,11 +158,10 @@ class ActionTableDownloadService(StateMachine):
|
|
|
302
158
|
reply_telegram: The parsed reply telegram.
|
|
303
159
|
"""
|
|
304
160
|
self.logger.debug(f"Received READ_DATAPOINT in {self.current_state}")
|
|
161
|
+
if reply_telegram.serial_number != self.serial_number:
|
|
162
|
+
return
|
|
305
163
|
|
|
306
164
|
if reply_telegram.datapoint_type != DataPointType.MODULE_ERROR_CODE:
|
|
307
|
-
self.logger.debug(
|
|
308
|
-
f"Filtered: not a MODULE_ERROR_CODE (got {reply_telegram.datapoint_type})"
|
|
309
|
-
)
|
|
310
165
|
return
|
|
311
166
|
|
|
312
167
|
if not self.waiting_ok.is_active:
|
|
@@ -314,44 +169,38 @@ class ActionTableDownloadService(StateMachine):
|
|
|
314
169
|
|
|
315
170
|
is_no_error = reply_telegram.data_value == NO_ERROR_CODE
|
|
316
171
|
if is_no_error:
|
|
317
|
-
self.
|
|
318
|
-
self.no_error_status_received() # Guards determine target state
|
|
172
|
+
self.handle_no_error_received()
|
|
319
173
|
else:
|
|
320
|
-
self.
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
self.error_status_received()
|
|
326
|
-
# Check if guard blocked the transition (still in waiting_ok)
|
|
327
|
-
if self.waiting_ok.is_active:
|
|
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
|
-
)
|
|
334
|
-
|
|
335
|
-
def _on_actiontable_chunk_received(self, reply_telegram: ReplyTelegram) -> None:
|
|
174
|
+
self.handle_error_received()
|
|
175
|
+
|
|
176
|
+
def _on_actiontable_chunk_received(
|
|
177
|
+
self, reply_telegram: ReplyTelegram, actiontable_chunk: str
|
|
178
|
+
) -> None:
|
|
336
179
|
"""Handle actiontable chunk telegram received.
|
|
337
180
|
|
|
338
181
|
Args:
|
|
339
182
|
reply_telegram: The parsed reply telegram containing chunk data.
|
|
183
|
+
actiontable_chunk: The chunk data.
|
|
340
184
|
"""
|
|
341
185
|
self.logger.debug(f"Received actiontable chunk in {self.current_state}")
|
|
186
|
+
if reply_telegram.serial_number != self.serial_number:
|
|
187
|
+
return
|
|
188
|
+
|
|
342
189
|
if self.waiting_data.is_active:
|
|
343
|
-
|
|
344
|
-
self.actiontable_data.append(data_part)
|
|
190
|
+
self.actiontable_data.append(actiontable_chunk)
|
|
345
191
|
self.on_progress.emit(".")
|
|
346
192
|
self.receive_chunk()
|
|
347
193
|
|
|
348
|
-
def _on_eof_received(self,
|
|
194
|
+
def _on_eof_received(self, reply_telegram: ReplyTelegram) -> None:
|
|
349
195
|
"""Handle EOF telegram received.
|
|
350
196
|
|
|
351
197
|
Args:
|
|
352
|
-
|
|
198
|
+
reply_telegram: The parsed reply telegram (unused).
|
|
353
199
|
"""
|
|
354
200
|
self.logger.debug(f"Received EOF in {self.current_state}")
|
|
201
|
+
if reply_telegram.serial_number != self.serial_number:
|
|
202
|
+
return
|
|
203
|
+
|
|
355
204
|
if self.waiting_data.is_active:
|
|
356
205
|
self.receive_eof()
|
|
357
206
|
|
|
@@ -369,42 +218,9 @@ class ActionTableDownloadService(StateMachine):
|
|
|
369
218
|
self.filter_telegram()
|
|
370
219
|
return
|
|
371
220
|
|
|
372
|
-
# Filter invalid telegrams
|
|
373
|
-
if not telegram_received.checksum_valid:
|
|
374
|
-
self.logger.debug("Filtered: invalid checksum")
|
|
375
|
-
return
|
|
376
|
-
|
|
377
|
-
if telegram_received.telegram_type != TelegramType.REPLY.value:
|
|
378
|
-
self.logger.debug(
|
|
379
|
-
f"Filtered: not a reply (got {telegram_received.telegram_type})"
|
|
380
|
-
)
|
|
381
|
-
return
|
|
382
|
-
|
|
383
|
-
if telegram_received.serial_number != self.serial_number:
|
|
384
|
-
self.logger.debug(
|
|
385
|
-
f"Filtered: wrong serial {telegram_received.serial_number} != {self.serial_number}"
|
|
386
|
-
)
|
|
387
|
-
return
|
|
388
|
-
|
|
389
|
-
reply_telegram = self.telegram_service.parse_reply_telegram(
|
|
390
|
-
telegram_received.frame
|
|
391
|
-
)
|
|
392
|
-
|
|
393
|
-
if reply_telegram.system_function == SystemFunction.READ_DATAPOINT:
|
|
394
|
-
self._on_read_datapoint_received(reply_telegram)
|
|
395
|
-
return
|
|
396
|
-
|
|
397
|
-
if reply_telegram.system_function == SystemFunction.ACTIONTABLE:
|
|
398
|
-
self._on_actiontable_chunk_received(reply_telegram)
|
|
399
|
-
return
|
|
400
|
-
|
|
401
|
-
if reply_telegram.system_function == SystemFunction.EOF:
|
|
402
|
-
self._on_eof_received(reply_telegram)
|
|
403
|
-
return
|
|
404
|
-
|
|
405
221
|
def _on_timeout(self) -> None:
|
|
406
222
|
"""Handle timeout event."""
|
|
407
|
-
self.logger.debug(f"Timeout occurred (phase={self.
|
|
223
|
+
self.logger.debug(f"Timeout occurred (phase={self.phase.value})")
|
|
408
224
|
if self.receiving.is_active:
|
|
409
225
|
self.do_timeout() # receiving -> resetting
|
|
410
226
|
elif self.waiting_ok.is_active:
|
|
@@ -472,8 +288,14 @@ class ActionTableDownloadService(StateMachine):
|
|
|
472
288
|
if self._signals_connected:
|
|
473
289
|
return
|
|
474
290
|
self.conbus_protocol.on_connection_made.connect(self._on_connection_made)
|
|
475
|
-
self.conbus_protocol.on_telegram_sent.connect(self._on_telegram_sent)
|
|
476
291
|
self.conbus_protocol.on_telegram_received.connect(self._on_telegram_received)
|
|
292
|
+
self.conbus_protocol.on_read_datapoint_received.connect(
|
|
293
|
+
self._on_read_datapoint_received
|
|
294
|
+
)
|
|
295
|
+
self.conbus_protocol.on_actiontable_chunk_received.connect(
|
|
296
|
+
self._on_actiontable_chunk_received
|
|
297
|
+
)
|
|
298
|
+
self.conbus_protocol.on_eof_received.connect(self._on_eof_received)
|
|
477
299
|
self.conbus_protocol.on_timeout.connect(self._on_timeout)
|
|
478
300
|
self.conbus_protocol.on_failed.connect(self._on_failed)
|
|
479
301
|
self._signals_connected = True
|
|
@@ -483,8 +305,14 @@ class ActionTableDownloadService(StateMachine):
|
|
|
483
305
|
if not self._signals_connected:
|
|
484
306
|
return
|
|
485
307
|
self.conbus_protocol.on_connection_made.disconnect(self._on_connection_made)
|
|
486
|
-
self.conbus_protocol.on_telegram_sent.disconnect(self._on_telegram_sent)
|
|
487
308
|
self.conbus_protocol.on_telegram_received.disconnect(self._on_telegram_received)
|
|
309
|
+
self.conbus_protocol.on_read_datapoint_received.disconnect(
|
|
310
|
+
self._on_read_datapoint_received
|
|
311
|
+
)
|
|
312
|
+
self.conbus_protocol.on_actiontable_chunk_received.disconnect(
|
|
313
|
+
self._on_actiontable_chunk_received
|
|
314
|
+
)
|
|
315
|
+
self.conbus_protocol.on_eof_received.disconnect(self._on_eof_received)
|
|
488
316
|
self.conbus_protocol.on_timeout.disconnect(self._on_timeout)
|
|
489
317
|
self.conbus_protocol.on_failed.disconnect(self._on_failed)
|
|
490
318
|
self._signals_connected = False
|
|
@@ -497,20 +325,12 @@ class ActionTableDownloadService(StateMachine):
|
|
|
497
325
|
"""
|
|
498
326
|
# Reset state for singleton reuse
|
|
499
327
|
self.actiontable_data = []
|
|
500
|
-
|
|
501
|
-
self.
|
|
502
|
-
# Reset state machine to idle
|
|
503
|
-
self._reset_state()
|
|
328
|
+
# Reset state machine
|
|
329
|
+
self.reset()
|
|
504
330
|
# Reconnect signals (in case previously disconnected)
|
|
505
331
|
self._connect_signals()
|
|
506
332
|
return self
|
|
507
333
|
|
|
508
|
-
def _reset_state(self) -> None:
|
|
509
|
-
"""Reset state machine to initial state."""
|
|
510
|
-
# python-statemachine uses model.state to track current state
|
|
511
|
-
# Set it directly to the initial state id
|
|
512
|
-
self.model.state = self.idle.id
|
|
513
|
-
|
|
514
334
|
def __exit__(
|
|
515
335
|
self, _exc_type: Optional[type], _exc_val: Optional[Exception], _exc_tb: Any
|
|
516
336
|
) -> None:
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""State machine for ActionTable download workflow."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from abc import ABCMeta, abstractmethod
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
from statemachine import State, StateMachine
|
|
8
|
+
from statemachine.factory import StateMachineMetaclass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AbstractStateMachineMeta(StateMachineMetaclass, ABCMeta):
|
|
12
|
+
"""Combined metaclass for abstract state machines.
|
|
13
|
+
|
|
14
|
+
Combines StateMachineMetaclass (for state machine introspection) with
|
|
15
|
+
ABCMeta (for abstract method enforcement).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Constants
|
|
22
|
+
MAX_ERROR_RETRIES = 3 # Max retries for error_status_received before giving up
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Phase(Enum):
|
|
26
|
+
"""Download workflow phases.
|
|
27
|
+
|
|
28
|
+
The download workflow consists of three sequential phases:
|
|
29
|
+
- INIT: Drain pending telegrams, query error status → proceed to DOWNLOAD
|
|
30
|
+
- DOWNLOAD: Request actiontable, receive chunks with ACK, until EOF
|
|
31
|
+
- CLEANUP: Drain pending telegrams, query error status → proceed to COMPLETED
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
INIT: Initial phase - drain pending telegrams and query error status.
|
|
35
|
+
DOWNLOAD: Download phase - request actiontable and receive chunks.
|
|
36
|
+
CLEANUP: Cleanup phase - drain remaining telegrams and verify status.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
INIT = "init"
|
|
40
|
+
DOWNLOAD = "download"
|
|
41
|
+
CLEANUP = "cleanup"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ActionTableDownloadStateMachine(StateMachine, metaclass=AbstractStateMachineMeta):
|
|
45
|
+
"""State machine for ActionTable download workflow.
|
|
46
|
+
|
|
47
|
+
Pure state machine with states, transitions, and guards. Subclasses can
|
|
48
|
+
override on_enter_* methods to add protocol-specific behavior.
|
|
49
|
+
|
|
50
|
+
States (9 total):
|
|
51
|
+
idle -> receiving -> resetting -> waiting_ok -> requesting
|
|
52
|
+
-> waiting_data <-> receiving_chunk -> processing_eof -> completed
|
|
53
|
+
|
|
54
|
+
Phases - INIT and CLEANUP share the same states (receiving, resetting, waiting_ok):
|
|
55
|
+
|
|
56
|
+
INIT phase (drain → reset → wait_ok):
|
|
57
|
+
idle -> receiving -> resetting -> waiting_ok --(guard: is_init_phase)--> requesting
|
|
58
|
+
|
|
59
|
+
DOWNLOAD phase (request → receive chunks → EOF):
|
|
60
|
+
requesting -> waiting_data <-> receiving_chunk -> processing_eof
|
|
61
|
+
|
|
62
|
+
CLEANUP phase (drain → reset → wait_ok):
|
|
63
|
+
processing_eof -> receiving -> resetting -> waiting_ok --(guard: is_cleanup_phase)--> completed
|
|
64
|
+
|
|
65
|
+
The drain/reset/wait_ok cycle:
|
|
66
|
+
1. Drain pending telegrams (receiving state discards telegrams)
|
|
67
|
+
2. Timeout triggers error status query (resetting)
|
|
68
|
+
3. Wait for response (waiting_ok)
|
|
69
|
+
4. On no error: guard determines target (requesting or completed)
|
|
70
|
+
On error: retry from drain step (limited by MAX_ERROR_RETRIES)
|
|
71
|
+
|
|
72
|
+
Attributes:
|
|
73
|
+
phase: Current workflow phase (INIT, DOWNLOAD, CLEANUP).
|
|
74
|
+
error_retry_count: Current error retry count.
|
|
75
|
+
idle: Initial state before connection.
|
|
76
|
+
receiving: Drain pending telegrams state (INIT or CLEANUP phase).
|
|
77
|
+
resetting: Query error status state.
|
|
78
|
+
waiting_ok: Await error status response state.
|
|
79
|
+
requesting: DOWNLOAD phase state - send download request.
|
|
80
|
+
waiting_data: DOWNLOAD phase state - await chunks.
|
|
81
|
+
receiving_chunk: DOWNLOAD phase state - process chunk.
|
|
82
|
+
processing_eof: DOWNLOAD phase state - deserialize result.
|
|
83
|
+
completed: Final state - download finished.
|
|
84
|
+
do_connect: Transition from idle to receiving.
|
|
85
|
+
filter_telegram: Self-transition in receiving state for draining.
|
|
86
|
+
do_timeout: Timeout transitions from receiving/waiting_ok.
|
|
87
|
+
send_error_status: Transition from resetting to waiting_ok.
|
|
88
|
+
error_status_received: Transition from waiting_ok to receiving on error.
|
|
89
|
+
no_error_status_received: Conditional transition based on phase.
|
|
90
|
+
send_download: Transition from requesting to waiting_data.
|
|
91
|
+
receive_chunk: Transition from waiting_data to receiving_chunk.
|
|
92
|
+
send_ack: Transition from receiving_chunk to waiting_data.
|
|
93
|
+
receive_eof: Transition from waiting_data to processing_eof.
|
|
94
|
+
do_finish: Transition from processing_eof to receiving.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
# States - unified for INIT and CLEANUP phases using guards
|
|
98
|
+
idle = State(initial=True)
|
|
99
|
+
receiving = State() # Drain telegrams (INIT or CLEANUP phase)
|
|
100
|
+
resetting = State() # Query error status
|
|
101
|
+
waiting_ok = State() # Await error status response
|
|
102
|
+
|
|
103
|
+
requesting = State() # DOWNLOAD phase: send download request
|
|
104
|
+
waiting_data = State() # DOWNLOAD phase: await chunks
|
|
105
|
+
receiving_chunk = State() # DOWNLOAD phase: process chunk
|
|
106
|
+
processing_eof = State() # DOWNLOAD phase: deserialize result
|
|
107
|
+
|
|
108
|
+
completed = State(final=True)
|
|
109
|
+
|
|
110
|
+
# Phase transitions - shared states with guards for phase-dependent routing
|
|
111
|
+
do_connect = idle.to(receiving)
|
|
112
|
+
filter_telegram = receiving.to(receiving) # Self-transition: drain to /dev/null
|
|
113
|
+
do_timeout = receiving.to(resetting) | waiting_ok.to(receiving)
|
|
114
|
+
send_error_status = resetting.to(waiting_ok)
|
|
115
|
+
error_status_received = waiting_ok.to(
|
|
116
|
+
receiving, cond="can_retry"
|
|
117
|
+
) # Retry if under limit
|
|
118
|
+
|
|
119
|
+
# Conditional transitions based on phase
|
|
120
|
+
no_error_status_received = waiting_ok.to(
|
|
121
|
+
requesting, cond="is_init_phase"
|
|
122
|
+
) | waiting_ok.to(completed, cond="is_cleanup_phase")
|
|
123
|
+
|
|
124
|
+
# DOWNLOAD phase transitions
|
|
125
|
+
send_download = requesting.to(waiting_data)
|
|
126
|
+
receive_chunk = waiting_data.to(receiving_chunk)
|
|
127
|
+
send_ack = receiving_chunk.to(waiting_data)
|
|
128
|
+
receive_eof = waiting_data.to(processing_eof)
|
|
129
|
+
|
|
130
|
+
# Return to drain/reset cycle for CLEANUP phase
|
|
131
|
+
do_finish = processing_eof.to(receiving)
|
|
132
|
+
|
|
133
|
+
def __init__(self) -> None:
|
|
134
|
+
"""Initialize the state machine."""
|
|
135
|
+
self.logger = logging.getLogger(__name__)
|
|
136
|
+
self._phase: Phase = Phase.INIT
|
|
137
|
+
self._error_retry_count: int = 0
|
|
138
|
+
|
|
139
|
+
# Initialize state machine
|
|
140
|
+
super().__init__(allow_event_without_transition=True)
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def phase(self) -> Phase:
|
|
144
|
+
"""Get current phase."""
|
|
145
|
+
return self._phase
|
|
146
|
+
|
|
147
|
+
@phase.setter
|
|
148
|
+
def phase(self, value: Phase) -> None:
|
|
149
|
+
"""Set current phase.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
value: The phase value to set.
|
|
153
|
+
"""
|
|
154
|
+
self._phase = value
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def error_retry_count(self) -> int:
|
|
158
|
+
"""Get current error retry count."""
|
|
159
|
+
return self._error_retry_count
|
|
160
|
+
|
|
161
|
+
@error_retry_count.setter
|
|
162
|
+
def error_retry_count(self, value: int) -> None:
|
|
163
|
+
"""Set error retry count.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
value: The error retry count value to set.
|
|
167
|
+
"""
|
|
168
|
+
self._error_retry_count = value
|
|
169
|
+
|
|
170
|
+
# Guard conditions for phase-dependent transitions
|
|
171
|
+
|
|
172
|
+
def is_init_phase(self) -> bool:
|
|
173
|
+
"""Guard: check if currently in INIT phase.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
True if in INIT phase, False otherwise.
|
|
177
|
+
"""
|
|
178
|
+
return self._phase == Phase.INIT
|
|
179
|
+
|
|
180
|
+
def is_cleanup_phase(self) -> bool:
|
|
181
|
+
"""Guard: check if currently in CLEANUP phase.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
True if in CLEANUP phase, False otherwise.
|
|
185
|
+
"""
|
|
186
|
+
return self._phase == Phase.CLEANUP
|
|
187
|
+
|
|
188
|
+
def can_retry(self) -> bool:
|
|
189
|
+
"""Guard: check if retry is allowed (under max limit).
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
True if retry count is under MAX_ERROR_RETRIES, False otherwise.
|
|
193
|
+
"""
|
|
194
|
+
return self._error_retry_count < MAX_ERROR_RETRIES
|
|
195
|
+
|
|
196
|
+
# State entry hooks - subclasses MUST implement these
|
|
197
|
+
|
|
198
|
+
@abstractmethod
|
|
199
|
+
def on_enter_receiving(self) -> None:
|
|
200
|
+
"""Enter receiving state - drain pending telegrams."""
|
|
201
|
+
...
|
|
202
|
+
|
|
203
|
+
@abstractmethod
|
|
204
|
+
def on_enter_resetting(self) -> None:
|
|
205
|
+
"""Enter resetting state - query error status."""
|
|
206
|
+
...
|
|
207
|
+
|
|
208
|
+
@abstractmethod
|
|
209
|
+
def on_enter_waiting_ok(self) -> None:
|
|
210
|
+
"""Enter waiting_ok state - awaiting error status response."""
|
|
211
|
+
...
|
|
212
|
+
|
|
213
|
+
@abstractmethod
|
|
214
|
+
def on_enter_requesting(self) -> None:
|
|
215
|
+
"""Enter requesting state - send download request."""
|
|
216
|
+
...
|
|
217
|
+
|
|
218
|
+
@abstractmethod
|
|
219
|
+
def on_enter_waiting_data(self) -> None:
|
|
220
|
+
"""Enter waiting_data state - wait for actiontable chunks."""
|
|
221
|
+
...
|
|
222
|
+
|
|
223
|
+
@abstractmethod
|
|
224
|
+
def on_enter_receiving_chunk(self) -> None:
|
|
225
|
+
"""Enter receiving_chunk state - send ACK."""
|
|
226
|
+
...
|
|
227
|
+
|
|
228
|
+
@abstractmethod
|
|
229
|
+
def on_enter_processing_eof(self) -> None:
|
|
230
|
+
"""Enter processing_eof state - deserialize and emit result."""
|
|
231
|
+
...
|
|
232
|
+
|
|
233
|
+
@abstractmethod
|
|
234
|
+
def on_enter_completed(self) -> None:
|
|
235
|
+
"""Enter completed state - download finished."""
|
|
236
|
+
...
|
|
237
|
+
|
|
238
|
+
@abstractmethod
|
|
239
|
+
def on_max_retries_exceeded(self) -> None:
|
|
240
|
+
"""Called when max error retries exceeded."""
|
|
241
|
+
...
|
|
242
|
+
|
|
243
|
+
# Public methods for state machine control
|
|
244
|
+
|
|
245
|
+
def enter_download_phase(self) -> None:
|
|
246
|
+
"""Enter requesting state - send download request."""
|
|
247
|
+
self._phase = Phase.DOWNLOAD
|
|
248
|
+
|
|
249
|
+
def handle_no_error_received(self) -> None:
|
|
250
|
+
"""Handle successful error status check (no error)."""
|
|
251
|
+
self._error_retry_count = 0 # Reset on success
|
|
252
|
+
self.no_error_status_received()
|
|
253
|
+
|
|
254
|
+
def handle_error_received(self) -> None:
|
|
255
|
+
"""Handle error status received - increment retry and attempt transition."""
|
|
256
|
+
self._error_retry_count += 1
|
|
257
|
+
self.logger.debug(
|
|
258
|
+
f"Error status received, retry {self._error_retry_count}/{MAX_ERROR_RETRIES}"
|
|
259
|
+
)
|
|
260
|
+
# Guard can_retry blocks transition if max retries exceeded
|
|
261
|
+
self.error_status_received()
|
|
262
|
+
# Check if guard blocked the transition (still in waiting_ok)
|
|
263
|
+
if self.waiting_ok.is_active:
|
|
264
|
+
self.on_max_retries_exceeded()
|
|
265
|
+
|
|
266
|
+
def start_cleanup_phase(self) -> None:
|
|
267
|
+
"""Switch to CLEANUP phase and trigger do_finish transition."""
|
|
268
|
+
self._phase = Phase.CLEANUP
|
|
269
|
+
self.do_finish()
|
|
270
|
+
|
|
271
|
+
def reset(self) -> None:
|
|
272
|
+
"""Reset state machine to initial state."""
|
|
273
|
+
self._phase = Phase.INIT
|
|
274
|
+
self._error_retry_count = 0
|
|
275
|
+
# python-statemachine uses model.state to track current state
|
|
276
|
+
self.model.state = self.idle.id
|
|
@@ -21,10 +21,17 @@ from xp.models import ConbusClientConfig, ModuleTypeCode
|
|
|
21
21
|
from xp.models.protocol.conbus_protocol import (
|
|
22
22
|
TelegramReceivedEvent,
|
|
23
23
|
)
|
|
24
|
+
from xp.models.telegram.datapoint_type import DataPointType
|
|
25
|
+
from xp.models.telegram.reply_telegram import ReplyTelegram
|
|
24
26
|
from xp.models.telegram.system_function import SystemFunction
|
|
25
27
|
from xp.models.telegram.telegram_type import TelegramType
|
|
28
|
+
from xp.services import TelegramService
|
|
26
29
|
from xp.utils import calculate_checksum
|
|
27
30
|
|
|
31
|
+
# Constants
|
|
32
|
+
NO_ERROR_CODE = "00"
|
|
33
|
+
CHUNK_HEADER_LENGTH = 2 # data_value format: 2-char counter + actiontable chunk
|
|
34
|
+
|
|
28
35
|
|
|
29
36
|
class ConbusEventProtocol(protocol.Protocol, protocol.ClientFactory):
|
|
30
37
|
"""Twisted protocol for XP telegram communication.
|
|
@@ -47,6 +54,10 @@ class ConbusEventProtocol(protocol.Protocol, protocol.ClientFactory):
|
|
|
47
54
|
on_telegram_sent: Signal emitted when a telegram is sent.
|
|
48
55
|
on_data_received: Signal emitted when data is received.
|
|
49
56
|
on_telegram_received: Signal emitted when a telegram is received.
|
|
57
|
+
on_invalid_telegram_received: Signal emitted when invalid telegram received.
|
|
58
|
+
on_read_datapoint_received: Signal emitted when read datapoint reply received.
|
|
59
|
+
on_actiontable_chunk_received: Signal emitted when actiontable chunk received.
|
|
60
|
+
on_eof_received: Signal emitted when EOF telegram received.
|
|
50
61
|
on_timeout: Signal emitted when timeout occurs.
|
|
51
62
|
on_failed: Signal emitted when operation fails.
|
|
52
63
|
on_start_reactor: Signal emitted when reactor starts.
|
|
@@ -68,6 +79,11 @@ class ConbusEventProtocol(protocol.Protocol, protocol.ClientFactory):
|
|
|
68
79
|
on_telegram_sent: Signal = Signal(bytes)
|
|
69
80
|
on_data_received: Signal = Signal(bytes)
|
|
70
81
|
on_telegram_received: Signal = Signal(TelegramReceivedEvent)
|
|
82
|
+
on_invalid_telegram_received: Signal = Signal(TelegramReceivedEvent)
|
|
83
|
+
on_read_datapoint_received: Signal = Signal(ReplyTelegram)
|
|
84
|
+
on_actiontable_chunk_received: Signal = Signal(ReplyTelegram, str)
|
|
85
|
+
on_eof_received: Signal = Signal(ReplyTelegram)
|
|
86
|
+
|
|
71
87
|
on_timeout: Signal = Signal()
|
|
72
88
|
on_failed: Signal = Signal(str)
|
|
73
89
|
on_start_reactor: Signal = Signal()
|
|
@@ -77,12 +93,14 @@ class ConbusEventProtocol(protocol.Protocol, protocol.ClientFactory):
|
|
|
77
93
|
self,
|
|
78
94
|
cli_config: ConbusClientConfig,
|
|
79
95
|
reactor: PosixReactorBase,
|
|
96
|
+
telegram_service: TelegramService,
|
|
80
97
|
) -> None:
|
|
81
98
|
"""Initialize ConbusProtocol.
|
|
82
99
|
|
|
83
100
|
Args:
|
|
84
101
|
cli_config: Configuration for Conbus client connection.
|
|
85
102
|
reactor: Twisted reactor for event handling.
|
|
103
|
+
telegram_service: Telegram service for parsing telegrams.
|
|
86
104
|
"""
|
|
87
105
|
self.buffer = b""
|
|
88
106
|
self.logger = logging.getLogger(__name__)
|
|
@@ -90,6 +108,7 @@ class ConbusEventProtocol(protocol.Protocol, protocol.ClientFactory):
|
|
|
90
108
|
self._reactor = reactor
|
|
91
109
|
self.timeout_seconds = self.cli_config.timeout
|
|
92
110
|
self.timeout_call: Optional[DelayedCall] = None
|
|
111
|
+
self.telegram_service = telegram_service
|
|
93
112
|
|
|
94
113
|
def connectionMade(self) -> None:
|
|
95
114
|
"""Handle connection established event.
|
|
@@ -171,7 +190,46 @@ class ConbusEventProtocol(protocol.Protocol, protocol.ClientFactory):
|
|
|
171
190
|
checksum=checksum,
|
|
172
191
|
checksum_valid=checksum_valid,
|
|
173
192
|
)
|
|
174
|
-
self.
|
|
193
|
+
self.emit_telegram_received(telegram_received)
|
|
194
|
+
|
|
195
|
+
def emit_telegram_received(self, telegram_received: TelegramReceivedEvent) -> None:
|
|
196
|
+
"""Handle telegram received event.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
telegram_received: The telegram received event.
|
|
200
|
+
"""
|
|
201
|
+
self.logger.debug(f"Received {telegram_received}")
|
|
202
|
+
self.on_telegram_received.emit(telegram_received)
|
|
203
|
+
|
|
204
|
+
# Filter invalid telegrams
|
|
205
|
+
if not telegram_received.checksum_valid:
|
|
206
|
+
self.logger.debug("Filtered: invalid checksum")
|
|
207
|
+
self.on_invalid_telegram_received.emit(telegram_received)
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
if telegram_received.telegram_type != TelegramType.REPLY.value:
|
|
211
|
+
self.logger.debug(
|
|
212
|
+
f"Filtered: not a reply (got {telegram_received.telegram_type})"
|
|
213
|
+
)
|
|
214
|
+
self.on_invalid_telegram_received.emit(telegram_received)
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
reply_telegram = self.telegram_service.parse_reply_telegram(
|
|
218
|
+
telegram_received.frame
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
if reply_telegram.system_function == SystemFunction.READ_DATAPOINT:
|
|
222
|
+
self.on_read_datapoint_received.emit(reply_telegram)
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
if reply_telegram.system_function == SystemFunction.ACTIONTABLE:
|
|
226
|
+
actiontable_chunk = reply_telegram.data_value[CHUNK_HEADER_LENGTH:]
|
|
227
|
+
self.on_actiontable_chunk_received.emit(reply_telegram, actiontable_chunk)
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
if reply_telegram.system_function == SystemFunction.EOF:
|
|
231
|
+
self.on_eof_received.emit(reply_telegram)
|
|
232
|
+
return
|
|
175
233
|
|
|
176
234
|
def sendFrame(self, data: bytes) -> None:
|
|
177
235
|
"""Send telegram frame.
|
|
@@ -243,6 +301,45 @@ class ConbusEventProtocol(protocol.Protocol, protocol.ClientFactory):
|
|
|
243
301
|
self.telegram_queue.put_nowait(payload.encode())
|
|
244
302
|
self.call_later(0.0, self.start_queue_manager)
|
|
245
303
|
|
|
304
|
+
def send_error_status_query(self, serial_number: str) -> None:
|
|
305
|
+
"""Send error status query telegram.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
serial_number: Device serial number.
|
|
309
|
+
"""
|
|
310
|
+
self.send_telegram(
|
|
311
|
+
telegram_type=TelegramType.SYSTEM,
|
|
312
|
+
serial_number=serial_number,
|
|
313
|
+
system_function=SystemFunction.READ_DATAPOINT,
|
|
314
|
+
data_value=DataPointType.MODULE_ERROR_CODE.value,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
def send_download_request(self, serial_number: str) -> None:
|
|
318
|
+
"""Send download request telegram.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
serial_number: Device serial number.
|
|
322
|
+
"""
|
|
323
|
+
self.send_telegram(
|
|
324
|
+
telegram_type=TelegramType.SYSTEM,
|
|
325
|
+
serial_number=serial_number,
|
|
326
|
+
system_function=SystemFunction.DOWNLOAD_ACTIONTABLE,
|
|
327
|
+
data_value=NO_ERROR_CODE,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
def send_ack(self, serial_number: str) -> None:
|
|
331
|
+
"""Send ACK telegram.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
serial_number: Device serial number.
|
|
335
|
+
"""
|
|
336
|
+
self.send_telegram(
|
|
337
|
+
telegram_type=TelegramType.SYSTEM,
|
|
338
|
+
serial_number=serial_number,
|
|
339
|
+
system_function=SystemFunction.ACK,
|
|
340
|
+
data_value=NO_ERROR_CODE,
|
|
341
|
+
)
|
|
342
|
+
|
|
246
343
|
def call_later(
|
|
247
344
|
self,
|
|
248
345
|
delay: float,
|
xp/utils/dependencies.py
CHANGED
|
@@ -207,6 +207,7 @@ class ServiceContainer:
|
|
|
207
207
|
factory=lambda: ConbusEventProtocol(
|
|
208
208
|
cli_config=self.container.resolve(ConbusClientConfig),
|
|
209
209
|
reactor=self.container.resolve(PosixReactorBase),
|
|
210
|
+
telegram_service=self.container.resolve(TelegramService),
|
|
210
211
|
),
|
|
211
212
|
scope=punq.Scope.singleton,
|
|
212
213
|
)
|
|
@@ -326,7 +327,6 @@ class ServiceContainer:
|
|
|
326
327
|
factory=lambda: ActionTableDownloadService(
|
|
327
328
|
conbus_protocol=self.container.resolve(ConbusEventProtocol),
|
|
328
329
|
actiontable_serializer=self.container.resolve(ActionTableSerializer),
|
|
329
|
-
telegram_service=self.container.resolve(TelegramService),
|
|
330
330
|
),
|
|
331
331
|
scope=punq.Scope.singleton,
|
|
332
332
|
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|