Control and digitize bench oscilloscopes. Targets Rigol SCPI scopes (DS1000Z / DS2000 / MSO5000 / DHO800/900 and similar) and is built to manage several scopes at once over LAN.
Two ways to use it:
- Multi-scope web dashboard (
scope-dashboard) -- view and fully control every connected scope from a browser: live waveform plot, live screen mirror, per-channel / timebase / trigger / acquisition controls, run/stop/single, and a raw SCPI console for everything else. - Python library + CLI (
scope-control) -- scripted capture to CSV/NPZ,ScopeFarmfor parallel reads, one-off deep-memory grabs.
scope_control/
scope_control/ package
webdash.py FastAPI multi-scope dashboard server (scope-dashboard)
scpi_tcp.py ScpiScope: fast raw-socket SCPI client (dashboard backend)
web/dashboard.html the dashboard UI (tabs + plot/screen + controls + console)
rigol.py RigolScope: pyvisa control + waveform readout (CLI/USB)
manager.py ScopeFarm: many scopes by name, parallel reads
discover.py list / identify VISA resources, find Rigol(s)
cli.py scope-control (list/info/capture/farm)
examples/ quick_capture.py, farm_capture.py
scopes.example.json copy to scopes.json, fill in your scope IPs
pyproject.toml pip-installable; defines the console scripts
requirements.txt
# dedicated venv (kept separate from the SLM/torch venv)
py -m venv .venv
.\.venv\Scripts\python -m pip install -e .pip install -e . installs the deps and creates the scope-dashboard and
scope-control commands. The dashboard itself needs only numpy + fastapi
uvicorn;pyvisa/pyvisa-py(for the CLI / USB) install with-e ".[visa]".
# one scope, quick:
scope-dashboard --host-scope 169.254.242.0
# all scopes from a config:
scope-dashboard --config scopes.jsonAccess (default --access tailscale, no password): binds 0.0.0.0 but
accepts requests ONLY from loopback + Tailscale (100.64.0.0/10 and the
Tailscale IPv6 ULA); any other source IP gets 403. So it's viewable from any
machine on your tailnet with no password, and unreachable from the plain LAN.
Other modes: --access local (loopback only) or --access all (unrestricted —
only behind a trusted network).
Then open http://127.0.0.1:8600/ locally, or http://<this-host-tailscale-ip>:8600/
from another tailnet machine. Each configured scope is a tab; the selected
scope shows:
- Plot (fast native waveform, all displayed channels) or Screen (the scope's actual display, PNG, ~1 Hz) -- toggle top-right.
- Per-channel Vpp / mean / Vrms tiles + live trigger status.
- Controls: Run / Stop / Single / Auto / Clear; per-channel on-off, V/div, coupling, offset; timebase; trigger (sweep / source / slope / level); acquisition (type / averages / mem depth).
- SCPI console: send any command from the programming guide
(e.g.
:MEASure:ITEM? FREQuency,CHANnel1,:MATH:DISPlay ON). Queries (with?) return a value. This covers the full long tail (math, FFT, cursors, measurements, save/recall, decode, utility).
It is read+control over Ethernet only -- no instrument driver to install. The Rigol accepts multiple simultaneous connections, so the dashboard coexists with other clients.
scopes.json (copy from scopes.example.json) maps a name to each scope's IP:
{ "trap_pd": "192.168.1.51", "awg_mon": "192.168.1.52" }The package is self-contained and config-driven -- nothing is hard-coded to this machine. To run the dashboard on a different PC:
- Put the scopes on a routable network. The current setup uses a direct
link-local cable (
169.254.x.x), which is reachable only from the PC it's plugged into. For multi-PC / multi-scope use, put the scopes on a switch with static IPs (Utility -> IO -> LAN) and list those IPs inscopes.json. - Copy the package (this folder) to the other machine, or
pip installa built wheel (python -m build->pip install scope_control-*.whl). python -m venv .venv; .\.venv\Scripts\python -m pip install -e .scope-dashboard --config scopes.json(add--host 0.0.0.0to view it from yet another machine's browser).
Because addressing is by IP in scopes.json, the same dashboard runs from the
rearrangement PC, the exp-control PC, or anywhere on the network / Tailscale --
just point it at the scopes.
from scope_control import RigolScope # pyvisa-based (also USB)
with RigolScope("TCPIP0::192.168.1.51::INSTR") as scope:
wf = scope.read_waveform(channel=1) # NORM: fast on-screen trace
wf.to_csv("ch1.csv")
deep = scope.read_waveform(channel=1, mode="RAW") # full memory (auto-stops + chunks)
from scope_control import ScpiScope # fast raw-socket (dashboard backend)
sc = ScpiScope("192.168.1.51")
print(sc.read_state(channels=(1, 2))["trigger"])
sc.set_trigger(sweep="NORMal", source="CHANnel1", level=0.5)scope-control --backend '@py' list
scope-control capture --resource TCPIP0::192.168.1.51::INSTR --channel 1 --out ch1.csv
scope-control farm --config scopes.json --channel 1 --outdir captures| Ethernet (LAN) | USB (USBTMC) | |
|---|---|---|
| Scaling to 6 scopes | One switch, talk to all concurrently | 6 ports/hubs; shared host controller |
| Addressing | Stable static IP per scope | Resource string keys off serial; order can shuffle |
| Cable length | ~100 m | ~5 m |
| Driver on Windows | None | NI-VISA / USBTMC driver, hub flakiness |
| Remote access | Yes (e.g. Tailscale) | Local only |
- Frame rate vs sample rate: the dashboard refresh (a few Hz) is how often a frame is pulled; within each frame the scope samples at its full rate (up to 1 GSa/s). Set the scope's timebase to frame the signal. It is not a gap-free recorder -- for that use a one-off RAW deep-memory capture via the library.
- NORM vs RAW (library):
NORM= ~1200 on-screen points (fast).RAW= full memory, scope must stop; read in <=250k-pt chunks automatically. - Scaling:
volts = (code - yorigin - yreference) * yincrement,t = (i - xreference) * xincrement + xorigin. - Screenshot: the dashboard uses PNG (
:DISPlay:DATA? ON,0,PNG, ~46 KB); BMP24 also works (~1.1 MB) if a model lacks PNG. - For exact SCPI on your model, see its Rigol Programming Guide PDF.