cgse-common 0.17.4__py3-none-any.whl → 0.18.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cgse-common
3
- Version: 0.17.4
3
+ Version: 0.18.0
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,13 +6,13 @@ egse/calibration.py,sha256=a5JDaXTC6fMwQ1M-qrwNO31Ass-yYSXxDQUK_PPsZg4,8818
6
6
  egse/config.py,sha256=M821_d1IfNT17J3tt_TDjh4bGJgGiYviGBYR0I5v5LA,9639
7
7
  egse/counter.py,sha256=7UwBeTAu213xdNdGAOYpUWNQ4jD4yVM1bOG10Ax4UFs,5097
8
8
  egse/decorators.py,sha256=ZJgXKP8IEniOFd8nHljNeUNpQLAB2Z9q4UiQsnLFzhA,27204
9
- egse/device.py,sha256=Ga5MNE96ERuPLDzmb0HF7UfjrTqfp0f_r3NDVZY9l28,13595
9
+ egse/device.py,sha256=hHDOWUpjLU86SIMiQAH9m8P8BvdgfXoPexISOKjAWy4,13612
10
10
  egse/dicts.py,sha256=dUAq7PTPvs73OrZb2Fh3loxvYv4ifUiK6bBcgrFU77Y,3972
11
- egse/env.py,sha256=MWftGEmodckKbjXxFGm3WsQRnZ96wCDR3bci0KzZP0Y,31705
11
+ egse/env.py,sha256=Lrx_G8DP8juyWx_Yh2yjhtDBesJZGMw_QWya984-Dr8,31704
12
12
  egse/exceptions.py,sha256=yhOtO5NkCxgvTyVo8fgTyxauX-ZtiEkxDB76ZdCbPrM,1301
13
13
  egse/heartbeat.py,sha256=2SeZzX3tWFog1rgYThX-iaZPwHYq8TVma2ll7r624Eg,3039
14
14
  egse/hk.py,sha256=01ejNkJ-RxPO8NPHYB91NryMVBNF9MN5B-BsvkCvzxQ,32006
15
- egse/log.py,sha256=cmPnqNmX8D55oCnPFGIwfUwRN4lIPmVzwZYwHuVhRnU,5479
15
+ egse/log.py,sha256=EhwzRufDOFd7vJVF8_C-q5D2NmO1TGIu6x7ske_JMZI,5547
16
16
  egse/metrics.py,sha256=2hHtJXG0Rn782l2bfmLNBbw6ucC5nf7jPnNzqbhP_Zs,7012
17
17
  egse/observer.py,sha256=xQ7F7NVHqdRZ6IIsBM5M0kMuullMghoR98dwAsjgh0s,1287
18
18
  egse/obsid.py,sha256=y87AYX5mtNEBqEtpEFEec2MhEmo1Hej3Wwi5od84wR8,5848
@@ -24,19 +24,19 @@ egse/randomwalk.py,sha256=dllGv_F4AFYanp_A5ynBsAjglYzxaPYpRCBifwQScx4,6451
24
24
  egse/reload.py,sha256=PzOE0m1tmcNcQPVFH8orMe_cMoQIIiH9Gw2anpQTC40,4717
25
25
  egse/resource.py,sha256=kzNI6kJOE6Jd5QKJs2MkVAycUpwpOTLi1qydh3NSRng,15345
26
26
  egse/response.py,sha256=F04uqOYv1ClpHgDLYZlKTuOCSldHs5TezI_4x6zf2Fw,2717
27
- egse/scpi.py,sha256=N4R9w7mN_cDBBaIT_cMLD6XLckndPNfOeOPvCgIOUK0,16231
27
+ egse/scpi.py,sha256=ta59pGtkfnOx8WunDaedOrrpuDw7vwsHANMWugiufzo,22541
28
28
  egse/settings.py,sha256=eiZ9eGydgF9lNBjHH8VqOgcFDxSdhO6dLs7pYA725lo,16849
29
29
  egse/settings.yaml,sha256=mz9O2QqmiptezsMvxJRLhnC1ROwIHENX0nbnhMaXUpE,190
30
- egse/setup.py,sha256=ezPYA3n1P3navdPR3qDxh0qJvZCzGl2bIREEP9n2w3Y,34116
30
+ egse/setup.py,sha256=GToyqe2esIbxVn4NT3mYIPDaFJgkWZQykbgWDOwChB8,34331
31
31
  egse/signal.py,sha256=f5pyOiNW9iTSIxV_ce5stIfG0ub9MRbaekE85kQOVzs,7992
32
- egse/socketdevice.py,sha256=d9NHQwJLe186HahKWRcDsiOtsgRYkgkNjQq8gMXarXw,14639
32
+ egse/socketdevice.py,sha256=rlTX9rpQ8VKcixBD42oNJg67-qKj9IIxk2UPM_mtz6I,14672
33
33
  egse/state.py,sha256=HdU2MFOlYRbawYRZmizV6Y8MgnZrUF0bx4fXaYU-M_s,3023
34
- egse/system.py,sha256=PbdUUbPG8cpP021X7Jecb_l0tc6EsYwZJpyBQXw8GWQ,77676
34
+ egse/system.py,sha256=XiMXm5tEB95TsQQsInm_9iZTRDuPqS82VyB_9XMPL8E,77697
35
35
  egse/task.py,sha256=ODSLE05f31CgWsSVcVFFq1WYUZrJMb1LioPTx6VM824,2804
36
36
  egse/version.py,sha256=vPUsCy9HYR7nKm0Sg6EDoq1JtkBKPCr3kYrt9QYM1B8,6602
37
37
  egse/zmq_ser.py,sha256=YJFupsxuvhI8TJMeS2Hem9oMMcVmSBx0rZv93gvN-hA,3263
38
38
  egse/plugins/metrics/influxdb.py,sha256=WnAqTWRkAyMSd7W2ASwUAIEwFborrv55iX-umceevFA,8162
39
- cgse_common-0.17.4.dist-info/METADATA,sha256=igm__eBmYEmgGrrXYK278PfjTnpuUej7dfbJZBGsvO0,3068
40
- cgse_common-0.17.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
41
- cgse_common-0.17.4.dist-info/entry_points.txt,sha256=xJsPRIDjtADVgd_oEDHVW10wS5LG30Ox_3brVKeyCGw,168
42
- cgse_common-0.17.4.dist-info/RECORD,,
39
+ cgse_common-0.18.0.dist-info/METADATA,sha256=stU8OErbxr__5BPUohIhl9VJ7_Q5dYYqav9yfZSSAYs,3068
40
+ cgse_common-0.18.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
41
+ cgse_common-0.18.0.dist-info/entry_points.txt,sha256=xJsPRIDjtADVgd_oEDHVW10wS5LG30Ox_3brVKeyCGw,168
42
+ cgse_common-0.18.0.dist-info/RECORD,,
egse/device.py CHANGED
@@ -54,7 +54,7 @@ class DeviceControllerError(DeviceError):
54
54
  super().__init__(device_name, message)
55
55
 
56
56
 
57
- class DeviceConnectionError(DeviceError):
57
+ class DeviceConnectionError(DeviceError, ConnectionError):
58
58
  """A generic error for all connection type of problems.
59
59
 
60
60
  Args:
egse/env.py CHANGED
@@ -170,7 +170,7 @@ def setup_env():
170
170
  return
171
171
 
172
172
  if VERBOSE_DEBUG:
173
- logger.debug(f"Initialising the environment...")
173
+ logger.debug("Initializing the environment...")
174
174
 
175
175
  load_dotenv()
176
176
 
egse/log.py CHANGED
@@ -131,12 +131,14 @@ for handler in root_logger.handlers:
131
131
  handler.addFilter(NonEGSEFilter())
132
132
  handler.addFilter(PackageFilter())
133
133
 
134
- try:
135
- from textual.logging import TextualHandler
136
-
137
- root_logger.addHandler(TextualHandler())
138
- except ImportError:
139
- pass
134
+ # Optional: integrate with Textual logging if available
135
+ #
136
+ # try:
137
+ # from textual.logging import TextualHandler
138
+
139
+ # root_logger.addHandler(TextualHandler())
140
+ # except ImportError:
141
+ # pass
140
142
 
141
143
 
142
144
  logger = egse_logger
egse/scpi.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import re
2
3
  import socket
3
4
  from typing import Any
4
5
  from typing import Dict
@@ -14,6 +15,7 @@ from egse.log import logger
14
15
 
15
16
  DEFAULT_READ_TIMEOUT = 1.0 # seconds
16
17
  DEFAULT_CONNECT_TIMEOUT = 3.0 # seconds
18
+ DEFAULT_READ_AFTER_CONNECT = False
17
19
  IDENTIFICATION_QUERY = "*IDN?"
18
20
 
19
21
  VERBOSE_DEBUG = bool_env("VERBOSE_DEBUG")
@@ -49,6 +51,7 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
49
51
  settings: Optional[Dict[str, Any]] = None,
50
52
  connect_timeout: float = DEFAULT_CONNECT_TIMEOUT,
51
53
  read_timeout: float = DEFAULT_READ_TIMEOUT,
54
+ read_after_connect: bool = DEFAULT_READ_AFTER_CONNECT,
52
55
  id_validation: Optional[str] = None,
53
56
  ):
54
57
  """Initialize an asynchronous Ethernet interface for SCPI communication.
