Drive a pitrex/Vectrex over a serial port from Python using the USB-DVG (vecterm) protocol
Project description
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pyvterm-0.2.0.tar.gz.
File metadata
- Download URL: pyvterm-0.2.0.tar.gz
- Upload date:
- Size: 11.0 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
aefbe65d5d41cea8840866060f72456436ec8108ec6ec8b669342f42f3cd71d1
|
|
| MD5 |
5f73db63ac33bc3f9d41f4ea116f53ec
|
|
| BLAKE2b-256 |
425330787364c3fc4a0cba185c37149fde05f334fafa1b1af4f509c85245e6b0
|
Provenance
The following attestation bundles were made for pyvterm-0.2.0.tar.gz:
Publisher:
release.yml on anarkiwi/pyvterm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyvterm-0.2.0.tar.gz -
Subject digest:
aefbe65d5d41cea8840866060f72456436ec8108ec6ec8b669342f42f3cd71d1 - Sigstore transparency entry: 1867544176
- Sigstore integration time:
-
Permalink:
anarkiwi/pyvterm@4666f72704333e423a99706537727a578b75596d -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/anarkiwi
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@4666f72704333e423a99706537727a578b75596d -
Trigger Event:
push
-
Statement type:
File details
Details for the file pyvterm-0.2.0-py3-none-any.whl.
File metadata
- Download URL: pyvterm-0.2.0-py3-none-any.whl
- Upload date:
- Size: 25.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c464cb3df627b1dd3c8588374ad9cea9cc6f7728d756369d26d3e21135f2fc87
|
|
| MD5 |
f65b474470268e09172cad7af8d1eded
|
|
| BLAKE2b-256 |
1ce809a253a2e045821a66b442890bec8939f7d1f8e3944cfdfb810d8cc25f6e
|
Provenance
The following attestation bundles were made for pyvterm-0.2.0-py3-none-any.whl:
Publisher:
release.yml on anarkiwi/pyvterm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyvterm-0.2.0-py3-none-any.whl -
Subject digest:
c464cb3df627b1dd3c8588374ad9cea9cc6f7728d756369d26d3e21135f2fc87 - Sigstore transparency entry: 1867544296
- Sigstore integration time:
-
Permalink:
anarkiwi/pyvterm@4666f72704333e423a99706537727a578b75596d -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/anarkiwi
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@4666f72704333e423a99706537727a578b75596d -
Trigger Event:
push
-
Statement type: