diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..bd3ad87 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,60 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project overview + +`qtm_rt` is the Qualisys SDK for Python — a client library that implements Qualisys' RealTime (RT) protocol for talking to QTM (Qualisys Track Manager). Published to PyPI as `qtm-rt`. Targets Python 3.10+ and RT protocol version 1.8+. Little-endian only; default port 22223. + +## Common commands + +```bash +# Setup +python -m venv .venv +source ./.venv/Scripts/activate # Windows bash; on PowerShell: .\.venv\Scripts\Activate.ps1 +pip install -r requirements-dev.txt + +# Tests +pytest test/ +pytest test/qrtconnection_test.py::test_connect_no_loop # single test + +# Lint +python -m ruff check . + +# Build sdist + wheel into dist/ +python -m build + +# Build Sphinx docs into docs/_build/html/ +make -C docs html + +# Enable debug logging at runtime +QTM_LOGGING=debug python your_script.py +``` + +Release flow — bump `version` in `pyproject.toml` (and `docs/conf.py`), open a PR, merge to master, then create a GitHub Release on the tag `vX.Y.Z`. Publishing the release fires `.github/workflows/release.yml`, which lints with Ruff, runs tests, builds the sdist+wheel, twine-checks, uploads to PyPI via Trusted Publishing, and attaches `qtm-rt-examples.zip` (the `examples/` directory plus `Demo.qtm`) to the release. Docs are still a manual step: build with `make -C docs html`, copy `docs/_build/html/*` into the sibling `../qualisys_python_sdk_gh_pages` checkout **preserving the legacy `v102/`, `v103/`, `v212/` directories**, commit/push that branch. + +## Architecture + +Everything is `asyncio`-based and built around `qtm_rt.connect()` → returns a `QRTConnection`. The data flow: + +- **`qrt.py`** — public API surface. `connect()` opens a TCP connection wrapped in `QTMProtocol`, negotiates the RT protocol version (default `"1.25"`), and returns a `QRTConnection` exposing async methods for every RT command (`stream_frames`, `get_current_frame`, `get_parameters`, `take_control`, `start`, `stop`, `load`, `save`, `calibrate`, etc.). The `@validate_response([...])` decorator asserts the server's reply starts with an expected prefix and raises `QRTCommandException` otherwise. `QRTConnection` is an async context manager — prefer `async with await qtm_rt.connect(...) as c:` so the transport is closed deterministically on scope exit. +- **`protocol.py`** — `QTMProtocol` is an `asyncio.Protocol` subclass. Outgoing commands are framed with `RTheader` (` QRTDiscoveryResponse: def protocol_factory(): return QRTDiscoveryProtocol(receiver=self.queue.put_nowait) - _, protocol = await loop.create_datagram_endpoint( + self._transport, protocol = await loop.create_datagram_endpoint( protocol_factory, local_addr=(self.ip_address, 0), allow_broadcast=True, @@ -92,10 +93,16 @@ def protocol_factory(): result = await self.queue.get() if result is None: LOG.debug("Discovery timed out") + self._close() raise StopAsyncIteration LOG.debug(result) call_handle.cancel() return result + def _close(self): + if self._transport is not None: + self._transport.close() + self._transport = None + diff --git a/qtm_rt/qrt.py b/qtm_rt/qrt.py index 2e0f449..ff60a1c 100644 --- a/qtm_rt/qrt.py +++ b/qtm_rt/qrt.py @@ -45,9 +45,16 @@ def __init__(self, protocol: QTMProtocol, timeout): self._protocol = protocol self._timeout = timeout + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + self.disconnect() + def disconnect(self): - """Disconnect from QTM.""" - self._protocol.transport.close() + """Disconnect from QTM. Safe to call multiple times.""" + if self._protocol.transport is not None: + self._protocol.transport.close() def has_transport(self): """ Check if connected to QTM """