conson-xp 1.44.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: conson-xp
3
- Version: 1.44.0
3
+ Version: 1.46.0
4
4
  Summary: XP Protocol Communication Tools
5
5
  Author-Email: ldvchosal <ldvchosal@github.com>
6
6
  License: MIT License
@@ -1,14 +1,14 @@
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
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
9
9
  xp/cli/commands/conbus/__init__.py,sha256=HYaX2__XxwD3Xaw4CzflvL8CwoUa4yR6wOyH8wwyofM,535
10
10
  xp/cli/commands/conbus/conbus.py,sha256=1j9-Nwnf6nqD4ztBZnZEbJOTlkYo4D-Ks2J7BGNF3MU,3575
11
- xp/cli/commands/conbus/conbus_actiontable_commands.py,sha256=yLVSADwei7tX6JFa25TCpTxyMHyNJfWlDFqpR8AGRmo,7657
11
+ xp/cli/commands/conbus/conbus_actiontable_commands.py,sha256=a46O8XYm7c8Z6Ao7ff92MdvlSrWcYZvNpZi2N2Vca6c,7661
12
12
  xp/cli/commands/conbus/conbus_autoreport_commands.py,sha256=TgXFVpoyFNiEatL6r78IghzyF0R2S3cgTrGZaiJPjwA,3934
13
13
  xp/cli/commands/conbus/conbus_blink_commands.py,sha256=HRn4Lr_BO7_WynsaUnO_hKezOi3MVhkPYEOnh0rMMlg,5324
14
14
  xp/cli/commands/conbus/conbus_config_commands.py,sha256=BugIbgNX6_s4MySGvI6tWZkwguciajHUX2Xz8XBux7k,716
@@ -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=RMGC3WxlMQDJkkXy3oKmqkORrXNwQyjDkyfEGWX3JJE,21294
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=XjdVwy7P87Utu_UqK7upfugrA5fYG2iWBwC4go9KFLY,15498
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=zrvWx28N0f28JwRDRyqaf5Q9eV_yLwh9xDw9mYBUXEQ,25379
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.44.0.dist-info/RECORD,,
211
+ conson_xp-1.46.0.dist-info/RECORD,,
xp/__init__.py CHANGED
@@ -3,7 +3,7 @@
3
3
  conson-xp package.
4
4
  """
5
5
 
6
- __version__ = "1.44.0"
6
+ __version__ = "1.46.0"
7
7
  __manufacturer__ = "salchichon"
8
8
  __model__ = "xp.cli"
9
9
  __serial__ = "2025.09.23.000"
@@ -98,7 +98,7 @@ def conbus_download_actiontable(ctx: Context, serial_number: str) -> None:
98
98
  service.on_finish.connect(on_finish)
99
99
  service.on_actiontable_received.connect(on_actiontable_received)
100
100
  service.on_error.connect(on_error)
101
- service.start(serial_number=serial_number)
101
+ service.configure(serial_number=serial_number)
102
102
  service.start_reactor()
103
103
 
104
104
 
@@ -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.protocol.conbus_event_protocol import ConbusEventProtocol
19
- from xp.services.telegram.telegram_service import TelegramService
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 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
27
+ Inherits from ActionTableDownloadStateMachine and overrides on_enter_*
28
+ methods to add protocol-specific behavior.
52
29
 
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):
30
+ The workflow consists of three phases:
58
31
 
59
32
  INIT phase (drain → reset → wait_ok):
60
- idle -> receiving -> resetting -> waiting_ok --(guard: is_init_phase)--> requesting
33
+ Connection established, drain pending telegrams, query error status.
61
34
 
62
35
  DOWNLOAD phase (request → receive chunks → EOF):
63
- requesting -> waiting_data <-> receiving_chunk -> processing_eof
36
+ Request actiontable, receive and ACK chunks until EOF.
64
37
 
65
38
  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
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
- # Signals (instance attributes to avoid conflict with statemachine)
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 first (before connecting signals)
178
- super().__init__(allow_event_without_transition=True)
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
- # 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
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 - drain pending telegrams."""
216
- self.logger.debug(f"Entering RECEIVING state (phase={self._phase.value})")
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 - query error status."""
221
- self.logger.debug(f"Entering RESETTING state (phase={self._phase.value})")
222
- self.conbus_protocol.send_telegram(
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 - awaiting error status response."""
232
- self.logger.debug(f"Entering WAITING_OK state (phase={self._phase.value})")
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._phase = Phase.DOWNLOAD
238
- self.logger.debug("Entering REQUESTING state - sending download request")
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.send_telegram(
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, then cleanup."""
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 before returning to receiving state
274
- self._phase = Phase.CLEANUP
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 - download finished."""
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._error_retry_count = 0 # Reset on success
318
- self.no_error_status_received() # Guards determine target state
172
+ self.handle_no_error_received()
319
173
  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)
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
- data_part = reply_telegram.data_value[CHUNK_HEADER_LENGTH:]
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, _reply_telegram: ReplyTelegram) -> None:
194
+ def _on_eof_received(self, reply_telegram: ReplyTelegram) -> None:
349
195
  """Handle EOF telegram received.
350
196
 
351
197
  Args:
352
- _reply_telegram: The parsed reply telegram (unused).
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._phase.value})")
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
- self._phase = Phase.INIT
501
- self._error_retry_count = 0
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.on_telegram_received.emit(telegram_received)
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
  )