Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: CI

on:
push:
branches: [master]
pull_request:

jobs:
test:
name: Lint and test Python ${{ matrix.python-version }}
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]

steps:
- name: Check out repository
uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -r requirements-dev.txt

- name: Lint
run: python -m ruff check .

- name: Run tests
run: python -m pytest test/
25 changes: 25 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ jobs:
python -m pip install --upgrade pip
python -m pip install -r requirements-dev.txt

- name: Lint
run: python -m ruff check .

- name: Run tests
run: python -m pytest test/

Expand Down Expand Up @@ -55,3 +58,25 @@ jobs:

- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1

upload-release-assets:
name: Upload release assets
runs-on: ubuntu-latest
needs: build
permissions:
contents: write

steps:
- name: Check out repository
uses: actions/checkout@v6

- name: Package examples and demo data
run: |
cp qtm_rt/data/Demo.qtm Demo.qtm
zip -r qtm-rt-examples.zip examples Demo.qtm

- name: Upload assets to GitHub release
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: gh release upload "$RELEASE_TAG" qtm-rt-examples.zip --clobber
3 changes: 3 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
prune examples
prune docs
prune qtm_rt/data
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ https://qualisys.github.io/qualisys_python_sdk/index.html
Examples
--------

See the examples folder.
See the examples folder in the GitHub repository. The examples and demo capture
file are also published as `qtm-rt-examples.zip` on each GitHub release.

The demo capture file `Demo.qtm` is not included in the PyPI package. Download
the release asset when running examples that use `--qtm-file`.

Logging
-------
Expand Down
19 changes: 12 additions & 7 deletions examples/asyncio_everything.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import logging
import asyncio
import argparse
import pkg_resources

import qtm_rt

Expand All @@ -12,9 +11,6 @@
LOG = logging.getLogger("example")


QTM_FILE = pkg_resources.resource_filename("qtm_rt", "data/Demo.qtm")


class AsyncEnumerate:
""" Simple async enumeration class """

Expand Down Expand Up @@ -66,7 +62,7 @@ async def choose_qtm_instance(interface):
return instances[choice].host


async def main(interface=None):
async def main(interface=None, qtm_file="Demo.qtm"):
""" Main function """

qtm_ip = await choose_qtm_instance(interface)
Expand All @@ -89,7 +85,7 @@ async def main(interface=None):
if result == b"Closing connection":
await connection.await_event(qtm_rt.QRTEvent.EventConnectionClosed)

await connection.load(QTM_FILE)
await connection.load(qtm_file)

await connection.start(rtfromfile=True)

Expand Down Expand Up @@ -159,10 +155,19 @@ def parse_args():
default="127.0.0.1",
help="IP of interface to search for QTM instances",
)
parser.add_argument(
"--qtm-file",
type=str,
required=False,
default="Demo.qtm",
help="QTM file to load for rtfromfile playback",
)

return parser.parse_args()


if __name__ == "__main__":
args = parse_args()
asyncio.get_event_loop().run_until_complete(main(interface=args.ip))
asyncio.get_event_loop().run_until_complete(
main(interface=args.ip, qtm_file=args.qtm_file)
)
2 changes: 1 addition & 1 deletion examples/qt_example/qt_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ async def _discover_qtm(self, interface):
async for qtm_instance in qtm_rt.Discover(interface):
info = qtm_instance.info.decode("utf-8").split(",")[0]

if not info in self._found_qtms:
if info not in self._found_qtms:
self.discoveredQTM.emit(info, qtm_instance.host)
self._found_qtms[info] = True
except Exception:
Expand Down
31 changes: 24 additions & 7 deletions examples/stream_6dof_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@
"""

import asyncio
import argparse
import xml.etree.ElementTree as ET
import pkg_resources

import qtm_rt

QTM_FILE = pkg_resources.resource_filename("qtm_rt", "data/Demo.qtm")


def create_body_index(xml_string):
""" Extract a name to index dictionary from 6dof settings xml """
Expand All @@ -21,11 +19,13 @@ def create_body_index(xml_string):

return body_to_index


def body_enabled_count(xml_string):
xml = ET.fromstring(xml_string)
return sum(enabled.text == "true" for enabled in xml.findall("*/Body/Enabled"))

async def main():

async def main(qtm_file):

# Connect to qtm
connection = await qtm_rt.connect("127.0.0.1")
Expand All @@ -45,7 +45,7 @@ async def main():
await connection.new()
else:
# Load qtm file
await connection.load(QTM_FILE)
await connection.load(qtm_file)

# start rtfromfile
await connection.start(rtfromfile=True)
Expand All @@ -54,7 +54,11 @@ async def main():
xml_string = await connection.get_parameters(parameters=["6d"])
body_index = create_body_index(xml_string)

print("{} of {} 6DoF bodies enabled".format(body_enabled_count(xml_string), len(body_index)))
print(
"{} of {} 6DoF bodies enabled".format(
body_enabled_count(xml_string), len(body_index)
)
)

wanted_body = "L-frame"

Expand Down Expand Up @@ -86,6 +90,19 @@ def on_packet(packet):
await connection.stream_frames_stop()


def parse_args():
parser = argparse.ArgumentParser(description="Stream 6DoF data from QTM")
parser.add_argument(
"--qtm-file",
type=str,
required=False,
default="Demo.qtm",
help="QTM file to load for rtfromfile playback",
)
return parser.parse_args()


if __name__ == "__main__":
args = parse_args()
# Run our asynchronous function until complete
asyncio.get_event_loop().run_until_complete(main())
asyncio.get_event_loop().run_until_complete(main(args.qtm_file))
10 changes: 10 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,13 @@ Source = "https://github.com/qualisys/qualisys_python_sdk"
packages = ["qtm_rt"]
zip-safe = true
include-package-data = false

[tool.ruff]
line-length = 88
target-version = "py310"

[tool.ruff.lint]
# Ruff's default rule set (pyflakes + a subset of pycodestyle), pinned
# explicitly so a Ruff version bump can't silently change what CI enforces.
# Broaden deliberately, e.g. add "I" (import sorting) or "UP" (pyupgrade).
select = ["E4", "E7", "E9", "F"]
21 changes: 9 additions & 12 deletions qtm_rt/__init__.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
""" Python SDK for QTM """

import logging
import sys
import os

PYTHON3 = sys.version_info.major == 3

if PYTHON3:
from .discovery import Discover
from .reboot import reboot
from .qrt import connect, QRTConnection
from .protocol import QRTCommandException
from .control import TakeControl

from .packet import QRTPacket, QRTEvent
from .receiver import Receiver
from .control import TakeControl as TakeControl
from .discovery import Discover as Discover
from .packet import QRTEvent as QRTEvent
from .packet import QRTPacket as QRTPacket
from .protocol import QRTCommandException as QRTCommandException
from .qrt import QRTConnection as QRTConnection
from .qrt import connect as connect
from .reboot import reboot as reboot
from .receiver import Receiver as Receiver

# pylint: disable=C0330

Expand Down
1 change: 0 additions & 1 deletion qtm_rt/control.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import asyncio
import logging

from .qrt import QRTConnection
Expand Down
5 changes: 2 additions & 3 deletions qtm_rt/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,8 @@ async def __anext__(self) -> QRTDiscoveryResponse:
loop = asyncio.get_event_loop()
if self.first:

protocol_factory = lambda: QRTDiscoveryProtocol(
receiver=self.queue.put_nowait
)
def protocol_factory():
return QRTDiscoveryProtocol(receiver=self.queue.put_nowait)

_, protocol = await loop.create_datagram_endpoint(
protocol_factory,
Expand Down
3 changes: 1 addition & 2 deletions qtm_rt/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
import logging

from qtm_rt.packet import QRTPacketType
from qtm_rt.packet import QRTPacket, QRTEvent
from qtm_rt.packet import RTheader, RTEvent, RTCommand
from qtm_rt.packet import RTheader, RTCommand
from qtm_rt.receiver import Receiver

# pylint: disable=C0330
Expand Down
4 changes: 2 additions & 2 deletions qtm_rt/qrt.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ async def get_parameters(self, parameters=None):
parameters = ["all"]
else:
for parameter in parameters:
if not parameter in [
if parameter not in [
"all",
"general",
"3d",
Expand Down Expand Up @@ -374,7 +374,7 @@ async def connect(

def _validate_components(components):
for component in components:
if not component.lower() in [
if component.lower() not in [
"2d",
"2dlin",
"3d",
Expand Down
8 changes: 4 additions & 4 deletions qtm_rt/receiver.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from qtm_rt.packet import QRTPacketType
from qtm_rt.packet import QRTPacket, QRTEvent
from qtm_rt.packet import RTheader, RTEvent, RTCommand
from qtm_rt.packet import RTheader, RTEvent

LOG = logging.getLogger("qtm_rt")

Expand All @@ -17,16 +17,16 @@ def data_received(self, data):
self._received_data += data
h_size = RTheader.size
data = self._received_data
data_len = len(data);
data_len = len(data)

while data_len >= h_size:
size, type_ = RTheader.unpack_from(data, 0)
if data_len >= size:
self._parse_received(data[h_size:size], type_)
data = data[size:]
data_len = len(data);
data_len = len(data)
else:
break;
break

self._received_data = data

Expand Down
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ build==1.2.1
pytest-asyncio==0.23.7
pytest-mock==3.14.0
pytest==8.2.2
ruff==0.15.15
sphinx==7.3.7
twine==6.2.0
2 changes: 1 addition & 1 deletion test/qrtconnection_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ async def xml(*_):
a_qrt._protocol.receive_response.side_effect = xml

with pytest.raises(QRTCommandException):
response = await a_qrt.calibrate()
await a_qrt.calibrate()

a_qrt._protocol.send_command.assert_called_once_with("calibrate")

Expand Down
2 changes: 1 addition & 1 deletion test/qtmprotocol_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import pytest

from qtm_rt.protocol import QTMProtocol, QRTCommandException
from qtm_rt.packet import QRTEvent, RTEvent
from qtm_rt.packet import QRTEvent

# pylint: disable=W0621, C0111, W0212

Expand Down