Keysight vs Rigol DMM: Switching SCPI Commands
Swap a Keysight 34461A for a Rigol DM3068 — or vice versa — without touching your test logic. Learn where the SCPI commands differ, what's compatible, and how to write a thin adapter layer that makes your scripts instrument-agnostic.
Contents
Why Commands Differ Between Manufacturers
SCPI (Standard Commands for Programmable Instruments) is a standard — but it defines a framework, not an exact vocabulary. Manufacturers fill in the gaps with their own command trees and parameter syntax. The result: two DMMs that both "speak SCPI" can require meaningfully different commands for the same measurement.
The Keysight 34461A and Rigol DM3068 are both 6½-digit DMMs popular in labs on a budget. The 34461A is the more expensive instrument with deeper SCPI coverage; the DM3068 is common as a cost-effective replacement. Swapping one for the other without a compatibility layer typically means editing dozens of command strings scattered through a test script.
Command Mapping Table
The table below covers the most common DMM operations. Commands marked identical work on both instruments without modification.
| Operation | Keysight 34461A | Rigol DM3068 | Compatible? |
|---|---|---|---|
| DC Voltage (auto-range) | :MEAS:VOLT:DC? | :MEAS:VOLT:DC? | ✓ Identical |
| AC Voltage (auto-range) | :MEAS:VOLT:AC? | :MEAS:VOLT:AC? | ✓ Identical |
| DC Current | :MEAS:CURR:DC? | :MEAS:CURR:DC? | ✓ Identical |
| Resistance (2-wire) | :MEAS:RES? | :MEAS:RES? | ✓ Identical |
| Resistance (4-wire) | :MEAS:FRES? | :MEAS:FRES? | ✓ Identical |
| Frequency | :MEAS:FREQ? | :MEAS:FREQ? | ✓ Identical |
| Configure DC Voltage | :CONF:VOLT:DC <range>,<res> | :CONF:VOLT:DC <range>,<res> | ✓ Identical |
| Set integration time (NPLC) | :VOLT:DC:NPLC <n> | :VOLT:DC:NPLC <n> | ✓ Identical |
| Trigger count | :TRIG:COUN <n> | :TRIG:COUN <n> | ✓ Identical |
| Initiate measurement | :INIT | :INIT | ✓ Identical |
| Fetch result | :FETC? | :FETC? | ✓ Identical |
| Auto-zero | :VOLT:DC:ZERO:AUTO ON|OFF|ONCE | :VOLT:DC:ZERO:AUTO ON|OFF (no ONCE) | ⚠ Partial |
| Display text | :DISP:TEXT "<msg>" | :DISP:TEXT:DATA "<msg>" | ✗ Different |
| Beeper on/off | :SYST:BEEP:STAT ON|OFF | :SYST:BEEP ON|OFF | ✗ Different |
| Error queue query | :SYST:ERR? | :SYST:ERR? | ✓ Identical |
| Math statistics (mean/std) | :CALC:AVER:AVER? / :CALC:AVER:SDEV? | :STAT:AVER? / :STAT:SDEV? | ✗ Different |
| Reading memory recall | :DATA:LAST? / :DATA:POIN? | :DATA:LAST? | ⚠ Partial |
*IDN? at startup to log the exact firmware version in use.
What's Directly Compatible
The good news: the most commonly used measurement commands are IEEE 488.2 / SCPI-standard and work identically on both instruments:
- All
:MEASure:one-shot measurement commands (VOLT:DC,VOLT:AC,CURR:DC,RES,FREQ) :CONFigure:+:INITiate+:FETCh?triggered measurement flow- NPLC settings (
:VOLT:DC:NPLC) - Range specification in
:MEASure:and:CONFigure:commands - IEEE 488.2 common commands:
*IDN?,*RST,*OPC?,*CLS - Error queue:
:SYST:ERR?
If your script only uses these commands, you can swap instruments with no code changes at all — just update the VISA resource string.
The Adapter Pattern in Python
For scripts that use the incompatible commands, wrap both instruments behind a common interface. Each adapter translates the common API into the vendor-specific commands:
import pyvisa
class DMM:
"""Vendor-agnostic DMM interface."""
def __init__(self, resource_string: str):
rm = pyvisa.ResourceManager()
self._inst = rm.open_resource(resource_string)
self._inst.timeout = 10000
idn = self._inst.query('*IDN?').strip()
print(f'Connected: {idn}')
# Detect vendor from IDN string
self._is_rigol = 'RIGOL' in idn.upper()
def measure_dc_voltage(self, range_v: float = 0) -> float:
"""Measure DC voltage. range_v=0 means auto-range."""
if range_v:
return float(self._inst.query(f':MEAS:VOLT:DC? {range_v}'))
return float(self._inst.query(':MEAS:VOLT:DC?'))
def measure_ac_voltage(self, range_v: float = 0) -> float:
"""Measure AC voltage (Vrms)."""
if range_v:
return float(self._inst.query(f':MEAS:VOLT:AC? {range_v}'))
return float(self._inst.query(':MEAS:VOLT:AC?'))
def measure_resistance(self, four_wire: bool = False) -> float:
"""Measure resistance (2-wire or 4-wire)."""
cmd = ':MEAS:FRES?' if four_wire else ':MEAS:RES?'
return float(self._inst.query(cmd))
def set_nplc(self, nplc: float):
"""Set integration time in power line cycles."""
self._inst.write(f':VOLT:DC:NPLC {nplc}')
def display_text(self, text: str):
"""Show a message on the front panel display."""
if self._is_rigol:
self._inst.write(f':DISP:TEXT:DATA "{text}"')
else:
self._inst.write(f':DISP:TEXT "{text}"')
def get_stats(self) -> dict:
"""Return mean and standard deviation of the reading buffer."""
if self._is_rigol:
mean = float(self._inst.query(':STAT:AVER?'))
sdev = float(self._inst.query(':STAT:SDEV?'))
else:
mean = float(self._inst.query(':CALC:AVER:AVER?'))
sdev = float(self._inst.query(':CALC:AVER:SDEV?'))
return {'mean': mean, 'sdev': sdev}
def reset(self):
self._inst.write('*RST')
self._inst.query('*OPC?')
def close(self):
self._inst.close()
Using the adapter, the rest of your test code never references a vendor name:
# Works with either instrument — just change the address
dmm = DMM('TCPIP0::192.168.1.11::hislip0::INSTR') # Keysight 34461A
# dmm = DMM('TCPIP0::192.168.1.12::inst0::INSTR') # Rigol DM3068
v = dmm.measure_dc_voltage()
print(f'DC voltage: {v:.5f} V')
dmm.display_text('TEST RUNNING')
dmm.close()
Full Example: Vendor-Agnostic DC Sweep
This script measures DC voltage at multiple points on a resistor divider, using whichever DMM is connected. The power supply steps the voltage; the DMM measures the result. Only the VISA addresses need to change when switching hardware.
import pyvisa
import csv
# ---- Adapter (paste the DMM class from above here, or import it) ----
# ---- Test configuration ----
DMM_ADDR = 'TCPIP0::192.168.1.11::hislip0::INSTR' # Keysight or Rigol
PSU_ADDR = 'USB0::0x2A8D::0x1602::MY12345::INSTR'
TEST_VOLTAGES = [1.0, 2.0, 3.3, 5.0, 10.0, 12.0] # V
# ---- Main ----
rm = pyvisa.ResourceManager()
psu = rm.open_resource(PSU_ADDR)
psu.timeout = 10000
dmm = DMM(DMM_ADDR)
try:
psu.write('*RST')
psu.query('*OPC?')
psu.write(':CURR 0.1') # 100 mA current limit
psu.write(':OUTP ON')
dmm.set_nplc(10) # slow but accurate: 10 PLC
rows = []
for setpoint in TEST_VOLTAGES:
psu.write(f':VOLT {setpoint}')
psu.query('*OPC?')
measured = dmm.measure_dc_voltage()
error_pct = (measured - setpoint) / setpoint * 100
rows.append({'setpoint_v': setpoint,
'measured_v': measured,
'error_pct': error_pct})
print(f'Set {setpoint:5.1f} V → Measured {measured:.5f} V '
f'({error_pct:+.3f}%)')
with open('dc_accuracy.csv', 'w', newline='') as f:
w = csv.DictWriter(f, fieldnames=['setpoint_v', 'measured_v', 'error_pct'])
w.writeheader()
w.writerows(rows)
print('\nSaved: dc_accuracy.csv')
finally:
psu.write(':OUTP OFF')
psu.close()
dmm.close()
rm.close()
Applying This to Other Instrument Pairs
The same adapter pattern works for any two instruments from different manufacturers. The process is always the same:
- List the commands your test script actually uses.
- Check both programming manuals — identify which commands differ.
- Write one adapter method per operation, branching on vendor where needed.
- Replace all direct
inst.write()/inst.query()calls in your test with adapter method calls.
Common instrument pairs where this is useful:
- Oscilloscopes: Keysight InfiniiVision vs Rigol DS/DHO series — trigger setup, channel coupling, and waveform data formats differ
- Signal generators: Keysight 33600A vs Rigol DG900 — output load impedance commands and burst mode syntax differ
- Power supplies: Keysight E36300 vs Rigol DP800 — output channel addressing differs (
:OUTP CH1vs:OUTP ON,(@1))
*IDN?. The IDN string reliably identifies the manufacturer and model. Parse it once at connection time and store the vendor flag — never hard-code it in test logic.