conson-xp 0.11.21__py3-none-any.whl → 1.1.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-0.11.21.dist-info → conson_xp-1.1.0.dist-info}/METADATA +1 -1
- {conson_xp-0.11.21.dist-info → conson_xp-1.1.0.dist-info}/RECORD +21 -22
- xp/__init__.py +1 -1
- xp/cli/commands/conbus/conbus_config_commands.py +3 -12
- xp/cli/commands/conbus/conbus_linknumber_commands.py +24 -11
- xp/cli/commands/conbus/conbus_output_commands.py +44 -18
- xp/cli/commands/conbus/conbus_scan_commands.py +18 -98
- xp/models/conbus/conbus.py +12 -11
- xp/models/conbus/conbus_linknumber.py +1 -1
- xp/services/conbus/conbus_autoreport_get_service.py +29 -80
- xp/services/conbus/conbus_datapoint_service.py +6 -1
- xp/services/conbus/conbus_lightlevel_get_service.py +36 -84
- xp/services/conbus/conbus_linknumber_get_service.py +86 -0
- xp/services/conbus/conbus_linknumber_set_service.py +155 -0
- xp/services/conbus/conbus_output_service.py +129 -92
- xp/services/conbus/conbus_scan_service.py +95 -99
- xp/services/protocol/conbus_protocol.py +6 -4
- xp/utils/dependencies.py +21 -44
- xp/services/conbus/conbus_connection_pool.py +0 -148
- xp/services/conbus/conbus_linknumber_service.py +0 -197
- xp/services/conbus/conbus_service.py +0 -306
- {conson_xp-0.11.21.dist-info → conson_xp-1.1.0.dist-info}/WHEEL +0 -0
- {conson_xp-0.11.21.dist-info → conson_xp-1.1.0.dist-info}/entry_points.txt +0 -0
- {conson_xp-0.11.21.dist-info → conson_xp-1.1.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
"""Connection pooling implementation for Conbus TCP connections.
|
|
2
|
-
|
|
3
|
-
This module provides a singleton connection pool for managing TCP socket connections
|
|
4
|
-
to Conbus servers with automatic lifecycle management, health checking, and reconnection.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import logging
|
|
8
|
-
import socket
|
|
9
|
-
import threading
|
|
10
|
-
import time
|
|
11
|
-
from typing import Any, Optional
|
|
12
|
-
|
|
13
|
-
from xp.models import ConbusClientConfig
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class ConbusSocketConnectionManager:
|
|
17
|
-
"""Connection manager for TCP socket connections to Conbus servers"""
|
|
18
|
-
|
|
19
|
-
def __init__(self, cli_config: ConbusClientConfig):
|
|
20
|
-
self.config = cli_config.conbus
|
|
21
|
-
self.logger = logging.getLogger(__name__)
|
|
22
|
-
|
|
23
|
-
def create(self) -> socket.socket:
|
|
24
|
-
"""Create and configure a new TCP socket connection"""
|
|
25
|
-
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
26
|
-
sock.settimeout(self.config.timeout)
|
|
27
|
-
sock.connect((self.config.ip, self.config.port))
|
|
28
|
-
self.logger.info(
|
|
29
|
-
f"Created new connection to {self.config.ip}:{self.config.port} (timeout: {self.config.timeout})"
|
|
30
|
-
)
|
|
31
|
-
return sock
|
|
32
|
-
|
|
33
|
-
def dispose(self, connection: socket.socket) -> None:
|
|
34
|
-
"""Close and cleanup socket connection"""
|
|
35
|
-
try:
|
|
36
|
-
connection.close()
|
|
37
|
-
self.logger.info("Disposed socket connection")
|
|
38
|
-
except Exception as e:
|
|
39
|
-
self.logger.warning(f"Error disposing connection: {e}")
|
|
40
|
-
|
|
41
|
-
@staticmethod
|
|
42
|
-
def check_aliveness(connection: socket.socket) -> bool:
|
|
43
|
-
"""Verify if connection is still alive"""
|
|
44
|
-
try:
|
|
45
|
-
# Use socket error checking rather than sending empty data
|
|
46
|
-
# to avoid potential protocol issues
|
|
47
|
-
error = connection.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR)
|
|
48
|
-
return error == 0
|
|
49
|
-
except (socket.error, OSError):
|
|
50
|
-
return False
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
class ConbusConnectionPool:
|
|
54
|
-
"""Singleton connection pool for Conbus TCP connections"""
|
|
55
|
-
|
|
56
|
-
_lock = threading.Lock()
|
|
57
|
-
|
|
58
|
-
def __init__(self, connection_manager: ConbusSocketConnectionManager) -> None:
|
|
59
|
-
if hasattr(self, "_initialized"):
|
|
60
|
-
return
|
|
61
|
-
|
|
62
|
-
self._connection_manager = connection_manager
|
|
63
|
-
self._connection: Optional[socket.socket] = None
|
|
64
|
-
self._current_connection: Optional[socket.socket] = None
|
|
65
|
-
self._connection_created_at: Optional[float] = None
|
|
66
|
-
self._lock = threading.Lock()
|
|
67
|
-
self.logger = logging.getLogger(__name__)
|
|
68
|
-
|
|
69
|
-
# Configuration
|
|
70
|
-
self.idle_timeout = 21600 # 6 hours
|
|
71
|
-
self.max_lifetime = 21600 # 6 hours
|
|
72
|
-
self._initialized = True
|
|
73
|
-
|
|
74
|
-
def _is_connection_expired(self) -> bool:
|
|
75
|
-
"""Check if the current connection has expired"""
|
|
76
|
-
if self._connection_created_at is None:
|
|
77
|
-
return True
|
|
78
|
-
|
|
79
|
-
age = time.time() - self._connection_created_at
|
|
80
|
-
return age > self.max_lifetime
|
|
81
|
-
|
|
82
|
-
def _is_connection_alive(self) -> bool:
|
|
83
|
-
"""Check if connection is still alive"""
|
|
84
|
-
if self._connection is None or self._connection_manager is None:
|
|
85
|
-
return False
|
|
86
|
-
|
|
87
|
-
return self._connection_manager.check_aliveness(self._connection)
|
|
88
|
-
|
|
89
|
-
def acquire_connection(self) -> socket.socket:
|
|
90
|
-
"""Acquire a connection from the pool"""
|
|
91
|
-
if self._connection_manager is None:
|
|
92
|
-
raise RuntimeError("Connection pool not initialized")
|
|
93
|
-
|
|
94
|
-
with self._lock:
|
|
95
|
-
# Check if we need a new connection
|
|
96
|
-
if (
|
|
97
|
-
self._connection is None
|
|
98
|
-
or self._is_connection_expired()
|
|
99
|
-
or not self._is_connection_alive()
|
|
100
|
-
):
|
|
101
|
-
|
|
102
|
-
# Close existing connection if any
|
|
103
|
-
if self._connection:
|
|
104
|
-
self._connection_manager.dispose(self._connection)
|
|
105
|
-
self._connection = None
|
|
106
|
-
|
|
107
|
-
# Create new connection
|
|
108
|
-
self._connection = self._connection_manager.create()
|
|
109
|
-
self._connection_created_at = time.time()
|
|
110
|
-
self.logger.debug("Created new connection")
|
|
111
|
-
|
|
112
|
-
self.logger.debug("Acquired connection from pool")
|
|
113
|
-
return self._connection
|
|
114
|
-
|
|
115
|
-
# noinspection PyUnusedLocal
|
|
116
|
-
def release_connection(self, connection: socket.socket) -> None:
|
|
117
|
-
"""Release a connection back to the pool (no-op for single connection pool)"""
|
|
118
|
-
self.logger.debug("Released connection back to pool")
|
|
119
|
-
# For single connection pool, we just log but don't actually close the connection
|
|
120
|
-
|
|
121
|
-
def __enter__(self) -> socket.socket:
|
|
122
|
-
"""Context manager entry - acquire connection"""
|
|
123
|
-
self._current_connection = self.acquire_connection()
|
|
124
|
-
return self._current_connection
|
|
125
|
-
|
|
126
|
-
def __exit__(
|
|
127
|
-
self,
|
|
128
|
-
_exc_type: Optional[type],
|
|
129
|
-
_exc_val: Optional[Exception],
|
|
130
|
-
_exc_tb: Optional[Any],
|
|
131
|
-
) -> None:
|
|
132
|
-
"""Context manager exit - release connection"""
|
|
133
|
-
if hasattr(self, "_current_connection") and self._current_connection:
|
|
134
|
-
self.release_connection(self._current_connection)
|
|
135
|
-
self._current_connection = None
|
|
136
|
-
|
|
137
|
-
def close(self) -> None:
|
|
138
|
-
"""Close the connection pool and cleanup resources"""
|
|
139
|
-
with self._lock:
|
|
140
|
-
if self._connection and self._connection_manager is not None:
|
|
141
|
-
try:
|
|
142
|
-
self._connection_manager.dispose(self._connection)
|
|
143
|
-
self.logger.info("Connection pool closed")
|
|
144
|
-
except Exception as e:
|
|
145
|
-
self.logger.error(f"Error closing connection pool: {e}")
|
|
146
|
-
finally:
|
|
147
|
-
self._connection = None
|
|
148
|
-
self._connection_created_at = None
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
"""Conbus Link Number Service for setting module link numbers.
|
|
2
|
-
|
|
3
|
-
This service handles setting link numbers for modules through Conbus telegrams.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
import logging
|
|
7
|
-
from typing import Any, Optional
|
|
8
|
-
|
|
9
|
-
from xp.models.conbus.conbus_linknumber import ConbusLinknumberResponse
|
|
10
|
-
from xp.models.telegram.datapoint_type import DataPointType
|
|
11
|
-
from xp.models.telegram.reply_telegram import ReplyTelegram
|
|
12
|
-
from xp.services.conbus.conbus_datapoint_service import ConbusDatapointService
|
|
13
|
-
from xp.services.conbus.conbus_service import ConbusService
|
|
14
|
-
from xp.services.telegram.telegram_link_number_service import (
|
|
15
|
-
LinkNumberError,
|
|
16
|
-
LinkNumberService,
|
|
17
|
-
)
|
|
18
|
-
from xp.services.telegram.telegram_service import TelegramService
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class ConbusLinknumberService:
|
|
22
|
-
"""
|
|
23
|
-
Service for setting and getting module link numbers via Conbus telegrams.
|
|
24
|
-
|
|
25
|
-
Handles link number assignment by sending F04D04 telegrams and processing
|
|
26
|
-
ACK/NAK responses from modules. Also handles link number reading using
|
|
27
|
-
datapoint queries.
|
|
28
|
-
"""
|
|
29
|
-
|
|
30
|
-
def __init__(
|
|
31
|
-
self,
|
|
32
|
-
conbus_service: ConbusService,
|
|
33
|
-
datapoint_service: ConbusDatapointService,
|
|
34
|
-
link_number_service: LinkNumberService,
|
|
35
|
-
telegram_service: TelegramService,
|
|
36
|
-
):
|
|
37
|
-
"""Initialize the Conbus link number service"""
|
|
38
|
-
|
|
39
|
-
# Service dependencies
|
|
40
|
-
self.conbus_service = conbus_service
|
|
41
|
-
self.datapoint_service = datapoint_service
|
|
42
|
-
self.link_number_service = link_number_service
|
|
43
|
-
self.telegram_service = telegram_service
|
|
44
|
-
|
|
45
|
-
# Set up logging
|
|
46
|
-
self.logger = logging.getLogger(__name__)
|
|
47
|
-
|
|
48
|
-
def __enter__(self) -> "ConbusLinknumberService":
|
|
49
|
-
return self
|
|
50
|
-
|
|
51
|
-
def __exit__(
|
|
52
|
-
self,
|
|
53
|
-
_exc_type: Optional[type],
|
|
54
|
-
_exc_val: Optional[Exception],
|
|
55
|
-
_exc_tb: Optional[Any],
|
|
56
|
-
) -> None:
|
|
57
|
-
# Cleanup logic if needed
|
|
58
|
-
pass
|
|
59
|
-
|
|
60
|
-
def set_linknumber(
|
|
61
|
-
self, serial_number: str, link_number: int
|
|
62
|
-
) -> ConbusLinknumberResponse:
|
|
63
|
-
"""
|
|
64
|
-
Set the link number for a specific module.
|
|
65
|
-
|
|
66
|
-
Args:
|
|
67
|
-
serial_number: 10-digit module serial number
|
|
68
|
-
link_number: Link number to set (0-99)
|
|
69
|
-
|
|
70
|
-
Returns:
|
|
71
|
-
ConbusLinknumberResponse with operation result
|
|
72
|
-
|
|
73
|
-
Raises:
|
|
74
|
-
LinkNumberError: If parameters are invalid
|
|
75
|
-
"""
|
|
76
|
-
try:
|
|
77
|
-
# Generate the link number setting telegram
|
|
78
|
-
telegram = self.link_number_service.generate_set_link_number_telegram(
|
|
79
|
-
serial_number, link_number
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
# Send telegram using ConbusService
|
|
83
|
-
with self.conbus_service:
|
|
84
|
-
response = self.conbus_service.send_raw_telegram(telegram)
|
|
85
|
-
|
|
86
|
-
# Determine result based on response
|
|
87
|
-
result = "NAK" # Default to NAK
|
|
88
|
-
if response.success and response.received_telegrams:
|
|
89
|
-
# Try to parse the first received telegram
|
|
90
|
-
if len(response.received_telegrams) > 0:
|
|
91
|
-
received_telegram = response.received_telegrams[0]
|
|
92
|
-
try:
|
|
93
|
-
parsed_telegram = self.telegram_service.parse_telegram(
|
|
94
|
-
received_telegram
|
|
95
|
-
)
|
|
96
|
-
if isinstance(parsed_telegram, ReplyTelegram):
|
|
97
|
-
if self.link_number_service.is_ack_response(
|
|
98
|
-
parsed_telegram
|
|
99
|
-
):
|
|
100
|
-
result = "ACK"
|
|
101
|
-
elif self.link_number_service.is_nak_response(
|
|
102
|
-
parsed_telegram
|
|
103
|
-
):
|
|
104
|
-
result = "NAK"
|
|
105
|
-
except Exception as e:
|
|
106
|
-
self.logger.warning(f"Failed to parse reply telegram: {e}")
|
|
107
|
-
|
|
108
|
-
return ConbusLinknumberResponse(
|
|
109
|
-
success=response.success and result == "ACK",
|
|
110
|
-
result=result,
|
|
111
|
-
link_number=link_number,
|
|
112
|
-
serial_number=serial_number,
|
|
113
|
-
sent_telegram=telegram,
|
|
114
|
-
received_telegrams=response.received_telegrams,
|
|
115
|
-
error=response.error,
|
|
116
|
-
timestamp=response.timestamp,
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
except LinkNumberError as e:
|
|
120
|
-
return ConbusLinknumberResponse(
|
|
121
|
-
success=False,
|
|
122
|
-
result="NAK",
|
|
123
|
-
serial_number=serial_number,
|
|
124
|
-
error=str(e),
|
|
125
|
-
)
|
|
126
|
-
except Exception as e:
|
|
127
|
-
return ConbusLinknumberResponse(
|
|
128
|
-
success=False,
|
|
129
|
-
result="NAK",
|
|
130
|
-
serial_number=serial_number,
|
|
131
|
-
error=f"Unexpected error: {e}",
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
def get_linknumber(self, serial_number: str) -> ConbusLinknumberResponse:
|
|
135
|
-
"""
|
|
136
|
-
Get the current link number for a specific module.
|
|
137
|
-
|
|
138
|
-
Args:
|
|
139
|
-
serial_number: 10-digit module serial number
|
|
140
|
-
|
|
141
|
-
Returns:
|
|
142
|
-
ConbusLinknumberResponse with operation result and link number
|
|
143
|
-
|
|
144
|
-
Raises:
|
|
145
|
-
Exception: If datapoint query fails
|
|
146
|
-
"""
|
|
147
|
-
try:
|
|
148
|
-
# TODO: Migrate to new ConbusDatapointService callback-based API
|
|
149
|
-
# Query the LINK_NUMBER datapoint
|
|
150
|
-
datapoint_response = self.datapoint_service.query_datapoint( # type: ignore[call-arg,func-returns-value]
|
|
151
|
-
serial_number=serial_number,
|
|
152
|
-
datapoint_type=DataPointType.LINK_NUMBER,
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
if datapoint_response.success and datapoint_response.datapoint_telegram:
|
|
156
|
-
# Extract link number from datapoint response
|
|
157
|
-
try:
|
|
158
|
-
link_number_value = int(
|
|
159
|
-
datapoint_response.datapoint_telegram.data_value
|
|
160
|
-
)
|
|
161
|
-
return ConbusLinknumberResponse(
|
|
162
|
-
success=True,
|
|
163
|
-
result="SUCCESS",
|
|
164
|
-
serial_number=serial_number,
|
|
165
|
-
link_number=link_number_value,
|
|
166
|
-
sent_telegram=datapoint_response.sent_telegram,
|
|
167
|
-
received_telegrams=datapoint_response.received_telegrams,
|
|
168
|
-
timestamp=datapoint_response.timestamp,
|
|
169
|
-
)
|
|
170
|
-
except (ValueError, TypeError) as e:
|
|
171
|
-
return ConbusLinknumberResponse(
|
|
172
|
-
success=False,
|
|
173
|
-
result="PARSE_ERROR",
|
|
174
|
-
serial_number=serial_number,
|
|
175
|
-
sent_telegram=datapoint_response.sent_telegram,
|
|
176
|
-
received_telegrams=datapoint_response.received_telegrams,
|
|
177
|
-
error=f"Failed to parse link number: {e}",
|
|
178
|
-
timestamp=datapoint_response.timestamp,
|
|
179
|
-
)
|
|
180
|
-
else:
|
|
181
|
-
return ConbusLinknumberResponse(
|
|
182
|
-
success=False,
|
|
183
|
-
result="QUERY_FAILED",
|
|
184
|
-
serial_number=serial_number,
|
|
185
|
-
sent_telegram=datapoint_response.sent_telegram,
|
|
186
|
-
received_telegrams=datapoint_response.received_telegrams,
|
|
187
|
-
error=datapoint_response.error or "Failed to query link number",
|
|
188
|
-
timestamp=datapoint_response.timestamp,
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
except Exception as e:
|
|
192
|
-
return ConbusLinknumberResponse(
|
|
193
|
-
success=False,
|
|
194
|
-
result="ERROR",
|
|
195
|
-
serial_number=serial_number,
|
|
196
|
-
error=f"Unexpected error: {e}",
|
|
197
|
-
)
|
|
@@ -1,306 +0,0 @@
|
|
|
1
|
-
"""Conbus Client Send Service for TCP communication with Conbus servers.
|
|
2
|
-
|
|
3
|
-
This service implements a TCP client that connects to Conbus servers and sends
|
|
4
|
-
various types of telegrams including discover, version, and sensor data requests.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import logging
|
|
8
|
-
import socket
|
|
9
|
-
from datetime import datetime
|
|
10
|
-
from typing import Any, List, Optional
|
|
11
|
-
|
|
12
|
-
from typing_extensions import Callable
|
|
13
|
-
|
|
14
|
-
from xp.models import (
|
|
15
|
-
ConbusConnectionStatus,
|
|
16
|
-
ConbusRequest,
|
|
17
|
-
ConbusResponse,
|
|
18
|
-
)
|
|
19
|
-
from xp.models.conbus.conbus_client_config import ConbusClientConfig
|
|
20
|
-
from xp.models.response import Response
|
|
21
|
-
from xp.models.telegram.system_function import SystemFunction
|
|
22
|
-
from xp.services.conbus.conbus_connection_pool import ConbusConnectionPool
|
|
23
|
-
from xp.utils.checksum import calculate_checksum
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class ConbusError(Exception):
|
|
27
|
-
"""Raised when Conbus client send operations fail"""
|
|
28
|
-
|
|
29
|
-
pass
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class ConbusService:
|
|
33
|
-
"""
|
|
34
|
-
TCP client service for sending telegrams to Conbus servers.
|
|
35
|
-
|
|
36
|
-
Manages TCP socket connections, handles telegram generation and transmission,
|
|
37
|
-
and processes server responses.
|
|
38
|
-
"""
|
|
39
|
-
|
|
40
|
-
def __init__(
|
|
41
|
-
self,
|
|
42
|
-
client_config: ConbusClientConfig,
|
|
43
|
-
connection_pool: ConbusConnectionPool,
|
|
44
|
-
):
|
|
45
|
-
"""Initialize the Conbus client send service
|
|
46
|
-
|
|
47
|
-
Args:
|
|
48
|
-
client_config: ConbusClientConfig for dependency injection
|
|
49
|
-
connection_pool: ConbusConnectionPool for dependency injection
|
|
50
|
-
"""
|
|
51
|
-
self.client_config: ConbusClientConfig = client_config
|
|
52
|
-
self.is_connected = False
|
|
53
|
-
self.last_activity: Optional[datetime] = None
|
|
54
|
-
|
|
55
|
-
# Set up logging
|
|
56
|
-
self.logger = logging.getLogger(__name__)
|
|
57
|
-
|
|
58
|
-
# Use injected connection pool
|
|
59
|
-
self._connection_pool = connection_pool
|
|
60
|
-
|
|
61
|
-
def get_config(self) -> ConbusClientConfig:
|
|
62
|
-
"""Get current client configuration"""
|
|
63
|
-
return self.client_config
|
|
64
|
-
|
|
65
|
-
def connect(self) -> Response:
|
|
66
|
-
"""Test connection using the connection pool"""
|
|
67
|
-
try:
|
|
68
|
-
# Test connection by acquiring and immediately releasing
|
|
69
|
-
with self._connection_pool:
|
|
70
|
-
self.is_connected = True
|
|
71
|
-
self.last_activity = datetime.now()
|
|
72
|
-
|
|
73
|
-
self.logger.info(
|
|
74
|
-
f"Connection pool ready for {self.client_config.conbus.ip}:{self.client_config.conbus.port}"
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
return Response(
|
|
78
|
-
success=True,
|
|
79
|
-
data={
|
|
80
|
-
"message": f"Connection pool ready for {self.client_config.conbus.ip}:{self.client_config.conbus.port}",
|
|
81
|
-
},
|
|
82
|
-
error=None,
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
except Exception as e:
|
|
86
|
-
error_msg = f"Failed to establish connection pool to {self.client_config.conbus.ip}:{self.client_config.conbus.port}: {e}"
|
|
87
|
-
self.logger.error(error_msg)
|
|
88
|
-
self.is_connected = False
|
|
89
|
-
return Response(success=False, data=None, error=error_msg)
|
|
90
|
-
|
|
91
|
-
def disconnect(self) -> None:
|
|
92
|
-
"""Close connection pool (graceful shutdown)"""
|
|
93
|
-
try:
|
|
94
|
-
self._connection_pool.close()
|
|
95
|
-
self.logger.info("Connection pool closed")
|
|
96
|
-
except Exception as e:
|
|
97
|
-
self.logger.error(f"Error closing connection pool: {e}")
|
|
98
|
-
finally:
|
|
99
|
-
self.is_connected = False
|
|
100
|
-
|
|
101
|
-
def get_connection_status(self) -> ConbusConnectionStatus:
|
|
102
|
-
"""Get current connection status"""
|
|
103
|
-
return ConbusConnectionStatus(
|
|
104
|
-
connected=self.is_connected,
|
|
105
|
-
ip=self.client_config.conbus.ip,
|
|
106
|
-
port=self.client_config.conbus.port,
|
|
107
|
-
last_activity=self.last_activity,
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
@staticmethod
|
|
111
|
-
def _parse_telegrams(raw_data: str) -> List[str]:
|
|
112
|
-
"""Parse raw data and extract telegrams using < and > delimiters"""
|
|
113
|
-
telegrams: list[str] = []
|
|
114
|
-
if not raw_data:
|
|
115
|
-
return telegrams
|
|
116
|
-
|
|
117
|
-
# Find all telegram patterns <...>
|
|
118
|
-
start_pos = 0
|
|
119
|
-
while True:
|
|
120
|
-
# Find the start of next telegram
|
|
121
|
-
start_idx = raw_data.find("<", start_pos)
|
|
122
|
-
if start_idx == -1:
|
|
123
|
-
break
|
|
124
|
-
|
|
125
|
-
# Find the end of this telegram
|
|
126
|
-
end_idx = raw_data.find(">", start_idx)
|
|
127
|
-
if end_idx == -1:
|
|
128
|
-
# Incomplete telegram at the end
|
|
129
|
-
break
|
|
130
|
-
|
|
131
|
-
# Extract telegram including < and >
|
|
132
|
-
telegram = raw_data[start_idx : end_idx + 1]
|
|
133
|
-
if telegram.strip():
|
|
134
|
-
telegrams.append(telegram.strip())
|
|
135
|
-
|
|
136
|
-
start_pos = end_idx + 1
|
|
137
|
-
|
|
138
|
-
return telegrams
|
|
139
|
-
|
|
140
|
-
def receive_responses(
|
|
141
|
-
self,
|
|
142
|
-
timeout: float = 0.1,
|
|
143
|
-
receive_callback: Optional[Callable[[list[str]], None]] = None,
|
|
144
|
-
) -> List[str]:
|
|
145
|
-
"""Receive responses from the server and properly split telegrams"""
|
|
146
|
-
import time
|
|
147
|
-
|
|
148
|
-
all_telegrams = []
|
|
149
|
-
start_time = time.time()
|
|
150
|
-
|
|
151
|
-
try:
|
|
152
|
-
with self._connection_pool as connection:
|
|
153
|
-
while time.time() - start_time < timeout:
|
|
154
|
-
# Call _receive_responses_with_connection with a short timeout for each iteration
|
|
155
|
-
telegrams = self._receive_responses_with_connection(
|
|
156
|
-
connection, timeout=0.1
|
|
157
|
-
)
|
|
158
|
-
if receive_callback is not None:
|
|
159
|
-
receive_callback(telegrams)
|
|
160
|
-
all_telegrams.extend(telegrams)
|
|
161
|
-
|
|
162
|
-
# Small sleep to avoid busy waiting when no data
|
|
163
|
-
time.sleep(0.01)
|
|
164
|
-
except Exception as e:
|
|
165
|
-
self.logger.error(f"Error receiving responses: {e}")
|
|
166
|
-
return []
|
|
167
|
-
|
|
168
|
-
return all_telegrams
|
|
169
|
-
|
|
170
|
-
def _receive_responses_with_connection(
|
|
171
|
-
self,
|
|
172
|
-
connection: socket.socket,
|
|
173
|
-
timeout: float = 1.0,
|
|
174
|
-
receive_callback: Optional[Callable[[list[str]], None]] = None,
|
|
175
|
-
) -> List[str]:
|
|
176
|
-
"""Receive responses from the server using a specific connection"""
|
|
177
|
-
accumulated_data = ""
|
|
178
|
-
|
|
179
|
-
try:
|
|
180
|
-
# Set a shorter timeout for receiving responses
|
|
181
|
-
original_timeout = connection.gettimeout()
|
|
182
|
-
connection.settimeout(timeout) # 1 second timeout for responses
|
|
183
|
-
|
|
184
|
-
while True:
|
|
185
|
-
try:
|
|
186
|
-
data = connection.recv(1024)
|
|
187
|
-
if not data:
|
|
188
|
-
break
|
|
189
|
-
|
|
190
|
-
# Accumulate all received data
|
|
191
|
-
message = data.decode("latin-1")
|
|
192
|
-
if receive_callback is not None:
|
|
193
|
-
parsed_telegrams = self._parse_telegrams(message)
|
|
194
|
-
receive_callback(parsed_telegrams)
|
|
195
|
-
accumulated_data += message
|
|
196
|
-
self.last_activity = datetime.now()
|
|
197
|
-
connection.settimeout(0.1) # 2 second timeout for responses
|
|
198
|
-
|
|
199
|
-
except socket.timeout:
|
|
200
|
-
# No more data available
|
|
201
|
-
break
|
|
202
|
-
|
|
203
|
-
# Restore original timeout
|
|
204
|
-
connection.settimeout(original_timeout)
|
|
205
|
-
|
|
206
|
-
except Exception as e:
|
|
207
|
-
self.logger.error(f"Error receiving responses: {e}")
|
|
208
|
-
|
|
209
|
-
# Parse telegrams from accumulated data
|
|
210
|
-
telegrams = self._parse_telegrams(accumulated_data)
|
|
211
|
-
for telegram in telegrams:
|
|
212
|
-
self.logger.info(f"Received telegram: {telegram}")
|
|
213
|
-
|
|
214
|
-
return telegrams
|
|
215
|
-
|
|
216
|
-
def send_telegram(
|
|
217
|
-
self,
|
|
218
|
-
serial_number: str,
|
|
219
|
-
system_function: SystemFunction,
|
|
220
|
-
data: str,
|
|
221
|
-
receive_callback: Optional[Callable[[list[str]], None]] = None,
|
|
222
|
-
) -> ConbusResponse:
|
|
223
|
-
"""Send custom telegram with specified function and data point codes"""
|
|
224
|
-
# Generate custom system telegram: <S{serial}F{function}{data_point}{checksum}>
|
|
225
|
-
function_code = system_function.value
|
|
226
|
-
telegram_body = f"S{serial_number}F{function_code}D{data}"
|
|
227
|
-
checksum = calculate_checksum(telegram_body)
|
|
228
|
-
telegram = f"<{telegram_body}{checksum}>"
|
|
229
|
-
|
|
230
|
-
return self.send_raw_telegram(telegram, receive_callback)
|
|
231
|
-
|
|
232
|
-
def send_telegram_body(
|
|
233
|
-
self,
|
|
234
|
-
telegram_body: str,
|
|
235
|
-
receive_callback: Optional[Callable[[list[str]], None]] = None,
|
|
236
|
-
) -> ConbusResponse:
|
|
237
|
-
"""Send custom telegram with specified function and data point codes"""
|
|
238
|
-
checksum = calculate_checksum(telegram_body)
|
|
239
|
-
telegram = f"<{telegram_body}{checksum}>"
|
|
240
|
-
|
|
241
|
-
return self.send_raw_telegram(telegram, receive_callback)
|
|
242
|
-
|
|
243
|
-
def send_raw_telegram(
|
|
244
|
-
self,
|
|
245
|
-
telegram: Optional[str] = None,
|
|
246
|
-
receive_callback: Optional[Callable[[list[str]], None]] = None,
|
|
247
|
-
) -> ConbusResponse:
|
|
248
|
-
"""Send telegram using connection pool with automatic acquire/release"""
|
|
249
|
-
request = ConbusRequest(telegram=telegram)
|
|
250
|
-
|
|
251
|
-
try:
|
|
252
|
-
# Use context manager for automatic connection management
|
|
253
|
-
with self._connection_pool as connection:
|
|
254
|
-
|
|
255
|
-
# Draining event waiting to be read (wait time 0)
|
|
256
|
-
responses = self._receive_responses_with_connection(connection, 0.001)
|
|
257
|
-
self.logger.info(f"Purged telegram: {responses}")
|
|
258
|
-
|
|
259
|
-
# Send telegram
|
|
260
|
-
if telegram is not None:
|
|
261
|
-
connection.send(telegram.encode("latin-1"))
|
|
262
|
-
self.logger.info(f"Sent telegram: {telegram}")
|
|
263
|
-
|
|
264
|
-
self.last_activity = datetime.now()
|
|
265
|
-
self.is_connected = True # Update connection status
|
|
266
|
-
|
|
267
|
-
# Receive responses
|
|
268
|
-
responses = self._receive_responses_with_connection(
|
|
269
|
-
connection, 0.1, receive_callback
|
|
270
|
-
)
|
|
271
|
-
|
|
272
|
-
return ConbusResponse(
|
|
273
|
-
success=True,
|
|
274
|
-
request=request,
|
|
275
|
-
sent_telegram=telegram,
|
|
276
|
-
received_telegrams=responses,
|
|
277
|
-
)
|
|
278
|
-
# Connection automatically released here
|
|
279
|
-
|
|
280
|
-
except Exception as e:
|
|
281
|
-
error_msg = f"Failed to send telegram: {e}"
|
|
282
|
-
self.logger.error(error_msg)
|
|
283
|
-
self.is_connected = False # Update connection status on error
|
|
284
|
-
return ConbusResponse(
|
|
285
|
-
success=False,
|
|
286
|
-
request=request,
|
|
287
|
-
error=error_msg,
|
|
288
|
-
)
|
|
289
|
-
|
|
290
|
-
def send_raw_telegrams(self, telegrams: List[str]) -> ConbusResponse:
|
|
291
|
-
self.logger.info(f"send_raw_telegrams: {telegrams}")
|
|
292
|
-
all_telegrams = "".join(telegrams)
|
|
293
|
-
return self.send_raw_telegram(all_telegrams)
|
|
294
|
-
|
|
295
|
-
def __enter__(self) -> "ConbusService":
|
|
296
|
-
"""Context manager entry"""
|
|
297
|
-
return self
|
|
298
|
-
|
|
299
|
-
def __exit__(
|
|
300
|
-
self,
|
|
301
|
-
_exc_type: Optional[type],
|
|
302
|
-
_exc_val: Optional[Exception],
|
|
303
|
-
_exc_tb: Optional[Any],
|
|
304
|
-
) -> None:
|
|
305
|
-
"""Context manager exit - ensure connection is closed"""
|
|
306
|
-
self.disconnect()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|