keithley-tempcontrol 2025.0.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,723 @@
1
+ import logging
2
+ import re
3
+ from pathlib import Path
4
+ from typing import Dict, Union
5
+ from typing import List
6
+ from typing import Tuple
7
+
8
+ from egse.decorators import dynamic_interface
9
+ from egse.device import DeviceConnectionState
10
+ from egse.device import DeviceInterface
11
+ from egse.mixin import DynamicCommandMixin, CommandType
12
+ from egse.mixin import add_lf
13
+ from egse.mixin import dynamic_command
14
+ from egse.proxy import Proxy
15
+ from egse.settings import Settings
16
+ from egse.tempcontrol.keithley.daq6510_devif import DAQ6510EthernetInterface
17
+ from egse.zmq_ser import connect_address
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ HERE = Path(__file__).parent
22
+
23
+ CTRL_SETTINGS = Settings.load("Keithley Control Server")
24
+ FW_SETTINGS = Settings.load("Keithley DAQ6510")
25
+ DEVICE_SETTINGS = Settings.load(location=HERE, filename="daq6510.yaml")
26
+
27
+
28
+ DEFAULT_BUFFER_1 = "defbuffer1"
29
+ DEFAULT_BUFFER_2 = "defbuffer2"
30
+
31
+
32
+ class DAQ6510Interface(DeviceInterface):
33
+ """
34
+ Interface definition for the Keithley DAQ6510 Controller, Proxy, and Simulator.
35
+ """
36
+
37
+ @dynamic_interface
38
+ def send_command(self, command: str, response: bool) -> Union[None, str]:
39
+ """ Sends the given SCPI command to the device.
40
+
41
+ The valid commands are described in the DAQ6510 Reference Manual [DAQ6510-901-01 Rev. B / September 2019].
42
+
43
+ Args:
44
+ command (str): SCPI command as specified in the DAQ6510 Reference Manual
45
+ response (bool): Indicates whether you expect a reply from the device
46
+
47
+ Returns: Response from the DAQ6510 is returned when a response was expected. When `response` is False, None
48
+ will be returned.
49
+ """
50
+
51
+ raise NotImplementedError
52
+
53
+ @dynamic_command(
54
+ cmd_type=CommandType.TRANSACTION,
55
+ cmd_string="*IDN?",
56
+ process_cmd_string=add_lf,
57
+ )
58
+ def info(self) -> str:
59
+ """ Returns basic information about the device, its name, firmware version, etc.
60
+
61
+ The string returned is subject to change without notice and can not be used for parsing information.
62
+
63
+ Returns: Identification string of the instrument.
64
+ """
65
+
66
+ raise NotImplementedError
67
+
68
+ @dynamic_command(
69
+ cmd_type=CommandType.WRITE,
70
+ cmd_string="*RST",
71
+ process_cmd_string=add_lf,
72
+ )
73
+ def reset(self) -> None:
74
+ """ Resets the DAQ6510.
75
+
76
+ This returns the instrument to default settings, and cancels all pending commands.
77
+
78
+ Note:
79
+ The `reset()` method also deletes all the user-defined buffers. The two default buffers are cleared.
80
+ """
81
+
82
+ raise NotImplementedError
83
+
84
+ @dynamic_command(cmd_type=CommandType.WRITE,
85
+ cmd_string=":SYST:TIME ${year}, ${month}, ${day}, ${hour}, ${minute}, ${second}")
86
+ def set_time(self, year: int, month: int, day: int, hour: int, minute: int, second: int) -> None:
87
+ """ Sets the absolute date and time for the device.
88
+
89
+ Args:
90
+ year (int): Year
91
+ month (int): Month
92
+ day (int): Day
93
+ hour (int): Hour
94
+ minute (int): Minute
95
+ second (int): Second
96
+ """
97
+
98
+ raise NotImplementedError
99
+
100
+ @dynamic_command(cmd_type=CommandType.TRANSACTION, cmd_string=":SYST:TIME? 1")
101
+ def get_time(self) -> str:
102
+ """ Gets the date and time from the device in UTC.
103
+
104
+ The returned string is of the format:
105
+
106
+ <weekday> <month> <day> <hour>:<minute>:<second> <year>
107
+
108
+ Returns: Data and time from the device in UTC.
109
+ """
110
+
111
+ raise NotImplementedError
112
+
113
+ @dynamic_interface
114
+ def read_buffer(self, start: int, end: int, buffer_name: str, elements: List[str]):
115
+ """ Reads specific data elements (measurements) from the given buffer.
116
+
117
+ Args:
118
+ start: (int) First index of the buffer that should be returned (>= 1)
119
+ end (int): Last index of the buffer that should be returned
120
+ buffer_name (str): Name of the buffer to read out
121
+ elements (List[str]): List of elements from the buffer to include in the response
122
+
123
+ Returns: List of all the readings.
124
+ """
125
+
126
+ raise NotImplementedError
127
+
128
+ @dynamic_command(cmd_type=CommandType.TRANSACTION, cmd_string="TRAC:ACTUAL? ${buffer_name}")
129
+ def get_buffer_count(self, buffer_name: str = DEFAULT_BUFFER_1):
130
+ """ Returns the number of data points in the specified buffer.
131
+
132
+ Args:
133
+ buffer_name (str): Name of the buffer
134
+ """
135
+
136
+ raise NotImplementedError
137
+
138
+ @dynamic_command(cmd_type=CommandType.TRANSACTION, cmd_string="TRACE:POINTS? ${buffer_name}")
139
+ def get_buffer_capacity(self, buffer_name: str):
140
+ """ Returns the capacity of the specified buffer.
141
+
142
+ Args:
143
+ buffer_name (str): Name of the buffer
144
+ """
145
+
146
+ raise NotImplementedError
147
+
148
+ @dynamic_command(cmd_type=CommandType.WRITE, cmd_string="TRACE:DELETE ${buffer_name}")
149
+ def delete_buffer(self, buffer_name: str) -> None:
150
+ """ Deletes the specified buffer.
151
+
152
+ Args:
153
+ buffer_name (str): Name of the buffer
154
+ """
155
+
156
+ raise NotImplementedError
157
+
158
+ @dynamic_interface
159
+ def clear_buffer(self, buffer_name: str) -> None:
160
+ """ Clears the given buffer.
161
+
162
+ Args:
163
+ buffer_name (str): Name of the buffer
164
+ """
165
+
166
+ raise NotImplementedError
167
+
168
+ @dynamic_interface
169
+ def create_buffer(self, buffer_name: str, size: int) -> None:
170
+ """ Creates a reading buffer with the given name and size.
171
+
172
+ Args:
173
+ buffer_name (str): Name of the buffer
174
+ size (size): Maximum number of readings (size >= 10) [default: 1000]
175
+ """
176
+
177
+ raise NotImplementedError
178
+
179
+ @dynamic_interface
180
+ def configure_sensors(self, channel_list: str, *, sense: Dict[str, List[Tuple]]):
181
+ """ Configures the DAQ6510 to sense the specified channels.
182
+
183
+ Args:
184
+ channel_list (str): List of channels, as understood by the device
185
+ sense (Dict[str], List[Tuple]): Dictionary with all the information on the configuration of the channels
186
+ """
187
+
188
+ raise NotImplementedError
189
+
190
+ @dynamic_interface
191
+ def setup_measurements(self, *, buffer_name: str, channel_list: str):
192
+ """ Sets up the measurements for the given channel list.
193
+
194
+ Args:
195
+ buffer_name (str): Name of the buffer to use [default: defbuffer1]
196
+ channel_list (str): Channels to read out
197
+ """
198
+
199
+ raise NotImplementedError
200
+
201
+ @dynamic_interface
202
+ def perform_measurement(self, *, buffer_name: str, channel_list: str, count: int, interval: int) -> list:
203
+ """ Performs the actual measurements.
204
+
205
+ Args:
206
+ buffer_name (str): Name of the buffer
207
+ channel_list (str): List of channels, as understood by the device
208
+ count (int): Number of measurements to perform
209
+ interval (int): Interval between measurements [s]
210
+ """
211
+
212
+ raise NotImplementedError
213
+
214
+
215
+ class DAQ6510Controller(DAQ6510Interface, DynamicCommandMixin):
216
+ """
217
+ The DAQ6510 Controller allows to remotely control the Keithley Data Acquisition System
218
+ through an Ethernet interface.
219
+ """
220
+
221
+ def __init__(self, hostname: str = FW_SETTINGS.HOSTNAME, port: int = FW_SETTINGS.PORT):
222
+ """ Opens a TCP/IP socket connection with the Keithley DAQ6510 Hardware.
223
+
224
+ Args:
225
+ hostname (str): IP address or fully qualified hostname of the Hexapod hardware controller. The default is
226
+ defined in the ``settings.yaml`` configuration file.
227
+ port (int): IP port number to connect to, by default set in the ``settings.yaml`` configuration file.
228
+
229
+ Raises:
230
+ Error: when the connection could not be established for some reason.
231
+ """
232
+
233
+ super().__init__()
234
+
235
+ logger.debug(f"Initializing the DAQ6510 Controller with hostname={hostname} on port={port}")
236
+
237
+ self.daq = self.transport = DAQ6510EthernetInterface(hostname, port)
238
+
239
+ # We set the default buffer here, this can be changed with the `create_buffer()` method.
240
+
241
+ self.buffer_name = DEFAULT_BUFFER_1
242
+
243
+ def is_simulator(self) -> bool:
244
+ """ Indicates that the device is a real hardware controller
245
+
246
+ Returns: False.
247
+ """
248
+
249
+ return False
250
+
251
+ def connect(self) -> None:
252
+ """ Connects to the device controller."""
253
+
254
+ self.daq.connect()
255
+ self.notify_observers(DeviceConnectionState.DEVICE_CONNECTED)
256
+
257
+ def disconnect(self) -> None:
258
+ """ Disconnects from the device controller."""
259
+
260
+ self.daq.disconnect()
261
+ self.notify_observers(DeviceConnectionState.DEVICE_NOT_CONNECTED)
262
+
263
+ def reconnect(self) -> None:
264
+ """ Reconnects to the device controller."""
265
+
266
+ if self.is_connected():
267
+ self.disconnect()
268
+
269
+ self.connect()
270
+
271
+ def is_connected(self) -> bool:
272
+ """ Checks whether the device controller is connected.
273
+
274
+ Returns: True if the device controller is connected; False otherwise.
275
+ """
276
+
277
+ return self.daq.is_connected()
278
+
279
+ def send_command(self, command: str, response: bool) -> Union[None, str]:
280
+ """ Sends an SCPI command to the device.
281
+
282
+ The valid commands are described in the DAQ6510 Reference Manual [DAQ6510-901-01 Rev. B / September 2019].
283
+
284
+ Args:
285
+ command (str): SCPI command as specified in the DAQ6510 Reference Manual
286
+ response (bool): Indicates whether you expect a reply from the device
287
+
288
+ Returns: The response from the DAQ6510 is returned when a response was expected. When `response` is False,
289
+ None will be returned.
290
+ """
291
+
292
+ return self.daq.trans(command) if response else self.daq.write(command)
293
+
294
+ def read_buffer(self, start: int, end: int, buffer_name: str = DEFAULT_BUFFER_1, elements: List[str] = None):
295
+ """ Reads specific data elements (measurements) from the given buffer.
296
+
297
+ Elements that can be specified to read out:
298
+
299
+ - CHANNEL: Channel for which the data was acquired
300
+ - DATE: Date when the data point was measured
301
+ - READING: Actual reading of the measurement
302
+ - TSTAMP: Timestamp when the data point was measured
303
+ - UNIT: Unit of measure for the measurement
304
+ - STATUS: Status information associated with the measurement
305
+
306
+ Args:
307
+ start: (int) First index of the buffer that should be returned (>= 1)
308
+ end (int): Last index of the buffer that should be returned
309
+ buffer_name (str): Name of the buffer to read out
310
+ elements (List[str]): List of elements from the buffer to include in the response
311
+
312
+ Returns: List of all the readings.
313
+ """
314
+
315
+ if elements is None:
316
+ elements = ["READING"]
317
+ else:
318
+ elements = ", ".join(elements)
319
+
320
+ return self.daq.trans(f'TRACE:DATA? {start}, {end}, "{buffer_name}", {elements}')
321
+
322
+ def clear_buffer(self, buffer_name: str = DEFAULT_BUFFER_1) -> None:
323
+ """ Clears the given buffer.
324
+
325
+ Args:
326
+ buffer_name (str): Name of the buffer
327
+ """
328
+
329
+ response = self.daq.trans(f'TRACE:ACTUAL? "{buffer_name}"')
330
+
331
+ logger.info(f"Clearing buffer '{buffer_name}' containing {response} readings.")
332
+
333
+ self.daq.write(f'TRACE:CLEAR "{buffer_name}"')
334
+
335
+ def create_buffer(self, buffer_name: str, size: int = 1000) -> None:
336
+ """ Creates a reading buffer with the given name.
337
+
338
+ The name of the buffer must adhere to the following rules:
339
+
340
+ - A buffer with this name should not exist in the device yet. When the buffer does exist, the DAQ6510 will
341
+ show a dialogue on the front panel with error 1115 saying the command cannot take an existing buffer
342
+ name.
343
+ - Buffer names must start with an alphabetic character.
344
+ - The names cannot contain any periods nor the underscore (_) character.
345
+ - The name can be up to 31 characters long.
346
+
347
+ If the given size is 0, the instrument creates the largest reading buffer possible based on the available
348
+ memory when the buffer is created.
349
+
350
+ Args:
351
+ buffer_name (str): Name of the buffer
352
+ size (size): Maximum number of readings (size >= 10) [default: 1000]
353
+ """
354
+
355
+ self.daq.write(f'TRACE:MAKE "{buffer_name}", {size}')
356
+
357
+ self.buffer_name = buffer_name
358
+
359
+ # def reset(self) -> None:
360
+ #
361
+ # self.daq.write("SYSTem:BEEPer 500, 0.1; :*RST; :SYSTem:BEEPer 1000, 0.1\n")
362
+
363
+ def configure_sensors(self, channel_list: str, *, sense: Dict[str, List[Tuple]]) -> None:
364
+ """ Configures the different sensors in the `channel_list`.
365
+
366
+ Each sensor in the list will be configured according to the settings given in the `sense` dictionary.
367
+
368
+ The following code will configure channels 101 and 102 as 4-wire transducers of type PT100.
369
+
370
+ ```
371
+ channel_list = create_channel_list(101, 102)
372
+
373
+ sense = {
374
+ "TEMPERATURE": [
375
+ ("TRANSDUCER", "FRTD"),
376
+ ("RTD:FOUR", "PT100"),
377
+ ]
378
+ }
379
+
380
+ daq.configure_sensors(channel_list, sense=sense)
381
+ ```
382
+
383
+ The `sense` argument is a dictionary where the keys are function names like "TEMPERATURE" or "VOLTAGE:DC",
384
+ and the values are a list of settings for that function. The list of settings is a list of tuples with the
385
+ command and the value, e.g. "TRANSDUCER" is the settings command and "FRTD" is its value. The list of settings
386
+ will be sent to the device in the order that they take in the list.
387
+
388
+ Args:
389
+ channel_list (str): Channels to configure
390
+ sense: Dictionary with all the information on the configuration
391
+ """
392
+
393
+ if "TEMPERATURE" in sense:
394
+
395
+ # Allowed settings for TEMPERATURE:
396
+ #
397
+ # - TEMPERATURE:APERTURE (@<channelList>)
398
+ # - TEMPERATURE:AVERAGE:COUNT (@<channelList>)
399
+ # - TEMPERATURE:AVERAGE:STATE (@<channelList>)
400
+ # - TEMPERATURE:AVERAGE:TCONTROL (@<channelList>)
401
+ # - TEMPERATURE:AVERAGE:WINDOW (@<channelList>)
402
+ # - TEMPERATURE:AVERAGE:AZERO:STATE
403
+ # - TEMPERATURE:DELAY:AUTO
404
+ # - TEMPERATURE:DELAY:USER<N>
405
+ # - TEMPERATURE:LINE:SYNC
406
+ # - TEMPERATURE:NPLCYCLES
407
+ # - TEMPERATURE:OCOMPENSATED
408
+ # - TEMPERATURE:ODETECTOR
409
+ # - TEMPERATURE:RELATIVE
410
+ # - TEMPERATURE:RELATIVE:ACQUIRE
411
+ # - TEMPERATURE:RELATIVE:STATE
412
+ # - TEMPERATURE:RTD:ALPHA, BETA, DELTA (@<channelList>)
413
+ # - TEMPERATURE:RTD:ZERO, TWO, THREE, FOUR (@<channelList>)
414
+ # - TEMPERATURE:TCOUPLE:RJUNCTION:SIMULATED
415
+ # - TEMPERATURE:TCOUPLE:RJUNCTION:RSELECT
416
+ # - TEMPERATURE:TCOUPLE:TYPE
417
+ # - TEMPERATURE:THERMISTOR (@<channelList>)
418
+ # - TEMPERATURE:TRANSDUCER (@<channelList>)
419
+ # - TEMPERATURE:UNIT (@<channelList>)
420
+ #
421
+
422
+ # set the function to temperature
423
+
424
+ self.daq.write(f'SENSE:FUNCTION "TEMPERATURE", {channel_list}')
425
+
426
+ for cmd, value in sense["TEMPERATURE"]:
427
+ self.daq.write(f"SENSE:TEMPERATURE:{cmd} {value}, {channel_list}")
428
+
429
+ def setup_measurements(self, *, buffer_name: str = DEFAULT_BUFFER_1, channel_list: str) -> None:
430
+ """ Sets up the measurements for the given channel list.
431
+
432
+ Args:
433
+ buffer_name (str): Name of the buffer to use [default: defbuffer1]
434
+ channel_list (str): Channels to read out
435
+ """
436
+
437
+ self.daq.write(f'ROUTE:SCAN:BUFFER "{buffer_name}"')
438
+ self.daq.write(f"ROUTE:SCAN:CREATE {channel_list}")
439
+ self.daq.write(f"ROUTE:CHANNEL:OPEN {channel_list}")
440
+ _ = self.daq.trans(f"ROUTE:CHANNEL:STATE? {channel_list}")
441
+ self.daq.write("ROUTE:SCAN:START:STIMULUS NONE")
442
+
443
+ def perform_measurement(self, *, buffer_name: str = DEFAULT_BUFFER_1, channel_list: str, count: int = 1,
444
+ interval: int = 2,) -> list:
445
+ """ Performs the actual measurements.
446
+
447
+ This function will wait until all measurements have completed, so be careful with the arguments `count` and
448
+ `interval` as they will multiply into the number of seconds that you will have to wait for the response.
449
+
450
+ Args:
451
+ buffer_name (str): Name of the buffer
452
+ channel_list (str): List of channels, as understood by the device
453
+ count (int): Number of measurements to perform
454
+ interval (int): Interval between measurements [s]
455
+
456
+ Returns: List of readings.
457
+ """
458
+
459
+ # Set the number of times a scan is repeated
460
+
461
+ self.daq.write(f"ROUTE:SCAN:COUNT:SCAN {count}")
462
+ self.daq.write(f"ROUTE:SCAN:INTERVAL {interval}") # [seconds]
463
+
464
+ #
465
+ self.daq.write("INITIATE:IMMEDIATE")
466
+ self.daq.write("*WAI")
467
+
468
+ # Read out the buffer
469
+
470
+ logger.debug("Buffer count = ", self.get_buffer_count())
471
+
472
+ num_sensors = count_number_of_channels(channel_list)
473
+
474
+ readings = []
475
+
476
+ for idx in range(1, count * num_sensors + 1):
477
+ response = self.read_buffer(
478
+ idx, idx, buffer_name=buffer_name, elements=["CHANNEL", "TSTAMP", "READING", "UNIT"]
479
+ )
480
+ if response != "" and response != str(count * num_sensors):
481
+ if "\n" in response:
482
+ response = response.split("\n")
483
+ for i in range(len(response)):
484
+ readings.append(response[i].split(","))
485
+ else:
486
+ readings.append(response.split(","))
487
+ if len(readings[0]) < 4:
488
+ del readings[0]
489
+
490
+ return readings
491
+
492
+
493
+ class DAQ6510Simulator(DAQ6510Interface):
494
+ """
495
+ Simulator for the Keithley DAQ6510 system.
496
+ """
497
+
498
+ def read_buffer(self, start: int, end: int, buffer_name: str, elements: List[str]):
499
+ pass
500
+
501
+ def get_buffer_count(self, buffer_name: str = DEFAULT_BUFFER_1):
502
+ pass
503
+
504
+ def get_buffer_capacity(self, buffer_name: str):
505
+ pass
506
+
507
+ def delete_buffer(self, buffer_name: str):
508
+ pass
509
+
510
+ def clear_buffer(self, buffer_name: str):
511
+ pass
512
+
513
+ def create_buffer(self, buffer_name: str, size: int):
514
+ pass
515
+
516
+ def configure_sensors(self, channel_list: str, *, sense: Dict[str, List[Tuple]]):
517
+ pass
518
+
519
+ def setup_measurements(self, *, buffer_name: str, channel_list: str):
520
+ pass
521
+
522
+ def perform_measurement(self, *, buffer_name: str, channel_list: str, count: int, interval: int):
523
+ pass
524
+
525
+ def send_command(self, command: str, response: bool):
526
+ pass
527
+
528
+ def info(self) -> str:
529
+ pass
530
+
531
+ def reset(self):
532
+ pass
533
+
534
+ def is_simulator(self):
535
+ """ Indicates that the device is a simulator.
536
+
537
+ Returns: True.
538
+ """
539
+
540
+ return True
541
+
542
+ def connect(self):
543
+ pass
544
+
545
+ def disconnect(self):
546
+ pass
547
+
548
+ def reconnect(self):
549
+ pass
550
+
551
+ def is_connected(self):
552
+ pass
553
+
554
+
555
+ class DAQ6510Proxy(Proxy, DAQ6510Interface):
556
+ """
557
+ The DAQ6510Proxy class is used to connect to the Keithley Control Server and send commands
558
+ to the Keithley Hardware Controller remotely.
559
+ """
560
+
561
+ def __init__(
562
+ self,
563
+ protocol: str = CTRL_SETTINGS.PROTOCOL,
564
+ hostname: str = CTRL_SETTINGS.HOSTNAME,
565
+ port: int = CTRL_SETTINGS.COMMANDING_PORT,
566
+ timeout: int = CTRL_SETTINGS.TIMEOUT * 1000 # Timeout [ms]: > scan count * interval + (one scan duration)
567
+ ):
568
+ """ Initialisation of a DAQ6510Proxy.
569
+
570
+ Args:
571
+ protocol (str): Transport protocol [default is taken from settings file]
572
+ hostname (str): Location of the Control Server (IP address) [default is taken from settings file]
573
+ port (int): TCP port on which the Control Server is listening for commands [default is taken from settings
574
+ file]
575
+ timeout (int): Timeout by which to establish the connection [ms]
576
+ """
577
+
578
+ super().__init__(connect_address(protocol, hostname, port), timeout=timeout)
579
+
580
+
581
+ def create_channel_list(*args) -> str:
582
+ """ Createa a channel list that is understood by the SCPI commands of the DAQ6510.
583
+
584
+ Channel names contain both the slot number and the channel number. The slot number is the number of the slot where
585
+ the card is installed at the back of the device.
586
+
587
+ When addressing multiple individual channels, add each of them as a separate argument, e.g. to include channels 1,
588
+ 3, and 7 from slot 1, use the following command:
589
+
590
+ >>> create_channel_list(101, 103, 107)
591
+ '(@101, 103, 107)'
592
+
593
+ To designate a range of channels, only one argument should be given, i.e. a tuple containing two channels
594
+ representing the range. The following tuple `(101, 110)` will create the following response: `"(@101:110)"`. The
595
+ range is inclusive, so this will define a range of 10 channels in slot 1.
596
+
597
+ >>> create_channel_list((201, 205))
598
+ '(@201:205)'
599
+
600
+ See reference manual for the Keithley DAQ6510 [DAQ6510-901-01 Rev. B / September 2019], chapter 11: Introduction to
601
+ SCPI commands, SCPI command formatting, channel naming.
602
+
603
+ Args:
604
+ *args: Tuple or a list of channels
605
+
606
+ Returns: String containing the channel list as understood by the device.
607
+ """
608
+
609
+ if not args:
610
+ return ""
611
+
612
+ # If only one argument is given, I expect either a tuple defining a range or just one channel. When several
613
+ # arguments are given, I expect them all to be individual channels.
614
+
615
+ if len(args) == 1:
616
+
617
+ arg = args[0]
618
+ if isinstance(arg, tuple):
619
+ ch_list = f"(@{arg[0]}:{arg[1]})"
620
+ else:
621
+ ch_list = f"(@{arg})"
622
+
623
+ else:
624
+ ch_list = "(@" + ", ".join([str(arg) for arg in args]) + ")"
625
+
626
+ return ch_list
627
+
628
+
629
+ def count_number_of_channels(channel_list: str) -> int:
630
+ """ Given a proper channel list, this function counts the number of channels.
631
+
632
+ For ranges, it returns the actual number of channels that are included in the range.
633
+
634
+ >>> count_number_of_channels("(@1,2,3,4,5)")
635
+ 5
636
+ >>> count_number_of_channels("(@1, 3, 5)")
637
+ 3
638
+ >>> count_number_of_channels("(@2:7)")
639
+ 6
640
+
641
+ Args:
642
+ channel_list (str): Channel list as understood by the SCPI commands of DAQ6510
643
+
644
+ Returns: Number of channels in the list.
645
+ """
646
+
647
+ match = re.match(r"\(@(.*)\)", channel_list)
648
+ group = match.groups()[0]
649
+
650
+ parts = group.replace(" ", "").split(",")
651
+ count = 0
652
+ for part in parts:
653
+ if ":" in part:
654
+ split_part = part.split(":")
655
+ count += int(split_part[1]) - int(split_part[0]) + 1
656
+ else:
657
+ count += 1
658
+
659
+ return count
660
+
661
+
662
+ def get_channel_names(channel_list: str) -> List[str]:
663
+ """ Generates a list of channel names from a given channel list.
664
+
665
+ Args:
666
+ channel_list (str): Channel list as understood by the SCPI commands of DAQ6510
667
+
668
+ Returns: List of channel names.
669
+ """
670
+
671
+ match = re.match(r"\(@(.*)\)", channel_list)
672
+ group = match.groups()[0]
673
+
674
+ parts = group.replace(" ", "").split(",")
675
+ names = []
676
+ for part in parts:
677
+ if ":" in part:
678
+ split_part = part.split(":")
679
+ names.extend(str(ch) for ch in range(int(split_part[0]), int(split_part[1]) + 1))
680
+ else:
681
+ names.append(part)
682
+
683
+ return names
684
+
685
+
686
+ if __name__ == "__main__":
687
+
688
+ logging.basicConfig(level=20)
689
+
690
+ print(f'{get_channel_names("(@101:105)")=}')
691
+ print(f'{get_channel_names("(@101, 102, 103, 105)")=}')
692
+ # sys.exit(0)
693
+
694
+ daq = DAQ6510Controller()
695
+ daq.connect()
696
+ daq.reset()
697
+
698
+ print(daq.info())
699
+
700
+ buffer_capacity = daq.get_buffer_capacity()
701
+ print(f"buffer {DEFAULT_BUFFER_1} can still hold {buffer_capacity} readings")
702
+
703
+ buffer_count = daq.get_buffer_count()
704
+ print(f"buffer {DEFAULT_BUFFER_1} holds {buffer_count} readings")
705
+
706
+ channels = create_channel_list((101, 102))
707
+
708
+ print(channels)
709
+
710
+ sense_dict = {"TEMPERATURE": [("TRANSDUCER", "FRTD"), ("RTD:FOUR", "PT100"), ("UNIT", "KELVIN")]}
711
+
712
+ daq.configure_sensors(channels, sense=sense_dict)
713
+
714
+ daq.setup_measurements(channel_list=channels)
715
+
716
+ meas_response = daq.perform_measurement(channel_list=channels, count=5, interval=1)
717
+
718
+ print(meas_response)
719
+
720
+ buffer_count = daq.get_buffer_count()
721
+ print(f"buffer {DEFAULT_BUFFER_1} holds {buffer_count} readings")
722
+
723
+ daq.disconnect()