Wire protocol¶
vstimd speaks Protocol Buffers (proto3) over ZMQ REQ/REP. Every client — the Python client, the web UI, or one you write yourself — encodes the same messages, so the protocol is the real contract for talking to the server.
Why ZeroMQ and protobuf¶
The transport is chosen for timing, not features:
- ZeroMQ gives low-latency messaging with very little overhead. It is a thin, brokerless layer over TCP with no authentication or session negotiation on the hot path, so a command round-trip costs little more than the socket itself. (The flip side: the server does no access control — keep it on a trusted rig network.)
- Binary messages. Commands and responses are serialized to compact binary rather than text (JSON/XML), keeping encode/decode time and wire size small — the point is to spend the frame's millisecond budget on rendering, not parsing.
- Protocol Buffers is the most widespread binary schema format, with mature code
generators for virtually every programming language. That means a client can be
written in Python, C#, C++, MATLAB, Go, Rust, … from the same
.protofiles, with no bespoke parser to maintain.
New to protobuf?
Protocol Buffers is Google's language- and platform-neutral serialization format. If you have not used it before, start here:
- Protocol Buffers overview — what it is and why.
- proto3 language guide — the schema syntax vstimd uses.
- Tutorials — generating code for Python, C++, C#, Go, and more.
The schema is the source of truth¶
The message definitions live in the repository and are the authoritative,
always-current reference — this page does not duplicate them, because a
hand-maintained message list drifts out of date. Read the .proto files directly:
| File | Contents |
|---|---|
service.proto |
Request / Response envelopes, ErrorCode |
system.proto |
Scene-wide commands, QueryServerInfoResponse, config persistence |
stimuli/ |
Create / mutate / query messages per stimulus type |
animations.proto |
Animation create / arm / query |
vtl.proto |
Virtual Trigger Line commands |
snapshot.proto |
Full scene snapshot pushed on the web UI's /events WebSocket |
Transport¶
- Socket type: ZMQ REQ/REP (synchronous request-reply)
- Address: the server binds
tcp://0.0.0.0:5555; clients connect totcp://<device>:5555 - Encoding: protobuf (proto3); every ZMQ frame carries exactly one
Request, answered by exactly oneResponse
Bind address gotcha
The server binds tcp://0.0.0.0:5555. Do not use tcp://*:5555 — the
zeromq crate resolves the host as DNS and that form fails.
The envelope, at a glance¶
Every Request carries a target (system for scene-wide commands, or a
stimulus handle) and a oneof body naming exactly one command. Every Response
carries a handle, an ErrorCode, and an optional query payload. The precise field
numbers and the full command set are in
service.proto;
the shape is:
message Request {
oneof target {
SystemTarget system = 1; // scene-wide commands
uint32 stimulus = 2; // per-stimulus commands (handle > 0)
}
oneof body {
CreateRectRequest create_rect = 10; // system target
SetPositionRequest set_position = 32; // stimulus target
// … one field per command; see service.proto
}
}
message Response {
int32 handle = 1; // new handle on Create*, -1 ack, 0 on error
ErrorCode code = 2; // ERROR_CODE_OK on success
string error = 3; // human-readable, only when code != OK
string id = 4; // UUID on Create*
// oneof body { … query payloads … }
}
A raw round-trip in Python¶
You rarely need this — the Python client wraps it — but it
shows the protocol end to end using the generated stubs and pyzmq:
import zmq
from vstimd._proto.vstimd.v1 import service_pb2, system_pb2
from vstimd._proto.vstimd.v1.stimuli import rect_pb2
ctx = zmq.Context()
sock = ctx.socket(zmq.REQ)
sock.connect("tcp://localhost:5555")
# Build a "create rectangle" request (system target).
req = service_pb2.Request(
system=service_pb2.SystemTarget(),
create_rect=rect_pb2.CreateRectRequest(width=300, height=150),
)
sock.send(req.SerializeToString())
resp = service_pb2.Response()
resp.ParseFromString(sock.recv())
assert resp.code == service_pb2.ERROR_CODE_OK
print("new stimulus handle:", resp.handle)
Regenerating stubs¶
The Python stubs under client/python/vstimd/_proto/ are generated from the
.proto files. Regenerate them with the client Makefile:
Error codes¶
ErrorCode values are defined in
service.proto.
Note that ERROR_CODE_OK is 1 (the proto3 zero value ERROR_CODE_UNSPECIFIED
marks an unset/malformed response). The Python client translates each non-OK code
into a typed exception — see Python client → Errors.
gRPC¶
service.proto also declares a VstimdService with Execute / ExecuteBatch
RPCs. The primary transport is ZMQ REQ/REP; the service block is included so tooling
(grpcurl, reflection) works and to keep the option of a gRPC adapter open without a
schema change. There is no gRPC listener today.