keithley-tempcontrol 0.17.3__tar.gz → 0.17.4__tar.gz

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.
Files changed (21) hide show
  1. {keithley_tempcontrol-0.17.3 → keithley_tempcontrol-0.17.4}/PKG-INFO +1 -1
  2. {keithley_tempcontrol-0.17.3 → keithley_tempcontrol-0.17.4}/pyproject.toml +1 -1
  3. {keithley_tempcontrol-0.17.3 → keithley_tempcontrol-0.17.4}/src/egse/tempcontrol/keithley/daq6510.py +31 -22
  4. keithley_tempcontrol-0.17.4/src/egse/tempcontrol/keithley/daq6510_adev.py +77 -0
  5. {keithley_tempcontrol-0.17.3 → keithley_tempcontrol-0.17.4}/src/egse/tempcontrol/keithley/daq6510_cs.py +89 -44
  6. {keithley_tempcontrol-0.17.3 → keithley_tempcontrol-0.17.4}/src/egse/tempcontrol/keithley/daq6510_dev.py +78 -20
  7. {keithley_tempcontrol-0.17.3 → keithley_tempcontrol-0.17.4}/src/egse/tempcontrol/keithley/daq6510_mon.py +58 -22
  8. {keithley_tempcontrol-0.17.3 → keithley_tempcontrol-0.17.4}/src/egse/tempcontrol/keithley/daq6510_protocol.py +18 -35
  9. {keithley_tempcontrol-0.17.3 → keithley_tempcontrol-0.17.4}/src/egse/tempcontrol/keithley/daq6510_sim.py +117 -60
  10. {keithley_tempcontrol-0.17.3 → keithley_tempcontrol-0.17.4}/src/keithley_tempcontrol/cgse_services.py +26 -7
  11. keithley_tempcontrol-0.17.3/justfile +0 -25
  12. keithley_tempcontrol-0.17.3/service_registry.db +0 -0
  13. keithley_tempcontrol-0.17.3/src/egse/tempcontrol/keithley/daq6510_adev.py +0 -97
  14. {keithley_tempcontrol-0.17.3 → keithley_tempcontrol-0.17.4}/.gitignore +0 -0
  15. {keithley_tempcontrol-0.17.3 → keithley_tempcontrol-0.17.4}/README.md +0 -0
  16. {keithley_tempcontrol-0.17.3 → keithley_tempcontrol-0.17.4}/noxfile.py +0 -0
  17. {keithley_tempcontrol-0.17.3 → keithley_tempcontrol-0.17.4}/src/egse/tempcontrol/keithley/__init__.py +0 -0
  18. {keithley_tempcontrol-0.17.3 → keithley_tempcontrol-0.17.4}/src/egse/tempcontrol/keithley/daq6510.yaml +0 -0
  19. {keithley_tempcontrol-0.17.3 → keithley_tempcontrol-0.17.4}/src/keithley_tempcontrol/__init__.py +0 -0
  20. {keithley_tempcontrol-0.17.3 → keithley_tempcontrol-0.17.4}/src/keithley_tempcontrol/cgse_explore.py +0 -0
  21. {keithley_tempcontrol-0.17.3 → keithley_tempcontrol-0.17.4}/src/keithley_tempcontrol/settings.yaml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: keithley-tempcontrol
3
- Version: 0.17.3
3
+ Version: 0.17.4
4
4
  Summary: Keithley Temperature Control for CGSE
5
5
  Author: IvS KU Leuven
6
6
  Maintainer-email: Rik Huygen <rik.huygen@kuleuven.be>, Sara Regibo <sara.regibo@kuleuven.be>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "keithley-tempcontrol"
3
- version = "0.17.3"
3
+ version = "0.17.4"
4
4
  description = "Keithley Temperature Control for CGSE"
