Skip to main content

Python client for Rigol MSO5000 oscilloscopes, driven by SCPI definitions extracted from the scope firmware

Project description

pyMSO5000

A Python library and MCP server for controlling Rigol MSO5000 series oscilloscopes over VISA (pyvisa / pyvisa-py).

Every call is validated against the SCPI command definitions extracted from the scope's own firmware - arity, value types and enum spellings - so a bad argument is rejected locally instead of becoming a silent entry in the scope's error queue.

Installation

pip install pymso5000

Two optional extras:

  • pymso5000[mcp] - the MCP server for AI agents.
  • pymso5000[firmware] - extracting SCPI definitions from a firmware .GEL image (also needs a system LZO development library). The bundled definitions need neither.

Quick start

Scope is the high-level API. It validates arguments before anything is sent, canonicalizes the instrument's replies, and returns typed values:

from pymso5000 import Scope

with Scope.connect("TCPIP::192.168.178.102::INSTR") as scope:
    print(scope.info().model)  # 'MSO5074'

    scope.set_channel(2, enabled=True, coupling="dc", scale_per_div=0.5)
    scope.set_timebase(scale_s_per_div=1e-4)
    scope.run()

    print(scope.get_acquisition().sample_rate_sa_s)  # 2000000000.0

Short SCPI spellings are accepted anywhere a mnemonic is: coupling="dc" reaches the instrument as DC. A value the command cannot take is rejected locally, before any I/O:

scope.set_channel(2, coupling="SIDEWAYS")
# ScopeUsageError: [usage] coupling must be one of ['AC', 'DC', 'GND'], got 'SIDEWAYS'.

Transports

Any message-based VISA resource works: TCPIP::<host>::INSTR (VXI-11), HiSLIP, USBTMC, GPIB, a raw socket (TCPIP::<host>::5555::SOCKET) or a serial line; framing is decided from the resource name. The bundled pyvisa-py backend covers TCPIP out of the box; USBTMC additionally needs pyusb, serial needs pyserial, GPIB needs gpib-ctypes or linux-gpib, and a full VISA implementation supplies all of them.

Library usage

Subsystems

The densely-parameterized parts of the instrument hang off Scope as their own objects:

from pymso5000.api.trigger_models import EdgeTriggerConfig
from pymso5000.api.measurement_models import WaveformMeasurementRequest

scope.generator.set(1, shape="SQUare", frequency_hz=10_000, amplitude_vpp=2.0, output_enabled=True)

scope.trigger.set(EdgeTriggerConfig(mode="EDGE", source="CHANnel2", level=0.0), sweep="AUTO")

results = scope.measure.measure(
    [
        WaveformMeasurementRequest(kind="waveform", item="FREQuency", source="CHANnel2"),
    ]
)
print(results.results[0].value, results.results[0].unit)  # 10000.0 Hz

Waveforms

inspect reads statistics (and optionally a decimated preview) without keeping the record; acquire keeps every point so it can be analyzed afterwards. Deep memory is transferred in windows, with statistics accumulated in that single pass:

scope.stop()  # RAW needs a stopped acquisition
wf = scope.waveform.acquire("CHANnel2", "RAW")
print(wf.point_count, wf.stats.peak_to_peak)  # 2000000 2.075656

# No whole-capture exemption here, so the scan is bounded to one call's worth.
scan = wf.find_edges(0.0, direction="rising", stop_index=min(wf.point_count, 5_000_000))
print(1 / scan.intervals.mean_s)  # 10000.0
print(wf.values(0, 5))  # first five samples, in volts
print(wf.summarize(0, 1000).stats.rms)  # statistics over one sample range

values, summarize and find_edges operate on the retained record; the same operations are available as pure functions in pymso5000.api.waveform_analysis. A whole-capture summarize is free at any depth; find_edges is bounded to 5,000,000 samples per call, so a deeper capture is walked in windows.

Errors

A write the instrument refuses raises ScopeCommandError. Setters that apply several settings at once complete the sequence and read the instrument back first, because the scope does not roll back what already landed:

from pymso5000 import ScopeCommandError

try:
    scope.generator.set(1, shape="PULSe", duty_cycle_pct=150.0)
except ScopeCommandError as exc:
    print(exc.errors)  # ['-200,"Command execute failed"']
    print(exc.partial.duty_cycle_pct)  # 20.0 - the shape applied, the duty cycle did not

Every error carries a kind and a retryable flag, so a caller can tell a bad argument from a dropped link from an instrument in the wrong state without matching on message text.

Rolling the instrument back

saved_setup() exports the whole instrument setup and puts it back when the block ends - the rollback point for anything that reconfigures the scope broadly, such as autoscale():

with scope.saved_setup():
    scope.autoscale()
    print(scope.measure.measure([...]))
# vertical, horizontal and trigger settings are as they were

Restoration runs on both exit paths; a failed restore during an exception is logged rather than raised, so it cannot hide the failure that triggered it. The setup blob does not include the built-in generator, so save scope.generator.get separately when changing the AWG.

Anything not wrapped

Scope.execute runs any of the 2236 firmware commands, still validating its arguments against the command's own definition:

