keithley-tempcontrol 0.17.0__tar.gz → 0.17.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. {keithley_tempcontrol-0.17.0 → keithley_tempcontrol-0.17.2}/.gitignore +5 -0
  2. {keithley_tempcontrol-0.17.0 → keithley_tempcontrol-0.17.2}/PKG-INFO +1 -1
  3. {keithley_tempcontrol-0.17.0 → keithley_tempcontrol-0.17.2}/pyproject.toml +1 -1
  4. {keithley_tempcontrol-0.17.0 → keithley_tempcontrol-0.17.2}/src/egse/tempcontrol/keithley/daq6510_cs.py +3 -0
  5. {keithley_tempcontrol-0.17.0 → keithley_tempcontrol-0.17.2}/src/egse/tempcontrol/keithley/daq6510_dev.py +10 -8
  6. {keithley_tempcontrol-0.17.0 → keithley_tempcontrol-0.17.2}/src/egse/tempcontrol/keithley/daq6510_sim.py +115 -58
  7. {keithley_tempcontrol-0.17.0 → keithley_tempcontrol-0.17.2}/README.md +0 -0
  8. {keithley_tempcontrol-0.17.0 → keithley_tempcontrol-0.17.2}/justfile +0 -0
  9. {keithley_tempcontrol-0.17.0 → keithley_tempcontrol-0.17.2}/noxfile.py +0 -0
  10. {keithley_tempcontrol-0.17.0 → keithley_tempcontrol-0.17.2}/service_registry.db +0 -0
  11. {keithley_tempcontrol-0.17.0 → keithley_tempcontrol-0.17.2}/src/egse/tempcontrol/keithley/__init__.py +0 -0
  12. {keithley_tempcontrol-0.17.0 → keithley_tempcontrol-0.17.2}/src/egse/tempcontrol/keithley/daq6510.py +0 -0
  13. {keithley_tempcontrol-0.17.0 → keithley_tempcontrol-0.17.2}/src/egse/tempcontrol/keithley/daq6510.yaml +0 -0
  14. {keithley_tempcontrol-0.17.0 → keithley_tempcontrol-0.17.2}/src/egse/tempcontrol/keithley/daq6510_acs.py +0 -0
  15. {keithley_tempcontrol-0.17.0 → keithley_tempcontrol-0.17.2}/src/egse/tempcontrol/keithley/daq6510_adev.py +0 -0
  16. {keithley_tempcontrol-0.17.0 → keithley_tempcontrol-0.17.2}/src/egse/tempcontrol/keithley/daq6510_mon.py +0 -0
  17. {keithley_tempcontrol-0.17.0 → keithley_tempcontrol-0.17.2}/src/egse/tempcontrol/keithley/daq6510_protocol.py +0 -0
  18. {keithley_tempcontrol-0.17.0 → keithley_tempcontrol-0.17.2}/src/keithley_tempcontrol/__init__.py +0 -0
  19. {keithley_tempcontrol-0.17.0 → keithley_tempcontrol-0.17.2}/src/keithley_tempcontrol/cgse_explore.py +0 -0
  20. {keithley_tempcontrol-0.17.0 → keithley_tempcontrol-0.17.2}/src/keithley_tempcontrol/cgse_services.py +0 -0
  21. {keithley_tempcontrol-0.17.0 → keithley_tempcontrol-0.17.2}/src/keithley_tempcontrol/settings.yaml +0 -0
@@ -32,6 +32,11 @@ venv
32
32
 
33
33
  .idea
34
34
 
35
+ # VSCode IDE
36
+
37
+ .vscode
38
+ *.code-workspace
39
+
35
40
  # MKDOCS documentation site
36
41
 
37
42
  /site
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: keithley-tempcontrol
3
- Version: 0.17.0
3
+ Version: 0.17.2
4
4
  Summary: Keithley Temperature Control for CGSE
5
5
  Author: IvS KU Leuven
6
6
  Maintainer-email: Rik Huygen <rik.huygen@kuleuven.be>, Sara Regibo <sara.regibo@kuleuven.be>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "keithley-tempcontrol"
3
- version = "0.17.0"
3
+ version = "0.17.2"
4
4
  description = "Keithley Temperature Control for CGSE"
