conson-xp 1.41.0__py3-none-any.whl → 1.43.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.41.0
3
+ Version: 1.43.0
4
4
  Summary: XP Protocol Communication Tools
5
5
  Author-Email: ldvchosal <ldvchosal@github.com>
6
6
  License: MIT License
@@ -49,6 +49,7 @@ Requires-Dist: twisted>=25.5.0
49
49
  Requires-Dist: bubus>=1.5.6
50
50
  Requires-Dist: psygnal>=0.15.0
51
51
  Requires-Dist: textual>=1.0.0
52
+ Requires-Dist: python-statemachine>=2.5.0
52
53
  Description-Content-Type: text/markdown
53
54
 
54
55
  # 🔌 XP Protocol Communication Tool
@@ -1,14 +1,14 @@
1
- conson_xp-1.41.0.dist-info/METADATA,sha256=pML1FepvoeR1ktngrS69ieWa5rMO0j43m8BC22G_UW0,11361
2
- conson_xp-1.41.0.dist-info/WHEEL,sha256=tsUv_t7BDeJeRHaSrczbGeuK-TtDpGsWi_JfpzD255I,90
3
- conson_xp-1.41.0.dist-info/entry_points.txt,sha256=1OcdIcDM1hz3ljCXgybaPUh1IOFEwkaTgLIW9u9zGeg,50
4
- conson_xp-1.41.0.dist-info/licenses/LICENSE,sha256=rxj6woMM-r3YCyGq_UHFtbh7kHTAJgrccH6O-33IDE4,1419
5
- xp/__init__.py,sha256=fIc-tmd56J8tWGwB8NrHfsUhz6rBuPHOZmnmWfimedo,181
1
+ conson_xp-1.43.0.dist-info/METADATA,sha256=UEKUuurqgD2CZtaFX4SC-svZfzBSrOhJ7T1Dqg-HTK4,11403
2
+ conson_xp-1.43.0.dist-info/WHEEL,sha256=tsUv_t7BDeJeRHaSrczbGeuK-TtDpGsWi_JfpzD255I,90
3
+ conson_xp-1.43.0.dist-info/entry_points.txt,sha256=1OcdIcDM1hz3ljCXgybaPUh1IOFEwkaTgLIW9u9zGeg,50
4
+ conson_xp-1.43.0.dist-info/licenses/LICENSE,sha256=rxj6woMM-r3YCyGq_UHFtbh7kHTAJgrccH6O-33IDE4,1419
5
+ xp/__init__.py,sha256=af_iRTXOsyO7zIzqM-7iNsCptIKD9yYi2OXdX5aD458,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=fIEe45iMWZ3Dbr0STzO8qbtWHR-R8qpGa_sLNeBm_og,7471
11
+ xp/cli/commands/conbus/conbus_actiontable_commands.py,sha256=yLVSADwei7tX6JFa25TCpTxyMHyNJfWlDFqpR8AGRmo,7657
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,7 @@ xp/services/actiontable/msactiontable_xp24_serializer.py,sha256=ku84HCCjYDM9XpRK
121
121
  xp/services/actiontable/msactiontable_xp33_serializer.py,sha256=MFjEwSsIa8l3RJt-ig828E6kyiFYYXMHFKe4Q0A3NvA,8781
122
122
  xp/services/conbus/__init__.py,sha256=Hi35sMKu9o6LpYoi2tQDaQoMb8M5sOt_-LUTxxaCU_0,28
123
123
  xp/services/conbus/actiontable/__init__.py,sha256=oD6vRk_Ye-eZ9s_hldAgtRJFu4mfAnODqpkJUGHHszk,40
124
- xp/services/conbus/actiontable/actiontable_download_service.py,sha256=C6cjNRRsl7_jjn94I6ycCDvoqIpivNv0cMVkR-CQBXk,7608
124
+ xp/services/conbus/actiontable/actiontable_download_service.py,sha256=OD58pJyk-ij26a_ISc4OoK4X7rVj7rKwanV2wYVhsQY,18670
125
125
  xp/services/conbus/actiontable/actiontable_list_service.py,sha256=BYNpxdhdVvn5Z3XXvUYyiGGZ3fOoKR1F0boYDfAQ80c,2894
126
126
  xp/services/conbus/actiontable/actiontable_show_service.py,sha256=Z11bVYstnIhahFKTL8WqgTmK8rMK6kvismdy7679ojY,3013
