Metadata-Version: 2.5
Name: scpi
Version: 2.6.0
Summary: Transport-independent SCPI command sender/parser and device base classes
Project-URL: Homepage, https://github.com/rambo/python-scpi/
Project-URL: Repository, https://github.com/rambo/python-scpi/
Author-email: Eero af Heurlin <eero.afheurlin@iki.fi>
License-Expression: LGPL-2.1-or-later
License-File: LICENSE
Requires-Python: <4.0,>=3.12
Requires-Dist: pyserial-asyncio<0.7,>=0.6
Requires-Dist: pyserial<4.0,>=3.4
Description-Content-Type: text/x-rst

====
scpi
====

Transport-independent, asyncio-based SCPI commands and device helpers for
Python 3.12, 3.13 and 3.14. Serial transport uses pyserial-asyncio; VISA is not required.

Usage
-----

Install with ``pip install scpi`` or add the package with ``uv add scpi``.

* Instantiate a transport. GPIB devices use ``GPIBDeviceTransport``.
* Instantiate ``SCPIDevice`` with the transport, or with an explicit
  ``SCPIProtocol`` when you need to share a protocol.
* Await the device methods from your application's event loop.

For blocking scripts and interactive use, wrap a device with ``AIOWrapper``.
Each wrapper creates its own loop and closes it on ``quit()``. When wrappers
share a transport, pass ``loop=controller.loop`` to the device wrappers; only
that controller owns the loop. Quit device wrappers before their controller.
Calling ``quit()`` repeatedly is safe. Use the asynchronous API inside an
already running event loop.

For example, connect to an HP 6632B power supply::

    uv run --locked python examples/hp6632b_serial.py /dev/ttyUSB0

The example leaves an interactive ``dev`` object and registers shutdown with
``atexit``. See ``examples/`` for TCP and Prologix GPIB examples. Physical
instrument access and serial-port permissions are required for those examples.

Serial I/O
----------

Serial factories and ``RS232Transport(serialdevice=port)`` remain synchronous.
They open or accept a pyserial port, then attach pyserial-asyncio to the running
event loop on the first command or read. Use one event loop per transport and
always await ``quit()`` (or call the blocking wrapper's ``quit()``).

Ordinary serial commands and replies use ASCII and CRLF; Prologix uses LF.
Replies are buffered, including replies received before ``get_response()``.
Response waits are cancellable; a disconnect fails pending reads. Explicit
message callbacks remain supported, and unsolicited callbacks receive lines
when no response read is pending. Serial BREAK uses an asynchronous delay.

Prologix initialization runs automatically on first use. To reset the controller
explicitly, use ``await transport.initialize_controller()``; this method is now
asynchronous. Serial read/write timeouts become zero for nonblocking I/O;
SCPI command timeouts and asynchronous write flow control bound waits instead.

On POSIX, pyserial-asyncio requires a serial file descriptor. In-memory
``loop://`` ports do not provide one; the tests use pseudo-terminals instead.
See the `pyserial-asyncio documentation <https://pyserial-asyncio.readthedocs.io/en/latest/shortintro.html>`_.

Development
-----------

The project uses uv, Hatchling, Ruff, strict Pyrefly and prek::

    uv sync --locked
    uv run --locked prek install
    uv run --locked prek run --all-files
    uv run --locked pytest
    uv run --locked pyrefly check
    uv run --locked bandit -r src --skip B101

Run the supported Python matrix with ``uv run --locked tox``. Tox needs Python
3.12, 3.13 and 3.14 available; ``uv python install 3.12 3.13 3.14`` can install
them. Tests use fake devices and POSIX pseudo-terminals and need no hardware.
Serial integration tests are skipped on Windows.
The Bandit B101 exclusion permits existing internal assertions.

Release preparation
-------------------

From a clean working tree::

    uv run --locked bump-my-version bump patch
    uv lock
    uv run --locked prek run --all-files
    uv run --locked pytest
    uv build

The bump updates package metadata, the package version and its test. Commit
those files and ``uv.lock`` together. It does not automatically commit, tag,
publish or push. Builds retain ``LICENSE`` and ``py.typed`` and exclude the
local, untracked ``HANDOFF.md`` from source distributions.

After a merge or direct push to ``main``, successful Python and container checks
automatically create a Git tag matching ``project.version`` (for example,
``2.6.0``). Existing tags are left unchanged, so bump the version before merging
a new release. The tagging job uses ``GITHUB_TOKEN`` with ``contents: write``;
it does not publish packages or create GitHub releases.

Containers
----------

``Dockerfile`` uses Debian Trixie; ``Dockerfile_alpine`` uses Alpine 3.24.
Both use Python 3.14 and expose ``test``, ``tox``, ``devel_shell`` and
``production`` targets. Tox checks Python 3.12 through 3.14. Production installs
only runtime dependencies, non-editably, and prints the package version by
default. Pass a command to run your own script.

Build and run all targets locally::

    ./run_tests.sh
    CONTAINER_ENGINE=podman ./run_tests.sh

Filter a run or use an optional wheelhouse::

    CONTAINER_ENGINE=podman VARIANTS=alpine TARGETS=tox ./run_tests.sh
    WHEELHOUSE_URL=http://host.docker.internal:3141/debian/ VARIANTS=debian ./run_tests.sh

Sources are copied into images; no host virtualenv, SSH agent or source bind
mount is required. The wheelhouse is optional. To build a production image::

    docker build --target production -t scpi:local .
    docker run --rm scpi:local

Use ``podman`` in place of ``docker`` if preferred. Access to physical serial
instruments must be configured separately for your container engine.

CI
--

GitHub Actions runs checks on Python 3.12, 3.13 and 3.14, and builds/runs all
four container targets on both distributions. Container jobs use Docker.

TODO
----

Consider configurable carrier-detect and CTS checks for RS232 devices that
support those signals.