@@ -60,6 +63,7 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
60
63
  settings: Additional device-specific settings
61
64
  connect_timeout: Timeout for connection attempts in seconds
62
65
  read_timeout: Timeout for read operations in seconds
66
+ read_after_connect: Whether to perform a read operation immediately after connecting
63
67
  id_validation: String that should appear in the device's identification response
64
68
  """
65
69
  super().__init__()
@@ -70,6 +74,7 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
70
74
  self.settings = settings or {}
71
75
  self.connect_timeout = connect_timeout
72
76
  self.read_timeout = read_timeout
77
+ self.read_after_connect = read_after_connect
73
78
  self.id_validation = id_validation
74
79
 
75
80
  self._reader = None
@@ -87,7 +92,29 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
87
92
  def device_name(self) -> str:
88
93
  return self._device_name
89
94
 
90
- async def initialize(self, commands: list[tuple[str, bool]] = None, reset_device: bool = False) -> list[str | None]:
95
+ async def get_idn_parts(self) -> list[str]:
96
+ """Get the device identification string and return its parts.
97
+
98
+ The *IDN? command is sent to the device, and the response is split into its
99
+ components: Manufacturer, Model, Serial Number, Firmware Version.
100
+
101
+ Returns:
102
+ A list containing Manufacturer, Model, Serial Number, Firmware Version.
103
+
104
+ Raises:
105
+ DeviceError: When the device is not connected or communication fails.
106
+ """
107
+ if not self._is_connection_open:
108
+ raise DeviceError(self._device_name, "Device not connected, use connect() first")
109
+
110
+ idn_str = (await self.query(IDENTIFICATION_QUERY)).decode().strip()
111
+ if VERBOSE_DEBUG:
112
+ logger.debug(f"{self._device_name} IDN response: {idn_str}")
113
+ return idn_str.split(",")
114
+
115
+ async def initialize(
116
+ self, commands: list[tuple[str, bool]] | None = None, reset_device: bool = False
117
+ ) -> list[str | None]:
91
118
  """Initialize the device with optional reset and command sequence.
92
119
 
93
120
  Performs device initialization by optionally resetting the device and then
@@ -168,10 +195,6 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
168
195
 
169
196
  self._is_connection_open = True
170
197
 
171
- response = await self.read_string()
172
- if VERBOSE_DEBUG:
173
- logger.debug(f"Response after connection: {response}")
174
-
175
198
  logger.debug(f"Successfully connected to {self._device_name}.")
176
199
 
177
200
  except asyncio.TimeoutError as exc:
@@ -189,6 +212,13 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
189
212
  except OSError as exc:
190
213
  raise DeviceConnectionError(self._device_name, f"OS error: {exc}") from exc
191
214
 
215
+ # Some devices require an initial read after connection.
216
+ # We have to read this message from the buffer and can do some version checking if needed.
217
+ if self.read_after_connect:
218
+ response = await self.read_string()
219
+ if VERBOSE_DEBUG:
220
+ logger.debug(f"Response after connection: {response}")
221
+
192
222
  # Validate device identity if requested
193
223
  if self.id_validation:
194
224
  logger.debug("Validating connection..")
@@ -268,7 +298,8 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
268
298
  if not command.endswith(SEPARATOR_STR):
269
299
  command += SEPARATOR_STR
270
300
 
271
- logger.info(f"-----> {command}")
301
+ if VERBOSE_DEBUG:
302
+ logger.debug(f"-----> {command}")
272
303
  self._writer.write(command.encode())
273
304
  await self._writer.drain()
274
305
 
@@ -301,7 +332,8 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
301
332
  response = await asyncio.wait_for(
302
333
  self._reader.readuntil(separator=SEPARATOR), timeout=self.read_timeout
303
334
  )
304
- logger.info(f"<----- {response}")
335
+ if VERBOSE_DEBUG:
336
+ logger.debug(f"<----- {response}")
305
337
  return response
306
338
 
307
339
  except asyncio.IncompleteReadError as exc:
@@ -341,14 +373,16 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
341
373
 