127
127
  xp/services/conbus/actiontable/actiontable_upload_service.py,sha256=HTWJqJTIDFsp5v0SBx1Wt5gUeso6w03N6bKbtsbHtJY,9705
@@ -161,7 +161,7 @@ xp/services/homekit/homekit_service.py,sha256=0lW-hg40ETco3gDBEYkR_sX-UIYsLSKCD4
161
161
  xp/services/log_file_service.py,sha256=fvPcZQj8XOKUA-4ep5R8n0PelvwvRlTLlVxvIWM5KR4,10506
162
162
  xp/services/module_type_service.py,sha256=xWhr1EAZMykL5uNWHWdpa5T8yNruGKH43XRTOS8GwZg,7477
163
163
  xp/services/protocol/__init__.py,sha256=qRufBmqRKGzpuzZ5bxBbmwf510TT00Ke8s5HcWGnqRY,818
164
- xp/services/protocol/conbus_event_protocol.py,sha256=PpSr5sWcibN_xb2iMWAtWc0dTkUkFZelgYuPiejutkE,15155
164
+ xp/services/protocol/conbus_event_protocol.py,sha256=XjdVwy7P87Utu_UqK7upfugrA5fYG2iWBwC4go9KFLY,15498
165
165
  xp/services/protocol/conbus_protocol.py,sha256=JO7yLkD_ohPT0ETjnAIx4CGpZyobf4LdbuozM_43btE,10276
166
166
  xp/services/protocol/protocol_factory.py,sha256=PmjN9AtW9sxNo3voqUiNgQA-pTvX1RW4XXFlHKfFr5Q,2470
167
167
  xp/services/protocol/telegram_protocol.py,sha256=Ki5DrXsKxiaqLcdP9WWUuhUI7cPu2DfwyZkh-Gv9Lb8,9496
@@ -171,7 +171,7 @@ xp/services/server/base_server_service.py,sha256=B-ntxp3swbwuri-9_2EuvBDi-4Uo9AH
171
171
  xp/services/server/client_buffer_manager.py,sha256=1d_MqfzuUqBwaQUiC1n5K76WwSxrdngYAmNH7he6u3o,2235
172
172
  xp/services/server/cp20_server_service.py,sha256=SXdI6Jt400T9sLdw86ovEqKRGeV3nYVaHEA9Gcj6W2A,2041
173
173
  xp/services/server/device_service_factory.py,sha256=Y4TvSFALeq0zYzHfCwcbikSpmIyYbLcvm9756n5Jm7Q,3744
174
- xp/services/server/server_service.py,sha256=YAkn3rxNYnxs3E1Dpwtt3ZzymFXZHKEz8Nitim-Xewg,16300
174
+ xp/services/server/server_service.py,sha256=2jMrL-Azn7KaqpgSI1ztBh0UD3JWom5ns33IkqMCDac,16302
175
175
  xp/services/server/xp130_server_service.py,sha256=YnvetDp72-QzkyDGB4qfZZIwFs03HuibUOz2zb9XR0c,2191
176
176
  xp/services/server/xp20_server_service.py,sha256=1wJ7A-bRkN9O5Spu3q3LNDW31mNtNF2eNMQ5E6O2ltA,2928
177
177
  xp/services/server/xp230_server_service.py,sha256=k9ftCY5tjLFP31mKVCspq283RVaPkGx-Yq61Urk8JLs,1815
@@ -207,4 +207,4 @@ xp/utils/logging.py,sha256=rZDXwlBrYK8A6MPq5StsMNpgsRowzJXM6fvROPwJdGM,3750
207
207
  xp/utils/serialization.py,sha256=RWHHk86feaB4ZP7rjE4qOWK0900yg2joUBDkP76gfOY,4618
208
208
  xp/utils/state_machine.py,sha256=Oe2sLwCh9z_vr1tF6X0ZRGTeuckRQAGzmef7xc9CNdc,2413
209
209
  xp/utils/time_utils.py,sha256=dEyViDlAG9GWU-J3D_YVa-sGma6yiyyMTgN4h2x3PY4,3781
210
- conson_xp-1.41.0.dist-info/RECORD,,
210
+ conson_xp-1.43.0.dist-info/RECORD,,
xp/__init__.py CHANGED
@@ -3,7 +3,7 @@
3
3
  conson-xp package.
