SCPI-Instrument-Control 1.0.1__py3-none-any.whl → 1.1.0__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.
- scpi_control/__init__.py +10 -1
- scpi_control/connection/mock.py +33 -1
- scpi_control/daq_models.py +316 -0
- scpi_control/daq_scpi_commands.py +227 -0
- scpi_control/data_logger.py +762 -0
- scpi_control/gui/daq_worker.py +205 -0
- scpi_control/gui/main_window.py +119 -1
- scpi_control/gui/widgets/__init__.py +14 -1
- scpi_control/gui/widgets/daq_ai_panel.py +392 -0
- scpi_control/gui/widgets/daq_channel_config.py +317 -0
- scpi_control/gui/widgets/daq_data_view.py +373 -0
- scpi_control/gui/widgets/daq_scan_config.py +279 -0
- scpi_control/gui/widgets/data_logger_control.py +364 -0
- scpi_control/report_generator/llm/__init__.py +14 -1
- scpi_control/report_generator/llm/daq_analyzer.py +316 -0
- scpi_control/report_generator/llm/daq_context_builder.py +340 -0
- scpi_control/report_generator/llm/daq_prompts.py +114 -0
- {scpi_instrument_control-1.0.1.dist-info → scpi_instrument_control-1.1.0.dist-info}/METADATA +1 -1
- {scpi_instrument_control-1.0.1.dist-info → scpi_instrument_control-1.1.0.dist-info}/RECORD +24 -11
- siglent/exceptions.py +40 -0
- {scpi_instrument_control-1.0.1.dist-info → scpi_instrument_control-1.1.0.dist-info}/WHEEL +0 -0
- {scpi_instrument_control-1.0.1.dist-info → scpi_instrument_control-1.1.0.dist-info}/entry_points.txt +0 -0
- {scpi_instrument_control-1.0.1.dist-info → scpi_instrument_control-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {scpi_instrument_control-1.0.1.dist-info → scpi_instrument_control-1.1.0.dist-info}/top_level.txt +0 -0
scpi_control/__init__.py
CHANGED
|
@@ -7,6 +7,7 @@ Supported equipment:
|
|
|
7
7
|
- Oscilloscopes (Siglent SDS series and SCPI-compatible models)
|
|
8
8
|
- Function Generators / AWGs (Siglent SDG series and SCPI-compatible models)
|
|
9
9
|
- Power Supplies (Siglent SPD series and SCPI-compatible models)
|
|
10
|
+
- Data Acquisition / Data Loggers (Keysight 34970A/DAQ970A and SCPI-compatible models)
|
|
10
11
|
|
|
11
12
|
For high-level automation and data collection, see the automation module:
|
|
12
13
|
from scpi_control.automation import DataCollector, TriggerWaitCollector
|
|
@@ -17,11 +18,14 @@ For power supply control:
|
|
|
17
18
|
For function generator control:
|
|
18
19
|
from scpi_control import FunctionGenerator
|
|
19
20
|
|
|
21
|
+
For data acquisition / data logging:
|
|
22
|
+
from scpi_control import DataLogger
|
|
23
|
+
|
|
20
24
|
For automated test report generation:
|
|
21
25
|
from scpi_control.report_generator import ReportGenerator
|
|
22
26
|
"""
|
|
23
27
|
|
|
24
|
-
__version__ = "1.0
|
|
28
|
+
__version__ = "1.1.0"
|
|
25
29
|
|
|
26
30
|
from scpi_control.exceptions import CommandError, SiglentConnectionError, SiglentError, SiglentTimeoutError
|
|
27
31
|
from scpi_control.oscilloscope import Oscilloscope
|
|
@@ -33,6 +37,9 @@ from scpi_control.psu_data_logger import PSUDataLogger, TimedPSULogger
|
|
|
33
37
|
# Function generator support (new in v0.6.0, stable in v1.0.0)
|
|
34
38
|
from scpi_control.function_generator import FunctionGenerator
|
|
35
39
|
|
|
40
|
+
# Data acquisition / Data logger support (new in v1.1.0)
|
|
41
|
+
from scpi_control.data_logger import DataLogger
|
|
42
|
+
|
|
36
43
|
__all__ = [
|
|
37
44
|
# Core features
|
|
38
45
|
"Oscilloscope",
|
|
@@ -46,4 +53,6 @@ __all__ = [
|
|
|
46
53
|
"TimedPSULogger",
|
|
47
54
|
# Function generator features
|
|
48
55
|
"FunctionGenerator",
|
|
56
|
+
# Data acquisition features
|
|
57
|
+
"DataLogger",
|
|
49
58
|
]
|
scpi_control/connection/mock.py
CHANGED
|
@@ -45,6 +45,10 @@ class MockConnection(BaseConnection):
|
|
|
45
45
|
awg_mode: bool = False,
|
|
46
46
|
awg_idn: str = "Siglent Technologies,SDG1032X,SDG1XXXXX,2.01.01.37R1",
|
|
47
47
|
awg_channels: Optional[Dict[int, Dict[str, any]]] = None,
|
|
48
|
+
# Data acquisition (DAQ) parameters
|
|
49
|
+
daq_mode: bool = False,
|
|
50
|
+
daq_idn: str = "Keysight Technologies,34970A,MY12345678,A.01.02",
|
|
51
|
+
daq_readings: str = "1.234,2.345,3.456",
|
|
48
52
|
):
|
|
49
53
|
super().__init__(host, port, timeout)
|
|
50
54
|
channels = channel_states.keys() if channel_states else range(1, 3)
|
|
@@ -112,6 +116,12 @@ class MockConnection(BaseConnection):
|
|
|
112
116
|
},
|
|
113
117
|
}
|
|
114
118
|
|
|
119
|
+
# Data acquisition (DAQ) mode
|
|
120
|
+
self.daq_mode = daq_mode
|
|
121
|
+
self.daq_idn = daq_idn
|
|
122
|
+
self.daq_readings = daq_readings
|
|
123
|
+
self.daq_scan_list = []
|
|
124
|
+
|
|
115
125
|
def connect(self) -> None:
|
|
116
126
|
"""Mark the connection as established."""
|
|
117
127
|
self._connected = True
|
|
@@ -365,13 +375,35 @@ class MockConnection(BaseConnection):
|
|
|
365
375
|
upper = command.upper()
|
|
366
376
|
|
|
367
377
|
if upper == "*IDN?":
|
|
368
|
-
if self.
|
|
378
|
+
if self.daq_mode:
|
|
379
|
+
return self.daq_idn
|
|
380
|
+
elif self.awg_mode:
|
|
369
381
|
return self.awg_idn
|
|
370
382
|
elif self.psu_mode:
|
|
371
383
|
return self.psu_idn
|
|
372
384
|
else:
|
|
373
385
|
return self.idn
|
|
374
386
|
|
|
387
|
+
# Data acquisition (DAQ) queries
|
|
388
|
+
if self.daq_mode:
|
|
389
|
+
# Return configured readings for any measurement/read/fetch query
|
|
390
|
+
if any(kw in upper for kw in ["READ?", "FETC?", "MEAS:", "R?"]):
|
|
391
|
+
return self.daq_readings
|
|
392
|
+
|
|
393
|
+
# Scan list query
|
|
394
|
+
if "ROUT:SCAN?" in upper:
|
|
395
|
+
if self.daq_scan_list:
|
|
396
|
+
return f"(@{','.join(str(ch) for ch in self.daq_scan_list)})"
|
|
397
|
+
return "(@)"
|
|
398
|
+
|
|
399
|
+
# Data points query
|
|
400
|
+
if "DATA:POIN?" in upper:
|
|
401
|
+
return str(len(self.daq_readings.split(",")))
|
|
402
|
+
|
|
403
|
+
# Error query
|
|
404
|
+
if "SYST:ERR?" in upper:
|
|
405
|
+
return '+0,"No error"'
|
|
406
|
+
|
|
375
407
|
# Function generator (AWG) queries
|
|
376
408
|
if self.awg_mode:
|
|
377
409
|
# Function queries: C1:BSWV? WVTP (Siglent) or SOUR1:FUNC? (generic)
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""Model capability definitions for SCPI-controlled Data Acquisition systems.
|
|
2
|
+
|
|
3
|
+
Supports generic SCPI-99 DAQ/Data Loggers and Keysight 34970A/DAQ970A series models.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import List, Optional
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MeasurementFunction(Enum):
|
|
16
|
+
"""Supported measurement functions for DAQ channels."""
|
|
17
|
+
|
|
18
|
+
VOLTAGE_DC = "VOLT:DC"
|
|
19
|
+
VOLTAGE_AC = "VOLT:AC"
|
|
20
|
+
CURRENT_DC = "CURR:DC"
|
|
21
|
+
CURRENT_AC = "CURR:AC"
|
|
22
|
+
RESISTANCE_2W = "RES" # 2-wire resistance
|
|
23
|
+
RESISTANCE_4W = "FRES" # 4-wire (Fresistance)
|
|
24
|
+
FREQUENCY = "FREQ"
|
|
25
|
+
PERIOD = "PER"
|
|
26
|
+
TEMPERATURE = "TEMP" # Thermocouple, RTD, thermistor
|
|
27
|
+
CONTINUITY = "CONT"
|
|
28
|
+
DIODE = "DIOD"
|
|
29
|
+
DIGITAL_INPUT = "DIG:INP"
|
|
30
|
+
DIGITAL_OUTPUT = "DIG:OUTP"
|
|
31
|
+
TOTALIZER = "TOT" # Counter/totalizer
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class ModuleSpec:
|
|
36
|
+
"""Specification for a DAQ plug-in module/card."""
|
|
37
|
+
|
|
38
|
+
slot: int # Slot number (1, 2, 3, etc.)
|
|
39
|
+
module_type: str # Module type identifier (e.g., "34901A", "DAQM901A")
|
|
40
|
+
num_channels: int # Number of channels on this module
|
|
41
|
+
channel_start: int # Starting channel number (e.g., 101 for slot 1)
|
|
42
|
+
supported_functions: List[MeasurementFunction] # Supported measurement types
|
|
43
|
+
max_voltage: float = 300.0 # Maximum input voltage
|
|
44
|
+
max_current: float = 1.0 # Maximum input current (if applicable)
|
|
45
|
+
has_switching: bool = True # Multiplexer/switching capability
|
|
46
|
+
description: str = "" # Human-readable description
|
|
47
|
+
|
|
48
|
+
def __str__(self) -> str:
|
|
49
|
+
"""String representation of module spec."""
|
|
50
|
+
return f"Slot {self.slot}: {self.module_type} ({self.num_channels}ch, {self.channel_start}-{self.channel_start + self.num_channels - 1})"
|
|
51
|
+
|
|
52
|
+
def get_channel_list(self) -> List[int]:
|
|
53
|
+
"""Get list of all channel numbers for this module."""
|
|
54
|
+
return list(range(self.channel_start, self.channel_start + self.num_channels))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class DAQCapability:
|
|
59
|
+
"""Defines capabilities and features for a specific DAQ model.
|
|
60
|
+
|
|
61
|
+
This dataclass contains all model-specific information including hardware
|
|
62
|
+
specifications and supported features.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
model_name: str # Full model name (e.g., "34970A", "DAQ970A")
|
|
66
|
+
manufacturer: str # Manufacturer name (e.g., "Keysight", "Agilent")
|
|
67
|
+
num_slots: int # Number of module slots
|
|
68
|
+
modules: List[ModuleSpec] = field(default_factory=list) # Installed modules
|
|
69
|
+
has_internal_dmm: bool = True # Built-in digital multimeter
|
|
70
|
+
dmm_resolution: float = 6.5 # DMM resolution in digits
|
|
71
|
+
max_sample_rate: float = 250.0 # Maximum scan rate (channels/second)
|
|
72
|
+
memory_readings: int = 50000 # Reading memory capacity
|
|
73
|
+
has_timestamp: bool = True # Timestamp capability
|
|
74
|
+
has_alarm: bool = True # Alarm/limit checking
|
|
75
|
+
has_math: bool = True # Math functions (mx+b scaling)
|
|
76
|
+
scpi_variant: str = "generic" # SCPI command variant
|
|
77
|
+
|
|
78
|
+
def __str__(self) -> str:
|
|
79
|
+
"""String representation of DAQ capability."""
|
|
80
|
+
return f"{self.manufacturer} {self.model_name} ({self.num_slots} slots, {self.scpi_variant})"
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def total_channels(self) -> int:
|
|
84
|
+
"""Get total number of channels across all modules."""
|
|
85
|
+
return sum(m.num_channels for m in self.modules)
|
|
86
|
+
|
|
87
|
+
def get_all_channels(self) -> List[int]:
|
|
88
|
+
"""Get list of all channel numbers across all modules."""
|
|
89
|
+
channels = []
|
|
90
|
+
for module in self.modules:
|
|
91
|
+
channels.extend(module.get_channel_list())
|
|
92
|
+
return sorted(channels)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# Common module types
|
|
96
|
+
MULTIPLEXER_20CH = lambda slot: ModuleSpec(
|
|
97
|
+
slot=slot,
|
|
98
|
+
module_type="34901A",
|
|
99
|
+
num_channels=20,
|
|
100
|
+
channel_start=slot * 100 + 1,
|
|
101
|
+
supported_functions=[
|
|
102
|
+
MeasurementFunction.VOLTAGE_DC,
|
|
103
|
+
MeasurementFunction.VOLTAGE_AC,
|
|
104
|
+
MeasurementFunction.CURRENT_DC,
|
|
105
|
+
MeasurementFunction.CURRENT_AC,
|
|
106
|
+
MeasurementFunction.RESISTANCE_2W,
|
|
107
|
+
MeasurementFunction.RESISTANCE_4W,
|
|
108
|
+
MeasurementFunction.FREQUENCY,
|
|
109
|
+
MeasurementFunction.PERIOD,
|
|
110
|
+
MeasurementFunction.TEMPERATURE,
|
|
111
|
+
],
|
|
112
|
+
max_voltage=300.0,
|
|
113
|
+
description="20-Channel Multiplexer (2/4-wire)",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
MULTIPLEXER_40CH = lambda slot: ModuleSpec(
|
|
117
|
+
slot=slot,
|
|
118
|
+
module_type="34902A",
|
|
119
|
+
num_channels=16,
|
|
120
|
+
channel_start=slot * 100 + 1,
|
|
121
|
+
supported_functions=[
|
|
122
|
+
MeasurementFunction.VOLTAGE_DC,
|
|
123
|
+
MeasurementFunction.VOLTAGE_AC,
|
|
124
|
+
MeasurementFunction.RESISTANCE_2W,
|
|
125
|
+
MeasurementFunction.FREQUENCY,
|
|
126
|
+
MeasurementFunction.PERIOD,
|
|
127
|
+
MeasurementFunction.TEMPERATURE,
|
|
128
|
+
],
|
|
129
|
+
max_voltage=300.0,
|
|
130
|
+
description="16-Channel Multiplexer (2-wire only)",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
DIGITAL_IO_MODULE = lambda slot: ModuleSpec(
|
|
134
|
+
slot=slot,
|
|
135
|
+
module_type="34903A",
|
|
136
|
+
num_channels=20,
|
|
137
|
+
channel_start=slot * 100 + 1,
|
|
138
|
+
supported_functions=[
|
|
139
|
+
MeasurementFunction.DIGITAL_INPUT,
|
|
140
|
+
MeasurementFunction.DIGITAL_OUTPUT,
|
|
141
|
+
],
|
|
142
|
+
has_switching=True,
|
|
143
|
+
description="20-Channel Actuator/GP Switch",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# DAQ Model Registry - Add new models here
|
|
148
|
+
DAQ_MODEL_REGISTRY = {
|
|
149
|
+
# Keysight/Agilent 34970A (classic model)
|
|
150
|
+
"34970A": DAQCapability(
|
|
151
|
+
model_name="34970A",
|
|
152
|
+
manufacturer="Keysight",
|
|
153
|
+
num_slots=3,
|
|
154
|
+
modules=[MULTIPLEXER_20CH(1)], # Default config - can be updated after connection
|
|
155
|
+
has_internal_dmm=True,
|
|
156
|
+
dmm_resolution=6.5,
|
|
157
|
+
max_sample_rate=250.0,
|
|
158
|
+
memory_readings=50000,
|
|
159
|
+
has_timestamp=True,
|
|
160
|
+
has_alarm=True,
|
|
161
|
+
has_math=True,
|
|
162
|
+
scpi_variant="keysight_daq",
|
|
163
|
+
),
|
|
164
|
+
# Keysight 34972A (USB enabled version)
|
|
165
|
+
"34972A": DAQCapability(
|
|
166
|
+
model_name="34972A",
|
|
167
|
+
manufacturer="Keysight",
|
|
168
|
+
num_slots=3,
|
|
169
|
+
modules=[MULTIPLEXER_20CH(1)],
|
|
170
|
+
has_internal_dmm=True,
|
|
171
|
+
dmm_resolution=6.5,
|
|
172
|
+
max_sample_rate=250.0,
|
|
173
|
+
memory_readings=50000,
|
|
174
|
+
has_timestamp=True,
|
|
175
|
+
has_alarm=True,
|
|
176
|
+
has_math=True,
|
|
177
|
+
scpi_variant="keysight_daq",
|
|
178
|
+
),
|
|
179
|
+
# Keysight DAQ970A (modern replacement)
|
|
180
|
+
"DAQ970A": DAQCapability(
|
|
181
|
+
model_name="DAQ970A",
|
|
182
|
+
manufacturer="Keysight",
|
|
183
|
+
num_slots=3,
|
|
184
|
+
modules=[MULTIPLEXER_20CH(1)],
|
|
185
|
+
has_internal_dmm=True,
|
|
186
|
+
dmm_resolution=6.5,
|
|
187
|
+
max_sample_rate=450.0, # Faster scanning
|
|
188
|
+
memory_readings=500000, # More memory
|
|
189
|
+
has_timestamp=True,
|
|
190
|
+
has_alarm=True,
|
|
191
|
+
has_math=True,
|
|
192
|
+
scpi_variant="keysight_daq",
|
|
193
|
+
),
|
|
194
|
+
# Keysight DAQ973A (no internal DMM)
|
|
195
|
+
"DAQ973A": DAQCapability(
|
|
196
|
+
model_name="DAQ973A",
|
|
197
|
+
manufacturer="Keysight",
|
|
198
|
+
num_slots=3,
|
|
199
|
+
modules=[],
|
|
200
|
+
has_internal_dmm=False,
|
|
201
|
+
dmm_resolution=0,
|
|
202
|
+
max_sample_rate=450.0,
|
|
203
|
+
memory_readings=500000,
|
|
204
|
+
has_timestamp=True,
|
|
205
|
+
has_alarm=True,
|
|
206
|
+
has_math=False,
|
|
207
|
+
scpi_variant="keysight_daq",
|
|
208
|
+
),
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def detect_daq_from_idn(idn_string: str) -> DAQCapability:
|
|
213
|
+
"""Detect DAQ model and return its capability profile.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
idn_string: The response from *IDN? command
|
|
217
|
+
Format: "Manufacturer,Model,Serial,Firmware"
|
|
218
|
+
Example: "Keysight Technologies,34970A,MY12345678,A.01.02"
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
DAQCapability object for the detected model
|
|
222
|
+
|
|
223
|
+
Note:
|
|
224
|
+
Unknown models will receive a generic SCPI capability profile
|
|
225
|
+
to enable basic control functionality.
|
|
226
|
+
"""
|
|
227
|
+
parts = idn_string.split(",")
|
|
228
|
+
if len(parts) < 2:
|
|
229
|
+
logger.warning(f"Invalid *IDN? response format: {idn_string}")
|
|
230
|
+
return create_generic_daq_capability(idn_string)
|
|
231
|
+
|
|
232
|
+
manufacturer = parts[0].strip()
|
|
233
|
+
model_from_idn = parts[1].strip()
|
|
234
|
+
logger.info(f"Detecting DAQ model from IDN: {manufacturer}, {model_from_idn}")
|
|
235
|
+
|
|
236
|
+
# Try exact match first
|
|
237
|
+
if model_from_idn in DAQ_MODEL_REGISTRY:
|
|
238
|
+
logger.info(f"Exact match found: {model_from_idn}")
|
|
239
|
+
return DAQ_MODEL_REGISTRY[model_from_idn]
|
|
240
|
+
|
|
241
|
+
# Try fuzzy matching
|
|
242
|
+
normalized_model = re.sub(r"[\s\-_]", "", model_from_idn).upper()
|
|
243
|
+
|
|
244
|
+
for registered_model, capability in DAQ_MODEL_REGISTRY.items():
|
|
245
|
+
normalized_registered = re.sub(r"[\s\-_]", "", registered_model).upper()
|
|
246
|
+
if normalized_model == normalized_registered:
|
|
247
|
+
logger.info(f"Fuzzy match found: {model_from_idn} -> {registered_model}")
|
|
248
|
+
return capability
|
|
249
|
+
|
|
250
|
+
# Try partial matching for Keysight/Agilent models
|
|
251
|
+
if any(mfr in manufacturer for mfr in ["Keysight", "Agilent", "HP"]):
|
|
252
|
+
for registered_model, capability in DAQ_MODEL_REGISTRY.items():
|
|
253
|
+
if registered_model in model_from_idn:
|
|
254
|
+
logger.info(f"Partial match found: {model_from_idn} -> {registered_model}")
|
|
255
|
+
return capability
|
|
256
|
+
|
|
257
|
+
# Model not found - create generic fallback
|
|
258
|
+
logger.warning(f"Model '{model_from_idn}' not in registry, using generic SCPI profile")
|
|
259
|
+
return create_generic_daq_capability(idn_string)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def create_generic_daq_capability(idn_string: str) -> DAQCapability:
|
|
263
|
+
"""Create generic SCPI capability for unknown DAQ.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
idn_string: The *IDN? response string
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Generic DAQCapability with conservative defaults
|
|
270
|
+
"""
|
|
271
|
+
parts = idn_string.split(",")
|
|
272
|
+
manufacturer = parts[0].strip() if len(parts) > 0 else "Unknown"
|
|
273
|
+
model = parts[1].strip() if len(parts) > 1 else "Generic DAQ"
|
|
274
|
+
|
|
275
|
+
logger.info(f"Creating generic DAQ capability for {manufacturer} {model}")
|
|
276
|
+
|
|
277
|
+
generic_capability = DAQCapability(
|
|
278
|
+
model_name=model,
|
|
279
|
+
manufacturer=manufacturer,
|
|
280
|
+
num_slots=1,
|
|
281
|
+
modules=[
|
|
282
|
+
ModuleSpec(
|
|
283
|
+
slot=1,
|
|
284
|
+
module_type="Generic",
|
|
285
|
+
num_channels=20,
|
|
286
|
+
channel_start=101,
|
|
287
|
+
supported_functions=[
|
|
288
|
+
MeasurementFunction.VOLTAGE_DC,
|
|
289
|
+
MeasurementFunction.VOLTAGE_AC,
|
|
290
|
+
MeasurementFunction.RESISTANCE_2W,
|
|
291
|
+
],
|
|
292
|
+
description="Generic Input Module",
|
|
293
|
+
)
|
|
294
|
+
],
|
|
295
|
+
has_internal_dmm=True,
|
|
296
|
+
dmm_resolution=5.5,
|
|
297
|
+
max_sample_rate=100.0,
|
|
298
|
+
memory_readings=10000,
|
|
299
|
+
has_timestamp=True,
|
|
300
|
+
has_alarm=False,
|
|
301
|
+
has_math=False,
|
|
302
|
+
scpi_variant="generic",
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
logger.info(f"Created generic capability: {generic_capability}")
|
|
306
|
+
return generic_capability
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def list_supported_models() -> List[str]:
|
|
310
|
+
"""Get list of all explicitly supported DAQ model names."""
|
|
311
|
+
return sorted(DAQ_MODEL_REGISTRY.keys())
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def get_models_by_manufacturer(manufacturer: str) -> List[DAQCapability]:
|
|
315
|
+
"""Get all models from a specific manufacturer."""
|
|
316
|
+
return [cap for cap in DAQ_MODEL_REGISTRY.values() if cap.manufacturer.lower() == manufacturer.lower()]
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""SCPI command sets for Data Acquisition system control.
|
|
2
|
+
|
|
3
|
+
Provides generic SCPI-99 commands for DAQ systems,
|
|
4
|
+
with Keysight-specific command overrides for 34970A/DAQ970A series.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Dict, List, Union
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def format_channel_list(channels: Union[int, List[int], str]) -> str:
|
|
14
|
+
"""Format channel list for SCPI commands.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
channels: Channel number(s) - can be:
|
|
18
|
+
- Single int: 101
|
|
19
|
+
- List of ints: [101, 102, 103]
|
|
20
|
+
- String (already formatted): "(@101:103)"
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Formatted channel string like "(@101,102,103)" or "(@101:110)"
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
>>> format_channel_list(101)
|
|
27
|
+
'(@101)'
|
|
28
|
+
>>> format_channel_list([101, 102, 103])
|
|
29
|
+
'(@101,102,103)'
|
|
30
|
+
>>> format_channel_list("(@101:110)")
|
|
31
|
+
'(@101:110)'
|
|
32
|
+
"""
|
|
33
|
+
if isinstance(channels, str):
|
|
34
|
+
# Already formatted
|
|
35
|
+
if channels.startswith("(@"):
|
|
36
|
+
return channels
|
|
37
|
+
return f"(@{channels})"
|
|
38
|
+
elif isinstance(channels, int):
|
|
39
|
+
return f"(@{channels})"
|
|
40
|
+
elif isinstance(channels, (list, tuple)):
|
|
41
|
+
return f"(@{','.join(str(ch) for ch in channels)})"
|
|
42
|
+
else:
|
|
43
|
+
raise ValueError(f"Invalid channel format: {channels}")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class DAQSCPICommandSet:
|
|
47
|
+
"""SCPI command abstraction for Data Acquisition systems.
|
|
48
|
+
|
|
49
|
+
Supports generic SCPI-99 commands with model-specific overrides for
|
|
50
|
+
known manufacturers (e.g., Keysight 34970A/DAQ970A series).
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
# Generic SCPI-99 commands (standard compliant)
|
|
54
|
+
GENERIC_COMMANDS: Dict[str, str] = {
|
|
55
|
+
# System commands (IEEE 488.2)
|
|
56
|
+
"identify": "*IDN?",
|
|
57
|
+
"reset": "*RST",
|
|
58
|
+
"clear_status": "*CLS",
|
|
59
|
+
"get_error": "SYST:ERR?",
|
|
60
|
+
"operation_complete": "*OPC?",
|
|
61
|
+
# Configuration commands
|
|
62
|
+
"configure_voltage_dc": "CONF:VOLT:DC {range},{resolution},{channels}",
|
|
63
|
+
"configure_voltage_ac": "CONF:VOLT:AC {range},{resolution},{channels}",
|
|
64
|
+
"configure_current_dc": "CONF:CURR:DC {range},{resolution},{channels}",
|
|
65
|
+
"configure_current_ac": "CONF:CURR:AC {range},{resolution},{channels}",
|
|
66
|
+
"configure_resistance_2w": "CONF:RES {range},{resolution},{channels}",
|
|
67
|
+
"configure_resistance_4w": "CONF:FRES {range},{resolution},{channels}",
|
|
68
|
+
"configure_frequency": "CONF:FREQ {range},{resolution},{channels}",
|
|
69
|
+
"configure_period": "CONF:PER {range},{resolution},{channels}",
|
|
70
|
+
"configure_temperature": "CONF:TEMP {sensor_type},{channels}",
|
|
71
|
+
# Scan list management
|
|
72
|
+
"set_scan_list": "ROUT:SCAN {channels}",
|
|
73
|
+
"get_scan_list": "ROUT:SCAN?",
|
|
74
|
+
"clear_scan_list": "ROUT:SCAN (@)",
|
|
75
|
+
# Channel delay
|
|
76
|
+
"set_channel_delay": "ROUT:CHAN:DEL {delay},{channels}",
|
|
77
|
+
"get_channel_delay": "ROUT:CHAN:DEL? {channels}",
|
|
78
|
+
# Trigger control
|
|
79
|
+
"set_trigger_source": "TRIG:SOUR {source}",
|
|
80
|
+
"get_trigger_source": "TRIG:SOUR?",
|
|
81
|
+
"set_trigger_count": "TRIG:COUN {count}",
|
|
82
|
+
"get_trigger_count": "TRIG:COUN?",
|
|
83
|
+
"set_trigger_delay": "TRIG:DEL {delay}",
|
|
84
|
+
"get_trigger_delay": "TRIG:DEL?",
|
|
85
|
+
"set_trigger_timer": "TRIG:TIM {interval}",
|
|
86
|
+
"get_trigger_timer": "TRIG:TIM?",
|
|
87
|
+
# Acquisition control
|
|
88
|
+
"initiate": "INIT",
|
|
89
|
+
"abort": "ABOR",
|
|
90
|
+
"trigger": "*TRG",
|
|
91
|
+
# Data retrieval
|
|
92
|
+
"read": "READ?",
|
|
93
|
+
"fetch": "FETC?",
|
|
94
|
+
"read_remove": "R? {max_readings}",
|
|
95
|
+
"get_data_points": "DATA:POIN?",
|
|
96
|
+
"clear_data": "DATA:DEL NVMEM",
|
|
97
|
+
# Measurement commands (immediate)
|
|
98
|
+
"measure_voltage_dc": "MEAS:VOLT:DC? {range},{resolution},{channels}",
|
|
99
|
+
"measure_voltage_ac": "MEAS:VOLT:AC? {range},{resolution},{channels}",
|
|
100
|
+
"measure_current_dc": "MEAS:CURR:DC? {range},{resolution},{channels}",
|
|
101
|
+
"measure_current_ac": "MEAS:CURR:AC? {range},{resolution},{channels}",
|
|
102
|
+
"measure_resistance_2w": "MEAS:RES? {range},{resolution},{channels}",
|
|
103
|
+
"measure_resistance_4w": "MEAS:FRES? {range},{resolution},{channels}",
|
|
104
|
+
"measure_frequency": "MEAS:FREQ? {range},{resolution},{channels}",
|
|
105
|
+
"measure_temperature": "MEAS:TEMP? {sensor_type},{channels}",
|
|
106
|
+
# Status
|
|
107
|
+
"get_scan_state": "ROUT:SCAN:STAT?",
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# Keysight DAQ series command overrides (34970A, DAQ970A, etc.)
|
|
111
|
+
KEYSIGHT_DAQ_OVERRIDES: Dict[str, str] = {
|
|
112
|
+
# Configuration with AUTO range/resolution support
|
|
113
|
+
"configure_voltage_dc": "CONF:VOLT:DC {range},{resolution},{channels}",
|
|
114
|
+
"configure_voltage_ac": "CONF:VOLT:AC {range},{resolution},{channels}",
|
|
115
|
+
"configure_current_dc": "CONF:CURR:DC {range},{resolution},{channels}",
|
|
116
|
+
"configure_current_ac": "CONF:CURR:AC {range},{resolution},{channels}",
|
|
117
|
+
"configure_resistance_2w": "CONF:RES {range},{resolution},{channels}",
|
|
118
|
+
"configure_resistance_4w": "CONF:FRES {range},{resolution},{channels}",
|
|
119
|
+
# Temperature with sensor type
|
|
120
|
+
"configure_temperature_tc": "CONF:TEMP TC,{tc_type},{channels}",
|
|
121
|
+
"configure_temperature_rtd": "CONF:TEMP RTD,{rtd_type},{channels}",
|
|
122
|
+
"configure_temperature_therm": "CONF:TEMP THER,{therm_type},{channels}",
|
|
123
|
+
# Sample count per trigger
|
|
124
|
+
"set_sample_count": "SAMP:COUN {count}",
|
|
125
|
+
"get_sample_count": "SAMP:COUN?",
|
|
126
|
+
# Advanced trigger modes
|
|
127
|
+
"set_trigger_source_immediate": "TRIG:SOUR IMM",
|
|
128
|
+
"set_trigger_source_timer": "TRIG:SOUR TIM",
|
|
129
|
+
"set_trigger_source_bus": "TRIG:SOUR BUS",
|
|
130
|
+
"set_trigger_source_external": "TRIG:SOUR EXT",
|
|
131
|
+
# Data format
|
|
132
|
+
"set_data_format": "FORM:READ:ALAR {state}", # Include alarm info
|
|
133
|
+
"set_data_timestamp": "FORM:READ:TIME {state}", # Include timestamp
|
|
134
|
+
"set_data_channel": "FORM:READ:CHAN {state}", # Include channel number
|
|
135
|
+
"set_data_unit": "FORM:READ:UNIT {state}", # Include units
|
|
136
|
+
# Alarm/limit configuration
|
|
137
|
+
"set_alarm_high": "CALC:LIM:UPP {limit},{channels}",
|
|
138
|
+
"get_alarm_high": "CALC:LIM:UPP? {channels}",
|
|
139
|
+
"set_alarm_low": "CALC:LIM:LOW {limit},{channels}",
|
|
140
|
+
"get_alarm_low": "CALC:LIM:LOW? {channels}",
|
|
141
|
+
"set_alarm_enable": "CALC:LIM:STAT {state},{channels}",
|
|
142
|
+
"get_alarm_enable": "CALC:LIM:STAT? {channels}",
|
|
143
|
+
# Scaling (mx+b)
|
|
144
|
+
"set_scaling_gain": "CALC:SCAL:GAIN {gain},{channels}",
|
|
145
|
+
"get_scaling_gain": "CALC:SCAL:GAIN? {channels}",
|
|
146
|
+
"set_scaling_offset": "CALC:SCAL:OFFS {offset},{channels}",
|
|
147
|
+
"get_scaling_offset": "CALC:SCAL:OFFS? {channels}",
|
|
148
|
+
"set_scaling_enable": "CALC:SCAL:STAT {state},{channels}",
|
|
149
|
+
"get_scaling_enable": "CALC:SCAL:STAT? {channels}",
|
|
150
|
+
# DMM digitize mode (fast acquisition)
|
|
151
|
+
"configure_digitize": "ACQ:VOLT:DC {range},{channels}",
|
|
152
|
+
"set_digitize_rate": "ACQ:SRAT {rate}",
|
|
153
|
+
"get_digitize_rate": "ACQ:SRAT?",
|
|
154
|
+
# Monitor a single channel continuously
|
|
155
|
+
"set_monitor_channel": "ROUT:MON {channel}",
|
|
156
|
+
"get_monitor_channel": "ROUT:MON?",
|
|
157
|
+
"set_monitor_enable": "ROUT:MON:STAT {state}",
|
|
158
|
+
"get_monitor_enable": "ROUT:MON:STAT?",
|
|
159
|
+
"read_monitor": "ROUT:MON:DATA?",
|
|
160
|
+
# Module identification
|
|
161
|
+
"get_module_info": "SYST:CTYP? {slot}",
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
def __init__(self, variant: str = "generic"):
|
|
165
|
+
"""Initialize SCPI command set with variant.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
variant: Command variant to use ("generic", "keysight_daq")
|
|
169
|
+
"""
|
|
170
|
+
self.variant = variant
|
|
171
|
+
logger.info(f"Initialized DAQ SCPI command set with variant: {variant}")
|
|
172
|
+
|
|
173
|
+
def get_command(self, command_name: str, **kwargs) -> str:
|
|
174
|
+
"""Get SCPI command string with parameter substitution.
|
|
175
|
+
|
|
176
|
+
Uses model-specific commands if available, falls back to generic.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
command_name: Name of the command (e.g., "configure_voltage_dc")
|
|
180
|
+
**kwargs: Parameters for command template substitution
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Formatted SCPI command string
|
|
184
|
+
|
|
185
|
+
Raises:
|
|
186
|
+
KeyError: If command_name is not found
|
|
187
|
+
ValueError: If required parameters are missing
|
|
188
|
+
|
|
189
|
+
Example:
|
|
190
|
+
>>> cmd_set = DAQSCPICommandSet("keysight_daq")
|
|
191
|
+
>>> cmd = cmd_set.get_command("configure_voltage_dc",
|
|
192
|
+
... range="AUTO", resolution="AUTO",
|
|
193
|
+
... channels="(@101:103)")
|
|
194
|
+
>>> print(cmd)
|
|
195
|
+
'CONF:VOLT:DC AUTO,AUTO,(@101:103)'
|
|
196
|
+
"""
|
|
197
|
+
# Try model-specific commands first
|
|
198
|
+
if self.variant == "keysight_daq":
|
|
199
|
+
if command_name in self.KEYSIGHT_DAQ_OVERRIDES:
|
|
200
|
+
template = self.KEYSIGHT_DAQ_OVERRIDES[command_name]
|
|
201
|
+
try:
|
|
202
|
+
return template.format(**kwargs)
|
|
203
|
+
except KeyError as e:
|
|
204
|
+
raise ValueError(f"Missing required parameter for command '{command_name}': {e}")
|
|
205
|
+
|
|
206
|
+
# Fall back to generic SCPI commands
|
|
207
|
+
if command_name in self.GENERIC_COMMANDS:
|
|
208
|
+
template = self.GENERIC_COMMANDS[command_name]
|
|
209
|
+
try:
|
|
210
|
+
return template.format(**kwargs)
|
|
211
|
+
except KeyError as e:
|
|
212
|
+
raise ValueError(f"Missing required parameter for command '{command_name}': {e}")
|
|
213
|
+
|
|
214
|
+
raise KeyError(f"Unknown command: '{command_name}' for variant '{self.variant}'")
|
|
215
|
+
|
|
216
|
+
def supports_command(self, command_name: str) -> bool:
|
|
217
|
+
"""Check if a command is supported by this variant."""
|
|
218
|
+
if self.variant == "keysight_daq" and command_name in self.KEYSIGHT_DAQ_OVERRIDES:
|
|
219
|
+
return True
|
|
220
|
+
return command_name in self.GENERIC_COMMANDS
|
|
221
|
+
|
|
222
|
+
def list_commands(self) -> list:
|
|
223
|
+
"""Get list of all available command names for this variant."""
|
|
224
|
+
commands = set(self.GENERIC_COMMANDS.keys())
|
|
225
|
+
if self.variant == "keysight_daq":
|
|
226
|
+
commands.update(self.KEYSIGHT_DAQ_OVERRIDES.keys())
|
|
227
|
+
return sorted(commands)
|