342
374
  async with self._io_lock:
343
375
  try:
344
- if not self._is_connection_open or self._writer is None:
376
+ if not self._is_connection_open or self._writer is None or self._reader is None:
345
377
  raise DeviceConnectionError(self._device_name, "Device not connected, use connect() first")
346
378
 
347
379
  # Ensure command ends with the required terminator
348
380
  if not command.endswith(SEPARATOR_STR):
349
381
  command += SEPARATOR_STR
350
382
 
351
- logger.info(f"-----> {command=}")
383
+ if VERBOSE_DEBUG:
384
+ logger.debug(f"-----> {command=}")
385
+
352
386
  self._writer.write(command.encode())
353
387
  await self._writer.drain()
354
388
 
@@ -360,7 +394,8 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
360
394
  response = await asyncio.wait_for(
361
395
  self._reader.readuntil(separator=SEPARATOR), timeout=self.read_timeout
362
396
  )
363
- logger.info(f"<----- {response=}")
397
+ if VERBOSE_DEBUG:
398
+ logger.debug(f"<----- {response=}")
364
399
  return response
365
400
 
366
401
  except asyncio.IncompleteReadError as exc:
@@ -392,3 +427,156 @@ class AsyncSCPIInterface(AsyncDeviceInterface, AsyncDeviceTransport):
392
427
  async def __aexit__(self, exc_type, exc_val, exc_tb):
393
428
  """Async context manager exit."""
394
429
  await self.disconnect()
430
+
431
+
432
+ def create_channel_list(*args) -> str:
433
+ """
434
+ Create a channel list that is understood by SCPI commands.
435
+
436
+ Channel names are device-specific.
437
+
438
+ For the DAQ6510: Channel names contain both the slot number and the channel number.
439
+ The slot number is the number of the slot where the card is installed at the back of
440
+ the device.
441
+
442
+ When addressing multiple individual channels, add each of them as a separate argument,
443
+ e.g. to include channels 1, 3, and 7 from slot 1, use the following command:
444
+
445
+ >>> create_channel_list(101, 103, 107)
446
+ '(@101, 103, 107)'
447
+
448
+ To designate a range of channels, only one argument should be given, i.e. a tuple containing
449
+ two channel representing the range. The following tuple `(101, 110)` will create the
450
+ following response: `"(@101:110)"`. The range is inclusive, so this will define a range of
451
+ 10 channels in slot 1.
452
+
453
+ >>> create_channel_list((201, 205))
454
+ '(@201:205)'
455
+
456
+ See reference manual for the Keithley DAQ6510 [DAQ6510-901-01 Rev. B / September 2019],
457
+ chapter 11: Introduction to SCPI commands, SCPI command formatting, channel naming.
458
+
459
+ Args:
460
+ *args: a tuple or a list of channels
461
+
462
+ Returns:
463
+ A string containing the channel list as understood by the SCPI device.
464
+
465
+ """
466
+ if not args:
467
+ return ""
468
+
469
+ # If only one argument is given, I expect either a tuple defining a range
470
+ # or just one channel. When several arguments are given, I expect them all
471
+ # to be individual channels.
472
+
473
+ ch_list = []
474
+ for arg in args:
475
+ if isinstance(arg, (tuple, list)):
476
+ match len(arg):
477
+ case 2:
478
+ ch_list.append(f"{arg[0]}:{arg[1]}")
479
+ case 1:
480
+ ch_list.append(f"{arg[0]}")
481
+ case _:
482
+ raise ValueError(
483
+ "Invalid argument: when providing a tuple or list, it must contain one or two elements."
484
+ )
485
+ else:
486
+ ch_list.append(f"{arg}")
487
+
488
+ # else:
489
+ # ch_list = "(@" + ",".join([str(arg) for arg in args]) + ")"
490
+
491
+ return "(@" + ",".join(x for x in ch_list if x.isdigit() or ":" in x) + ")"
492
+
493
+
494
+ def count_number_of_channels(channel_list: str) -> int:
495
+ """
496
+ Given a proper channel list, this function counts the number of channels.
497
+ For ranges, it returns the actual number of channels that are included in the range.
498
+
499
+ >>> count_number_of_channels("(@1,2,3,4,5)")
500
+ 5
501
+ >>> count_number_of_channels("(@1, 3, 5)")
502
+ 3
503
+ >>> count_number_of_channels("(@2:7)")
504
+ 6
505
+
506
+ Args:
507
+ channel_list: a channel list as understood by the SCPI commands of DAQ6510.
508
+
509
+ Returns:
510
+ The number of channels in the list.
511
+ """
512
+
513
+ match = re.match(r"\(@(.*)\)", channel_list)
514
+ if match is None:
515
+ logger.error(f"Invalid channel specification in '{channel_list}'")
516
+ return 0
517
+ group = match.groups()[0]
518
+
519
+ parts = group.replace(" ", "").split(",")
520
+
521
+ try:
522
+ count = 0
523
+ for part in parts:
524
+ if ":" in part:
525
+ channels = part.split(":")
526
+ if len(channels) != 2:
527
+ raise ValueError()
528
+ count += int(channels[1]) - int(channels[0]) + 1
529
+ else:
530
+ if not part.isdigit():
531
+ raise ValueError()
532
+ count += 1
533
+ except ValueError:
534
+ logger.error(f"Invalid channel specification in '{channel_list}'")
535
+ return 0
536
+
537
+ return count
538
+
539
+
540
+ def get_channel_names(channel_list: str) -> list[str]:
541
+ """
542
+ Generate a list of channel names from a given channel list.
543
+
544
+ Args:
545
+ channel_list: a channel list as understood by the SCPI.
546
+
547
+ Returns:
548
+ A list of channel names.
549
+ """
550
+
551
+ match = re.match(r"\(@(.*)\)", channel_list)
552
+ if match is None:
553
+ logger.error(f"Invalid channel specification in '{channel_list}'")
554
+ return []
555
+
556
+ group = match.groups()[0]
557
+
558
+ parts = group.replace(" ", "").split(",")
559
+
560
+ try:
561
+ names: list[str] = []
562
+
563
+ for part in parts:
564
+ if ":" in part:
565
+ channels = part.split(":")
566
+ if len(channels) != 2:
567
+ raise ValueError()
568
+ names.extend(str(ch) for ch in range(int(channels[0]), int(channels[1]) + 1))
569
+ else:
570
+ if not part.isdigit():
571
+ raise ValueError()
572
+ names.append(part)
573
+
574
+ # If there are still any invalid names, raise an error
575
+ if not all(True if x and x.isdigit() else False for x in names):
576
+ raise ValueError()
577
+
578
+ except ValueError:
579
+ logger.error(f"Invalid channel specification in '{channel_list}'")
580
+ return []
581
+
582
+ return names
egse/setup.py CHANGED
@@ -936,9 +936,13 @@ class SetupManager:
936
936
  def set_default_source(self, source: str):
937
937
  self._default_source = source
938
938
 
939
- def load_setup(self, setup_id: int = None, **kwargs):
940
- source = kwargs.get("source") or self._default_source
939
+ def get_default_source(self):
940
+ # Trigger provider discovery because they can change the default source if they handle core-services
941
+ _ = self.providers
942
+ return self._default_source
941
943
 
944
+ def load_setup(self, setup_id: int = None, **kwargs):
945
+ source = kwargs.get("source") or self.get_default_source()
942
946
  for provider in self.providers:
943
947
  if provider.can_handle(source):
944
948
  return provider.load_setup(setup_id, **kwargs)
@@ -947,7 +951,7 @@ class SetupManager:
947
951
  return LocalSetupProvider().load_setup(setup_id, **kwargs)
948
952
 
949
953
  def submit_setup(self, setup: Setup, description: str, **kwargs):
950
- source = kwargs.get("source") or self._default_source
954
+ source = kwargs.get("source") or self.get_default_source()
951
955
  for provider in self.providers:
952
956
  if provider.can_handle(source):
953
957
  return provider.submit_setup(setup, description, **kwargs)
@@ -957,7 +961,6 @@ class SetupManager:
957
961
 
958
962
  _setup_manager = SetupManager()
959
963
 
960
-
961
964
  if __name__ == "__main__":
962
965
  from egse.env import setup_env
963
966
 
egse/socketdevice.py CHANGED
@@ -45,6 +45,7 @@ class SocketDevice(DeviceConnectionInterface, DeviceTransport):
45
45
  self.connect_timeout = connect_timeout
46
46
  self.read_timeout = read_timeout
47
47
  self.separator = separator
48
+ self.separator_str = separator.decode()
48
49
  self.socket = None
49
50
 
50
51
  @property
@@ -57,8 +58,8 @@ class SocketDevice(DeviceConnectionInterface, DeviceTransport):
57
58
  Connect the device.
58
59
 
59
60
  Raises:
