conson-xp 1.22.0__py3-none-any.whl → 1.24.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.22.0.dist-info → conson_xp-1.24.0.dist-info}/METADATA +1 -1
- {conson_xp-1.22.0.dist-info → conson_xp-1.24.0.dist-info}/RECORD +18 -16
- xp/__init__.py +1 -1
- xp/cli/commands/conbus/conbus_discover_commands.py +7 -2
- xp/cli/commands/conbus/conbus_receive_commands.py +5 -2
- xp/models/term/connection_state.py +58 -0
- xp/models/term/status_message.py +16 -0
- xp/services/conbus/conbus_discover_service.py +61 -53
- xp/services/conbus/conbus_receive_service.py +30 -25
- xp/services/protocol/conbus_event_protocol.py +19 -6
- xp/term/protocol.py +10 -0
- xp/term/protocol.tcss +1 -1
- xp/term/protocol.yml +14 -14
- xp/term/widgets/protocol_log.py +39 -123
- xp/term/widgets/status_footer.py +10 -0
- {conson_xp-1.22.0.dist-info → conson_xp-1.24.0.dist-info}/WHEEL +0 -0
- {conson_xp-1.22.0.dist-info → conson_xp-1.24.0.dist-info}/entry_points.txt +0 -0
- {conson_xp-1.22.0.dist-info → conson_xp-1.24.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
conson_xp-1.
|
|
2
|
-
conson_xp-1.
|
|
3
|
-
conson_xp-1.
|
|
4
|
-
conson_xp-1.
|
|
5
|
-
xp/__init__.py,sha256=
|
|
1
|
+
conson_xp-1.24.0.dist-info/METADATA,sha256=OIqtn9P69KMHIOn2EV8vgrHgUSQRQnFd1VGMFOU5sdE,10298
|
|
2
|
+
conson_xp-1.24.0.dist-info/WHEEL,sha256=tsUv_t7BDeJeRHaSrczbGeuK-TtDpGsWi_JfpzD255I,90
|
|
3
|
+
conson_xp-1.24.0.dist-info/entry_points.txt,sha256=1OcdIcDM1hz3ljCXgybaPUh1IOFEwkaTgLIW9u9zGeg,50
|
|
4
|
+
conson_xp-1.24.0.dist-info/licenses/LICENSE,sha256=rxj6woMM-r3YCyGq_UHFtbh7kHTAJgrccH6O-33IDE4,1419
|
|
5
|
+
xp/__init__.py,sha256=WnwD09XucegEUWyg8Au3w-_Cz33wTXvYu4cW_mzxpmE,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=noh8fdZAWq-ihJEboP8WugbIgq4LJ3jUWMRA7720xWE,4909
|
|
@@ -14,7 +14,7 @@ xp/cli/commands/conbus/conbus_blink_commands.py,sha256=UK-Ey4K0FvaPQ96U0gyMid236
|
|
|
14
14
|
xp/cli/commands/conbus/conbus_config_commands.py,sha256=BugIbgNX6_s4MySGvI6tWZkwguciajHUX2Xz8XBux7k,716
|
|
15
15
|
xp/cli/commands/conbus/conbus_custom_commands.py,sha256=lICT93ijMdhVRm8KjNMLo7kQ2BLlnOZvMPbR3SxSmZ4,1692
|
|
16
16
|
xp/cli/commands/conbus/conbus_datapoint_commands.py,sha256=r36OuTjREtbGKL-bskAGa0-WLw7x06td6woZn3GYJNA,3630
|
|
17
|
-
xp/cli/commands/conbus/conbus_discover_commands.py,sha256
|
|
17
|
+
xp/cli/commands/conbus/conbus_discover_commands.py,sha256=MnTCzvERO5xerfs0fuuIBoo1O9h_0IfoJ6snLGVl0lA,1899
|
|
18
18
|
xp/cli/commands/conbus/conbus_event_commands.py,sha256=7URf-2u8Kzcy0chLYShbZfCbKawf--i-8U88AjhxleQ,3177
|
|
19
19
|
xp/cli/commands/conbus/conbus_lightlevel_commands.py,sha256=FpCwogdxa7yFUjlrxM7e8Q2Ut32tKAHabngQQChvtJI,6763
|
|
20
20
|
xp/cli/commands/conbus/conbus_linknumber_commands.py,sha256=KitaGDM5HpwVUz8rLpO8VZUypUTcAg3Bzl0DVm6gnSk,3391
|
|
@@ -22,7 +22,7 @@ xp/cli/commands/conbus/conbus_modulenumber_commands.py,sha256=L7-6y3rDllOjQ9g6Bk
|
|
|
22
22
|
xp/cli/commands/conbus/conbus_msactiontable_commands.py,sha256=fb9MQ4O04H0Dinpt7vSF5GtfntTZHelQ5TuUmSBbCTg,2899
|
|
23
23
|
xp/cli/commands/conbus/conbus_output_commands.py,sha256=zdRVbHzVhMbZpG2x5WXtujc3wKTsoQUV4IgkVIbJbCc,5019
|
|
24
24
|
xp/cli/commands/conbus/conbus_raw_commands.py,sha256=8BKUarwvHgz-sxML7n99YVsb8B1HJNExjQpRsuY_tQw,1829
|
|
25
|
-
xp/cli/commands/conbus/conbus_receive_commands.py,sha256=
|
|
25
|
+
xp/cli/commands/conbus/conbus_receive_commands.py,sha256=_PsC-3xidmJBuOWUS60iDzhSHYYn5ZFmORXap-ljVGM,1902
|
|
26
26
|
xp/cli/commands/conbus/conbus_scan_commands.py,sha256=JfXucOwOadvLEKT_fW9fwvqWKHaEODOojLjnO8JV_00,1730
|
|
27
27
|
xp/cli/commands/file_commands.py,sha256=GV102X7FRZDUNKLlzvSsIGcoXAaofOzmjCp3HUpE9lw,5532
|
|
28
28
|
xp/cli/commands/homekit/__init__.py,sha256=qqwY8ulxTx1S74Mzpb6EKjBLT6fWTNdf9PQ3HKuERKY,50
|
|
@@ -105,7 +105,9 @@ xp/models/telegram/telegram.py,sha256=IJUxHX6ftLcET9C1pjvLhUO5Db5JO6W7rUItzdEW30
|
|
|
105
105
|
xp/models/telegram/telegram_type.py,sha256=GhqKP63oNMyh2tIvCPcsC5RFp4s4JjhmEqCLCC-8XMk,423
|
|
106
106
|
xp/models/telegram/timeparam_type.py,sha256=Ar8xvSfPmOAgR2g2Je0FgvP01SL7bPvZn5_HrVDpmJM,1137
|
|
107
107
|
xp/models/term/__init__.py,sha256=c1AMtVitYk80o9K_zWjYNzZYpFDASqM8S1Djm1PD4Qo,192
|
|
108
|
+
xp/models/term/connection_state.py,sha256=floDRMeMcfgMrYIVsyoVHBXHtxd3hqm-xOdr3oXtaHY,1793
|
|
108
109
|
xp/models/term/protocol_keys_config.py,sha256=CTujcfI2_NOeltjvHy_cnsHzxLSVsGFXieMZlD-zj0Q,1204
|
|
110
|
+
xp/models/term/status_message.py,sha256=DOmzL0dbig5mP1UEoXdgzGT4UG2RyAXa_yRVo5c4x8w,394
|
|
109
111
|
xp/models/write_config_type.py,sha256=T2RaO52RpzoJ4782uMHE-fX7Ymx3CaIQAEwByydXq1M,881
|
|
110
112
|
xp/services/__init__.py,sha256=W9YZyrkh7vm--ZHhAXNQiOYQs5yhhmUHXP5I0Lf1XBg,782
|
|
111
113
|
xp/services/actiontable/__init__.py,sha256=z6js4EuJ6xKHaseTEhuEvKo1tr9K1XyQiruReJtBiPY,26
|
|
@@ -126,12 +128,12 @@ xp/services/conbus/conbus_blink_service.py,sha256=x9uM-sLnIEV8wSNsvJgo08E042g-Hh
|
|
|
126
128
|
xp/services/conbus/conbus_custom_service.py,sha256=4aneYdPObiZOGxPFYg5Wr70cl_xFxlQIdJBPQSa0enI,5826
|
|
127
129
|
xp/services/conbus/conbus_datapoint_queryall_service.py,sha256=p9R02cVimhdJILHQ6BoeZj8Hog4oRpqBnMo3t4R8ecY,6816
|
|
128
130
|
xp/services/conbus/conbus_datapoint_service.py,sha256=SYhHj9RmTmaJ750tyZ1IW2kl7tgDQ1xm_EM1zUjk1aQ,6421
|
|
129
|
-
xp/services/conbus/conbus_discover_service.py,sha256=
|
|
131
|
+
xp/services/conbus/conbus_discover_service.py,sha256=ZwjYBlgP6FgpHBJk7pcKr4JHfH7WUHDxe4he4F_HblQ,12740
|
|
130
132
|
xp/services/conbus/conbus_event_list_service.py,sha256=0xyXXNU44epN5bFkU6oiZMyhxfUguul3evqClvPJDcA,3618
|
|
131
133
|
xp/services/conbus/conbus_event_raw_service.py,sha256=FZFu-LNLInrTKTpiGLyootozvyIF5Si5FMrxNk2ALD0,7000
|
|
132
134
|
xp/services/conbus/conbus_output_service.py,sha256=mHFOAPx2zo0TStZ3pokp6v94AQjIamcwZDeg5YH_-eo,7240
|
|
133
135
|
xp/services/conbus/conbus_raw_service.py,sha256=4yZLLTIAOxpgByUTWZXw1ihGa6Xtl98ckj9T7VfprDI,4335
|
|
134
|
-
xp/services/conbus/conbus_receive_service.py,sha256=
|
|
136
|
+
xp/services/conbus/conbus_receive_service.py,sha256=7wOaEDrdoXwZE9MeUM89eB3hobYpvtbYk_YLv3MVAtc,5352
|
|
135
137
|
xp/services/conbus/conbus_scan_service.py,sha256=tHJ5qaxcNXxAZb2D2F1v6IrzydfxjJOYllM6Txt1eBE,5176
|
|
136
138
|
xp/services/conbus/write_config_service.py,sha256=6feNdixI_Nli4MRLe15nea-7gTEXMUwZIvTqv_1OqHI,7157
|
|
137
139
|
xp/services/homekit/__init__.py,sha256=xAMKmln_AmEFdOOJGKWYi96seRlKDQpKx3-hm7XbdIo,36
|
|
@@ -151,7 +153,7 @@ xp/services/homekit/homekit_service.py,sha256=0lW-hg40ETco3gDBEYkR_sX-UIYsLSKCD4
|
|
|
151
153
|
xp/services/log_file_service.py,sha256=fvPcZQj8XOKUA-4ep5R8n0PelvwvRlTLlVxvIWM5KR4,10506
|
|
152
154
|
xp/services/module_type_service.py,sha256=xWhr1EAZMykL5uNWHWdpa5T8yNruGKH43XRTOS8GwZg,7477
|
|
153
155
|
xp/services/protocol/__init__.py,sha256=qRufBmqRKGzpuzZ5bxBbmwf510TT00Ke8s5HcWGnqRY,818
|
|
154
|
-
xp/services/protocol/conbus_event_protocol.py,sha256=
|
|
156
|
+
xp/services/protocol/conbus_event_protocol.py,sha256=h9ZdnN9CWSBXQqE-M9qmPPbMT3r25cxzXuJsvURp1WQ,14390
|
|
155
157
|
xp/services/protocol/conbus_protocol.py,sha256=JO7yLkD_ohPT0ETjnAIx4CGpZyobf4LdbuozM_43btE,10276
|
|
156
158
|
xp/services/protocol/protocol_factory.py,sha256=PmjN9AtW9sxNo3voqUiNgQA-pTvX1RW4XXFlHKfFr5Q,2470
|
|
157
159
|
xp/services/protocol/telegram_protocol.py,sha256=Ki5DrXsKxiaqLcdP9WWUuhUI7cPu2DfwyZkh-Gv9Lb8,9496
|
|
@@ -176,13 +178,13 @@ xp/services/telegram/telegram_output_service.py,sha256=UaUv_14fR8o5K2PxQBXrCzx-H
|
|
|
176
178
|
xp/services/telegram/telegram_service.py,sha256=XrP1CPi0ckxoKBaNwLA6lo-TogWxXgmXDOsU4Xl8BlY,13237
|
|
177
179
|
xp/services/telegram/telegram_version_service.py,sha256=M5HdOTsLdcwo122FP-jW6R740ktLrtKf2TiMDVz23h8,10528
|
|
178
180
|
xp/term/__init__.py,sha256=Xg2DhBeI3xQJLfc7_BPWI1por-rUXemyer5OtOt9Cus,51
|
|
179
|
-
xp/term/protocol.py,sha256=
|
|
180
|
-
xp/term/protocol.tcss,sha256=
|
|
181
|
-
xp/term/protocol.yml,sha256=
|
|
181
|
+
xp/term/protocol.py,sha256=acYYsv7_4z5ePrnslSC1exKzqbKOE5ZGds4J33Q2XNs,4784
|
|
182
|
+
xp/term/protocol.tcss,sha256=r_KfxrbpycGHLVXqZc6INBBcUJME0hLrAZkF1oqnab4,2126
|
|
183
|
+
xp/term/protocol.yml,sha256=BI1dyWfYsINsJnbSR-z4fzFOsYcY27dS6it8eo7AVnU,2124
|
|
182
184
|
xp/term/widgets/__init__.py,sha256=ftWmN_fmjxy2E8Qfm-YSRmzKfgL0KTBCTpgvYWCPbUY,274
|
|
183
185
|
xp/term/widgets/help_menu.py,sha256=bdT5AYRdtKt_tvZTVbG7-DPMb1mj78kggtjjsa-95BA,1780
|
|
184
|
-
xp/term/widgets/protocol_log.py,sha256=
|
|
185
|
-
xp/term/widgets/status_footer.py,sha256=
|
|
186
|
+
xp/term/widgets/protocol_log.py,sha256=4wh6tpaaKiEUOQTqqpUml6b5WpV3YVmXlG-W8_UYuLA,11693
|
|
187
|
+
xp/term/widgets/status_footer.py,sha256=eRZHkrG5aZCMulibX56KfFyGHS8IgL-7psvr9f9S6FI,1992
|
|
186
188
|
xp/utils/__init__.py,sha256=_avMF_UOkfR3tNeDIPqQ5odmbq5raKkaq1rZ9Cn1CJs,332
|
|
187
189
|
xp/utils/checksum.py,sha256=HDpiQxmdIedbCbZ4o_Box0i_Zig417BtCV_46ZyhiTk,1711
|
|
188
190
|
xp/utils/dependencies.py,sha256=ECS6p0eXzocM5INLwJeckHXn_Dim18uOjXTJ29qQvkQ,22001
|
|
@@ -191,4 +193,4 @@ xp/utils/logging.py,sha256=rZDXwlBrYK8A6MPq5StsMNpgsRowzJXM6fvROPwJdGM,3750
|
|
|
191
193
|
xp/utils/serialization.py,sha256=RWHHk86feaB4ZP7rjE4qOWK0900yg2joUBDkP76gfOY,4618
|
|
192
194
|
xp/utils/state_machine.py,sha256=Oe2sLwCh9z_vr1tF6X0ZRGTeuckRQAGzmef7xc9CNdc,2413
|
|
193
195
|
xp/utils/time_utils.py,sha256=dEyViDlAG9GWU-J3D_YVa-sGma6yiyyMTgN4h2x3PY4,3781
|
|
194
|
-
conson_xp-1.
|
|
196
|
+
conson_xp-1.24.0.dist-info/RECORD,,
|
xp/__init__.py
CHANGED
|
@@ -36,6 +36,7 @@ def send_discover_telegram(ctx: click.Context) -> None:
|
|
|
36
36
|
discovered_devices: Discover response with all found devices.
|
|
37
37
|
"""
|
|
38
38
|
click.echo(json.dumps(discovered_devices.to_dict(), indent=2))
|
|
39
|
+
service.stop_reactor()
|
|
39
40
|
|
|
40
41
|
def on_device_discovered(discovered_device: DiscoveredDevice) -> None:
|
|
41
42
|
"""Handle discovery of sa single module.
|
|
@@ -57,5 +58,9 @@ def send_discover_telegram(ctx: click.Context) -> None:
|
|
|
57
58
|
service: ConbusDiscoverService = (
|
|
58
59
|
ctx.obj.get("container").get_container().resolve(ConbusDiscoverService)
|
|
59
60
|
)
|
|
60
|
-
service
|
|
61
|
-
|
|
61
|
+
with service:
|
|
62
|
+
service.on_progress.connect(progress)
|
|
63
|
+
service.on_device_discovered.connect(on_device_discovered)
|
|
64
|
+
service.on_finish.connect(on_finish)
|
|
65
|
+
service.set_timeout(5)
|
|
66
|
+
service.start_reactor()
|
|
@@ -43,8 +43,9 @@ def receive_telegrams(ctx: Context, timeout: float) -> None:
|
|
|
43
43
|
response_received: Receive response object with telegrams.
|
|
44
44
|
"""
|
|
45
45
|
click.echo(json.dumps(response_received.to_dict(), indent=2))
|
|
46
|
+
service.stop_reactor()
|
|
46
47
|
|
|
47
|
-
def
|
|
48
|
+
def on_progress(telegram_received: str) -> None:
|
|
48
49
|
"""Handle progress updates during telegram receive operation.
|
|
49
50
|
|
|
50
51
|
Args:
|
|
@@ -56,5 +57,7 @@ def receive_telegrams(ctx: Context, timeout: float) -> None:
|
|
|
56
57
|
ctx.obj.get("container").get_container().resolve(ConbusReceiveService)
|
|
57
58
|
)
|
|
58
59
|
with service:
|
|
59
|
-
service.
|
|
60
|
+
service.on_progress.connect(on_progress)
|
|
61
|
+
service.on_finish.connect(on_finish)
|
|
62
|
+
service.set_timeout(timeout)
|
|
60
63
|
service.start_reactor()
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Connection state management module."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
from xp.utils.state_machine import StateMachine
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ConnectionState(str, Enum):
|
|
9
|
+
"""Connection state enumeration.
|
|
10
|
+
|
|
11
|
+
Attributes:
|
|
12
|
+
DISCONNECTING: Disconnecting to server.
|
|
13
|
+
DISCONNECTED: Not connected to server.
|
|
14
|
+
CONNECTING: Connection in progress.
|
|
15
|
+
CONNECTED: Successfully connected.
|
|
16
|
+
FAILED: Connection failed.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
DISCONNECTING = "DISCONNECTING"
|
|
20
|
+
DISCONNECTED = "DISCONNECTED"
|
|
21
|
+
CONNECTING = "CONNECTING"
|
|
22
|
+
CONNECTED = "CONNECTED"
|
|
23
|
+
FAILED = "FAILED"
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def create_state_machine() -> StateMachine:
|
|
27
|
+
"""Create and configure state machine for connection management.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Configured StateMachine with connection state transitions.
|
|
31
|
+
"""
|
|
32
|
+
sm = StateMachine(ConnectionState.DISCONNECTED)
|
|
33
|
+
|
|
34
|
+
# Define valid transitions
|
|
35
|
+
sm.define_transition(
|
|
36
|
+
"connect", {ConnectionState.DISCONNECTED, ConnectionState.FAILED}
|
|
37
|
+
)
|
|
38
|
+
sm.define_transition(
|
|
39
|
+
"disconnect", {ConnectionState.CONNECTED, ConnectionState.CONNECTING}
|
|
40
|
+
)
|
|
41
|
+
sm.define_transition(
|
|
42
|
+
"connecting", {ConnectionState.DISCONNECTED, ConnectionState.FAILED}
|
|
43
|
+
)
|
|
44
|
+
sm.define_transition("connected", {ConnectionState.CONNECTING})
|
|
45
|
+
sm.define_transition(
|
|
46
|
+
"disconnecting", {ConnectionState.CONNECTED, ConnectionState.CONNECTING}
|
|
47
|
+
)
|
|
48
|
+
sm.define_transition("disconnected", {ConnectionState.DISCONNECTING})
|
|
49
|
+
sm.define_transition(
|
|
50
|
+
"failed",
|
|
51
|
+
{
|
|
52
|
+
ConnectionState.CONNECTING,
|
|
53
|
+
ConnectionState.CONNECTED,
|
|
54
|
+
ConnectionState.DISCONNECTING,
|
|
55
|
+
},
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return sm
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Status message models for terminal UI."""
|
|
2
|
+
|
|
3
|
+
from textual.message import Message
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class StatusMessageChanged(Message):
|
|
7
|
+
"""Message posted when status message changes."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, message: str) -> None:
|
|
10
|
+
"""Initialize the message.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
message: The status message to display.
|
|
14
|
+
"""
|
|
15
|
+
super().__init__()
|
|
16
|
+
self.message = message
|
|
@@ -4,8 +4,11 @@ This service implements a TCP client that connects to Conbus servers and sends
|
|
|
4
4
|
discover telegrams to find modules on the network.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import asyncio
|
|
7
8
|
import logging
|
|
8
|
-
from typing import
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
from psygnal import Signal
|
|
9
12
|
|
|
10
13
|
from xp.models import ConbusDiscoverResponse
|
|
11
14
|
from xp.models.conbus.conbus_discover import DiscoveredDevice
|
|
@@ -26,9 +29,15 @@ class ConbusDiscoverService:
|
|
|
26
29
|
|
|
27
30
|
Attributes:
|
|
28
31
|
conbus_protocol: Protocol instance for Conbus communication.
|
|
32
|
+
on_progress: Signal emitted when discovery progress is made (with serial number).
|
|
33
|
+
on_finish: Signal emitted when discovery finishes (with result).
|
|
34
|
+
on_device_discovered: Signal emitted when a device is discovered (with device info).
|
|
29
35
|
"""
|
|
30
36
|
|
|
31
37
|
conbus_protocol: ConbusEventProtocol
|
|
38
|
+
on_progress: Signal = Signal(str)
|
|
39
|
+
on_finish: Signal = Signal(DiscoveredDevice)
|
|
40
|
+
on_device_discovered: Signal = Signal(ConbusDiscoverResponse)
|
|
32
41
|
|
|
33
42
|
def __init__(self, conbus_protocol: ConbusEventProtocol) -> None:
|
|
34
43
|
"""Initialize the Conbus discover service.
|
|
@@ -36,12 +45,6 @@ class ConbusDiscoverService:
|
|
|
36
45
|
Args:
|
|
37
46
|
conbus_protocol: ConbusProtocol.
|
|
38
47
|
"""
|
|
39
|
-
self.progress_callback: Optional[Callable[[str], None]] = None
|
|
40
|
-
self.device_discover_callback: Optional[Callable[[DiscoveredDevice], None]] = (
|
|
41
|
-
None
|
|
42
|
-
)
|
|
43
|
-
self.finish_callback: Optional[Callable[[ConbusDiscoverResponse], None]] = None
|
|
44
|
-
|
|
45
48
|
self.conbus_protocol: ConbusEventProtocol = conbus_protocol
|
|
46
49
|
self.conbus_protocol.on_connection_made.connect(self.connection_made)
|
|
47
50
|
self.conbus_protocol.on_telegram_sent.connect(self.telegram_sent)
|
|
@@ -135,9 +138,7 @@ class ConbusDiscoverService:
|
|
|
135
138
|
"module_type_name": None,
|
|
136
139
|
}
|
|
137
140
|
self.discovered_device_result.discovered_devices.append(device)
|
|
138
|
-
|
|
139
|
-
if self.device_discover_callback:
|
|
140
|
-
self.device_discover_callback(device)
|
|
141
|
+
self.on_device_discovered.emit(device)
|
|
141
142
|
|
|
142
143
|
# Send READ_DATAPOINT telegram to query module type
|
|
143
144
|
self.logger.debug(f"Sending module type query for {serial_number}")
|
|
@@ -147,8 +148,7 @@ class ConbusDiscoverService:
|
|
|
147
148
|
system_function=SystemFunction.READ_DATAPOINT,
|
|
148
149
|
data_value=DataPointType.MODULE_TYPE.value,
|
|
149
150
|
)
|
|
150
|
-
|
|
151
|
-
self.progress_callback(serial_number)
|
|
151
|
+
self.on_progress.emit(serial_number)
|
|
152
152
|
|
|
153
153
|
def handle_module_type_code_response(
|
|
154
154
|
self, serial_number: str, module_type_code: str
|
|
@@ -194,8 +194,7 @@ class ConbusDiscoverService:
|
|
|
194
194
|
device["module_type_code"] = code
|
|
195
195
|
device["module_type_name"] = module_type_name
|
|
196
196
|
|
|
197
|
-
|
|
198
|
-
self.device_discover_callback(device)
|
|
197
|
+
self.on_device_discovered.emit(device)
|
|
199
198
|
|
|
200
199
|
self.logger.debug(
|
|
201
200
|
f"Updated device {serial_number} with module_type {module_type_name}"
|
|
@@ -231,9 +230,7 @@ class ConbusDiscoverService:
|
|
|
231
230
|
self.logger.debug(
|
|
232
231
|
f"Updated device {serial_number} with module_type {module_type}"
|
|
233
232
|
)
|
|
234
|
-
|
|
235
|
-
self.device_discover_callback(device)
|
|
236
|
-
|
|
233
|
+
self.on_device_discovered.emit(device)
|
|
237
234
|
break
|
|
238
235
|
|
|
239
236
|
self.conbus_protocol.send_telegram(
|
|
@@ -249,10 +246,7 @@ class ConbusDiscoverService:
|
|
|
249
246
|
self.logger.info("Discovery stopped after: %ss", timeout)
|
|
250
247
|
self.discovered_device_result.success = False
|
|
251
248
|
self.discovered_device_result.error = "Discovered device timeout"
|
|
252
|
-
|
|
253
|
-
self.finish_callback(self.discovered_device_result)
|
|
254
|
-
|
|
255
|
-
self.stop_reactor()
|
|
249
|
+
self.on_finish.emit(self.discovered_device_result)
|
|
256
250
|
|
|
257
251
|
def failed(self, message: str) -> None:
|
|
258
252
|
"""Handle failed connection event.
|
|
@@ -263,50 +257,64 @@ class ConbusDiscoverService:
|
|
|
263
257
|
self.logger.debug(f"Failed: {message}")
|
|
264
258
|
self.discovered_device_result.success = False
|
|
265
259
|
self.discovered_device_result.error = message
|
|
266
|
-
|
|
267
|
-
self.finish_callback(self.discovered_device_result)
|
|
268
|
-
|
|
269
|
-
self.stop_reactor()
|
|
260
|
+
self.on_finish.emit(self.discovered_device_result)
|
|
270
261
|
|
|
271
262
|
def succeed(self) -> None:
|
|
272
263
|
"""Handle discovered device success event."""
|
|
273
264
|
self.logger.debug("Succeed")
|
|
274
265
|
self.discovered_device_result.success = True
|
|
275
266
|
self.discovered_device_result.error = None
|
|
276
|
-
|
|
277
|
-
self.finish_callback(self.discovered_device_result)
|
|
267
|
+
self.on_finish.emit(self.discovered_device_result)
|
|
278
268
|
|
|
279
|
-
|
|
269
|
+
def set_timeout(self, timeout_seconds: float) -> None:
|
|
270
|
+
"""Setup callbacks and timeout for receiving telegrams.
|
|
280
271
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
self.
|
|
285
|
-
|
|
286
|
-
def start_reactor(self) -> None:
|
|
287
|
-
"""Start reactor."""
|
|
288
|
-
self.logger.info("Starting reactor")
|
|
289
|
-
self.conbus_protocol.start_reactor()
|
|
272
|
+
Args:
|
|
273
|
+
timeout_seconds: Optional timeout in seconds.
|
|
274
|
+
"""
|
|
275
|
+
self.logger.debug("Set timeout")
|
|
276
|
+
self.conbus_protocol.timeout_seconds = timeout_seconds
|
|
290
277
|
|
|
291
|
-
def
|
|
278
|
+
def set_event_loop(
|
|
292
279
|
self,
|
|
293
|
-
|
|
294
|
-
device_discover_callback: Callable[[DiscoveredDevice], None],
|
|
295
|
-
finish_callback: Callable[[ConbusDiscoverResponse], None],
|
|
296
|
-
timeout_seconds: Optional[float] = None,
|
|
280
|
+
event_loop: asyncio.AbstractEventLoop,
|
|
297
281
|
) -> None:
|
|
298
|
-
"""
|
|
282
|
+
"""Setup callbacks and timeout for receiving telegrams.
|
|
299
283
|
|
|
300
284
|
Args:
|
|
301
|
-
|
|
302
|
-
device_discover_callback: Callback for each discovered device.
|
|
303
|
-
finish_callback: Callback when discovery completes.
|
|
304
|
-
timeout_seconds: Optional timeout in seconds.
|
|
285
|
+
event_loop: Optional event loop to use for async operations.
|
|
305
286
|
"""
|
|
306
|
-
self.logger.
|
|
287
|
+
self.logger.debug("Set eventloop")
|
|
288
|
+
self.conbus_protocol.set_event_loop(event_loop)
|
|
307
289
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
self.
|
|
311
|
-
|
|
312
|
-
|
|
290
|
+
def start_reactor(self) -> None:
|
|
291
|
+
"""Start the reactor."""
|
|
292
|
+
self.conbus_protocol.start_reactor()
|
|
293
|
+
|
|
294
|
+
def stop_reactor(self) -> None:
|
|
295
|
+
"""Start the reactor."""
|
|
296
|
+
self.conbus_protocol.stop_reactor()
|
|
297
|
+
|
|
298
|
+
def __enter__(self) -> "ConbusDiscoverService":
|
|
299
|
+
"""Enter context manager.
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
Self for context manager protocol.
|
|
303
|
+
"""
|
|
304
|
+
# Reset state for singleton reuse
|
|
305
|
+
self.receive_response = ConbusDiscoverResponse(success=True)
|
|
306
|
+
return self
|
|
307
|
+
|
|
308
|
+
def __exit__(
|
|
309
|
+
self, _exc_type: Optional[type], _exc_val: Optional[Exception], _exc_tb: Any
|
|
310
|
+
) -> None:
|
|
311
|
+
"""Exit context manager and disconnect signals."""
|
|
312
|
+
self.conbus_protocol.on_connection_made.disconnect(self.connection_made)
|
|
313
|
+
self.conbus_protocol.on_telegram_sent.disconnect(self.telegram_sent)
|
|
314
|
+
self.conbus_protocol.on_telegram_received.disconnect(self.telegram_received)
|
|
315
|
+
self.conbus_protocol.on_timeout.disconnect(self.timeout)
|
|
316
|
+
self.conbus_protocol.on_failed.disconnect(self.failed)
|
|
317
|
+
self.on_device_discovered.disconnect()
|
|
318
|
+
self.on_progress.disconnect()
|
|
319
|
+
self.on_finish.disconnect()
|
|
320
|
+
self.stop_reactor()
|
|
@@ -6,7 +6,9 @@ allowing clients to receive waiting event telegrams using empty telegram sends.
|
|
|
6
6
|
|
|
7
7
|
import asyncio
|
|
8
8
|
import logging
|
|
9
|
-
from typing import Any,
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
from psygnal import Signal
|
|
10
12
|
|
|
11
13
|
from xp.models.conbus.conbus_receive import ConbusReceiveResponse
|
|
12
14
|
from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
|
|
@@ -22,9 +24,13 @@ class ConbusReceiveService:
|
|
|
22
24
|
|
|
23
25
|
Attributes:
|
|
24
26
|
conbus_protocol: Protocol instance for Conbus communication.
|
|
27
|
+
on_progress: Signal emitted when a telegram is received (with telegram frame).
|
|
28
|
+
on_finish: Signal emitted when receiving finishes (with result).
|
|
25
29
|
"""
|
|
26
30
|
|
|
27
31
|
conbus_protocol: ConbusEventProtocol
|
|
32
|
+
on_progress: Signal = Signal(str)
|
|
33
|
+
on_finish: Signal = Signal(ConbusReceiveResponse)
|
|
28
34
|
|
|
29
35
|
def __init__(self, conbus_protocol: ConbusEventProtocol) -> None:
|
|
30
36
|
"""Initialize the Conbus receive service.
|
|
@@ -32,8 +38,6 @@ class ConbusReceiveService:
|
|
|
32
38
|
Args:
|
|
33
39
|
conbus_protocol: ConbusEventProtocol instance.
|
|
34
40
|
"""
|
|
35
|
-
self.progress_callback: Optional[Callable[[str], None]] = None
|
|
36
|
-
self.finish_callback: Optional[Callable[[ConbusReceiveResponse], None]] = None
|
|
37
41
|
self.receive_response: ConbusReceiveResponse = ConbusReceiveResponse(
|
|
38
42
|
success=True
|
|
39
43
|
)
|
|
@@ -67,8 +71,7 @@ class ConbusReceiveService:
|
|
|
67
71
|
telegram_received: The telegram received event.
|
|
68
72
|
"""
|
|
69
73
|
self.logger.debug(f"Telegram received: {telegram_received}")
|
|
70
|
-
|
|
71
|
-
self.progress_callback(telegram_received.frame)
|
|
74
|
+
self.on_progress.emit(telegram_received.frame)
|
|
72
75
|
|
|
73
76
|
if not self.receive_response.received_telegrams:
|
|
74
77
|
self.receive_response.received_telegrams = []
|
|
@@ -79,8 +82,7 @@ class ConbusReceiveService:
|
|
|
79
82
|
timeout = self.conbus_protocol.timeout_seconds
|
|
80
83
|
self.logger.info("Receive stopped after: %ss", timeout)
|
|
81
84
|
self.receive_response.success = True
|
|
82
|
-
|
|
83
|
-
self.finish_callback(self.receive_response)
|
|
85
|
+
self.on_finish.emit(self.receive_response)
|
|
84
86
|
|
|
85
87
|
def failed(self, message: str) -> None:
|
|
86
88
|
"""Handle failed connection event.
|
|
@@ -91,37 +93,37 @@ class ConbusReceiveService:
|
|
|
91
93
|
self.logger.debug("Failed %s:", message)
|
|
92
94
|
self.receive_response.success = False
|
|
93
95
|
self.receive_response.error = message
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
self.on_finish.emit(self.receive_response)
|
|
97
|
+
|
|
98
|
+
def set_timeout(self, timeout_seconds: float) -> None:
|
|
99
|
+
"""Setup callbacks and timeout for receiving telegrams.
|
|
96
100
|
|
|
97
|
-
|
|
101
|
+
Args:
|
|
102
|
+
timeout_seconds: Optional timeout in seconds.
|
|
103
|
+
"""
|
|
104
|
+
self.logger.debug("Set timeout")
|
|
105
|
+
self.conbus_protocol.timeout_seconds = timeout_seconds
|
|
106
|
+
|
|
107
|
+
def set_event_loop(
|
|
98
108
|
self,
|
|
99
|
-
|
|
100
|
-
finish_callback: Callable[[ConbusReceiveResponse], None],
|
|
101
|
-
timeout_seconds: Optional[float] = None,
|
|
102
|
-
event_loop: Optional[asyncio.AbstractEventLoop] = None,
|
|
109
|
+
event_loop: asyncio.AbstractEventLoop,
|
|
103
110
|
) -> None:
|
|
104
111
|
"""Setup callbacks and timeout for receiving telegrams.
|
|
105
112
|
|
|
106
113
|
Args:
|
|
107
|
-
progress_callback: Callback for each received telegram.
|
|
108
|
-
finish_callback: Callback when receiving completes.
|
|
109
|
-
timeout_seconds: Optional timeout in seconds.
|
|
110
114
|
event_loop: Optional event loop to use for async operations.
|
|
111
115
|
"""
|
|
112
|
-
self.logger.
|
|
113
|
-
|
|
114
|
-
self.conbus_protocol.timeout_seconds = timeout_seconds
|
|
115
|
-
self.progress_callback = progress_callback
|
|
116
|
-
self.finish_callback = finish_callback
|
|
117
|
-
|
|
118
|
-
if event_loop:
|
|
119
|
-
self.conbus_protocol.set_event_loop(event_loop)
|
|
116
|
+
self.logger.debug("Set eventloop")
|
|
117
|
+
self.conbus_protocol.set_event_loop(event_loop)
|
|
120
118
|
|
|
121
119
|
def start_reactor(self) -> None:
|
|
122
120
|
"""Start the reactor."""
|
|
123
121
|
self.conbus_protocol.start_reactor()
|
|
124
122
|
|
|
123
|
+
def stop_reactor(self) -> None:
|
|
124
|
+
"""Start the reactor."""
|
|
125
|
+
self.conbus_protocol.stop_reactor()
|
|
126
|
+
|
|
125
127
|
def __enter__(self) -> "ConbusReceiveService":
|
|
126
128
|
"""Enter context manager.
|
|
127
129
|
|
|
@@ -141,3 +143,6 @@ class ConbusReceiveService:
|
|
|
141
143
|
self.conbus_protocol.on_telegram_received.disconnect(self.telegram_received)
|
|
142
144
|
self.conbus_protocol.on_timeout.disconnect(self.timeout)
|
|
143
145
|
self.conbus_protocol.on_failed.disconnect(self.failed)
|
|
146
|
+
self.on_progress.disconnect()
|
|
147
|
+
self.on_finish.disconnect()
|
|
148
|
+
self.stop_reactor()
|
|
@@ -17,7 +17,7 @@ from twisted.internet.interfaces import IAddress, IConnector
|
|
|
17
17
|
from twisted.internet.posixbase import PosixReactorBase
|
|
18
18
|
from twisted.python.failure import Failure
|
|
19
19
|
|
|
20
|
-
from xp.models import ConbusClientConfig
|
|
20
|
+
from xp.models import ConbusClientConfig, ModuleTypeCode
|
|
21
21
|
from xp.models.protocol.conbus_protocol import (
|
|
22
22
|
TelegramReceivedEvent,
|
|
23
23
|
)
|
|
@@ -168,9 +168,6 @@ class ConbusEventProtocol(protocol.Protocol, protocol.ClientFactory):
|
|
|
168
168
|
|
|
169
169
|
Args:
|
|
170
170
|
data: Raw telegram payload (without checksum/framing).
|
|
171
|
-
|
|
172
|
-
Raises:
|
|
173
|
-
IOError: If transport is not open.
|
|
174
171
|
"""
|
|
175
172
|
self.on_send_frame.emit(data)
|
|
176
173
|
|
|
@@ -180,8 +177,9 @@ class ConbusEventProtocol(protocol.Protocol, protocol.ClientFactory):
|
|
|
180
177
|
frame = b"<" + frame_data.encode() + b">"
|
|
181
178
|
|
|
182
179
|
if not self.transport:
|
|
183
|
-
self.logger.info("Invalid transport")
|
|
184
|
-
|
|
180
|
+
self.logger.info("Invalid transport, connection closed.")
|
|
181
|
+
self.on_connection_failed.emit(Failure("Invalid transport."))
|
|
182
|
+
return
|
|
185
183
|
|
|
186
184
|
self.logger.debug(f"Sending frame: {frame.decode()}")
|
|
187
185
|
self.transport.write(frame) # type: ignore
|
|
@@ -211,6 +209,21 @@ class ConbusEventProtocol(protocol.Protocol, protocol.ClientFactory):
|
|
|
211
209
|
)
|
|
212
210
|
self.send_raw_telegram(payload)
|
|
213
211
|
|
|
212
|
+
def send_event_telegram(
|
|
213
|
+
self, module_type_code: ModuleTypeCode, link_number: int, input_number: int
|
|
214
|
+
) -> None:
|
|
215
|
+
"""Send telegram with specified parameters.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
module_type_code: Type code of module.
|
|
219
|
+
link_number: Link number.
|
|
220
|
+
input_number: Input number.
|
|
221
|
+
"""
|
|
222
|
+
payload = (
|
|
223
|
+
f"E" f"{module_type_code}" f"L{link_number:02d}" f"I{input_number:02d}"
|
|
224
|
+
)
|
|
225
|
+
self.send_raw_telegram(payload)
|
|
226
|
+
|
|
214
227
|
def send_raw_telegram(self, payload: str) -> None:
|
|
215
228
|
"""Send telegram with specified parameters.
|
|
216
229
|
|
xp/term/protocol.py
CHANGED
|
@@ -7,6 +7,7 @@ from textual.app import App, ComposeResult
|
|
|
7
7
|
from textual.containers import Horizontal
|
|
8
8
|
|
|
9
9
|
from xp.models.term import ProtocolKeysConfig
|
|
10
|
+
from xp.models.term.status_message import StatusMessageChanged
|
|
10
11
|
from xp.term.widgets.help_menu import HelpMenuWidget
|
|
11
12
|
from xp.term.widgets.protocol_log import ProtocolLogWidget
|
|
12
13
|
from xp.term.widgets.status_footer import StatusFooterWidget
|
|
@@ -125,3 +126,12 @@ class ProtocolMonitorApp(App[None]):
|
|
|
125
126
|
"""
|
|
126
127
|
if self.footer_widget:
|
|
127
128
|
self.footer_widget.update_status(state)
|
|
129
|
+
|
|
130
|
+
def on_status_message_changed(self, message: StatusMessageChanged) -> None:
|
|
131
|
+
"""Handle status message changes from protocol widget.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
message: Message containing the status text.
|
|
135
|
+
"""
|
|
136
|
+
if self.footer_widget:
|
|
137
|
+
self.footer_widget.update_message(message.message)
|
xp/term/protocol.tcss
CHANGED
xp/term/protocol.yml
CHANGED
|
@@ -63,8 +63,8 @@ protocol:
|
|
|
63
63
|
"e":
|
|
64
64
|
name: "Link 1 Off"
|
|
65
65
|
telegrams:
|
|
66
|
-
-
|
|
67
|
-
-
|
|
66
|
+
- E02L01I09M
|
|
67
|
+
- E02L01I09B
|
|
68
68
|
|
|
69
69
|
"f":
|
|
70
70
|
name: "Link 2 On"
|
|
@@ -75,8 +75,8 @@ protocol:
|
|
|
75
75
|
"g":
|
|
76
76
|
name: "Link 2 Off"
|
|
77
77
|
telegrams:
|
|
78
|
-
-
|
|
79
|
-
-
|
|
78
|
+
- E02L02I09M
|
|
79
|
+
- E02L02I09B
|
|
80
80
|
|
|
81
81
|
"h":
|
|
82
82
|
name: "Link 3 On"
|
|
@@ -87,8 +87,8 @@ protocol:
|
|
|
87
87
|
"i":
|
|
88
88
|
name: "Link 3 Off"
|
|
89
89
|
telegrams:
|
|
90
|
-
-
|
|
91
|
-
-
|
|
90
|
+
- E02L03I09M
|
|
91
|
+
- E02L03I09B
|
|
92
92
|
|
|
93
93
|
"j":
|
|
94
94
|
name: "Link 4 On"
|
|
@@ -99,8 +99,8 @@ protocol:
|
|
|
99
99
|
"k":
|
|
100
100
|
name: "Link 4 Off"
|
|
101
101
|
telegrams:
|
|
102
|
-
-
|
|
103
|
-
-
|
|
102
|
+
- E02L04I09M
|
|
103
|
+
- E02L04I09B
|
|
104
104
|
|
|
105
105
|
"l":
|
|
106
106
|
name: "Link 5 On"
|
|
@@ -111,8 +111,8 @@ protocol:
|
|
|
111
111
|
"m":
|
|
112
112
|
name: "Link 5 Off"
|
|
113
113
|
telegrams:
|
|
114
|
-
-
|
|
115
|
-
-
|
|
114
|
+
- E02L05I09M
|
|
115
|
+
- E02L05I09B
|
|
116
116
|
|
|
117
117
|
"n":
|
|
118
118
|
name: "Link 6 On"
|
|
@@ -123,8 +123,8 @@ protocol:
|
|
|
123
123
|
"o":
|
|
124
124
|
name: "Link 6 Off"
|
|
125
125
|
telegrams:
|
|
126
|
-
-
|
|
127
|
-
-
|
|
126
|
+
- E02L06I09M
|
|
127
|
+
- E02L06I09B
|
|
128
128
|
|
|
129
129
|
"p":
|
|
130
130
|
name: "Link 7 On"
|
|
@@ -135,5 +135,5 @@ protocol:
|
|
|
135
135
|
"q":
|
|
136
136
|
name: "Link 7 Off"
|
|
137
137
|
telegrams:
|
|
138
|
-
-
|
|
139
|
-
-
|
|
138
|
+
- E02L07I09M
|
|
139
|
+
- E02L07I09B
|
xp/term/widgets/protocol_log.py
CHANGED
|
@@ -2,71 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
5
|
-
from enum import Enum
|
|
6
5
|
from typing import Any, Optional
|
|
7
6
|
|
|
8
|
-
from textual.message import Message
|
|
9
7
|
from textual.reactive import reactive
|
|
10
8
|
from textual.widget import Widget
|
|
11
9
|
from textual.widgets import RichLog
|
|
10
|
+
from twisted.python.failure import Failure
|
|
12
11
|
|
|
13
12
|
from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
|
|
14
|
-
from xp.
|
|
13
|
+
from xp.models.term.connection_state import ConnectionState
|
|
14
|
+
from xp.models.term.status_message import StatusMessageChanged
|
|
15
15
|
from xp.services.protocol import ConbusEventProtocol
|
|
16
|
-
from xp.utils.state_machine import StateMachine
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class ConnectionState(str, Enum):
|
|
20
|
-
"""Connection state enumeration.
|
|
21
|
-
|
|
22
|
-
Attributes:
|
|
23
|
-
DISCONNECTING: Disconnecting to server.
|
|
24
|
-
DISCONNECTED: Not connected to server.
|
|
25
|
-
CONNECTING: Connection in progress.
|
|
26
|
-
CONNECTED: Successfully connected.
|
|
27
|
-
FAILED: Connection failed.
|
|
28
|
-
"""
|
|
29
|
-
|
|
30
|
-
DISCONNECTING = "DISCONNECTING"
|
|
31
|
-
DISCONNECTED = "DISCONNECTED"
|
|
32
|
-
CONNECTING = "CONNECTING"
|
|
33
|
-
CONNECTED = "CONNECTED"
|
|
34
|
-
FAILED = "FAILED"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def create_connection_state_machine() -> StateMachine:
|
|
38
|
-
"""Create and configure state machine for connection management.
|
|
39
|
-
|
|
40
|
-
Returns:
|
|
41
|
-
Configured StateMachine with connection state transitions.
|
|
42
|
-
"""
|
|
43
|
-
sm = StateMachine(ConnectionState.DISCONNECTED)
|
|
44
|
-
|
|
45
|
-
# Define valid transitions
|
|
46
|
-
sm.define_transition(
|
|
47
|
-
"connect", {ConnectionState.DISCONNECTED, ConnectionState.FAILED}
|
|
48
|
-
)
|
|
49
|
-
sm.define_transition(
|
|
50
|
-
"disconnect", {ConnectionState.CONNECTED, ConnectionState.CONNECTING}
|
|
51
|
-
)
|
|
52
|
-
sm.define_transition(
|
|
53
|
-
"connecting", {ConnectionState.DISCONNECTED, ConnectionState.FAILED}
|
|
54
|
-
)
|
|
55
|
-
sm.define_transition("connected", {ConnectionState.CONNECTING})
|
|
56
|
-
sm.define_transition(
|
|
57
|
-
"disconnecting", {ConnectionState.CONNECTED, ConnectionState.CONNECTING}
|
|
58
|
-
)
|
|
59
|
-
sm.define_transition("disconnected", {ConnectionState.DISCONNECTING})
|
|
60
|
-
sm.define_transition(
|
|
61
|
-
"failed",
|
|
62
|
-
{
|
|
63
|
-
ConnectionState.CONNECTING,
|
|
64
|
-
ConnectionState.CONNECTED,
|
|
65
|
-
ConnectionState.DISCONNECTING,
|
|
66
|
-
},
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
return sm
|
|
70
16
|
|
|
71
17
|
|
|
72
18
|
class ProtocolLogWidget(Widget):
|
|
@@ -79,23 +25,10 @@ class ProtocolLogWidget(Widget):
|
|
|
79
25
|
container: ServiceContainer for dependency injection.
|
|
80
26
|
connection_state: Current connection state (reactive).
|
|
81
27
|
protocol: Reference to ConbusEventProtocol (prevents duplicate connections).
|
|
82
|
-
service: ConbusReceiveService instance.
|
|
83
28
|
logger: Logger instance for this widget.
|
|
84
29
|
log_widget: RichLog widget for displaying messages.
|
|
85
30
|
"""
|
|
86
31
|
|
|
87
|
-
class StatusMessageChanged(Message):
|
|
88
|
-
"""Message posted when status message changes."""
|
|
89
|
-
|
|
90
|
-
def __init__(self, message: str) -> None:
|
|
91
|
-
"""Initialize the message.
|
|
92
|
-
|
|
93
|
-
Args:
|
|
94
|
-
message: The status message to display.
|
|
95
|
-
"""
|
|
96
|
-
super().__init__()
|
|
97
|
-
self.message = message
|
|
98
|
-
|
|
99
32
|
connection_state = reactive(ConnectionState.DISCONNECTED)
|
|
100
33
|
|
|
101
34
|
def __init__(self, container: Any) -> None:
|
|
@@ -108,10 +41,9 @@ class ProtocolLogWidget(Widget):
|
|
|
108
41
|
self.border_title = "Protocol"
|
|
109
42
|
self.container = container
|
|
110
43
|
self.protocol: Optional[ConbusEventProtocol] = None
|
|
111
|
-
self.service: Optional[ConbusReceiveService] = None
|
|
112
44
|
self.logger = logging.getLogger(__name__)
|
|
113
45
|
self.log_widget: Optional[RichLog] = None
|
|
114
|
-
self._state_machine =
|
|
46
|
+
self._state_machine = ConnectionState.create_state_machine()
|
|
115
47
|
|
|
116
48
|
def compose(self) -> Any:
|
|
117
49
|
"""Compose the widget layout.
|
|
@@ -129,11 +61,11 @@ class ProtocolLogWidget(Widget):
|
|
|
129
61
|
Resolves ConbusReceiveService and connects signals.
|
|
130
62
|
"""
|
|
131
63
|
# Resolve service from container (singleton)
|
|
132
|
-
self.
|
|
133
|
-
self.protocol = self.service.conbus_protocol
|
|
64
|
+
self.protocol = self.container.resolve(ConbusEventProtocol)
|
|
134
65
|
|
|
135
66
|
# Connect psygnal signals
|
|
136
67
|
self.protocol.on_connection_made.connect(self._on_connection_made)
|
|
68
|
+
self.protocol.on_connection_failed.connect(self._on_connection_failed)
|
|
137
69
|
self.protocol.on_telegram_received.connect(self._on_telegram_received)
|
|
138
70
|
self.protocol.on_telegram_sent.connect(self._on_telegram_sent)
|
|
139
71
|
self.protocol.on_timeout.connect(self._on_timeout)
|
|
@@ -150,10 +82,6 @@ class ProtocolLogWidget(Widget):
|
|
|
150
82
|
Integrates Twisted reactor with Textual's asyncio loop cleanly.
|
|
151
83
|
"""
|
|
152
84
|
# Guard against duplicate connections (race condition)
|
|
153
|
-
if self.service is None:
|
|
154
|
-
self.logger.error("Service not initialized")
|
|
155
|
-
return
|
|
156
|
-
|
|
157
85
|
if self.protocol is None:
|
|
158
86
|
self.logger.error("Protocol not initialized")
|
|
159
87
|
return
|
|
@@ -169,10 +97,8 @@ class ProtocolLogWidget(Widget):
|
|
|
169
97
|
# Transition to CONNECTING
|
|
170
98
|
if self._state_machine.transition("connecting", ConnectionState.CONNECTING):
|
|
171
99
|
self.connection_state = ConnectionState.CONNECTING
|
|
172
|
-
self.
|
|
173
|
-
self.
|
|
174
|
-
f"Connecting to {self.protocol.cli_config.ip}:{self.protocol.cli_config.port}..."
|
|
175
|
-
)
|
|
100
|
+
self.post_status(
|
|
101
|
+
f"Connecting to {self.protocol.cli_config.ip}:{self.protocol.cli_config.port}..."
|
|
176
102
|
)
|
|
177
103
|
|
|
178
104
|
# Store protocol reference
|
|
@@ -180,41 +106,12 @@ class ProtocolLogWidget(Widget):
|
|
|
180
106
|
self.logger.info(f"Reactor object: {self.protocol._reactor}")
|
|
181
107
|
self.logger.info(f"Reactor running: {self.protocol._reactor.running}")
|
|
182
108
|
|
|
183
|
-
# Setup service callbacks
|
|
184
|
-
def progress_callback(telegram: str) -> None:
|
|
185
|
-
"""Handle progress updates for telegram reception.
|
|
186
|
-
|
|
187
|
-
Args:
|
|
188
|
-
telegram: Received telegram string.
|
|
189
|
-
"""
|
|
190
|
-
pass
|
|
191
|
-
|
|
192
|
-
def finish_callback(response: Any) -> None:
|
|
193
|
-
"""Handle completion of telegram reception.
|
|
194
|
-
|
|
195
|
-
Args:
|
|
196
|
-
response: Response object from telegram reception.
|
|
197
|
-
"""
|
|
198
|
-
pass
|
|
199
|
-
|
|
200
109
|
# Get the currently running asyncio event loop (Textual's loop)
|
|
201
110
|
event_loop = asyncio.get_running_loop()
|
|
202
111
|
self.logger.info(f"Current running loop: {event_loop}")
|
|
203
112
|
self.logger.info(f"Loop is running: {event_loop.is_running()}")
|
|
204
113
|
|
|
205
|
-
self.
|
|
206
|
-
progress_callback=progress_callback,
|
|
207
|
-
finish_callback=finish_callback,
|
|
208
|
-
timeout_seconds=None, # Continuous monitoring
|
|
209
|
-
event_loop=event_loop,
|
|
210
|
-
)
|
|
211
|
-
|
|
212
|
-
reactor = self.service.conbus_protocol._reactor
|
|
213
|
-
reactor.connectTCP(
|
|
214
|
-
self.protocol.cli_config.ip,
|
|
215
|
-
self.protocol.cli_config.port,
|
|
216
|
-
self.protocol,
|
|
217
|
-
)
|
|
114
|
+
self.protocol.connect()
|
|
218
115
|
|
|
219
116
|
# Wait for connection to establish
|
|
220
117
|
await asyncio.sleep(1.0)
|
|
@@ -222,15 +119,16 @@ class ProtocolLogWidget(Widget):
|
|
|
222
119
|
|
|
223
120
|
except Exception as e:
|
|
224
121
|
self.logger.error(f"Connection failed: {e}")
|
|
122
|
+
self.post_status(f"Connection failed: {e}")
|
|
225
123
|
# Transition to FAILED
|
|
226
124
|
if self._state_machine.transition("failed", ConnectionState.FAILED):
|
|
227
125
|
self.connection_state = ConnectionState.FAILED
|
|
228
|
-
self.post_message(self.StatusMessageChanged(f"Connection error: {e}"))
|
|
229
126
|
|
|
230
127
|
def _start_connection(self) -> None:
|
|
231
128
|
"""Start connection (sync wrapper for async method)."""
|
|
232
129
|
# Use run_worker to run async method from sync context
|
|
233
130
|
self.logger.debug("Start connection")
|
|
131
|
+
self.post_status("Start connection")
|
|
234
132
|
self.run_worker(self._start_connection_async(), exclusive=True)
|
|
235
133
|
|
|
236
134
|
def _on_connection_made(self) -> None:
|
|
@@ -239,16 +137,26 @@ class ProtocolLogWidget(Widget):
|
|
|
239
137
|
Sets state to CONNECTED and displays success message.
|
|
240
138
|
"""
|
|
241
139
|
self.logger.debug("Connection made")
|
|
140
|
+
self.post_status("Connection made")
|
|
242
141
|
# Transition to CONNECTED
|
|
243
142
|
if self._state_machine.transition("connected", ConnectionState.CONNECTED):
|
|
244
143
|
self.connection_state = ConnectionState.CONNECTED
|
|
245
144
|
if self.protocol:
|
|
246
|
-
self.
|
|
247
|
-
self.
|
|
248
|
-
f"Connected to {self.protocol.cli_config.ip}:{self.protocol.cli_config.port}"
|
|
249
|
-
)
|
|
145
|
+
self.post_status(
|
|
146
|
+
f"Connected to {self.protocol.cli_config.ip}:{self.protocol.cli_config.port}"
|
|
250
147
|
)
|
|
251
148
|
|
|
149
|
+
def _on_connection_failed(self, failure: Failure) -> None:
|
|
150
|
+
"""Handle connection failed signal.
|
|
151
|
+
|
|
152
|
+
Sets state to DISCONNECTED and displays success message.
|
|
153
|
+
"""
|
|
154
|
+
self.logger.debug("Connection failed")
|
|
155
|
+
self.post_status(failure.getErrorMessage())
|
|
156
|
+
# Transition to CONNECTED
|
|
157
|
+
if self._state_machine.transition("disconnected", ConnectionState.DISCONNECTED):
|
|
158
|
+
self.connection_state = ConnectionState.DISCONNECTED
|
|
159
|
+
|
|
252
160
|
def _on_telegram_received(self, event: TelegramReceivedEvent) -> None:
|
|
253
161
|
"""Handle telegram received signal.
|
|
254
162
|
|
|
@@ -289,7 +197,15 @@ class ProtocolLogWidget(Widget):
|
|
|
289
197
|
if self._state_machine.transition("failed", ConnectionState.FAILED):
|
|
290
198
|
self.connection_state = ConnectionState.FAILED
|
|
291
199
|
self.logger.error(f"Connection failed: {error}")
|
|
292
|
-
self.
|
|
200
|
+
self.post_status(f"Failed: {error}")
|
|
201
|
+
|
|
202
|
+
def post_status(self, message: str) -> None:
|
|
203
|
+
"""Post status message.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
message: message to be sent to status bar.
|
|
207
|
+
"""
|
|
208
|
+
self.post_message(StatusMessageChanged(message))
|
|
293
209
|
|
|
294
210
|
def connect(self) -> None:
|
|
295
211
|
"""Connect to Conbus server.
|
|
@@ -326,7 +242,7 @@ class ProtocolLogWidget(Widget):
|
|
|
326
242
|
"disconnecting", ConnectionState.DISCONNECTING
|
|
327
243
|
):
|
|
328
244
|
self.connection_state = ConnectionState.DISCONNECTING
|
|
329
|
-
self.
|
|
245
|
+
self.post_status("Disconnecting...")
|
|
330
246
|
|
|
331
247
|
if self.protocol:
|
|
332
248
|
self.protocol.disconnect()
|
|
@@ -334,7 +250,7 @@ class ProtocolLogWidget(Widget):
|
|
|
334
250
|
# Transition to DISCONNECTED
|
|
335
251
|
if self._state_machine.transition("disconnected", ConnectionState.DISCONNECTED):
|
|
336
252
|
self.connection_state = ConnectionState.DISCONNECTED
|
|
337
|
-
self.
|
|
253
|
+
self.post_status("Disconnected")
|
|
338
254
|
|
|
339
255
|
def send_telegram(self, name: str, telegram: str) -> None:
|
|
340
256
|
"""Send a raw telegram string.
|
|
@@ -349,19 +265,19 @@ class ProtocolLogWidget(Widget):
|
|
|
349
265
|
|
|
350
266
|
try:
|
|
351
267
|
# Remove angle brackets if present
|
|
352
|
-
self.
|
|
268
|
+
self.post_status(f"{name} sent.")
|
|
353
269
|
# Send raw telegram
|
|
354
270
|
self.protocol.send_raw_telegram(telegram)
|
|
355
271
|
|
|
356
272
|
except Exception as e:
|
|
357
273
|
self.logger.error(f"Failed to send telegram: {e}")
|
|
358
|
-
self.
|
|
274
|
+
self.post_status(f"Failed: {e}")
|
|
359
275
|
|
|
360
276
|
def clear_log(self) -> None:
|
|
361
277
|
"""Clear the protocol log widget."""
|
|
362
278
|
if self.log_widget:
|
|
363
279
|
self.log_widget.clear()
|
|
364
|
-
self.
|
|
280
|
+
self.post_status("Log cleared")
|
|
365
281
|
|
|
366
282
|
def on_unmount(self) -> None:
|
|
367
283
|
"""Clean up when widget unmounts.
|
xp/term/widgets/status_footer.py
CHANGED
|
@@ -25,6 +25,7 @@ class StatusFooterWidget(Horizontal):
|
|
|
25
25
|
kwargs: Additional keyword arguments for Horizontal.
|
|
26
26
|
"""
|
|
27
27
|
super().__init__(*args, **kwargs)
|
|
28
|
+
self.status_text_widget: Static = Static("", id="status-text")
|
|
28
29
|
self.status_widget: Static = Static("○", id="status-line")
|
|
29
30
|
|
|
30
31
|
def compose(self) -> ComposeResult:
|
|
@@ -34,6 +35,7 @@ class StatusFooterWidget(Horizontal):
|
|
|
34
35
|
Footer and status indicator widgets.
|
|
35
36
|
"""
|
|
36
37
|
yield Footer()
|
|
38
|
+
yield self.status_text_widget
|
|
37
39
|
yield self.status_widget
|
|
38
40
|
|
|
39
41
|
def update_status(self, state: Any) -> None:
|
|
@@ -51,3 +53,11 @@ class StatusFooterWidget(Horizontal):
|
|
|
51
53
|
"DISCONNECTED": "○",
|
|
52
54
|
}.get(state.value, "○")
|
|
53
55
|
self.status_widget.update(dot)
|
|
56
|
+
|
|
57
|
+
def update_message(self, message: str) -> None:
|
|
58
|
+
"""Update status text with message.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
message: Status message to display.
|
|
62
|
+
"""
|
|
63
|
+
self.status_text_widget.update(message)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|