5
5
  authors = [
6
6
  {name = "IvS KU Leuven"}
@@ -1,33 +1,40 @@
1
1
  import logging
2
2
  import re
3
3
  from pathlib import Path
4
- from typing import Dict, Union
4
+ from typing import Dict
5
5
  from typing import List
6
6
  from typing import Tuple
7
7
 
8
+ from egse.connect import get_endpoint
8
9
  from egse.decorators import dynamic_interface
9
10
  from egse.device import DeviceConnectionState
10
11
  from egse.device import DeviceInterface
11
- from egse.mixin import DynamicCommandMixin, CommandType
12
+ from egse.log import logger
13
+ from egse.mixin import CommandType
14
+ from egse.mixin import DynamicCommandMixin
12
15
  from egse.mixin import add_lf
13
16
  from egse.mixin import dynamic_command
14
17
  from egse.proxy import Proxy
15
18
  from egse.settings import Settings
16
- from egse.tempcontrol.keithley.daq6510_dev import DAQ6510EthernetInterface
17
- from egse.zmq_ser import connect_address
18
-
19
- logger = logging.getLogger(__name__)
19
+ from egse.tempcontrol.keithley.daq6510_dev import DAQ6510
20
20
 
21
21
  HERE = Path(__file__).parent
22
22
 
23
- CTRL_SETTINGS = Settings.load("Keithley Control Server")
24
- FW_SETTINGS = Settings.load("Keithley DAQ6510")
25
- DEVICE_SETTINGS = Settings.load(location=HERE, filename="daq6510.yaml")
23
+ cs_settings = Settings.load("Keithley Control Server")
24
+ dev_settings = Settings.load("Keithley DAQ6510")
26
25
 
26
+ PROTOCOL = cs_settings.get("PROTOCOL", "tcp")
27
+ HOSTNAME = cs_settings.get("HOSTNAME", "localhost")
28
+ COMMANDING_PORT = cs_settings.get("COMMANDING_PORT", 0)
29
+ TIMEOUT = cs_settings.get("TIMEOUT")
30
+ SERVICE_TYPE = cs_settings.get("SERVICE_TYPE", "daq6510")
27
31
 
28
32
  DEFAULT_BUFFER_1 = "defbuffer1"
29
33
  DEFAULT_BUFFER_2 = "defbuffer2"
30
34
 
35
+ DEV_HOST = dev_settings.get("HOSTNAME")
36
+ DEV_PORT = dev_settings.get("PORT")
37
+
31
38
 
32
39
  class DAQ6510Interface(DeviceInterface):
33
40
  """
@@ -35,7 +42,7 @@ class DAQ6510Interface(DeviceInterface):
35
42
  """
36
43
 
37
44
  @dynamic_interface
38
- def send_command(self, command: str, response: bool) -> Union[None, str]:
45
+ def send_command(self, command: str, response: bool) -> str | None:
39
46
  """Sends the given SCPI command to the device.
40
47
 
41
48
  The valid commands are described in the DAQ6510 Reference Manual [DAQ6510-901-01 Rev. B / September 2019].
@@ -219,12 +226,12 @@ class DAQ6510Controller(DAQ6510Interface, DynamicCommandMixin):
219
226
  through an Ethernet interface.
220
227
  """
221
228
 
222
- def __init__(self, hostname: str = FW_SETTINGS.HOSTNAME, port: int = FW_SETTINGS.PORT):
229
+ def __init__(self, hostname: str = DEV_HOST, port: int = DEV_PORT):
223
230
  """Opens a TCP/IP socket connection with the Keithley DAQ6510 Hardware.
224
231
 
225
232
  Args:
226
- hostname (str): IP address or fully qualified hostname of the Hexapod hardware controller. The default is
227
- defined in the ``settings.yaml`` configuration file.
233
+ hostname (str): IP address or fully qualified hostname of the DAQ6510 hardware controller.
234
+ The default is defined in the ``settings.yaml`` configuration file.
228
235
  port (int): IP port number to connect to, by default set in the ``settings.yaml`` configuration file.
229
236
 
230
237
  Raises:
@@ -235,7 +242,7 @@ class DAQ6510Controller(DAQ6510Interface, DynamicCommandMixin):
235
242
 
236
243
  logger.debug(f"Initializing the DAQ6510 Controller with hostname={hostname} on port={port}")
237
244
 
238
- self.daq = self.transport = DAQ6510EthernetInterface(hostname, port)
245
+ self.daq = self.transport = DAQ6510(hostname, port)
239
246
 
240
247
  # We set the default buffer here, this can be changed with the `create_buffer()` method.
241
248
 
@@ -277,7 +284,7 @@ class DAQ6510Controller(DAQ6510Interface, DynamicCommandMixin):
277
284
 
278
285
  return self.daq.is_connected()
279
286
 
280
- def send_command(self, command: str, response: bool) -> Union[None, str]:
287
+ def send_command(self, command: str, response: bool) -> str | None:
281
288
  """Sends an SCPI command to the device.
282
289
 
283
290
  The valid commands are described in the DAQ6510 Reference Manual [DAQ6510-901-01 Rev. B / September 2019].
@@ -292,7 +299,7 @@ class DAQ6510Controller(DAQ6510Interface, DynamicCommandMixin):
292
299
 
293
300
  return self.daq.trans(command) if response else self.daq.write(command)
294
301
 
295
- def read_buffer(self, start: int, end: int, buffer_name: str = DEFAULT_BUFFER_1, elements: List[str] = None):
302
+ def read_buffer(self, start: int, end: int, buffer_name: str = DEFAULT_BUFFER_1, elements: list[str] = None):
296
303
  """Reads specific data elements (measurements) from the given buffer.
297
304
 
298
305
  Elements that can be specified to read out:
@@ -566,10 +573,10 @@ class DAQ6510Proxy(Proxy, DAQ6510Interface):
566
573
 
567
574
  def __init__(
568
575
  self,
569
- protocol: str = CTRL_SETTINGS.PROTOCOL,
570
- hostname: str = CTRL_SETTINGS.HOSTNAME,
571
- port: int = CTRL_SETTINGS.COMMANDING_PORT,
572
- timeout: int = CTRL_SETTINGS.TIMEOUT * 1000, # Timeout [ms]: > scan count * interval + (one scan duration)
576
+ protocol: str = PROTOCOL,
577
+ hostname: str = HOSTNAME,
578
+ port: int = COMMANDING_PORT,
579
+ timeout: float = TIMEOUT, # Timeout [s]: > scan count * interval + (one scan duration)
573
580
  ):
574
581
  """Initialisation of a DAQ6510Proxy.
575
582
 
@@ -578,10 +585,12 @@ class DAQ6510Proxy(Proxy, DAQ6510Interface):
578
585
  hostname (str): Location of the Control Server (IP address) [default is taken from settings file]
579
586
  port (int): TCP port on which the Control Server is listening for commands [default is taken from settings
580
587
  file]
581
- timeout (int): Timeout by which to establish the connection [ms]
588
+ timeout (float): Timeout by which to establish the connection [s]
582
589
  """
583
590
 
584
- super().__init__(connect_address(protocol, hostname, port), timeout=timeout)
591
+ endpoint = get_endpoint(SERVICE_TYPE, protocol, hostname, port)
592
+
593
+ super().__init__(endpoint, timeout=timeout)
585
594
 
586
595
 
587
596
  def create_channel_list(*args) -> str:
@@ -0,0 +1,77 @@
1
+ __all__ = [
2
+ "DAQ6510",
3
+ ]
4
+
5
+ import asyncio
6
+ from typing import Any
7
+ from typing import Dict
8
+ from typing import Optional
9
+
10
+ from egse.log import logger
11
+ from egse.scpi import AsyncSCPIInterface
12
+ from egse.settings import Settings
13
+
14
+ dev_settings = Settings.load("Keithley DAQ6510")
15
+
16
+ DEV_HOST = dev_settings.get("HOSTNAME", "localhost")
17
+ DEV_PORT = dev_settings.get("PORT", 0)
18
+ DEVICE_NAME = dev_settings.get("DEVICE_NAME", "DAQ6510")
19
+ DEV_ID_VALIDATION = "DAQ6510"
20
+
21
+
22
+ class DAQ6510(AsyncSCPIInterface):
23
+ """Keithley DAQ6510 specific implementation."""
24
+
25
+ def __init__(self, hostname: str = DEV_HOST, port: int = DEV_PORT, settings: Optional[Dict[str, Any]] = None):
26
+ """Initialize a Keithley DAQ6510 interface.
27
+
28
+ Args:
29
+ hostname: Hostname or IP address
30
+ port: TCP port (default 5025 for SCPI)
31
+ settings: Additional device settings
32
+ """
33
+ super().__init__(
34
+ device_name=DEVICE_NAME,
35
+ hostname=hostname,
36
+ port=port,
37
+ settings=settings,
38
+ id_validation=DEV_ID_VALIDATION, # String that must appear in IDN? response
39
+ )
40
+
41
+ self._measurement_lock = asyncio.Lock()
42
+
43
+ async def get_measurement(self, channel: str) -> float:
44
+ """Get a measurement from a specific channel.
45
+
46
+ Args:
47
+ channel: Channel to measure (e.g., "101")
48
+
49
+ Returns:
50
+ The measured value as a float
51
+ """
52
+ async with self._measurement_lock:
53
+ cmd = "INIT:IMM"
54
+ logger.info(f"Sending {cmd}...")
55
+ await self.write(cmd)
56
+ cmd = "*WAI"
57
+ logger.info(f"Sending {cmd}...")
58
+ await self.write(cmd)
59
+
60
+ if channel == "101":
61
+ start_index = end_index = 1
62
+ elif channel == "102":
63
+ start_index = end_index = 2
64
+ else:
65
+ return float("nan")
66
+
67
+ response = (
68
+ (await self.trans(f'TRAC:DATA? {start_index}, {end_index}, "test1", CHAN, TST, READ')).decode().strip()
69
+ )
70
+
71
+ logger.info(f"{response = }")
72
+
73
+ ch, tst, val = response.split(",")
74
+
75
+ logger.info(f"Channel: {ch} Time: {tst} Value: {float(val):.4f}")
76
+
77
+ return float(val)
@@ -1,22 +1,29 @@
1
- import logging
2
1
  import multiprocessing
3
2
  import sys
4
3
 
5
- import click
6
- import invoke
7
4
  import rich
5
+ import typer
8
6
  import zmq
7
+
8
+ from egse.connect import get_endpoint
9
9
  from egse.control import ControlServer
10
10
  from egse.control import is_control_server_active
11
+ from egse.log import logger
12
+ from egse.logger import remote_logging
13
+ from egse.registry.client import RegistryClient
11
14
  from egse.settings import Settings
15
+ from egse.storage import store_housekeeping_information
12
16
  from egse.tempcontrol.keithley.daq6510 import DAQ6510Proxy
13
17
  from egse.tempcontrol.keithley.daq6510_protocol import DAQ6510Protocol
14
18
  from egse.zmq_ser import connect_address
15
- from prometheus_client import start_http_server
16
19
 
17
- logger = logging.getLogger(__name__)
20
+ cs_settings = Settings.load("Keithley Control Server")
18
21
 
19
- CTRL_SETTINGS = Settings.load("Keithley Control Server")
22
+ PROTOCOL = cs_settings.get("PROTOCOL", "tcp")
23
+ HOSTNAME = cs_settings.get("HOSTNAME", "localhost")
24
+ COMMANDING_PORT = cs_settings.get("COMMANDING_PORT", 0)
25
+ STORAGE_MNEMONIC = cs_settings.get("STORAGE_MNEMONIC", "DAQ6510")
26
+ SERVICE_TYPE = cs_settings.get("SERVICE_TYPE", "daq6510")
20
27
 
21
28
 
22
29
  def is_daq6510_cs_active(timeout: float = 0.5) -> bool:
@@ -28,7 +35,14 @@ def is_daq6510_cs_active(timeout: float = 0.5) -> bool:
28
35
  Returns: True if the Control Server is running and replied with the expected answer; False otherwise.
29
36
  """
30
37
 
31
- endpoint = connect_address(CTRL_SETTINGS.PROTOCOL, CTRL_SETTINGS.HOSTNAME, CTRL_SETTINGS.COMMANDING_PORT)
38
+ if COMMANDING_PORT == 0:
39
+ with RegistryClient() as client:
40
+ endpoint = client.get_endpoint(SERVICE_TYPE)
41
+ if endpoint is None:
42
+ logger.debug(f"No endpoint for {SERVICE_TYPE}")
43
+ return False
44
+ else:
45
+ endpoint = connect_address(PROTOCOL, HOSTNAME, COMMANDING_PORT)
32
46
 
33
47
  return is_control_server_active(endpoint, timeout)
34
48
 
@@ -62,13 +76,15 @@ class DAQ6510ControlServer(ControlServer):
62
76
 
63
77
  self.poller.register(self.dev_ctrl_cmd_sock, zmq.POLLIN)
64
78
 
79
+ self.register_service(service_type=cs_settings.SERVICE_TYPE)
80
+
65
81
  def get_communication_protocol(self) -> str:
66
82
  """Returns the communication protocol used by the Control Server.
67
83
 
68
84
  Returns: Communication protocol used by the Control Server, as specified in the settings.
69
85
  """
70
86
 
71
- return CTRL_SETTINGS.PROTOCOL
87
+ return cs_settings.PROTOCOL
72
88
 
73
89
  def get_commanding_port(self) -> int:
74
90
  """Returns the commanding port used by the Control Server.
@@ -76,7 +92,7 @@ class DAQ6510ControlServer(ControlServer):
76
92
  Returns: Commanding port used by the Control Server, as specified in the settings.
77
93
  """
78
94
 
79
- return CTRL_SETTINGS.COMMANDING_PORT
95
+ return cs_settings.COMMANDING_PORT
80
96
 
81
97
  def get_service_port(self):
82
98
  """Returns the service port used by the Control Server.
@@ -84,7 +100,7 @@ class DAQ6510ControlServer(ControlServer):
84
100
  Returns: Service port used by the Control Server, as specified in the settings.
85
101
  """
86
102
 
87
- return CTRL_SETTINGS.SERVICE_PORT
103
+ return cs_settings.SERVICE_PORT
88
104
 
89
105
  def get_monitoring_port(self):
90
106
  """Returns the monitoring port used by the Control Server.
@@ -92,7 +108,7 @@ class DAQ6510ControlServer(ControlServer):
92
108
  Returns: Monitoring port used by the Control Server, as specified in the settings.
93
109
  """
94
110
 
95
- return CTRL_SETTINGS.MONITORING_PORT
111
+ return cs_settings.MONITORING_PORT
96
112
 
97
113
  def get_storage_mnemonic(self):
98
114
  """Returns the storage mnemonics used by the Control Server.
@@ -104,57 +120,88 @@ class DAQ6510ControlServer(ControlServer):
104
120
  settings, "DAQ6510" will be used.
105
121
  """
106
122
 
107
- try:
108
- return CTRL_SETTINGS.STORAGE_MNEMONIC
109
- except AttributeError:
110
- return "DAQ6510"
123
+ return STORAGE_MNEMONIC
124
+
125
+ def is_storage_manager_active(self):
126
+ from egse.storage import is_storage_manager_active
127
+
128
+ return is_storage_manager_active()
129
+
130
+ def store_housekeeping_information(self, data):
131
+ """Send housekeeping information to the Storage manager."""
132
+
133
+ origin = self.get_storage_mnemonic()
134
+ store_housekeeping_information(origin, data)
135
+
136
+ def register_to_storage_manager(self):
137
+ from egse.storage import register_to_storage_manager
138
+ from egse.storage.persistence import TYPES
139
+
140
+ register_to_storage_manager(
141
+ origin=self.get_storage_mnemonic(),
142
+ persistence_class=TYPES["CSV"],
143
+ prep={
144
+ "column_names": list(self.device_protocol.get_housekeeping().keys()),
145
+ "mode": "a",
146
+ },
147
+ )
148
+
149
+ def unregister_from_storage_manager(self):
150
+ from egse.storage import unregister_from_storage_manager
151
+
152
+ unregister_from_storage_manager(origin=self.get_storage_mnemonic())
111
153
 
112
154
  def before_serve(self):
113
155
  """Steps to take before the Control Server is activated."""
114
156
 
115
- start_http_server(CTRL_SETTINGS.METRICS_PORT)
116
-
117
157
 
118
- @click.group()
119
- def cli():
120
- pass
158
+ app = typer.Typer(name="daq6510_cs")
121
159
 
122
160
 
123
- @cli.command()
161
+ @app.command()
124
162
  def start():
125
163
  """Starts the Keithley DAQ6510 Control Server."""
126
164
 
127
165
  multiprocessing.current_process().name = "daq6510_cs (start)"
128
166
 
129
- try:
130
- control_server = DAQ6510ControlServer()
131
- control_server.serve()
132
- except KeyboardInterrupt:
133
- logger.debug("Shutdown requested...exiting")
134
- except SystemExit as exit_code:
135
- logger.debug("System Exit with code {}.".format(exit_code))
136
- sys.exit(exit_code)
137
- except Exception:
138
- msg = "Cannot start the DAQ6510 Control Server"
139
- logger.exception(msg)
140
- rich.print(f"[red]{msg}.")
167
+ with remote_logging():
168
+ from egse.env import setup_env
169
+
170
+ setup_env()
171
+
172
+ try:
173
+ control_server = DAQ6510ControlServer()
174
+ control_server.serve()
175
+ except KeyboardInterrupt:
176
+ logger.debug("Shutdown requested...exiting")
177
+ except SystemExit as exit_code:
178
+ logger.debug("System Exit with code {}.".format(exit_code))
179
+ sys.exit(exit_code.code)
180
+ except Exception:
181
+ msg = "Cannot start the DAQ6510 Control Server"
182
+ logger.exception(msg)
183
+ rich.print(f"[red]{msg}.")
141
184
 
142
185
  return 0
143
186
 
144
187
 
145
- @cli.command()
188
+ @app.command()
146
189
  def start_bg():
147
190
  """Starts the DAQ6510 Control Server in the background."""
148
191
 
149
- invoke.run("daq6510_cs start", disown=True)
192
+ print("Starting the DAQ6510 in the background is not implemented.")
150
193
 
151
194
 
152
- @cli.command()
195
+ @app.command()
153
196
  def stop():
154
197
  """Sends a 'quit_server' command to the Keithley DAQ6510 Control Server."""
155
198
 
156
199
  multiprocessing.current_process().name = "daq6510_cs (stop)"
157
200
 
201
+ from egse.env import setup_env
202
+
203
+ setup_env()
204
+
158
205
  try:
159
206
  with DAQ6510Proxy() as daq:
160
207
  sp = daq.get_service_proxy()
@@ -165,17 +212,17 @@ def stop():
165
212
  rich.print(f"[red]{msg}, could not send the Quit command. [black]Check log messages.")
166
213
 
167
214
 
168
- @cli.command()
215
+ @app.command()
169
216
  def status():
170
217
  """Requests status information from the Control Server."""
171
218
 
172
219
  multiprocessing.current_process().name = "daq6510_cs (status)"
173
220
 
174
- protocol = CTRL_SETTINGS.PROTOCOL
175
- hostname = CTRL_SETTINGS.HOSTNAME
176
- port = CTRL_SETTINGS.COMMANDING_PORT
221
+ from egse.env import setup_env
177
222
 
178
- endpoint = connect_address(protocol, hostname, port)
223
+ setup_env()
224
+
225
+ endpoint = get_endpoint(SERVICE_TYPE, PROTOCOL, HOSTNAME, COMMANDING_PORT)
179
226
 
180
227
  if is_control_server_active(endpoint):
181
228
  rich.print("DAQ6510 CS: [green]active")
@@ -190,6 +237,4 @@ def status():
190
237
 
191
238
 
192
239
  if __name__ == "__main__":
193
- logging.basicConfig(level=logging.DEBUG, format=Settings.LOG_FORMAT_FULL)
194
-
195
- sys.exit(cli())
240
+ sys.exit(app())
@@ -1,23 +1,30 @@
1
- import logging
1
+ __all__ = [
2
+ "DAQ6510",
3
+ "DAQ6510Command",
4
+ ]
2
5
  import socket
3
6
  import time
4
7
 
5
8
  from egse.command import ClientServerCommand
6
9
  from egse.device import DeviceConnectionError
7
- from egse.device import DeviceConnectionInterface
8
10
  from egse.device import DeviceError
11
+ from egse.device import DeviceInterface
9
12
  from egse.device import DeviceTimeoutError
10
13
  from egse.device import DeviceTransport
14
+ from egse.log import logger
11
15
  from egse.settings import Settings
12
- from egse.system import Timer
13
-
14
- logger = logging.getLogger(__name__)
15
16
 
16
17
  IDENTIFICATION_QUERY = "*IDN?"
17
18
 
18
- DEVICE_SETTINGS = Settings.load("Keithley DAQ6510")
19
- DEVICE_NAME = "DAQ6510"
20
- READ_TIMEOUT = DEVICE_SETTINGS.TIMEOUT # [s], can be smaller than timeout (for DAQ6510Proxy) (e.g. 1s)
19
+ dev_settings = Settings.load("Keithley DAQ6510")
20
+
21
+ DEVICE_NAME = dev_settings.get("DEVICE_NAME", "DAQ6510")
22
+ DEV_HOST = dev_settings.get("HOSTNAME")
23
+ DEV_PORT = dev_settings.get("PORT")
24
+ READ_TIMEOUT = dev_settings.get("TIMEOUT") # [s], can be smaller than timeout (for DAQ6510Proxy) (e.g. 1s)
25
+
26
+ SEPARATOR = b"\n"
27
+ SEPARATOR_STR = SEPARATOR.decode()
21
28
 
22
29
 
23
30
  class DAQ6510Command(ClientServerCommand):
@@ -32,13 +39,13 @@ class DAQ6510Command(ClientServerCommand):
32
39
  """
33
40
 
34
41
  out = super().get_cmd_string(*args, **kwargs)
35
- return out + "\n"
42
+ return out + SEPARATOR_STR
36
43
 
37
44
 
38
- class DAQ6510EthernetInterface(DeviceConnectionInterface, DeviceTransport):
45
+ class DAQ6510(DeviceInterface, DeviceTransport):
39
46
  """Defines the low-level interface to the Keithley DAQ6510 Controller."""
40
47
 
41
- def __init__(self, hostname: str = None, port: int = None):
48
+ def __init__(self, hostname: str = DEV_HOST, port: int = DEV_PORT):
42
49
  """Initialisation of an Ethernet interface for the DAQ6510.
43
50
 
44
51
  Args:
@@ -48,12 +55,66 @@ class DAQ6510EthernetInterface(DeviceConnectionInterface, DeviceTransport):
48
55
 
49
56
  super().__init__()
50
57
 
51
- self.hostname = DEVICE_SETTINGS.HOSTNAME if hostname is None else hostname
52
- self.port = DEVICE_SETTINGS.PORT if port is None else port
58
+ self.device_name = DEVICE_NAME
59
+ self.hostname = hostname
60
+ self.port = port
53
61
  self._sock = None
54
62
 
55
63
  self._is_connection_open = False
56
64
 
65
+ def initialize(self, commands: list[tuple[str, bool]] = None, reset_device: bool = False) -> list[str | None]:
66
+ """Initialize the device with optional reset and command sequence.
67
+
68
+ Performs device initialization by optionally resetting the device and then
69
+ executing a sequence of commands. Each command can optionally expect a
70
+ response that will be logged for debugging purposes.
71
+
72
+ Args:
73
+ commands: List of tuples containing (command_string, expects_response).
74
+ Each tuple specifies a command to send and whether to wait for and
75
+ log the response. Defaults to None (no commands executed).
76
+ reset_device: Whether to send a reset command (*RST) before executing
77
+ the command sequence. Defaults to False.
78
+
79
+ Returns:
80
+ Response for each of the commands, or None when no response was expected.
81
+
82
+ Raises:
83
+ Any exceptions raised by the underlying write() or trans() methods,
84
+ typically communication errors or device timeouts.
85
+
86
+ Example:
87
+ responses = device.initialize(
88
+ [
89
+ ("*IDN?", True), # Query device ID, expect response
90
+ ("SYST:ERR?", True), # Check for errors, expect response
91
+ ("OUTP ON", False) # Enable output, no response expected
92
+ ],
93
+ reset_device=True
94
+ )
95
+ """
96
+
97
+ commands = commands or []
98
+ responses = []
99
+
100
+ if reset_device:
101
+ logger.info(f"Resetting the {self.device_name}...")
102
+ self.write("*RST") # this also resets the user-defined buffer
103
+
104
+ for cmd, expects_response in commands:
105
+ if expects_response:
106
+ logger.debug(f"Sending {cmd}...")
107
+ response = self.trans(cmd).decode().strip()
108
+ logger.debug(f"{response = }")
109
+ else:
110
+ logger.debug(f"Sending {cmd}...")
111
+ self.write(cmd)
112
+
113
+ return responses
114
+
115
+ def is_simulator(self) -> bool:
116
+ return False
117
+
57
118
  def connect(self) -> None:
58
119
  """Connects the device.
59
120
 
@@ -187,7 +248,7 @@ class DAQ6510EthernetInterface(DeviceConnectionInterface, DeviceTransport):
187
248
  """
188
249
 
189
250
  try:
190
- command += "\n" if not command.endswith("\n") else ""
251
+ command += SEPARATOR_STR if not command.endswith(SEPARATOR_STR) else ""
191
252
 
192
253
  self._sock.sendall(command.encode())
193
254
 
@@ -202,7 +263,7 @@ class DAQ6510EthernetInterface(DeviceConnectionInterface, DeviceTransport):
202
263
  raise DeviceConnectionError(DEVICE_NAME, msg)
203
264
  raise
204
265
 
205
- def trans(self, command: str) -> str:
266
+ def trans(self, command: str) -> bytes:
206
267
  """Sends a single command to the device controller and block until a response from the controller.
207
268
 
208
269
  This is seen as a transaction.
@@ -221,7 +282,7 @@ class DAQ6510EthernetInterface(DeviceConnectionInterface, DeviceTransport):
221
282
  try:
222
283
  # Attempt to send the complete command
223
284
 
224
- command += "\n" if not command.endswith("\n") else ""
285
+ command += SEPARATOR_STR if not command.endswith(SEPARATOR_STR) else ""
225
286
 
226
287
  self._sock.sendall(command.encode())
227
288
 
@@ -267,10 +328,7 @@ class DAQ6510EthernetInterface(DeviceConnectionInterface, DeviceTransport):
267
328
  break
268
329
  except socket.timeout:
269
330
  logger.warning(f"Socket timeout error for {self.hostname}:{self.port}")
270
- return b"\r\n"
271
- except TimeoutError as exc:
272
- logger.warning(f"Socket timeout error: {exc}")
273
- return b"\r\n"
331
+ return SEPARATOR
274
332
  finally:
275
333
  self._sock.settimeout(saved_timeout)
276
334