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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cgse-common
3
- Version: 0.14.1
3
+ Version: 0.15.1
4
4
  Summary: Software framework to support hardware testing
5
5
  Author: IvS KU Leuven
6
6
  Maintainer-email: Rik Huygen <rik.huygen@kuleuven.be>, Sara Regibo <sara.regibo@kuleuven.be>
@@ -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=-FVwbf95kp_BTYs_Evl6kRs5Jb-oMUfaX3z3atkoT7I,10973
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=DBTvB_p7yaGVW1aNJ6yhxGhWCuSe2uFzMCjFOy3XmZM,13375
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=PfE0-Y6-ZEtMIVjt7R7qEXQXGts9m7yDKA3AA9H18Xw,72867
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.14.1.dist-info/METADATA,sha256=7mu_CKI3uRvkTZwkFDFFbM6Bsdo8jWz641lcdQ7x_5g,3032
43
- cgse_common-0.14.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
44
- cgse_common-0.14.1.dist-info/entry_points.txt,sha256=erQovXd1bGzsngB0_sfY7IYRNwHIhwq3K8fmQvGS12o,198
45
- cgse_common-0.14.1.dist-info/RECORD,,
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
- async with self._connect_lock:
150
- if self._is_connection_open:
151
- await self.disconnect()
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.exception(exc)
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__":