Python interfaces for controlling Fluke digital multimeters, Siglent arbitrary waveform generators, and Rigol power supplies via PyVISA.
This package provides high-level Python interfaces for instrument control:
- Fluke 45 and Fluke 8845A/8846A digital multimeters
- Siglent SDG2042X arbitrary waveform generator
- Rigol DP800 series programmable power supplies
All instruments support multiple connection methods (GPIB, USB, Ethernet, RS-232) and provide type-safe, well-documented APIs with comprehensive error handling.
- Python >= 3.13
- NI-VISA or PyVISA-py backend
- uv package manager (recommended)
uv pip install -e .pip install -e .For pint unit support with Siglent instruments:
uv pip install -e ".[units]"from inst_ctrl import Fluke45, Func, Rate
# Connect via GPIB
with Fluke45(gpib_address=3) as dmm:
dmm.primary_function = Func.VDC
dmm.rate = Rate.FAST
voltage = dmm.primary()
print(f"Voltage: {voltage} V")from inst_ctrl import Fluke88, Func
# Connect via Ethernet
with Fluke88(ip_address='192.168.1.100') as dmm:
dmm.primary_function = Func.OHMS
resistance = dmm.primary()
print(f"Resistance: {resistance} Ohms")from inst_ctrl import SiglentSDG2042X
# Auto-discover and connect
with SiglentSDG2042X() as sig_gen:
sig_gen.channel = 1
sig_gen.waveform_type = 'SINE'
sig_gen.frequency = 1000
sig_gen.amplitude = 1.0
sig_gen.output_state = Truefrom inst_ctrl import RigolDP800, Channel
# Connect via Ethernet
with RigolDP800(ip_address='192.168.1.100') as psu:
psu.channel = Channel.CH1
psu.voltage = 5.0
psu.current = 1.0
psu.output = TrueGPIB Connection:
dmm = Fluke45(gpib_address=3)
dmm.connect()Ethernet Connection (Fluke 88xx only):
dmm = Fluke88(ip_address='192.168.1.100')
dmm.connect()Serial Connection:
# Windows
dmm = Fluke45(serial_port='COM1')
# Linux/Mac
dmm = Fluke45(serial_port='/dev/ttyUSB0')
dmm.connect()Direct Resource Name:
dmm = Fluke45(resource_name='GPIB0::3::INSTR')
dmm.connect()Auto-Discovery:
# Automatically finds first Fluke instrument on GPIB bus
dmm = Fluke45()
dmm.connect()Auto-Discovery:
# Automatically finds first Siglent SDG instrument
sig_gen = SiglentSDG2042X()
sig_gen.connect()Direct Resource Name:
sig_gen = SiglentSDG2042X(resource_name='USB0::0x0483::0x7540::SDG2042X12345678::INSTR')
sig_gen.connect()Ethernet Connection:
psu = RigolDP800(ip_address='192.168.1.100')
psu.connect()USB Connection:
psu = RigolDP800(usb_serial='DP8C12345678')
psu.connect()GPIB Connection:
psu = RigolDP800(gpib_address=5)
psu.connect()Direct Resource Name:
psu = RigolDP800(resource_name='TCPIP0::192.168.1.100::INSTR')
psu.connect()Auto-Discovery:
# Automatically finds first Rigol DP800 instrument
psu = RigolDP800()
psu.connect()from inst_ctrl import Fluke45, Func
with Fluke45(gpib_address=3) as dmm:
dmm.primary_function = Func.VDC
dmm.auto_range = True
voltage = dmm.primary()
print(f"DC Voltage: {voltage} V")from inst_ctrl import Fluke45, Func, Func2
with Fluke45() as dmm:
dmm.primary_function = Func.VDC
dmm.secondary_function = Func2.FREQ
voltage, frequency = dmm.both()
print(f"Voltage: {voltage} V, Frequency: {frequency} Hz")from inst_ctrl import Fluke45, Func, Rate
with Fluke45() as dmm:
dmm.primary_function = Func.OHMS
dmm.auto_range = False
dmm.range = 3
dmm.rate = Rate.SLOW
resistance = dmm.primary()
print(f"Resistance: {resistance} Ohms")with Fluke45() as dmm:
dmm.primary_function = Func.VDC
dmm.relative_mode = True
dmm.set_relative_offset(1.0)
relative_voltage = dmm.primary()
print(f"Relative to 1.0V: {relative_voltage} V")with Fluke45() as dmm:
dmm.primary_function = Func.VDC
dmm.compare_mode = True
dmm.compare_hi = 10.0
dmm.compare_lo = 5.0
result = dmm.compare_result
print(f"Compare result: {result}") # 'HI', 'LO', or 'PASS'from inst_ctrl import Fluke45, TriggerMode
with Fluke45() as dmm:
dmm.trigger_mode = TriggerMode.EXTERNAL
dmm.trigger()
measurement = dmm.primary()from inst_ctrl import Fluke88, Func
with Fluke88(ip_address='192.168.1.100') as dmm:
dmm.primary_function = Func.VDC
dmm.auto_range = True
voltage = dmm.primary()
print(f"Voltage: {voltage} V")from inst_ctrl import Fluke88, Func, Rate
with Fluke88(gpib_address=1) as dmm:
dmm.primary_function = Func.OHMS
dmm.rate = Rate.FAST
resistance = dmm.primary()from inst_ctrl import SiglentSDG2042X
with SiglentSDG2042X() as sig_gen:
sig_gen.channel = 1
sig_gen.waveform_type = 'SINE'
sig_gen.frequency = 1000
sig_gen.amplitude = 2.0
sig_gen.offset = 0.5
sig_gen.output_state = Truewith SiglentSDG2042X() as sig_gen:
sig_gen.channel = 1
sig_gen.configure_waveform(
waveform_type='SINE',
frequency=1000,
amplitude=2.0,
offset=0.5,
phase=45
)
sig_gen.output_state = Truewith SiglentSDG2042X() as sig_gen:
sig_gen.channel = 1
sig_gen.waveform_type = 'SQUARE'
sig_gen.frequency = 10000
sig_gen.amplitude = 3.3
sig_gen.set_duty_cycle(25.0)
sig_gen.output_state = Truewith SiglentSDG2042X() as sig_gen:
sig_gen.channel = 1
sig_gen.waveform_type = 'PULSE'
sig_gen.frequency = 1000
sig_gen.amplitude = 5.0
sig_gen.set_pulse_width(1e-3)
sig_gen.set_rise_time(1e-6)
sig_gen.set_fall_time(1e-6)
sig_gen.output_state = Truewith SiglentSDG2042X() as sig_gen:
sig_gen.channel = 1
sig_gen.waveform_type = 'RAMP'
sig_gen.frequency = 500
sig_gen.amplitude = 2.0
sig_gen.set_symmetry(75.0)
sig_gen.output_state = Truefrom inst_ctrl import SiglentSDG2042X
sig_gen = SiglentSDG2042X(unit_mode='pint')
sig_gen.connect()
sig_gen.channel = 1
sig_gen.frequency = 1000
freq = sig_gen.frequency
print(f"Frequency: {freq}") # Prints as pint Quantitywith SiglentSDG2042X() as sig_gen:
sig_gen.channel = 1
sig_gen.load_impedance = 'HiZ'
sig_gen.load_impedance = 50with SiglentSDG2042X() as sig_gen:
waveforms = sig_gen.list_waveforms()
for wv in waveforms:
print(f"Index {wv['index']}: {wv['name']}")
sig_gen.channel = 1
sig_gen.waveform_type = 'ARB'
sig_gen.select_arbitrary_waveform(index=1)from inst_ctrl import RigolDP800, Channel
with RigolDP800(ip_address='192.168.1.100') as psu:
psu.channel = Channel.CH1
psu.voltage = 5.0
psu.current = 1.0
psu.output = Truewith RigolDP800() as psu:
psu.apply(voltage=12.0, current=2.0, channel=Channel.CH1)
psu.output_on(Channel.CH1)with RigolDP800() as psu:
psu.channel = Channel.CH1
voltage = psu.measured_voltage
current = psu.measured_current
power = psu.measured_power
print(f"V: {voltage}V, I: {current}A, P: {power}W")with RigolDP800() as psu:
voltage, current, power = psu.measure_all(Channel.CH1)
print(f"V: {voltage}V, I: {current}A, P: {power}W")with RigolDP800() as psu:
# Configure and enable CH1
psu.apply(voltage=5.0, current=1.0, channel=Channel.CH1)
psu.output_on(Channel.CH1)
# Configure and enable CH2
psu.apply(voltage=12.0, current=2.0, channel=Channel.CH2)
psu.output_on(Channel.CH2)with RigolDP800() as psu:
psu.channel = Channel.CH1
psu.set_ovp(15.0) # Set OVP to 15V
psu.enable_ovp(True) # Enable OVP
psu.set_ocp(2.5) # Set OCP to 2.5A
psu.enable_ocp(True) # Enable OCPwith RigolDP800() as psu:
voltage, current = psu.get_settings(Channel.CH1)
print(f"CH1 settings: {voltage}V, {current}A")Fluke45(gpib_address=None, serial_port=None, resource_name=None, timeout=5000)connect()- Establish connectiondisconnect()- Close connectioncheck_connection()- Verify communication
primary_function- Set/get primary measurement function (Func enum)secondary_function- Set/get secondary measurement function (Func2 enum, Fluke 45 only)primary()- Trigger and read primary measurementsecondary()- Trigger and read secondary measurement (Fluke 45 only)both()- Trigger and read both displays (Fluke 45 only)primary_value- Read current primary value without triggeringsecondary_value- Read current secondary value without triggering
auto_range- Enable/disable auto rangerange- Set manual range (1-7)rate- Set measurement rate (Rate enum: SLOW, MEDIUM, FAST)trigger_mode- Set trigger mode (TriggerMode enum)trigger()- Send trigger command
relative_mode- Enable/disable relative measurementset_relative_offset(offset)- Set relative offset valuedb_mode- Enable/disable dB measurementset_db_reference(impedance)- Set dB reference impedancehold_mode- Enable/disable hold modemin_max_mode(mode)- Set min/max tracking modecompare_mode- Enable/disable compare modecompare_hi- Set compare high limitcompare_lo- Set compare low limitcompare_result- Get compare result
reset()- Reset instrument to defaultsclear_status()- Clear status registersself_test()- Execute self-testread_status_byte()- Read status byteread_event_status()- Read event status register
Fluke88 inherits from Fluke45 and provides the same interface. Key differences:
- Supports Ethernet connections via
ip_addressparameter - Uses SCPI commands internally
- Does not support secondary display (Func2)
secondary()andboth()methods raise exceptions
SiglentSDG2042X(resource_name=None, timeout=5000, unit_mode='tuple')connect()- Establish connectiondisconnect()- Close connectioncheck_connection()- Verify communication
channel- Set active channel (1 or 2)
waveform_type- Set/get waveform type ('SINE', 'SQUARE', 'RAMP', 'PULSE', 'NOISE', 'ARB', 'DC', 'PRBS', 'IQ')frequency- Set/get frequencyamplitude- Set/get amplitudeoffset- Set/get DC offsetphase- Set/get phase
get_duty_cycle()/set_duty_cycle(duty)- For SQUARE/PULSE waveformsget_symmetry()/set_symmetry(symmetry)- For RAMP waveformsget_pulse_width()/set_pulse_width(width)- For PULSE waveformsget_rise_time()/set_rise_time(rise_time)- For PULSE waveformsget_fall_time()/set_fall_time(fall_time)- For PULSE waveforms
output_state- Enable/disable outputload_impedance- Set/get load impedance ('HiZ' or numeric value)
configure_waveform(waveform_type, frequency, amplitude, offset=0, phase=0)- Configure all parameters at oncelist_waveforms()- List available arbitrary waveformsselect_arbitrary_waveform(index=None, name=None)- Select arbitrary waveformget_all_parameters()- Get raw parameter string
limits- ParameterLimits object for validationlimits.freq_min,limits.freq_maxlimits.amp_min,limits.amp_maxlimits.offset_min,limits.offset_maxlimits.phase_min,limits.phase_maxlimits.reset_to_defaults()
RigolDP800(resource_name=None, ip_address=None, usb_serial=None, gpib_address=None, timeout=5000)connect()- Establish connectiondisconnect()- Close connectioncheck_connection()- Verify communication
channel- Set/get active channel (Channel enum, string, or int)
voltage- Set/get voltage setting for active channel (volts)current- Set/get current limit for active channel (amperes)apply(voltage, current, channel=None)- Set both voltage and current for specified channelget_settings(channel=None)- Query voltage and current settings (returns tuple)
measured_voltage- Measure actual output voltage for active channelmeasured_current- Measure actual output voltagemeasured_power- Measure actual output power (watts)power- Convenience alias for measured_powermeasure_all(channel=None)- Measure voltage, current, and power (returns tuple)
output- Set/get output state (True/False or 'ON'/'OFF')output_on(channel=None)- Enable output for specified channeloutput_off(channel=None)- Disable output for specified channel
set_ovp(value, channel=None)- Set over-voltage protection levelset_ocp(value, channel=None)- Set over-current protection levelenable_ovp(state=True, channel=None)- Enable/disable over-voltage protectionenable_ocp(state=True, channel=None)- Enable/disable over-current protection
reset()- Reset instrument to factory defaultsclear_status()- Clear status registersself_test()- Execute self-test (returns True if passed)
Func- Primary measurement functions: VDC, VAC, VACDC, ADC, AAC, OHMS, FREQ, DIODEFunc2- Secondary measurement functions (Fluke 45 only): VDC, VAC, ADC, AAC, OHMS, FREQ, DIODE, CLEARRate- Measurement rates: SLOW, MEDIUM, FASTTriggerMode- Trigger modes: INTERNAL, EXTERNAL, EXTERNAL_NO_DELAY, EXTERNAL_DELAY, EXTERNAL_REAR_NO_DELAY, EXTERNAL_REAR_DELAY
- 'SINE', 'SQUARE', 'RAMP', 'PULSE', 'NOISE', 'ARB', 'DC', 'PRBS', 'IQ'
Channel- Power supply channels: CH1, CH2, CH3
All instruments raise specific exception types:
FlukeError- Base exceptionFlukeConnectionError- Connection failuresFlukeValidationError- Invalid parameter valuesFlukeCommandError- Command execution failures
SiglentError- Base exceptionSiglentConnectionError- Connection failuresSiglentValidationError- Invalid parameter valuesSiglentCommandError- Command execution failures
RigolError- Base exceptionRigolConnectionError- Connection failuresRigolValidationError- Invalid parameter valuesRigolCommandError- Command execution failures
from inst_ctrl import Fluke45, FlukeConnectionError, FlukeValidationError
try:
dmm = Fluke45(gpib_address=3)
dmm.connect()
dmm.primary_function = 'INVALID'
except FlukeConnectionError as e:
print(f"Connection failed: {e}")
except FlukeValidationError as e:
print(f"Invalid parameter: {e}")All instruments support context manager syntax for automatic connection management:
from inst_ctrl import Fluke45, SiglentSDG2042X
# Automatically connects on entry, disconnects on exit
with Fluke45(gpib_address=3) as dmm:
voltage = dmm.primary()
with SiglentSDG2042X() as sig_gen:
sig_gen.frequency = 1000
sig_gen.output_state = True
with RigolDP800(ip_address='192.168.1.100') as psu:
psu.voltage = 5.0
psu.output = TrueAll functions are fully typed with type hints. The package uses strict typing practices:
- Functions must have complete type annotations
- Variables are typed only when ambiguous
- Type narrowing ensures safe instrument access after connection verification
- Python >= 3.13
- pyvisa >= 1.16.1
- NI-VISA or PyVISA-py backend
- pint >= 0.25.2 (optional, for unit support)
inst_ctrl/
├── src/
│ └── inst_ctrl/
│ ├── __init__.py
│ ├── fluke.py # Fluke 45 and 8845A/8846A interfaces
│ ├── siglent.py # Siglent SDG2042X interface
│ └── rigol.py # Rigol DP800 series power supply interface
├── pyproject.toml
└── README.md
uv build[Add your license information here]
[Add contribution guidelines here]