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/scpi.py
CHANGED
|
@@ -15,6 +15,9 @@ DEFAULT_READ_TIMEOUT = 1.0 # seconds
|
|
|
15
15
|
DEFAULT_CONNECT_TIMEOUT = 3.0 # seconds
|
|
16
16
|
IDENTIFICATION_QUERY = "*IDN?"
|
|
17
17
|
|
|
18
|
+
SEPARATOR = b"\n"
|
|
19
|
+
SEPARATOR_STR = SEPARATOR.decode()
|
|
20
|
+
|
|
18
21
|
|
|
19
22
|
class SCPICommand:
|
|
20
23
|
"""Base class for SCPI commands."""
|
|
@@ -57,7 +60,8 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
|
|
|
57
60
|
id_validation: String that should appear in the device's identification response
|
|
58
61
|
"""
|
|
59
62
|
super().__init__()
|
|
60
|
-
|
|
63
|
+
|
|
64
|
+
self._device_name = device_name
|
|
61
65
|
self.hostname = hostname
|
|
62
66
|
self.port = port
|
|
63
67
|
self.settings = settings or {}
|
|
@@ -77,7 +81,11 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
|
|
|
77
81
|
def is_simulator(self) -> bool:
|
|
78
82
|
return False
|
|
79
83
|
|
|
80
|
-
|
|
84
|
+
@property
|
|
85
|
+
def device_name(self) -> str:
|
|
86
|
+
return self._device_name
|
|
87
|
+
|
|
88
|
+
async def initialize(self, commands: list[tuple[str, bool]] = None, reset_device: bool = False) -> list[str | None]:
|
|
81
89
|
"""Initialize the device with optional reset and command sequence.
|
|
82
90
|
|
|
83
91
|
Performs device initialization by optionally resetting the device and then
|
|
@@ -92,34 +100,42 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
|
|
|
92
100
|
the command sequence. Defaults to False.
|
|
93
101
|
|
|
94
102
|
Returns:
|
|
95
|
-
|
|
103
|
+
Response for each of the commands, or None when no response was expected.
|
|
96
104
|
|
|
97
105
|
Raises:
|
|
98
106
|
Any exceptions raised by the underlying write() or trans() methods,
|
|
99
107
|
typically communication errors or device timeouts.
|
|
100
108
|
|
|
101
109
|
Example:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
110
|
+
await device.initialize(
|
|
111
|
+
[
|
|
112
|
+
("*IDN?", True), # Query device ID, expect response
|
|
113
|
+
("SYST:ERR?", True), # Check for errors, expect response
|
|
114
|
+
("OUTP ON", False), # Enable output, no response expected
|
|
115
|
+
],
|
|
116
|
+
reset_device=True
|
|
117
|
+
)
|
|
107
118
|
"""
|
|
108
119
|
|
|
109
120
|
commands = commands or []
|
|
121
|
+
responses = []
|
|
110
122
|
|
|
111
123
|
if reset_device:
|
|
112
|
-
logger.info(f"Resetting the {self.
|
|
124
|
+
logger.info(f"Resetting the {self._device_name}...")
|
|
113
125
|
await self.write("*RST") # this also resets the user-defined buffer
|
|
114
126
|
|
|
115
127
|
for cmd, expects_response in commands:
|
|
116
128
|
if expects_response:
|
|
117
129
|
logger.debug(f"Sending {cmd}...")
|
|
118
130
|
response = (await self.trans(cmd)).decode().strip()
|
|
131
|
+
responses.append(response)
|
|
119
132
|
logger.debug(f"{response = }")
|
|
120
133
|
else:
|
|
121
134
|
logger.debug(f"Sending {cmd}...")
|
|
122
135
|
await self.write(cmd)
|
|
136
|
+
responses.append(None)
|
|
137
|
+
|
|
138
|
+
return responses
|
|
123
139
|
|
|
124
140
|
async def connect(self) -> None:
|
|
125
141
|
"""Connect to the device asynchronously.
|
|
@@ -132,47 +148,47 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
|
|
|
132
148
|
async with self._connect_lock:
|
|
133
149
|
# Sanity checks
|
|
134
150
|
if self._is_connection_open:
|
|
135
|
-
logger.warning(f"{self.
|
|
151
|
+
logger.warning(f"{self._device_name}: Trying to connect to an already connected device.")
|
|
136
152
|
return
|
|
137
153
|
|
|
138
154
|
if not self.hostname:
|
|
139
|
-
raise ValueError(f"{self.
|
|
155
|
+
raise ValueError(f"{self._device_name}: Hostname is not initialized.")
|
|
140
156
|
|
|
141
157
|
if not self.port:
|
|
142
|
-
raise ValueError(f"{self.
|
|
158
|
+
raise ValueError(f"{self._device_name}: Port number is not initialized.")
|
|
143
159
|
|
|
144
160
|
# Attempt to establish a connection
|
|
145
161
|
try:
|
|
146
|
-
logger.debug(f'Connecting to {self.
|
|
162
|
+
logger.debug(f'Connecting to {self._device_name} at "{self.hostname}" using port {self.port}')
|
|
147
163
|
|
|
148
164
|
connect_task = asyncio.open_connection(self.hostname, self.port)
|
|
149
165
|
self._reader, self._writer = await asyncio.wait_for(connect_task, timeout=self.connect_timeout)
|
|
150
166
|
|
|
151
167
|
self._is_connection_open = True
|
|
152
168
|
|
|
153
|
-
logger.debug(f"Successfully connected to {self.
|
|
169
|
+
logger.debug(f"Successfully connected to {self._device_name}.")
|
|
154
170
|
|
|
155
171
|
except asyncio.TimeoutError as exc:
|
|
156
172
|
raise DeviceTimeoutError(
|
|
157
|
-
self.
|
|
173
|
+
self._device_name, f"Connection to {self.hostname}:{self.port} timed out"
|
|
158
174
|
) from exc
|
|
159
175
|
except ConnectionRefusedError as exc:
|
|
160
176
|
raise DeviceConnectionError(
|
|
161
|
-
self.
|
|
177
|
+
self._device_name, f"Connection refused to {self.hostname}:{self.port}"
|
|
162
178
|
) from exc
|
|
163
179
|
except socket.gaierror as exc:
|
|
164
|
-
raise DeviceConnectionError(self.
|
|
180
|
+
raise DeviceConnectionError(self._device_name, f"Address resolution error for {self.hostname}") from exc
|
|
165
181
|
except socket.herror as exc:
|
|
166
|
-
raise DeviceConnectionError(self.
|
|
182
|
+
raise DeviceConnectionError(self._device_name, f"Host address error for {self.hostname}") from exc
|
|
167
183
|
except OSError as exc:
|
|
168
|
-
raise DeviceConnectionError(self.
|
|
184
|
+
raise DeviceConnectionError(self._device_name, f"OS error: {exc}") from exc
|
|
169
185
|
|
|
170
186
|
# Validate device identity if requested
|
|
171
187
|
if self.id_validation:
|
|
172
188
|
logger.debug("Validating connection..")
|
|
173
189
|
if not await self.is_connected():
|
|
174
190
|
await self.disconnect()
|
|
175
|
-
raise DeviceConnectionError(self.
|
|
191
|
+
raise DeviceConnectionError(self._device_name, "Device connected but failed identity verification")
|
|
176
192
|
|
|
177
193
|
async def disconnect(self) -> None:
|
|
178
194
|
"""Disconnect from the device asynchronously.
|
|
@@ -183,14 +199,14 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
|
|
|
183
199
|
async with self._connect_lock:
|
|
184
200
|
try:
|
|
185
201
|
if self._is_connection_open and self._writer is not None:
|
|
186
|
-
logger.debug(f"Disconnecting from {self.
|
|
202
|
+
logger.debug(f"Disconnecting from {self._device_name} at {self.hostname}")
|
|
187
203
|
self._writer.close()
|
|
188
204
|
await self._writer.wait_closed()
|
|
189
205
|
self._writer = None
|
|
190
206
|
self._reader = None
|
|
191
207
|
self._is_connection_open = False
|
|
192
208
|
except Exception as exc:
|
|
193
|
-
raise DeviceConnectionError(self.
|
|
209
|
+
raise DeviceConnectionError(self._device_name, f"Could not close connection: {exc}") from exc
|
|
194
210
|
|
|
195
211
|
async def reconnect(self) -> None:
|
|
196
212
|
"""Reconnect to the device asynchronously."""
|
|
@@ -214,7 +230,7 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
|
|
|
214
230
|
# Validate the response if validation string is provided
|
|
215
231
|
if self.id_validation and self.id_validation not in id_response:
|
|
216
232
|
logger.error(
|
|
217
|
-
f"{self.
|
|
233
|
+
f"{self._device_name}: Device did not respond correctly to identification query. "
|
|
218
234
|
f'Expected "{self.id_validation}" in response, got: {id_response}'
|
|
219
235
|
)
|
|
220
236
|
await self.disconnect()
|
|
@@ -223,7 +239,7 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
|
|
|
223
239
|
return True
|
|
224
240
|
|
|
225
241
|
except DeviceError as exc:
|
|
226
|
-
logger.error(f"{self.
|
|
242
|
+
logger.error(f"{self._device_name}: Connection test failed: {exc}", exc_info=True)
|
|
227
243
|
await self.disconnect()
|
|
228
244
|
return False
|
|
229
245
|
|
|
@@ -240,20 +256,20 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
|
|
|
240
256
|
async with self._io_lock:
|
|
241
257
|
try:
|
|
242
258
|
if not self._is_connection_open or self._writer is None:
|
|
243
|
-
raise DeviceConnectionError(self.
|
|
259
|
+
raise DeviceConnectionError(self._device_name, "Device not connected, use connect() first")
|
|
244
260
|
|
|
245
|
-
# Ensure command ends with
|
|
246
|
-
if not command.endswith(
|
|
247
|
-
command +=
|
|
261
|
+
# Ensure command ends with the proper separator or terminator
|
|
262
|
+
if not command.endswith(SEPARATOR_STR):
|
|
263
|
+
command += SEPARATOR_STR
|
|
248
264
|
|
|
249
265
|
logger.info(f"-----> {command}")
|
|
250
266
|
self._writer.write(command.encode())
|
|
251
267
|
await self._writer.drain()
|
|
252
268
|
|
|
253
269
|
except asyncio.TimeoutError as exc:
|
|
254
|
-
raise DeviceTimeoutError(self.
|
|
270
|
+
raise DeviceTimeoutError(self._device_name, "Write operation timed out") from exc
|
|
255
271
|
except (ConnectionError, OSError) as exc:
|
|
256
|
-
raise DeviceConnectionError(self.
|
|
272
|
+
raise DeviceConnectionError(self._device_name, f"Communication error: {exc}") from exc
|
|
257
273
|
|
|
258
274
|
async def read(self) -> bytes:
|
|
259
275
|
"""
|
|
@@ -268,7 +284,7 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
|
|
|
268
284
|
"""
|
|
269
285
|
async with self._io_lock:
|
|
270
286
|
if not self._is_connection_open or self._reader is None:
|
|
271
|
-
raise DeviceConnectionError(self.
|
|
287
|
+
raise DeviceConnectionError(self._device_name, "Device not connected, use connect() first")
|
|
272
288
|
|
|
273
289
|
try:
|
|
274
290
|
# First, small delay to allow device to prepare response
|
|
@@ -277,19 +293,19 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
|
|
|
277
293
|
# Try to read until newline (common SCPI terminator)
|
|
278
294
|
try:
|
|
279
295
|
response = await asyncio.wait_for(
|
|
280
|
-
self._reader.readuntil(separator=
|
|
296
|
+
self._reader.readuntil(separator=SEPARATOR), timeout=self.read_timeout
|
|
281
297
|
)
|
|
282
298
|
logger.info(f"<----- {response}")
|
|
283
299
|
return response
|
|
284
300
|
|
|
285
301
|
except asyncio.IncompleteReadError as exc:
|
|
286
302
|
# Connection closed before receiving full response
|
|
287
|
-
logger.warning(f"{self.
|
|
288
|
-
return exc.partial if exc.partial else
|
|
303
|
+
logger.warning(f"{self._device_name}: Incomplete read, got {len(exc.partial)} bytes")
|
|
304
|
+
return exc.partial if exc.partial else SEPARATOR
|
|
289
305
|
|
|
290
306
|
except asyncio.LimitOverrunError:
|
|
291
307
|
# Response too large for buffer
|
|
292
|
-
logger.warning(f"{self.
|
|
308
|
+
logger.warning(f"{self._device_name}: Response exceeded buffer limits")
|
|
293
309
|
# Fall back to reading a large chunk
|
|
294
310
|
return await asyncio.wait_for(
|
|
295
311
|
self._reader.read(8192), # Larger buffer for exceptional cases
|
|
@@ -297,9 +313,9 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
|
|
|
297
313
|
)
|
|
298
314
|
|
|
299
315
|
except asyncio.TimeoutError as exc:
|
|
300
|
-
raise DeviceTimeoutError(self.
|
|
316
|
+
raise DeviceTimeoutError(self._device_name, "Read operation timed out") from exc
|
|
301
317
|
except Exception as exc:
|
|
302
|
-
raise DeviceConnectionError(self.
|
|
318
|
+
raise DeviceConnectionError(self._device_name, f"Read error: {exc}") from exc
|
|
303
319
|
|
|
304
320
|
async def trans(self, command: str) -> bytes:
|
|
305
321
|
"""
|
|
@@ -320,11 +336,11 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
|
|
|
320
336
|
async with self._io_lock:
|
|
321
337
|
try:
|
|
322
338
|
if not self._is_connection_open or self._writer is None:
|
|
323
|
-
raise DeviceConnectionError(self.
|
|
339
|
+
raise DeviceConnectionError(self._device_name, "Device not connected, use connect() first")
|
|
324
340
|
|
|
325
341
|
# Ensure command ends with newline
|
|
326
|
-
if not command.endswith(
|
|
327
|
-
command +=
|
|
342
|
+
if not command.endswith(SEPARATOR_STR):
|
|
343
|
+
command += SEPARATOR_STR
|
|
328
344
|
|
|
329
345
|
logger.info(f"-----> {command}")
|
|
330
346
|
self._writer.write(command.encode())
|
|
@@ -336,19 +352,19 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
|
|
|
336
352
|
# Try to read until newline (common SCPI terminator)
|
|
337
353
|
try:
|
|
338
354
|
response = await asyncio.wait_for(
|
|
339
|
-
self._reader.readuntil(separator=
|
|
355
|
+
self._reader.readuntil(separator=SEPARATOR), timeout=self.read_timeout
|
|
340
356
|
)
|
|
341
357
|
logger.info(f"<----- {response}")
|
|
342
358
|
return response
|
|
343
359
|
|
|
344
360
|
except asyncio.IncompleteReadError as exc:
|
|
345
361
|
# Connection closed before receiving full response
|
|
346
|
-
logger.warning(f"{self.
|
|
347
|
-
return exc.partial if exc.partial else
|
|
362
|
+
logger.warning(f"{self._device_name}: Incomplete read, got {len(exc.partial)} bytes")
|
|
363
|
+
return exc.partial if exc.partial else SEPARATOR
|
|
348
364
|
|
|
349
365
|
except asyncio.LimitOverrunError:
|
|
350
366
|
# Response too large for buffer
|
|
351
|
-
logger.warning(f"{self.
|
|
367
|
+
logger.warning(f"{self._device_name}: Response exceeded buffer limits")
|
|
352
368
|
# Fall back to reading a large chunk
|
|
353
369
|
return await asyncio.wait_for(
|
|
354
370
|
self._reader.read(8192), # Larger buffer for exceptional cases
|
|
@@ -356,11 +372,11 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
|
|
|
356
372
|
)
|
|
357
373
|
|
|
358
374
|
except asyncio.TimeoutError as exc:
|
|
359
|
-
raise DeviceTimeoutError(self.
|
|
375
|
+
raise DeviceTimeoutError(self._device_name, "Communication timed out") from exc
|
|
360
376
|
except (ConnectionError, OSError) as exc:
|
|
361
|
-
raise DeviceConnectionError(self.
|
|
377
|
+
raise DeviceConnectionError(self._device_name, f"Communication error: {exc}") from exc
|
|
362
378
|
except Exception as exc:
|
|
363
|
-
raise DeviceConnectionError(self.
|
|
379
|
+
raise DeviceConnectionError(self._device_name, f"Transaction error: {exc}") from exc
|
|
364
380
|
|
|
365
381
|
async def __aenter__(self):
|
|
366
382
|
"""Async context manager entry."""
|
egse/settings.py
CHANGED
|
@@ -66,22 +66,20 @@ The above code will read the YAML file from the given location and not from the
|
|
|
66
66
|
|
|
67
67
|
"""
|
|
68
68
|
|
|
69
|
-
from __future__ import annotations
|
|
70
|
-
|
|
71
|
-
import logging
|
|
72
69
|
import re
|
|
73
70
|
from pathlib import Path
|
|
74
71
|
from typing import Any
|
|
75
72
|
|
|
76
73
|
import yaml # This module is provided by the pip package PyYaml - pip install pyyaml
|
|
77
74
|
|
|
75
|
+
from egse.env import bool_env
|
|
78
76
|
from egse.env import get_local_settings_env_name
|
|
79
77
|
from egse.env import get_local_settings_path
|
|
80
78
|
from egse.log import logger
|
|
81
79
|
from egse.system import attrdict
|
|
82
80
|
from egse.system import recursive_dict_update
|
|
83
81
|
|
|
84
|
-
|
|
82
|
+
VERBOSE_DEBUG = bool_env("VERBOSE_DEBUG")
|
|
85
83
|
|
|
86
84
|
|
|
87
85
|
class SettingsError(Exception):
|
|
@@ -195,9 +193,12 @@ def load_local_settings(force: bool = False) -> attrdict:
|
|
|
195
193
|
local_settings = attrdict()
|
|
196
194
|
|
|
197
195
|
local_settings_path = get_local_settings_path()
|
|
196
|
+
if VERBOSE_DEBUG:
|
|
197
|
+
logger.debug(f"{get_local_settings_env_name()=}")
|
|
198
|
+
logger.debug(f"{local_settings_path=}")
|
|
198
199
|
|
|
199
200
|
if local_settings_path:
|
|
200
|
-
path = Path(local_settings_path)
|
|
201
|
+
path = Path(local_settings_path).expanduser()
|
|
201
202
|
local_settings = load_settings_file(path.parent, path.name, force)
|
|
202
203
|
|
|
203
204
|
return local_settings
|
|
@@ -221,7 +222,8 @@ def read_configuration_file(filename: Path, *, force=False) -> dict:
|
|
|
221
222
|
filename = str(filename)
|
|
222
223
|
|
|
223
224
|
if force or not Settings.is_memoized(filename):
|
|
224
|
-
|
|
225
|
+
if VERBOSE_DEBUG:
|
|
226
|
+
logger.debug(f"Parsing YAML configuration file {filename}.")
|
|
225
227
|
|
|
226
228
|
with open(filename, "r") as stream:
|
|
227
229
|
try:
|
|
@@ -386,7 +388,9 @@ def main(args: list | None = None): # pragma: no cover
|
|
|
386
388
|
#
|
|
387
389
|
# Use the '--help' option to see what your choices are.
|
|
388
390
|
|
|
389
|
-
|
|
391
|
+
from egse.env import setup_env
|
|
392
|
+
|
|
393
|
+
setup_env()
|
|
390
394
|
|
|
391
395
|
import argparse
|
|
392
396
|
|
egse/setup.py
CHANGED
|
@@ -644,7 +644,7 @@ def _check_conditions_for_get_path_of_setup_file(site_id: str) -> Path:
|
|
|
644
644
|
|
|
645
645
|
print_env()
|
|
646
646
|
|
|
647
|
-
repo_location = Path(repo_location)
|
|
647
|
+
repo_location = Path(repo_location).expanduser()
|
|
648
648
|
setup_location = repo_location / "data" / site_id / "conf"
|
|
649
649
|
|
|
650
650
|
if not repo_location.is_dir():
|
|
@@ -689,7 +689,7 @@ def get_path_of_setup_file(setup_id: int, site_id: str) -> Path:
|
|
|
689
689
|
"""
|
|
690
690
|
|
|
691
691
|
if not has_conf_repo_location():
|
|
692
|
-
setup_location = Path(get_conf_data_location(site_id))
|
|
692
|
+
setup_location = Path(get_conf_data_location(site_id)).expanduser()
|
|
693
693
|
else:
|
|
694
694
|
setup_location = _check_conditions_for_get_path_of_setup_file(site_id)
|
|
695
695
|
|
|
@@ -823,10 +823,11 @@ def submit_setup(setup: Setup, description: str, **kwargs) -> str | None:
|
|
|
823
823
|
|
|
824
824
|
def main(args: list = None): # pragma: no cover
|
|
825
825
|
import argparse
|
|
826
|
-
|
|
827
826
|
from rich import print
|
|
828
|
-
|
|
829
827
|
from egse.config import find_files
|
|
828
|
+
from egse.env import setup_env
|
|
829
|
+
|
|
830
|
+
setup_env()
|
|
830
831
|
|
|
831
832
|
site_id = get_site_id()
|
|
832
833
|
location = get_conf_data_location()
|
|
@@ -882,7 +883,7 @@ def main(args: list = None): # pragma: no cover
|
|
|
882
883
|
|
|
883
884
|
|
|
884
885
|
class SetupManager:
|
|
885
|
-
"""
|
|
886
|
+
"""A unified manager that routes Setup access to appropriate providers.
|
|
886
887
|
|
|
887
888
|
Providers are loaded from the `cgse.extension.setup_providers` entrypoints.
|
|
888
889
|
Providers serve different purposes, the default provider accesses Setups from
|
|
@@ -958,6 +959,11 @@ _setup_manager = SetupManager()
|
|
|
958
959
|
|
|
959
960
|
|
|
960
961
|
if __name__ == "__main__":
|
|
962
|
+
from egse.env import setup_env
|
|
963
|
+
|
|
964
|
+
setup_env()
|
|
965
|
+
|
|
966
|
+
# import sys
|
|
961
967
|
# main(sys.argv[1:])
|
|
962
|
-
|
|
968
|
+
|
|
963
969
|
rich.print(load_setup(site_id=get_site_id()))
|