keithley-tempcontrol 0.17.4__tar.gz → 0.18.1__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.
- {keithley_tempcontrol-0.17.4 → keithley_tempcontrol-0.18.1}/.gitignore +6 -1
- {keithley_tempcontrol-0.17.4 → keithley_tempcontrol-0.18.1}/PKG-INFO +1 -1
- keithley_tempcontrol-0.18.1/justfile +25 -0
- {keithley_tempcontrol-0.17.4 → keithley_tempcontrol-0.18.1}/pyproject.toml +3 -4
- keithley_tempcontrol-0.18.1/service_registry.db +0 -0
- keithley_tempcontrol-0.18.1/service_registry.db-shm +0 -0
- keithley_tempcontrol-0.18.1/service_registry.db-wal +0 -0
- {keithley_tempcontrol-0.17.4 → keithley_tempcontrol-0.18.1}/src/egse/tempcontrol/keithley/__init__.py +0 -2
- {keithley_tempcontrol-0.17.4 → keithley_tempcontrol-0.18.1}/src/egse/tempcontrol/keithley/daq6510.py +79 -242
- {keithley_tempcontrol-0.17.4 → keithley_tempcontrol-0.18.1}/src/egse/tempcontrol/keithley/daq6510_adev.py +13 -1
- keithley_tempcontrol-0.17.4/src/egse/tempcontrol/keithley/daq6510_mon.py → keithley_tempcontrol-0.18.1/src/egse/tempcontrol/keithley/daq6510_amon.py +14 -5
- {keithley_tempcontrol-0.17.4 → keithley_tempcontrol-0.18.1}/src/egse/tempcontrol/keithley/daq6510_cs.py +27 -1
- keithley_tempcontrol-0.18.1/src/egse/tempcontrol/keithley/daq6510_dev.py +154 -0
- keithley_tempcontrol-0.18.1/src/egse/tempcontrol/keithley/daq6510_mon.py +313 -0
- {keithley_tempcontrol-0.17.4 → keithley_tempcontrol-0.18.1}/src/egse/tempcontrol/keithley/daq6510_protocol.py +29 -7
- {keithley_tempcontrol-0.17.4 → keithley_tempcontrol-0.18.1}/src/egse/tempcontrol/keithley/daq6510_sim.py +4 -4
- {keithley_tempcontrol-0.17.4 → keithley_tempcontrol-0.18.1}/src/keithley_tempcontrol/cgse_services.py +32 -0
- keithley_tempcontrol-0.17.4/src/egse/tempcontrol/keithley/daq6510_dev.py +0 -345
- {keithley_tempcontrol-0.17.4 → keithley_tempcontrol-0.18.1}/README.md +0 -0
- {keithley_tempcontrol-0.17.4 → keithley_tempcontrol-0.18.1}/noxfile.py +0 -0
- {keithley_tempcontrol-0.17.4 → keithley_tempcontrol-0.18.1}/src/egse/tempcontrol/keithley/daq6510.yaml +0 -0
- {keithley_tempcontrol-0.17.4 → keithley_tempcontrol-0.18.1}/src/keithley_tempcontrol/__init__.py +0 -0
- {keithley_tempcontrol-0.17.4 → keithley_tempcontrol-0.18.1}/src/keithley_tempcontrol/cgse_explore.py +0 -0
- {keithley_tempcontrol-0.17.4 → keithley_tempcontrol-0.18.1}/src/keithley_tempcontrol/settings.yaml +0 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# If you don't have 'just' installed, install it with the following command:
|
|
2
|
+
#
|
|
3
|
+
# $ uv tool install rust-just
|
|
4
|
+
#
|
|
5
|
+
# The 'just' website: https://just.systems/man/en/
|
|
6
|
+
|
|
7
|
+
default:
|
|
8
|
+
@just --list
|
|
9
|
+
|
|
10
|
+
typecheck:
|
|
11
|
+
uv run --with mypy mypy -p egse --strict
|
|
12
|
+
|
|
13
|
+
test target="":
|
|
14
|
+
#!/usr/bin/env bash
|
|
15
|
+
if [ -z "{{target}}" ]; then
|
|
16
|
+
uv run pytest -v
|
|
17
|
+
else
|
|
18
|
+
uv run pytest -v -k {{target}}
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
format:
|
|
22
|
+
uvx ruff format
|
|
23
|
+
|
|
24
|
+
check:
|
|
25
|
+
uvx ruff check --no-fix
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "keithley-tempcontrol"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.18.1"
|
|
4
4
|
description = "Keithley Temperature Control for CGSE"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "IvS KU Leuven"}
|
|
@@ -32,6 +32,7 @@ test = ["pytest", "pytest-mock", "pytest-cov"]
|
|
|
32
32
|
[project.scripts]
|
|
33
33
|
daq6510_cs = 'egse.tempcontrol.keithley.daq6510_cs:app'
|
|
34
34
|
daq6510_sim = 'egse.tempcontrol.keithley.daq6510_sim:app'
|
|
35
|
+
daq6510_mon = 'egse.tempcontrol.keithley.daq6510_mon:app'
|
|
35
36
|
|
|
36
37
|
[project.gui-scripts]
|
|
37
38
|
daq6510_ui = "egse.tempcontrol.keithley.daq6510_ui:main"
|
|
@@ -73,12 +74,10 @@ build-backend = "hatchling.build"
|
|
|
73
74
|
|
|
74
75
|
[dependency-groups]
|
|
75
76
|
dev = [
|
|
76
|
-
"setuptools", # needed by PyCharm
|
|
77
77
|
"pytest>=8.3.4",
|
|
78
78
|
"pytest-cov>=6.0.0",
|
|
79
79
|
"pytest-mock>=3.14.0",
|
|
80
|
+
"pytest-asyncio>=0.26.0",
|
|
80
81
|
"ruff>=0.9.0",
|
|
81
82
|
"nox>=2025.2.9",
|
|
82
|
-
"setuptools>=75.8.2", # needed by PyCharm
|
|
83
|
-
"pytest-asyncio>=0.26.0",
|
|
84
83
|
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
{keithley_tempcontrol-0.17.4 → keithley_tempcontrol-0.18.1}/src/egse/tempcontrol/keithley/daq6510.py
RENAMED
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
import re
|
|
3
2
|
from pathlib import Path
|
|
4
|
-
from typing import Dict
|
|
5
|
-
from typing import List
|
|
6
|
-
from typing import Tuple
|
|
3
|
+
from typing import Dict, List, Tuple
|
|
7
4
|
|
|
8
5
|
from egse.connect import get_endpoint
|
|
9
6
|
from egse.decorators import dynamic_interface
|
|
10
|
-
from egse.device import DeviceConnectionState
|
|
11
|
-
from egse.
|
|
7
|
+
from egse.device import DeviceConnectionState, DeviceInterface
|
|
8
|
+
from egse.env import bool_env
|
|
9
|
+
from egse.response import Failure
|
|
12
10
|
from egse.log import logger
|
|
13
|
-
from egse.mixin import CommandType
|
|
14
|
-
from egse.
|
|
15
|
-
from egse.
|
|
16
|
-
from egse.mixin import dynamic_command
|
|
17
|
-
from egse.proxy import Proxy
|
|
11
|
+
from egse.mixin import CommandType, DynamicCommandMixin, add_lf, dynamic_command
|
|
12
|
+
from egse.proxy import DynamicProxy
|
|
13
|
+
from egse.scpi import count_number_of_channels, create_channel_list, get_channel_names
|
|
18
14
|
from egse.settings import Settings
|
|
19
15
|
from egse.tempcontrol.keithley.daq6510_dev import DAQ6510
|
|
20
16
|
|
|
17
|
+
VERBOSE_DEBUG = bool_env("VERBOSE_DEBUG")
|
|
18
|
+
|
|
21
19
|
HERE = Path(__file__).parent
|
|
22
20
|
|
|
23
21
|
cs_settings = Settings.load("Keithley Control Server")
|
|
@@ -26,14 +24,29 @@ dev_settings = Settings.load("Keithley DAQ6510")
|
|
|
26
24
|
PROTOCOL = cs_settings.get("PROTOCOL", "tcp")
|
|
27
25
|
HOSTNAME = cs_settings.get("HOSTNAME", "localhost")
|
|
28
26
|
COMMANDING_PORT = cs_settings.get("COMMANDING_PORT", 0)
|
|
29
|
-
TIMEOUT = cs_settings.get("TIMEOUT")
|
|
27
|
+
TIMEOUT = cs_settings.get("TIMEOUT", 60)
|
|
30
28
|
SERVICE_TYPE = cs_settings.get("SERVICE_TYPE", "daq6510")
|
|
31
29
|
|
|
32
30
|
DEFAULT_BUFFER_1 = "defbuffer1"
|
|
33
31
|
DEFAULT_BUFFER_2 = "defbuffer2"
|
|
34
32
|
|
|
35
|
-
DEV_HOST = dev_settings.get("HOSTNAME")
|
|
36
|
-
DEV_PORT = dev_settings.get("PORT")
|
|
33
|
+
DEV_HOST = dev_settings.get("HOSTNAME", "localhost")
|
|
34
|
+
DEV_PORT = dev_settings.get("PORT", 5025)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def decode_response(response: bytes) -> str | Failure:
|
|
38
|
+
"""Decodes the bytes object, strips off the trailing 'CRLF'."""
|
|
39
|
+
|
|
40
|
+
if VERBOSE_DEBUG:
|
|
41
|
+
logger.debug(f"{response = } <- decode_response")
|
|
42
|
+
|
|
43
|
+
if isinstance(response, Failure):
|
|
44
|
+
return response
|
|
45
|
+
|
|
46
|
+
if isinstance(response, memoryview):
|
|
47
|
+
response = response.tobytes()
|
|
48
|
+
|
|
49
|
+
return response.decode().rstrip()
|
|
37
50
|
|
|
38
51
|
|
|
39
52
|
class DAQ6510Interface(DeviceInterface):
|
|
@@ -61,6 +74,23 @@ class DAQ6510Interface(DeviceInterface):
|
|
|
61
74
|
cmd_type=CommandType.TRANSACTION,
|
|
62
75
|
cmd_string="*IDN?",
|
|
63
76
|
process_cmd_string=add_lf,
|
|
77
|
+
process_response=decode_response,
|
|
78
|
+
)
|
|
79
|
+
def get_idn(self) -> str:
|
|
80
|
+
"""Returns basic information about the device, its name, firmware version, etc.
|
|
81
|
+
|
|
82
|
+
The string returned is subject to change without notice and can not be used for parsing information.
|
|
83
|
+
|
|
84
|
+
Returns: Identification string of the instrument.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
raise NotImplementedError
|
|
88
|
+
|
|
89
|
+
@dynamic_command(
|
|
90
|
+
cmd_type=CommandType.TRANSACTION,
|
|
91
|
+
cmd_string="*IDN?",
|
|
92
|
+
process_cmd_string=add_lf,
|
|
93
|
+
process_response=decode_response,
|
|
64
94
|
)
|
|
65
95
|
def info(self) -> str:
|
|
66
96
|
"""Returns basic information about the device, its name, firmware version, etc.
|
|
@@ -105,7 +135,7 @@ class DAQ6510Interface(DeviceInterface):
|
|
|
105
135
|
|
|
106
136
|
raise NotImplementedError
|
|
107
137
|
|
|
108
|
-
@dynamic_command(cmd_type=CommandType.TRANSACTION, cmd_string=":SYST:TIME? 1")
|
|
138
|
+
@dynamic_command(cmd_type=CommandType.TRANSACTION, cmd_string=":SYST:TIME? 1", process_response=decode_response)
|
|
109
139
|
def get_time(self) -> str:
|
|
110
140
|
"""Gets the date and time from the device in UTC.
|
|
111
141
|
|
|
@@ -133,7 +163,7 @@ class DAQ6510Interface(DeviceInterface):
|
|
|
133
163
|
|
|
134
164
|
raise NotImplementedError
|
|
135
165
|
|
|
136
|
-
@dynamic_command(cmd_type=CommandType.TRANSACTION, cmd_string=
|
|
166
|
+
@dynamic_command(cmd_type=CommandType.TRANSACTION, cmd_string='TRAC:ACTUAL? "${buffer_name}"')
|
|
137
167
|
def get_buffer_count(self, buffer_name: str = DEFAULT_BUFFER_1):
|
|
138
168
|
"""Returns the number of data points in the specified buffer.
|
|
139
169
|
|
|
@@ -143,8 +173,12 @@ class DAQ6510Interface(DeviceInterface):
|
|
|
143
173
|
|
|
144
174
|
raise NotImplementedError
|
|
145
175
|
|
|
146
|
-
@dynamic_command(
|
|
147
|
-
|
|
176
|
+
@dynamic_command(
|
|
177
|
+
cmd_type=CommandType.TRANSACTION,
|
|
178
|
+
cmd_string='TRACE:POINTS? "${buffer_name}"',
|
|
179
|
+
process_cmd_string=add_lf,
|
|
180
|
+
)
|
|
181
|
+
def get_buffer_capacity(self, buffer_name: str = DEFAULT_BUFFER_1):
|
|
148
182
|
"""Returns the capacity of the specified buffer.
|
|
149
183
|
|
|
150
184
|
Args:
|
|
@@ -153,7 +187,7 @@ class DAQ6510Interface(DeviceInterface):
|
|
|
153
187
|
|
|
154
188
|
raise NotImplementedError
|
|
155
189
|
|
|
156
|
-
@dynamic_command(cmd_type=CommandType.WRITE, cmd_string=
|
|
190
|
+
@dynamic_command(cmd_type=CommandType.WRITE, cmd_string='TRACE:DELETE "${buffer_name}"')
|
|
157
191
|
def delete_buffer(self, buffer_name: str) -> None:
|
|
158
192
|
"""Deletes the specified buffer.
|
|
159
193
|
|
|
@@ -196,7 +230,7 @@ class DAQ6510Interface(DeviceInterface):
|
|
|
196
230
|
raise NotImplementedError
|
|
197
231
|
|
|
198
232
|
@dynamic_interface
|
|
199
|
-
def setup_measurements(self, *, buffer_name: str, channel_list: str):
|
|
233
|
+
def setup_measurements(self, *, buffer_name: str = DEFAULT_BUFFER_1, channel_list: str):
|
|
200
234
|
"""Sets up the measurements for the given channel list.
|
|
201
235
|
|
|
202
236
|
Args:
|
|
@@ -207,11 +241,13 @@ class DAQ6510Interface(DeviceInterface):
|
|
|
207
241
|
raise NotImplementedError
|
|
208
242
|
|
|
209
243
|
@dynamic_interface
|
|
210
|
-
def perform_measurement(
|
|
244
|
+
def perform_measurement(
|
|
245
|
+
self, *, buffer_name: str = DEFAULT_BUFFER_1, channel_list: str, count: int, interval: int
|
|
246
|
+
) -> list:
|
|
211
247
|
"""Performs the actual measurements.
|
|
212
248
|
|
|
213
249
|
Args:
|
|
214
|
-
buffer_name (str): Name of the buffer
|
|
250
|
+
buffer_name (str): Name of the buffer [default: defbuffer1]
|
|
215
251
|
channel_list (str): List of channels, as understood by the device
|
|
216
252
|
count (int): Number of measurements to perform
|
|
217
253
|
interval (int): Interval between measurements [s]
|
|
@@ -297,9 +333,11 @@ class DAQ6510Controller(DAQ6510Interface, DynamicCommandMixin):
|
|
|
297
333
|
None will be returned.
|
|
298
334
|
"""
|
|
299
335
|
|
|
300
|
-
return self.daq.trans(command) if response else self.daq.write(command)
|
|
336
|
+
return self.daq.trans(command).decode() if response else self.daq.write(command)
|
|
301
337
|
|
|
302
|
-
def read_buffer(
|
|
338
|
+
def read_buffer(
|
|
339
|
+
self, start: int, end: int, buffer_name: str = DEFAULT_BUFFER_1, elements: list[str] | None = None
|
|
340
|
+
) -> bytes:
|
|
303
341
|
"""Reads specific data elements (measurements) from the given buffer.
|
|
304
342
|
|
|
305
343
|
Elements that can be specified to read out:
|
|
@@ -315,17 +353,17 @@ class DAQ6510Controller(DAQ6510Interface, DynamicCommandMixin):
|
|
|
315
353
|
start: (int) First index of the buffer that should be returned (>= 1)
|
|
316
354
|
end (int): Last index of the buffer that should be returned
|
|
317
355
|
buffer_name (str): Name of the buffer to read out
|
|
318
|
-
elements (
|
|
356
|
+
elements (list[str] | None): List of elements from the buffer to include in the response
|
|
319
357
|
|
|
320
358
|
Returns: List of all the readings.
|
|
321
359
|
"""
|
|
322
360
|
|
|
323
361
|
if elements is None:
|
|
324
|
-
|
|
362
|
+
elements_str = ["READING"]
|
|
325
363
|
else:
|
|
326
|
-
|
|
364
|
+
elements_str = ", ".join(elements)
|
|
327
365
|
|
|
328
|
-
return self.daq.trans(f'TRACE:DATA? {start}, {end}, "{buffer_name}", {
|
|
366
|
+
return self.daq.trans(f'TRACE:DATA? {start}, {end}, "{buffer_name}", {elements_str}')
|
|
329
367
|
|
|
330
368
|
def clear_buffer(self, buffer_name: str = DEFAULT_BUFFER_1) -> None:
|
|
331
369
|
"""Clears the given buffer.
|
|
@@ -480,7 +518,7 @@ class DAQ6510Controller(DAQ6510Interface, DynamicCommandMixin):
|
|
|
480
518
|
|
|
481
519
|
# Read out the buffer
|
|
482
520
|
|
|
483
|
-
logger.debug("Buffer count =
|
|
521
|
+
logger.debug(f"Buffer count = {self.get_buffer_count()}")
|
|
484
522
|
|
|
485
523
|
num_sensors = count_number_of_channels(channel_list)
|
|
486
524
|
|
|
@@ -490,82 +528,24 @@ class DAQ6510Controller(DAQ6510Interface, DynamicCommandMixin):
|
|
|
490
528
|
response = self.read_buffer(
|
|
491
529
|
idx, idx, buffer_name=buffer_name, elements=["CHANNEL", "TSTAMP", "READING", "UNIT"]
|
|
492
530
|
)
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
531
|
+
response_str = response.decode().strip()
|
|
532
|
+
|
|
533
|
+
if response_str != "" and response_str != str(count * num_sensors):
|
|
534
|
+
if "\n" in response_str:
|
|
535
|
+
response_str = response_str.split("\n")
|
|
536
|
+
for i in range(len(response_str)):
|
|
537
|
+
readings.append(response_str[i].split(","))
|
|
498
538
|
else:
|
|
499
|
-
readings.append(
|
|
539
|
+
readings.append(response_str.split(","))
|
|
540
|
+
|
|
541
|
+
# Remove incomplete readings
|
|
500
542
|
if len(readings[0]) < 4:
|
|
501
543
|
del readings[0]
|
|
502
544
|
|
|
503
545
|
return readings
|
|
504
546
|
|
|
505
547
|
|
|
506
|
-
class
|
|
507
|
-
"""
|
|
508
|
-
Simulator for the Keithley DAQ6510 system.
|
|
509
|
-
"""
|
|
510
|
-
|
|
511
|
-
def read_buffer(self, start: int, end: int, buffer_name: str, elements: List[str]):
|
|
512
|
-
pass
|
|
513
|
-
|
|
514
|
-
def get_buffer_count(self, buffer_name: str = DEFAULT_BUFFER_1):
|
|
515
|
-
pass
|
|
516
|
-
|
|
517
|
-
def get_buffer_capacity(self, buffer_name: str):
|
|
518
|
-
pass
|
|
519
|
-
|
|
520
|
-
def delete_buffer(self, buffer_name: str):
|
|
521
|
-
pass
|
|
522
|
-
|
|
523
|
-
def clear_buffer(self, buffer_name: str):
|
|
524
|
-
pass
|
|
525
|
-
|
|
526
|
-
def create_buffer(self, buffer_name: str, size: int):
|
|
527
|
-
pass
|
|
528
|
-
|
|
529
|
-
def configure_sensors(self, channel_list: str, *, sense: Dict[str, List[Tuple]]):
|
|
530
|
-
pass
|
|
531
|
-
|
|
532
|
-
def setup_measurements(self, *, buffer_name: str, channel_list: str):
|
|
533
|
-
pass
|
|
534
|
-
|
|
535
|
-
def perform_measurement(self, *, buffer_name: str, channel_list: str, count: int, interval: int):
|
|
536
|
-
pass
|
|
537
|
-
|
|
538
|
-
def send_command(self, command: str, response: bool):
|
|
539
|
-
pass
|
|
540
|
-
|
|
541
|
-
def info(self) -> str:
|
|
542
|
-
pass
|
|
543
|
-
|
|
544
|
-
def reset(self):
|
|
545
|
-
pass
|
|
546
|
-
|
|
547
|
-
def is_simulator(self):
|
|
548
|
-
"""Indicates that the device is a simulator.
|
|
549
|
-
|
|
550
|
-
Returns: True.
|
|
551
|
-
"""
|
|
552
|
-
|
|
553
|
-
return True
|
|
554
|
-
|
|
555
|
-
def connect(self):
|
|
556
|
-
pass
|
|
557
|
-
|
|
558
|
-
def disconnect(self):
|
|
559
|
-
pass
|
|
560
|
-
|
|
561
|
-
def reconnect(self):
|
|
562
|
-
pass
|
|
563
|
-
|
|
564
|
-
def is_connected(self):
|
|
565
|
-
pass
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
class DAQ6510Proxy(Proxy, DAQ6510Interface):
|
|
548
|
+
class DAQ6510Proxy(DynamicProxy, DAQ6510Interface):
|
|
569
549
|
"""
|
|
570
550
|
The DAQ6510Proxy class is used to connect to the Keithley Control Server and send commands
|
|
571
551
|
to the Keithley Hardware Controller remotely.
|
|
@@ -578,7 +558,7 @@ class DAQ6510Proxy(Proxy, DAQ6510Interface):
|
|
|
578
558
|
port: int = COMMANDING_PORT,
|
|
579
559
|
timeout: float = TIMEOUT, # Timeout [s]: > scan count * interval + (one scan duration)
|
|
580
560
|
):
|
|
581
|
-
"""
|
|
561
|
+
"""Initialization of a DAQ6510Proxy.
|
|
582
562
|
|
|
583
563
|
Args:
|
|
584
564
|
protocol (str): Transport protocol [default is taken from settings file]
|
|
@@ -591,146 +571,3 @@ class DAQ6510Proxy(Proxy, DAQ6510Interface):
|
|
|
591
571
|
endpoint = get_endpoint(SERVICE_TYPE, protocol, hostname, port)
|
|
592
572
|
|
|
593
573
|
super().__init__(endpoint, timeout=timeout)
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
def create_channel_list(*args) -> str:
|
|
597
|
-
"""Createa a channel list that is understood by the SCPI commands of the DAQ6510.
|
|
598
|
-
|
|
599
|
-
Channel names contain both the slot number and the channel number. The slot number is the number of the slot where
|
|
600
|
-
the card is installed at the back of the device.
|
|
601
|
-
|
|
602
|
-
When addressing multiple individual channels, add each of them as a separate argument, e.g. to include channels 1,
|
|
603
|
-
3, and 7 from slot 1, use the following command:
|
|
604
|
-
|
|
605
|
-
>>> create_channel_list(101, 103, 107)
|
|
606
|
-
'(@101, 103, 107)'
|
|
607
|
-
|
|
608
|
-
To designate a range of channels, only one argument should be given, i.e. a tuple containing two channels
|
|
609
|
-
representing the range. The following tuple `(101, 110)` will create the following response: `"(@101:110)"`. The
|
|
610
|
-
range is inclusive, so this will define a range of 10 channels in slot 1.
|
|
611
|
-
|
|
612
|
-
>>> create_channel_list((201, 205))
|
|
613
|
-
'(@201:205)'
|
|
614
|
-
|
|
615
|
-
See reference manual for the Keithley DAQ6510 [DAQ6510-901-01 Rev. B / September 2019], chapter 11: Introduction to
|
|
616
|
-
SCPI commands, SCPI command formatting, channel naming.
|
|
617
|
-
|
|
618
|
-
Args:
|
|
619
|
-
*args: Tuple or a list of channels
|
|
620
|
-
|
|
621
|
-
Returns: String containing the channel list as understood by the device.
|
|
622
|
-
"""
|
|
623
|
-
|
|
624
|
-
if not args:
|
|
625
|
-
return ""
|
|
626
|
-
|
|
627
|
-
# If only one argument is given, I expect either a tuple defining a range or just one channel. When several
|
|
628
|
-
# arguments are given, I expect them all to be individual channels.
|
|
629
|
-
|
|
630
|
-
if len(args) == 1:
|
|
631
|
-
arg = args[0]
|
|
632
|
-
if isinstance(arg, tuple):
|
|
633
|
-
ch_list = f"(@{arg[0]}:{arg[1]})"
|
|
634
|
-
else:
|
|
635
|
-
ch_list = f"(@{arg})"
|
|
636
|
-
|
|
637
|
-
else:
|
|
638
|
-
ch_list = "(@" + ", ".join([str(arg) for arg in args]) + ")"
|
|
639
|
-
|
|
640
|
-
return ch_list
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
def count_number_of_channels(channel_list: str) -> int:
|
|
644
|
-
"""Given a proper channel list, this function counts the number of channels.
|
|
645
|
-
|
|
646
|
-
For ranges, it returns the actual number of channels that are included in the range.
|
|
647
|
-
|
|
648
|
-
>>> count_number_of_channels("(@1,2,3,4,5)")
|
|
649
|
-
5
|
|
650
|
-
>>> count_number_of_channels("(@1, 3, 5)")
|
|
651
|
-
3
|
|
652
|
-
>>> count_number_of_channels("(@2:7)")
|
|
653
|
-
6
|
|
654
|
-
|
|
655
|
-
Args:
|
|
656
|
-
channel_list (str): Channel list as understood by the SCPI commands of DAQ6510
|
|
657
|
-
|
|
658
|
-
Returns: Number of channels in the list.
|
|
659
|
-
"""
|
|
660
|
-
|
|
661
|
-
match = re.match(r"\(@(.*)\)", channel_list)
|
|
662
|
-
group = match.groups()[0]
|
|
663
|
-
|
|
664
|
-
parts = group.replace(" ", "").split(",")
|
|
665
|
-
count = 0
|
|
666
|
-
for part in parts:
|
|
667
|
-
if ":" in part:
|
|
668
|
-
split_part = part.split(":")
|
|
669
|
-
count += int(split_part[1]) - int(split_part[0]) + 1
|
|
670
|
-
else:
|
|
671
|
-
count += 1
|
|
672
|
-
|
|
673
|
-
return count
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
def get_channel_names(channel_list: str) -> List[str]:
|
|
677
|
-
"""Generates a list of channel names from a given channel list.
|
|
678
|
-
|
|
679
|
-
Args:
|
|
680
|
-
channel_list (str): Channel list as understood by the SCPI commands of DAQ6510
|
|
681
|
-
|
|
682
|
-
Returns: List of channel names.
|
|
683
|
-
"""
|
|
684
|
-
|
|
685
|
-
match = re.match(r"\(@(.*)\)", channel_list)
|
|
686
|
-
group = match.groups()[0]
|
|
687
|
-
|
|
688
|
-
parts = group.replace(" ", "").split(",")
|
|
689
|
-
names = []
|
|
690
|
-
for part in parts:
|
|
691
|
-
if ":" in part:
|
|
692
|
-
split_part = part.split(":")
|
|
693
|
-
names.extend(str(ch) for ch in range(int(split_part[0]), int(split_part[1]) + 1))
|
|
694
|
-
else:
|
|
695
|
-
names.append(part)
|
|
696
|
-
|
|
697
|
-
return names
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
if __name__ == "__main__":
|
|
701
|
-
logging.basicConfig(level=20)
|
|
702
|
-
|
|
703
|
-
print(f'{get_channel_names("(@101:105)")=}')
|
|
704
|
-
print(f'{get_channel_names("(@101, 102, 103, 105)")=}')
|
|
705
|
-
# sys.exit(0)
|
|
706
|
-
|
|
707
|
-
daq = DAQ6510Controller()
|
|
708
|
-
daq.connect()
|
|
709
|
-
daq.reset()
|
|
710
|
-
|
|
711
|
-
print(daq.info())
|
|
712
|
-
|
|
713
|
-
buffer_capacity = daq.get_buffer_capacity()
|
|
714
|
-
print(f"buffer {DEFAULT_BUFFER_1} can still hold {buffer_capacity} readings")
|
|
715
|
-
|
|
716
|
-
buffer_count = daq.get_buffer_count()
|
|
717
|
-
print(f"buffer {DEFAULT_BUFFER_1} holds {buffer_count} readings")
|
|
718
|
-
|
|
719
|
-
channels = create_channel_list((101, 102))
|
|
720
|
-
|
|
721
|
-
print(channels)
|
|
722
|
-
|
|
723
|
-
sense_dict = {"TEMPERATURE": [("TRANSDUCER", "FRTD"), ("RTD:FOUR", "PT100"), ("UNIT", "KELVIN")]}
|
|
724
|
-
|
|
725
|
-
daq.configure_sensors(channels, sense=sense_dict)
|
|
726
|
-
|
|
727
|
-
daq.setup_measurements(channel_list=channels)
|
|
728
|
-
|
|
729
|
-
meas_response = daq.perform_measurement(channel_list=channels, count=5, interval=1)
|
|
730
|
-
|
|
731
|
-
print(meas_response)
|
|
732
|
-
|
|
733
|
-
buffer_count = daq.get_buffer_count()
|
|
734
|
-
print(f"buffer {DEFAULT_BUFFER_1} holds {buffer_count} readings")
|
|
735
|
-
|
|
736
|
-
daq.disconnect()
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Keithley DAQ6510 device implementation as an SCPI interface.
|
|
3
|
+
|
|
4
|
+
This module provides an asynchronous interface to communicate with
|
|
5
|
+
the Keithley DAQ6510 data acquisition unit using SCPI commands.
|
|
6
|
+
|
|
7
|
+
Classes:
|
|
8
|
+
DAQ6510: An asynchronous SCPI interface for the Keithley DAQ6510 device.
|
|
9
|
+
|
|
10
|
+
"""
|
|
11
|
+
|
|
1
12
|
__all__ = [
|
|
2
13
|
"DAQ6510",
|
|
3
14
|
]
|
|
@@ -14,7 +25,7 @@ from egse.settings import Settings
|
|
|
14
25
|
dev_settings = Settings.load("Keithley DAQ6510")
|
|
15
26
|
|
|
16
27
|
DEV_HOST = dev_settings.get("HOSTNAME", "localhost")
|
|
17
|
-
DEV_PORT = dev_settings.get("PORT",
|
|
28
|
+
DEV_PORT = dev_settings.get("PORT", 5025)
|
|
18
29
|
DEVICE_NAME = dev_settings.get("DEVICE_NAME", "DAQ6510")
|
|
19
30
|
DEV_ID_VALIDATION = "DAQ6510"
|
|
20
31
|
|
|
@@ -36,6 +47,7 @@ class DAQ6510(AsyncSCPIInterface):
|
|
|
36
47
|
port=port,
|
|
37
48
|
settings=settings,
|
|
38
49
|
id_validation=DEV_ID_VALIDATION, # String that must appear in IDN? response
|
|
50
|
+
read_timeout=5.0,
|
|
39
51
|
)
|
|
40
52
|
|
|
41
53
|
self._measurement_lock = asyncio.Lock()
|
|
@@ -22,8 +22,8 @@ from egse.tempcontrol.keithley.daq6510_adev import DAQ6510
|
|
|
22
22
|
|
|
23
23
|
settings = Settings.load("Keithley DAQ6510")
|
|
24
24
|
|
|
25
|
-
DAQ_DEV_HOST = settings.get("HOSTNAME")
|
|
26
|
-
DAQ_DEV_PORT = settings.get("PORT")
|
|
25
|
+
DAQ_DEV_HOST = settings.get("HOSTNAME", "localhost")
|
|
26
|
+
DAQ_DEV_PORT = settings.get("PORT", 5025)
|
|
27
27
|
|
|
28
28
|
DAQ_MON_CMD_PORT = 5556
|
|
29
29
|
|
|
@@ -40,7 +40,7 @@ class DAQ6510Monitor:
|
|
|
40
40
|
daq_port: int = DAQ_DEV_PORT,
|
|
41
41
|
zmq_port: int = DAQ_MON_CMD_PORT,
|
|
42
42
|
log_file: str = "temperature_readings.log",
|
|
43
|
-
channels: list[str] = None,
|
|
43
|
+
channels: list[str] | None = None,
|
|
44
44
|
poll_interval: float = 60.0,
|
|
45
45
|
):
|
|
46
46
|
"""Initialize the DAQ6510 monitoring service.
|
|
@@ -115,6 +115,8 @@ class DAQ6510Monitor:
|
|
|
115
115
|
async def connect_daq(self):
|
|
116
116
|
"""Establish connection to the DAQ6510."""
|
|
117
117
|
while self.running:
|
|
118
|
+
assert self.daq_interface is not None # extra check and make mypy happy
|
|
119
|
+
|
|
118
120
|
init_commands = [
|
|
119
121
|
('TRAC:MAKE "test1", 1000', False), # create a new buffer
|
|
120
122
|
# settings for channel 1 and 2 of slot 1
|
|
@@ -166,6 +168,8 @@ class DAQ6510Monitor:
|
|
|
166
168
|
"""Main polling loop for temperature measurements."""
|
|
167
169
|
logger.info(f"Starting temperature polling loop (interval: {self.poll_interval}s, channels: {self.channels})")
|
|
168
170
|
|
|
171
|
+
assert self.daq_interface is not None # extra check and make mypy happy
|
|
172
|
+
|
|
169
173
|
# The next lines are a way to calculate the sleep time between two measurements, this takes the time of the
|
|
170
174
|
# measurement itself into account.
|
|
171
175
|
def interval():
|
|
@@ -329,6 +333,8 @@ class DAQ6510Monitor:
|
|
|
329
333
|
"""Get a reading for the given channel(s)."""
|
|
330
334
|
logger.info(f"GET_READING – {params = }")
|
|
331
335
|
|
|
336
|
+
assert self.daq_interface is not None # extra check and make mypy happy
|
|
337
|
+
|
|
332
338
|
readings = {"status": "ok", "data": {}}
|
|
333
339
|
|
|
334
340
|
for channel in params["channels"]:
|
|
@@ -424,7 +430,8 @@ class DAQMonitorClient:
|
|
|
424
430
|
|
|
425
431
|
def disconnect(self):
|
|
426
432
|
"""Close the client connection."""
|
|
427
|
-
self.socket
|
|
433
|
+
if self.socket:
|
|
434
|
+
self.socket.close(linger=100)
|
|
428
435
|
self.ctx.term()
|
|
429
436
|
|
|
430
437
|
def __enter__(self):
|
|
@@ -436,7 +443,7 @@ class DAQMonitorClient:
|
|
|
436
443
|
if exc_type:
|
|
437
444
|
logger.error(f"Caught {exc_type}: {exc_val}")
|
|
438
445
|
|
|
439
|
-
def _send_command(self, command: str, params: dict[str, Any] = None) -> dict[str, Any]:
|
|
446
|
+
def _send_command(self, command: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
440
447
|
"""Send a command to the monitoring service.
|
|
441
448
|
|
|
442
449
|
Args:
|
|
@@ -449,6 +456,8 @@ class DAQMonitorClient:
|
|
|
449
456
|
params = params or {}
|
|
450
457
|
message = {"command": command, "params": params}
|
|
451
458
|
|
|
459
|
+
assert self.socket is not None, "Client not connected. Call connect() first."
|
|
460
|
+
|
|
452
461
|
try:
|
|
453
462
|
self.socket.send_multipart([b"", json.dumps(message).encode("utf-8")])
|
|
454
463
|
_, response_data = self.socket.recv_multipart()
|