print(scope.execute("CHANnel2:SCALe?"))  # 0.5
scope.execute("CHANnel2:SCALe", [0.2])

ScpiCatalog searches and describes that command set, and needs no connection at all:

from pymso5000 import ScpiCatalog

catalog = ScpiCatalog.bundled()
described = catalog.describe("CHAN1:COUP?")  # short forms resolve
print(described.outputs[0].enum_values)  # ['AC', 'DC', 'GND']
print(described.documentation.short_description)

CommandDocs bundles the programming guide's documentation - what each command does - and matches it to command strings, short forms and firmware items:

from pymso5000 import CommandDocs, ScpiCatalog

docs = CommandDocs.load()  # bundled, cached
doc = docs.find(":BUS1:SPI:TIMeout:TIME?")  # long form (case-insensitive)
print(doc.render())  # syntax, description, params, examples

item = ScpiCatalog.bundled().resolve("CHAN1:SCAL")  # short form -> firmware item
print(docs.find_for_item(item).short_description)  # for an alias, pass item.target

The low-level client

MSO5000 is the transport underneath: it serializes a command and parses the reply (including TMC binary blocks for screenshots, waveforms and setups), with no connection management on top:

from pymso5000 import MSO5000, load_bundled_scpi_config

cfg = load_bundled_scpi_config()
with MSO5000.create_from_resource_name("TCPIP::192.168.178.102::INSTR") as scope:
    print(scope.execute_command(cfg, "CHANnel1:SCALe?"))  # -> 0.2 (float)
    scope.execute_command(cfg, "CHANnel1:COUPling", ["AC"])
    image = scope.execute_command(cfg, "SAVE:IMAGe:DATA?")  # -> numpy ndarray

Definitions can also come from a firmware image rather than the bundled copy:

from pymso5000 import SCPIConfig, get_scpi_definition_files_from_firmware

cfg = SCPIConfig.create_from_file_dictionary(
    get_scpi_definition_files_from_firmware("resources/DS5000Update_01.03.03.00.GEL")
)

The definitions the package ships live in src/pymso5000/data/scpi_mso5000/, and are the only copy. tests/test_firmware_extraction.py extracts the image above and asserts the result matches them byte for byte, so the shipped definitions are checked against the firmware rather than against a second copy of themselves.

MCP server (for AI agents)

An MCP (Model Context Protocol) server exposes the scope to AI agents, including screenshot and touchscreen/front-panel control.

Install the optional dependency and run it (stdio transport):

uv sync --extra mcp                       # or: pip install 'pymso5000[mcp]'
pymso5000-mcp --resource TCPIP::192.168.178.102::INSTR

Example client configuration:

{
  "mcpServers": {
    "mso5000": {
      "command": "uv",
      "args": ["run", "pymso5000-mcp"],
      "env": { "PYMSO5000_RESOURCE": "TCPIP::192.168.178.102::INSTR" }
    }
  }
}

⚠️ What this server can do to your instrument

By default the server can change the state of real hardware on your bench. It provides normal operator access: acquisition, channels, trigger, timebase, waveform-generator and display settings; reset/recall; and unrestricted touchscreen, key and knob controls. Reversible preferences such as date/time, language, beeper, screen saver and power-on behavior are also available.

Direct SCPI operations with persistent, administrative or service-level effects are divided into six permissions, all disabled by default:

Risk category Examples
storage Save/export files, stored setup slots (*SAV), reference saves
connectivity LAN configuration/application, GPIB address, remote server configuration
security Password clearing, web-control reset, front-panel/remote locking
calibration Factory/service calibration registers and calibration-data writes
licensing Option installation and removal
firmware Flash writes, nonvolatile clearing and undocumented low-level service operations

Enable only the categories needed by a deployment; repeat the option for multiple categories:

pymso5000-mcp --resource TCPIP::192.168.178.102::INSTR \
  --allow-risk storage \
  --allow-risk connectivity

Matching happens on the resolved canonical command and alias target, so a short form such as CAL:ADC:REG cannot bypass the calibration restriction. scpi_describe reports risk_category, required_permission and allowed_by_policy before a caller attempts execution.

The categories guard direct SCPI execution. Raw touch_tap, press_key and turn_knob are deliberately a trusted, front-panel-equivalent lane and can reach anything available through the scope's menus, including storage and service operations. Do not expose these tools to an untrusted caller expecting the SCPI categories to form a strict sandbox.

Configuration

Definition-source precedence: --scpi-dir > --firmware <file.GEL> > bundled definitions. Every flag has an environment-variable equivalent:

