keithley-tempcontrol 0.17.4__py3-none-any.whl → 0.18.1__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.
@@ -1,4 +1,2 @@
1
1
  class TemperatureControlError(Exception):
2
2
  """Temperature control error for Keithley temperature control systems."""
3
-
4
- pass
@@ -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.device import DeviceInterface
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.mixin import DynamicCommandMixin
15
- from egse.mixin import add_lf
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="TRAC:ACTUAL? ${buffer_name}")
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(cmd_type=CommandType.TRANSACTION, cmd_string="TRACE:POINTS? ${buffer_name}")
147
- def get_buffer_capacity(self, buffer_name: str):
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="TRACE:DELETE ${buffer_name}")
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(self, *, buffer_name: str, channel_list: str, count: int, interval: int) -> list:
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(self, start: int, end: int, buffer_name: str = DEFAULT_BUFFER_1, elements: list[str] = None):
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 (List[str]): List of elements from the buffer to include in the response
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
- elements = ["READING"]
362
+ elements_str = ["READING"]
325
363
  else:
326
- elements = ", ".join(elements)
364
+ elements_str = ", ".join(elements)
327
365
 
328
- return self.daq.trans(f'TRACE:DATA? {start}, {end}, "{buffer_name}", {elements}')
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 = ", self.get_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
- if response != "" and response != str(count * num_sensors):
494
- if "\n" in response:
495
- response = response.split("\n")
496
- for i in range(len(response)):
497
- readings.append(response[i].split(","))
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(response.split(","))
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 DAQ6510Simulator(DAQ6510Interface):
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
- """Initialisation of a DAQ6510Proxy.
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", 0)
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()