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.
- {conson_xp-1.41.0.dist-info → conson_xp-1.43.0.dist-info}/METADATA +2 -1
- {conson_xp-1.41.0.dist-info → conson_xp-1.43.0.dist-info}/RECORD +10 -10
- xp/__init__.py +1 -1
- xp/cli/commands/conbus/conbus_actiontable_commands.py +5 -1
- xp/services/conbus/actiontable/actiontable_download_service.py +325 -61
- xp/services/protocol/conbus_event_protocol.py +10 -0
- xp/services/server/server_service.py +1 -1
- {conson_xp-1.41.0.dist-info → conson_xp-1.43.0.dist-info}/WHEEL +0 -0
- {conson_xp-1.41.0.dist-info → conson_xp-1.43.0.dist-info}/entry_points.txt +0 -0
- {conson_xp-1.41.0.dist-info → conson_xp-1.43.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: conson-xp
|
|
3
|
-
Version: 1.
|
|
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.
|
|
2
|
-
conson_xp-1.
|
|
3
|
-
conson_xp-1.
|
|
4
|
-
conson_xp-1.
|
|
5
|
-
xp/__init__.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
210
|
+
conson_xp-1.43.0.dist-info/RECORD,,
|
xp/__init__.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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.
|
|
59
|
-
self.conbus_protocol.on_telegram_sent.connect(self.
|
|
60
|
-
self.conbus_protocol.on_telegram_received.connect(self.
|
|
61
|
-
self.conbus_protocol.on_timeout.connect(self.
|
|
62
|
-
self.conbus_protocol.on_failed.connect(self.
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
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
|
|
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"
|
|
91
|
-
if
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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.
|
|
110
|
-
self.
|
|
111
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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.
|
|
135
|
-
|
|
136
|
-
|
|
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] =
|
|
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.
|
|
194
|
-
self.conbus_protocol.on_telegram_sent.disconnect(self.
|
|
195
|
-
self.conbus_protocol.on_telegram_received.disconnect(self.
|
|
196
|
-
self.conbus_protocol.on_timeout.disconnect(self.
|
|
197
|
-
self.conbus_protocol.on_failed.disconnect(self.
|
|
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):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|