4
4
  """
5
5
 
6
- __version__ = "1.41.0"
6
+ __version__ = "1.43.0"
7
7
  __manufacturer__ = "salchichon"
8
8
  __model__ = "xp.cli"
9
9
  __serial__ = "2025.09.23.000"
@@ -61,7 +61,7 @@ def conbus_download_actiontable(ctx: Context, serial_number: str) -> None:
61
61
  """
62
62
  click.echo(progress, nl=False)
63
63
 
64
- def on_finish(
64
+ def on_actiontable_received(
65
65
  _actiontable: ActionTable,
66
66
  actiontable_dict: Dict[str, Any],
67
67
  actiontable_short: list[str],
@@ -79,6 +79,9 @@ def conbus_download_actiontable(ctx: Context, serial_number: str) -> None:
79
79
  "actiontable": actiontable_dict,
80
80
  }
81
81
  click.echo(json.dumps(output, indent=2, default=str))
82
+
83
+ def on_finish() -> None:
84
+ """Handle successful completion of action table download."""
82
85
  service.stop_reactor()
83
86
 
84
87
  def on_error(error: str) -> None:
@@ -93,6 +96,7 @@ def conbus_download_actiontable(ctx: Context, serial_number: str) -> None:
93
96
  with service:
94
97
  service.on_progress.connect(on_progress)
95
98
  service.on_finish.connect(on_finish)
99
+ service.on_actiontable_received.connect(on_actiontable_received)
96
100
  service.on_error.connect(on_error)
97
101
  service.start(serial_number=serial_number)
98
102
  service.start_reactor()
@@ -4,10 +4,13 @@ import logging
4
4
  from dataclasses import asdict
5
5
  from typing import Any, Dict, Optional
6
6
 
7
- from psygnal import Signal
7
+ from psygnal import SignalInstance
8
+ from statemachine import State, StateMachine
8
9
 
9
10
  from xp.models.actiontable.actiontable import ActionTable
10
11
  from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
12
+ from xp.models.telegram.datapoint_type import DataPointType
13
+ from xp.models.telegram.reply_telegram import ReplyTelegram
11
14
  from xp.models.telegram.system_function import SystemFunction
12
15
  from xp.models.telegram.telegram_type import TelegramType
13
16
  from xp.services.actiontable.actiontable_serializer import ActionTableSerializer
@@ -15,9 +18,11 @@ from xp.services.protocol.conbus_event_protocol import ConbusEventProtocol
15
18
  from xp.services.telegram.telegram_service import TelegramService
16
19
 
17
20
 
18
- class ActionTableDownloadService:
21
+ class ActionTableDownloadService(StateMachine):
19
22
  """TCP client service for downloading action tables from Conbus modules.
20
23
 
24
+ Inherits from StateMachine - the service IS the state machine.
25
+
21
26
  Manages TCP socket connections, handles telegram generation and transmission,
22
27
  and processes server responses for action table downloads.
23
28
 
@@ -25,13 +30,74 @@ class ActionTableDownloadService:
25
30
  on_progress: Signal emitted with telegram frame when progress is made.
26
31
  on_error: Signal emitted with error message string when an error occurs.
27
32
  on_finish: Signal emitted with (ActionTable, Dict[str, Any], list[str]) when complete.
33
+ idle: Initial state, waiting for connection.
34
+ receiving: Listening for telegrams, filtering relevant responses.
35
+ resetting: Timeout occurred, preparing error status query.
36
+ waiting_ok: Sent error status query, awaiting ACK/NAK.
37
+ requesting: Ready to send download request.
38
+ waiting_data: Awaiting actiontable chunk or EOF.
39
+ receiving_chunk: Processing received actiontable data.
40
+ processing_eof: Received EOF, deserializing actiontable.
41
+ completed: Download finished successfully.
42
+ do_connect: Transition from idle to receiving.
43
+ do_timeout: Transition from receiving to resetting.
44
+ send_error_status: Transition from resetting to waiting_ok.
45
+ error_status_received: Transition from waiting_ok to receiving (retry).
46
+ no_error_status_received: Transition from waiting_ok to requesting or completed.
47
+ send_download: Transition from requesting to waiting_data.
48
+ receive_chunk: Transition from waiting_data to receiving_chunk.
49
+ send_ack: Transition from receiving_chunk to waiting_data.
50
+ receive_eof: Transition from waiting_data to processing_eof.
51
+ do_finish: Transition from processing_eof to receiving.
52
+ receiving2: Second receiving state after EOF processing.
53
+ resetting2: Second resetting state for finalization phase.
54
+ waiting_ok2: Second waiting_ok state for finalization phase.
55
+ filter_telegram: Self-transition for filtering telegrams in receiving state.
56
+ filter_telegram2: Self-transition for filtering telegrams in receiving2 state.
57
+ do_timeout2: Timeout transition for finalization phase.
58
+ send_error_status2: Error status query transition for finalization phase.
59
+ error_status_received2: Error received transition for finalization phase.
60
+ no_error_status_received2: No error received transition to completed state.
28
61
  """
29
62
 
30
- on_progress: Signal = Signal(str)
31
- on_error: Signal = Signal(str)
32
- on_finish: Signal = Signal(
33
- ActionTable, Dict[str, Any], list[str]
34
- ) # (ActionTable, Dict[str, Any], list[str])
63
+ # States (9 states as per spec)
64
+ idle = State(initial=True)
65
+ receiving = State()
66
+ resetting = State()
67
+ waiting_ok = State()
68
+
69
+ requesting = State()
70
+ waiting_data = State()
71
+ receiving_chunk = State()
72
+ processing_eof = State()
73
+
74
+ receiving2 = State()
75
+ resetting2 = State()
76
+ waiting_ok2 = State()
77
+
78
+ completed = State(final=True)
79
+
80
+ # Phase 1: Connection & Initialization
81
+ do_connect = idle.to(receiving)
82
+ filter_telegram = receiving.to(receiving) # Self-transition for filtering
83
+ do_timeout = receiving.to(resetting) | waiting_ok.to(receiving)
84
+ send_error_status = resetting.to(waiting_ok)
85
+ error_status_received = waiting_ok.to(receiving)
86
+ no_error_status_received = waiting_ok.to(requesting)
87
+
88
+ # Phase 2: Download
89
+ send_download = requesting.to(waiting_data)
90
+ receive_chunk = waiting_data.to(receiving_chunk)
91
+ send_ack = receiving_chunk.to(waiting_data)
92
+ receive_eof = waiting_data.to(processing_eof)
93
+
94
+ # Phase 3: Finalization
95
+ do_finish = processing_eof.to(receiving2)
96
+ filter_telegram2 = receiving2.to(receiving2) # Self-transition for filtering
97
+ do_timeout2 = receiving2.to(resetting2) | waiting_ok2.to(receiving2)
98
+ send_error_status2 = resetting2.to(waiting_ok2)
99
+ error_status_received2 = waiting_ok2.to(receiving2)
100
+ no_error_status_received2 = waiting_ok2.to(completed)
35
101
 
36
102
  def __init__(
37
103
  self,
@@ -51,29 +117,127 @@ class ActionTableDownloadService:
51
117
  self.telegram_service = telegram_service
52
118
  self.serial_number: str = ""
53
119
  self.actiontable_data: list[str] = []
54
- # Set up logging
55
120
  self.logger = logging.getLogger(__name__)
56
121
 
122
+ # Signals (instance attributes to avoid conflict with statemachine)
123
+ self.on_progress: SignalInstance = SignalInstance((str,))
124
+ self.on_error: SignalInstance = SignalInstance((str,))
125
+ self.on_finish: SignalInstance = SignalInstance()
126
+ self.on_actiontable_received: SignalInstance = SignalInstance(
127
+ (ActionTable, Dict[str, Any], list[str])
128
+ )
129
+
57
130
  # Connect protocol signals
58
- self.conbus_protocol.on_connection_made.connect(self.connection_made)
59
- self.conbus_protocol.on_telegram_sent.connect(self.telegram_sent)
60
- self.conbus_protocol.on_telegram_received.connect(self.telegram_received)
61
- self.conbus_protocol.on_timeout.connect(self.timeout)
62
- self.conbus_protocol.on_failed.connect(self.failed)
131
+ self.conbus_protocol.on_connection_made.connect(self._on_connection_made)
132
+ self.conbus_protocol.on_telegram_sent.connect(self._on_telegram_sent)
133
+ self.conbus_protocol.on_telegram_received.connect(self._on_telegram_received)
134
+ self.conbus_protocol.on_timeout.connect(self._on_timeout)
135
+ self.conbus_protocol.on_failed.connect(self._on_failed)
63
136
 
64
- def connection_made(self) -> None:
65
- """Handle connection established event."""
66
- self.logger.debug(
67
- "Connection established, sending download actiontable telegram"
137
+ # Initialize state machine
138
+ super().__init__(allow_event_without_transition=True)
139
+
140
+ # State machine lifecycle hooks
141
+
142
+ def on_enter_receiving(self) -> None:
143
+ """Enter receiving state - listening for telegrams."""
144
+ self.logger.debug("Entering RECEIVING state - waiting for telegrams")
145
+ self.conbus_protocol.wait()
146
+
147
+ def on_enter_receiving2(self) -> None:
148
+ """Enter receiving state - listening for telegrams."""
149
+ self.logger.debug("Entering RECEIVING2 state - waiting for telegrams")
150
+ self.conbus_protocol.wait()
151
+
152
+ def on_enter_resetting(self) -> None:
153
+ """Enter resetting state - query error status."""
154
+ self.logger.debug("Entering RESETTING state - querying error status")
155
+
156
+ # query_datapoint_module_error_code
157
+ self.conbus_protocol.send_telegram(
158
+ telegram_type=TelegramType.SYSTEM,
159
+ serial_number=self.serial_number,
160
+ system_function=SystemFunction.READ_DATAPOINT,
161
+ data_value=DataPointType.MODULE_ERROR_CODE.value,
68
162
  )
163
+ self.send_error_status()
164
+
165
+ def on_enter_resetting2(self) -> None:
166
+ """Enter resetting state - query error status."""
167
+ self.logger.debug("Entering RESETTING2 state - querying error status")
168
+
169
+ # query_datapoint_module_error_code
170
+ self.conbus_protocol.send_telegram(
171
+ telegram_type=TelegramType.SYSTEM,
172
+ serial_number=self.serial_number,
173
+ system_function=SystemFunction.READ_DATAPOINT,
174
+ data_value=DataPointType.MODULE_ERROR_CODE.value,
175
+ )
176
+ self.send_error_status2()
177
+
178
+ def on_enter_waiting_ok(self) -> None:
179
+ """Enter waiting_ok state - awaiting ERROR/NO_ERROR."""
180
+ self.logger.debug("Entering WAITING_OK state - awaiting ERROR/NO_ERROR")
181
+ self.conbus_protocol.wait()
182
+
183
+ def on_enter_waiting_ok2(self) -> None:
184
+ """Enter waiting_ok state - awaiting ERROR/NO_ERROR."""
185
+ self.logger.debug("Entering WAITING_OK state - awaiting ERROR/NO_ERROR")
186
+ self.conbus_protocol.wait()
187
+
188
+ def on_enter_requesting(self) -> None:
189
+ """Enter requesting state - send download request."""
190
+ self.logger.debug("Entering REQUESTING state - sending download request")
69
191
  self.conbus_protocol.send_telegram(
70
192
  telegram_type=TelegramType.SYSTEM,
71
193
  serial_number=self.serial_number,
72
194
  system_function=SystemFunction.DOWNLOAD_ACTIONTABLE,
73
195
  data_value="00",
74
196
  )
197
+ self.send_download()
198
+
199
+ def on_enter_waiting_data(self) -> None:
200
+ """Enter waiting_data state - wait for actiontable chunks."""
201
+ self.logger.debug("Entering WAITING_DATA state - awaiting chunks")
202
+ self.conbus_protocol.wait()
75
203
 
76
- def telegram_sent(self, telegram_sent: str) -> None:
204
+ def on_enter_receiving_chunk(self) -> None:
205
+ """Enter receiving_chunk state - send ACK."""
206
+ self.logger.debug("Entering RECEIVING_CHUNK state - sending ACK")
207
+ self.conbus_protocol.send_telegram(
208
+ telegram_type=TelegramType.SYSTEM,
209
+ serial_number=self.serial_number,
210
+ system_function=SystemFunction.ACK,
211
+ data_value="00",
212
+ )
213
+ self.send_ack()
214
+
215
+ def on_enter_processing_eof(self) -> None:
216
+ """Enter processing_eof state - deserialize and emit result."""
217
+ self.logger.debug("Entering PROCESSING_EOF state - deserializing")
218
+ all_data = "".join(self.actiontable_data)
219
+ actiontable = self.serializer.from_encoded_string(all_data)
220
+ actiontable_dict = asdict(actiontable)
221
+ actiontable_short = self.serializer.format_decoded_output(actiontable)
222
+ self.on_actiontable_received.emit(
223
+ actiontable, actiontable_dict, actiontable_short
224
+ )
225
+ self.do_finish()
226
+
227
+ def on_enter_completed(self) -> None:
228
+ """Enter completed state - download finished."""
229
+ self.logger.debug("Entering COMPLETED state - download finished")
230
+ self.on_finish.emit()
231
+
232
+ # Protocol event handlers
233
+
234
+ def _on_connection_made(self) -> None:
235
+ """Handle connection established event."""
236
+ self.logger.debug("Connection made")
237
+ if self.idle.is_active:
238
+ self.do_connect()
239
+
240
+ def _on_telegram_sent(self, telegram_sent: str) -> None:
77
241
  """Handle telegram sent event.
78
242
 
79
243
  Args:
@@ -81,59 +245,148 @@ class ActionTableDownloadService:
81
245
  """
82
246
  self.logger.debug(f"Telegram sent: {telegram_sent}")
83
247
 
84
- def telegram_received(self, telegram_received: TelegramReceivedEvent) -> None:
248
+ def _on_read_datapoint_received(self, reply_telegram: ReplyTelegram) -> None:
249
+ """Handle READ_DATAPOINT response for error status check.
250
+
251
+ Args:
252
+ reply_telegram: The parsed reply telegram.
253
+ """
254
+ self.logger.debug(f"Received READ_DATAPOINT in {self.current_state}")
255
+
256
+ if reply_telegram.datapoint_type != DataPointType.MODULE_ERROR_CODE:
257
+ self.logger.debug(
258
+ f"Filtered: not a MODULE_ERROR_CODE (got {reply_telegram.datapoint_type})"
259
+ )
260
+ return
261
+
262
+ if reply_telegram.data_value == "00":
263
+ if self.waiting_ok.is_active:
264
+ self.no_error_status_received()
265
+
266
+ if reply_telegram.data_value != "00":
267
+ if self.waiting_ok.is_active:
268
+ self.error_status_received()
269
+
270
+ if reply_telegram.data_value == "00":
271
+ if self.waiting_ok2.is_active:
272
+ self.no_error_status_received2()
273
+
274
+ if reply_telegram.data_value != "00":
275
+ if self.waiting_ok2.is_active:
276
+ self.error_status_received2()
277
+
278
+ def _on_ack_received(self, _reply_telegram: ReplyTelegram) -> None:
279
+ """Handle ACK telegram received.
280
+
281
+ Args:
282
+ _reply_telegram: The parsed reply telegram (unused).
283
+ """
284
+ self.logger.debug(f"Received ACK in {self.current_state}")
285
+ if self.waiting_ok.is_active:
286
+ self.ack_received()
287
+
288
+ if self.waiting_ok2.is_active:
289
+ self.ack_received2()
290
+
291
+ def _on_nack_received(self, _reply_telegram: ReplyTelegram) -> None:
292
+ """Handle NAK telegram received.
293
+
294
+ Args:
295
+ _reply_telegram: The parsed reply telegram (unused).
296
+ """
297
+ self.logger.debug(f"Received NAK in {self.current_state}")
298
+ if self.waiting_ok.is_active:
299
+ self.nak_received()
300
+
301
+ def _on_actiontable_chunk_received(self, reply_telegram: ReplyTelegram) -> None:
302
+ """Handle actiontable chunk telegram received.
303
+
304
+ Args:
305
+ reply_telegram: The parsed reply telegram containing chunk data.
306
+ """
307
+ self.logger.debug(f"Received actiontable chunk in {self.current_state}")
308
+ if self.waiting_data.is_active:
309
+ data_part = reply_telegram.data_value[2:]
310
+ self.actiontable_data.append(data_part)
311
+ self.on_progress.emit(".")
312
+ self.receive_chunk()
313
+
314
+ def _on_eof_received(self, _reply_telegram: ReplyTelegram) -> None:
315
+ """Handle EOF telegram received.
316
+
317
+ Args:
318
+ _reply_telegram: The parsed reply telegram (unused).
319
+ """
320
+ self.logger.debug(f"Received EOF in {self.current_state}")
321
+ if self.waiting_data.is_active:
322
+ self.receive_eof()
323
+
324
+ def _on_telegram_received(self, telegram_received: TelegramReceivedEvent) -> None:
85
325
  """Handle telegram received event.
86
326
 
87
327
  Args:
88
328
  telegram_received: The telegram received event.
89
329
  """
90
- self.logger.debug(f"Telegram received: {telegram_received}")
91
- if (
92
- not telegram_received.checksum_valid
93
- or telegram_received.telegram_type != TelegramType.REPLY.value
94
- or telegram_received.serial_number != self.serial_number
95
- ):
96
- self.logger.debug("Not a reply response")
330
+ self.logger.debug(f"Received{telegram_received} in {self.current_state}")
331
+ if self.receiving.is_active:
332
+ self.filter_telegram()
333
+ return
334
+
335
+ # Filter invalid telegrams
336
+ if not telegram_received.checksum_valid:
337
+ self.logger.debug("Filtered: invalid checksum")
338
+ return
339
+ if telegram_received.telegram_type != TelegramType.REPLY.value:
340
+ self.logger.debug(
341
+ f"Filtered: not a reply (got {telegram_received.telegram_type})"
342
+ )
343
+ return
344
+ if telegram_received.serial_number != self.serial_number:
345
+ self.logger.debug(
346
+ f"Filtered: wrong serial {telegram_received.serial_number} != {self.serial_number}"
347
+ )
97
348
  return
98
349
 
99
350
  reply_telegram = self.telegram_service.parse_reply_telegram(
100
351
  telegram_received.frame
101
352
  )
102
- if reply_telegram.system_function not in (
103
- SystemFunction.ACTIONTABLE,
104
- SystemFunction.EOF,
105
- ):
106
- self.logger.debug("Not a actiontable response")
353
+
354
+ if reply_telegram.system_function == SystemFunction.READ_DATAPOINT:
355
+ self._on_read_datapoint_received(reply_telegram)
107
356
  return
108
357
 
109
- if reply_telegram.system_function == SystemFunction.ACTIONTABLE:
110
- self.logger.debug("Saving actiontable response")
111
- data_part = reply_telegram.data_value[2:]
112
- self.actiontable_data.append(data_part)
113
- self.on_progress.emit(".")
358
+ if reply_telegram.system_function == SystemFunction.ACK:
359
+ self._on_ack_received(reply_telegram)
360
+ return
114
361
 
115
- self.conbus_protocol.send_telegram(
116
- telegram_type=TelegramType.SYSTEM,
117
- serial_number=self.serial_number,
118
- system_function=SystemFunction.ACK,
119
- data_value="00",
120
- )
362
+ if reply_telegram.system_function == SystemFunction.NAK:
363
+ self._on_nack_received(reply_telegram)
364
+ return
365
+
366
+ if reply_telegram.system_function == SystemFunction.ACTIONTABLE:
367
+ self._on_actiontable_chunk_received(reply_telegram)
121
368
  return
122
369
 
123
370
  if reply_telegram.system_function == SystemFunction.EOF:
124
- all_data = "".join(self.actiontable_data)
125
- # Deserialize from received data
126
- actiontable = self.serializer.from_encoded_string(all_data)
127
- actiontable_dict = asdict(actiontable)
128
- actiontable_short = self.serializer.format_decoded_output(actiontable)
129
- self.on_finish.emit(actiontable, actiontable_dict, actiontable_short)
130
-
131
- def timeout(self) -> None:
371
+ self._on_eof_received(reply_telegram)
372
+ return
373
+
374
+ def _on_timeout(self) -> None:
132
375
  """Handle timeout event."""
133
376
  self.logger.debug("Timeout occurred")
134
- self.failed("Timeout")
135
-
136
- def failed(self, message: str) -> None:
377
+ if self.receiving.is_active:
378
+ self.do_timeout() # receiving -> resetting
379
+ elif self.waiting_ok.is_active:
380
+ self.do_timeout() # waiting_ok -> receiving
381
+ elif self.receiving2.is_active:
382
+ self.do_timeout2() # receiving2 -> resetting2
383
+ elif self.waiting_ok2.is_active:
384
+ self.do_timeout2() # waiting_ok2 -> receiving2
385
+ else:
386
+ self.logger.debug("Timeout in non-recoverable state")
387
+ self.on_error.emit("Timeout")
388
+
389
+ def _on_failed(self, message: str) -> None:
137
390
  """Handle failed connection event.
138
391
 
139
392
  Args:
@@ -142,10 +395,12 @@ class ActionTableDownloadService:
142
395
  self.logger.debug(f"Failed: {message}")
143
396
  self.on_error.emit(message)
144
397
 
398
+ # Public API
399
+
145
400
  def start(
146
401
  self,
147
402
  serial_number: str,
148
- timeout_seconds: Optional[float] = None,
403
+ timeout_seconds: Optional[float] = 2.0,
149
404
  ) -> None:
150
405
  """Run reactor in dedicated thread with its own event loop.
151
406
 
@@ -153,11 +408,10 @@ class ActionTableDownloadService:
153
408
  serial_number: Module serial number.
154
409
  timeout_seconds: Optional timeout in seconds.
155
410
  """
156
- self.logger.info("Starting actiontable")
411
+ self.logger.info("Starting actiontable download")
157
412
  self.serial_number = serial_number
158
413
  if timeout_seconds:
159
414
  self.conbus_protocol.timeout_seconds = timeout_seconds
160
- # Caller invokes start_reactor()
161
415
 
162
416
  def set_timeout(self, timeout_seconds: float) -> None:
163
417
  """Set operation timeout.
@@ -183,18 +437,28 @@ class ActionTableDownloadService:
183
437
  """
184
438
  # Reset state for singleton reuse
185
439
  self.actiontable_data = []
440
+ self._download_complete = False
441
+ # Reset state machine to idle
442
+ self._reset_state()
186
443
  return self
187
444
 
445
+ def _reset_state(self) -> None:
446
+ """Reset state machine to initial state."""
447
+ # python-statemachine uses model.state to track current state
448
+ # Set it directly to the initial state id
449
+ self.model.state = self.idle.id
450
+ self._download_complete = False
451
+
188
452
  def __exit__(
189
453
  self, _exc_type: Optional[type], _exc_val: Optional[Exception], _exc_tb: Any
190
454
  ) -> None:
191
455
  """Exit context manager and disconnect signals."""
192
456
  # Disconnect protocol signals
193
- self.conbus_protocol.on_connection_made.disconnect(self.connection_made)
194
- self.conbus_protocol.on_telegram_sent.disconnect(self.telegram_sent)
195
- self.conbus_protocol.on_telegram_received.disconnect(self.telegram_received)
196
- self.conbus_protocol.on_timeout.disconnect(self.timeout)
197
- self.conbus_protocol.on_failed.disconnect(self.failed)
457
+ self.conbus_protocol.on_connection_made.disconnect(self._on_connection_made)
458
+ self.conbus_protocol.on_telegram_sent.disconnect(self._on_telegram_sent)
459
+ self.conbus_protocol.on_telegram_received.disconnect(self._on_telegram_received)
460
+ self.conbus_protocol.on_timeout.disconnect(self._on_timeout)
461
+ self.conbus_protocol.on_failed.disconnect(self._on_failed)
198
462
  # Disconnect service signals
199
463
  self.on_progress.disconnect()
200
464
  self.on_error.disconnect()
@@ -103,6 +103,16 @@ class ConbusEventProtocol(protocol.Protocol, protocol.ClientFactory):
103
103
  # Start inactivity timeout
104
104
  self._reset_timeout()
105
105
 
106
+ def wait(self, wait_timeout: Optional[float] = None) -> None:
107
+ """Wait for incoming telegrams with optional timeout override.
108
+
109
+ Args:
110
+ wait_timeout: Optional timeout in seconds to override default.
111
+ """
112
+ if wait_timeout:
113
+ self.timeout_seconds = wait_timeout
114
+ self._reset_timeout()
115
+
106
116
  def dataReceived(self, data: bytes) -> None:
107
117
  """Handle received data from TCP connection.
108
118
 
@@ -337,7 +337,7 @@ class ServerService:
337
337
  self.logger.warning(f"Failed to parse telegram: {telegram}")
338
338
  return responses
339
339
 
340
- self.client_buffers.broadcast(parsed_telegram.raw_telegram)
340
+ # self.client_buffers.broadcast(parsed_telegram.raw_telegram)
341
341
 
342
342
  # Handle discover requests
343
343
  if self.discover_service.is_discover_request(parsed_telegram):