conson-xp 1.27.0__py3-none-any.whl → 1.29.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.27.0.dist-info → conson_xp-1.29.0.dist-info}/METADATA +2 -1
- {conson_xp-1.27.0.dist-info → conson_xp-1.29.0.dist-info}/RECORD +22 -16
- xp/__init__.py +1 -1
- xp/cli/commands/term/term_commands.py +21 -0
- xp/models/term/__init__.py +2 -0
- xp/models/term/module_state.py +30 -0
- xp/services/server/client_buffer_manager.py +69 -0
- xp/services/server/server_service.py +21 -11
- xp/services/server/xp24_server_service.py +1 -4
- xp/services/server/xp33_server_service.py +1 -4
- xp/services/telegram/telegram_output_service.py +33 -0
- xp/services/term/__init__.py +2 -1
- xp/services/term/state_monitor_service.py +413 -0
- xp/term/state.py +97 -0
- xp/term/state.tcss +86 -0
- xp/term/widgets/help_menu.py +3 -3
- xp/term/widgets/modules_list.py +224 -0
- xp/term/widgets/status_footer.py +5 -4
- xp/utils/dependencies.py +20 -0
- {conson_xp-1.27.0.dist-info → conson_xp-1.29.0.dist-info}/WHEEL +0 -0
- {conson_xp-1.27.0.dist-info → conson_xp-1.29.0.dist-info}/entry_points.txt +0 -0
- {conson_xp-1.27.0.dist-info → conson_xp-1.29.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.29.0
|
|
4
4
|
Summary: XP Protocol Communication Tools
|
|
5
5
|
Author-Email: ldvchosal <ldvchosal@github.com>
|
|
6
6
|
License: MIT License
|
|
@@ -417,6 +417,7 @@ xp telegram version
|
|
|
417
417
|
|
|
418
418
|
xp term
|
|
419
419
|
xp term protocol
|
|
420
|
+
xp term state
|
|
420
421
|
|
|
421
422
|
<!-- END CLI HELP -->
|
|
422
423
|
```
|
|
@@ -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.29.0.dist-info/METADATA,sha256=k_I_mYoEaWaCsYx4kk3JOZ5FaBuAFkggSTKk28JMyGk,10312
|
|
2
|
+
conson_xp-1.29.0.dist-info/WHEEL,sha256=tsUv_t7BDeJeRHaSrczbGeuK-TtDpGsWi_JfpzD255I,90
|
|
3
|
+
conson_xp-1.29.0.dist-info/entry_points.txt,sha256=1OcdIcDM1hz3ljCXgybaPUh1IOFEwkaTgLIW9u9zGeg,50
|
|
4
|
+
conson_xp-1.29.0.dist-info/licenses/LICENSE,sha256=rxj6woMM-r3YCyGq_UHFtbh7kHTAJgrccH6O-33IDE4,1419
|
|
5
|
+
xp/__init__.py,sha256=CTv0SXJngPEN2pQxHNESqU87b-94c8Ke6ShS2yRsqDA,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
|
|
@@ -42,7 +42,7 @@ xp/cli/commands/telegram/telegram_parse_commands.py,sha256=_OYOso1hS4f_ox96qlkYL
|
|
|
42
42
|
xp/cli/commands/telegram/telegram_version_commands.py,sha256=WQyx1-B9yJ8V9WrFyBpOvULJ-jq12GoZZDDoRbM7eyw,1553
|
|
43
43
|
xp/cli/commands/term/__init__.py,sha256=1NNST_8YJfj5LCujQISwQflK6LyEn7mDmZpMpvI9d-o,116
|
|
44
44
|
xp/cli/commands/term/term.py,sha256=gjvsv2OE-F_KNWQrWi04fXQ5cGo0l8P-Ortbb5KTA-A,309
|
|
45
|
-
xp/cli/commands/term/term_commands.py,sha256=
|
|
45
|
+
xp/cli/commands/term/term_commands.py,sha256=CwqnLPEi7LuC7bCo7kIGKMZoVICY0nu42k8C554A1TA,1206
|
|
46
46
|
xp/cli/main.py,sha256=ap5jU0DrSnrCKDKqGXcz9N-sngZodyyN-5ReWE8Fh1s,1817
|
|
47
47
|
xp/cli/utils/__init__.py,sha256=gTGIj60Uai0iE7sr9_TtEpl04fD7krtTzbbigXUsUVU,46
|
|
48
48
|
xp/cli/utils/click_tree.py,sha256=ilmM2IMa_c-TqUMsv2alrZXuS0BNhvVlrBlSfyN8lzM,1670
|
|
@@ -104,8 +104,9 @@ xp/models/telegram/system_telegram.py,sha256=9FNQ4Mf47mRK7wGrTg2GzziVsrEWCE5ZkZp
|
|
|
104
104
|
xp/models/telegram/telegram.py,sha256=IJUxHX6ftLcET9C1pjvLhUO5Db5JO6W7rUItzdEW30I,842
|
|
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
|
-
xp/models/term/__init__.py,sha256=
|
|
107
|
+
xp/models/term/__init__.py,sha256=aFvzGZHr_dI6USb8MJuYLSLMvxi_ZWMVtokHDt8428s,263
|
|
108
108
|
xp/models/term/connection_state.py,sha256=floDRMeMcfgMrYIVsyoVHBXHtxd3hqm-xOdr3oXtaHY,1793
|
|
109
|
+
xp/models/term/module_state.py,sha256=tg5V3HNicXhXE10WuDSCN8OleVrorXrOosXOEgEAVE0,934
|
|
109
110
|
xp/models/term/protocol_keys_config.py,sha256=CTujcfI2_NOeltjvHy_cnsHzxLSVsGFXieMZlD-zj0Q,1204
|
|
110
111
|
xp/models/term/status_message.py,sha256=DOmzL0dbig5mP1UEoXdgzGT4UG2RyAXa_yRVo5c4x8w,394
|
|
111
112
|
xp/models/term/telegram_display.py,sha256=RJDrJh4tqRmT0i1-tfYy17paEmVb3HY3DMuFPsEhZyc,533
|
|
@@ -161,38 +162,43 @@ xp/services/protocol/telegram_protocol.py,sha256=Ki5DrXsKxiaqLcdP9WWUuhUI7cPu2Df
|
|
|
161
162
|
xp/services/reverse_proxy_service.py,sha256=BUOlcLlTU-R5iuC_96rasug21xo19wK9_4fMQXxc0QM,15061
|
|
162
163
|
xp/services/server/__init__.py,sha256=QEcCj-jK0goAukJCe15TKYFQfSAzWsduPT_wW0HxZU8,48
|
|
163
164
|
xp/services/server/base_server_service.py,sha256=B-ntxp3swbwuri-9_2EuvBDi-4Uo9AH-AA4iAFGWIS4,12682
|
|
165
|
+
xp/services/server/client_buffer_manager.py,sha256=1d_MqfzuUqBwaQUiC1n5K76WwSxrdngYAmNH7he6u3o,2235
|
|
164
166
|
xp/services/server/cp20_server_service.py,sha256=SXdI6Jt400T9sLdw86ovEqKRGeV3nYVaHEA9Gcj6W2A,2041
|
|
165
167
|
xp/services/server/device_service_factory.py,sha256=Y4TvSFALeq0zYzHfCwcbikSpmIyYbLcvm9756n5Jm7Q,3744
|
|
166
|
-
xp/services/server/server_service.py,sha256=
|
|
168
|
+
xp/services/server/server_service.py,sha256=2t3guPVX3YUyNJo7B5b1U80eRMyEgE7irT2X8MMQMag,16302
|
|
167
169
|
xp/services/server/xp130_server_service.py,sha256=YnvetDp72-QzkyDGB4qfZZIwFs03HuibUOz2zb9XR0c,2191
|
|
168
170
|
xp/services/server/xp20_server_service.py,sha256=1wJ7A-bRkN9O5Spu3q3LNDW31mNtNF2eNMQ5E6O2ltA,2928
|
|
169
171
|
xp/services/server/xp230_server_service.py,sha256=k9ftCY5tjLFP31mKVCspq283RVaPkGx-Yq61Urk8JLs,1815
|
|
170
|
-
xp/services/server/xp24_server_service.py,sha256=
|
|
171
|
-
xp/services/server/xp33_server_service.py,sha256=
|
|
172
|
+
xp/services/server/xp24_server_service.py,sha256=a-RZzmieoPd8-SrNX1qwdqzsizjjWwz6TAb0f4Ehz2k,8598
|
|
173
|
+
xp/services/server/xp33_server_service.py,sha256=vvgQxnYXfHlzh3uFYxexHrrOr4l1qPI85n6ig17iWA0,19673
|
|
172
174
|
xp/services/telegram/__init__.py,sha256=kv0JgMg13Fp18WgGQpalNRAWwiWbrz18X4kZAP9xpSQ,48
|
|
173
175
|
xp/services/telegram/telegram_blink_service.py,sha256=Xctc9mCSZiiW1YTh8cA-4jlc8fTioS5OxT6ymhSqiYI,4487
|
|
174
176
|
xp/services/telegram/telegram_checksum_service.py,sha256=rp_C5PlraOOIyqZDp9XjBBNZLUeBLdQNNHVpN6D-1v8,4729
|
|
175
177
|
xp/services/telegram/telegram_datapoint_service.py,sha256=iZ-zp_EM_1ouyeTbd2erhIY2x-98nEHveWWN_a9NfFU,2750
|
|
176
178
|
xp/services/telegram/telegram_discover_service.py,sha256=oTpiq-yzP_UmC0xVOMMFeHO-rIlK1pF3aG-Kq4SeiBI,9025
|
|
177
179
|
xp/services/telegram/telegram_link_number_service.py,sha256=1_c-_QCRPTHYn3BmMElrBJqGG6vnoIst8CB-N42hazk,6862
|
|
178
|
-
xp/services/telegram/telegram_output_service.py,sha256=
|
|
180
|
+
xp/services/telegram/telegram_output_service.py,sha256=LK9xHAc3eNeXz82Xs9Nm8WfrHNr7-u2vboDiB7mIFPQ,11950
|
|
179
181
|
xp/services/telegram/telegram_service.py,sha256=XrP1CPi0ckxoKBaNwLA6lo-TogWxXgmXDOsU4Xl8BlY,13237
|
|
180
182
|
xp/services/telegram/telegram_version_service.py,sha256=M5HdOTsLdcwo122FP-jW6R740ktLrtKf2TiMDVz23h8,10528
|
|
181
|
-
xp/services/term/__init__.py,sha256=
|
|
183
|
+
xp/services/term/__init__.py,sha256=BIeOK042bMR-0l6MA80wdW5VuHlpWOXtRER9IG5ilQA,245
|
|
182
184
|
xp/services/term/protocol_monitor_service.py,sha256=PhEzLNzWf1XieQw94ua-hJu9ccwrAzhdxSZGe4kHghs,9945
|
|
185
|
+
xp/services/term/state_monitor_service.py,sha256=PgwCH8nce1RODV33aJefiX3on-pSGEgP_4FDAoU5Trc,16218
|
|
183
186
|
xp/term/__init__.py,sha256=Xg2DhBeI3xQJLfc7_BPWI1por-rUXemyer5OtOt9Cus,51
|
|
184
187
|
xp/term/protocol.py,sha256=oLJAExvIaOSpy75A5TaYB_7R9skTTtNtPx8hiJLdy_U,3425
|
|
185
188
|
xp/term/protocol.tcss,sha256=r_KfxrbpycGHLVXqZc6INBBcUJME0hLrAZkF1oqnab4,2126
|
|
189
|
+
xp/term/state.py,sha256=sR7I6t4gJSkO2YS3TwonAnGPR_f43coCk4xKdWETus0,3233
|
|
190
|
+
xp/term/state.tcss,sha256=Njp7fc16cCunLq7hi5RvXjPi4jSCGi5aPDnusb9dq1Y,1401
|
|
186
191
|
xp/term/widgets/__init__.py,sha256=ftWmN_fmjxy2E8Qfm-YSRmzKfgL0KTBCTpgvYWCPbUY,274
|
|
187
|
-
xp/term/widgets/help_menu.py,sha256=
|
|
192
|
+
xp/term/widgets/help_menu.py,sha256=w2NjwiC_s16St0rigZ9ef9S0V9Y4v0J5eCVCHAdRKF4,1789
|
|
193
|
+
xp/term/widgets/modules_list.py,sha256=DD0PUnY4gv05hkCVxThWULHg1ZNNY8xr1XFaZrv9kS4,7666
|
|
188
194
|
xp/term/widgets/protocol_log.py,sha256=CJUpckWj7GC1kVqixDadteyGnI4aHyzd4kkH-pSbzno,2600
|
|
189
|
-
xp/term/widgets/status_footer.py,sha256=
|
|
195
|
+
xp/term/widgets/status_footer.py,sha256=bxrcqKzJ9V0aPSn_WwraVpJz7NxBUh3yIjA3fwv5nVA,3256
|
|
190
196
|
xp/utils/__init__.py,sha256=_avMF_UOkfR3tNeDIPqQ5odmbq5raKkaq1rZ9Cn1CJs,332
|
|
191
197
|
xp/utils/checksum.py,sha256=HDpiQxmdIedbCbZ4o_Box0i_Zig417BtCV_46ZyhiTk,1711
|
|
192
|
-
xp/utils/dependencies.py,sha256=
|
|
198
|
+
xp/utils/dependencies.py,sha256=UmVAEpGqEG6Li0h6u6I-mFgBTu6dsTeWjWUnfaGFofQ,24227
|
|
193
199
|
xp/utils/event_helper.py,sha256=W-A_xmoXlpWZBbJH6qdaN50o3-XrwFsDgvAGMJDiAgo,1001
|
|
194
200
|
xp/utils/logging.py,sha256=rZDXwlBrYK8A6MPq5StsMNpgsRowzJXM6fvROPwJdGM,3750
|
|
195
201
|
xp/utils/serialization.py,sha256=RWHHk86feaB4ZP7rjE4qOWK0900yg2joUBDkP76gfOY,4618
|
|
196
202
|
xp/utils/state_machine.py,sha256=Oe2sLwCh9z_vr1tF6X0ZRGTeuckRQAGzmef7xc9CNdc,2413
|
|
197
203
|
xp/utils/time_utils.py,sha256=dEyViDlAG9GWU-J3D_YVa-sGma6yiyyMTgN4h2x3PY4,3781
|
|
198
|
-
conson_xp-1.
|
|
204
|
+
conson_xp-1.29.0.dist-info/RECORD,,
|
xp/__init__.py
CHANGED
|
@@ -25,3 +25,24 @@ def protocol_monitor(ctx: Context) -> None:
|
|
|
25
25
|
|
|
26
26
|
# Resolve ProtocolMonitorApp from container and run
|
|
27
27
|
ctx.obj.get("container").get_container().resolve(ProtocolMonitorApp).run()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@term.command("state")
|
|
31
|
+
@click.pass_context
|
|
32
|
+
def state_monitor(ctx: Context) -> None:
|
|
33
|
+
r"""Start TUI for module state monitoring.
|
|
34
|
+
|
|
35
|
+
Displays module states from Conson configuration with real-time
|
|
36
|
+
updates in an interactive terminal interface.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
ctx: Click context object.
|
|
40
|
+
|
|
41
|
+
Examples:
|
|
42
|
+
\b
|
|
43
|
+
xp term state
|
|
44
|
+
"""
|
|
45
|
+
from xp.term.state import StateMonitorApp
|
|
46
|
+
|
|
47
|
+
# Resolve StateMonitorApp from container and run
|
|
48
|
+
ctx.obj.get("container").get_container().resolve(StateMonitorApp).run()
|
xp/models/term/__init__.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"""Terminal UI models."""
|
|
2
2
|
|
|
3
|
+
from xp.models.term.module_state import ModuleState
|
|
3
4
|
from xp.models.term.protocol_keys_config import (
|
|
4
5
|
ProtocolKeyConfig,
|
|
5
6
|
ProtocolKeysConfig,
|
|
6
7
|
)
|
|
7
8
|
|
|
8
9
|
__all__ = [
|
|
10
|
+
"ModuleState",
|
|
9
11
|
"ProtocolKeyConfig",
|
|
10
12
|
"ProtocolKeysConfig",
|
|
11
13
|
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Module state data model."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class ModuleState:
|
|
10
|
+
"""State of a Conson module for TUI display.
|
|
11
|
+
|
|
12
|
+
Attributes:
|
|
13
|
+
name: Module name/identifier (e.g., A01, A02).
|
|
14
|
+
serial_number: Module serial number.
|
|
15
|
+
module_type: Module type designation (e.g., XP130, XP230, XP24).
|
|
16
|
+
link_number: Link number for the module.
|
|
17
|
+
outputs: Output states as space-separated binary values. Empty string for modules without outputs.
|
|
18
|
+
auto_report: Auto-report enabled status (Y/N).
|
|
19
|
+
error_status: Module status ("OK" or error code like "E10").
|
|
20
|
+
last_update: Last communication timestamp. None if never updated.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
name: str
|
|
24
|
+
serial_number: str
|
|
25
|
+
module_type: str
|
|
26
|
+
link_number: int
|
|
27
|
+
outputs: str
|
|
28
|
+
auto_report: bool
|
|
29
|
+
error_status: str
|
|
30
|
+
last_update: Optional[datetime]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Client buffer manager for broadcasting telegrams to connected clients.
|
|
2
|
+
|
|
3
|
+
This module provides thread-safe management of per-client telegram queues,
|
|
4
|
+
enabling broadcast of telegrams from device services to all connected clients.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import queue
|
|
8
|
+
import socket
|
|
9
|
+
import threading
|
|
10
|
+
from typing import Dict, Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ClientBufferManager:
|
|
14
|
+
"""
|
|
15
|
+
Thread-safe manager for client telegram queues.
|
|
16
|
+
|
|
17
|
+
Manages individual queues for each connected client, providing thread-safe
|
|
18
|
+
operations for client registration, unregistration, and telegram broadcasting.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self) -> None:
|
|
22
|
+
"""Initialize the client buffer manager."""
|
|
23
|
+
self._buffers: Dict[socket.socket, queue.Queue[str]] = {}
|
|
24
|
+
self._lock = threading.Lock()
|
|
25
|
+
|
|
26
|
+
def register_client(self, client_socket: socket.socket) -> queue.Queue[str]:
|
|
27
|
+
"""Register a new client and create its telegram queue.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
client_socket: The socket of the connecting client.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
The newly created queue for this client.
|
|
34
|
+
"""
|
|
35
|
+
with self._lock:
|
|
36
|
+
client_queue: queue.Queue[str] = queue.Queue()
|
|
37
|
+
self._buffers[client_socket] = client_queue
|
|
38
|
+
return client_queue
|
|
39
|
+
|
|
40
|
+
def unregister_client(self, client_socket: socket.socket) -> None:
|
|
41
|
+
"""Unregister a client and remove its telegram queue.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
client_socket: The socket of the disconnecting client.
|
|
45
|
+
"""
|
|
46
|
+
with self._lock:
|
|
47
|
+
self._buffers.pop(client_socket, None)
|
|
48
|
+
|
|
49
|
+
def broadcast(self, telegram: str) -> None:
|
|
50
|
+
"""Broadcast a telegram to all connected clients.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
telegram: The telegram string to broadcast.
|
|
54
|
+
"""
|
|
55
|
+
with self._lock:
|
|
56
|
+
for client_queue in self._buffers.values():
|
|
57
|
+
client_queue.put(telegram)
|
|
58
|
+
|
|
59
|
+
def get_queue(self, client_socket: socket.socket) -> Optional[queue.Queue[str]]:
|
|
60
|
+
"""Retrieve the queue for a specific client.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
client_socket: The socket of the client.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
The client's queue if registered, None otherwise.
|
|
67
|
+
"""
|
|
68
|
+
with self._lock:
|
|
69
|
+
return self._buffers.get(client_socket)
|
|
@@ -5,6 +5,7 @@ Discover Request telegrams with configurable device information.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
|
+
import queue
|
|
8
9
|
import socket
|
|
9
10
|
import threading
|
|
10
11
|
from pathlib import Path
|
|
@@ -15,6 +16,7 @@ from xp.models.homekit.homekit_conson_config import (
|
|
|
15
16
|
ConsonModuleListConfig,
|
|
16
17
|
)
|
|
17
18
|
from xp.services.server.base_server_service import BaseServerService
|
|
19
|
+
from xp.services.server.client_buffer_manager import ClientBufferManager
|
|
18
20
|
from xp.services.server.device_service_factory import DeviceServiceFactory
|
|
19
21
|
from xp.services.telegram.telegram_discover_service import TelegramDiscoverService
|
|
20
22
|
from xp.services.telegram.telegram_service import TelegramService
|
|
@@ -68,7 +70,7 @@ class ServerService:
|
|
|
68
70
|
None # Background thread for storm
|
|
69
71
|
)
|
|
70
72
|
self.collector_stop_event = threading.Event() # Event to stop thread
|
|
71
|
-
self.
|
|
73
|
+
self.client_buffers = ClientBufferManager() # Per-client queue manager
|
|
72
74
|
|
|
73
75
|
# Set up logging
|
|
74
76
|
self.logger = logging.getLogger(__name__)
|
|
@@ -191,6 +193,9 @@ class ServerService:
|
|
|
191
193
|
self, client_socket: socket.socket, client_address: tuple[str, int]
|
|
192
194
|
) -> None:
|
|
193
195
|
"""Handle individual client connection."""
|
|
196
|
+
# Register client and get its dedicated queue
|
|
197
|
+
client_queue = self.client_buffers.register_client(client_socket)
|
|
198
|
+
|
|
194
199
|
try:
|
|
195
200
|
|
|
196
201
|
idle_timeout = 300
|
|
@@ -202,11 +207,14 @@ class ServerService:
|
|
|
202
207
|
|
|
203
208
|
while True:
|
|
204
209
|
|
|
205
|
-
# send waiting buffer
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
+
# send waiting buffer from client's dedicated queue
|
|
211
|
+
while not client_queue.empty():
|
|
212
|
+
try:
|
|
213
|
+
buffer = client_queue.get_nowait()
|
|
214
|
+
client_socket.send(buffer.encode("latin-1"))
|
|
215
|
+
self.logger.debug(f"Sent buffer to {client_address}")
|
|
216
|
+
except queue.Empty:
|
|
217
|
+
break
|
|
210
218
|
|
|
211
219
|
# Receive data from client
|
|
212
220
|
data = None
|
|
@@ -230,11 +238,8 @@ class ServerService:
|
|
|
230
238
|
|
|
231
239
|
# Process request (discover or data request)
|
|
232
240
|
responses = self._process_request(message)
|
|
233
|
-
|
|
234
|
-
# Send responses
|
|
235
241
|
for response in responses:
|
|
236
|
-
|
|
237
|
-
self.logger.debug(f"Sent to {client_address}: {response[:-1]}")
|
|
242
|
+
self.client_buffers.broadcast(response)
|
|
238
243
|
|
|
239
244
|
except socket.timeout:
|
|
240
245
|
self.logger.debug(f"Client {client_address} timed out")
|
|
@@ -242,6 +247,8 @@ class ServerService:
|
|
|
242
247
|
self.logger.error(f"Error handling client {client_address}: {e}")
|
|
243
248
|
finally:
|
|
244
249
|
try:
|
|
250
|
+
# Unregister client before closing socket
|
|
251
|
+
self.client_buffers.unregister_client(client_socket)
|
|
245
252
|
client_socket.close()
|
|
246
253
|
self.logger.info(f"Client {client_address} disconnected")
|
|
247
254
|
except Exception as e:
|
|
@@ -330,6 +337,8 @@ class ServerService:
|
|
|
330
337
|
self.logger.warning(f"Failed to parse telegram: {telegram}")
|
|
331
338
|
return responses
|
|
332
339
|
|
|
340
|
+
self.client_buffers.broadcast(parsed_telegram.raw_telegram)
|
|
341
|
+
|
|
333
342
|
# Handle discover requests
|
|
334
343
|
if self.discover_service.is_discover_request(parsed_telegram):
|
|
335
344
|
for device_service in self.device_services.values():
|
|
@@ -421,7 +430,8 @@ class ServerService:
|
|
|
421
430
|
collected = 0
|
|
422
431
|
for device_service in self.device_services.values():
|
|
423
432
|
telegram_buffer = device_service.collect_telegram_buffer()
|
|
424
|
-
|
|
433
|
+
for telegram in telegram_buffer:
|
|
434
|
+
self.client_buffers.broadcast(telegram)
|
|
425
435
|
collected += len(telegram_buffer)
|
|
426
436
|
|
|
427
437
|
# Wait a bit before checking again
|
|
@@ -161,10 +161,7 @@ class XP24ServerService(BaseServerService):
|
|
|
161
161
|
|
|
162
162
|
data_value = handler()
|
|
163
163
|
data_part = (
|
|
164
|
-
f"R{self.serial_number}"
|
|
165
|
-
f"F02D{datapoint_type.value}"
|
|
166
|
-
f"{self.module_type_code.value:02}"
|
|
167
|
-
f"{data_value}"
|
|
164
|
+
f"R{self.serial_number}" f"F02D{datapoint_type.value}" f"{data_value}"
|
|
168
165
|
)
|
|
169
166
|
telegram = self._build_response_telegram(data_part)
|
|
170
167
|
|
|
@@ -226,10 +226,7 @@ class XP33ServerService(BaseServerService):
|
|
|
226
226
|
|
|
227
227
|
data_value = handler()
|
|
228
228
|
data_part = (
|
|
229
|
-
f"R{self.serial_number}"
|
|
230
|
-
f"F02D{datapoint_type.value}"
|
|
231
|
-
f"{self.module_type_code.value:02}"
|
|
232
|
-
f"{data_value}"
|
|
229
|
+
f"R{self.serial_number}" f"F02D{datapoint_type.value}" f"{data_value}"
|
|
233
230
|
)
|
|
234
231
|
telegram = self._build_response_telegram(data_part)
|
|
235
232
|
|
|
@@ -320,3 +320,36 @@ class TelegramOutputService:
|
|
|
320
320
|
f"Timestamp: {telegram.timestamp}\n"
|
|
321
321
|
f"Checksum: {telegram.checksum}{checksum_status}"
|
|
322
322
|
)
|
|
323
|
+
|
|
324
|
+
@staticmethod
|
|
325
|
+
def format_output_state(data_value: str) -> str:
|
|
326
|
+
"""Format module output state data value for display.
|
|
327
|
+
|
|
328
|
+
Algorithm:
|
|
329
|
+
1. Remove 'x' characters
|
|
330
|
+
2. Format to 4 chars with space padding on the right
|
|
331
|
+
3. Invert order
|
|
332
|
+
4. Add spaces between characters
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
data_value: Raw data value from module output state datapoint (e.g., "xxxx0101", "xx1110").
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
Formatted output string with spaces (e.g., "1 0 1 0", "0 1 1 1").
|
|
339
|
+
|
|
340
|
+
Examples:
|
|
341
|
+
>>> TelegramOutputService.format_output_state("xxxx0101")
|
|
342
|
+
"1 0 1 0"
|
|
343
|
+
>>> TelegramOutputService.format_output_state("xx1110")
|
|
344
|
+
"0 1 1 1"
|
|
345
|
+
>>> TelegramOutputService.format_output_state("xxxx01")
|
|
346
|
+
" 1 0"
|
|
347
|
+
"""
|
|
348
|
+
# Remove 'x' characters
|
|
349
|
+
cleaned = data_value.replace("x", "").replace("X", "")
|
|
350
|
+
# Format to 4 chars with space padding on the right
|
|
351
|
+
padded = cleaned.ljust(4)[:4]
|
|
352
|
+
# Invert order
|
|
353
|
+
inverted = padded[::-1]
|
|
354
|
+
# Add spaces between characters
|
|
355
|
+
return " ".join(inverted)
|
xp/services/term/__init__.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Terminal interface services."""
|
|
2
2
|
|
|
3
3
|
from xp.services.term.protocol_monitor_service import ProtocolMonitorService
|
|
4
|
+
from xp.services.term.state_monitor_service import StateMonitorService
|
|
4
5
|
|
|
5
|
-
__all__ = ["ProtocolMonitorService"]
|
|
6
|
+
__all__ = ["ProtocolMonitorService", "StateMonitorService"]
|