keithley-tempcontrol 0.17.3__py3-none-any.whl → 0.17.4__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/daq6510.py +31 -22
- egse/tempcontrol/keithley/daq6510_adev.py +15 -35
- egse/tempcontrol/keithley/daq6510_cs.py +89 -44
- egse/tempcontrol/keithley/daq6510_dev.py +78 -20
- egse/tempcontrol/keithley/daq6510_mon.py +58 -22
- egse/tempcontrol/keithley/daq6510_protocol.py +18 -35
- egse/tempcontrol/keithley/daq6510_sim.py +117 -60
- keithley_tempcontrol/cgse_services.py +26 -7
- {keithley_tempcontrol-0.17.3.dist-info → keithley_tempcontrol-0.17.4.dist-info}/METADATA +1 -1
- keithley_tempcontrol-0.17.4.dist-info/RECORD +17 -0
- keithley_tempcontrol-0.17.3.dist-info/RECORD +0 -17
- {keithley_tempcontrol-0.17.3.dist-info → keithley_tempcontrol-0.17.4.dist-info}/WHEEL +0 -0
- {keithley_tempcontrol-0.17.3.dist-info → keithley_tempcontrol-0.17.4.dist-info}/entry_points.txt +0 -0
|
@@ -1,33 +1,40 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import re
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Dict
|
|
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.
|
|
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
|
|
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
|
-
|
|
24
|
-
|
|
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) ->
|
|
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 =
|
|
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
|
|
227
|
-
|
|
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 =
|
|
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) ->
|
|
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:
|
|
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 =
|
|
570
|
-
hostname: str =
|
|
571
|
-
port: int =
|
|
572
|
-
timeout:
|
|
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 (
|
|
588
|
+
timeout (float): Timeout by which to establish the connection [s]
|
|
582
589
|
"""
|
|
583
590
|
|
|
584
|
-
|
|
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:
|
|
@@ -1,18 +1,28 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"DAQ6510",
|
|
3
|
+
]
|
|
4
|
+
|
|
1
5
|
import asyncio
|
|
2
|
-
import logging
|
|
3
6
|
from typing import Any
|
|
4
7
|
from typing import Dict
|
|
5
8
|
from typing import Optional
|
|
6
9
|
|
|
10
|
+
from egse.log import logger
|
|
7
11
|
from egse.scpi import AsyncSCPIInterface
|
|
12
|
+
from egse.settings import Settings
|
|
13
|
+
|
|
14
|
+
dev_settings = Settings.load("Keithley DAQ6510")
|
|
8
15
|
|
|
9
|
-
|
|
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"
|
|
10
20
|
|
|
11
21
|
|
|
12
22
|
class DAQ6510(AsyncSCPIInterface):
|
|
13
23
|
"""Keithley DAQ6510 specific implementation."""
|
|
14
24
|
|
|
15
|
-
def __init__(self, hostname: str, port: int =
|
|
25
|
+
def __init__(self, hostname: str = DEV_HOST, port: int = DEV_PORT, settings: Optional[Dict[str, Any]] = None):
|
|
16
26
|
"""Initialize a Keithley DAQ6510 interface.
|
|
17
27
|
|
|
18
28
|
Args:
|
|
@@ -21,45 +31,15 @@ class DAQ6510(AsyncSCPIInterface):
|
|
|
21
31
|
settings: Additional device settings
|
|
22
32
|
"""
|
|
23
33
|
super().__init__(
|
|
24
|
-
device_name=
|
|
34
|
+
device_name=DEVICE_NAME,
|
|
25
35
|
hostname=hostname,
|
|
26
36
|
port=port,
|
|
27
37
|
settings=settings,
|
|
28
|
-
id_validation=
|
|
38
|
+
id_validation=DEV_ID_VALIDATION, # String that must appear in IDN? response
|
|
29
39
|
)
|
|
30
40
|
|
|
31
41
|
self._measurement_lock = asyncio.Lock()
|
|
32
42
|
|
|
33
|
-
async def initialize(self):
|
|
34
|
-
# Initialize
|
|
35
|
-
|
|
36
|
-
await self.write("*RST") # this also the user-defined buffer "test1"
|
|
37
|
-
|
|
38
|
-
for cmd, response in [
|
|
39
|
-
('TRAC:MAKE "test1", 1000', False), # create a new buffer
|
|
40
|
-
# settings for channel 1 and 2 of slot 1
|
|
41
|
-
('SENS:FUNC "TEMP", (@101:102)', False), # set the function to temperature
|
|
42
|
-
("SENS:TEMP:TRAN FRTD, (@101)", False), # set the transducer to 4-wire RTD
|
|
43
|
-
("SENS:TEMP:RTD:FOUR PT100, (@101)", False), # set the type of the 4-wire RTD
|
|
44
|
-
("SENS:TEMP:TRAN RTD, (@102)", False), # set the transducer to 2-wire RTD
|
|
45
|
-
("SENS:TEMP:RTD:TWO PT100, (@102)", False), # set the type of the 2-wire RTD
|
|
46
|
-
('ROUT:SCAN:BUFF "test1"', False),
|
|
47
|
-
("ROUT:SCAN:CRE (@101:102)", False),
|
|
48
|
-
("ROUT:CHAN:OPEN (@101:102)", False),
|
|
49
|
-
("ROUT:STAT? (@101:102)", True),
|
|
50
|
-
("ROUT:SCAN:STAR:STIM NONE", False),
|
|
51
|
-
# ("ROUT:SCAN:ADD:SING (@101, 102)", False), # not sure what this does, not really needed
|
|
52
|
-
("ROUT:SCAN:COUN:SCAN 1", False), # not sure if this is needed in this setting
|
|
53
|
-
# ("ROUT:SCAN:INT 1", False),
|
|
54
|
-
]:
|
|
55
|
-
if response:
|
|
56
|
-
logger.info(f"Sending {cmd}...")
|
|
57
|
-
response = (await self.trans(cmd)).decode().strip()
|
|
58
|
-
logger.info(f"{response = }")
|
|
59
|
-
else:
|
|
60
|
-
logger.info(f"Sending {cmd}...")
|
|
61
|
-
await self.write(cmd)
|
|
62
|
-
|
|
63
43
|
async def get_measurement(self, channel: str) -> float:
|
|
64
44
|
"""Get a measurement from a specific channel.
|
|
65
45
|
|
|
@@ -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
|
-
|
|
20
|
+
cs_settings = Settings.load("Keithley Control Server")
|
|
18
21
|
|
|
19
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
119
|
-
def cli():
|
|
120
|
-
pass
|
|
158
|
+
app = typer.Typer(name="daq6510_cs")
|
|
121
159
|
|
|
122
160
|
|
|
123
|
-
@
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
@
|
|
188
|
+
@app.command()
|
|
146
189
|
def start_bg():
|
|
147
190
|
"""Starts the DAQ6510 Control Server in the background."""
|
|
148
191
|
|
|
149
|
-
|
|
192
|
+
print("Starting the DAQ6510 in the background is not implemented.")
|
|
150
193
|
|
|
151
194
|
|
|
152
|
-
@
|
|
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
|
-
@
|
|
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
|
-
|
|
175
|
-
hostname = CTRL_SETTINGS.HOSTNAME
|
|
176
|
-
port = CTRL_SETTINGS.COMMANDING_PORT
|
|
221
|
+
from egse.env import setup_env
|
|
177
222
|
|
|
178
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
sys.exit(cli())
|
|
240
|
+
sys.exit(app())
|
|
@@ -1,23 +1,30 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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 +
|
|
42
|
+
return out + SEPARATOR_STR
|
|
36
43
|
|
|
37
44
|
|
|
38
|
-
class
|
|
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 =
|
|
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.
|
|
52
|
-
self.
|
|
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 +=
|
|
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) ->
|
|
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 +=
|
|
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
|
|
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
|
|
|
@@ -1,22 +1,31 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import datetime
|
|
3
3
|
import json
|
|
4
|
-
import logging
|
|
5
4
|
import signal
|
|
6
5
|
import time
|
|
7
6
|
from asyncio import Task
|
|
8
7
|
from pathlib import Path
|
|
9
8
|
from typing import Any
|
|
9
|
+
from typing import Callable
|
|
10
10
|
from typing import Optional
|
|
11
11
|
|
|
12
|
+
import typer
|
|
12
13
|
import zmq
|
|
13
14
|
import zmq.asyncio
|
|
14
15
|
|
|
15
16
|
from egse.device import DeviceConnectionError
|
|
16
17
|
from egse.device import DeviceTimeoutError
|
|
18
|
+
from egse.log import logger
|
|
19
|
+
from egse.settings import Settings
|
|
20
|
+
from egse.system import TyperAsyncCommand
|
|
17
21
|
from egse.tempcontrol.keithley.daq6510_adev import DAQ6510
|
|
18
22
|
|
|
19
|
-
|
|
23
|
+
settings = Settings.load("Keithley DAQ6510")
|
|
24
|
+
|
|
25
|
+
DAQ_DEV_HOST = settings.get("HOSTNAME")
|
|
26
|
+
DAQ_DEV_PORT = settings.get("PORT")
|
|
27
|
+
|
|
28
|
+
DAQ_MON_CMD_PORT = 5556
|
|
20
29
|
|
|
21
30
|
|
|
22
31
|
class DAQ6510Monitor:
|
|
@@ -28,8 +37,8 @@ class DAQ6510Monitor:
|
|
|
28
37
|
def __init__(
|
|
29
38
|
self,
|
|
30
39
|
daq_hostname: str,
|
|
31
|
-
daq_port: int =
|
|
32
|
-
zmq_port: int =
|
|
40
|
+
daq_port: int = DAQ_DEV_PORT,
|
|
41
|
+
zmq_port: int = DAQ_MON_CMD_PORT,
|
|
33
42
|
log_file: str = "temperature_readings.log",
|
|
34
43
|
channels: list[str] = None,
|
|
35
44
|
poll_interval: float = 60.0,
|
|
@@ -60,7 +69,7 @@ class DAQ6510Monitor:
|
|
|
60
69
|
self.running = False
|
|
61
70
|
self.polling_active = False
|
|
62
71
|
self.daq_interface = None
|
|
63
|
-
self.command_handlers = {
|
|
72
|
+
self.command_handlers: dict[str, Callable] = {
|
|
64
73
|
"START_POLLING": self._handle_start_polling,
|
|
65
74
|
"STOP_POLLING": self._handle_stop_polling,
|
|
66
75
|
"SET_INTERVAL": self._handle_set_interval,
|
|
@@ -78,6 +87,8 @@ class DAQ6510Monitor:
|
|
|
78
87
|
self.log_file.parent.mkdir(exist_ok=True, parents=True)
|
|
79
88
|
|
|
80
89
|
# Create DAQ interface
|
|
90
|
+
# In this case we use the device itself, no control server. That means
|
|
91
|
+
# the monitoring must be the only service connecting to the device.
|
|
81
92
|
self.daq_interface = DAQ6510(hostname=daq_hostname, port=daq_port)
|
|
82
93
|
|
|
83
94
|
async def start(self):
|
|
@@ -85,9 +96,12 @@ class DAQ6510Monitor:
|
|
|
85
96
|
logger.info(f"Starting DAQ6510 Monitoring Service on ZMQ port {self.zmq_port}")
|
|
86
97
|
self.running = True
|
|
87
98
|
|
|
99
|
+
def handle_shutdown():
|
|
100
|
+
asyncio.create_task(self.shutdown())
|
|
101
|
+
|
|
88
102
|
# Register signal handlers for graceful shutdown
|
|
89
103
|
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
90
|
-
asyncio.get_event_loop().add_signal_handler(sig,
|
|
104
|
+
asyncio.get_event_loop().add_signal_handler(sig, handle_shutdown)
|
|
91
105
|
|
|
92
106
|
# Start the main service tasks
|
|
93
107
|
await asyncio.gather(self.command_listener(), self.connect_daq(), return_exceptions=True)
|
|
@@ -101,11 +115,29 @@ class DAQ6510Monitor:
|
|
|
101
115
|
async def connect_daq(self):
|
|
102
116
|
"""Establish connection to the DAQ6510."""
|
|
103
117
|
while self.running:
|
|
118
|
+
init_commands = [
|
|
119
|
+
('TRAC:MAKE "test1", 1000', False), # create a new buffer
|
|
120
|
+
# settings for channel 1 and 2 of slot 1
|
|
121
|
+
('SENS:FUNC "TEMP", (@101:102)', False), # set the function to temperature
|
|
122
|
+
("SENS:TEMP:TRAN FRTD, (@101)", False), # set the transducer to 4-wire RTD
|
|
123
|
+
("SENS:TEMP:RTD:FOUR PT100, (@101)", False), # set the type of the 4-wire RTD
|
|
124
|
+
("SENS:TEMP:TRAN RTD, (@102)", False), # set the transducer to 2-wire RTD
|
|
125
|
+
("SENS:TEMP:RTD:TWO PT100, (@102)", False), # set the type of the 2-wire RTD
|
|
126
|
+
('ROUT:SCAN:BUFF "test1"', False),
|
|
127
|
+
("ROUT:SCAN:CRE (@101:102)", False),
|
|
128
|
+
("ROUT:CHAN:OPEN (@101:102)", False),
|
|
129
|
+
("ROUT:STAT? (@101:102)", True),
|
|
130
|
+
("ROUT:SCAN:STAR:STIM NONE", False),
|
|
131
|
+
# ("ROUT:SCAN:ADD:SING (@101, 102)", False), # not sure what this does, not really needed
|
|
132
|
+
("ROUT:SCAN:COUN:SCAN 1", False), # not sure if this is needed in this setting
|
|
133
|
+
# ("ROUT:SCAN:INT 1", False),
|
|
134
|
+
]
|
|
135
|
+
|
|
104
136
|
try:
|
|
105
137
|
logger.info(f"Connecting to DAQ6510 at {self.daq_hostname}:{self.daq_port}")
|
|
106
138
|
await self.daq_interface.connect()
|
|
107
139
|
logger.info("Successfully connected to DAQ6510.")
|
|
108
|
-
await self.daq_interface.initialize()
|
|
140
|
+
await self.daq_interface.initialize(commands=init_commands, reset_device=True)
|
|
109
141
|
logger.info("Successfully initialized DAQ6510 for measurements.")
|
|
110
142
|
|
|
111
143
|
# If we were polling before, restart it.
|
|
@@ -367,9 +399,9 @@ class DAQ6510Monitor:
|
|
|
367
399
|
|
|
368
400
|
|
|
369
401
|
class DAQMonitorClient:
|
|
370
|
-
"""
|
|
402
|
+
"""A simple client for interacting with the DAQ Monitor Service."""
|
|
371
403
|
|
|
372
|
-
def __init__(self, server_address: str = "localhost", port: int =
|
|
404
|
+
def __init__(self, server_address: str = "localhost", port: int = DAQ_MON_CMD_PORT, timeout: float = 5.0):
|
|
373
405
|
"""Initialize the client.
|
|
374
406
|
|
|
375
407
|
Args:
|
|
@@ -412,7 +444,7 @@ class DAQMonitorClient:
|
|
|
412
444
|
params: Optional command parameters
|
|
413
445
|
|
|
414
446
|
Returns:
|
|
415
|
-
Response from the service
|
|
447
|
+
Response from the service as a dictionary.
|
|
416
448
|
"""
|
|
417
449
|
params = params or {}
|
|
418
450
|
message = {"command": command, "params": params}
|
|
@@ -488,8 +520,10 @@ class DAQMonitorClient:
|
|
|
488
520
|
def get_status(self) -> dict[str, Any]:
|
|
489
521
|
"""Get current service status.
|
|
490
522
|
|
|
523
|
+
To confirm the status is 'ok', check the response for the key 'status'.
|
|
524
|
+
|
|
491
525
|
Returns:
|
|
492
|
-
Status information
|
|
526
|
+
Status information as dictionary.
|
|
493
527
|
"""
|
|
494
528
|
return self._send_command("GET_STATUS")
|
|
495
529
|
|
|
@@ -502,12 +536,19 @@ class DAQMonitorClient:
|
|
|
502
536
|
return self._send_command("SHUTDOWN")
|
|
503
537
|
|
|
504
538
|
|
|
505
|
-
|
|
539
|
+
app = typer.Typer(name="daq6510_mon")
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
@app.command(cls=TyperAsyncCommand, name="monitor")
|
|
543
|
+
async def main(log_file: str = "temperature_readings.log"):
|
|
544
|
+
"""
|
|
545
|
+
Start the DAQ6510 monitoring app in the background.
|
|
546
|
+
"""
|
|
506
547
|
monitor = DAQ6510Monitor(
|
|
507
|
-
daq_hostname=
|
|
508
|
-
daq_port=
|
|
509
|
-
zmq_port=
|
|
510
|
-
log_file=
|
|
548
|
+
daq_hostname=DAQ_DEV_HOST,
|
|
549
|
+
daq_port=DAQ_DEV_PORT,
|
|
550
|
+
zmq_port=DAQ_MON_CMD_PORT,
|
|
551
|
+
log_file=log_file,
|
|
511
552
|
channels=["101", "102"],
|
|
512
553
|
poll_interval=10.0,
|
|
513
554
|
)
|
|
@@ -516,9 +557,4 @@ async def main():
|
|
|
516
557
|
|
|
517
558
|
|
|
518
559
|
if __name__ == "__main__":
|
|
519
|
-
|
|
520
|
-
level=logging.DEBUG,
|
|
521
|
-
format="[%(asctime)s] %(threadName)-12s %(levelname)-8s %(name)-12s %(lineno)5d:%(module)-20s %(message)s",
|
|
522
|
-
)
|
|
523
|
-
|
|
524
|
-
asyncio.run(main())
|
|
560
|
+
asyncio.run(app())
|
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from pathlib import Path
|
|
2
3
|
|
|
3
4
|
from egse.control import ControlServer
|
|
4
5
|
from egse.device import DeviceTimeoutError
|
|
5
|
-
from egse.hk import read_conversion_dict
|
|
6
|
-
from egse.metrics import define_metrics
|
|
7
6
|
from egse.protocol import CommandProtocol
|
|
8
7
|
from egse.settings import Settings
|
|
9
8
|
from egse.setup import load_setup
|
|
10
|
-
from egse.synoptics import SynopticsManagerProxy
|
|
11
9
|
from egse.system import format_datetime
|
|
12
10
|
from egse.tempcontrol.keithley.daq6510 import DAQ6510Controller
|
|
13
11
|
from egse.tempcontrol.keithley.daq6510 import DAQ6510Interface
|
|
14
|
-
from egse.tempcontrol.keithley.daq6510 import DAQ6510Simulator
|
|
15
12
|
from egse.tempcontrol.keithley.daq6510_dev import DAQ6510Command
|
|
16
13
|
from egse.zmq_ser import bind_address
|
|
17
14
|
|
|
18
|
-
|
|
15
|
+
HERE = Path(__file__).parent
|
|
16
|
+
|
|
17
|
+
COMMAND_SETTINGS = Settings.load(location=HERE, filename="daq6510.yaml")
|
|
19
18
|
|
|
20
19
|
MODULE_LOGGER = logging.getLogger(__name__)
|
|
21
20
|
|
|
@@ -28,13 +27,9 @@ class DAQ6510Protocol(CommandProtocol):
|
|
|
28
27
|
control_server: Control Server for which to send out status and monitoring information
|
|
29
28
|
"""
|
|
30
29
|
|
|
31
|
-
super().__init__()
|
|
32
|
-
self.control_server = control_server
|
|
30
|
+
super().__init__(control_server)
|
|
33
31
|
|
|
34
|
-
|
|
35
|
-
self.daq = DAQ6510Simulator()
|
|
36
|
-
else:
|
|
37
|
-
self.daq = DAQ6510Controller()
|
|
32
|
+
self.daq = DAQ6510Controller()
|
|
38
33
|
|
|
39
34
|
try:
|
|
40
35
|
self.daq.connect()
|
|
@@ -47,16 +42,12 @@ class DAQ6510Protocol(CommandProtocol):
|
|
|
47
42
|
setup = load_setup()
|
|
48
43
|
self.channels = setup.gse.DAQ6510.channels
|
|
49
44
|
|
|
50
|
-
self.hk_conversion_table = read_conversion_dict(self.control_server.get_storage_mnemonic(), setup=setup)
|
|
51
|
-
|
|
52
|
-
self.synoptics = SynopticsManagerProxy()
|
|
53
|
-
self.metrics = define_metrics(origin="DAS-DAQ6510", use_site=True, setup=setup)
|
|
54
|
-
|
|
55
45
|
def get_bind_address(self) -> str:
|
|
56
|
-
"""
|
|
57
|
-
|
|
46
|
+
"""
|
|
47
|
+
Returns a string with the bind address, the endpoint, for accepting connections and bind a socket to.
|
|
58
48
|
|
|
59
|
-
Returns:
|
|
49
|
+
Returns:
|
|
50
|
+
String with the protocol and port to bind a socket to.
|
|
60
51
|
"""
|
|
61
52
|
|
|
62
53
|
return bind_address(
|
|
@@ -65,34 +56,26 @@ class DAQ6510Protocol(CommandProtocol):
|
|
|
65
56
|
)
|
|
66
57
|
|
|
67
58
|
def get_status(self) -> dict:
|
|
68
|
-
"""
|
|
59
|
+
"""
|
|
60
|
+
Returns a dictionary with status information for the Control Server and the DAQ6510.
|
|
69
61
|
|
|
70
|
-
Returns:
|
|
62
|
+
Returns:
|
|
63
|
+
Dictionary with status information for the Control Server and the DAQ6510.
|
|
71
64
|
"""
|
|
72
65
|
|
|
73
66
|
return super().get_status()
|
|
74
67
|
|
|
75
68
|
def get_housekeeping(self) -> dict:
|
|
76
|
-
"""
|
|
69
|
+
"""
|
|
70
|
+
Returns a dictionary with housekeeping information about the DAQ6510.
|
|
77
71
|
|
|
78
|
-
Returns:
|
|
72
|
+
Returns:
|
|
73
|
+
Dictionary with housekeeping information about the DAQ6510.
|
|
79
74
|
"""
|
|
80
75
|
|
|
81
76
|
hk_dict = dict()
|
|
82
77
|
hk_dict["timestamp"] = format_datetime()
|
|
83
78
|
|
|
84
|
-
# # TODO I guess we have to do something along those lines
|
|
85
|
-
# # (We'll have to increase the HK delay, cfr. Agilents)
|
|
86
|
-
# measurement = self.daq.perform_measurement(channel_list=self.channels)
|
|
87
|
-
# temperatures = convert_hk_names(measurement, self.hk_conversion_table)
|
|
88
|
-
# hk_dict.update(temperatures)
|
|
89
|
-
#
|
|
90
|
-
# self.synoptics.store_th_synoptics(hk_dict)
|
|
91
|
-
#
|
|
92
|
-
# for key, value in hk_dict.items():
|
|
93
|
-
# if key != "timestamp":
|
|
94
|
-
# self.metrics[key].set(value)
|
|
95
|
-
|
|
96
79
|
return hk_dict
|
|
97
80
|
|
|
98
81
|
def quit(self) -> None:
|
|
@@ -1,21 +1,32 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
1
|
import contextlib
|
|
4
2
|
import datetime
|
|
5
|
-
import logging
|
|
6
3
|
import re
|
|
7
4
|
import socket
|
|
8
5
|
import time
|
|
6
|
+
from functools import partial
|
|
7
|
+
from typing import Annotated
|
|
9
8
|
|
|
10
9
|
import typer
|
|
10
|
+
|
|
11
|
+
from egse.env import bool_env
|
|
12
|
+
from egse.log import logging
|
|
11
13
|
from egse.settings import Settings
|
|
12
14
|
from egse.system import SignalCatcher
|
|
13
15
|
|
|
14
|
-
logger = logging.getLogger("daq6510-sim")
|
|
16
|
+
logger = logging.getLogger("egse.daq6510-sim")
|
|
15
17
|
|
|
18
|
+
VERSION = "0.1.0"
|
|
19
|
+
VERBOSE_DEBUG = bool_env("VERBOSE_DEBUG")
|
|
16
20
|
HOST = "localhost"
|
|
17
21
|
DAQ_SETTINGS = Settings.load("Keithley DAQ6510")
|
|
18
22
|
|
|
23
|
+
READ_TIMEOUT = 2.0
|
|
24
|
+
"""The timeout set on the connection socket, applicable when reading from the socket with `recv`."""
|
|
25
|
+
CONNECTION_TIMEOUT = 2.0
|
|
26
|
+
"""The timeout set on the socket before accepting a connection."""
|
|
27
|
+
|
|
28
|
+
SEPARATOR = b"\n"
|
|
29
|
+
SEPARATOR_STR = SEPARATOR.decode()
|
|
19
30
|
|
|
20
31
|
device_time = datetime.datetime.now(datetime.timezone.utc)
|
|
21
32
|
reference_time = device_time
|
|
@@ -23,8 +34,8 @@ reference_time = device_time
|
|
|
23
34
|
|
|
24
35
|
app = typer.Typer(help="DAQ6510 Simulator")
|
|
25
36
|
|
|
26
|
-
error_msg: str
|
|
27
|
-
"""Global error message, always contains the last error. Reset in the inner loop of run_simulator."""
|
|
37
|
+
error_msg: str = ""
|
|
38
|
+
"""Global error message, always contains the last error. Reset to an empty string in the inner loop of run_simulator."""
|
|
28
39
|
|
|
29
40
|
|
|
30
41
|
def create_datetime(year, month, day, hour, minute, second):
|
|
@@ -62,8 +73,14 @@ def reset():
|
|
|
62
73
|
logger.info("RESET")
|
|
63
74
|
|
|
64
75
|
|
|
76
|
+
def log(level: int, msg: str):
|
|
77
|
+
logger.log(level, msg)
|
|
78
|
+
|
|
79
|
+
|
|
65
80
|
COMMAND_ACTIONS_RESPONSES = {
|
|
66
|
-
"*IDN?": (None, "KEITHLEY INSTRUMENTS,
|
|
81
|
+
"*IDN?": (None, f"KEITHLEY INSTRUMENTS,DAQ6510,SIMULATOR,{VERSION}"),
|
|
82
|
+
"*ACTION-RESPONSE?": (partial(log, logging.INFO, "Requested action with response."), get_time),
|
|
83
|
+
"*ACTION-NO-RESPONSE": (partial(log, logging.INFO, "Requested action without response."), None),
|
|
67
84
|
}
|
|
68
85
|
|
|
69
86
|
# Check the regex at https://regex101.com
|
|
@@ -79,65 +96,105 @@ COMMAND_PATTERNS_ACTIONS_RESPONSES = {
|
|
|
79
96
|
|
|
80
97
|
|
|
81
98
|
def write(conn, response: str):
|
|
82
|
-
response = f"{response}
|
|
83
|
-
|
|
99
|
+
response = f"{response}{SEPARATOR_STR}".encode()
|
|
100
|
+
if VERBOSE_DEBUG:
|
|
101
|
+
logger.debug(f"write: {response = }")
|
|
84
102
|
conn.sendall(response)
|
|
85
103
|
|
|
86
104
|
|
|
105
|
+
# Keep a receive buffer per connection
|
|
106
|
+
_recv_buffers: dict[int, bytes] = {}
|
|
107
|
+
|
|
108
|
+
|
|
87
109
|
def read(conn) -> str:
|
|
88
110
|
"""
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
The command string with the linefeed stripped off.
|
|
111
|
+
Read bytes from `conn` until a `SEPARATOR` is found (or connection closed / timeout).
|
|
112
|
+
Returns the first chunk (separator stripped). Any bytes after the separator are kept
|
|
113
|
+
in a per-connection buffer for the next call.
|
|
93
114
|
"""
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
buf_size = 1024 * 4
|
|
97
|
-
command_string = bytes()
|
|
115
|
+
fileno = conn.fileno()
|
|
116
|
+
buf = _recv_buffers.get(fileno, b"")
|
|
98
117
|
|
|
99
118
|
try:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
119
|
+
while True:
|
|
120
|
+
# If we already have a full line in the buffer, split and return it.
|
|
121
|
+
if SEPARATOR in buf:
|
|
122
|
+
line, rest = buf.split(SEPARATOR, 1)
|
|
123
|
+
_recv_buffers[fileno] = rest
|
|
124
|
+
logger.info(f"read: {line=}")
|
|
125
|
+
return line.decode().rstrip()
|
|
126
|
+
|
|
127
|
+
# Read more data
|
|
128
|
+
data = conn.recv(1024 * 4)
|
|
129
|
+
if not data:
|
|
130
|
+
# Connection closed by peer; return whatever we have (may be empty)
|
|
131
|
+
_recv_buffers.pop(fileno, None)
|
|
132
|
+
logger.info(f"read (connection closed): {buf=}")
|
|
133
|
+
return buf.decode().rstrip()
|
|
134
|
+
buf += data
|
|
135
|
+
_recv_buffers[fileno] = buf
|
|
136
|
+
|
|
108
137
|
except socket.timeout:
|
|
109
|
-
#
|
|
138
|
+
# If we have accumulated data without a separator, return it (partial read),
|
|
139
|
+
# otherwise propagate the timeout so caller can handle/suppress it.
|
|
140
|
+
if buf:
|
|
141
|
+
_recv_buffers[fileno] = buf
|
|
142
|
+
logger.info(f"read (timeout, partial): {buf=}")
|
|
143
|
+
return buf.decode().rstrip()
|
|
110
144
|
raise
|
|
111
145
|
|
|
112
|
-
logger.info(f"read: {command_string=}")
|
|
113
|
-
|
|
114
|
-
return command_string.decode().rstrip()
|
|
115
|
-
|
|
116
146
|
|
|
117
|
-
def process_command(command_string: str) -> str:
|
|
147
|
+
def process_command(command_string: str) -> str | None:
|
|
148
|
+
"""Process the given command string and return a response."""
|
|
118
149
|
global COMMAND_ACTIONS_RESPONSES
|
|
119
150
|
global COMMAND_PATTERNS_ACTIONS_RESPONSES
|
|
120
151
|
global error_msg
|
|
121
152
|
|
|
122
|
-
|
|
153
|
+
if VERBOSE_DEBUG:
|
|
154
|
+
logger.debug(f"{command_string=}")
|
|
123
155
|
|
|
124
156
|
try:
|
|
125
157
|
action, response = COMMAND_ACTIONS_RESPONSES[command_string]
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
158
|
+
if VERBOSE_DEBUG:
|
|
159
|
+
logger.debug(f"{action=}, {response=}")
|
|
160
|
+
|
|
161
|
+
if action:
|
|
162
|
+
action()
|
|
163
|
+
|
|
164
|
+
if response:
|
|
165
|
+
if error_msg:
|
|
166
|
+
return error_msg
|
|
167
|
+
else:
|
|
168
|
+
return response() if callable(response) else response
|
|
129
169
|
else:
|
|
130
|
-
|
|
170
|
+
if error_msg:
|
|
171
|
+
logger.error(f"Error occurred during process command: {error_msg}")
|
|
172
|
+
return None
|
|
131
173
|
except KeyError:
|
|
132
174
|
# try to match with a value
|
|
133
175
|
for key, value in COMMAND_PATTERNS_ACTIONS_RESPONSES.items():
|
|
134
176
|
if match := re.match(key, command_string, flags=re.IGNORECASE):
|
|
135
|
-
|
|
177
|
+
if VERBOSE_DEBUG:
|
|
178
|
+
logger.debug(f"{match=}, {match.groups()}")
|
|
136
179
|
action, response = value
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
180
|
+
if VERBOSE_DEBUG:
|
|
181
|
+
logger.debug(f"{action=}, {response=}")
|
|
182
|
+
|
|
183
|
+
if action:
|
|
184
|
+
action(*match.groups())
|
|
185
|
+
|
|
186
|
+
if response:
|
|
187
|
+
if error_msg:
|
|
188
|
+
return error_msg
|
|
189
|
+
else:
|
|
190
|
+
return response() if callable(response) else response
|
|
191
|
+
else:
|
|
192
|
+
if error_msg:
|
|
193
|
+
logger.error(f"Error occurred during process command: {error_msg}")
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
logger.error(f"ERROR: unknown command string: {command_string}")
|
|
197
|
+
return None
|
|
141
198
|
|
|
142
199
|
|
|
143
200
|
def run_simulator():
|
|
@@ -150,7 +207,7 @@ def run_simulator():
|
|
|
150
207
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
151
208
|
s.bind((HOST, DAQ_SETTINGS.PORT))
|
|
152
209
|
s.listen()
|
|
153
|
-
s.settimeout(
|
|
210
|
+
s.settimeout(CONNECTION_TIMEOUT)
|
|
154
211
|
while True:
|
|
155
212
|
while True:
|
|
156
213
|
with contextlib.suppress(socket.timeout):
|
|
@@ -161,13 +218,17 @@ def run_simulator():
|
|
|
161
218
|
with conn:
|
|
162
219
|
logger.info(f"Accepted connection from {addr}")
|
|
163
220
|
write(conn, "This is PLATO DAQ6510 X.X.sim")
|
|
164
|
-
conn.settimeout(
|
|
221
|
+
conn.settimeout(READ_TIMEOUT)
|
|
165
222
|
try:
|
|
166
223
|
while True:
|
|
167
224
|
error_msg = ""
|
|
168
225
|
with contextlib.suppress(socket.timeout):
|
|
169
226
|
data = read(conn)
|
|
170
|
-
|
|
227
|
+
if VERBOSE_DEBUG:
|
|
228
|
+
logger.debug(f"{data = }")
|
|
229
|
+
if not data:
|
|
230
|
+
logger.info("Client closed connection, accepting new connection...")
|
|
231
|
+
break
|
|
171
232
|
if data.strip() == "STOP":
|
|
172
233
|
logger.info("Client requested to terminate...")
|
|
173
234
|
s.close()
|
|
@@ -176,9 +237,6 @@ def run_simulator():
|
|
|
176
237
|
response = process_command(cmd.strip())
|
|
177
238
|
if response is not None:
|
|
178
239
|
write(conn, response)
|
|
179
|
-
if not data:
|
|
180
|
-
logger.info("Client closed connection, accepting new connection...")
|
|
181
|
-
break
|
|
182
240
|
if killer.term_signal_received:
|
|
183
241
|
logger.info("Terminating...")
|
|
184
242
|
s.close()
|
|
@@ -196,20 +254,20 @@ def run_simulator():
|
|
|
196
254
|
logger.info(f"{exc.__class__.__name__} caught: {exc.args}")
|
|
197
255
|
|
|
198
256
|
|
|
199
|
-
def send_request(cmd: str,
|
|
200
|
-
from egse.tempcontrol.keithley.daq6510_dev import
|
|
257
|
+
def send_request(cmd: str, cmd_type: str = "query") -> str | None:
|
|
258
|
+
from egse.tempcontrol.keithley.daq6510_dev import DAQ6510
|
|
201
259
|
|
|
202
260
|
response = None
|
|
203
261
|
|
|
204
|
-
daq_dev =
|
|
262
|
+
daq_dev = DAQ6510(hostname="localhost", port=5025)
|
|
205
263
|
daq_dev.connect()
|
|
206
264
|
|
|
207
|
-
if
|
|
265
|
+
if cmd_type.lower().strip() == "query":
|
|
208
266
|
response = daq_dev.query(cmd)
|
|
209
|
-
elif
|
|
267
|
+
elif cmd_type.lower().strip() == "write":
|
|
210
268
|
daq_dev.write(cmd)
|
|
211
269
|
else:
|
|
212
|
-
logger.info(f"Unknown type {
|
|
270
|
+
logger.info(f"Unknown command type {cmd_type} for send_request.")
|
|
213
271
|
|
|
214
272
|
daq_dev.disconnect()
|
|
215
273
|
|
|
@@ -234,15 +292,14 @@ def stop():
|
|
|
234
292
|
|
|
235
293
|
|
|
236
294
|
@app.command()
|
|
237
|
-
def command(
|
|
238
|
-
|
|
295
|
+
def command(
|
|
296
|
+
cmd: str,
|
|
297
|
+
cmd_type: Annotated[str, typer.Argument(help="either 'write', 'query'")] = "query",
|
|
298
|
+
):
|
|
299
|
+
"""Send an SCPI command directly to the simulator. The response will be in the log info."""
|
|
300
|
+
response = send_request(cmd, cmd_type)
|
|
239
301
|
logger.info(f"{response}")
|
|
240
302
|
|
|
241
303
|
|
|
242
304
|
if __name__ == "__main__":
|
|
243
|
-
logging.basicConfig(
|
|
244
|
-
level=logging.DEBUG,
|
|
245
|
-
format="%(asctime)s %(threadName)-12s %(levelname)-8s %(name)-12s %(module)-20s %(message)s",
|
|
246
|
-
)
|
|
247
|
-
|
|
248
305
|
app()
|
|
@@ -2,14 +2,13 @@
|
|
|
2
2
|
#
|
|
3
3
|
import subprocess
|
|
4
4
|
import sys
|
|
5
|
-
from pathlib import Path
|
|
6
5
|
|
|
7
6
|
import rich
|
|
8
7
|
import typer
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
)
|
|
9
|
+
from egse.system import redirect_output_to_log
|
|
10
|
+
|
|
11
|
+
daq6510 = typer.Typer(name="daq6510", help="DAQ6510 Data Acquisition Unit, Keithley, temperature monitoring")
|
|
13
12
|
|
|
14
13
|
|
|
15
14
|
@daq6510.command(name="start")
|
|
@@ -17,19 +16,39 @@ def start_daq6510():
|
|
|
17
16
|
"""Start the daq6510 service."""
|
|
18
17
|
rich.print("Starting service daq6510")
|
|
19
18
|
|
|
19
|
+
out = redirect_output_to_log("daq6510_cs.start.log")
|
|
20
|
+
|
|
21
|
+
subprocess.Popen(
|
|
22
|
+
[sys.executable, "-m", "egse.tempcontrol.keithley.daq6510_cs", "start"],
|
|
23
|
+
stdout=out,
|
|
24
|
+
stderr=out,
|
|
25
|
+
stdin=subprocess.DEVNULL,
|
|
26
|
+
close_fds=True,
|
|
27
|
+
)
|
|
28
|
+
|
|
20
29
|
|
|
21
30
|
@daq6510.command(name="stop")
|
|
22
31
|
def stop_daq6510():
|
|
23
32
|
"""Stop the daq6510 service."""
|
|
24
33
|
rich.print("Terminating service daq6510")
|
|
25
34
|
|
|
35
|
+
out = redirect_output_to_log("daq6510_cs.stop.log")
|
|
36
|
+
|
|
37
|
+
subprocess.Popen(
|
|
38
|
+
[sys.executable, "-m", "egse.tempcontrol.keithley.daq6510_cs", "stop"],
|
|
39
|
+
stdout=out,
|
|
40
|
+
stderr=out,
|
|
41
|
+
stdin=subprocess.DEVNULL,
|
|
42
|
+
close_fds=True,
|
|
43
|
+
)
|
|
44
|
+
|
|
26
45
|
|
|
27
46
|
@daq6510.command(name="status")
|
|
28
47
|
def status_daq6510():
|
|
29
48
|
"""Print status information on the daq6510 service."""
|
|
30
49
|
|
|
31
50
|
proc = subprocess.Popen(
|
|
32
|
-
[sys.executable, "-m", "egse.tempcontrol.keithley.
|
|
51
|
+
[sys.executable, "-m", "egse.tempcontrol.keithley.daq6510_cs", "status"],
|
|
33
52
|
stdout=subprocess.PIPE,
|
|
34
53
|
stderr=subprocess.PIPE,
|
|
35
54
|
stdin=subprocess.DEVNULL,
|
|
@@ -47,7 +66,7 @@ def start_daq6510_sim():
|
|
|
47
66
|
"""Start the DAQ6510 Simulator."""
|
|
48
67
|
rich.print("Starting service DAQ6510 Simulator")
|
|
49
68
|
|
|
50
|
-
out =
|
|
69
|
+
out = redirect_output_to_log("daq6510_sim.start.log")
|
|
51
70
|
|
|
52
71
|
subprocess.Popen(
|
|
53
72
|
[sys.executable, "-m", "egse.tempcontrol.keithley.daq6510_sim", "start"],
|
|
@@ -63,7 +82,7 @@ def stop_daq6510_sim():
|
|
|
63
82
|
"""Stop the DAQ6510 Simulator."""
|
|
64
83
|
rich.print("Terminating the DAQ6510 simulator.")
|
|
65
84
|
|
|
66
|
-
out =
|
|
85
|
+
out = redirect_output_to_log("daq6510_sim.stop.log")
|
|
67
86
|
|
|
68
87
|
subprocess.Popen(
|
|
69
88
|
[sys.executable, "-m", "egse.tempcontrol.keithley.daq6510_sim", "stop"],
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
egse/tempcontrol/keithley/__init__.py,sha256=QMm0vy6OMqzWmJZ1K6IwKSOpgYeCmUdbcRhv75LH9ZY,130
|
|
2
|
+
egse/tempcontrol/keithley/daq6510.py,sha256=pe12HIC2Yav5ZCGYecoQzhypYcCwecEaJpWZn-OHi8A,24867
|
|
3
|
+
egse/tempcontrol/keithley/daq6510.yaml,sha256=dHHVNyUpOQpdrZpnxPbT6slsl-8Gbnhifj4Q8QOfOYg,4400
|
|
4
|
+
egse/tempcontrol/keithley/daq6510_adev.py,sha256=Pp5U6a0FrUs1TFLqzvGQHhb3vfw6bhtAt4uX0OVRjBI,2263
|
|
5
|
+
egse/tempcontrol/keithley/daq6510_cs.py,sha256=Ga7z8S6z0oTxL_qQP8FXPaNKlJ6o9RrsPFOXeIJ2TT4,7700
|
|
6
|
+
egse/tempcontrol/keithley/daq6510_dev.py,sha256=EKFFDhP8-FdIPBPosc1DfzE3h4DQmNU1d0Fi7Yhx4I4,12906
|
|
7
|
+
egse/tempcontrol/keithley/daq6510_mon.py,sha256=Xbn2U-l9uxPwNN1-aYW72oJodL2sx13suCiPPbDSti0,20932
|
|
8
|
+
egse/tempcontrol/keithley/daq6510_protocol.py,sha256=v8FUrxEm7bnRzM_iQzW0mMCHTgAMZw4f2Ronl8fdKIE,2676
|
|
9
|
+
egse/tempcontrol/keithley/daq6510_sim.py,sha256=Ys5jroT-2i-V_qCR0L6yOYM9CiktnNbB3LvVLGwaD8A,9917
|
|
10
|
+
keithley_tempcontrol/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
keithley_tempcontrol/cgse_explore.py,sha256=y_FkFxJW0vdqGNp9yTU0ELBKxby74-ev3fTuf99Vl1s,400
|
|
12
|
+
keithley_tempcontrol/cgse_services.py,sha256=tndviv2rvygkNSsGy1oA43VfFpyVkdB9If-9sVlLbK4,2466
|
|
13
|
+
keithley_tempcontrol/settings.yaml,sha256=wbrgSZQAdqFl6AxiLJIN36UsdiVHQCzdsgi7Hs7dv7o,1467
|
|
14
|
+
keithley_tempcontrol-0.17.4.dist-info/METADATA,sha256=D8Uz0pSzzRUVMpVz05Uhy-4l_u21GF-eryb17PkqhZs,962
|
|
15
|
+
keithley_tempcontrol-0.17.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
16
|
+
keithley_tempcontrol-0.17.4.dist-info/entry_points.txt,sha256=_0j2BwcwPi4LlRrhvEWfp9GO9KT8WhCkJe2gFgMzOPs,491
|
|
17
|
+
keithley_tempcontrol-0.17.4.dist-info/RECORD,,
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
egse/tempcontrol/keithley/__init__.py,sha256=QMm0vy6OMqzWmJZ1K6IwKSOpgYeCmUdbcRhv75LH9ZY,130
|
|
2
|
-
egse/tempcontrol/keithley/daq6510.py,sha256=QA3K0xJ_-bsY6D4jzOyeDHz1uYSrgf8Z8O-9RynCB5E,24711
|
|
3
|
-
egse/tempcontrol/keithley/daq6510.yaml,sha256=dHHVNyUpOQpdrZpnxPbT6slsl-8Gbnhifj4Q8QOfOYg,4400
|
|
4
|
-
egse/tempcontrol/keithley/daq6510_adev.py,sha256=kodJGonsy_whr15XbVC5CA94mMqm1036FGK0DXmZtxo,3482
|
|
5
|
-
egse/tempcontrol/keithley/daq6510_cs.py,sha256=YVmUtEkGZ0ZKQ7BtvloL_da_fSy_rVr0EB1V2SjLdq0,6089
|
|
6
|
-
egse/tempcontrol/keithley/daq6510_dev.py,sha256=ZjzDU3x7XYRLRoNK9lcqE-t32XDMt0IHW26GO87xt98,10780
|
|
7
|
-
egse/tempcontrol/keithley/daq6510_mon.py,sha256=uox4IaHKwC_hXDQpj4sGEnZyK-dKXjqQdZ2yiaCbt_c,19103
|
|
8
|
-
egse/tempcontrol/keithley/daq6510_protocol.py,sha256=fdcJxsOFYaEDVIY4XzLVC4PSIAL20gKj8os8f0nsSh4,3655
|
|
9
|
-
egse/tempcontrol/keithley/daq6510_sim.py,sha256=GHC7NY2NZ5fm20AjMx7glSGWY1OOynndHUnCPcs5MZM,7568
|
|
10
|
-
keithley_tempcontrol/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
-
keithley_tempcontrol/cgse_explore.py,sha256=y_FkFxJW0vdqGNp9yTU0ELBKxby74-ev3fTuf99Vl1s,400
|
|
12
|
-
keithley_tempcontrol/cgse_services.py,sha256=H62jEzwRQMvKjkefFw9mSQTYRnT8ga_-SdJ26gPaUE8,1960
|
|
13
|
-
keithley_tempcontrol/settings.yaml,sha256=wbrgSZQAdqFl6AxiLJIN36UsdiVHQCzdsgi7Hs7dv7o,1467
|
|
14
|
-
keithley_tempcontrol-0.17.3.dist-info/METADATA,sha256=JhlNFJaYoCoFBeh8A3boUq3XSNPLxJs72_0NrW7BgcY,962
|
|
15
|
-
keithley_tempcontrol-0.17.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
16
|
-
keithley_tempcontrol-0.17.3.dist-info/entry_points.txt,sha256=_0j2BwcwPi4LlRrhvEWfp9GO9KT8WhCkJe2gFgMzOPs,491
|
|
17
|
-
keithley_tempcontrol-0.17.3.dist-info/RECORD,,
|
|
File without changes
|
{keithley_tempcontrol-0.17.3.dist-info → keithley_tempcontrol-0.17.4.dist-info}/entry_points.txt
RENAMED
|
File without changes
|