Flag Environment variable
--resource (required) PYMSO5000_RESOURCE
--scpi-dir PYMSO5000_SCPI_DIR
--firmware PYMSO5000_FIRMWARE
--timeout-ms (default 10000) PYMSO5000_TIMEOUT_MS
--waveform-store-max-mib (default 256) PYMSO5000_WAVEFORM_STORE_MAX_MIB
--waveform-store-max-captures (default 32, max 100) PYMSO5000_WAVEFORM_STORE_MAX_CAPTURES
--waveform-capture-ttl-s (default 1800) PYMSO5000_WAVEFORM_CAPTURE_TTL_S
--setup-store-max-mib (default 16) PYMSO5000_SETUP_STORE_MAX_MIB
--setup-store-max-setups (default 32, max 100) PYMSO5000_SETUP_STORE_MAX_SETUPS
--setup-ttl-s (default 1800, max 31536000) PYMSO5000_SETUP_TTL_S
--allow-risk CATEGORY (repeatable) PYMSO5000_ALLOW_RISKS=storage,connectivity
--log-level (default INFO) PYMSO5000_LOG_LEVEL

The connection is opened lazily, so the server starts fine with the scope switched off. A structured audit line (tool, arguments, duration, outcome or error kind) is written to stderr for every call; stdout carries only the JSON-RPC stream.

Tools

  • Screen & UI: screenshot (1024x600 PNG whose pixels map 1:1 to touch coordinates), touch_tap, press_key, turn_knob.
  • Acquisition & setup: run, stop, single, get_acquisition, set_acquisition, autoscale, get_trigger, set_trigger, instrument_info, get_channel, set_channel, get_timebase, set_timebase, get_digital, set_digital, get_generator, set_generator, upload_generator_waveform, export_scope_setup, restore_scope_setup, list_scope_setups, delete_scope_setup.
  • Data: batched measure, clear_measurement_items, get_measurement_reference_levels, set_measurement_reference_levels, configure_measurement_statistics, get_measurement_statistics, inspect_waveform, capture_waveform, read_waveform_samples, summarize_waveform_capture, find_waveform_edges, list_waveform_captures, delete_waveform_capture.
  • Generic SCPI: scpi_search, scpi_describe, scpi_execute cover the full firmware command set for anything the typed tools do not, and surface the programming guide's own documentation. scpi_execute validates arguments against the command's firmware schema before writing anything, so a rejected call has no effect on the instrument.

Detailed per-tool behavior - side effects, transfer costs, cancellation semantics, and measured firmware quirks - is documented in the tool descriptions themselves, where the agents that call them can see it.

Resources and prompts

  • scpi-doc://command/{command} — the programming-guide entry for one command (works with the scope switched off; accepts long or short forms).
  • scope://state — the whole setup in one read: run state, trigger, timebase, acquisition, all four analog channels, the logic analyzer and both generators. 62 VISA round trips with the analyzer off, and the digital block collapses to the master switch alone while it is.
  • oscilloscope://captures/{capture_id} and oscilloscope://setups/{setup_id} — small JSON metadata manifests for retained captures and setups; samples and setup blobs are accessed only through bounded tools, never embedded.
  • Prompts characterize_signal and debug_no_trigger encode the screenshot → act → screenshot workflow for the two most common tasks.

Development environment (NixOS)

nix-shell            # enters an FHS environment with uv
uv sync --extra firmware --extra mcp  # include firmware extraction and MCP server
uv run pytest        # offline parser/IO/MCP tests (no scope needed)
uv run ruff check    # lint
uv run ruff format   # format
uv run pyright       # type-check
uv run python examples/first_test.py --resource TCPIP::192.168.178.102::INSTR

nix-shell is interactive; for a one-off command outside it, build the FHS wrapper instead:

nix-build shell.nix -A fhs && ./result/bin/pymso5000-dev -c "uv run pytest"

The MCP server's published surface (tool names, descriptions, annotations and JSON schemas, plus resources and prompts) is snapshotted in tests/data/tool_schemas.json so an unintended change shows up as a diff. After an intended change, regenerate it:

uv run pytest tests/test_mcp_schemas.py --snapshot-update

The same checks run in CI (.github/workflows/ci.yml).

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pymso5000-0.1.0.tar.gz (596.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pymso5000-0.1.0-py3-none-any.whl (426.5 kB view details)

Uploaded Python 3

File details

Details for the file pymso5000-0.1.0.tar.gz.

File metadata

  • Download URL: pymso5000-0.1.0.tar.gz
  • Upload date:
  • Size: 596.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for pymso5000-0.1.0.tar.gz
Algorithm Hash digest
SHA256 3ec590db1a7b72350a3292290c1a97dd896f086fcd662fb42debd8b5a8e45915
MD5 7166a164ecd4de4376e6e70e94c919c8
BLAKE2b-256 be985b3fd132caf681beafabe20128686c4420d6531d00b7da4d7c29faed0674

See more details on using hashes here.

Provenance

The following attestation bundles were made for pymso5000-0.1.0.tar.gz:

Publisher: release.yml on mabl/pyMSO5000

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pymso5000-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: pymso5000-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 426.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for pymso5000-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 38ba16e279c8abe769ce05aa5c7447149a6118627379f09451606626fb8e2d1b
MD5 b7a6ddd9945e44d03c8c8b02f8b9bf2d
BLAKE2b-256 24b783166c00ebdcfba9c3dd3765631aa25670ac7aa4864db2d7c2abd3862ef7

See more details on using hashes here.

Provenance

The following attestation bundles were made for pymso5000-0.1.0-py3-none-any.whl:

Publisher: release.yml on mabl/pyMSO5000

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page