Keysight vs Rigol DMM: Switching SCPI Commands

PyVISA · Intermediate · ~15 min

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.

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.

The goal: isolate every instrument-specific string into one place — an adapter class — so the rest of your test code never needs to change when the hardware changes.

Command Mapping Table

The table below covers the most common DMM operations. Commands marked identical work on both instruments without modification.

OperationKeysight 34461ARigol DM3068Compatible?
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
Always verify against the programming manual. Firmware versions can change command behavior. The table above reflects typical firmware for 34461A (A.02.x) and DM3068 (01.01.x). Run *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()
Keep the adapter thin. Resist the urge to add logic beyond translation. The adapter should map one API call to one or two SCPI commands — nothing more. Business logic (pass/fail limits, retry loops) belongs in the test script, not the adapter.

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:

  1. List the commands your test script actually uses.
  2. Check both programming manuals — identify which commands differ.
  3. Write one adapter method per operation, branching on vendor where needed.
  4. 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 CH1 vs :OUTP ON,(@1))
Start with *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.

Next Steps