keithley-tempcontrol 2025.0.8__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.
- egse/tempcontrol/keithley/__init__.py +4 -0
- egse/tempcontrol/keithley/daq6510.py +723 -0
- egse/tempcontrol/keithley/daq6510.yaml +102 -0
- egse/tempcontrol/keithley/daq6510_cs.py +197 -0
- egse/tempcontrol/keithley/daq6510_devif.py +359 -0
- egse/tempcontrol/keithley/daq6510_protocol.py +105 -0
- keithley_tempcontrol-2025.0.8.dist-info/METADATA +27 -0
- keithley_tempcontrol-2025.0.8.dist-info/RECORD +10 -0
- keithley_tempcontrol-2025.0.8.dist-info/WHEEL +4 -0
- keithley_tempcontrol-2025.0.8.dist-info/entry_points.txt +14 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
BaseClass:
|
|
2
|
+
egse.tempcontrol.keithley.daq6510.DAQ6510Interface
|
|
3
|
+
ProxyClass:
|
|
4
|
+
egse.tempcontrol.keithley.daq6510.DAQ6510Proxy
|
|
5
|
+
ControlServerClass:
|
|
6
|
+
egse.tempcontrol.keithley.daq6510_cs.DAQ6510ControlServer
|
|
7
|
+
ControlServer:
|
|
8
|
+
egse.tempcontrol.keithley.daq6510_cs
|
|
9
|
+
|
|
10
|
+
Commands:
|
|
11
|
+
|
|
12
|
+
# Each of these groups is parsed and used on both the server and the client side.
|
|
13
|
+
# The group name (e.g. is_simulator) will be monkey-patched in the Proxy class for the device or service.
|
|
14
|
+
# The other fields are:
|
|
15
|
+
# description: Used by the doc_string method to generate a help string
|
|
16
|
+
# cmd: Command string that will eventually be sent to the hardware controller for the
|
|
17
|
+
# device after the arguments have been filled.
|
|
18
|
+
# device_method: The name of the method to be called on the device class.
|
|
19
|
+
# These should all be defined by the base class for the device, i.e. KeithleyBase.
|
|
20
|
+
# response: The name of the method to be called from the device protocol.
|
|
21
|
+
# This method should exist in the sub-class of the CommandProtocol base class, i.e.
|
|
22
|
+
# in this case it will be the KeithleyProtocol class.
|
|
23
|
+
|
|
24
|
+
# Definition of the DeviceInterface
|
|
25
|
+
|
|
26
|
+
disconnect:
|
|
27
|
+
description : Disconnects from the Keithley controller. This command will be sent to the
|
|
28
|
+
Keithley Control Server which will then disconnect from the hardware controller.
|
|
29
|
+
This command doesn't affect the ZeroMQ connection of this Proxy to the
|
|
30
|
+
control server. Use the service command ``disconnect()`` to disconnect
|
|
31
|
+
from the control server.
|
|
32
|
+
|
|
33
|
+
connect:
|
|
34
|
+
description: Connects the Keithley hardware controller
|
|
35
|
+
|
|
36
|
+
reconnect:
|
|
37
|
+
description: Reconnects the Keithley hardware controller.
|
|
38
|
+
|
|
39
|
+
This command will force a disconnect and then try to re-connect to the controller.
|
|
40
|
+
|
|
41
|
+
is_simulator:
|
|
42
|
+
description: Asks if the control server is a simulator instead of the real KeithleyController class.
|
|
43
|
+
returns: bool | True if the far end is a simulator instead of the real hardware
|
|
44
|
+
|
|
45
|
+
is_connected:
|
|
46
|
+
description: Checks if the Keithley Hardware Controller is connected.
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Definition of the device commands
|
|
50
|
+
|
|
51
|
+
info:
|
|
52
|
+
description: Retrieves basic information about the Keithley and the Controller.
|
|
53
|
+
|
|
54
|
+
reset:
|
|
55
|
+
description: Resets the device. This returns the instrument to default settings, and cancels all
|
|
56
|
+
pending commands.
|
|
57
|
+
|
|
58
|
+
send_command:
|
|
59
|
+
description: Sends a SCPI command to the device
|
|
60
|
+
cmd: '{command} {response}'
|
|
61
|
+
|
|
62
|
+
set_time:
|
|
63
|
+
description: Sets the absolute date and time of the device.
|
|
64
|
+
cmd: '{year} {month} {day} {hour} {minute} {second}'
|
|
65
|
+
|
|
66
|
+
get_time:
|
|
67
|
+
description: Gets the time and time of the device.
|
|
68
|
+
|
|
69
|
+
read_buffer:
|
|
70
|
+
description: Reads specific data elements (measurements) from the given buffer.
|
|
71
|
+
|
|
72
|
+
get_buffer_count:
|
|
73
|
+
description: The number of readings in the specified reading buffer.
|
|
74
|
+
|
|
75
|
+
get_buffer_capacity:
|
|
76
|
+
description: The total number of readings that the buffer can store.
|
|
77
|
+
|
|
78
|
+
delete_buffer:
|
|
79
|
+
description: Deletes the given buffer.
|
|
80
|
+
|
|
81
|
+
clear_buffer:
|
|
82
|
+
description: Clears all readings and statistics from the specified buffer.
|
|
83
|
+
|
|
84
|
+
create_buffer:
|
|
85
|
+
description: Creates a Reading Buffer with the given name.
|
|
86
|
+
|
|
87
|
+
configure_sensors:
|
|
88
|
+
description: Allows to configure the different sensors in the `channel_list`. Each sensor
|
|
89
|
+
in the list will be configured according to the settings given in the
|
|
90
|
+
`sense` dictionary.
|
|
91
|
+
cmd: '{channel_list} {sense}'
|
|
92
|
+
|
|
93
|
+
setup_measurements:
|
|
94
|
+
description: Sets up the measurements for the given channel list.
|
|
95
|
+
cmd: '{channel_list}'
|
|
96
|
+
|
|
97
|
+
perform_measurement:
|
|
98
|
+
description: Performs the actual measurements. This function will wait until all
|
|
99
|
+
measurements have completed, so be careful with the arguments `count` and
|
|
100
|
+
`interval` as they will multiply into the number of seconds that you will
|
|
101
|
+
have to wait for the response.
|
|
102
|
+
cmd: '{channel_list} {count} {interval}'
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from prometheus_client import start_http_server
|
|
4
|
+
|
|
5
|
+
import multiprocessing
|
|
6
|
+
multiprocessing.current_process().name = "daq6510_cs"
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
import invoke
|
|
12
|
+
import rich
|
|
13
|
+
import zmq
|
|
14
|
+
|
|
15
|
+
from egse.control import ControlServer
|
|
16
|
+
from egse.control import is_control_server_active
|
|
17
|
+
from egse.settings import Settings
|
|
18
|
+
from egse.tempcontrol.keithley.daq6510 import DAQ6510Proxy
|
|
19
|
+
from egse.tempcontrol.keithley.daq6510_protocol import DAQ6510Protocol
|
|
20
|
+
from egse.zmq_ser import connect_address
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
CTRL_SETTINGS = Settings.load("Keithley Control Server")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def is_daq6510_cs_active(timeout: float = 0.5) -> bool:
|
|
28
|
+
""" Checks if the DAQ6510 Control Server is running.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
timeout (float): Timeout when waiting for a reply [s, default=0.5]
|
|
32
|
+
|
|
33
|
+
Returns: True if the Control Server is running and replied with the expected answer; False otherwise.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
endpoint = connect_address(
|
|
37
|
+
CTRL_SETTINGS.PROTOCOL, CTRL_SETTINGS.HOSTNAME, CTRL_SETTINGS.COMMANDING_PORT
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
return is_control_server_active(endpoint, timeout)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class DAQ6510ControlServer(ControlServer):
|
|
44
|
+
"""
|
|
45
|
+
Keithley DAQ6510ControlServer - Command and monitor the Keithley Data Acquisition System.
|
|
46
|
+
|
|
47
|
+
This class works as a command and monitoring server to control the DAQ6510 Controller.
|
|
48
|
+
|
|
49
|
+
The sever binds to the following ZeroMQ sockets:
|
|
50
|
+
|
|
51
|
+
- REQ-REP socket that can be used as a command server. Any client can connect and send a command to the
|
|
52
|
+
DAQ6510 controller.
|
|
53
|
+
|
|
54
|
+
- PUB-SUP socket that serves as a monitoring server. It will send out DAQ6510 status information to all the
|
|
55
|
+
connected clients every DELAY seconds.
|
|
56
|
+
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(self):
|
|
60
|
+
""" Initialisation of a DAQ6510 Control Server."""
|
|
61
|
+
|
|
62
|
+
super().__init__()
|
|
63
|
+
|
|
64
|
+
self.device_protocol = DAQ6510Protocol(self)
|
|
65
|
+
|
|
66
|
+
self.logger.info(f"Binding ZeroMQ socket to {self.device_protocol.get_bind_address()}")
|
|
67
|
+
|
|
68
|
+
self.device_protocol.bind(self.dev_ctrl_cmd_sock)
|
|
69
|
+
|
|
70
|
+
self.poller.register(self.dev_ctrl_cmd_sock, zmq.POLLIN)
|
|
71
|
+
|
|
72
|
+
def get_communication_protocol(self) -> str:
|
|
73
|
+
""" Returns the communication protocol used by the Control Server.
|
|
74
|
+
|
|
75
|
+
Returns: Communication protocol used by the Control Server, as specified in the settings.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
return CTRL_SETTINGS.PROTOCOL
|
|
79
|
+
|
|
80
|
+
def get_commanding_port(self) -> int:
|
|
81
|
+
""" Returns the commanding port used by the Control Server.
|
|
82
|
+
|
|
83
|
+
Returns: Commanding port used by the Control Server, as specified in the settings.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
return CTRL_SETTINGS.COMMANDING_PORT
|
|
87
|
+
|
|
88
|
+
def get_service_port(self):
|
|
89
|
+
""" Returns the service port used by the Control Server.
|
|
90
|
+
|
|
91
|
+
Returns: Service port used by the Control Server, as specified in the settings.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
return CTRL_SETTINGS.SERVICE_PORT
|
|
95
|
+
|
|
96
|
+
def get_monitoring_port(self):
|
|
97
|
+
""" Returns the monitoring port used by the Control Server.
|
|
98
|
+
|
|
99
|
+
Returns: Monitoring port used by the Control Server, as specified in the settings.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
return CTRL_SETTINGS.MONITORING_PORT
|
|
103
|
+
|
|
104
|
+
def get_storage_mnemonic(self):
|
|
105
|
+
""" Returns the storage mnemonics used by the Control Server.
|
|
106
|
+
|
|
107
|
+
This is a string that will appear in the filename with the housekeeping information of the device, as a way of
|
|
108
|
+
identifying the device. If this is not implemented in the sub-class, then the class name will be used.
|
|
109
|
+
|
|
110
|
+
Returns: Storage mnemonics used by the Control Server, as specified in the settings. If not specified in the
|
|
111
|
+
settings, "DAQ6510" will be used.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
return CTRL_SETTINGS.STORAGE_MNEMONIC
|
|
116
|
+
except AttributeError:
|
|
117
|
+
return "DAQ6510"
|
|
118
|
+
|
|
119
|
+
def before_serve(self):
|
|
120
|
+
""" Steps to take before the Control Server is activated."""
|
|
121
|
+
|
|
122
|
+
start_http_server(CTRL_SETTINGS.METRICS_PORT)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@click.group()
|
|
126
|
+
def cli():
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@cli.command()
|
|
131
|
+
def start():
|
|
132
|
+
""" Starts the Keithley DAQ6510 Control Server."""
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
control_server = DAQ6510ControlServer()
|
|
136
|
+
control_server.serve()
|
|
137
|
+
except KeyboardInterrupt:
|
|
138
|
+
logger.debug("Shutdown requested...exiting")
|
|
139
|
+
except SystemExit as exit_code:
|
|
140
|
+
logger.debug("System Exit with code {}.".format(exit_code))
|
|
141
|
+
sys.exit(exit_code)
|
|
142
|
+
except Exception:
|
|
143
|
+
msg = "Cannot start the DAQ6510 Control Server"
|
|
144
|
+
logger.exception(msg)
|
|
145
|
+
rich.print(f"[red]{msg}.")
|
|
146
|
+
|
|
147
|
+
return 0
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@cli.command()
|
|
151
|
+
def start_bg():
|
|
152
|
+
""" Starts the DAQ6510 Control Server in the background."""
|
|
153
|
+
|
|
154
|
+
invoke.run("daq6510_cs start", disown=True)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@cli.command()
|
|
158
|
+
def stop():
|
|
159
|
+
""" Sends a 'quit_server' command to the Keithley DAQ6510 Control Server."""
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
with DAQ6510Proxy() as daq:
|
|
163
|
+
sp = daq.get_service_proxy()
|
|
164
|
+
sp.quit_server()
|
|
165
|
+
except ConnectionError as exc:
|
|
166
|
+
msg = "Cannot stop the DAQ6510 Control Server"
|
|
167
|
+
logger.exception(msg)
|
|
168
|
+
rich.print(f"[red]{msg}, could not send the Quit command. [black]Check log messages.")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@cli.command()
|
|
172
|
+
def status():
|
|
173
|
+
""" Requests status information from the Control Server."""
|
|
174
|
+
|
|
175
|
+
protocol = CTRL_SETTINGS.PROTOCOL
|
|
176
|
+
hostname = CTRL_SETTINGS.HOSTNAME
|
|
177
|
+
port = CTRL_SETTINGS.COMMANDING_PORT
|
|
178
|
+
|
|
179
|
+
endpoint = connect_address(protocol, hostname, port)
|
|
180
|
+
|
|
181
|
+
if is_control_server_active(endpoint):
|
|
182
|
+
rich.print(f"DAQ6510 CS: [green]active")
|
|
183
|
+
with DAQ6510Proxy() as daq6510:
|
|
184
|
+
sim = daq6510.is_simulator()
|
|
185
|
+
connected = daq6510.is_connected()
|
|
186
|
+
ip = daq6510.get_ip_address()
|
|
187
|
+
rich.print(f"mode: {'simulator' if sim else 'device'}{' not' if not connected else ''} connected")
|
|
188
|
+
rich.print(f"hostname: {ip}")
|
|
189
|
+
else:
|
|
190
|
+
rich.print(f"DAQ6510 CS: [red]not active")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
if __name__ == "__main__":
|
|
194
|
+
|
|
195
|
+
logging.basicConfig(level=logging.DEBUG, format=Settings.LOG_FORMAT_FULL)
|
|
196
|
+
|
|
197
|
+
sys.exit(cli())
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import socket
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
from egse.command import ClientServerCommand
|
|
6
|
+
from egse.device import DeviceConnectionError
|
|
7
|
+
from egse.device import DeviceConnectionInterface
|
|
8
|
+
from egse.device import DeviceError
|
|
9
|
+
from egse.device import DeviceTimeoutError
|
|
10
|
+
from egse.device import DeviceTransport
|
|
11
|
+
from egse.settings import Settings
|
|
12
|
+
from egse.system import Timer
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
IDENTIFICATION_QUERY = "*IDN?"
|
|
17
|
+
|
|
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)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DAQ6510Command(ClientServerCommand):
|
|
24
|
+
|
|
25
|
+
def get_cmd_string(self, *args, **kwargs) -> str:
|
|
26
|
+
""" Constructs the command string, based on the given positional and/or keyword arguments.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
*args: Positional arguments that are needed to construct the command string
|
|
30
|
+
**kwargs: Keyword arguments that are needed to construct the command string
|
|
31
|
+
|
|
32
|
+
Returns: Command string with the given positional and/or keyword arguments filled out.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
out = super().get_cmd_string(*args, **kwargs)
|
|
36
|
+
return out + "\n"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class DAQ6510EthernetInterface(DeviceConnectionInterface, DeviceTransport):
|
|
40
|
+
""" Defines the low-level interface to the Keithley DAQ6510 Controller."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, hostname: str = None, port: int = None):
|
|
43
|
+
""" Initialisation of an Ethernet interface for the DAQ6510.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
hostname(str): Hostname to which to open a socket
|
|
47
|
+
port (int): Port to which to open a socket
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
super().__init__()
|
|
51
|
+
|
|
52
|
+
self.hostname = DEVICE_SETTINGS.HOSTNAME if hostname is None else hostname
|
|
53
|
+
self.port = DEVICE_SETTINGS.PORT if port is None else port
|
|
54
|
+
self.sock = None
|
|
55
|
+
|
|
56
|
+
self.is_connection_open = False
|
|
57
|
+
|
|
58
|
+
def connect(self):
|
|
59
|
+
""" Connects the device.
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
DeviceConnectionError: When the connection could not be established. Check the logging messages for more
|
|
63
|
+
details.
|
|
64
|
+
DeviceTimeoutError: When the connection timed out.
|
|
65
|
+
ValueError: When hostname or port number are not provided.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
# Sanity checks
|
|
69
|
+
|
|
70
|
+
if self.is_connection_open:
|
|
71
|
+
logger.warning(f"{DEVICE_NAME}: trying to connect to an already connected socket.")
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
if self.hostname in (None, ""):
|
|
75
|
+
raise ValueError(f"{DEVICE_NAME}: hostname is not initialized.")
|
|
76
|
+
|
|
77
|
+
if self.port in (None, 0):
|
|
78
|
+
raise ValueError(f"{DEVICE_NAME}: port number is not initialized.")
|
|
79
|
+
|
|
80
|
+
# Create a new socket instance
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
84
|
+
# The following lines are to experiment with blocking and timeout, but there is no need.
|
|
85
|
+
# self.sock.setblocking(1)
|
|
86
|
+
# self.sock.settimeout(3)
|
|
87
|
+
except socket.error as e_socket:
|
|
88
|
+
raise DeviceConnectionError(DEVICE_NAME, "Failed to create socket.") from e_socket
|
|
89
|
+
|
|
90
|
+
# Attempt to establish a connection to the remote host
|
|
91
|
+
|
|
92
|
+
# FIXME: Socket shall be closed on exception?
|
|
93
|
+
|
|
94
|
+
# We set a timeout of 3s before connecting and reset to None (=blocking) after the `connect` method has been
|
|
95
|
+
# called. This is because when no device is available, e.g. during testing, the timeout will take about
|
|
96
|
+
# two minutes, which is way too long. It needs to be evaluated if this approach is acceptable and not causing
|
|
97
|
+
# problems during production.
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
logger.debug(f'Connecting a socket to host "{self.hostname}" using port {self.port}')
|
|
101
|
+
self.sock.settimeout(3)
|
|
102
|
+
self.sock.connect((self.hostname, self.port))
|
|
103
|
+
self.sock.settimeout(None)
|
|
104
|
+
except ConnectionRefusedError as exc:
|
|
105
|
+
raise DeviceConnectionError(
|
|
106
|
+
DEVICE_NAME, f"Connection refused to {self.hostname}:{self.port}."
|
|
107
|
+
) from exc
|
|
108
|
+
except TimeoutError as exc:
|
|
109
|
+
raise DeviceTimeoutError(
|
|
110
|
+
DEVICE_NAME, f"Connection to {self.hostname}:{self.port} timed out."
|
|
111
|
+
) from exc
|
|
112
|
+
except socket.gaierror as exc:
|
|
113
|
+
raise DeviceConnectionError(
|
|
114
|
+
DEVICE_NAME, f"Socket address info error for {self.hostname}"
|
|
115
|
+
) from exc
|
|
116
|
+
except socket.herror as exc:
|
|
117
|
+
raise DeviceConnectionError(
|
|
118
|
+
DEVICE_NAME, f"Socket host address error for {self.hostname}"
|
|
119
|
+
) from exc
|
|
120
|
+
except socket.timeout as exc:
|
|
121
|
+
raise DeviceTimeoutError(
|
|
122
|
+
DEVICE_NAME, f"Socket timeout error for {self.hostname}:{self.port}"
|
|
123
|
+
) from exc
|
|
124
|
+
except OSError as exc:
|
|
125
|
+
raise DeviceConnectionError(DEVICE_NAME, f"OSError caught ({exc}).") from exc
|
|
126
|
+
|
|
127
|
+
self.is_connection_open = True
|
|
128
|
+
|
|
129
|
+
# Check that we are connected to the controller by issuing the "VERSION" or
|
|
130
|
+
# "*ISDN?" query. If we don't get the right response, then disconnect automatically.
|
|
131
|
+
|
|
132
|
+
if not self.is_connected():
|
|
133
|
+
raise DeviceConnectionError(
|
|
134
|
+
DEVICE_NAME, "Device is not connected, check logging messages for the cause."
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def disconnect(self):
|
|
138
|
+
""" Disconnects from the Ethernet connection.
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
DeviceConnectionError when the socket could not be closed.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
if self.is_connection_open:
|
|
146
|
+
logger.debug(f"Disconnecting from {self.hostname}")
|
|
147
|
+
self.sock.close()
|
|
148
|
+
self.is_connection_open = False
|
|
149
|
+
except Exception as e_exc:
|
|
150
|
+
raise DeviceConnectionError(
|
|
151
|
+
DEVICE_NAME, f"Could not close socket to {self.hostname}") from e_exc
|
|
152
|
+
|
|
153
|
+
def reconnect(self):
|
|
154
|
+
""" Reconnects to the device controller.
|
|
155
|
+
|
|
156
|
+
Raises:
|
|
157
|
+
ConnectionError when the device cannot be reconnected for some reason.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
if self.is_connection_open:
|
|
161
|
+
self.disconnect()
|
|
162
|
+
self.connect()
|
|
163
|
+
|
|
164
|
+
def is_connected(self) -> bool:
|
|
165
|
+
""" Checks if the device is connected.
|
|
166
|
+
|
|
167
|
+
This will send a query for the device identification and validate the answer.
|
|
168
|
+
|
|
169
|
+
Returns: True is the device is connected and answered with the proper ID; False otherwise.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
if not self.is_connection_open:
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
version = self.query(IDENTIFICATION_QUERY)
|
|
177
|
+
except DeviceError as exc:
|
|
178
|
+
logger.exception(exc)
|
|
179
|
+
logger.error("Most probably the client connection was closed. Disconnecting...")
|
|
180
|
+
self.disconnect()
|
|
181
|
+
return False
|
|
182
|
+
|
|
183
|
+
if "DAQ6510" not in version:
|
|
184
|
+
logger.error(
|
|
185
|
+
f'Device did not respond correctly to a "VERSION" command, response={version}. '
|
|
186
|
+
f"Disconnecting..."
|
|
187
|
+
)
|
|
188
|
+
self.disconnect()
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
return True
|
|
192
|
+
|
|
193
|
+
def write(self, command: str):
|
|
194
|
+
""" Senda a single command to the device controller without waiting for a response.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
command (str): Command to send to the controller
|
|
198
|
+
|
|
199
|
+
Raises:
|
|
200
|
+
DeviceConnectionError when the command could not be sent due to a communication problem.
|
|
201
|
+
DeviceTimeoutError when the command could not be sent due to a timeout.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
command += "\n" if not command.endswith("\n") else ""
|
|
206
|
+
|
|
207
|
+
self.sock.sendall(command.encode())
|
|
208
|
+
|
|
209
|
+
except socket.timeout as e_timeout:
|
|
210
|
+
raise DeviceTimeoutError(DEVICE_NAME, "Socket timeout error") from e_timeout
|
|
211
|
+
except socket.error as e_socket:
|
|
212
|
+
# Interpret any socket-related error as a connection error
|
|
213
|
+
raise DeviceConnectionError(DEVICE_NAME, "Socket communication error.") from e_socket
|
|
214
|
+
except AttributeError:
|
|
215
|
+
if not self.is_connection_open:
|
|
216
|
+
msg = "The DAQ6510 is not connected, use the connect() method."
|
|
217
|
+
raise DeviceConnectionError(DEVICE_NAME, msg)
|
|
218
|
+
raise
|
|
219
|
+
|
|
220
|
+
def trans(self, command: str) -> str:
|
|
221
|
+
""" Sends a single command to the device controller and block until a response from the controller.
|
|
222
|
+
|
|
223
|
+
This is seen as a transaction.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
command (str): Command to send to the controller
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Either a string returned by the controller (on success), or an error message (on failure).
|
|
230
|
+
|
|
231
|
+
Raises:
|
|
232
|
+
DeviceConnectionError when there was an I/O problem during communication with the controller.
|
|
233
|
+
DeviceTimeoutError when there was a timeout in either sending the command or receiving the response.
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
# Attempt to send the complete command
|
|
238
|
+
|
|
239
|
+
command += "\n" if not command.endswith("\n") else ""
|
|
240
|
+
|
|
241
|
+
self.sock.sendall(command.encode())
|
|
242
|
+
|
|
243
|
+
# wait for, read and return the response from HUBER (will be at most TBD chars)
|
|
244
|
+
|
|
245
|
+
return_string = self.read()
|
|
246
|
+
|
|
247
|
+
return return_string.decode().rstrip()
|
|
248
|
+
|
|
249
|
+
except socket.timeout as e_timeout:
|
|
250
|
+
raise DeviceTimeoutError(DEVICE_NAME, "Socket timeout error") from e_timeout
|
|
251
|
+
except socket.error as e_socket:
|
|
252
|
+
# Interpret any socket-related error as an I/O error
|
|
253
|
+
raise DeviceConnectionError(DEVICE_NAME, "Socket communication error.") from e_socket
|
|
254
|
+
except ConnectionError as exc:
|
|
255
|
+
raise DeviceConnectionError(DEVICE_NAME, "Connection error.") from exc
|
|
256
|
+
except AttributeError:
|
|
257
|
+
if not self.is_connection_open:
|
|
258
|
+
raise DeviceConnectionError(
|
|
259
|
+
DEVICE_NAME, "Device not connected, use the connect() method."
|
|
260
|
+
)
|
|
261
|
+
raise
|
|
262
|
+
|
|
263
|
+
def read(self) -> bytes:
|
|
264
|
+
""" Reads from the device buffer.
|
|
265
|
+
|
|
266
|
+
Returns: Content of the device buffer.
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
n_total = 0
|
|
270
|
+
buf_size = 2048
|
|
271
|
+
|
|
272
|
+
# Set a timeout of READ_TIMEOUT to the socket.recv
|
|
273
|
+
|
|
274
|
+
saved_timeout = self.sock.gettimeout()
|
|
275
|
+
self.sock.settimeout(READ_TIMEOUT)
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
for idx in range(100):
|
|
279
|
+
time.sleep(0.001) # Give the device time to fill the buffer
|
|
280
|
+
data = self.sock.recv(buf_size)
|
|
281
|
+
n = len(data)
|
|
282
|
+
n_total += n
|
|
283
|
+
if n < buf_size:
|
|
284
|
+
break
|
|
285
|
+
except socket.timeout:
|
|
286
|
+
logger.warning(f"Socket timeout error for {self.hostname}:{self.port}")
|
|
287
|
+
return b"\r\n"
|
|
288
|
+
except TimeoutError as exc:
|
|
289
|
+
logger.warning(f"Socket timeout error: {exc}")
|
|
290
|
+
return b"\r\n"
|
|
291
|
+
finally:
|
|
292
|
+
self.sock.settimeout(saved_timeout)
|
|
293
|
+
|
|
294
|
+
# logger.debug(f"Total number of bytes received is {n_total}, idx={idx}")
|
|
295
|
+
|
|
296
|
+
return data
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
if __name__ == "__main__":
|
|
300
|
+
|
|
301
|
+
daq = DAQ6510EthernetInterface()
|
|
302
|
+
|
|
303
|
+
with Timer():
|
|
304
|
+
daq.connect()
|
|
305
|
+
|
|
306
|
+
# print(daq.info())
|
|
307
|
+
|
|
308
|
+
# Initialize
|
|
309
|
+
|
|
310
|
+
daq.write('TRAC:DEL "test1"\n')
|
|
311
|
+
|
|
312
|
+
for cmd, response in [
|
|
313
|
+
('TRAC:MAKE "test1", 1000', False), # create a new buffer
|
|
314
|
+
# settings for channel 1 and 2 of slot 1
|
|
315
|
+
('SENS:FUNC "TEMP", (@101:102)', False), # set the function to temperature
|
|
316
|
+
("SENS:TEMP:TRAN FRTD, (@101:102)", False), # set the transducer to 4-wire RTD
|
|
317
|
+
("SENS:TEMP:RTD:FOUR PT100, (@101:102)", False), # set the type of the 4-wire RTD
|
|
318
|
+
('ROUT:SCAN:BUFF "test1"', False),
|
|
319
|
+
("ROUT:SCAN:CRE (@101:102)", False),
|
|
320
|
+
("ROUT:CHAN:OPEN (@101:102)", False),
|
|
321
|
+
("ROUT:STAT? (@101:102)", True),
|
|
322
|
+
("ROUT:SCAN:STAR:STIM NONE", False),
|
|
323
|
+
# ("ROUT:SCAN:ADD:SING (@101, 102)", False), # not sure what this does, not really needed
|
|
324
|
+
("ROUT:SCAN:COUN:SCAN 1", False), # not sure if this is needed in this setting
|
|
325
|
+
# ("ROUT:SCAN:INT 1", False),
|
|
326
|
+
]:
|
|
327
|
+
print(f"Sending {cmd}...")
|
|
328
|
+
if response:
|
|
329
|
+
print(daq.trans(cmd), end="")
|
|
330
|
+
else:
|
|
331
|
+
daq.write(cmd)
|
|
332
|
+
|
|
333
|
+
# Read out the channels
|
|
334
|
+
|
|
335
|
+
# daq.write('TRAC:CLE "test1"\n')
|
|
336
|
+
|
|
337
|
+
for _ in range(10):
|
|
338
|
+
daq.write("INIT:IMM")
|
|
339
|
+
daq.write("*WAI")
|
|
340
|
+
|
|
341
|
+
# Reading the data
|
|
342
|
+
|
|
343
|
+
# When a trigger mode is running, these READ? commands can not be used.
|
|
344
|
+
|
|
345
|
+
# print(daq.trans('READ? "test1", CHAN, TST, READ\n', wait=False), end="")
|
|
346
|
+
# print(daq.trans('READ? "test1", CHAN, TST, READ\n', wait=False), end="")
|
|
347
|
+
# time.sleep(1)
|
|
348
|
+
# print(daq.trans('READ? "test1", CHAN, TST, READ\n', wait=False), end="")
|
|
349
|
+
# print(daq.trans('READ? "test1", CHAN, TST, READ\n', wait=False), end="")
|
|
350
|
+
|
|
351
|
+
# Read out the buffer
|
|
352
|
+
|
|
353
|
+
response = daq.trans('TRAC:DATA? 1, 2, "test1", CHAN, TST, READ')
|
|
354
|
+
ch1, tst1, val1, ch2, tst2, val2 = response[:-1].split(",")
|
|
355
|
+
print(f"Channel: {ch1} Time: {tst1} Value: {float(val1):.4f}\t", end="")
|
|
356
|
+
print(f"Channel: {ch2} Time: {tst2} Value: {float(val2):.4f}")
|
|
357
|
+
time.sleep(2)
|
|
358
|
+
|
|
359
|
+
daq.disconnect()
|