5
5
  authors = [
6
6
  {name = "IvS KU Leuven"}
@@ -166,6 +166,7 @@ def start():
166
166
 
167
167
  with remote_logging():
168
168
  from egse.env import setup_env
169
+
169
170
  setup_env()
170
171
 
171
172
  try:
@@ -198,6 +199,7 @@ def stop():
198
199
  multiprocessing.current_process().name = "daq6510_cs (stop)"
199
200
 
200
201
  from egse.env import setup_env
202
+
201
203
  setup_env()
202
204
 
203
205
  try:
@@ -217,6 +219,7 @@ def status():
217
219
  multiprocessing.current_process().name = "daq6510_cs (status)"
218
220
 
219
221
  from egse.env import setup_env
222
+
220
223
  setup_env()
221
224
 
222
225
  endpoint = get_endpoint(SERVICE_TYPE, PROTOCOL, HOSTNAME, COMMANDING_PORT)
@@ -23,6 +23,9 @@ DEV_HOST = dev_settings.get("HOSTNAME")
23
23
  DEV_PORT = dev_settings.get("PORT")
24
24
  READ_TIMEOUT = dev_settings.get("TIMEOUT") # [s], can be smaller than timeout (for DAQ6510Proxy) (e.g. 1s)
25
25
 
26
+ SEPARATOR = b"\n"
27
+ SEPARATOR_STR = SEPARATOR.decode()
28
+
26
29
 
27
30
  class DAQ6510Command(ClientServerCommand):
28
31
  def get_cmd_string(self, *args, **kwargs) -> str:
@@ -36,7 +39,7 @@ class DAQ6510Command(ClientServerCommand):
36
39
  """
37
40
 
38
41
  out = super().get_cmd_string(*args, **kwargs)
39
- return out + "\n"
42
+ return out + SEPARATOR_STR
40
43
 
41
44
 
42
45
  class DAQ6510(DeviceInterface, DeviceTransport):
@@ -107,6 +110,8 @@ class DAQ6510(DeviceInterface, DeviceTransport):
107
110
  logger.debug(f"Sending {cmd}...")
108
111
  self.write(cmd)
109
112
 
113
+ return responses
114
+
110
115
  def is_simulator(self) -> bool:
111
116
  return False
112
117
 
@@ -243,7 +248,7 @@ class DAQ6510(DeviceInterface, DeviceTransport):
243
248
  """
244
249
 
245
250
  try:
246
- command += "\n" if not command.endswith("\n") else ""
251
+ command += SEPARATOR_STR if not command.endswith(SEPARATOR_STR) else ""
247
252
 
248
253
  self._sock.sendall(command.encode())
249
254
 
@@ -258,7 +263,7 @@ class DAQ6510(DeviceInterface, DeviceTransport):
258
263
  raise DeviceConnectionError(DEVICE_NAME, msg)
259
264
  raise
260
265
 
261
- def trans(self, command: str) -> str:
266
+ def trans(self, command: str) -> bytes:
262
267
  """Sends a single command to the device controller and block until a response from the controller.
263
268
 
264
269
  This is seen as a transaction.
@@ -277,7 +282,7 @@ class DAQ6510(DeviceInterface, DeviceTransport):
277
282
  try:
278
283
  # Attempt to send the complete command
279
284
 
280
- command += "\n" if not command.endswith("\n") else ""
285
+ command += SEPARATOR_STR if not command.endswith(SEPARATOR_STR) else ""
281
286
 
282
287
  self._sock.sendall(command.encode())
283
288
 
@@ -323,10 +328,7 @@ class DAQ6510(DeviceInterface, DeviceTransport):
323
328
  break
324
329
  except socket.timeout:
325
330
  logger.warning(f"Socket timeout error for {self.hostname}:{self.port}")
326
- return b"\r\n"
327
- except TimeoutError as exc:
328
- logger.warning(f"Socket timeout error: {exc}")
329
- return b"\r\n"
331
+ return SEPARATOR
330
332
  finally:
331
333
  self._sock.settimeout(saved_timeout)
332
334
 
@@ -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 | None = None
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, MODEL DAQ6510, SIMULATOR"),
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}\n".encode()
83
- logger.debug(f"write: {response = }")
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
- Reads one command string from the socket, i.e. until a linefeed ('\n') is received.
90
-
91
- Returns:
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
- n_total = 0
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
- for _ in range(100):
101
- data = conn.recv(buf_size)
102
- n = len(data)
103
- n_total += n
104
- command_string += data
105
- # if data.endswith(b'\n'):
106
- if n < buf_size:
107
- break
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
- # This timeout is caught at the caller, where the timeout is set.
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
- # LOGGER.debug(f"{command_string=}")
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
- action and action()
127
- if error_msg:
128
- return error_msg
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
- return response if isinstance(response, str) else response()
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
- # LOGGER.debug(f"{match=}, {match.groups()}")
177
+ if VERBOSE_DEBUG:
178
+ logger.debug(f"{match=}, {match.groups()}")
136
179
  action, response = value
137
- # LOGGER.debug(f"{action=}, {response=}")
138
- action and action(*match.groups())
139
- return error_msg or (response if isinstance(response, str) or response is None else response())
140
- return f"ERROR: unknown command string: {command_string}"
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(2.0)
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(2.0)
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
- logger.info(f"{data = }")
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,7 +254,7 @@ def run_simulator():
196
254
  logger.info(f"{exc.__class__.__name__} caught: {exc.args}")
197
255
 
198
256
 
199
- def send_request(cmd: str, type_: str = "query"):
257
+ def send_request(cmd: str, cmd_type: str = "query") -> str | None:
200
258
  from egse.tempcontrol.keithley.daq6510_dev import DAQ6510
201
259
 
202
260
  response = None
@@ -204,12 +262,12 @@ def send_request(cmd: str, type_: str = "query"):
204
262
  daq_dev = DAQ6510(hostname="localhost", port=5025)
205
263
  daq_dev.connect()
206
264
 
207
- if type_.lower().strip() == "query":
265
+ if cmd_type.lower().strip() == "query":
208
266
  response = daq_dev.query(cmd)
209
- elif type_.lower().strip() == "write":
267
+ elif cmd_type.lower().strip() == "write":
210
268
  daq_dev.write(cmd)
211
269
  else:
212
- logger.info(f"Unknown type {type_} for send_request.")
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(type_: str, cmd: str):
238
- response = send_request(cmd, type_)
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()