Skip to main content

Drive a pitrex/Vectrex over a serial port from Python using the USB-DVG (vecterm) protocol

Project description

pyvterm

CI PyPI Python versions License

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:

Lissajous curve preview

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).

3D rotating cube preview

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.

3D waterfall spectrum analyzer preview

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.

Rutt-Etra scan processing preview

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=1 in config.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), or COMx (Windows).
  • SerialTransport waits ~2 s after opening before flushing buffers (the reference driver does the same "to make flush work, for some reason"); pass settle=0 to 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyvterm-0.2.0.tar.gz (11.0 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyvterm-0.2.0-py3-none-any.whl (25.4 kB view details)

Uploaded Python 3

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

Hashes for pyvterm-0.2.0.tar.gz
Algorithm Hash digest
SHA256 aefbe65d5d41cea8840866060f72456436ec8108ec6ec8b669342f42f3cd71d1
MD5 5f73db63ac33bc3f9d41f4ea116f53ec
BLAKE2b-256 425330787364c3fc4a0cba185c37149fde05f334fafa1b1af4f509c85245e6b0

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyvterm-0.2.0.tar.gz:

Publisher: release.yml on anarkiwi/pyvterm

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for pyvterm-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c464cb3df627b1dd3c8588374ad9cea9cc6f7728d756369d26d3e21135f2fc87
MD5 f65b474470268e09172cad7af8d1eded
BLAKE2b-256 1ce809a253a2e045821a66b442890bec8939f7d1f8e3944cfdfb810d8cc25f6e

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyvterm-0.2.0-py3-none-any.whl:

Publisher: release.yml on anarkiwi/pyvterm

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page