Skip to main content
Glama
Jimbwlah

OWON-VDS1022-MCP

by Jimbwlah

OWON-VDS1022-MCP

Control an OWON VDS1022 / VDS1022i USB oscilloscope from Claude (or any MCP client). Configure the scope, capture waveforms, read measurements, export raw samples, decode serial protocols — and get the trace back as an image the AI can actually see, instead of reading the scope GUI and typing numbers by hand.

real capture

A real capture through this server: the scope's built-in 1 kHz probe-compensation square wave, measured at f = 1000.0 Hz, duty 49.9%.


Contents


How it works

Claude Code  ──stdio (MCP)──▶  owon-mcp server (Python)
                                    │  one shared VDS1022 handle, lock-guarded
                                    ▼
                      vendored vds1022 driver (pyusb + libusb0)
                                    │  USB bulk transfers, VID 5345 / PID 1234
                                    ▼
                            OWON VDS1022 / VDS1022i

The USB protocol is not reverse-engineered here — that work already exists. This project vendors the mature open-source driver from florentbr/OWON-VDS1022 and wraps it as 10 MCP tools. On first connect the driver automatically uploads the FPGA bitstream (bundled in vendor/vds1022/fwr/) and reads the factory calibration from the scope's flash — there is no manual firmware or calibration step.

Requirements

  • Hardware: OWON VDS1022 or VDS1022i (also sold as Multicomp MP720016/MP720017). USB identity VID 5345 / PID 1234.

  • OS: Windows (tested on Windows 11). The driver stack also works on Linux/macOS via libusb, but this repo's setup instructions are Windows-first.

  • USB driver: the libusb-win32 (libusb0) driver that OWON's official VDS_C2 software installs. If you've ever run the official software on the machine, you already have it — no Zadig / driver swap needed.

  • Python: 3.10 or newer.

Install

git clone https://github.com/Jimbwlah/OWON-VDS1022-MCP owon-scope
cd owon-scope
python -m venv .venv-win
.venv-win\Scripts\python.exe -m pip install -e ".[dev]"

# verify the install (no scope needed — runs against a mocked device)
.venv-win\Scripts\python.exe -m pytest -q     # expect: 13 passed

numpy must stay < 2. The vendored driver uses numpy-1.x integer promotion (int8 + 128); numpy 2 raises there. The pin is already in pyproject.toml — don't "upgrade" it away.

Register with Claude Code

claude mcp add owon-scope "<ABSOLUTE-PATH>\.venv-win\Scripts\owon-mcp.exe" --scope user --env "OWON_LIBUSB_DLL=C:\Program Files (x86)\OWON\VDS_C2\USBDRV\amd64\libusb0.dll"
  • Positional args (name, command) must come before --env — the --env option is greedy and will swallow the command path otherwise.

  • --scope user makes the scope available in every project; use --scope project to write a shareable .mcp.json instead.

  • OWON_LIBUSB_DLL tells pyusb where to find a libusb backend DLL. The path above is where OWON's own software ships it. Omit only if libusb0.dll is already on your PATH.

Verify: claude mcp list should show owon-scope … ✔ Connected.

Manual .mcp.json equivalent:

{
  "mcpServers": {
    "owon-scope": {
      "command": "D:\\projects\\owon-scope\\.venv-win\\Scripts\\owon-mcp.exe",
      "env": {
        "OWON_LIBUSB_DLL": "C:\\Program Files (x86)\\OWON\\VDS_C2\\USBDRV\\amd64\\libusb0.dll"
      }
    }
  }
}

The golden rule

⚠️ Close the official OWON VDS_C2 application before use. The scope allows exactly one program to hold its USB handle. If VDS_C2 (or a second MCP session) has it, connection fails with a clear error saying so. Conversely, while this server is using the scope, VDS_C2 won't see it.

Using it — example prompts

Once registered, just talk to Claude:

  • "Check the scope is connected"scope_status

  • "Capture whatever is on the scope and show me"scope_capture

  • "Set CH1 to 5 V range, DC coupling, x10 probe, then capture"

  • "Trigger on a rising edge through 1.5 V on CH2, 5 ms window, and show me the wave"

  • "What's the frequency and duty cycle on CH1?"scope_measure

  • "Export the current waveform to CSV"scope_export

  • "Decode the UART traffic on CH1 at 115200 baud"scope_decode

The first call that touches the scope takes a few seconds (FPGA upload + calibration read); everything after is fast.

Tool reference

All voltage/time/rate arguments accept human-friendly strings: '20v', '500mv', '10ms', '50us', '100M', 'x10', 'CH1'.

scope_status()

Connects (if not already) and returns JSON: serial, hardware version, FPGA version, sampling rate, sweep mode, and per-channel config (range, probe, coupling, offset).

scope_set_channel(channel='CH1', range='20v', coupling='DC', probe='x10', offset=0.5)

Vertical setup. range is the full-scale volts across 10 divisions at the probe tip (hardware ranges 50 mV–50 V per 10 div at x1; multiply by probe factor). coupling is DC/AC/GND. offset positions zero volts on screen (0 = bottom, 1 = top). Make probe match the physical switch on your probe, or every voltage will be off by 10×.

scope_set_timebase(timerange=None, sample_rate=None)

Horizontal setup — give one or the other. timerange is the duration of the 5000-sample frame ('50us''2000s'); sample_rate is samples/sec ('2.5''100M').

