Metadata-Version: 2.4
Name: vstimd-client
Version: 0.3.0a1
Summary: Python client for the vstimd visual stimulus server (ZMQ + protobuf)
Author-email: Joscha Schmiedt <joscha@schmiedt.dev>
License-Expression: LGPL-3.0-only
Project-URL: Homepage, https://github.com/braemons/vstimd
Project-URL: Repository, https://github.com/braemons/vstimd
Project-URL: Bug Tracker, https://github.com/braemons/vstimd/issues
Project-URL: Changelog, https://github.com/braemons/vstimd/blob/main/client/python/CHANGELOG.md
Keywords: psychopy,visual-stimulus,neuroscience,vision-science,zmq,zeromq,protobuf
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Multimedia :: Graphics
Classifier: Typing :: Typed
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: LICENSE.GPL
Requires-Dist: pyzmq>=25
Requires-Dist: protobuf>=5
Requires-Dist: zeroconf>=0.132
Provides-Extra: discover
Provides-Extra: tui
Requires-Dist: textual>=0.80; extra == "tui"
Dynamic: license-file

# 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

```bash
pip install vstimd-client
```

Or with [uv](https://docs.astral.sh/uv/):

```bash
uv add vstimd-client
```

### Development install

```bash
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

```python
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`:

```python
# Before
from psychopy import visual

# After
from vstimd.psychopy import visual
```

The only required addition is `address=` on `Window`:

```python
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:

```bash
pip install vstimd-client
```

```console
$ 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](https://pypi.org/project/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](https://github.com/braemons/vstimd/blob/main/client/python/docs/cli.md)
for details.

## Regenerating protobuf stubs

```bash
cd client/python
make proto   # requires grpcio-tools in the dev dependency group
```

## Tests

```bash
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:

```bash
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`/`G` jump to the ends, `/` searches ids and
  captions.
- `⏎` runs the selected test and steps on; `space` runs it and stays; `r`
  replays it; `a` runs from here to the end until you hit `s`.
- `f` flags the test and asks what was wrong with it, `u` takes the flag back.
- `v` cycles the side panel between the test's own detail, the live scene
  (every stimulus the server holds) and the trigger lines — where `t` toggles a
  line and `p` pulses it, so a trigger-driven scene can be exercised with no DAQ
  attached.
- `w` writes the notes out; quitting writes them too. They land in
  `e2e-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:

```python
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.
