diff --git a/.gitignore b/.gitignore index c76b7d9..64c1d38 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ _templates /.vscode /.pytest_cache */__pycache__ -.coverage* \ No newline at end of file +.coverage* +examples/pyside6_example/.DS_Store diff --git a/examples/pyside6_example/Readme.md b/examples/pyside6_example/Readme.md new file mode 100644 index 0000000..f2c3073 --- /dev/null +++ b/examples/pyside6_example/Readme.md @@ -0,0 +1,32 @@ +# Streamning example with Pyside6 + +Example of combining the qtm package with Qt +Requires PySide6 and qasync +

+ +

+ +## Usage + +Recomended to use a virtual enviorment +https://github.com/pypa/virtualenv + +### Setup a virtual enviorment + +>virtualenv [directory] + +On Windows: + +>myenv\Scripts\activate.bat + +Linux and MacOS + +>source myvenv/bin/activate + +### Install dependecies + +>pip install -r requirements.txt + +### Run + +> python qt_example.py diff --git a/examples/pyside6_example/qt_example.py b/examples/pyside6_example/qt_example.py new file mode 100644 index 0000000..864effd --- /dev/null +++ b/examples/pyside6_example/qt_example.py @@ -0,0 +1,284 @@ +""" + Example of combining the qtm package with Qt + Requires PySide6 and qasync + Use pip to install requirements: + pip install -r requirements.txt + +""" + +import sys +import asyncio +import subprocess +import re +import xml.etree.cElementTree as ET +from ui.ui_Main import Ui_MainWindow + +from PySide6.QtWidgets import QApplication, QMainWindow +from PySide6.QtCore import Signal, QObject, Property +from PySide6.QtNetwork import QNetworkInterface, QAbstractSocket + + +import qtm +from qtm import QRTEvent +import qasync + +if sys.platform == "win32": + asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) + + +def start_async_task(task): + asyncio.ensure_future(task) + + +def get_interfaces(): + result = QNetworkInterface.allAddresses() + + for line in result: + if QAbstractSocket.NetworkLayerProtocol.IPv4Protocol == line.protocol(): + print(f"Ip: {line.toString()}") + yield line.toString() + + +class QDiscovery(QObject): + discoveringChanged = Signal(bool) + discoveredQTM = Signal(str, str) + + def __init__(self, *args): + super().__init__(*args) + self._discovering = False + self._found_qtms = {} + + @Property(bool, notify=discoveringChanged) + def discovering(self): + return self._discovering + + @discovering.setter + def discovering(self, value): + if value != self._discovering: + self._discovering = value + self.discoveringChanged.emit(value) + + def discover(self): + self.discovering = True + + self._found_qtms = {} + for interface in get_interfaces(): + start_async_task(self._discover_qtm(interface)) + + async def _discover_qtm(self, interface): + + try: + async for qtm_instance in qtm.Discover(interface): + info = qtm_instance.info.decode("utf-8").split(",")[0] + + if not info in self._found_qtms: + self.discoveredQTM.emit(info, qtm_instance.host) + self._found_qtms[info] = True + except Exception: + pass + + self.discovering = False + + +class MainUI(QMainWindow, Ui_MainWindow): + def __init__(self, *args): + super().__init__(*args) + self.setupUi(self) + + # Discovery + self._discovery = QDiscovery() + self._discovery.discoveringChanged.connect(self._is_discovering) + self._discovery.discoveredQTM.connect(self._qtm_discovered) + self.discover_button.clicked.connect(self._discovery.discover) + + # Connection + self.connect_button.clicked.connect(self.connect_qtm) + self.disconnect_button.clicked.connect(self.disconnect_qtm) + self._is_streaming = False + + # Trajectory & sixdof + self._trajectory_index = None + self.trajectory_combo.currentIndexChanged.connect( + self._trajectory_index_changed + ) + self._sixdof_index = None + self.sixdof_combo.currentIndexChanged.connect(self._sixdof_index_changed) + + # Settings + for setting in [ + "all", + "general", + "3d", + "6d", + "analog", + "force", + "gazevector", + "image", + ]: + self.settings_combo.addItem(setting) + self.settings_combo.currentTextChanged.connect( + self._settings_index_changed) + + self._to_be_cleared = [ + self.x_trajectory, + self.y_trajectory, + self.z_trajectory, + self.x_sixdof, + self.y_sixdof, + self.z_sixdof, + self.settings_viewer, + self.trajectory_combo, + self.sixdof_combo, + ] + + self._discovery.discover() + + def _is_discovering(self, discovering): + if discovering: + self.qtm_combo.clear() + self.discover_button.setEnabled(not discovering) + + def _settings_index_changed(self, setting): + start_async_task(self._get_settings(setting)) + + def _qtm_discovered(self, info, ip): + self.qtm_combo.addItem("{} {}".format(info, ip)) + self.connect_button.setEnabled(True) + + def connect_qtm(self): + self.connect_button.setEnabled(False) + self.discover_button.setEnabled(False) + self.qtm_combo.setEnabled(False) + + start_async_task(self._connect_qtm()) + + async def _connect_qtm(self): + ip = self.qtm_combo.currentText().split(" ")[1] + + self._connection = await qtm.connect( + ip, on_disconnect=self.on_disconnect, on_event=self.on_event + ) + + if self._connection is None: + self.on_disconnect("Failed to connect") + return + + await self._connection.take_control("password") + await self._connection.get_state() + + self.disconnect_button.setEnabled(True) + self.settings_combo.setEnabled(True) + + def disconnect_qtm(self): + self._connection.disconnect() + + def on_disconnect(self, reason): + self.disconnect_button.setEnabled(False) + self.connect_button.setEnabled(True) + self.discover_button.setEnabled(True) + self.qtm_combo.setEnabled(True) + self.settings_combo.setEnabled(False) + + for item in self._to_be_cleared: + item.clear() + + def _sixdof_index_changed(self, index): + self._sixdof_index = index + + def _trajectory_index_changed(self, index): + self._trajectory_index = index + + def set_3d_values(self, controls, values): + for control, component in zip(controls, values): + control.setText("{0:.3f}".format(component)) + + def on_packet(self, packet): + if qtm.packet.QRTComponentType.Component3d in packet.components: + _, markers = packet.get_3d_markers() + if self._trajectory_index is not None: + marker = markers[self._trajectory_index] + self.set_3d_values( + [self.x_trajectory, self.y_trajectory, self.z_trajectory], marker + ) + + if qtm.packet.QRTComponentType.Component6d in packet.components: + _, sixdofs = packet.get_6d() + if self._sixdof_index is not None: + position, _ = sixdofs[self._sixdof_index] + self.set_3d_values( + [self.x_sixdof, self.y_sixdof, self.z_sixdof], position + ) + + def on_event(self, event): + start_async_task(self._async_event_handler(event)) + + async def _async_event_handler(self, event): + + if event == QRTEvent.EventRTfromFileStarted or event == QRTEvent.EventConnected: + await self._setup_qtm() + + elif ( + event == QRTEvent.EventRTfromFileStopped + or event == QRTEvent.EventConnectionClosed + ) and self._is_streaming: + start_async_task(self._stop_stream()) + + async def _setup_qtm(self, stream=True): + await self._get_sixdof() + await self._get_labels() + await self._get_settings(self.settings_combo.currentText()) + await self._start_stream() + + async def _get_settings(self, setting="all"): + result = await self._connection.get_parameters(parameters=[setting]) + + self.settings_viewer.setText(result.decode("utf-8")) + + async def _get_sixdof(self): + result = await self._connection.get_parameters(parameters=["6d"]) + + try: + xml = ET.fromstring(result) + except ET.ParseError: + print(result) + return + + self.sixdof_combo.clear() + for label in (label.text for label in xml.iter("Name")): + self.sixdof_combo.addItem(label) + + async def _get_labels(self): + result = await self._connection.get_parameters(parameters=["3d"]) + + xml = ET.fromstring(result) + self.trajectory_combo.clear() + for label in (label.text for label in xml.iter("Name")): + self.trajectory_combo.addItem(label) + + async def _stop_stream(self): + await self._connection.stream_frames_stop() + self._is_streaming = False + + async def _start_stream(self): + result = await self._connection.stream_frames( + frames="frequency:10", components=["3d", "6d"], on_packet=self.on_packet + ) + if result == b"Ok": + self._is_streaming = True + + +def main(): + + app = QApplication(sys.argv) + + # Create and set an event loop that combines qt and asyncio + loop = qasync.QEventLoop(app) + asyncio.set_event_loop(loop) + + main_window = MainUI() + main_window.show() + sys.exit(app.exec_()) + + +if __name__ == "__main__": + main() diff --git a/examples/pyside6_example/requirements.txt b/examples/pyside6_example/requirements.txt new file mode 100644 index 0000000..4b42a14 --- /dev/null +++ b/examples/pyside6_example/requirements.txt @@ -0,0 +1,3 @@ +qtm +PySide6 +qasync \ No newline at end of file diff --git a/examples/pyside6_example/ui/Main.ui b/examples/pyside6_example/ui/Main.ui new file mode 100644 index 0000000..81d48be --- /dev/null +++ b/examples/pyside6_example/ui/Main.ui @@ -0,0 +1,352 @@ + + + MainWindow + + + + 0 + 0 + 905 + 537 + + + + MainWindow + + + + + + + Streaming Info + + + + + + + + Trajectories + + + + + + + 9 + + + 9 + + + 9 + + + 9 + + + + + Qt::LeftToRight + + + Y: + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + true + + + + + + + Qt::LeftToRight + + + X: + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + true + + + + + + + Qt::LeftToRight + + + Z: + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + true + + + + + + + + + + + + + + + + 6DOF + + + + + + + 9 + + + 9 + + + 9 + + + 9 + + + + + X: + + + Qt::AlignCenter + + + + + + + Z: + + + Qt::AlignCenter + + + + + + + Y: + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + + + + + QTM Settings + + + + + + + + + + true + + + + + + + + + + + 0 + 0 + + + + + 240 + 180 + + + + QDockWidget::DockWidgetMovable + + + QTM Controls + + + 1 + + + + + 9 + + + 9 + + + + + + + + + + QTM: + + + + + + + + 0 + 0 + + + + + + + + + + Discover QTM + + + + + + + + + false + + + Connect + + + + + + + false + + + Disconnect + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + diff --git a/examples/pyside6_example/ui/ui_Main.py b/examples/pyside6_example/ui/ui_Main.py new file mode 100644 index 0000000..213b7bb --- /dev/null +++ b/examples/pyside6_example/ui/ui_Main.py @@ -0,0 +1,281 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'MainXtezAc.ui' +## +## Created by: Qt User Interface Compiler version 6.2.3 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QApplication, QComboBox, QDockWidget, QGridLayout, + QGroupBox, QHBoxLayout, QLabel, QLineEdit, + QMainWindow, QPushButton, QSizePolicy, QSpacerItem, + QTextEdit, QVBoxLayout, QWidget) + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + if not MainWindow.objectName(): + MainWindow.setObjectName(u"MainWindow") + MainWindow.resize(905, 537) + self.centralwidget = QWidget(MainWindow) + self.centralwidget.setObjectName(u"centralwidget") + self.gridLayout = QGridLayout(self.centralwidget) + self.gridLayout.setObjectName(u"gridLayout") + self.groupBox = QGroupBox(self.centralwidget) + self.groupBox.setObjectName(u"groupBox") + self.horizontalLayout_5 = QHBoxLayout(self.groupBox) + self.horizontalLayout_5.setObjectName(u"horizontalLayout_5") + self.verticalLayout_2 = QVBoxLayout() + self.verticalLayout_2.setObjectName(u"verticalLayout_2") + self.label_6 = QLabel(self.groupBox) + self.label_6.setObjectName(u"label_6") + + self.verticalLayout_2.addWidget(self.label_6) + + self.gridLayout_2 = QGridLayout() + self.gridLayout_2.setObjectName(u"gridLayout_2") + self.gridLayout_2.setContentsMargins(9, 9, 9, 9) + self.label_4 = QLabel(self.groupBox) + self.label_4.setObjectName(u"label_4") + self.label_4.setLayoutDirection(Qt.LeftToRight) + self.label_4.setAlignment(Qt.AlignCenter) + + self.gridLayout_2.addWidget(self.label_4, 1, 1, 1, 1) + + self.y_trajectory = QLineEdit(self.groupBox) + self.y_trajectory.setObjectName(u"y_trajectory") + sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.y_trajectory.sizePolicy().hasHeightForWidth()) + self.y_trajectory.setSizePolicy(sizePolicy) + self.y_trajectory.setReadOnly(True) + + self.gridLayout_2.addWidget(self.y_trajectory, 1, 2, 1, 1) + + self.label_5 = QLabel(self.groupBox) + self.label_5.setObjectName(u"label_5") + self.label_5.setLayoutDirection(Qt.LeftToRight) + self.label_5.setAlignment(Qt.AlignCenter) + + self.gridLayout_2.addWidget(self.label_5, 0, 1, 1, 1) + + self.x_trajectory = QLineEdit(self.groupBox) + self.x_trajectory.setObjectName(u"x_trajectory") + sizePolicy.setHeightForWidth(self.x_trajectory.sizePolicy().hasHeightForWidth()) + self.x_trajectory.setSizePolicy(sizePolicy) + self.x_trajectory.setReadOnly(True) + + self.gridLayout_2.addWidget(self.x_trajectory, 0, 2, 1, 1) + + self.label_3 = QLabel(self.groupBox) + self.label_3.setObjectName(u"label_3") + self.label_3.setLayoutDirection(Qt.LeftToRight) + self.label_3.setAlignment(Qt.AlignCenter) + + self.gridLayout_2.addWidget(self.label_3, 2, 1, 1, 1) + + self.z_trajectory = QLineEdit(self.groupBox) + self.z_trajectory.setObjectName(u"z_trajectory") + sizePolicy.setHeightForWidth(self.z_trajectory.sizePolicy().hasHeightForWidth()) + self.z_trajectory.setSizePolicy(sizePolicy) + self.z_trajectory.setReadOnly(True) + + self.gridLayout_2.addWidget(self.z_trajectory, 2, 2, 1, 1) + + + self.verticalLayout_2.addLayout(self.gridLayout_2) + + self.trajectory_combo = QComboBox(self.groupBox) + self.trajectory_combo.setObjectName(u"trajectory_combo") + + self.verticalLayout_2.addWidget(self.trajectory_combo) + + + self.horizontalLayout_5.addLayout(self.verticalLayout_2) + + self.verticalLayout_3 = QVBoxLayout() + self.verticalLayout_3.setObjectName(u"verticalLayout_3") + self.label_7 = QLabel(self.groupBox) + self.label_7.setObjectName(u"label_7") + + self.verticalLayout_3.addWidget(self.label_7) + + self.gridLayout_4 = QGridLayout() + self.gridLayout_4.setObjectName(u"gridLayout_4") + self.gridLayout_4.setContentsMargins(9, 9, 9, 9) + self.label_8 = QLabel(self.groupBox) + self.label_8.setObjectName(u"label_8") + self.label_8.setAlignment(Qt.AlignCenter) + + self.gridLayout_4.addWidget(self.label_8, 0, 0, 1, 1) + + self.label_10 = QLabel(self.groupBox) + self.label_10.setObjectName(u"label_10") + self.label_10.setAlignment(Qt.AlignCenter) + + self.gridLayout_4.addWidget(self.label_10, 2, 0, 1, 1) + + self.label_9 = QLabel(self.groupBox) + self.label_9.setObjectName(u"label_9") + self.label_9.setAlignment(Qt.AlignCenter) + + self.gridLayout_4.addWidget(self.label_9, 1, 0, 1, 1) + + self.x_sixdof = QLineEdit(self.groupBox) + self.x_sixdof.setObjectName(u"x_sixdof") + sizePolicy.setHeightForWidth(self.x_sixdof.sizePolicy().hasHeightForWidth()) + self.x_sixdof.setSizePolicy(sizePolicy) + + self.gridLayout_4.addWidget(self.x_sixdof, 0, 1, 1, 1) + + self.y_sixdof = QLineEdit(self.groupBox) + self.y_sixdof.setObjectName(u"y_sixdof") + sizePolicy.setHeightForWidth(self.y_sixdof.sizePolicy().hasHeightForWidth()) + self.y_sixdof.setSizePolicy(sizePolicy) + + self.gridLayout_4.addWidget(self.y_sixdof, 1, 1, 1, 1) + + self.z_sixdof = QLineEdit(self.groupBox) + self.z_sixdof.setObjectName(u"z_sixdof") + sizePolicy.setHeightForWidth(self.z_sixdof.sizePolicy().hasHeightForWidth()) + self.z_sixdof.setSizePolicy(sizePolicy) + + self.gridLayout_4.addWidget(self.z_sixdof, 2, 1, 1, 1) + + + self.verticalLayout_3.addLayout(self.gridLayout_4) + + self.sixdof_combo = QComboBox(self.groupBox) + self.sixdof_combo.setObjectName(u"sixdof_combo") + + self.verticalLayout_3.addWidget(self.sixdof_combo) + + + self.horizontalLayout_5.addLayout(self.verticalLayout_3) + + + self.gridLayout.addWidget(self.groupBox, 0, 0, 1, 1) + + self.verticalLayout_4 = QVBoxLayout() + self.verticalLayout_4.setObjectName(u"verticalLayout_4") + self.label_11 = QLabel(self.centralwidget) + self.label_11.setObjectName(u"label_11") + + self.verticalLayout_4.addWidget(self.label_11) + + self.settings_combo = QComboBox(self.centralwidget) + self.settings_combo.setObjectName(u"settings_combo") + + self.verticalLayout_4.addWidget(self.settings_combo) + + self.settings_viewer = QTextEdit(self.centralwidget) + self.settings_viewer.setObjectName(u"settings_viewer") + self.settings_viewer.setReadOnly(True) + + self.verticalLayout_4.addWidget(self.settings_viewer) + + + self.gridLayout.addLayout(self.verticalLayout_4, 1, 0, 1, 1) + + MainWindow.setCentralWidget(self.centralwidget) + self.dockWidget_2 = QDockWidget(MainWindow) + self.dockWidget_2.setObjectName(u"dockWidget_2") + sizePolicy1 = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) + sizePolicy1.setHorizontalStretch(0) + sizePolicy1.setVerticalStretch(0) + sizePolicy1.setHeightForWidth(self.dockWidget_2.sizePolicy().hasHeightForWidth()) + self.dockWidget_2.setSizePolicy(sizePolicy1) + self.dockWidget_2.setMinimumSize(QSize(240, 180)) + self.dockWidget_2.setFeatures(QDockWidget.DockWidgetMovable) + self.dockWidgetContents_2 = QWidget() + self.dockWidgetContents_2.setObjectName(u"dockWidgetContents_2") + self.verticalLayout = QVBoxLayout(self.dockWidgetContents_2) + self.verticalLayout.setObjectName(u"verticalLayout") + self.verticalLayout.setContentsMargins(9, -1, 9, -1) + self.horizontalLayout_4 = QHBoxLayout() + self.horizontalLayout_4.setObjectName(u"horizontalLayout_4") + + self.verticalLayout.addLayout(self.horizontalLayout_4) + + self.horizontalLayout_2 = QHBoxLayout() + self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") + self.label = QLabel(self.dockWidgetContents_2) + self.label.setObjectName(u"label") + + self.horizontalLayout_2.addWidget(self.label) + + self.qtm_combo = QComboBox(self.dockWidgetContents_2) + self.qtm_combo.setObjectName(u"qtm_combo") + sizePolicy2 = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + sizePolicy2.setHorizontalStretch(0) + sizePolicy2.setVerticalStretch(0) + sizePolicy2.setHeightForWidth(self.qtm_combo.sizePolicy().hasHeightForWidth()) + self.qtm_combo.setSizePolicy(sizePolicy2) + + self.horizontalLayout_2.addWidget(self.qtm_combo) + + + self.verticalLayout.addLayout(self.horizontalLayout_2) + + self.discover_button = QPushButton(self.dockWidgetContents_2) + self.discover_button.setObjectName(u"discover_button") + + self.verticalLayout.addWidget(self.discover_button) + + self.horizontalLayout = QHBoxLayout() + self.horizontalLayout.setObjectName(u"horizontalLayout") + self.connect_button = QPushButton(self.dockWidgetContents_2) + self.connect_button.setObjectName(u"connect_button") + self.connect_button.setEnabled(False) + + self.horizontalLayout.addWidget(self.connect_button) + + self.disconnect_button = QPushButton(self.dockWidgetContents_2) + self.disconnect_button.setObjectName(u"disconnect_button") + self.disconnect_button.setEnabled(False) + + self.horizontalLayout.addWidget(self.disconnect_button) + + + self.verticalLayout.addLayout(self.horizontalLayout) + + self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) + + self.verticalLayout.addItem(self.verticalSpacer) + + self.dockWidget_2.setWidget(self.dockWidgetContents_2) + MainWindow.addDockWidget(Qt.LeftDockWidgetArea, self.dockWidget_2) + + self.retranslateUi(MainWindow) + + QMetaObject.connectSlotsByName(MainWindow) + # setupUi + + def retranslateUi(self, MainWindow): + MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"MainWindow", None)) + self.groupBox.setTitle(QCoreApplication.translate("MainWindow", u"Streaming Info", None)) + self.label_6.setText(QCoreApplication.translate("MainWindow", u"Trajectories", None)) + self.label_4.setText(QCoreApplication.translate("MainWindow", u"Y:", None)) + self.label_5.setText(QCoreApplication.translate("MainWindow", u"X:", None)) + self.label_3.setText(QCoreApplication.translate("MainWindow", u"Z:", None)) + self.label_7.setText(QCoreApplication.translate("MainWindow", u"6DOF", None)) + self.label_8.setText(QCoreApplication.translate("MainWindow", u"X:", None)) + self.label_10.setText(QCoreApplication.translate("MainWindow", u"Z:", None)) + self.label_9.setText(QCoreApplication.translate("MainWindow", u"Y:", None)) + self.label_11.setText(QCoreApplication.translate("MainWindow", u"QTM Settings", None)) + self.dockWidget_2.setWindowTitle(QCoreApplication.translate("MainWindow", u"QTM Controls", None)) + self.label.setText(QCoreApplication.translate("MainWindow", u"QTM: ", None)) + self.discover_button.setText(QCoreApplication.translate("MainWindow", u"Discover QTM", None)) + self.connect_button.setText(QCoreApplication.translate("MainWindow", u"Connect", None)) + self.disconnect_button.setText(QCoreApplication.translate("MainWindow", u"Disconnect", None)) + # retranslateUi +