scope_set_trigger(source='CH1', condition='RISE', level='0v', position=0.5, sweep='AUTO', mode='EDGE')

  • sweep='AUTO' free-runs: a capture always returns, triggered or not. Default.

  • sweep='NORMAL'/'ONCE' wait for the condition (captures time out if it never fires).

  • mode can be EDGE, PULSE, or SLOPE; for pulse/slope use conditions like RISE_SUP/FALL_INF (width comparisons).

  • position puts the trigger point left↔right in the frame (0.5 = centre).

scope_autoset()

Auto-fit range/timebase/trigger to the signal (like the front-panel Auto button).

scope_capture(timeout=8, autorange=False)the primary tool

Captures one frame from all enabled channels and returns both a JSON measurement block (per channel) and a rendered PNG of the trace. Set autorange=True to let it fix a clipped/too-small range automatically.

scope_screenshot(timeout=8)

Same capture, image only.

scope_measure(channel='CH1', timeout=8)

Numbers only, as JSON: vpp, vmin, vmax, vavg, vrms, vamp, vbase, vtop, freq_hz, period_s, phase_deg, duty_cycle, samples, clipped. Metrics that don't apply (e.g. frequency of a DC level) come back null. clipped: true means the signal exceeded the ADC window — increase the range.

scope_export(path, format='csv', timeout=8)

Captures and writes raw samples. CSV: time_s column + one volts column per enabled channel (5000 rows). JSON: {time_s: [...], channels: {CH1: [...]}}.

scope_decode(channel='CH1', protocol='uart', baud=9600, timeout=8)

Captures and decodes a serial protocol from the trace. protocol='uart' (set baud) or 'wire' (raw logic transitions).

Typical workflows

Sanity check / first use — clip a probe onto the scope's probe-compensation tab (front metal tab, ~1 kHz square wave), then: scope_autosetscope_capture. Expect f ≈ 1000 Hz, duty ≈ 50%. A tilted or overshooting top on the square means the probe's compensation trimmer needs adjusting — which is exactly what that signal is for.

Measure a signalscope_set_channel (range comfortably above the expected Vpp, correct probe factor) → scope_set_timebase (aim for 2–10 periods in the window) → scope_capture. If clipped: true, increase range or use autorange=True.

Catch a one-shot eventscope_set_trigger(source, level=..., sweep='ONCE')scope_capture(timeout=30). The capture returns when the event fires, or times out cleanly (nothing hangs).

Log data for analysisscope_export('capture.csv'), then analyse the CSV however you like; the assistant can read it back and do the math.

Behaviour notes

  • Lazy connect: the server starts instantly; the USB connection (and the few-second FPGA upload) happens on the first tool call that needs the device.

  • Fresh-start defaults: if you capture before configuring anything, the server applies a safe free-running default (CH1, 20 V range, 10 ms window, AUTO sweep) so the first capture always works.

  • Config lives in the server process: settings persist across tool calls within a session; restarting the MCP server resets them (the scope itself keeps nothing).

  • Captures can't hang: every capture runs under a timeout; a waiting NORMAL/ONCE trigger is cleanly unblocked (dev.stop()) and reported.

  • One frame = 5000 samples regardless of timebase; sample rate = 5000 / timerange.

Troubleshooting

Symptom

Cause / fix

not found or locked by another application

Scope unplugged, or the VDS_C2 GUI is open — close it. Also check Device Manager shows the device under libusb-win32 devices.

No libusb backend found

Set OWON_LIBUSB_DLL (see registration) or put a libusb0.dll on PATH.

Capture timed out waiting for a trigger

You're in NORMAL/ONCE sweep and the condition never fired. Use sweep='AUTO', lower the level, or raise the timeout.

Voltages exactly 10× wrong

The probe setting doesn't match the physical x1/x10 switch on the probe.

clipped: true, measurements look railed

Signal exceeds the ADC window — increase range or call scope_capture(autorange=True).

vamp/duty_cycle suddenly null after a dependency update

numpy was upgraded to 2.x — reinstall with pip install "numpy<2".

First call is slow (~5 s)

Normal: FPGA bitstream upload + calibration read on first connect.

Development

src/owon_mcp/
  server.py    FastMCP server, the 10 scope_* tools
  device.py    singleton device handle, lock, lazy connect, error mapping
  measure.py   Frame → measurements dict (defensive, never raises)
  render.py    Frames → oscilloscope-style PNG (matplotlib Agg)
vendor/vds1022/  vendored driver + FPGA firmware (do not edit)
tests/test_offline.py  13 mocked-device tests — run without hardware
.venv-win\Scripts\python.exe -m pytest -q      # offline test suite
.venv-win\Scripts\python.exe -m owon_mcp.server  # run the server manually (stdio)

Verified end-to-end on real hardware (VDS1022i, fw V5.0.1, FPGA v5): FPGA upload, channel/timebase/trigger config, 1 kHz cal-wave capture (f = 1000.0 Hz, duty 49.9%), PNG render, CSV export, clip detection.

Attribution & license

The USB driver in vendor/vds1022/ (and the FPGA firmware it uploads) is the excellent open-source work of florentbrflorentbr/OWON-VDS1022 — vendored unmodified. The upstream repo carries no explicit license file; this repository is private/personal-use. If you plan to make this public or redistribute it, clarify licensing with the upstream author first. See NOTICE.md.

The MCP wrapper code (src/owon_mcp/) is © James Milward.