conson-xp 1.19.0__py3-none-any.whl → 1.21.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.
@@ -3,6 +3,7 @@
3
3
  This module implements the Twisted protocol for Conbus communication.
4
4
  """
5
5
 
6
+ import asyncio
6
7
  import logging
7
8
  from queue import SimpleQueue
8
9
  from random import randint
@@ -208,6 +209,14 @@ class ConbusEventProtocol(protocol.Protocol, protocol.ClientFactory):
208
209
  f"F{system_function.value}"
209
210
  f"D{data_value}"
210
211
  )
212
+ self.send_raw_telegram(payload)
213
+
214
+ def send_raw_telegram(self, payload: str) -> None:
215
+ """Send telegram with specified parameters.
216
+
217
+ Args:
218
+ payload: Telegram to send.
219
+ """
211
220
  self.telegram_queue.put_nowait(payload.encode())
212
221
  self.call_later(0.0, self.start_queue_manager)
213
222
 
@@ -302,14 +311,21 @@ class ConbusEventProtocol(protocol.Protocol, protocol.ClientFactory):
302
311
  self.logger.info("Stopping reactor")
303
312
  self._reactor.stop()
304
313
 
305
- def start_reactor(self) -> None:
306
- """Start the reactor if it's running."""
307
- # Connect to TCP server
314
+ def connect(self) -> None:
315
+ """Connect to TCP server."""
308
316
  self.logger.info(
309
317
  f"Connecting to TCP server {self.cli_config.ip}:{self.cli_config.port}"
310
318
  )
311
319
  self._reactor.connectTCP(self.cli_config.ip, self.cli_config.port, self)
312
320
 
321
+ def disconnect(self) -> None:
322
+ """Disconnect from TCP server."""
323
+ self.logger.info("Disconnecting TCP server")
324
+ self._reactor.disconnectAll()
325
+
326
+ def start_reactor(self) -> None:
327
+ """Start the reactor if it's running."""
328
+ self.connect()
313
329
  # Run the reactor (which now uses asyncio underneath)
314
330
  self.logger.info("Starting reactor event loop.")
315
331
  self._reactor.run()
@@ -340,6 +356,23 @@ class ConbusEventProtocol(protocol.Protocol, protocol.ClientFactory):
340
356
  later = randint(10, 80) / 100
341
357
  self.call_later(later, self.process_telegram_queue)
342
358
 
359
+ def set_event_loop(self, event_loop: asyncio.AbstractEventLoop) -> None:
360
+ """Change the event loop.
361
+
362
+ Args:
363
+ event_loop: the event loop instance.
364
+ """
365
+ reactor = self._reactor
366
+ if hasattr(reactor, "_asyncioEventloop"):
367
+ reactor._asyncioEventloop = event_loop
368
+
369
+ # Set reactor to running state
370
+ if not reactor.running:
371
+ reactor.running = True
372
+ if hasattr(reactor, "startRunning"):
373
+ reactor.startRunning()
374
+ self.logger.info("Set reactor to running state")
375
+
343
376
  def __enter__(self) -> "ConbusEventProtocol":
