Remote control of oscilloscopes from Linux, in Ada: at present the Rigol DS1000Z-E series (DS1102Z-E and DS1202Z-E), over USB or the network. Developed and tested on a Rigol DS1202Z-E (firmware 00.06.04) only; see Compatibility for other models.
The user manual walks through every function with examples and screenshots.
- An Ada library (
src/) with strongly typed SCPI commands: channels, timebase, trigger, acquisition, measurements, the math channel, the pass/fail test, the bus decoders, waveform and memory reads, screenshots. scopebridge-server, which owns the scope connection and serves it over a documented protocol (docs/PROTOCOL.md): JSON lines over TCP, with binary payloads for waveform data. It adds what the scope cannot do on its own: FFTs of the whole acquisition memory, zooming through millions of points, decoding UART, I2C and SPI traffic from the whole capture, timing code (every run of a block a program marks on a pin: statistics, histogram, the slowest run), and reference waveforms shared by all clients.scopebridge-gui, a GtkAda front end: live view, all common controls (edge, pulse and slope triggers), measurements (recordable to CSV), cursors, math and FFT, an XY display, pass/fail testing, reference waveforms, bus decoding with a list of the decoded bytes, code timing with a histogram, full-memory capture with zoom and pan, setups saved to files, screenshots and CSV export.- Python scripting (
clients/python/scopebridge.py): a small API for automating measurements, captures and logging, alongside the GUI. scopebridge-term, a terminal client in Ada: an interactive command line with a live waveform drawn in characters; commands can also be piped in.- A web interface:
scopebridge-server --web 8080serves a page that any browser, on any computer, tablet or phone, can use as its GUI, with whatscopebridge-guihas: live view and captures, math, FFT, cursors, bus decoding, code timing, references, pass/fail, setups and files. - A second, small GUI in Python/tkinter (
clients/python/scopebridge_tk.py), showing that any front end can use the server, even next to the first. - On a Raspberry Pi: the server can run on a small computer the scope
hangs off, with the GUIs, scripts and browsers anywhere on the network;
contrib/cross-build.shmakes a ready-to-install package for it. - Microcontroller examples (
examples/arduino/): UART, I2C and SPI test signals and code timing on an Arduino Uno in plain C, markers for timing code on any microcontroller (timing.h), and an automated test bench that flashes the Uno and checks it with the scope.
scopebridge-gui --+ +-- USB (/dev/usbtmcN)
Python --+-- TCP 127.0.0.1:5026 -- scopebridge-server --+
scripts --+ docs/PROTOCOL.md +-- LAN (port 5555)
- GNAT (tested with GNAT 10) and gprbuild
- GNATCOLL, for the server:
libgnatcoll19-devon Ubuntu 22.04 (the number varies with the release:apt search libgnatcoll) - GtkAda, for the GUI:
sudo apt install libgtkada20-dev - Python 3, for the protocol tests and scripts
make deps-check tells which of these are missing, and on Debian and
Ubuntu the exact apt install command for them; make deps runs that
command, after asking.
make # everything, into bin/
make check # library tests against a mock transport
make check-server # FFT, decoding and timing tests, then the protocol,
# scripting, terminal and web tests on the simulated scope
make check-web # the web page in headless Firefox (needs Firefox)
None of the checks needs an instrument.
make checks first that GNAT, gprbuild, GNATCOLL and GtkAda are there.
Without GtkAda it builds everything but the GUI, and without GNATCOLL
everything but the server and the terminal client; it says what it left
out, and how to install what is missing.
make # as yourself
sudo make install # into /usr/local; or PREFIX=...
This installs scopebridge-server, scopebridge-gui, scopebridge-term, scopebridge (the
start script), scopebridge-run (the Python runner) and scopebridge-tk, man pages
for them, the Python module (in share/scopebridge/python), this README, the
manual and the protocol (in share/doc/scopebridge), an example settings
file, and a systemd user unit that keeps a server running with the
settings in ~/.scopebridgerc (see Settings below):
cp /usr/local/share/doc/scopebridge/scopebridgerc.example ~/.scopebridgerc
systemctl --user enable --now scopebridge-server # the scope on USB, unless it says otherwise
The addresses in the examples are examples, from one network:
192.168.0.83 is the scope, 192.168.0.19 a Raspberry Pi. Use your own:
the scope shows its address under Utility -> IO Setting -> LAN Conf, and
hostname -I shows a Linux computer's, a Pi's for instance. Or find them
from any computer on the network: sudo arp-scan --localnet (sudo apt install arp-scan) lists every device with its maker, a Rigol scope as
"Rigol Technologies, Inc." and a Pi as "Raspberry Pi Trading Ltd" or
"Raspberry Pi Foundation":
$ sudo arp-scan --localnet
192.168.0.83 00:19:af:xx:xx:xx Rigol Technologies, Inc.
With the service running, scopebridge, scopebridge-term and scripts connect to it
straight away. For a PREFIX in your home directory, add
SYSTEMD_USER_DIR=~/.config/systemd/user. make uninstall (with the same
PREFIX) removes it all again; DESTDIR is honoured for packaging.
./scopebridge.sh # finds the scope on USB, starts server and GUI
./scopebridge.sh --lan 192.168.0.83 # over the network
./scopebridge.sh --sim # a simulated scope, no hardware
scopebridge.sh stops the server when the GUI closes. If a server is already
running on the port, the GUI connects to it and leaves it running. The
parts can also be started separately:
./bin/scopebridge-server --usb auto # or --usb /dev/usbtmcN, --lan HOST[:PORT], --sim
./bin/scopebridge-gui # connects to 127.0.0.1:5026
Any number of clients can use one server at once, for example the GUI and
a script, or both GUIs (clients/python/scopebridge_tk.py is the small one).
The programs take their defaults from ~/.scopebridgerc; options on the
command line win over it. contrib/scopebridgerc.example (installed in
share/doc/scopebridge) is a commented start:
[server]
source = usb auto # or: usb /dev/usbtmc0, lan 192.168.0.83, sim
port = 5026
listen = 127.0.0.1 # 0.0.0.0: from the network too
web = 8080 # the web interface; empty: none
[client]
host = 192.168.0.19 # where the server runs: here 127.0.0.1
port = 5026
scopebridge-server (and its service) reads [server]; without a source
anywhere, it looks for the scope on USB. scopebridge-gui,
scopebridge-term, scopebridge-run and Python's Scope() read
[client]. scopebridge reads both: with [client] host set to another
computer, it starts only the GUI, connected there, so a plain scopebridge
opens the scope on your Raspberry Pi. A line the programs cannot use gives
a warning with its line number. $SCOPEBRIDGE_RC names another file
instead (empty: none); $SCOPEBRIDGE_HOST and $SCOPEBRIDGE_PORT win over
[client] for scripts.
./bin/scopebridge-server --usb auto --web 8080 # then open http://localhost:8080/
./scopebridge.sh --web 8080 # the same, with the GUI
The page is a GUI for any browser, laid out for a desktop or a phone, with
the desktop GUI's functions: live view and captures with zoom and pan,
triggers, measurements and recording, math and FFT, cursors, XY, bus
decoding, references, pass/fail, setups, screenshots and CSV export
(files go to and from the browser's computer). With --listen 0.0.0.0
other computers can open it at http://this-computer:8080/. Several
browsers and the other clients can use the server at the same time. It
has the same lack of security as the protocol: on a network you do not
trust, use an SSH tunnel (see the Raspberry Pi section).
The server listens on 127.0.0.1 unless given --listen ADDRESS. The
protocol has no authentication, so only expose it on a network you trust.
./bin/scopebridge-term # with a server running
scopebridge> ch 1 scale 500m coupling dc
scopebridge> measure 1 freq vpp vrms
freq 1.00 kHz vpp 3.10 V vrms 2.09 V
scopebridge> watch 1 # live, in characters; Enter stops
scopebridge> capture 1
scopebridge> save capture.csv
scopebridge> spectrum 1 flattop # the strongest lines
scopebridge> help
Numbers take SI suffixes (500m, 2u, 1k). Piped commands run as a
script (scopebridge-term < commands.txt); the exit status is 1 if any failed.
The scope appears as /dev/usbtmcN, readable by root only. A udev rule
(contrib/70-rigol.rules.in) gives the plugdev group access, and the
user logged in at the computer:
sudo make install-udev
This installs /etc/udev/rules.d/70-rigol.rules, makes the group if the
system has none (Fedora, Arch), and applies the rule to a scope that is
already connected. It also says whether you are in the group; if not:
sudo usermod -aG plugdev $USER # then log out and in again
Check with the scope connected and on:
$ ls -l /dev/usbtmc*
crw-rw----+ 1 root plugdev 180, 176 ... /dev/usbtmc0
$ groups # should list plugdev
If the device is still root root, unplug the scope's USB cable and
plug it in again. UDEV_GROUP=dialout (or any group) uses another group;
sudo make uninstall-udev removes the rule. A rule written by hand
earlier as 99-rigol.rules is replaced.
The scope listens for SCPI on TCP port 5555; its address is shown under Utility -> IO Setting -> LAN Conf.
If the connection breaks (the scope switched off and on, the network gone for a moment), the request it happens in fails and the next one connects again.
The scope can hang off a small computer, such as a Raspberry Pi 3 or 4, that runs only the server; the GUI, the terminal client, scripts and browsers then run on any computer on the network, several at once. Running everything on one computer stays exactly as it is.
Every release on GitHub has the packages ready to download, on the Releases page, so step 1 can be skipped: download the one for the Pi and go on with step 2.
1. Build the package, on this computer. Which one the Pi needs,
dpkg --print-architecture on the Pi tells: arm64 (64-bit) or armhf
(32-bit).
contrib/cross-build.sh # arm64: 64-bit Raspberry Pi OS, 64-bit Ubuntu
contrib/cross-build.sh --arch armhf # armhf: 32-bit Raspberry Pi OS (Pi 2, 3, 4)
This makes dist/scopebridge-VERSION-arm64.tar.gz (or -armhf), about 3 MB,
in a few minutes. The programs in it need nothing on the Pi but its C
library (glibc 2.17 or later), so one arm64 package serves several
systems: it has run on Raspberry Pi OS 11 and Ubuntu 22.04 (see Tried
on), and should on Raspberry Pi OS 12 too. The first run fetches the build system,
about 80 MB, into ~/.cache/scopebridge-cross.
2. Copy it over, to your home folder on the Pi, with your user name
and the Pi's address there (hostname -I on the Pi shows it; here and
below it is 192.168.0.19). The : at the end is what makes it the Pi's;
without it scp copies to a local file named user@...:
scp dist/scopebridge-*-arm64.tar.gz user@192.168.0.19:
3. Install it, on the Pi:
tar -xzf scopebridge-*-arm64.tar.gz
cd scopebridge-*-arm64
sudo ./install.sh
This installs under /usr/local: scopebridge-server, scopebridge-term, the web
interface, the Python client (scopebridge-run), man pages, a systemd user
service, and the udev rule that lets the scope on USB be used without
root. If it says you are not in the plugdev group yet: sudo usermod -aG plugdev $USER, and log in again.
4. Start the server as a service, as yourself (not root), with the
scope on USB and the web interface on port 8080, as set in
~/.scopebridgerc (see Settings):
printf '[server]\nsource = usb auto\nlisten = 0.0.0.0\nweb = 8080\n' > ~/.scopebridgerc
systemctl --user daemon-reload
systemctl --user enable --now scopebridge-server
sudo loginctl enable-linger $USER # start it at boot, without a login
5. Check it, on the Pi and from another computer:
systemctl --user status scopebridge-server # on the Pi: active (running)
journalctl --user -u scopebridge-server # its messages, if not
scopebridge-term --host 192.168.0.19 # elsewhere: then scpi *IDN? or status
and http://192.168.0.19:8080/ in a browser. A reboot of the Pi is a good
last test: the service should come back by itself.
Updating to a newer package: sudo ./uninstall.sh in the old
package's folder, then steps 2 and 3 with the new one, and systemctl --user restart scopebridge-server; the settings in ~/.config/scopebridge stay.
Removing: systemctl --user disable --now scopebridge-server, then sudo ./uninstall.sh.
How the package is made: contrib/cross-build.sh builds in a small root
of the Pi system's own Debian packages (GNAT, gprbuild, GNATCOLL), checked
against Debian's archive keys, run under qemu in a bubblewrap sandbox;
no root is needed. It needs a Debian or Ubuntu computer with sudo apt install qemu-user-static binfmt-support bubblewrap, and builds the
committed sources (git archive HEAD). make package packs a build the
same way for the computer it runs on.
With a compiler on the Pi (Raspberry Pi OS or Ubuntu, 32 or 64 bit), steps 1 to 3 become:
sudo apt install gnat gprbuild libgnatcoll19-dev # the number varies:
# apt search libgnatcoll
make server term # the server and terminal client; no GtkAda
sudo make install
sudo make install-udev # USB access for the plugdev group
and step 4 is the same.
scopebridge --host 192.168.0.19 # the GUI
scopebridge-term --host 192.168.0.19
scopebridge-run --host 192.168.0.19 script.py
# or a browser, also on a phone or tablet: http://192.168.0.19:8080/
A name such as raspberrypi.local works as well as the address, where
the network resolves it.
The protocol has no password or encryption: anyone who can reach the port
can use the scope. On your own network that is usually fine; otherwise
keep the server on 127.0.0.1 (leave out --listen) and reach it through
SSH, which also works from outside:
ssh -N -L 5026:127.0.0.1:5026 user@192.168.0.19 &
scopebridge-gui # through the tunnel
Both from the same arm64 package, the DS1202Z-E on the Pi's USB, the clients on a PC on the network, and each through a reboot:
-
Raspberry Pi 4, 64-bit Raspberry Pi OS 11: a 2.4M-point memory capture in 3.7 s (654 kpoints/s, close to what the scope's USB gives a PC directly), the spectrum of all of it, computed on the Pi, in 0.8 s, a screenshot in 1.3 s.
-
Raspberry Pi 3 (1 GB), 64-bit Ubuntu MATE 22.04, up to the scope's largest memory:
Capture Reading it from the scope Its spectrum, on the Pi 3 1.2M points 1.9 s 1.3 s 12M points 16.5 s 1.6 s 24M points 32.8 s (733 kpoints/s) 1.9 s
The scope's USB sets the pace, the same as on a PC. Not tried: 32-bit
Raspberry Pi OS (--arch armhf; the USB transport adapts to 32-bit
systems), and building for Raspberry Pi OS 12 (--suite bookworm), which
also needs a build computer whose debian-archive-keyring knows
bookworm's keys (Debian 12, Ubuntu 24.04 or later); the bullseye
package should run on it as it is.
Scripts are Python, using the scopebridge module. Several scripts and the GUI
can use the server at the same time.
from scopebridge import Scope
with Scope() as scope:
scope.channel(1, scale=0.5, coupling="dc")
for n in range(10):
scope.single()
scope.wait_for_trigger()
cap = scope.capture(1) # the whole acquisition memory
cap.save_csv(f"run_{n}.csv")
print(scope.measure(1, "freq", "vpp"), cap.spectrum().peak())
scope.record("log.csv", items=("freq", "vpp"), interval=1, duration=3600)Run a script with clients/python/scopebridge_run.py, which can also start an
server for it:
clients/python/scopebridge_run.py script.py # server already running
clients/python/scopebridge_run.py --lan 192.168.0.83 script.py
clients/python/scopebridge_run.py --sim script.py
Scope offers run, stop, single, wait_for_trigger, channel,
timebase, trigger (and pulse_trigger, slope_trigger,
edge_trigger), math, acquire, save_setup, load_setup, status, measure, screen, capture
(with volts, times, save_csv, spectrum), screenshot, record
(measurements to CSV at an interval), mask (pass/fail), ref_save,
ref, refs, ref_load, ref_clear (references), decode and text
(bus decoding of captures), bus (the scope's own decoder), timing and
timing_report (code timing) and scpi;
request sends any request of the protocol. Examples in examples/scripts/:
| Script | Does |
|---|---|
measure_log.py |
measurements to CSV at an interval, until stopped |
capture_series.py |
a series of single-shot captures saved to disk |
harmonics.py |
harmonic levels from a full-memory spectrum |
timebase_sweep.py |
measurements and a screenshot at several timebases |
examples/arduino/ has test programs for an Arduino Uno (UART, I2C, SPI)
and bench.py, an automated test bench: it flashes each program, has the
scope capture and decode it, checks the bytes and measures the timing,
and writes a report with the scope's screenshots. It also has
timing.h, markers for timing code with the scope on AVR, STM32,
RP2040, ESP32 or any Arduino board, timing.c, an example of it, and
delay.c, which checks the timing against a known delay.
scopebridge-server --log session.jsonl records every request and reply of
every client, with timestamps. A log can be replayed, which repeats what
was done in the GUI or by a script:
clients/python/scopebridge_run.py --replay session.jsonl [--client N] [--timing]
with Ada.Text_IO; use Ada.Text_IO;
with Rigol, Rigol.Channel, Rigol.Measure;
with Rigol_Transport.USBTMC;
procedure Example is
T : aliased Rigol_Transport.USBTMC.Handle;
Scope : Rigol.Oscilloscope (T'Access);
begin
Rigol_Transport.USBTMC.Open (T, "/dev/usbtmc0");
Rigol.Channel.Set_Scale (Scope, 1, 0.5);
Put_Line (Float'Image (Rigol.Measure.Frequency (Scope, 1)));
end Example;Transports: Rigol_Transport.USBTMC, Rigol_Transport.LAN,
Rigol_Transport.Simulator (a simulated scope) and Rigol_Transport.Mock
(for tests). See examples/ for complete programs.
Found while bringing the library up on real hardware, and handled by it:
- USB transfers longer than 512 bytes come back partly overwritten, so the
USB transport requests at most 500 bytes per transfer. The kernel's
plain
read()also loses everything after the first 52 bytes of a reply, so the transport frames USBTMC itself. - Clearing the USB device (
USBTMC_IOCTL_CLEAR) right after the scope has booted locks up its USB interface until it is power-cycled. - Every setting command delays the scope's next reply by about 50 ms, and it keeps at most 5 measurements armed; a sixth, or another channel, costs about half a second per query. The server sends only settings that change.
- Over LAN, a query the scope does not know is answered with
Command errorand no line terminator. - Screenshots: BMP24 and JPEG are valid files; PNG has wrong CRCs on its text chunks, BMP8 a wrong size field.
- The scope's own setup block (
:SYSTem:SETup) restores only part of the setup: the timebase, but not the channels' scales, and the trigger level is not saved in it at all. The server's setups add the settings it knows and set them after the block. - A setting the scope ignores (such as a memory depth it does not offer
for the channels on) queues an error whose
:SYSTem:ERRor?gets no reply at all; the query after that works again. - Changing a channel's V/div scales its trigger level with it: the level keeps its place on the screen (1.5 V at 0.5 V/div becomes 3 V at 1 V/div). Set the level after the scale.
- The memory depth is the number of points a capture holds. A point is one value of the waveform, one byte: in the normal acquisition mode one sample, in the others a value made from several (an average, a peak, a high-resolution value). Memory depths depend on the number of channels on (12k ... 24M points with one, 6k ... 12M with two), and the scope converts the depth when that number changes. It ignores a change of depth while stopped.
The implementation follows Rigol's DS1000Z-E Programming Guide, publication number PGA27100-1110 (2019, for software version 00.06.01). It is RIGOL's copyrighted document and is not included here; the current edition is available from Rigol:
https://rigoltechnologies.com/wp-content/uploads/2026/07/DS1000ZE_ProgrammingGuide_EN.pdf
(publication number PGA27101-1110, software version 00.06.02, at the time
of writing). A local copy may be kept in docs/vendor/, which Git ignores.
Everything here was developed and tested against one instrument: a Rigol DS1202Z-E, firmware 00.06.04, over USB and LAN, on Linux (GNAT 10). Several of the instrument's behaviours the code works around (listed above) were found by testing on it and are not in the programming guide, so other models may behave differently.
| Model | Expectation |
|---|---|
| DS1202Z-E | Tested. |
| DS1102Z-E | Should work: same series, firmware and commands. Untested. |
| DS1000Z (DS1054Z, DS1074Z, DS1104Z, Plus models) | Largely the same commands, but 4 channels: the library, protocol and GUIs handle channels 1 and 2 only. The USB quirks are likely shared but unverified. Expect partial use without changes. |
| MSO1000Z | As DS1000Z for the analog channels; digital channels are not supported. |
| Other Rigol families (DS2000, DS4000, MSO5000, DHO, ...) | Not expected to work: their commands, data formats and quirks differ in many places. |
Verified on the DS1202Z-E, each over USB or LAN or both (both transports are verified in full; above them, the code is the same either way):
- the library: settings, measurements, screen and memory reads (up to 24M points), screenshots, error recovery;
- the server and all its clients (both GUIs, the terminal client, Python scripting, the web page, several at once): live view, captures, math and FFT (the scope's and the server's), cursors, XY, edge, pulse and slope triggers, acquisition modes and memory depths, setups (with the restore over USB), pass/fail, references;
- bus decoding and the scope's own bus decoders, on real UART, I2C and SPI signals, and code timing, checked against a delay of known length, all with the Arduino test bench (over USB);
- the server on a Raspberry Pi 4 and a Pi 3, with the scope on their USB.
The simulator (--sim) imitates the DS1202Z-E as found, and is what
the automated tests run against.
Reports from other models are welcome; the simulator and the tests make it easy to see what differs.
| Path | What |
|---|---|
src/ |
the library |
examples/ |
basic_demo, lan_demo, capture_demo; scripts/ (Python); arduino/ (test signals, code timing, the test bench) |
server/ |
scopebridge-server, its FFT, bus decoding and code timing, their tests |
gui/ |
scopebridge-gui |
term/ |
scopebridge-term, the terminal client |
web/ |
the web interface (HTML, CSS, JavaScript) |
clients/python/ |
scopebridge.py (scripting), scopebridge_client.py (protocol), scopebridge_run.py, scopebridge_tk.py (tkinter GUI) |
docs/ |
the protocol, the manual and its images, man pages |
contrib/ |
the systemd user unit, the udev rule, the Raspberry Pi cross build and packaging |
tests/ |
library tests (Ada); protocol, scripting, terminal and web tests (Python) |
MIT; see LICENSE. Copyright (C) 2026 Ulrik Hørlyk Hjort.
