SigScope

software / status: active

I have a Siglent SDS1204X-E on the bench. Siglent ships a pretty app for it, but only for Windows, and I’m on a Mac. The USB port on the back speaks a standard protocol though, so I figured I could write my own. This is that: a native Mac app in C++ and Qt that drives the scope like its own front panel, knobs and lit buttons, live traces, the works. Not just pull a screenshot, actually run the thing.

The app connected to the scope. Square wave on CH1 at 50 µs/div, 8.3 fps, 267 queries a second keeping the panel in sync with the physical knobs.
The app connected to the scope. Square wave on CH1 at 50 µs/div, 8.3 fps, 267 queries a second keeping the panel in sync with the physical knobs.

Current state

As of 2026-09-10: Four versions done in three days, all merged and running against the real scope.

Selftest passes against the scope with a cursor round trip added. Live plot ran 4.5 fps with two channels, 7 Mpt and five measurement slots in the last capture. The manual acceptance checklist for v0.4 is written and unticked.

Architecture

Four layers, bottom to top.

The scope’s USB port is USB Test and Measurement Class (USBTMC). macOS has no driver for it, so the app claims the interface itself with libusb in user space. No kext, no VISA install. I hand-wrote the USBTMC framing because Siglent’s implementation has a pile of quirks: it rejects the standard clear and abort requests, sends stray zero-length packets, lies about transfer sizes, and wedges the link if you send it certain commands. The transport bakes in workarounds for each.

On top of that is SCPI in the older LeCroy short-form dialect this series uses (C1:VDIV 1.3V, TDIV 50NS, TRMD SINGLE). Waveforms and screenshots come back as binary blocks with a length header, so the reader trusts the header and never a line terminator.

Above that, a typed command layer with a denylist of commands that hang the scope, a table mapping every UI control to its SCPI strings, and a scheduler on its own I/O thread that interleaves user actions, waveform fetches, and state polls. The GUI is Qt Widgets and never touches USB directly; everything crosses the thread boundary as queued signals.

Hardware

Siglent SDS1204X-E, 4 channels, 200 MHz, firmware 6.1.37R10, on the rear USB Device port into an M-series Mac running macOS 26. The port enumerates as USBTMC (class 0xFE/0x03/0x01), full speed only, 64-byte packets. Real throughput is around 500 KB/s. Signal source for testing is a square wave into CH1.

Built with CMake, Qt 6.11 from Homebrew, libusb 1.0, Apple clang. Twelve test binaries under ctest cover everything below the GUI with a scripted fake transport, and a --selftest mode runs a 35 s regression against the real scope and puts every setting back the way it found it.

Gateware and firmware

Repos, toolchain, how to build.

Decisions

Open problems

Build logs

Dated working notes for this project, newest first. Corrections land as new entries.