dslogic MCP Server
by heng5411
README.md
# dslogic-tool
Python driver, CLI and MCP server for DreamSourceLab DSLogic U3Pro32/U3Pro16 logic analysers: capture, trigger, decode SPI/I2C/UART, save DSView .dsl files, and let an AI assistant (Claude, etc.) drive captures over the Model Context Protocol.
## What is this?
**A Python replacement for DSView's capture side: the same analyser, the same USB protocol and the same `.dsl` files, driven from a script, a command line or an AI assistant.**
- Works like a printer driver: DSView's own C driver (libsigrok4DSL), ported line by line to Python, talks to the analyser over USB.
- sigrok and PulseView do not support the U3 series; this tool does.
- Captures are saved as DSView `.dsl` files with channel names, colours and decoders, so they open in DSView ready to read.
- An MCP server lets an AI assistant such as Claude Code capture, decode and measure on its own.
- Pure Python 3.14+, Windows first, Linux and WSL2 too.
## The big picture
```mermaid
flowchart LR
LA["DSLogic U3Pro32 / U3Pro16"] -- USB 3 --> DRV["dslogic driver<br/>(Python port of libsigrok4DSL)"]
FW["FPGA bitstream<br/>from DSView install"] -. loaded at open .-> DRV
DRV --> API["Python API<br/>DSLogic, Capture"]
API --> CLI["dslogic command line"]
API --> MCP["MCP server<br/>dslogic mcp"]
API --> VIEW["Waveform viewer<br/>dslogic view"]
MCP <--> AI["AI assistant<br/>Claude Code, Claude Desktop"]
API --> FILES[".dsl .sr .vcd .csv .npz"]
```
- Solid arrow = data. Dashed arrow = a file read once when the analyser opens.
- The Python API does the work; the command line, the MCP server and the viewer are thin layers on top of it.
- The FPGA bitstream is DreamSourceLab's and is not in this repository (see [FPGA bitstream](#fpga-bitstream)).
## Install
### Windows
- Install Python 3.14 or newer from python.org.
- Install [DSView](https://www.dreamsourcelab.com/download/) once and check it sees the analyser. Its installer binds the WinUSB driver that libusb needs, and its `res` folder holds the FPGA bitstream.
- Close DSView before using this tool: only one program can hold the analyser.
- In the project folder:
```bat
py -3.14 -m venv .venv
.venv\Scripts\activate
pip install -e .[view,mcp,test]
dslogic scan
```
- Extras: `view` = viewer (pyqtgraph, PySide6, matplotlib), `mcp` = MCP server (mcp 2.x SDK), `test` = pytest and everything above.
- If `dslogic scan` lists the analyser but opening fails with "no WinUSB/libusb driver bound", bind WinUSB to it with [Zadig](https://zadig.akeo.ie/). DSView keeps working with WinUSB.
### Linux
```sh
uv venv --python 3.14 .venv && . .venv/bin/activate
uv pip install -e .[view,mcp,test]
echo 'SUBSYSTEM=="usb", ATTRS{idVendor}=="2a0e", MODE="0666"' | sudo tee /etc/udev/rules.d/60-dslogic.rules
sudo udevadm control --reload && sudo udevadm trigger # let normal users open the analyser
```
### WSL2
- A native Windows Python is simpler and faster; WSL can start it directly (see [MCP server](#mcp-server)).
- To use the analyser inside WSL instead, attach it from Windows:
```powershell
usbipd list
usbipd attach --wsl --busid <BUSID>
```
### FPGA bitstream
- The analyser needs DreamSourceLab's FPGA bitstream, `DSLogicU3Pro32.bin` or `DSLogicU3Pro16.bin`, loaded at every open.
- These files belong to DreamSourceLab and are **not included in this repository**. Get them from a [DSView](https://www.dreamsourcelab.com/download/) install (its `res` folder) or from the [DSView repository](https://github.com/DreamSourceLab/DSView/tree/master/DSView/res).
- With DSView installed in its default place, nothing else is needed. The tool looks in this order:
1. `firmware_dir=` in Python, `--firmware-dir` on the command line
2. the folder in the `DSLOGIC_RES` environment variable
3. `dslogic/res/` in this package (git ignores `.bin` files there)
4. DSView's install: `C:\Program Files\DSView\res` (or `Program Files (x86)`); `/usr/share/DSView/res`, `/usr/local/share/DSView/res` or `/mnt/c/Program Files/DSView/res` on Linux and WSL
- The analyser's USB firmware is in its own flash, so nothing else is uploaded.
## Three ways to use it
### Python
```python
from dslogic import DSLogic, SPI
with DSLogic.open() as la: # loads the FPGA on first use
la.samplerate = "250M"
la.duration = "5ms"
la.threshold = 1.65 # volts: half of a 3.3 V supply
la.enable_channels([0, 1, 2, 3])
la.set_names({0: "CS", 1: "SCLK", 2: "MISO", 3: "MOSI"})
la.set_trigger("CS", "falling")
la.trigger_position = 10 # % of the capture kept before the trigger
la.add_decoder(SPI(clk="SCLK", mosi="MOSI", miso="MISO", cs="CS"))
cap = la.capture(timeout=10)
for t in cap.decoders[0].transfers(cap):
print(cap.time_of(t.start), t.text) # seconds relative to the trigger
cap.save("spi.dsl") # opens in DSView with names and decoder
```
- More in `examples/`: SPI capture, UART streaming, multi-stage trigger.
### Command line
```bat
dslogic capture -r 250M -t 5ms -c 0-3 --name 0=CS,1=SCLK,2=MISO,3=MOSI --threshold 1.65 --trigger CS=falling --pos 10 --decoder spi:clk=SCLK,mosi=MOSI,miso=MISO,cs=CS -o spi.dsl -p
dslogic decode spi.dsl
dslogic view spi.dsl
```
- While a capture runs, **Ctrl-X** stops it and saves what was captured; **Ctrl-C** stops the analyser and exits without saving.
### MCP server
- `dslogic mcp` runs the tool as an [MCP](https://modelcontextprotocol.io) (Model Context Protocol) server over stdio, so an AI assistant can list analysers, capture, decode, measure and render waveforms into the chat.
- Needs the `mcp` extra. Register it with Claude Code on Windows, or on Linux with the analyser attached:
```sh
claude mcp add dslogic -- <venv python> -m dslogic mcp
```
- Claude Code in WSL with the analyser on Windows (WSL starts the Windows Python):
```sh
claude mcp add dslogic -- /mnt/c/<path>/dslogic-tool/.venv/Scripts/python.exe -m dslogic mcp
```
- An MCP client stops waiting after its tool timeout (Claude Code: `MCP_TOOL_TIMEOUT`, in ms). Set it above the longest capture `timeout` in use.
## Reference
<ol>
<li>
<details>
<summary>Settings</summary>
| Setting | API | Notes |
|---|---|---|
| Sample rate | `la.samplerate = "250M"` | DSView's list, 10 Hz to 1 GHz; `la.samplerates()` lists them |
| Capture length | `la.sample_count = "10M"` or `la.duration = "20ms"` | Buffer mode: at most `la.max_samples()` (2 Gbit ÷ enabled channels) |
| Operation mode | `la.operation_mode = "buffer"` / `"stream"` | Buffer: stored in the analyser, then uploaded. Stream: sent live, length limited by PC RAM or disk |
| Channel mode | automatic, or `la.channel_mode = 24` | `la.channel_modes()` lists them: buffer 32 ch @ 250 MHz, 16 ch @ 500 MHz, 8 ch @ 1 GHz; stream 32 ch @ 50 MHz … 3 ch @ 1 GHz (USB 3) |
| Threshold | `la.threshold = 1.65` | Volts, 0–5; written to the hardware at once |
| Enable / disable | `la.enable_channels([0, 1])`, `la.set_enabled(5, False)`, `la.disable_channels([..])` | |
| Channel name | `la.set_name(0, "CS")`, `la.set_names([...])` | A name works anywhere a channel index does |
| Channel colour | `la.set_color("CS", "red")`, `"#rrggbb"` or `(r, g, b)` | Saved into `.dsl` for DSView |
| Simple trigger | `la.set_trigger("CS", "falling")` | `rising`, `falling`, `high`, `low`, `edge`, `X`/none; conditions on several channels are ANDed |
| Trigger position | `la.trigger_position = 10` | Percent; the hardware caps it at 90 % (buffer) or 10 % of depth (stream) |
| No trigger | `la.clear_trigger()` | Starts at once |
| Advanced trigger | `la.set_advanced_trigger([Stage(...), ...])` | Up to 16 stages; each has two conditions, `op="and"/"or"`, `inv0/inv1`, `count`, `contiguous` |
| Serial trigger | `la.set_serial_trigger(SerialTrigger(...))` | Experimental |
| Glitch filter | `la.filter = True` | DSView's "1 Sample Clock" filter |
| External clock | `la.external_clock = True`, `la.clock_negedge = True/False` | |
| Stop behaviour | `la.stop_option = "upload"` / `"stop"` | Buffer mode, on timeout or stop: upload what was captured, or discard it |
| Decoders | `la.add_decoder(SPI(...))`, `la.remove_decoder("SPI")` | Copied to each capture and saved to `.dsl` |
| Save / load settings | `la.save_settings("bench.json")`, `la.load_settings(...)` | Everything above, as JSON |
| Repeat a DSView capture | `la.load_settings("old_capture.dsl")` | Reads the setup DSView stored in a `.dsl` or settings file: mode, rate, count, threshold, channels, triggers, decoders |
| Large captures | `la.spool_dir = r"D:\scratch"`, `la.spool = "auto"/"always"/"never"` | Captures bigger than half the free RAM go to disk as they arrive (deleted when Python exits) |
| Summary | `print(la.describe())` | |
- `la.capture(timeout=None, progress=None)` blocks until done; `progress(triggered, fraction)` is called about five times a second.
- Ctrl-C, `timeout` or `la.stop()` from another thread ends a capture early. `la.stop()` keeps the data; `la.cancel()` or Ctrl-C raises `CaptureCancelled`.
- Buffer mode with `stop_option="upload"` returns the samples captured so far. In stream mode a USB overflow raises `OverflowError_`.
</details>
</li>
<li>
<details>
<summary>Captures</summary>
```python
cap[0], cap["CS"] # numpy uint8 0/1 array of one channel
cap.samples("CS", 1000, 2000) # window
cap.edges("SCLK", "rising") # sample indices of edges
cap.time_of(i) # seconds relative to the trigger
cap.trigger_pos, cap.triggered, cap.samplerate, cap.num_samples
cap.set_name(...), cap.set_color(...), cap.add_decoder(...)
cap.decode(SPI(...)) # -> [Annotation(start, end, kind, data, text)]
cap.pulse_at("SCLK", sample), cap.measure("SCLK", a, b)
cap.slice(start, stop)
cap.save("x.dsl" | "x.sr" | "x.vcd" | "x.csv" | "x.npz")
Capture.load("x.dsl") # also reads files saved by DSView
```
- Channels are kept packed, 1 bit per sample, so long captures stay small in memory.
</details>
</li>
<li>
<details>
<summary>Decoders</summary>
```python
SPI(clk, mosi=None, miso=None, cs=None, cpol=0, cpha=0, bitorder="msb-first", wordsize=8,
cs_polarity="active-low") # .transfers(cap) groups words per CS window
I2C(scl, sda, address_format="unshifted")
UART(rx, baudrate=115200, num_data_bits=8, parity_type="none", num_stop_bits=1.0,
bit_order="lsb-first", invert=False) # .text(cap) returns the decoded string
DSViewDecoder("can", {"can_rx": 5}, {"bitrate": 500000}, label="CAN") # DSView only
```
- SPI, I²C and UART decode in Python and are also written to the `.dsl` with DSView's ids (`0:spi`, `1:i2c`, `1:uart`).
- `DSViewDecoder` adds any decoder from DSView's `libsigrokdecode4DSL/decoders` folder to a saved file; DSView decodes it on open.
- `SPI(...).mode` and `.settings_text()` give the SPI mode, CPOL, CPHA, bit order and word size.
</details>
</li>
<li>
<details>
<summary>Command line</summary>
```bat
dslogic scan
dslogic info
dslogic capture -r 250M -t 5ms -c 0-3 --name 0=CS,1=SCLK,2=MISO,3=MOSI --color CS=yellow ^
--threshold 1.65 --trigger CS=falling --pos 10 ^
--decoder spi:clk=SCLK,mosi=MOSI,miso=MISO,cs=CS -o spi.dsl -o spi.vcd -p
dslogic capture -m stream -r 10M -t 2s -c 6 --decoder uart:rx=6,baud=115200 -o uart.dsl -p
dslogic capture --settings bench.json -o run.dsl :: reuse saved settings
dslogic info --settings D:\captures\old.dsl :: show the setup stored in a DSView capture
dslogic capture --settings D:\captures\old.dsl --spool-dir D:\captures -o new.dsl
dslogic decode run.dsl --decoder i2c:scl=4,sda=5 :: decode a saved file
dslogic decode run.dsl --decoder "dsview:can:can_rx=5;bitrate=500000"
dslogic convert run.dsl run.vcd --name 4=SCL,5=SDA
dslogic convert run.dsl part.dsl --start 1.20s --end 1.30s --channels 0-7 :: crop for DSView
dslogic view run.dsl --start 1.20s --end 1.21s --channels 0-3
dslogic view run.dsl --png run.png :: image, no window
```
- `--settings old.dsl` repeats the setup of a DSView capture; Ctrl-C then stops it like DSView's Stop and keeps the data.
- `-p` shows a progress bar; `-vv` logs every USB step.
- `dslogic decode` prints each decoder's settings above its output.
</details>
</li>
<li>
<details>
<summary>MCP tools</summary>
| Tool | Does |
|---|---|
| `scan`, `info` | List analysers; settings and channel modes of one |
| `capture` | Capture with typed settings (rate, length, channels, names, mode, threshold, trigger, position, decoders, settings file, timeout, outputs); reports progress; returns summary, timing and the first decoded lines |
| `stop_capture` | End a running capture and keep the data (DSView's Stop) |
| `load` | Open a saved `.dsl`/`.npz` and return its rate, channels, trigger and decoders |
| `decode` | Annotations as data (time from the trigger, samples, text, values); window, kind filter, other decoders |
| `measure`, `pulse`, `edges` | Frequency, duty and periods over a window; the pulse at a time; edge times and gaps |
| `render` | The waveform as an image, returned into the chat (optionally saved as PNG) |
| `convert` | Save as `.dsl` `.sr` `.vcd` `.csv` `.npz`, cropped, renamed, with decoders |
| `view` | Open the interactive viewer on the desktop |
- Loaded captures stay in memory between calls (the last four files, reloaded when changed), so several queries on a multi-GB capture pay for one load.
- A tool that takes `file` uses the last captured or loaded capture when `file` is left out.
- Paths may be given as `D:\x.dsl` or `/mnt/d/x.dsl`; the server converts them for its host.
</details>
</li>
</ol>
## Waveform viewer
- `dslogic view` (or `--view` on a capture, which skips the reload) opens a `.dsl` in seconds even at many GB, where DSView decompresses every sample first.
- It reads only the 2 MiB blocks it needs, draws one min/max per pixel, and keeps a cached three-level overview, so any zoom level draws in a few milliseconds.
- Decoded words are drawn like a bus on a scope: each word spans exactly the bit times it was read from, and zooming in shows the bit boundaries and then every bit.
<ol>
<li>
<details>
<summary>Mouse and keys</summary>
| Action | Does |
|---|---|
| Left-drag on the waveform | Zoom to the region (or pan or measure, set by the Zoom / Pan / Measure switch, keys Z, P, M) |
| Right-drag or middle-drag | Pan |
| Right-click | Menu: fit, go to trigger, zoom here, add cursor, pin pulse, copy time, clear marks, screenshot, save visible range. On a cursor: delete it. On a row name: hide, move, show hidden rows |
| Wheel / Shift+wheel | Zoom at the pointer / scroll left-right |
| Double-click | Add a cursor; Δt and 1/Δt between neighbours are shown. Drag a cursor to move it |
| Hover | Time and all channel levels in the status bar; the pulse under the pointer lights up with its width, frequency and duty |
| Click | Pin that pulse measurement |
| Shift+drag along a channel | Measure between two edges: Δt, 1/Δt, edges, periods, average frequency, duty |
| Drag a row name | Reorder rows |
| Hover a decoded word | Its time span and sample points light up; the status bar shows it in hex, decimal and bits |
| Format pill on a decoder name | Show values as hex (default), decimal, octal, binary or ASCII |
| Double-click a row name | Rename it in place (Enter keeps, Esc cancels); renaming a channel also updates its decoder rows |
| Drag the name column's edge | Resize the column; double-click the edge for the default width |
| Round button, bottom right | Fit all, go to trigger, add cursor, clear marks, screenshot, save range, light/dark theme, About |
| `f`, `t`, `+`/`-`, arrows, `c`, Esc | Fit, trigger, zoom, pan, clear cursors and measurements, close the button menu |
</details>
</li>
<li>
<details>
<summary>Look and settings</summary>
- Layout: a top bar (file name, size, mode switch, a status chip that spins while a view is prepared), the name column and waveforms, and a status bar with the pointer readout, measurement and cursor intervals.
- SPI decoders get two rows, MOSI and MISO, placed under the channels they decode, in that channel's colour, with the SPI mode under the name. Hovering a decoder name shows all its settings.
- Theme: light or dark, following the system until one is picked on the round button. Colours follow VS Code's "Modern" greys with iOS system colours.
- The round button is liquid glass that blurs the waveform under it. On Windows 11 the window is slightly translucent over the system backdrop.
| Environment variable | Effect |
|---|---|
| `DSLOGIC_THEME=light` or `dark` | Fixes the theme (in Python: `QtViewer(cap, theme="light")`) |
| `DSLOGIC_WINDOW_GLASS=0` | Solid window background on Windows 11 |
| `DSLOGIC_REDUCED_TRANSPARENCY=1` | Solid glass (also follows Windows "Transparency effects") |
| `DSLOGIC_REDUCED_MOTION=1` | No animations (also follows Windows "show animations") |
</details>
</li>
<li>
<details>
<summary>Performance on large captures</summary>
- The overview holds min/max per 4 Ki, 256 Ki and 16 Mi samples. It is built in the background on all cores when a file opens and fills in from left to right.
- First open of a 57 s, 8-channel, 250 MHz capture: about 6 s. The overview is then cached (under `%LOCALAPPDATA%\dslogic\overview`, a few hundred KB), so the file reopens in a fraction of a second.
- The same pass records an edge index, so decoders read edges without decompressing: decoding two SPI buses over that 57 s capture takes about 0.5 s (about 53 s without the index).
- A zoom or pan step, including the repaint, takes about 10–15 ms on pyqtgraph (Qt); detail for the new view is computed in a background thread.
- `--backend mpl` uses the older matplotlib viewer, which also draws `--png` images.
- `.vcd` output is written from edges, so it stays small for bus traffic and opens fast in GTKWave or Surfer.
</details>
</li>
</ol>
## Status and limits
- Buffer-mode captures run on a real U3Pro32, including through the MCP server.
- 114 tests run against a simulated analyser: USB command order, arm block layout, trigger words, data interleaving, early stop, stream overflow, and randomised capture-to-file round trips.
- U3Pro16 support follows DSView's driver but has not been run on a real U3Pro16.
- The serial trigger is encoded exactly as DSView's GUI does it, but is experimental.
- Only logic-analyser mode is implemented; the U3 series has no scope mode.
- A capture needs samples × channels ÷ 8 bytes in RAM, or on disk when spooled: a 60 s stream at 250 MHz × 8 channels is 15 GB. SPI decoding and edge search work in chunks on such captures; I²C and UART decoding and `.vcd`/`.csv` export load whole channels and suit shorter captures.
- To report a failure, run the command with `-vv` and include the log.
Run the tests:
```sh
pytest
```
<ol>
<li>
<details>
<summary>Words explained</summary>
| Term | Meaning |
|---|---|
| DSLogic U3Pro32 / U3Pro16 | DreamSourceLab's USB 3 logic analysers with 32 or 16 channels |
| DSView | DreamSourceLab's own capture program; its driver is the source this tool was ported from |
| libsigrok4DSL | The C driver inside DSView, DreamSourceLab's fork of sigrok's libsigrok |
| FPGA bitstream | The configuration file loaded into the analyser's FPGA each time it opens |
| `.dsl` | DSView's capture file format: compressed samples plus the session setup |
| Buffer mode | The analyser stores the capture in its own memory, then uploads it |
| Stream mode | Samples are sent to the PC live; length is limited by PC RAM or disk |
| Trigger position | The share of the capture kept from before the trigger fired |
| Spool | Writing a capture to disk as it arrives instead of holding it in RAM |
| MCP | Model Context Protocol: a standard way for an AI assistant to call tools such as this server's `capture` and `decode` |
| WinUSB | The Windows USB driver that libusb (and DSView) use to reach the analyser |
</details>
</li>
</ol>
## License and credits
- GPL-3.0-or-later (see `LICENSE`), as a derivative of DSView / libsigrok4DSL (GPL-3.0).
- The FPGA bitstreams are DreamSourceLab's and are not distributed here.
- Built on DSView / libsigrok4DSL by DreamSourceLab (USB protocol, `.dsl` format), pyqtgraph, Qt for Python, NumPy and python-libusb1. The viewer's About sheet shows the same credits.
- Author: Wong Yiek Heng (yiekheng@04080616.xyz).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues