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 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.1"
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
  ]
@@ -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.awg_mode:
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)