344
377
  """Enter context manager.
345
378
 
xp/term/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """TUI (Terminal User Interface) module for XP."""
xp/term/app.py ADDED
@@ -0,0 +1,158 @@
1
+ """Protocol Monitor TUI Application."""
2
+
3
+ from pathlib import Path
4
+ from typing import Any, Optional
5
+
6
+ from textual.app import App, ComposeResult
7
+ from textual.containers import Horizontal, Vertical
8
+ from textual.widgets import DataTable, Footer, Static
9
+
10
+ from xp.models.term import ProtocolKeysConfig
11
+ from xp.term.widgets.protocol_log import ProtocolLogWidget
12
+
13
+
14
+ class ProtocolMonitorApp(App[None]):
15
+ """Textual app for real-time protocol monitoring.
16
+
17
+ Displays live RX/TX telegram stream from Conbus server in an interactive
18
+ terminal interface with keyboard shortcuts for control.
19
+
20
+ Attributes:
21
+ container: ServiceContainer for dependency injection.
22
+ CSS_PATH: Path to CSS stylesheet file.
23
+ BINDINGS: Keyboard bindings for app actions.
24
+ TITLE: Application title displayed in header.
25
+ ENABLE_COMMAND_PALETTE: Disable Textual's command palette feature.
26
+ """
27
+
28
+ CSS_PATH = Path(__file__).parent / "protocol.tcss"
29
+ TITLE = "Protocol Monitor"
30
+ ENABLE_COMMAND_PALETTE = False
31
+
32
+ BINDINGS = [
33
+ ("Q", "quit", "Quit"),
34
+ ("C", "toggle_connection", "Connect"),
35
+ ("R", "reset", "Reset"),
36
+ ("0-9,a-q", "protocol_keys", "Keys"),
37
+ ]
38
+
39
+ def __init__(self, container: Any) -> None:
40
+ """Initialize the Protocol Monitor app.
41
+
42
+ Args:
43
+ container: ServiceContainer for resolving services.
44
+ """
45
+ super().__init__()
46
+ self.container = container
47
+ self.protocol_widget: Optional[ProtocolLogWidget] = None
48
+ self.status_widget: Optional[Static] = None
49
+ self.status_text_widget: Optional[Static] = None
50
+ self.help_table: Optional[DataTable] = None
51
+ self.protocol_keys = self._load_protocol_keys()
52
+
53
+ def _load_protocol_keys(self) -> ProtocolKeysConfig:
54
+ """Load protocol keys from YAML config file.
55
+
56
+ Returns:
57
+ ProtocolKeysConfig instance.
58
+ """
59
+ config_path = Path(__file__).parent / "protocol.yml"
60
+ return ProtocolKeysConfig.from_yaml(config_path)
61
+
62
+ def compose(self) -> ComposeResult:
63
+ """Compose the app layout with widgets.
64
+
65
+ Yields:
66
+ ProtocolLogWidget and Footer widgets.
67
+ """
68
+ with Horizontal(id="main-container"):
69
+ self.protocol_widget = ProtocolLogWidget(container=self.container)
70
+ yield self.protocol_widget
71
+
72
+ # Help menu (hidden by default)
73
+ help_container = Vertical(id="help-menu")
74
+ help_container.border_title = "Help menu"
75
+ help_container.can_focus = False
76
+ with help_container:
77
+ self.help_table = DataTable(id="help-table", show_header=False)
78
+ self.help_table.can_focus = False
79
+ yield self.help_table
80
+
81
+ with Horizontal(id="footer-container"):
82
+ yield Footer()
83
+ self.status_widget = Static("○", id="status-line")
84
+ yield self.status_widget
85
+
86
+ def action_toggle_connection(self) -> None:
87
+ """Toggle connection on 'c' key press.
88
+
89
+ Connects if disconnected/failed, disconnects if connected/connecting.
90
+ """
91
+ if self.protocol_widget:
92
+ from xp.term.widgets.protocol_log import ConnectionState
93
+
94
+ state = self.protocol_widget.connection_state
95
+ if state in (ConnectionState.CONNECTED, ConnectionState.CONNECTING):
96
+ self.protocol_widget.disconnect()
97
+ else:
98
+ self.protocol_widget.connect()
99
+
100
+ def action_reset(self) -> None:
101
+ """Reset and clear protocol widget on 'r' key press."""
102
+ if self.protocol_widget:
103
+ self.protocol_widget.clear_log()
104
+
105
+ def on_key(self, event: Any) -> None:
106
+ """Handle key press events for protocol keys.
107
+
108
+ Args:
109
+ event: Key press event from Textual.
110
+ """
111
+ if event.key in self.protocol_keys.protocol and self.protocol_widget:
112
+ key_config = self.protocol_keys.protocol[event.key]
113
+ for telegram in key_config.telegrams:
114
+ self.protocol_widget.send_telegram(key_config.name, telegram)
115
+
116
+ def on_mount(self) -> None:
117
+ """Set up status line updates when app mounts."""
118
+ if self.protocol_widget:
119
+ self.protocol_widget.watch(
120
+ self.protocol_widget,
121
+ "connection_state",
122
+ self._update_status,
123
+ )
124
+
125
+ # Initialize help table
126
+ if self.help_table:
127
+ self.help_table.add_columns("Key", "Command")
128
+ for key, config in self.protocol_keys.protocol.items():
129
+ self.help_table.add_row(key, config.name)
130
+
131
+ def _update_status(self, state: Any) -> None:
132
+ """Update status line with connection state.
133
+
134
+ Args:
135
+ state: Current connection state.
136
+ """
137
+ if self.status_widget:
138
+ # Map states to colored dots
139
+ status_map = {
140
+ "CONNECTED": "[green]●[/green]",
141
+ "CONNECTING": "[yellow]●[/yellow]",
142
+ "DISCONNECTING": "[yellow]●[/yellow]",
143
+ "FAILED": "[red]●[/red]",
144
+ "DISCONNECTED": "○",
145
+ }
146
+ dot = status_map.get(state.value, "○")
147
+ self.status_widget.update(dot)
148
+
149
+ def on_protocol_log_widget_status_message_changed(
150
+ self, message: ProtocolLogWidget.StatusMessageChanged
151
+ ) -> None:
152
+ """Handle status message changes from protocol widget.
153
+
154
+ Args:
155
+ message: Message containing the status text.
156
+ """
157
+ if self.status_text_widget:
158
+ self.status_text_widget.update(message.message)
xp/term/protocol.tcss ADDED
@@ -0,0 +1,135 @@
1
+ /* Protocol Monitor TUI Styling */
2
+
3
+ /* App-level styling */
4
+ Screen {
5
+ background: $background;
6
+ }
7
+
8
+ /* Protocol Log Widget */
9
+ ProtocolLogWidget {
10
+ border: solid $success;
11
+ width: 1fr;
12
+ height: 1fr;
13
+ background: $background;
14
+ padding: 1;
15
+ }
16
+
17
+ ProtocolLogWidget > RichLog {
18
+ background: $background !important;
19
+ scrollbar-background: $background;
20
+ }
21
+
22
+ ProtocolLogWidget > RichLog:focus {
23
+ background: $background !important;
24
+ background-tint: transparent;
25
+ }
26
+
27
+ ProtocolLogWidget > .connection-status {
28
+ color: $text;
29
+ text-align: center;
30
+ padding: 1;
31
+ }
32
+
33
+ ProtocolLogWidget > .connection-status.connecting {
34
+ color: $warning;
35
+ }
36
+
37
+ ProtocolLogWidget > .connection-status.connected {
38
+ color: $success;
39
+ }
40
+
41
+ ProtocolLogWidget > .connection-status.failed {
42
+ color: $error;
43
+ }
44
+
45
+ /* Message display styling */
46
+ .message-tx {
47
+ color: $success;
48
+ }
49
+
50
+ .message-rx {
51
+ color: $success;
52
+ }
53
+
54
+ .message-frame {
55
+ color: $text-muted;
56
+ }
57
+
58
+ /* Main container */
59
+ #main-container {
60
+ height: 1fr;
61
+ }
62
+
63
+ /* Help menu styling */
64
+ #help-menu {
65
+ width: 35;
66
+ height: 1fr;
67
+ background: $background;
68
+ border: solid $success;
69
+ padding: 1;
70
+ }
71
+
72
+ #help-menu:focus {
73
+ border: solid $success;
74
+ }
75
+
76
+ #help-title {
77
+ color: $success;
78
+ text-align: center;
79
+ text-style: bold;
80
+ margin-bottom: 1;
81
+ }
82
+
83
+ #help-table {
84
+ background: $background;
85
+ height: auto;
86
+ color: $success;
87
+ }
88
+
89
+ DataTable {
90
+ background: $background;
91
+ }
92
+
93
+ DataTable > .datatable--header {
94
+ background: $background;
95
+ color: $success;
96
+ }
97
+
98
+ DataTable > .datatable--cursor {
99
+ background: $background;
100
+ }
101
+
102
+ DataTable:focus > .datatable--cursor {
103
+ background: $background;
104
+ }
105
+
106
+ /* Footer styling */
107
+ #footer-container {
108
+ dock: bottom;
109
+ height: 1;
110
+ background: $background;
111
+ }
112
+
113
+ Footer {
114
+ width: auto;
115
+ background: $background;
116
+ color: $text;
117
+ }
118
+
119
+ #status-text {
120
+ dock: right;
121
+ width: auto;
122
+ padding: 0 1;
123
+ background: $background;
124
+ color: $text;
125
+ text-align: right;
126
+ }
127
+
128
+ #status-line {
129
+ dock: right;
130
+ width: auto;
131
+ padding: 0 1;
132
+ background: $background;
133
+ color: $text;
134
+ text-align: right;
135
+ }
xp/term/protocol.yml ADDED
@@ -0,0 +1,139 @@
1
+ protocol:
2
+ "1":
3
+ name: "Discover"
4
+ telegrams:
5
+ - S0000000000F01D00
6
+ "2":
7
+ name: "Error Code"
8
+ telegrams:
9
+ - S0020044966F02D10
10
+ "3":
11
+ name: "Module Type"
12
+ telegrams:
13
+ - S0020044966F02D00
14
+ "4":
15
+ name: "Auto Report"
16
+ telegrams:
17
+ - S0020044966F02D21
18
+ "5":
19
+ name: "Link Number"
20
+ telegrams:
21
+ - S0020044966F02D04
22
+ "6":
23
+ name: "Blink On"
24
+ telegrams:
25
+ - S0020044966F01D01
26
+ "7":
27
+ name: "Blink Off"
28
+ telegrams:
29
+ - S0020044966F01D00
30
+ "8":
31
+ name: "Output 1 On"
32
+ telegrams:
33
+ - S0020044966F02101
34
+ "9":
35
+ name: "Output 1 Off"
36
+ telegrams:
37
+ - S0020044966F02100
38
+ "0":
39
+ name: "Output State"
40
+ telegrams:
41
+ - S0020044966F02D09
42
+ "a":
43
+ name: "Module State"
44
+ telegrams:
45
+ - S0020044966F02D09
46
+ "b":
47
+ name: "All Off"
48
+ telegrams:
49
+ - E02L00I00M
50
+ - E02L00I00B
51
+ "c":
52
+ name: "All On"
53
+ telegrams:
54
+ - E02L00I08M
55
+ - E02L00I08B
56
+
57
+ "d":
58
+ name: "All 1 On"
59
+ telegrams:
60
+ - E02L01I08M
61
+ - E02L01I08B
62
+
63
+ "e":
64
+ name: "All 1 Off"
65
+ telegrams:
66
+ - E02L01I00M
67
+ - E02L01I00B
68
+
69
+ "f":
70
+ name: "All 2 On"
71
+ telegrams:
72
+ - E02L02I08M
73
+ - E02L02I08B
74
+
75
+ "g":
76
+ name: "All 2 Off"
77
+ telegrams:
78
+ - E02L02I00M
79
+ - E02L02I00B
80
+
81
+ "h":
82
+ name: "All 3 On"
83
+ telegrams:
84
+ - E02L03I08M
85
+ - E02L03I08B
86
+
87
+ "i":
88
+ name: "All 3 Off"
89
+ telegrams:
90
+ - E02L03I00M
91
+ - E02L03I00B
92
+
93
+ "j":
94
+ name: "All 4 On"
95
+ telegrams:
96
+ - E02L04I08M
97
+ - E02L04I08B
98
+
99
+ "k":
100
+ name: "All 4 Off"
101
+ telegrams:
102
+ - E02L04I00M
103
+ - E02L04I00B
104
+
105
+ "l":
106
+ name: "All 5 On"
107
+ telegrams:
108
+ - E02L05I08M
109
+ - E02L05I08B
110
+
111
+ "m":
112
+ name: "All 5 Off"
113
+ telegrams:
114
+ - E02L05I00M
115
+ - E02L05I00B
116
+
117
+ "n":
118
+ name: "All 6 On"
119
+ telegrams:
120
+ - E02L06I08M
121
+ - E02L06I08B
122
+
123
+ "o":
124
+ name: "All 6 Off"
125
+ telegrams:
126
+ - E02L06I00M
127
+ - E02L06I00B
128
+
129
+ "p":
130
+ name: "All 7 On"
131
+ telegrams:
132
+ - E02L07I08M
133
+ - E02L07I08B
134
+
135
+ "q":
136
+ name: "All 7 Off"
137
+ telegrams:
138
+ - E02L07I00M
139
+ - E02L07I00B
@@ -0,0 +1 @@
1
+ """TUI widgets package."""