This release is a pre-release and may not be stable for production use.
vstimd-client
Python client for the vstimd visual stimulus server. Talks to the server
over ZMQ using protobuf encoding.
The distribution is vstimd-client; the import package is vstimd.
Install
pip install vstimd-client
Or with uv:
uv add vstimd-client
Development install
cd client/python
uv sync
make proto # the protobuf stubs are generated, not committed
A checkout without generated stubs imports as an empty shell, so run make proto (every other make target does it for you) before working from source.
Quick start
from vstimd import Connection
from vstimd.stimuli import Color, RectParams, ShapeAppearance, Vec2
with Connection() as conn:
h = conn.stimuli.shapes.create_rect(
position_px=Vec2(-200, 0),
params=RectParams(width_px=300, height_px=200,
appearance=ShapeAppearance(fill_color=Color(1.0, 0.0, 0.0))),
)
conn.stimuli.set_enabled(h, False)
conn.stimuli.delete(h)
info = conn.system.query_server_info()
print(info.version)
Connection(address="tcp://localhost:5555") — default address shown.
vstimd.psychopy — PsychoPy-compatible layer
Drop-in replacement for psychopy.visual:
# Before
from psychopy import visual
# After
from vstimd.psychopy import visual
The only required addition is address= on Window:
win = visual.Window(address='tcp://192.168.1.10:5555')
circ = visual.Circle(win, radius=50, fillColor='red')
rect = visual.Rect(win, width=200, height=100, fillColor=(-1, 1, -1))
grat = visual.GratingStim(win, sf=0.05, mask='circle')
circ.draw()
win.flip()
Implemented classes
| Class | Notes |
|---|---|
Window |
Owns the Connection; flip() flushes the command queue |
Rect |
create_rect, position, size, fill color, rotation_deg, alpha |
Circle |
create_circle, position, radius, fill color, rotation_deg, alpha |
GratingStim |
create_grating, all grating parameters; mask accepts 'circle', 'gauss', 'raisedCos' |
All constructor arguments from psychopy.visual are accepted. Parameters that
have no server-side equivalent (autoLog, depth, interpolate, etc.) are
accepted and silently ignored for drop-in compatibility.
Deferred (frame-buffer) mode
By default (deferred=True) property changes are sent to the server's deferred
queue immediately; win.flip() tells the server to apply the entire queue
atomically before the next vsync. Set deferred=False to apply each command
immediately as it arrives.
Color formats accepted
Named strings ('red'), hex strings ('#ff0000'), PsychoPy rgb tuples
(-1..1), plain 0..1 tuples, rgb255 tuples, and scalar greyscale values.
vstimd-client — command-line tool
Installing the package also installs a vstimd-client executable for the
system-level commands, plus mDNS discovery of servers on the local network:
pip install vstimd-client
$ vstimd-client discover
ID HOSTNAME ADDRESSES ADDRESS
vstimd-a1b2c3 vstimd-a1b2c3.local 10.0.1.42 tcp://vstimd-a1b2c3.local:5555
$ vstimd-client --host vstimd-a1b2c3 info
version 0.4.1
resolution 1920x1080
frame rate 60.00 Hz
background 0.000 0.000 0.000 1.000
Discovery browses for _vstimd._tcp using the
zeroconf package, which is installed
alongside the client, and falls back to avahi-browse if it is somehow
missing. The ID column is the server's
id= TXT record — the reliable identity, unlike the display name which Avahi
may suffix with #2 on collision.
Other commands: ls, background, delete-all, enable-all/disable-all,
wait-frames, wait-ready, shutdown, and config list|save|load|get|upload.
Run vstimd-client with no arguments for the grouped list.
The target server comes from --address, --host, $VSTIMD_ADDRESS, or — if
none of those is given — from mDNS: the single rig on the network is used, and
several means you are asked which (--non-interactive fails instead of
asking). --address fills in what you leave out, so -a 10.0.1.42 and
-a tcp://10.0.1.42:5555 mean the same thing. --json
makes every command emit machine-readable output, and failures are
distinguished by exit code (3 unreachable, 4 timed out, 5 server error,
6 not found) rather than by parsing stderr. See
the CLI docs
for details.
Regenerating protobuf stubs
cd client/python
make proto # requires grpcio-tools in the dev dependency group
Tests
cd client/python
# Unit tests (no server required)
make test
# E2E against the null renderer — no display, no GPU (builds the server for you)
make test-e2e-null
# E2E on a real display, start to finish
make test-e2e-visible
# The same, one test at a time, to review by eye and flag what looks wrong
make test-e2e-interactive
# Any of them against a server elsewhere
VSTIMD_SERVER=tcp://192.168.1.10:5555 make test-e2e-visible
Watching the on-screen suite
make test-e2e-visible renders for real, and every test captions itself in yellow near
the top of the display:
[GRAT-09] mask — none, circle, gaussian, hann, raised cosine, left to right
The id in front is stable and belongs to that test alone, so anything that does
not look right can simply be written down as its id and found again with
grep -rn GRAT-09 client/python/tests/e2e. The prefix names the area — RECT,
CIRC, ELLI, POLY, SHAPE, GRAT, TEXT, ANIM, SHARED, QUERY,
SYS, CFG, VTL, DEMO, and PSY for the PsychoPy-compatible API.
Each test holds its scene on screen long enough to be judged. --step-delay
sets that dwell in seconds (default 1.0); the null suites pin it to 0, so
headless runs pay nothing for it:
make test-e2e-visible PYTEST_ARGS="--step-delay 2.5" # slower, easier to watch
Reviewing it by hand
make test-e2e-interactive opens a terminal UI over the same suite. It lists every
test with what it should put on screen; you pick one, run it, watch the display,
and flag it if it looks wrong:
┌ vstimd on-screen review ──────────────────────────────────────────────────┐
│ # id should show │ test │ scene │ triggers │ │
│ 1 ANIM-01 a dark red 80×80 px squ… │ │
│ ▸2 ANIM-02 ✓ a red square left of ce… │ [ANIM-02] test 2 of 147 │
│ 3 ANIM-03 a red square right of c… │ │
│ 4 ANIM-04 nothing visible below c… │ should show: a red square left │
│ 5 ANIM-05 a red square in the cen… │ of centre, on for the 60 frames │
│ │ the flash runs… │
├───────────────────────────────────────────────────────────────────────────┤
│ 1280×720 @ 60.0 Hz v0.1.0 background (0.00, 0.00, 0.00) │
│ ▉▉▉▉░░░░░░░░░░░░░░░░ 2/147 run 0 flagged [ANIM-02] passed │
└ ⏎ run+next space run r replay a run from here f flag v panels q quit ┘
j/k(or the arrows) move,g/Gjump to the ends,/searches ids and captions.⏎runs the selected test and steps on;spaceruns it and stays;rreplays it;aruns from here to the end until you hits.fflags the test and asks what was wrong with it,utakes the flag back.vcycles the side panel between the test's own detail, the live scene (every stimulus the server holds) and the trigger lines — wherettoggles a line andppulses it, so a trigger-driven scene can be exercised with no DAQ attached.wwrites the notes out; quitting writes them too. They land ine2e-review.md: each note, what the test claims should be visible, and a ready-made command to re-run just the flagged tests.
pytest still does the work underneath — collection, fixtures, reporting — but
the app decides what runs and when, so a test can be repeated or gone back to,
which a plain pytest run cannot do. The app also starts its own windowed
server (1280×720; --fullscreen for the real thing) so this terminal stays
visible, and reuses a server that is already running.
The scene and trigger panels are vstimd.tui widgets, packaged for reuse:
from vstimd import Connection
from vstimd.tui import ServerStatus, StimulusList, TriggerLines
They need the tui extra (pip install "vstimd-client[tui]").
Status and versioning
Pre-1.0: the API may still change between minor versions.
Release candidates (0.1.0rc1) are published to PyPI. While no final release
exists yet, pip install vstimd-client resolves to the newest candidate — pip
falls back to pre-releases only when nothing stable satisfies the request. Once
0.1.0 is out, that same command gives you the stable version, and picking up
later candidates needs pip install --pre vstimd-client.
License
GNU LGPLv3. Copyright © 2026 Joscha Schmiedt, University of Bremen.
The client is licensed more permissively than the vstimd server itself, which is AGPLv3: importing this package into your experiment does not place your experiment under copyleft.
Release files for vstimd-client 0.2.0a1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| vstimd_client-0.2.0a1.tar.gz | 122.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| vstimd_client-0.2.0a1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 289.9 kB
Release files / vstimd_client-0.2.0a1.tar.gz
| Download URL | vstimd_client-0.2.0a1.tar.gz |
|---|---|
| Size | 122.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9f1e02f812ec52542b31913531a81ee0d077389434b8f91d4507e7bd836451b0
|
|
BLAKE2b-256 checksum How to use checksums |
0b012414c00b0e92ed42dca829c237bfb8becc34eee5739107ad6986f047942f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / vstimd_client-0.2.0a1-py3-none-any.whl
| Download URL | vstimd_client-0.2.0a1-py3-none-any.whl |
|---|---|
| Size | 167.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
cc5e89e60fce7c526178a9968cb49fc8fd9c7eb57f3cf2b37b7fac3ba38f0925
|
|
BLAKE2b-256 checksum How to use checksums |
e0e2ce7aafce775a7a0207fc711ba9407e8c4bf3639c67fc0e85c7122a2d5b9e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|