cgse-common 0.14.1__py3-none-any.whl → 0.15.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-0.14.1.dist-info → cgse_common-0.15.1.dist-info}/METADATA +1 -1
- {cgse_common-0.14.1.dist-info → cgse_common-0.15.1.dist-info}/RECORD +7 -7
- egse/device.py +73 -0
- egse/scpi.py +58 -9
- egse/system.py +51 -0
- {cgse_common-0.14.1.dist-info → cgse_common-0.15.1.dist-info}/WHEEL +0 -0
- {cgse_common-0.14.1.dist-info → cgse_common-0.15.1.dist-info}/entry_points.txt +0 -0
|
@@ -6,7 +6,7 @@ egse/calibration.py,sha256=a5JDaXTC6fMwQ1M-qrwNO31Ass-yYSXxDQUK_PPsZg4,8818
|
|
|
6
6
|
egse/config.py,sha256=qNW3uvwuEV9VSjiaoQfM_svBYfz4Kwxd1-jv1eTGqyw,9530
|
|
7
7
|
egse/counter.py,sha256=7UwBeTAu213xdNdGAOYpUWNQ4jD4yVM1bOG10Ax4UFs,5097
|
|
8
8
|
egse/decorators.py,sha256=B-zRa1WdLO71zqS5M27JBglcThYPho7seYfa4HOGj5c,27171
|
|
9
|
-
egse/device.py,sha256
|
|
9
|
+
egse/device.py,sha256=nn2HkN1KIHAmo37WZcqig-p2mQz1LgqpIfj1wPrUTLc,13240
|
|
10
10
|
egse/dicts.py,sha256=dUAq7PTPvs73OrZb2Fh3loxvYv4ifUiK6bBcgrFU77Y,3972
|
|
11
11
|
egse/env.py,sha256=0YFBGrA4xnk7MgGdHYSFEpiUj6EjLJ54S3-MqtXUm5Y,28367
|
|
12
12
|
egse/exceptions.py,sha256=QB3MZRJizecWOj1cPbvG0UcIqFn7NRJ6rw1xtdNSFxw,1225
|
|
@@ -25,21 +25,21 @@ egse/ratelimit.py,sha256=JdJxD6UIi9LYngKEsG9zh8bTE9r_56D4EZCnp_fkrI0,9161
|
|
|
25
25
|
egse/reload.py,sha256=PzOE0m1tmcNcQPVFH8orMe_cMoQIIiH9Gw2anpQTC40,4717
|
|
26
26
|
egse/resource.py,sha256=kzNI6kJOE6Jd5QKJs2MkVAycUpwpOTLi1qydh3NSRng,15345
|
|
27
27
|
egse/response.py,sha256=F04uqOYv1ClpHgDLYZlKTuOCSldHs5TezI_4x6zf2Fw,2717
|
|
28
|
-
egse/scpi.py,sha256=
|
|
28
|
+
egse/scpi.py,sha256=WJ73EaLgRUV6ah1V41l0L7AXI-Dc6Jct7hPHlbbCIcg,15461
|
|
29
29
|
egse/settings.py,sha256=YrRsMUn_IpOVnhTqUGREQUjMw8-AQ6aUBulQiij9MwY,15486
|
|
30
30
|
egse/settings.yaml,sha256=mz9O2QqmiptezsMvxJRLhnC1ROwIHENX0nbnhMaXUpE,190
|
|
31
31
|
egse/setup.py,sha256=1k-5CjzY3_tZ6XCitNOIyZEWyAUC8LqGcnmdKS68vYw,33945
|
|
32
32
|
egse/signal.py,sha256=zW_36xm-RpzwuCu6x30dTvjkfnuyRebKKpl69Yk1QSc,7990
|
|
33
33
|
egse/socketdevice.py,sha256=R8XwYHTH3lFhFngfsGbi_L7bTnTLHxMTEKIF7gmm5rc,7465
|
|
34
34
|
egse/state.py,sha256=HdU2MFOlYRbawYRZmizV6Y8MgnZrUF0bx4fXaYU-M_s,3023
|
|
35
|
-
egse/system.py,sha256=
|
|
35
|
+
egse/system.py,sha256=SiWJNet9eI1ut5htX6A0M93WstzeJjAMfLZ-7IQfg6Q,74781
|
|
36
36
|
egse/task.py,sha256=ODSLE05f31CgWsSVcVFFq1WYUZrJMb1LioPTx6VM824,2804
|
|
37
37
|
egse/version.py,sha256=e9GvelUZ9mfCDlRju4MWEJeMHJW9kUzK6SKzJpyj91s,6156
|
|
38
38
|
egse/zmq_ser.py,sha256=d2lETLkLUll_F1Phc1pI7MEeA41uX7YZ8lhSFmBQZVw,3022
|
|
39
39
|
egse/plugins/metrics/duckdb.py,sha256=E2eeNo3I7ajRuByodaYiPNvC0Zwyc7hsIlhr1W_eXdo,16148
|
|
40
40
|
egse/plugins/metrics/influxdb.py,sha256=ecxjA_csYwf8RW3sXjiQxZHREfyrfStH1HA_rAs1AA8,6690
|
|
41
41
|
egse/plugins/metrics/timescaledb.py,sha256=Ug0NWDV1Ky2VeFY6tDZL9xg6AFgnAEh2F_llVPnlRBA,21191
|
|
42
|
-
cgse_common-0.
|
|
43
|
-
cgse_common-0.
|
|
44
|
-
cgse_common-0.
|
|
45
|
-
cgse_common-0.
|
|
42
|
+
cgse_common-0.15.1.dist-info/METADATA,sha256=IwIBVVopd15h4-PcEBumskB92pxsPSwdNZY7j0KX1_k,3032
|
|
43
|
+
cgse_common-0.15.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
44
|
+
cgse_common-0.15.1.dist-info/entry_points.txt,sha256=erQovXd1bGzsngB0_sfY7IYRNwHIhwq3K8fmQvGS12o,198
|
|
45
|
+
cgse_common-0.15.1.dist-info/RECORD,,
|
egse/device.py
CHANGED
|
@@ -236,6 +236,9 @@ class DeviceTransport:
|
|
|
236
236
|
|
|
237
237
|
raise NotImplementedError
|
|
238
238
|
|
|
239
|
+
def read_string(self, encoding="utf-8") -> str:
|
|
240
|
+
return self.read().decode(encoding).strip()
|
|
241
|
+
|
|
239
242
|
def trans(self, command: str) -> bytes:
|
|
240
243
|
"""
|
|
241
244
|
Send a single command to the device controller and block until a response from the
|
|
@@ -330,6 +333,76 @@ class AsyncDeviceTransport:
|
|
|
330
333
|
return await self.trans(command)
|
|
331
334
|
|
|
332
335
|
|
|
336
|
+
class AsyncDeviceConnectionInterface(DeviceConnectionObservable):
|
|
337
|
+
"""Generic connection interface for all Device classes and Controllers.
|
|
338
|
+
|
|
339
|
+
This interface shall be implemented in the Controllers that directly connect to the
|
|
340
|
+
hardware, but also in the simulators to guarantee an identical interface as the controllers.
|
|
341
|
+
|
|
342
|
+
This interface will be implemented in the Proxy classes through the
|
|
343
|
+
YAML definitions. Therefore, the YAML files shall define at least
|
|
344
|
+
the following commands: `connect`, `disconnect`, `reconnect`, `is_connected`.
|
|
345
|
+
"""
|
|
346
|
+
|
|
347
|
+
def __init__(self):
|
|
348
|
+
super().__init__()
|
|
349
|
+
|
|
350
|
+
def __enter__(self):
|
|
351
|
+
self.connect()
|
|
352
|
+
return self
|
|
353
|
+
|
|
354
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
355
|
+
self.disconnect()
|
|
356
|
+
|
|
357
|
+
async def connect(self) -> None:
|
|
358
|
+
"""Connect to the device controller.
|
|
359
|
+
|
|
360
|
+
Raises:
|
|
361
|
+
ConnectionError: when the connection can not be opened.
|
|
362
|
+
"""
|
|
363
|
+
|
|
364
|
+
raise NotImplementedError
|
|
365
|
+
|
|
366
|
+
async def disconnect(self) -> None:
|
|
367
|
+
"""Disconnect from the device controller.
|
|
368
|
+
|
|
369
|
+
Raises:
|
|
370
|
+
ConnectionError: when the connection can not be closed.
|
|
371
|
+
"""
|
|
372
|
+
raise NotImplementedError
|
|
373
|
+
|
|
374
|
+
async def reconnect(self):
|
|
375
|
+
"""Reconnect the device controller.
|
|
376
|
+
|
|
377
|
+
Raises:
|
|
378
|
+
ConnectionError: when the device can not be reconnected for some reason.
|
|
379
|
+
"""
|
|
380
|
+
raise NotImplementedError
|
|
381
|
+
|
|
382
|
+
async def is_connected(self) -> bool:
|
|
383
|
+
"""Check if the device is connected.
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
True if the device is connected and responds to a command, False otherwise.
|
|
387
|
+
"""
|
|
388
|
+
raise NotImplementedError
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
class AsyncDeviceInterface(AsyncDeviceConnectionInterface):
|
|
392
|
+
"""Generic interface for all device classes."""
|
|
393
|
+
|
|
394
|
+
def is_simulator(self) -> bool:
|
|
395
|
+
"""Checks whether the device is a simulator rather than a real hardware controller.
|
|
396
|
+
|
|
397
|
+
This can be useful for testing purposes or when doing actual movement simulations.
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
True if the Device is a Simulator; False if the Device is connected to real hardware.
|
|
401
|
+
"""
|
|
402
|
+
|
|
403
|
+
raise NotImplementedError
|
|
404
|
+
|
|
405
|
+
|
|
333
406
|
class DeviceFactoryInterface:
|
|
334
407
|
"""
|
|
335
408
|
Base class for creating a device factory class to access devices.
|
egse/scpi.py
CHANGED
|
@@ -4,13 +4,13 @@ from typing import Any
|
|
|
4
4
|
from typing import Dict
|
|
5
5
|
from typing import Optional
|
|
6
6
|
|
|
7
|
+
from egse.device import AsyncDeviceInterface
|
|
7
8
|
from egse.device import AsyncDeviceTransport
|
|
8
9
|
from egse.device import DeviceConnectionError
|
|
9
10
|
from egse.device import DeviceError
|
|
10
11
|
from egse.device import DeviceTimeoutError
|
|
11
12
|
from egse.log import logger
|
|
12
13
|
|
|
13
|
-
# Constants that can be overridden by specific device implementations
|
|
14
14
|
DEFAULT_READ_TIMEOUT = 1.0 # seconds
|
|
15
15
|
DEFAULT_CONNECT_TIMEOUT = 3.0 # seconds
|
|
16
16
|
IDENTIFICATION_QUERY = "*IDN?"
|
|
@@ -32,7 +32,7 @@ class SCPICommand:
|
|
|
32
32
|
raise NotImplementedError("Subclasses must implement get_cmd_string().")
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
class AsyncSCPIInterface(AsyncDeviceTransport):
|
|
35
|
+
class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
|
|
36
36
|
"""Generic asynchronous interface for devices that use SCPI commands over Ethernet."""
|
|
37
37
|
|
|
38
38
|
def __init__(
|
|
@@ -56,6 +56,7 @@ class AsyncSCPIInterface(AsyncDeviceTransport):
|
|
|
56
56
|
read_timeout: Timeout for read operations in seconds
|
|
57
57
|
id_validation: String that should appear in the device's identification response
|
|
58
58
|
"""
|
|
59
|
+
super().__init__()
|
|
59
60
|
self.device_name = device_name
|
|
60
61
|
self.hostname = hostname
|
|
61
62
|
self.port = port
|
|
@@ -73,6 +74,53 @@ class AsyncSCPIInterface(AsyncDeviceTransport):
|
|
|
73
74
|
"""Prevents multiple coroutines from attempting to read, write or query from the same stream
|
|
74
75
|
at the same time."""
|
|
75
76
|
|
|
77
|
+
def is_simulator(self) -> bool:
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
async def initialize(self, commands: list[tuple[str, bool]] = None, reset_device: bool = False):
|
|
81
|
+
"""Initialize the device with optional reset and command sequence.
|
|
82
|
+
|
|
83
|
+
Performs device initialization by optionally resetting the device and then
|
|
84
|
+
executing a sequence of commands. Each command can optionally expect a
|
|
85
|
+
response that will be logged for debugging purposes.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
commands: List of tuples containing (command_string, expects_response).
|
|
89
|
+
Each tuple specifies a command to send and whether to wait for and
|
|
90
|
+
log the response. Defaults to None (no commands executed).
|
|
91
|
+
reset_device: Whether to send a reset command (*RST) before executing
|
|
92
|
+
the command sequence. Defaults to False.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
None
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
Any exceptions raised by the underlying write() or trans() methods,
|
|
99
|
+
typically communication errors or device timeouts.
|
|
100
|
+
|
|
101
|
+
Example:
|
|
102
|
+
>>> await device.initialize([
|
|
103
|
+
... ("*IDN?", True), # Query device ID, expect response
|
|
104
|
+
... ("SYST:ERR?", True), # Check for errors, expect response
|
|
105
|
+
... ("OUTP ON", False) # Enable output, no response expected
|
|
106
|
+
... ], reset_device=True)
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
commands = commands or []
|
|
110
|
+
|
|
111
|
+
if reset_device:
|
|
112
|
+
logger.info(f"Resetting the {self.device_name}...")
|
|
113
|
+
await self.write("*RST") # this also resets the user-defined buffer
|
|
114
|
+
|
|
115
|
+
for cmd, expects_response in commands:
|
|
116
|
+
if expects_response:
|
|
117
|
+
logger.debug(f"Sending {cmd}...")
|
|
118
|
+
response = (await self.trans(cmd)).decode().strip()
|
|
119
|
+
logger.debug(f"{response = }")
|
|
120
|
+
else:
|
|
121
|
+
logger.debug(f"Sending {cmd}...")
|
|
122
|
+
await self.write(cmd)
|
|
123
|
+
|
|
76
124
|
async def connect(self) -> None:
|
|
77
125
|
"""Connect to the device asynchronously.
|
|
78
126
|
|
|
@@ -146,11 +194,9 @@ class AsyncSCPIInterface(AsyncDeviceTransport):
|
|
|
146
194
|
|
|
147
195
|
async def reconnect(self) -> None:
|
|
148
196
|
"""Reconnect to the device asynchronously."""
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
await asyncio.sleep(0.1)
|
|
153
|
-
await self.connect()
|
|
197
|
+
await self.disconnect()
|
|
198
|
+
await asyncio.sleep(0.1)
|
|
199
|
+
await self.connect()
|
|
154
200
|
|
|
155
201
|
async def is_connected(self) -> bool:
|
|
156
202
|
"""Check if the device is connected and responds correctly to identification.
|
|
@@ -177,8 +223,7 @@ class AsyncSCPIInterface(AsyncDeviceTransport):
|
|
|
177
223
|
return True
|
|
178
224
|
|
|
179
225
|
except DeviceError as exc:
|
|
180
|
-
logger.
|
|
181
|
-
logger.error(f"{self.device_name}: Connection test failed")
|
|
226
|
+
logger.error(f"{self.device_name}: Connection test failed: {exc}", exc_info=True)
|
|
182
227
|
await self.disconnect()
|
|
183
228
|
return False
|
|
184
229
|
|
|
@@ -201,6 +246,7 @@ class AsyncSCPIInterface(AsyncDeviceTransport):
|
|
|
201
246
|
if not command.endswith("\n"):
|
|
202
247
|
command += "\n"
|
|
203
248
|
|
|
249
|
+
logger.info(f"-----> {command}")
|
|
204
250
|
self._writer.write(command.encode())
|
|
205
251
|
await self._writer.drain()
|
|
206
252
|
|
|
@@ -233,6 +279,7 @@ class AsyncSCPIInterface(AsyncDeviceTransport):
|
|
|
233
279
|
response = await asyncio.wait_for(
|
|
234
280
|
self._reader.readuntil(separator=b"\n"), timeout=self.read_timeout
|
|
235
281
|
)
|
|
282
|
+
logger.info(f"<----- {response}")
|
|
236
283
|
return response
|
|
237
284
|
|
|
238
285
|
except asyncio.IncompleteReadError as exc:
|
|
@@ -279,6 +326,7 @@ class AsyncSCPIInterface(AsyncDeviceTransport):
|
|
|
279
326
|
if not command.endswith("\n"):
|
|
280
327
|
command += "\n"
|
|
281
328
|
|
|
329
|
+
logger.info(f"-----> {command}")
|
|
282
330
|
self._writer.write(command.encode())
|
|
283
331
|
await self._writer.drain()
|
|
284
332
|
|
|
@@ -290,6 +338,7 @@ class AsyncSCPIInterface(AsyncDeviceTransport):
|
|
|
290
338
|
response = await asyncio.wait_for(
|
|
291
339
|
self._reader.readuntil(separator=b"\n"), timeout=self.read_timeout
|
|
292
340
|
)
|
|
341
|
+
logger.info(f"<----- {response}")
|
|
293
342
|
return response
|
|
294
343
|
|
|
295
344
|
except asyncio.IncompleteReadError as exc:
|
egse/system.py
CHANGED
|
@@ -29,6 +29,7 @@ import operator
|
|
|
29
29
|
import os
|
|
30
30
|
import platform # For getting the operating system name
|
|
31
31
|
import re
|
|
32
|
+
import shutil
|
|
32
33
|
import socket
|
|
33
34
|
import subprocess # For executing a shell command
|
|
34
35
|
import sys
|
|
@@ -2256,6 +2257,56 @@ def snake_to_title(snake_str: str) -> str:
|
|
|
2256
2257
|
return snake_str.replace("_", " ").title()
|
|
2257
2258
|
|
|
2258
2259
|
|
|
2260
|
+
def caffeinate(pid: int = None):
|
|
2261
|
+
"""Prevent your macOS system from entering idle sleep while a process is running.
|
|
2262
|
+
|
|
2263
|
+
This function uses the macOS 'caffeinate' utility to prevent the system from
|
|
2264
|
+
going to sleep due to inactivity. It's particularly useful for long-running
|
|
2265
|
+
background processes that may lose network connections or be interrupted
|
|
2266
|
+
when the system sleeps.
|
|
2267
|
+
|
|
2268
|
+
The function only operates on macOS systems and silently does nothing on
|
|
2269
|
+
other operating systems.
|
|
2270
|
+
|
|
2271
|
+
Args:
|
|
2272
|
+
pid (int, optional): Process ID to monitor. If provided, caffeinate will
|
|
2273
|
+
keep the system awake as long as the specified process is running.
|
|
2274
|
+
If None or 0, defaults to the current process ID (os.getpid()).
|
|
2275
|
+
|
|
2276
|
+
Returns:
|
|
2277
|
+
None
|
|
2278
|
+
|
|
2279
|
+
Raises:
|
|
2280
|
+
FileNotFoundError: If 'caffeinate' command is not found in PATH (shouldn't
|
|
2281
|
+
happen on standard macOS installations).
|
|
2282
|
+
OSError: If subprocess.Popen fails to start the caffeinate process.
|
|
2283
|
+
|
|
2284
|
+
Example:
|
|
2285
|
+
>>> # Keep system awake while current process runs
|
|
2286
|
+
>>> caffeinate()
|
|
2287
|
+
|
|
2288
|
+
>>> # Keep system awake while specific process runs
|
|
2289
|
+
>>> caffeinate(1234)
|
|
2290
|
+
|
|
2291
|
+
Note:
|
|
2292
|
+
- Uses 'caffeinate -i -w <pid>' which prevents idle sleep (-i) and monitors
|
|
2293
|
+
a specific process (-w)
|
|
2294
|
+
- The caffeinate process will automatically terminate when the monitored
|
|
2295
|
+
process exits
|
|
2296
|
+
- On non-macOS systems, this function does nothing
|
|
2297
|
+
- Logs a warning message when caffeinate is started
|
|
2298
|
+
|
|
2299
|
+
See Also:
|
|
2300
|
+
macOS caffeinate(8) man page for more details on the underlying utility.
|
|
2301
|
+
"""
|
|
2302
|
+
if not pid:
|
|
2303
|
+
pid = os.getpid()
|
|
2304
|
+
|
|
2305
|
+
if get_os_name() == "macos":
|
|
2306
|
+
logger.warning(f"Running 'caffeinate -i -w {pid}' on macOS to prevent the system from idle sleeping.")
|
|
2307
|
+
subprocess.Popen([shutil.which('caffeinate'), "-i", "-w", str(pid)])
|
|
2308
|
+
|
|
2309
|
+
|
|
2259
2310
|
ignore_m_warning("egse.system")
|
|
2260
2311
|
|
|
2261
2312
|
if __name__ == "__main__":
|
|
File without changes
|
|
File without changes
|