60
- ConnectionError: When the connection could not be established. Check the logging
61
- messages for more detail.
61
+ ConnectionError: When the connection could not be established.
62
+ Check the logging messages for more detail.
62
63
  TimeoutError: When the connection timed out.
63
64
  ValueError: When hostname or port number are not provided.
64
65
  """
@@ -92,9 +93,9 @@ class SocketDevice(DeviceConnectionInterface, DeviceTransport):
92
93
  except TimeoutError as exc:
93
94
  raise TimeoutError(f"{self.device_name}: Connection to {self.hostname}:{self.port} timed out.") from exc
94
95
  except socket.gaierror as exc:
95
- raise ConnectionError(f"{self.device_name}: socket address info error for {self.hostname}") from exc
96
+ raise ConnectionError(f"{self.device_name}: Socket address info error for {self.hostname}") from exc
96
97
  except socket.herror as exc:
97
- raise ConnectionError(f"{self.device_name}: socket host address error for {self.hostname}") from exc
98
+ raise ConnectionError(f"{self.device_name}: Socket host address error for {self.hostname}") from exc
98
99
  except OSError as exc:
99
100
  raise ConnectionError(f"{self.device_name}: OSError caught ({exc}).") from exc
100
101
 
@@ -108,6 +109,8 @@ class SocketDevice(DeviceConnectionInterface, DeviceTransport):
108
109
  ConnectionError when the socket could not be closed.
109
110
  """
110
111
 
112
+ assert self.socket is not None # extra check + for mypy type checking
113
+
111
114
  try:
112
115
  if self.is_connection_open:
113
116
  logger.debug(f"Disconnecting from {self.hostname}")
@@ -143,12 +146,12 @@ class SocketDevice(DeviceConnectionInterface, DeviceTransport):
143
146
  If `self.read_timeout` was set to None in the constructor, this will block anyway.
144
147
  """
145
148
  if not self.socket:
146
- raise DeviceConnectionError(self.device_name, "Not connected")
149
+ raise DeviceConnectionError(self.device_name, "The device is not connected, connect before reading.")
147
150
 
148
151
  buf_size = 1024 * 4
149
152
  response = bytearray()
150
153
 
151
- # If read_timeout is None we preserve blocking behaviour; otherwise enforce overall timeout.
154
+ # If read_timeout is None we preserve blocking behavior; otherwise enforce overall timeout.
152
155
  if self.read_timeout is None:
153
156
  end_time = None
154
157
  else:
@@ -211,7 +214,11 @@ class SocketDevice(DeviceConnectionInterface, DeviceTransport):
211
214
  there was a socket related error.
212
215
  """
213
216
 
217
+ if not self.socket:
218
+ raise DeviceConnectionError(self.device_name, "The device is not connected, connect before writing.")
219
+
214
220
  try:
221
+ command += self.separator_str if not command.endswith(self.separator_str) else ""
215
222
  self.socket.sendall(command.encode())
216
223
  except socket.timeout as exc:
217
224
  raise DeviceTimeoutError(self.device_name, "Socket timeout error") from exc
@@ -237,22 +244,14 @@ class SocketDevice(DeviceConnectionInterface, DeviceTransport):
237
244
  there was a socket related error.
238
245
  """
239
246
 
240
- try:
241
- # Attempt to send the complete command
242
-
243
- self.socket.sendall(command.encode())
244
-
245
- # wait for, read and return the response (will be at most TBD chars)
247
+ if not self.socket:
248
+ raise DeviceConnectionError(self.device_name, "The device is not connected, connect before writing.")
246
249
 
247
- return_string = self.read()
250
+ self.write(command)
248
251
 
249
- return return_string
252
+ response = self.read()
250
253
 
251
- except socket.timeout as exc:
252
- raise DeviceTimeoutError(self.device_name, "Socket timeout error") from exc
253
- except socket.error as exc:
254
- # Interpret any socket-related error as an I/O error
255
- raise DeviceConnectionError(self.device_name, "Socket communication error.") from exc
254
+ return response
256
255
 
257
256
 
258
257
  class AsyncSocketDevice(AsyncDeviceInterface, AsyncDeviceTransport):
egse/system.py CHANGED
@@ -379,7 +379,7 @@ def ignore_m_warning(modules=None):
379
379
  pass
380
380
 
381
381
 
382
- def now(utc: bool = True):
382
+ def now(utc: bool = True) -> datetime.datetime:
383
383
  """Returns a datetime object for the current time in UTC or local time."""
384
384
  if utc:
385
385
  return datetime.datetime.now(tz=datetime.timezone.utc)