pyvterm
Drive a PiTrex/Vectrex over a serial port from Python.
pyvterm speaks the USB-DVG / vecterm serial protocol — the same wire format a custom MAME build uses to push vector frames to a Vectrex through the PiTrex. With pyvterm your Python program becomes the "custom MAME": you build a frame of vectors and stream it to real hardware over a serial link.
The protocol is documented in full in docs/PROTOCOL.md;
docs/PROTOCOL-EXTENSIONS.md proposes
backward-compatible extensions for streaming structured, high-vector-count
content (raster video, spectra, parametric curves).
How it fits together
┌─────────────────────────┐ USB-DVG / vecterm ┌──────────────────┐ GPIO/VIA ┌─────────┐
│ your Python program │ protocol over a │ PiTrex running │ 6522 VIA │ Vectrex │
│ (pyvterm) ── or ── │ ─────────────────────▶ │ the "vecterm" │ ───────────▶ │ CRT │
│ a custom MAME build │ serial @ 2 Mbaud │ receiver │ │ (beam) │
└─────────────────────────┘ └──────────────────┘ └─────────┘
pyvterm implements the sender half — exactly what
VMMenu/Win32/dvg/zvgFrame.c
does in the PiTrex repository (USB-DVG drivers by Mario Montminy, 2020), cross-checked
against AdvanceMAME's canonical advance/osd/dvg.c.
Install
pip install pyvterm
pyvterm requires Python 3.9+ and depends only on pyserial.
Quick start
from pyvterm import VectorTerminal
# Open the serial link (USB-CDC device shows up as /dev/ttyACM0 on Linux).
with VectorTerminal(port="/dev/ttyACM0") as vt:
with vt.frame(): # clears, then sends on exit
vt.set_intensity(15) # full brightness (0 = beam off)
vt.polyline( # a centred square
[(-200, -200), (200, -200), (200, 200), (-200, 200)],
closed=True,
)
No hardware handy? Swap in a MemoryTransport and inspect the bytes:
from pyvterm import VectorTerminal, MemoryTransport, protocol
mem = MemoryTransport()
vt = VectorTerminal(transport=mem)
vt.set_intensity(15)
vt.draw_to(100, 0) # pen starts at (0, 0)
frame = vt.send_frame()
print([protocol.decode_word(int.from_bytes(frame[i:i+4], "big"))
for i in range(0, len(frame), 4)])
Coordinate system
The default host space matches MAME's vector resolution and the PiTrex zvgFrame.h
defaults: X ∈ [−512, 511], Y ∈ [−384, 383], origin at centre. pyvterm maps these
onto the device's 0..4095 grid for you. Pass a custom Bounds to VectorTerminal
if you want different limits.
The Vectrex CRT is monochrome, so colour is really intensity: use
set_intensity(0..15). A colour/intensity of 0 blanks the beam, turning the next
vector into an invisible move. (set_rgb(r, g, b) is available for protocol fidelity
with colour vector monitors.)
API at a glance
| pyvterm | zvgFrame.c equivalent |
Purpose |
|---|---|---|
VectorTerminal(port=...) / .open(port) |
zvgFrameOpen |
open the serial link |
.set_rgb(r, g, b) / .set_intensity(n) |
zvgFrameSetRGB15 |
set colour/brightness |
.set_clip_window(...) |
zvgFrameSetClipWin |
set the clip rectangle |
.vector(x0, y0, x1, y1) |
zvgFrameVector |
add one vector |
.move_to / .draw_to / .polyline |
— | pen-style convenience helpers |
.send_frame() |
zvgFrameSend |
serialise + transmit the frame |
.close() |
zvgFrameClose |
send EXIT, close the port |
Lower-level building blocks are exposed too: pyvterm.protocol (pure word
encoders/decoders), pyvterm.geometry (clipping), FrameBuilder (assemble a frame
to bytes without any I/O), and the Transport hierarchy
(SerialTransport, MemoryTransport). The optional pyvterm.preview module
(pip install "pyvterm[preview]") decodes frames back into beam segments and
renders them to images or animated PNGs — handy for previewing without hardware.
Examples
Lissajous patterns
examples/lissajous.py animates Lissajous curves on the
display:
Animated preview (open the PNG to play it) rendered by --preview.
# On real hardware:
python examples/lissajous.py --port /dev/ttyACM0
# Without hardware (prints per-frame byte counts):
python examples/lissajous.py --dry-run --frames 5
# Render the animated PNG above (no hardware; needs the preview extra):
pip install "pyvterm[preview]"
python examples/lissajous.py --preview lissajous.png
3D rotating cube
examples/cube3d.py flies a wireframe cube around the
screen: it tumbles on all three axes, drifts along a Lissajous path, and swings
toward and away from the viewer so perspective makes it loom up close and shrink
into the distance. The projection is plain trigonometry — no numpy — so it runs
from the core package alone (only --preview needs the preview extra).
Animated preview (open the PNG to play it) rendered by --preview — exactly
the vectors the device would draw, reconstructed from the wire bytes. The motion
loops seamlessly over one --period.
# On real hardware:
python examples/cube3d.py --port /dev/ttyACM0
# Without hardware (prints per-frame byte/vector counts and distance):
python examples/cube3d.py --dry-run --frames 5
# Render the animated PNG above (no hardware; needs the preview extra):
pip install "pyvterm[preview]"
python examples/cube3d.py --preview cube3d.png
3D spectrum analyzer
examples/spectrum3d.py is a real-time 3D waterfall
spectrum analyzer: it captures live audio (ALSA), runs an FFT each frame, and
draws frequency across X, magnitude as height, and time receding into the
distance. The slab is viewed through an orbiting camera that is never quite
still — it drifts slowly in steady state, and when the audio makes a major
change (an onset or new texture, detected via spectral flux) it sweeps round
to a fresh viewpoint. Pass --no-rotate for a fixed head-on view.
Animated preview (open the PNG to play it) rendered by --preview from the
built-in synthetic source — exactly the vectors the device would draw,
reconstructed from the wire bytes. Watch the camera swing to a new angle each
time the spectrum shifts.
# Live, visualising the default output by tapping its monitor
# (Linux; needs pyalsaaudio):
pip install "pyvterm[analyzer]" pyalsaaudio
PULSE_SOURCE=@DEFAULT_SINK@.monitor python examples/spectrum3d.py --device pulse
# No hardware? Render the animated PNG above from synthetic audio:
pip install "pyvterm[preview]"
python examples/spectrum3d.py --synthetic --preview spectrum3d.png
# Or just stream synthetic audio to a real Vectrex:
python examples/spectrum3d.py --synthetic --port /dev/ttyACM0
Rutt-Etra video scan processing
examples/ruttetra.py reads video with OpenCV (a file, a
camera, or any cv2.VideoCapture source) and renders a Rutt-Etra
style scan: each frame is reduced to a coarse grid and every scan line is drawn
as a polyline displaced vertically by luminance — a flat image becomes a 3D
relief of horizontal lines, which is exactly what a vector display does best.
Animated preview (open the PNG to play it) rendered by --preview from the
built-in synthetic scene.
The Vectrex can only draw so many vectors per refresh, so the grid is kept
small; turn --cols (horizontal resolution), --rows (scan lines / line
spacing) and --fps down further if your display flickers.
# Live webcam to a real Vectrex (needs OpenCV):
pip install "pyvterm[video]"
python examples/ruttetra.py --video 0 --port /dev/ttyACM0
# A video file, downscaled and slowed for the Vectrex:
python examples/ruttetra.py --video clip.mp4 --cols 40 --rows 22 --fps 12 --port /dev/ttyACM0
# No camera? Render the animated PNG above from the synthetic scene:
pip install "pyvterm[preview]"
python examples/ruttetra.py --synthetic --preview ruttetra.png
Connecting to the hardware
pyvterm just needs a serial port that reaches the PiTrex. The PiTrex hosts a Raspberry Pi Zero on its GPIO header, and there are two places to attach a host link — the PiTrex Developer Release Hardware Guide is the authoritative reference for the header location and exact wiring.
- USB gadget port (simplest, easily fast enough). The Pi Zero's data
micro-USB port — the inner one marked USB, not PWR — can act as a
USB-CDC serial gadget and shows up on your PC as
/dev/ttyACM0, pyvterm's default port. USB-CDC ignores the line rate, so the nominal 2 Mbaud is met with room to spare and no serial adapter is required — just a micro-USB-to-USB-A data cable. Remove the POWER FROM VEC. jumper first, since the PC then supplies power. - GPIO UART header. Alternatively wire a USB-to-TTL adapter to the Pi Zero
header pins 1 (3.3 V), 6 (GND), 8 (Tx, GPIO14), 10 (Rx, GPIO15), crossing
Tx↔Rx, and set
enable_uart=1inconfig.txt(this is the guide's documented serial-console path). Going this route, the adapter itself must keep up.
Choosing a USB-to-serial adapter
For the GPIO-UART route the adapter has to satisfy two hard requirements:
- 3.3 V logic levels — never 5 V (5 V on the Pi's pins will damage it).
- Sustain ≥ 2 Mbaud, the protocol's line rate.
A genuine FTDI FT232R-based 3.3 V adapter is the safe pick — it is rated to 3 Mbaud, comfortably above the 2 Mbaud the protocol asks for:
- FTDI TTL-232R-3V3 — 3.3 V FT232R USB-to-TTL cable (datasheet; rated to 3 Mbaud). Choose the 0.1″-socket variant so it drops onto the header pins.
- Adafruit FTDI Friend — FT232RL, switchable to 3.3 V.
- A Silicon Labs CP2102N board (rated to 3 Mbaud) is a fine alternative.
Avoid low-cost CH340 modules and anything labelled only CP2102 (the original tops out near 1 Mbaud) — they are unreliable at 2 Mbaud. The USB gadget port above sidesteps the line-rate question entirely, so it's the recommended path unless you specifically need the UART pins.
Notes
- Host device path:
/dev/ttyACM0(Linux),/dev/tty.usbmodem*(macOS), orCOMx(Windows). SerialTransportwaits ~2 s after opening before flushing buffers (the reference driver does the same "to make flush work, for some reason"); passsettle=0to skip it.
Development
python -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"
ruff check . && ruff format --check .
mypy
pytest
Credits
The protocol and the PiTrex platform are the work of Graham Toal and contributors (gtoal/pitrex); the USB-DVG drivers this library mirrors were written by Mario Montminy. pyvterm is an independent, clean-room Python reimplementation of the sender protocol.
License
Apache-2.0. See LICENSE.
Metadata
Release files for pyvterm 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pyvterm-0.2.0.tar.gz | 11.0 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pyvterm-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 11.0 MB
Release files / pyvterm-0.2.0.tar.gz
| Download URL | pyvterm-0.2.0.tar.gz |
|---|---|
| Size | 11.0 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
aefbe65d5d41cea8840866060f72456436ec8108ec6ec8b669342f42f3cd71d1
|
|
BLAKE2b-256 checksum How to use checksums |
425330787364c3fc4a0cba185c37149fde05f334fafa1b1af4f509c85245e6b0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 19, 2026.
Transparency logRelease files / pyvterm-0.2.0-py3-none-any.whl
| Download URL | pyvterm-0.2.0-py3-none-any.whl |
|---|---|
| Size | 25.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c464cb3df627b1dd3c8588374ad9cea9cc6f7728d756369d26d3e21135f2fc87
|
|
BLAKE2b-256 checksum How to use checksums |
1ce809a253a2e045821a66b442890bec8939f7d1f8e3944cfdfb810d8cc25f6e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 19, 2026.
Transparency log