cgse-common 0.16.14__py3-none-any.whl → 0.17.1__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.
- cgse_common/cgse.py +5 -1
- {cgse_common-0.16.14.dist-info → cgse_common-0.17.1.dist-info}/METADATA +1 -1
- {cgse_common-0.16.14.dist-info → cgse_common-0.17.1.dist-info}/RECORD +16 -16
- {cgse_common-0.16.14.dist-info → cgse_common-0.17.1.dist-info}/entry_points.txt +0 -1
- egse/config.py +3 -2
- egse/decorators.py +2 -1
- egse/device.py +8 -2
- egse/env.py +117 -49
- egse/heartbeat.py +1 -1
- egse/log.py +9 -2
- egse/plugins/metrics/influxdb.py +34 -2
- egse/scpi.py +63 -47
- egse/settings.py +11 -7
- egse/setup.py +12 -6
- egse/socketdevice.py +212 -38
- {cgse_common-0.16.14.dist-info → cgse_common-0.17.1.dist-info}/WHEEL +0 -0
egse/socketdevice.py
CHANGED
|
@@ -2,29 +2,52 @@
|
|
|
2
2
|
This module defines base classes and generic functions to work with sockets.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
import asyncio
|
|
6
|
+
import select
|
|
5
7
|
import socket
|
|
8
|
+
import time
|
|
9
|
+
from typing import Optional
|
|
6
10
|
|
|
11
|
+
from egse.device import AsyncDeviceInterface
|
|
12
|
+
from egse.device import AsyncDeviceTransport
|
|
7
13
|
from egse.device import DeviceConnectionError
|
|
8
14
|
from egse.device import DeviceConnectionInterface
|
|
9
15
|
from egse.device import DeviceTimeoutError
|
|
10
16
|
from egse.device import DeviceTransport
|
|
11
17
|
from egse.log import logger
|
|
18
|
+
from egse.system import type_name
|
|
12
19
|
|
|
13
20
|
|
|
14
21
|
class SocketDevice(DeviceConnectionInterface, DeviceTransport):
|
|
15
22
|
"""Base class that implements the socket interface."""
|
|
16
23
|
|
|
17
|
-
|
|
24
|
+
# We set a default connect timeout of 3.0 sec before connecting and reset
|
|
25
|
+
# to None (=blocking) after connecting. The reason for this is that when no
|
|
26
|
+
# device is available, e.g. during testing, the timeout will take about
|
|
27
|
+
# two minutes which is way too long. It needs to be evaluated if this
|
|
28
|
+
# approach is acceptable and not causing problems during production.
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
hostname: str,
|
|
33
|
+
port: int,
|
|
34
|
+
connect_timeout: float = 3.0,
|
|
35
|
+
read_timeout: float | None = 1.0,
|
|
36
|
+
separator: str = b"\x03",
|
|
37
|
+
):
|
|
18
38
|
super().__init__()
|
|
19
39
|
self.is_connection_open = False
|
|
20
40
|
self.hostname = hostname
|
|
21
41
|
self.port = port
|
|
42
|
+
self.connect_timeout = connect_timeout
|
|
43
|
+
self.read_timeout = read_timeout
|
|
44
|
+
self.separator = separator
|
|
22
45
|
self.socket = None
|
|
23
46
|
|
|
24
47
|
@property
|
|
25
48
|
def device_name(self):
|
|
26
49
|
"""The name of the device that this interface connects to."""
|
|
27
|
-
|
|
50
|
+
return f"SocketDevice({self.hostname}:{self.port})"
|
|
28
51
|
|
|
29
52
|
def connect(self):
|
|
30
53
|
"""
|
|
@@ -53,18 +76,12 @@ class SocketDevice(DeviceConnectionInterface, DeviceTransport):
|
|
|
53
76
|
|
|
54
77
|
try:
|
|
55
78
|
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
56
|
-
except socket.error as
|
|
57
|
-
raise ConnectionError(f"{self.device_name}: Failed to create socket.") from
|
|
58
|
-
|
|
59
|
-
# We set a timeout of 3 sec before connecting and reset to None
|
|
60
|
-
# (=blocking) after the connect. The reason for this is because when no
|
|
61
|
-
# device is available, e.g during testing, the timeout will take about
|
|
62
|
-
# two minutes which is way too long. It needs to be evaluated if this
|
|
63
|
-
# approach is acceptable and not causing problems during production.
|
|
79
|
+
except socket.error as exc:
|
|
80
|
+
raise ConnectionError(f"{self.device_name}: Failed to create socket.") from exc
|
|
64
81
|
|
|
65
82
|
try:
|
|
66
83
|
logger.debug(f'Connecting a socket to host "{self.hostname}" using port {self.port}')
|
|
67
|
-
self.socket.settimeout(
|
|
84
|
+
self.socket.settimeout(self.connect_timeout)
|
|
68
85
|
self.socket.connect((self.hostname, self.port))
|
|
69
86
|
self.socket.settimeout(None)
|
|
70
87
|
except ConnectionRefusedError as exc:
|
|
@@ -120,32 +137,64 @@ class SocketDevice(DeviceConnectionInterface, DeviceTransport):
|
|
|
120
137
|
|
|
121
138
|
def read(self) -> bytes:
|
|
122
139
|
"""
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
140
|
+
Read until ETX (b'\x03') or until `self.read_timeout` elapses.
|
|
141
|
+
Uses `select` to avoid blocking indefinitely when no data is available.
|
|
142
|
+
If `self.read_timeout` was set to None in the constructor, this will block anyway.
|
|
126
143
|
"""
|
|
127
|
-
|
|
144
|
+
if not self.socket:
|
|
145
|
+
raise DeviceConnectionError(self.device_name, "Not connected")
|
|
146
|
+
|
|
128
147
|
buf_size = 1024 * 4
|
|
129
|
-
response =
|
|
148
|
+
response = bytearray()
|
|
149
|
+
|
|
150
|
+
# If read_timeout is None we preserve blocking behaviour; otherwise enforce overall timeout.
|
|
151
|
+
if self.read_timeout is None:
|
|
152
|
+
end_time = None
|
|
153
|
+
else:
|
|
154
|
+
end_time = time.monotonic() + self.read_timeout
|
|
130
155
|
|
|
131
156
|
try:
|
|
132
|
-
|
|
133
|
-
#
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
157
|
+
while True:
|
|
158
|
+
# compute remaining timeout for select, this is needed because we read in different parts
|
|
159
|
+
# until ETX is received, and we want to receive the complete messages including ETX within
|
|
160
|
+
# the read timeout.
|
|
161
|
+
if end_time is None:
|
|
162
|
+
timeout = None
|
|
163
|
+
else:
|
|
164
|
+
remaining = end_time - time.monotonic()
|
|
165
|
+
if remaining <= 0.0:
|
|
166
|
+
raise DeviceTimeoutError(self.device_name, "Socket read timed out")
|
|
167
|
+
timeout = remaining
|
|
168
|
+
|
|
169
|
+
ready, _, _ = select.select([self.socket], [], [], timeout)
|
|
170
|
+
|
|
171
|
+
if not ready:
|
|
172
|
+
# no socket ready within timeout
|
|
173
|
+
raise DeviceTimeoutError(self.device_name, "Socket read timed out")
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
data = self.socket.recv(buf_size)
|
|
177
|
+
except OSError as exc:
|
|
178
|
+
raise DeviceConnectionError(self.device_name, f"Caught {type_name(exc)}: {exc}") from exc
|
|
179
|
+
|
|
180
|
+
if not data:
|
|
181
|
+
# remote closed connection (EOF)
|
|
182
|
+
raise DeviceConnectionError(self.device_name, "Connection closed by peer")
|
|
183
|
+
|
|
184
|
+
response.extend(data)
|
|
185
|
+
|
|
186
|
+
if self.separator in response:
|
|
141
187
|
break
|
|
142
|
-
except socket.timeout as e_timeout:
|
|
143
|
-
logger.warning(f"Socket timeout error from {e_timeout}")
|
|
144
|
-
raise DeviceTimeoutError(self.device_name, "Socket timeout error") from e_timeout
|
|
145
188
|
|
|
146
|
-
|
|
189
|
+
except DeviceTimeoutError:
|
|
190
|
+
raise
|
|
191
|
+
except DeviceConnectionError:
|
|
192
|
+
raise
|
|
193
|
+
except Exception as exc:
|
|
194
|
+
# unexpected errors - translate to DeviceConnectionError
|
|
195
|
+
raise DeviceConnectionError(self.device_name, "Socket read error") from exc
|
|
147
196
|
|
|
148
|
-
return response
|
|
197
|
+
return bytes(response)
|
|
149
198
|
|
|
150
199
|
def write(self, command: str):
|
|
151
200
|
"""
|
|
@@ -163,11 +212,11 @@ class SocketDevice(DeviceConnectionInterface, DeviceTransport):
|
|
|
163
212
|
|
|
164
213
|
try:
|
|
165
214
|
self.socket.sendall(command.encode())
|
|
166
|
-
except socket.timeout as
|
|
167
|
-
raise DeviceTimeoutError(self.device_name, "Socket timeout error") from
|
|
168
|
-
except socket.error as
|
|
215
|
+
except socket.timeout as exc:
|
|
216
|
+
raise DeviceTimeoutError(self.device_name, "Socket timeout error") from exc
|
|
217
|
+
except socket.error as exc:
|
|
169
218
|
# Interpret any socket-related error as an I/O error
|
|
170
|
-
raise DeviceConnectionError(self.device_name, "Socket communication error.") from
|
|
219
|
+
raise DeviceConnectionError(self.device_name, "Socket communication error.") from exc
|
|
171
220
|
|
|
172
221
|
def trans(self, command: str) -> bytes:
|
|
173
222
|
"""
|
|
@@ -198,8 +247,133 @@ class SocketDevice(DeviceConnectionInterface, DeviceTransport):
|
|
|
198
247
|
|
|
199
248
|
return return_string
|
|
200
249
|
|
|
201
|
-
except socket.timeout as
|
|
202
|
-
raise DeviceTimeoutError(self.device_name, "Socket timeout error") from
|
|
203
|
-
except socket.error as
|
|
250
|
+
except socket.timeout as exc:
|
|
251
|
+
raise DeviceTimeoutError(self.device_name, "Socket timeout error") from exc
|
|
252
|
+
except socket.error as exc:
|
|
204
253
|
# Interpret any socket-related error as an I/O error
|
|
205
|
-
raise DeviceConnectionError(self.device_name, "Socket communication error.") from
|
|
254
|
+
raise DeviceConnectionError(self.device_name, "Socket communication error.") from exc
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class AsyncSocketDevice(AsyncDeviceInterface, AsyncDeviceTransport):
|
|
258
|
+
"""
|
|
259
|
+
Async socket-backed device using asyncio streams.
|
|
260
|
+
|
|
261
|
+
- async connect() / disconnect()
|
|
262
|
+
- async read() reads until ETX (b'\\x03') or timeout
|
|
263
|
+
- async write() and async trans()
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
def __init__(
|
|
267
|
+
self,
|
|
268
|
+
hostname: str,
|
|
269
|
+
port: int,
|
|
270
|
+
connect_timeout: float = 3.0,
|
|
271
|
+
read_timeout: float | None = 1.0,
|
|
272
|
+
separator: str = b"\x03",
|
|
273
|
+
):
|
|
274
|
+
super().__init__()
|
|
275
|
+
self.hostname = hostname
|
|
276
|
+
self.port = port
|
|
277
|
+
self.connect_timeout = connect_timeout
|
|
278
|
+
self.read_timeout = read_timeout
|
|
279
|
+
self.separator = separator
|
|
280
|
+
self.reader: Optional[asyncio.StreamReader] = None
|
|
281
|
+
self.writer: Optional[asyncio.StreamWriter] = None
|
|
282
|
+
self.is_connection_open = False
|
|
283
|
+
|
|
284
|
+
@property
|
|
285
|
+
def device_name(self) -> str:
|
|
286
|
+
# Override this property for a decent name
|
|
287
|
+
return f"AsyncSocketDevice({self.hostname}:{self.port})"
|
|
288
|
+
|
|
289
|
+
async def connect(self) -> None:
|
|
290
|
+
if self.is_connection_open:
|
|
291
|
+
logger.debug(f"{self.device_name}: already connected")
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
if not self.hostname:
|
|
295
|
+
raise ValueError(f"{self.device_name}: hostname is not initialized.")
|
|
296
|
+
if not self.port:
|
|
297
|
+
raise ValueError(f"{self.device_name}: port is not initialized.")
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
logger.debug(f"{self.device_name}: connect() called; is_connection_open={self.is_connection_open}")
|
|
301
|
+
coro = asyncio.open_connection(self.hostname, self.port)
|
|
302
|
+
self.reader, self.writer = await asyncio.wait_for(coro, timeout=self.connect_timeout)
|
|
303
|
+
self.is_connection_open = True
|
|
304
|
+
logger.debug(f"{self.device_name}: connected -> peer={self.writer.get_extra_info('peername')}")
|
|
305
|
+
|
|
306
|
+
except asyncio.TimeoutError as exc:
|
|
307
|
+
await self._cleanup()
|
|
308
|
+
logger.warning(f"{self.device_name}: connect timed out")
|
|
309
|
+
raise DeviceTimeoutError(self.device_name, f"Connection to {self.hostname}:{self.port} timed out.") from exc
|
|
310
|
+
except Exception as exc:
|
|
311
|
+
await self._cleanup()
|
|
312
|
+
logger.warning(f"{self.device_name}: connect failed: {type_name(exc)} – {exc}")
|
|
313
|
+
raise DeviceConnectionError(self.device_name, f"Failed to connect to {self.hostname}:{self.port}") from exc
|
|
314
|
+
|
|
315
|
+
async def disconnect(self) -> None:
|
|
316
|
+
logger.debug(f"{self.device_name}: disconnect() called; writer_exists={bool(self.writer)}")
|
|
317
|
+
peer = None
|
|
318
|
+
try:
|
|
319
|
+
if self.writer and not self.writer.is_closing():
|
|
320
|
+
peer = self.writer.get_extra_info("peername")
|
|
321
|
+
self.writer.close()
|
|
322
|
+
# wait for close, but don't hang forever
|
|
323
|
+
try:
|
|
324
|
+
await asyncio.wait_for(self.writer.wait_closed(), timeout=1.0)
|
|
325
|
+
except asyncio.TimeoutError:
|
|
326
|
+
logger.debug(f"{self.device_name}: wait_closed() timed out for peer={peer}")
|
|
327
|
+
|
|
328
|
+
finally:
|
|
329
|
+
await self._cleanup()
|
|
330
|
+
logger.debug(f"{self.device_name}: disconnected ({peer=})")
|
|
331
|
+
|
|
332
|
+
def is_connected(self) -> bool:
|
|
333
|
+
return bool(self.is_connection_open and self.writer and not self.writer.is_closing())
|
|
334
|
+
|
|
335
|
+
async def _cleanup(self) -> None:
|
|
336
|
+
self.reader = None
|
|
337
|
+
self.writer = None
|
|
338
|
+
self.is_connection_open = False
|
|
339
|
+
|
|
340
|
+
async def read(self) -> bytes:
|
|
341
|
+
if not self.reader:
|
|
342
|
+
raise DeviceConnectionError(self.device_name, "Not connected")
|
|
343
|
+
try:
|
|
344
|
+
# readuntil includes the separator; we keep it for parity with existing code
|
|
345
|
+
data = await asyncio.wait_for(self.reader.readuntil(separator=self.separator), timeout=self.read_timeout)
|
|
346
|
+
return data
|
|
347
|
+
except asyncio.IncompleteReadError as exc:
|
|
348
|
+
# EOF before separator
|
|
349
|
+
await self._cleanup()
|
|
350
|
+
raise DeviceConnectionError(self.device_name, "Connection closed while reading") from exc
|
|
351
|
+
except asyncio.TimeoutError as exc:
|
|
352
|
+
raise DeviceTimeoutError(self.device_name, "Socket read timed out") from exc
|
|
353
|
+
except Exception as exc:
|
|
354
|
+
await self._cleanup()
|
|
355
|
+
raise DeviceConnectionError(self.device_name, "Socket read error") from exc
|
|
356
|
+
|
|
357
|
+
async def write(self, command: str) -> None:
|
|
358
|
+
if not self.writer:
|
|
359
|
+
raise DeviceConnectionError(self.device_name, "Not connected")
|
|
360
|
+
try:
|
|
361
|
+
self.writer.write(command.encode())
|
|
362
|
+
await asyncio.wait_for(self.writer.drain(), timeout=self.read_timeout)
|
|
363
|
+
except asyncio.TimeoutError as exc:
|
|
364
|
+
raise DeviceTimeoutError(self.device_name, "Socket write timed out") from exc
|
|
365
|
+
except Exception as exc:
|
|
366
|
+
await self._cleanup()
|
|
367
|
+
raise DeviceConnectionError(self.device_name, "Socket write error") from exc
|
|
368
|
+
|
|
369
|
+
async def trans(self, command: str) -> bytes:
|
|
370
|
+
if not self.writer or not self.reader:
|
|
371
|
+
raise DeviceConnectionError(self.device_name, "Not connected")
|
|
372
|
+
try:
|
|
373
|
+
await self.write(command)
|
|
374
|
+
return await self.read()
|
|
375
|
+
except (DeviceTimeoutError, DeviceConnectionError):
|
|
376
|
+
raise
|
|
377
|
+
except Exception as exc:
|
|
378
|
+
await self._cleanup()
|
|
379
|
+
raise DeviceConnectionError(self.device_name, "Socket trans error") from exc
|
|
File without changes
|