diff --git a/.gitignore b/.gitignore index 675824ba0c8a629efc532cc0b68f5af293da1b28..7ed439565d369ace3cc8f64db96e8a422bd6ebf0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ groundStation/gui/build* .vscode /crazyflie_hardware/sd_test_stand/sd_test_stand-backups/* /crazyflie_hardware/sd_test_stand_nano/sd_test_stand_nano-backups/* +controls/Sim-nonlinear-quad-example/Sim-nonlinear-quad-example/slprj/ +*/.idea/ diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..13566b81b018ad684f3a35fee301741b2734c8f4 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/MicroCART.iml b/.idea/MicroCART.iml new file mode 100644 index 0000000000000000000000000000000000000000..caa57a1c754d3b46004608ce57c9fd2e505127df --- /dev/null +++ b/.idea/MicroCART.iml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="PYTHON_MODULE" version="4"> + <component name="NewModuleRootManager"> + <content url="file://$MODULE_DIR$" /> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + </component> + <component name="TemplatesService"> + <option name="TEMPLATE_CONFIGURATION" value="Jinja2" /> + <option name="TEMPLATE_FOLDERS"> + <list> + <option value="$MODULE_DIR$/website/themes/notmyidea/templates" /> + </list> + </option> + </component> +</module> \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000000000000000000000000000000000000..105ce2da2d6447d11dfe32bfb846c3d5b199fc99 --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ +<component name="InspectionProjectProfileManager"> + <settings> + <option name="USE_PROJECT_PROFILE" value="false" /> + <version value="1.0" /> + </settings> +</component> \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000000000000000000000000000000000000..d1e22ecb89619a9c2dcf51a28d891a196d2462a0 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8" project-jdk-type="Python SDK" /> +</project> \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000000000000000000000000000000000000..04c33fd2a42c4e6f4049c8f5a3e087cf4a0ee91c --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectModuleManager"> + <modules> + <module fileurl="file://$PROJECT_DIR$/.idea/MicroCART.iml" filepath="$PROJECT_DIR$/.idea/MicroCART.iml" /> + </modules> + </component> +</project> \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000000000000000000000000000000000000..94a25f7f4cb416c083d265558da75d457237d671 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="VcsDirectoryMappings"> + <mapping directory="$PROJECT_DIR$" vcs="Git" /> + </component> +</project> \ No newline at end of file diff --git a/.qt_for_python/uic/mainwindow.py b/.qt_for_python/uic/mainwindow.py deleted file mode 100644 index 9e950c612ef5834e778d01bae1f0f2274d2fd1ed..0000000000000000000000000000000000000000 --- a/.qt_for_python/uic/mainwindow.py +++ /dev/null @@ -1,663 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file '/home/bitcraze/Desktop/groundstation/groundStation/gui/MicroCART/mainwindow.ui' -# -# Created by: PyQt5 UI code generator 5.15.4 -# -# WARNING: Any manual changes made to this file will be lost when pyuic5 is -# run again. Do not edit this file unless you know what you are doing. - - -from PyQt5 import QtCore, QtGui, QtWidgets - - -class Ui_MainWindow(object): - def setupUi(self, MainWindow): - MainWindow.setObjectName("MainWindow") - MainWindow.setEnabled(True) - MainWindow.resize(1186, 1034) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(MainWindow.sizePolicy().hasHeightForWidth()) - MainWindow.setSizePolicy(sizePolicy) - self.centralWidget = QtWidgets.QWidget(MainWindow) - self.centralWidget.setObjectName("centralWidget") - self.verticalLayout_1 = QtWidgets.QVBoxLayout(self.centralWidget) - self.verticalLayout_1.setContentsMargins(11, 11, 11, 11) - self.verticalLayout_1.setSpacing(6) - self.verticalLayout_1.setObjectName("verticalLayout_1") - self.tabWidget = QtWidgets.QTabWidget(self.centralWidget) - self.tabWidget.setMaximumSize(QtCore.QSize(16777215, 1677)) - self.tabWidget.setObjectName("tabWidget") - self.backend = QtWidgets.QWidget() - self.backend.setObjectName("backend") - self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.backend) - self.verticalLayout_2.setContentsMargins(11, 11, 11, 11) - self.verticalLayout_2.setSpacing(6) - self.verticalLayout_2.setObjectName("verticalLayout_2") - self.horizontalLayout_1 = QtWidgets.QHBoxLayout() - self.horizontalLayout_1.setSpacing(6) - self.horizontalLayout_1.setObjectName("horizontalLayout_1") - self.backendPath = QtWidgets.QLineEdit(self.backend) - self.backendPath.setEnabled(True) - self.backendPath.setObjectName("backendPath") - self.horizontalLayout_1.addWidget(self.backendPath) - self.chooseBackend = QtWidgets.QPushButton(self.backend) - self.chooseBackend.setEnabled(True) - self.chooseBackend.setObjectName("chooseBackend") - self.horizontalLayout_1.addWidget(self.chooseBackend) - self.verticalLayout_2.addLayout(self.horizontalLayout_1) - self.horizontalLayout_2 = QtWidgets.QHBoxLayout() - self.horizontalLayout_2.setSpacing(6) - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.pbConnect = QtWidgets.QPushButton(self.backend) - self.pbConnect.setEnabled(True) - self.pbConnect.setObjectName("pbConnect") - self.horizontalLayout_2.addWidget(self.pbConnect) - self.pb_connectTS = QtWidgets.QPushButton(self.backend) - self.pb_connectTS.setObjectName("pb_connectTS") - self.horizontalLayout_2.addWidget(self.pb_connectTS) - self.pb_disconnectTS = QtWidgets.QPushButton(self.backend) - self.pb_disconnectTS.setObjectName("pb_disconnectTS") - self.horizontalLayout_2.addWidget(self.pb_disconnectTS) - spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout_2.addItem(spacerItem) - self.pbStop = QtWidgets.QPushButton(self.backend) - self.pbStop.setEnabled(False) - self.pbStop.setObjectName("pbStop") - self.horizontalLayout_2.addWidget(self.pbStop) - self.verticalLayout_2.addLayout(self.horizontalLayout_2) - self.line_7 = QtWidgets.QFrame(self.backend) - self.line_7.setFrameShape(QtWidgets.QFrame.HLine) - self.line_7.setFrameShadow(QtWidgets.QFrame.Sunken) - self.line_7.setObjectName("line_7") - self.verticalLayout_2.addWidget(self.line_7) - self.vConsole = QtWidgets.QTextEdit(self.backend) - self.vConsole.setEnabled(True) - self.vConsole.setReadOnly(True) - self.vConsole.setObjectName("vConsole") - self.verticalLayout_2.addWidget(self.vConsole) - self.tabWidget.addTab(self.backend, "") - self.param = QtWidgets.QWidget() - self.param.setObjectName("param") - self.paramGroupComboBox = QtWidgets.QComboBox(self.param) - self.paramGroupComboBox.setGeometry(QtCore.QRect(420, 100, 201, 25)) - self.paramGroupComboBox.setObjectName("paramGroupComboBox") - self.paramEntriesComboBox = QtWidgets.QComboBox(self.param) - self.paramEntriesComboBox.setGeometry(QtCore.QRect(420, 150, 201, 25)) - self.paramEntriesComboBox.setObjectName("paramEntriesComboBox") - self.groupLabel = QtWidgets.QLabel(self.param) - self.groupLabel.setGeometry(QtCore.QRect(350, 100, 54, 31)) - self.groupLabel.setObjectName("groupLabel") - self.getParamLabel = QtWidgets.QLabel(self.param) - self.getParamLabel.setGeometry(QtCore.QRect(480, 40, 71, 17)) - self.getParamLabel.setObjectName("getParamLabel") - self.label = QtWidgets.QLabel(self.param) - self.label.setGeometry(QtCore.QRect(350, 150, 54, 17)) - self.label.setObjectName("label") - self.line = QtWidgets.QFrame(self.param) - self.line.setGeometry(QtCore.QRect(0, 279, 1111, 31)) - self.line.setFrameShape(QtWidgets.QFrame.HLine) - self.line.setFrameShadow(QtWidgets.QFrame.Sunken) - self.line.setObjectName("line") - self.setParamLabel = QtWidgets.QLabel(self.param) - self.setParamLabel.setGeometry(QtCore.QRect(480, 310, 71, 17)) - self.setParamLabel.setObjectName("setParamLabel") - self.valueLabel = QtWidgets.QLabel(self.param) - self.valueLabel.setGeometry(QtCore.QRect(350, 240, 54, 17)) - self.valueLabel.setObjectName("valueLabel") - self.valueVallabel = QtWidgets.QLabel(self.param) - self.valueVallabel.setGeometry(QtCore.QRect(420, 240, 201, 20)) - self.valueVallabel.setText("") - self.valueVallabel.setObjectName("valueVallabel") - self.groupLabel_set = QtWidgets.QLabel(self.param) - self.groupLabel_set.setGeometry(QtCore.QRect(350, 360, 54, 31)) - self.groupLabel_set.setObjectName("groupLabel_set") - self.label_5 = QtWidgets.QLabel(self.param) - self.label_5.setGeometry(QtCore.QRect(350, 400, 54, 17)) - self.label_5.setObjectName("label_5") - self.valueLabel_2 = QtWidgets.QLabel(self.param) - self.valueLabel_2.setGeometry(QtCore.QRect(350, 450, 54, 17)) - self.valueLabel_2.setObjectName("valueLabel_2") - self.paramGroupComboBox_2 = QtWidgets.QComboBox(self.param) - self.paramGroupComboBox_2.setGeometry(QtCore.QRect(420, 360, 201, 25)) - self.paramGroupComboBox_2.setObjectName("paramGroupComboBox_2") - self.paramEntriesComboBox_2 = QtWidgets.QComboBox(self.param) - self.paramEntriesComboBox_2.setGeometry(QtCore.QRect(420, 400, 201, 25)) - self.paramEntriesComboBox_2.setObjectName("paramEntriesComboBox_2") - self.pb_getParam = QtWidgets.QPushButton(self.param) - self.pb_getParam.setGeometry(QtCore.QRect(480, 200, 80, 25)) - self.pb_getParam.setObjectName("pb_getParam") - self.pb_setParam = QtWidgets.QPushButton(self.param) - self.pb_setParam.setGeometry(QtCore.QRect(480, 500, 80, 25)) - self.pb_setParam.setObjectName("pb_setParam") - self.setValueSpin = QtWidgets.QDoubleSpinBox(self.param) - self.setValueSpin.setGeometry(QtCore.QRect(470, 450, 111, 26)) - self.setValueSpin.setDecimals(3) - self.setValueSpin.setObjectName("setValueSpin") - self.tabWidget.addTab(self.param, "") - self.Gamepad = QtWidgets.QWidget() - self.Gamepad.setObjectName("Gamepad") - self.pb_configRoll = QtWidgets.QPushButton(self.Gamepad) - self.pb_configRoll.setGeometry(QtCore.QRect(720, 130, 141, 25)) - self.pb_configRoll.setObjectName("pb_configRoll") - self.rollVizBar = QtWidgets.QSlider(self.Gamepad) - self.rollVizBar.setEnabled(False) - self.rollVizBar.setGeometry(QtCore.QRect(710, 160, 160, 16)) - self.rollVizBar.setMinimum(-30) - self.rollVizBar.setMaximum(30) - self.rollVizBar.setProperty("value", 0) - self.rollVizBar.setOrientation(QtCore.Qt.Horizontal) - self.rollVizBar.setObjectName("rollVizBar") - self.pb_resetConfig = QtWidgets.QPushButton(self.Gamepad) - self.pb_resetConfig.setGeometry(QtCore.QRect(440, 90, 191, 25)) - self.pb_resetConfig.setObjectName("pb_resetConfig") - self.pb_configYaw = QtWidgets.QPushButton(self.Gamepad) - self.pb_configYaw.setGeometry(QtCore.QRect(160, 140, 141, 25)) - self.pb_configYaw.setObjectName("pb_configYaw") - self.yawVizBar = QtWidgets.QSlider(self.Gamepad) - self.yawVizBar.setEnabled(False) - self.yawVizBar.setGeometry(QtCore.QRect(150, 170, 160, 16)) - self.yawVizBar.setMinimum(-45) - self.yawVizBar.setMaximum(45) - self.yawVizBar.setProperty("value", 0) - self.yawVizBar.setOrientation(QtCore.Qt.Horizontal) - self.yawVizBar.setTickPosition(QtWidgets.QSlider.NoTicks) - self.yawVizBar.setObjectName("yawVizBar") - self.thrustVizBar = QtWidgets.QSlider(self.Gamepad) - self.thrustVizBar.setEnabled(False) - self.thrustVizBar.setGeometry(QtCore.QRect(223, 190, 16, 160)) - self.thrustVizBar.setMaximum(60000) - self.thrustVizBar.setProperty("value", 50) - self.thrustVizBar.setOrientation(QtCore.Qt.Vertical) - self.thrustVizBar.setObjectName("thrustVizBar") - self.pitchVizBar = QtWidgets.QSlider(self.Gamepad) - self.pitchVizBar.setEnabled(False) - self.pitchVizBar.setGeometry(QtCore.QRect(783, 180, 16, 160)) - self.pitchVizBar.setMinimum(-30) - self.pitchVizBar.setMaximum(30) - self.pitchVizBar.setProperty("value", 0) - self.pitchVizBar.setOrientation(QtCore.Qt.Vertical) - self.pitchVizBar.setObjectName("pitchVizBar") - self.pb_configThrust = QtWidgets.QPushButton(self.Gamepad) - self.pb_configThrust.setGeometry(QtCore.QRect(246, 280, 141, 25)) - self.pb_configThrust.setObjectName("pb_configThrust") - self.pb_configPitch = QtWidgets.QPushButton(self.Gamepad) - self.pb_configPitch.setGeometry(QtCore.QRect(810, 270, 141, 25)) - self.pb_configPitch.setObjectName("pb_configPitch") - self.noGamepadWarning = QtWidgets.QLabel(self.Gamepad) - self.noGamepadWarning.setGeometry(QtCore.QRect(381, 10, 311, 61)) - font = QtGui.QFont() - font.setPointSize(20) - font.setUnderline(True) - font.setKerning(True) - self.noGamepadWarning.setFont(font) - self.noGamepadWarning.setScaledContents(False) - self.noGamepadWarning.setAlignment(QtCore.Qt.AlignCenter) - self.noGamepadWarning.setObjectName("noGamepadWarning") - self.yawScaleLabel = QtWidgets.QLabel(self.Gamepad) - self.yawScaleLabel.setGeometry(QtCore.QRect(162, 113, 71, 20)) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.yawScaleLabel.sizePolicy().hasHeightForWidth()) - self.yawScaleLabel.setSizePolicy(sizePolicy) - self.yawScaleLabel.setObjectName("yawScaleLabel") - self.gamepadYawScale = QtWidgets.QSpinBox(self.Gamepad) - self.gamepadYawScale.setGeometry(QtCore.QRect(240, 110, 51, 26)) - self.gamepadYawScale.setMinimum(-999) - self.gamepadYawScale.setMaximum(999) - self.gamepadYawScale.setProperty("value", 45) - self.gamepadYawScale.setObjectName("gamepadYawScale") - self.horizontalLayoutWidget = QtWidgets.QWidget(self.Gamepad) - self.horizontalLayoutWidget.setGeometry(QtCore.QRect(160, 390, 791, 151)) - self.horizontalLayoutWidget.setObjectName("horizontalLayoutWidget") - self.horizontalLayout = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget) - self.horizontalLayout.setContentsMargins(11, 11, 11, 11) - self.horizontalLayout.setSpacing(6) - self.horizontalLayout.setObjectName("horizontalLayout") - self.label_6 = QtWidgets.QLabel(self.horizontalLayoutWidget) - self.label_6.setAlignment(QtCore.Qt.AlignCenter) - self.label_6.setWordWrap(True) - self.label_6.setObjectName("label_6") - self.horizontalLayout.addWidget(self.label_6) - self.line_2 = QtWidgets.QFrame(self.horizontalLayoutWidget) - self.line_2.setFrameShape(QtWidgets.QFrame.VLine) - self.line_2.setFrameShadow(QtWidgets.QFrame.Sunken) - self.line_2.setObjectName("line_2") - self.horizontalLayout.addWidget(self.line_2) - spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout.addItem(spacerItem1) - self.label_7 = QtWidgets.QLabel(self.horizontalLayoutWidget) - self.label_7.setAlignment(QtCore.Qt.AlignCenter) - self.label_7.setWordWrap(True) - self.label_7.setObjectName("label_7") - self.horizontalLayout.addWidget(self.label_7) - self.line_3 = QtWidgets.QFrame(self.horizontalLayoutWidget) - self.line_3.setFrameShape(QtWidgets.QFrame.VLine) - self.line_3.setFrameShadow(QtWidgets.QFrame.Sunken) - self.line_3.setObjectName("line_3") - self.horizontalLayout.addWidget(self.line_3) - spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout.addItem(spacerItem2) - self.label_8 = QtWidgets.QLabel(self.horizontalLayoutWidget) - self.label_8.setAlignment(QtCore.Qt.AlignCenter) - self.label_8.setWordWrap(True) - self.label_8.setObjectName("label_8") - self.horizontalLayout.addWidget(self.label_8) - self.gamepadThrustScale = QtWidgets.QSpinBox(self.Gamepad) - self.gamepadThrustScale.setGeometry(QtCore.QRect(342, 228, 71, 26)) - self.gamepadThrustScale.setMinimum(-60000) - self.gamepadThrustScale.setMaximum(60000) - self.gamepadThrustScale.setProperty("value", 60000) - self.gamepadThrustScale.setObjectName("gamepadThrustScale") - self.thrustScaleLabel = QtWidgets.QLabel(self.Gamepad) - self.thrustScaleLabel.setGeometry(QtCore.QRect(247, 230, 191, 20)) - self.thrustScaleLabel.setObjectName("thrustScaleLabel") - self.thrustScaleNote = QtWidgets.QLabel(self.Gamepad) - self.thrustScaleNote.setGeometry(QtCore.QRect(248, 256, 221, 17)) - self.thrustScaleNote.setObjectName("thrustScaleNote") - self.gamepadRollScale = QtWidgets.QSpinBox(self.Gamepad) - self.gamepadRollScale.setGeometry(QtCore.QRect(798, 100, 51, 26)) - self.gamepadRollScale.setMinimum(-999) - self.gamepadRollScale.setMaximum(999) - self.gamepadRollScale.setProperty("value", 30) - self.gamepadRollScale.setObjectName("gamepadRollScale") - self.rollScaleLabel = QtWidgets.QLabel(self.Gamepad) - self.rollScaleLabel.setEnabled(True) - self.rollScaleLabel.setGeometry(QtCore.QRect(720, 103, 71, 20)) - self.rollScaleLabel.setObjectName("rollScaleLabel") - self.gamepadPitchScale = QtWidgets.QSpinBox(self.Gamepad) - self.gamepadPitchScale.setGeometry(QtCore.QRect(895, 239, 51, 26)) - self.gamepadPitchScale.setMinimum(-999) - self.gamepadPitchScale.setMaximum(999) - self.gamepadPitchScale.setProperty("value", 30) - self.gamepadPitchScale.setObjectName("gamepadPitchScale") - self.pitchScaleLabel = QtWidgets.QLabel(self.Gamepad) - self.pitchScaleLabel.setGeometry(QtCore.QRect(812, 242, 211, 20)) - self.pitchScaleLabel.setObjectName("pitchScaleLabel") - self.degPerSecLabel = QtWidgets.QLabel(self.Gamepad) - self.degPerSecLabel.setGeometry(QtCore.QRect(294, 114, 54, 17)) - self.degPerSecLabel.setObjectName("degPerSecLabel") - self.degLabel = QtWidgets.QLabel(self.Gamepad) - self.degLabel.setGeometry(QtCore.QRect(852, 104, 54, 17)) - self.degLabel.setObjectName("degLabel") - self.pitchScaleLabel.raise_() - self.rollScaleLabel.raise_() - self.thrustScaleLabel.raise_() - self.yawScaleLabel.raise_() - self.pb_configRoll.raise_() - self.rollVizBar.raise_() - self.pb_resetConfig.raise_() - self.pb_configYaw.raise_() - self.yawVizBar.raise_() - self.thrustVizBar.raise_() - self.pitchVizBar.raise_() - self.pb_configThrust.raise_() - self.pb_configPitch.raise_() - self.noGamepadWarning.raise_() - self.gamepadYawScale.raise_() - self.horizontalLayoutWidget.raise_() - self.gamepadThrustScale.raise_() - self.thrustScaleNote.raise_() - self.gamepadRollScale.raise_() - self.gamepadPitchScale.raise_() - self.degPerSecLabel.raise_() - self.degLabel.raise_() - self.tabWidget.addTab(self.Gamepad, "") - self.plots = QtWidgets.QWidget() - self.plots.setObjectName("plots") - self.comboBox_logBlocks = QtWidgets.QComboBox(self.plots) - self.comboBox_logBlocks.setGeometry(QtCore.QRect(740, 370, 201, 25)) - self.comboBox_logBlocks.setObjectName("comboBox_logBlocks") - self.comboBox_logBlocks.addItem("") - self.pb_logBlockResume = QtWidgets.QPushButton(self.plots) - self.pb_logBlockResume.setGeometry(QtCore.QRect(740, 470, 80, 25)) - self.pb_logBlockResume.setObjectName("pb_logBlockResume") - self.pb_logBlockPause = QtWidgets.QPushButton(self.plots) - self.pb_logBlockPause.setGeometry(QtCore.QRect(860, 470, 80, 25)) - self.pb_logBlockPause.setObjectName("pb_logBlockPause") - self.pb_logBlockFile = QtWidgets.QPushButton(self.plots) - self.pb_logBlockFile.setGeometry(QtCore.QRect(650, 110, 401, 25)) - self.pb_logBlockFile.setObjectName("pb_logBlockFile") - self.pb_logBlockRefresh = QtWidgets.QPushButton(self.plots) - self.pb_logBlockRefresh.setGeometry(QtCore.QRect(650, 250, 141, 25)) - self.pb_logBlockRefresh.setObjectName("pb_logBlockRefresh") - self.pb_logBlockStop = QtWidgets.QPushButton(self.plots) - self.pb_logBlockStop.setGeometry(QtCore.QRect(910, 250, 141, 25)) - self.pb_logBlockStop.setObjectName("pb_logBlockStop") - self.label_2 = QtWidgets.QLabel(self.plots) - self.label_2.setGeometry(QtCore.QRect(780, 60, 141, 20)) - self.label_2.setObjectName("label_2") - self.logVariableList = QtWidgets.QListWidget(self.plots) - self.logVariableList.setGeometry(QtCore.QRect(0, 40, 511, 721)) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.logVariableList.sizePolicy().hasHeightForWidth()) - self.logVariableList.setSizePolicy(sizePolicy) - self.logVariableList.setObjectName("logVariableList") - self.label_3 = QtWidgets.QLabel(self.plots) - self.label_3.setGeometry(QtCore.QRect(180, 10, 131, 17)) - self.label_3.setObjectName("label_3") - self.tabWidget.addTab(self.plots, "") - self.navigation = QtWidgets.QWidget() - self.navigation.setObjectName("navigation") - self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.navigation) - self.verticalLayout_3.setContentsMargins(11, 11, 11, 11) - self.verticalLayout_3.setSpacing(6) - self.verticalLayout_3.setObjectName("verticalLayout_3") - self.horizontalLayout_3 = QtWidgets.QHBoxLayout() - self.horizontalLayout_3.setSpacing(6) - self.horizontalLayout_3.setObjectName("horizontalLayout_3") - self.verticalLayout_4 = QtWidgets.QVBoxLayout() - self.verticalLayout_4.setSpacing(6) - self.verticalLayout_4.setObjectName("verticalLayout_4") - self.label_1 = QtWidgets.QLabel(self.navigation) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.label_1.sizePolicy().hasHeightForWidth()) - self.label_1.setSizePolicy(sizePolicy) - font = QtGui.QFont() - font.setBold(True) - font.setWeight(75) - self.label_1.setFont(font) - self.label_1.setAlignment(QtCore.Qt.AlignCenter) - self.label_1.setObjectName("label_1") - self.verticalLayout_4.addWidget(self.label_1) - self.rbManualSetpoint = QtWidgets.QRadioButton(self.navigation) - self.rbManualSetpoint.setCheckable(True) - self.rbManualSetpoint.setChecked(True) - self.rbManualSetpoint.setObjectName("rbManualSetpoint") - self.buttonGroup_2 = QtWidgets.QButtonGroup(MainWindow) - self.buttonGroup_2.setObjectName("buttonGroup_2") - self.buttonGroup_2.addButton(self.rbManualSetpoint) - self.verticalLayout_4.addWidget(self.rbManualSetpoint) - self.rbGamepadControl = QtWidgets.QRadioButton(self.navigation) - self.rbGamepadControl.setEnabled(True) - self.rbGamepadControl.setObjectName("rbGamepadControl") - self.buttonGroup_2.addButton(self.rbGamepadControl) - self.verticalLayout_4.addWidget(self.rbGamepadControl) - self.formLayout = QtWidgets.QFormLayout() - self.formLayout.setFieldGrowthPolicy(QtWidgets.QFormLayout.ExpandingFieldsGrow) - self.formLayout.setSpacing(6) - self.formLayout.setObjectName("formLayout") - self.pitchLabel = QtWidgets.QLabel(self.navigation) - self.pitchLabel.setObjectName("pitchLabel") - self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.pitchLabel) - self.pitchSetpointBox = QtWidgets.QLineEdit(self.navigation) - self.pitchSetpointBox.setEnabled(True) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.pitchSetpointBox.sizePolicy().hasHeightForWidth()) - self.pitchSetpointBox.setSizePolicy(sizePolicy) - self.pitchSetpointBox.setObjectName("pitchSetpointBox") - self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.pitchSetpointBox) - self.rollLabel = QtWidgets.QLabel(self.navigation) - self.rollLabel.setObjectName("rollLabel") - self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.rollLabel) - self.rollSetpointBox = QtWidgets.QLineEdit(self.navigation) - self.rollSetpointBox.setEnabled(True) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.rollSetpointBox.sizePolicy().hasHeightForWidth()) - self.rollSetpointBox.setSizePolicy(sizePolicy) - self.rollSetpointBox.setObjectName("rollSetpointBox") - self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.rollSetpointBox) - self.yawLabel = QtWidgets.QLabel(self.navigation) - self.yawLabel.setObjectName("yawLabel") - self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.yawLabel) - self.yawSetpointBox = QtWidgets.QLineEdit(self.navigation) - self.yawSetpointBox.setEnabled(True) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.yawSetpointBox.sizePolicy().hasHeightForWidth()) - self.yawSetpointBox.setSizePolicy(sizePolicy) - self.yawSetpointBox.setObjectName("yawSetpointBox") - self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.yawSetpointBox) - self.tLabel = QtWidgets.QLabel(self.navigation) - self.tLabel.setObjectName("tLabel") - self.formLayout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.tLabel) - self.tActual = QtWidgets.QSlider(self.navigation) - self.tActual.setEnabled(True) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.tActual.sizePolicy().hasHeightForWidth()) - self.tActual.setSizePolicy(sizePolicy) - self.tActual.setMinimumSize(QtCore.QSize(140, 0)) - self.tActual.setToolTipDuration(-1) - self.tActual.setMaximum(60000) - self.tActual.setOrientation(QtCore.Qt.Horizontal) - self.tActual.setTickPosition(QtWidgets.QSlider.TicksBelow) - self.tActual.setTickInterval(30000) - self.tActual.setObjectName("tActual") - self.formLayout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.tActual) - self.angleRadioButton = QtWidgets.QRadioButton(self.navigation) - self.angleRadioButton.setChecked(True) - self.angleRadioButton.setObjectName("angleRadioButton") - self.AngleRateButtonGroup = QtWidgets.QButtonGroup(MainWindow) - self.AngleRateButtonGroup.setObjectName("AngleRateButtonGroup") - self.AngleRateButtonGroup.addButton(self.angleRadioButton) - self.formLayout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.angleRadioButton) - self.rateRadioButton = QtWidgets.QRadioButton(self.navigation) - self.rateRadioButton.setObjectName("rateRadioButton") - self.AngleRateButtonGroup.addButton(self.rateRadioButton) - self.formLayout.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.rateRadioButton) - self.verticalLayout_4.addLayout(self.formLayout) - self.applySetpointButton = QtWidgets.QPushButton(self.navigation) - self.applySetpointButton.setObjectName("applySetpointButton") - self.verticalLayout_4.addWidget(self.applySetpointButton) - self.stopSetpointButton = QtWidgets.QPushButton(self.navigation) - self.stopSetpointButton.setObjectName("stopSetpointButton") - self.verticalLayout_4.addWidget(self.stopSetpointButton) - spacerItem3 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.MinimumExpanding) - self.verticalLayout_4.addItem(spacerItem3) - self.logging_label = QtWidgets.QLabel(self.navigation) - font = QtGui.QFont() - font.setBold(True) - font.setWeight(75) - self.logging_label.setFont(font) - self.logging_label.setAlignment(QtCore.Qt.AlignCenter) - self.logging_label.setObjectName("logging_label") - self.verticalLayout_4.addWidget(self.logging_label) - self.logEntriesComboBox1 = QtWidgets.QComboBox(self.navigation) - self.logEntriesComboBox1.setObjectName("logEntriesComboBox1") - self.logEntriesComboBox1.addItem("") - self.verticalLayout_4.addWidget(self.logEntriesComboBox1) - self.logEntriesComboBox2 = QtWidgets.QComboBox(self.navigation) - self.logEntriesComboBox2.setObjectName("logEntriesComboBox2") - self.logEntriesComboBox2.addItem("") - self.verticalLayout_4.addWidget(self.logEntriesComboBox2) - self.logEntriesComboBox3 = QtWidgets.QComboBox(self.navigation) - self.logEntriesComboBox3.setObjectName("logEntriesComboBox3") - self.logEntriesComboBox3.addItem("") - self.verticalLayout_4.addWidget(self.logEntriesComboBox3) - self.logEntriesComboBox4 = QtWidgets.QComboBox(self.navigation) - self.logEntriesComboBox4.setObjectName("logEntriesComboBox4") - self.logEntriesComboBox4.addItem("") - self.verticalLayout_4.addWidget(self.logEntriesComboBox4) - self.logEntriesComboBox5 = QtWidgets.QComboBox(self.navigation) - self.logEntriesComboBox5.setObjectName("logEntriesComboBox5") - self.logEntriesComboBox5.addItem("") - self.verticalLayout_4.addWidget(self.logEntriesComboBox5) - self.pb_startLog = QtWidgets.QPushButton(self.navigation) - self.pb_startLog.setObjectName("pb_startLog") - self.verticalLayout_4.addWidget(self.pb_startLog) - self.pb_stopLog = QtWidgets.QPushButton(self.navigation) - self.pb_stopLog.setObjectName("pb_stopLog") - self.verticalLayout_4.addWidget(self.pb_stopLog) - self.formLayout1 = QtWidgets.QFormLayout() - self.formLayout1.setFieldGrowthPolicy(QtWidgets.QFormLayout.ExpandingFieldsGrow) - self.formLayout1.setSpacing(6) - self.formLayout1.setObjectName("formLayout1") - self.tsLabel = QtWidgets.QLabel(self.navigation) - self.tsLabel.setObjectName("tsLabel") - self.formLayout1.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.tsLabel) - self.currThrustActual = QtWidgets.QLabel(self.navigation) - self.currThrustActual.setObjectName("currThrustActual") - self.formLayout1.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.currThrustActual) - self.currRollLabel = QtWidgets.QLabel(self.navigation) - self.currRollLabel.setObjectName("currRollLabel") - self.formLayout1.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.currRollLabel) - self.currRollActual = QtWidgets.QLabel(self.navigation) - self.currRollActual.setObjectName("currRollActual") - self.formLayout1.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.currRollActual) - self.currPitchLabel = QtWidgets.QLabel(self.navigation) - self.currPitchLabel.setObjectName("currPitchLabel") - self.formLayout1.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.currPitchLabel) - self.currPitchActual = QtWidgets.QLabel(self.navigation) - self.currPitchActual.setObjectName("currPitchActual") - self.formLayout1.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.currPitchActual) - self.currYawLabel = QtWidgets.QLabel(self.navigation) - self.currYawLabel.setObjectName("currYawLabel") - self.formLayout1.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.currYawLabel) - self.currYawActual = QtWidgets.QLabel(self.navigation) - self.currYawActual.setObjectName("currYawActual") - self.formLayout1.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.currYawActual) - self.currRoleRateLabel = QtWidgets.QLabel(self.navigation) - self.currRoleRateLabel.setObjectName("currRoleRateLabel") - self.formLayout1.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.currRoleRateLabel) - self.currRollRateActual = QtWidgets.QLabel(self.navigation) - self.currRollRateActual.setObjectName("currRollRateActual") - self.formLayout1.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.currRollRateActual) - self.currPitchRateLabel = QtWidgets.QLabel(self.navigation) - self.currPitchRateLabel.setObjectName("currPitchRateLabel") - self.formLayout1.setWidget(5, QtWidgets.QFormLayout.LabelRole, self.currPitchRateLabel) - self.currPitchRateActual = QtWidgets.QLabel(self.navigation) - self.currPitchRateActual.setObjectName("currPitchRateActual") - self.formLayout1.setWidget(5, QtWidgets.QFormLayout.FieldRole, self.currPitchRateActual) - self.currYawRateLabel = QtWidgets.QLabel(self.navigation) - self.currYawRateLabel.setObjectName("currYawRateLabel") - self.formLayout1.setWidget(6, QtWidgets.QFormLayout.LabelRole, self.currYawRateLabel) - self.currYawRateActual = QtWidgets.QLabel(self.navigation) - self.currYawRateActual.setObjectName("currYawRateActual") - self.formLayout1.setWidget(6, QtWidgets.QFormLayout.FieldRole, self.currYawRateActual) - self.verticalLayout_4.addLayout(self.formLayout1) - self.horizontalLayout_3.addLayout(self.verticalLayout_4) - self.verticalLayout_5 = QtWidgets.QVBoxLayout() - self.verticalLayout_5.setSpacing(6) - self.verticalLayout_5.setObjectName("verticalLayout_5") - self.dataPlot = QCustomPlot(self.navigation) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.dataPlot.sizePolicy().hasHeightForWidth()) - self.dataPlot.setSizePolicy(sizePolicy) - self.dataPlot.setObjectName("dataPlot") - self.verticalLayout_5.addWidget(self.dataPlot) - self.horizontalLayout_3.addLayout(self.verticalLayout_5) - self.verticalLayout_3.addLayout(self.horizontalLayout_3) - self.tabWidget.addTab(self.navigation, "") - self.verticalLayout_1.addWidget(self.tabWidget) - MainWindow.setCentralWidget(self.centralWidget) - self.menuBar = QtWidgets.QMenuBar(MainWindow) - self.menuBar.setGeometry(QtCore.QRect(0, 0, 1186, 22)) - self.menuBar.setObjectName("menuBar") - self.menuScripts = QtWidgets.QMenu(self.menuBar) - self.menuScripts.setObjectName("menuScripts") - MainWindow.setMenuBar(self.menuBar) - self.mainToolBar = QtWidgets.QToolBar(MainWindow) - self.mainToolBar.setObjectName("mainToolBar") - MainWindow.addToolBar(QtCore.Qt.TopToolBarArea, self.mainToolBar) - self.statusBar = QtWidgets.QStatusBar(MainWindow) - self.statusBar.setObjectName("statusBar") - MainWindow.setStatusBar(self.statusBar) - self.menuBar.addAction(self.menuScripts.menuAction()) - - self.retranslateUi(MainWindow) - self.tabWidget.setCurrentIndex(4) - QtCore.QMetaObject.connectSlotsByName(MainWindow) - - def retranslateUi(self, MainWindow): - _translate = QtCore.QCoreApplication.translate - MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) - self.backendPath.setText(_translate("MainWindow", "./BackEnd")) - self.chooseBackend.setText(_translate("MainWindow", "Choose...")) - self.pbConnect.setText(_translate("MainWindow", "Connect")) - self.pb_connectTS.setText(_translate("MainWindow", "Connect Test Stand")) - self.pb_disconnectTS.setText(_translate("MainWindow", "Disconnect Test Stand")) - self.pbStop.setText(_translate("MainWindow", "Stop/Disconnect")) - self.tabWidget.setTabText(self.tabWidget.indexOf(self.backend), _translate("MainWindow", "Backend")) - self.groupLabel.setText(_translate("MainWindow", "Group:")) - self.getParamLabel.setText(_translate("MainWindow", "Get Param")) - self.label.setText(_translate("MainWindow", "Entry:")) - self.setParamLabel.setText(_translate("MainWindow", "Set Param")) - self.valueLabel.setText(_translate("MainWindow", "Value:")) - self.groupLabel_set.setText(_translate("MainWindow", "Group:")) - self.label_5.setText(_translate("MainWindow", "Entry:")) - self.valueLabel_2.setText(_translate("MainWindow", "Value:")) - self.pb_getParam.setText(_translate("MainWindow", "Get Param")) - self.pb_setParam.setText(_translate("MainWindow", "Set Param")) - self.tabWidget.setTabText(self.tabWidget.indexOf(self.param), _translate("MainWindow", "Parameters")) - self.pb_configRoll.setText(_translate("MainWindow", "Bind Roll")) - self.pb_resetConfig.setText(_translate("MainWindow", "Reset Configuration")) - self.pb_configYaw.setText(_translate("MainWindow", "Bind Yaw")) - self.pb_configThrust.setText(_translate("MainWindow", "Bind Thrust")) - self.pb_configPitch.setText(_translate("MainWindow", "Bind Pitch")) - self.noGamepadWarning.setText(_translate("MainWindow", "No Gamepad Detected")) - self.yawScaleLabel.setText(_translate("MainWindow", "Yaw Scale:")) - self.label_6.setText(_translate("MainWindow", "Select the input to bind then move the joystick that will control that input to its maximum values.")) - self.label_7.setText(_translate("MainWindow", "The scale of each input controls the maximum values sent to the Crazyflie with max joystick deflection. ")) - self.label_8.setText(_translate("MainWindow", "Negative scale values can be used to invert a joystick if needed.")) - self.thrustScaleLabel.setText(_translate("MainWindow", "Thrust Scale: ")) - self.thrustScaleNote.setText(_translate("MainWindow", "Note: max thrust is 60,000")) - self.rollScaleLabel.setText(_translate("MainWindow", "Roll Scale:")) - self.pitchScaleLabel.setText(_translate("MainWindow", "Pitch Scale: Deg")) - self.degPerSecLabel.setText(_translate("MainWindow", "deg/s")) - self.degLabel.setText(_translate("MainWindow", "deg")) - self.tabWidget.setTabText(self.tabWidget.indexOf(self.Gamepad), _translate("MainWindow", "Gamepad")) - self.comboBox_logBlocks.setItemText(0, _translate("MainWindow", "Logging Block")) - self.pb_logBlockResume.setText(_translate("MainWindow", "Resume")) - self.pb_logBlockPause.setText(_translate("MainWindow", "Pause")) - self.pb_logBlockFile.setText(_translate("MainWindow", "Open Logging Block Setup File")) - self.pb_logBlockRefresh.setText(_translate("MainWindow", "Refresh Log Blocks")) - self.pb_logBlockStop.setText(_translate("MainWindow", "Stop All Log Blocks")) - self.label_2.setText(_translate("MainWindow", "Log Block Commands")) - self.label_3.setText(_translate("MainWindow", "Logging Variables")) - self.tabWidget.setTabText(self.tabWidget.indexOf(self.plots), _translate("MainWindow", "Log Blocks")) - self.label_1.setText(_translate("MainWindow", "Send Setpoint")) - self.rbManualSetpoint.setText(_translate("MainWindow", "Manual Setpoint")) - self.rbGamepadControl.setText(_translate("MainWindow", "Gamepad Control")) - self.pitchLabel.setText(_translate("MainWindow", "Pitch")) - self.pitchSetpointBox.setText(_translate("MainWindow", "0")) - self.rollLabel.setText(_translate("MainWindow", "Roll")) - self.rollSetpointBox.setText(_translate("MainWindow", "0")) - self.yawLabel.setText(_translate("MainWindow", "Yaw")) - self.yawSetpointBox.setText(_translate("MainWindow", "0")) - self.tLabel.setText(_translate("MainWindow", "Thrust")) - self.angleRadioButton.setText(_translate("MainWindow", "Angle")) - self.rateRadioButton.setText(_translate("MainWindow", "Rate")) - self.applySetpointButton.setText(_translate("MainWindow", "Apply")) - self.stopSetpointButton.setText(_translate("MainWindow", "Stop")) - self.logging_label.setText(_translate("MainWindow", "Logging Variables")) - self.logEntriesComboBox1.setCurrentText(_translate("MainWindow", "Logging Variable 1")) - self.logEntriesComboBox1.setItemText(0, _translate("MainWindow", "Logging Variable 1")) - self.logEntriesComboBox2.setItemText(0, _translate("MainWindow", "Logging Variable 2")) - self.logEntriesComboBox3.setItemText(0, _translate("MainWindow", "Logging Variable 3")) - self.logEntriesComboBox4.setItemText(0, _translate("MainWindow", "Logging Variable 4")) - self.logEntriesComboBox5.setItemText(0, _translate("MainWindow", "Logging Variable 5")) - self.pb_startLog.setText(_translate("MainWindow", "Start Logging")) - self.pb_stopLog.setText(_translate("MainWindow", "Stop Logging")) - self.tsLabel.setText(_translate("MainWindow", "Curent Thrust:")) - self.currThrustActual.setText(_translate("MainWindow", "N/A")) - self.currRollLabel.setText(_translate("MainWindow", "Current Roll: ")) - self.currRollActual.setText(_translate("MainWindow", "N/A")) - self.currPitchLabel.setText(_translate("MainWindow", "Current Pitch: ")) - self.currPitchActual.setText(_translate("MainWindow", "N/A")) - self.currYawLabel.setText(_translate("MainWindow", "Current Yaw: ")) - self.currYawActual.setText(_translate("MainWindow", "N/A")) - self.currRoleRateLabel.setText(_translate("MainWindow", "Current Roll Rate:")) - self.currRollRateActual.setText(_translate("MainWindow", "N/A")) - self.currPitchRateLabel.setText(_translate("MainWindow", "Current Pitch Rate:")) - self.currPitchRateActual.setText(_translate("MainWindow", "N/A")) - self.currYawRateLabel.setText(_translate("MainWindow", "Current Yaw Rate:")) - self.currYawRateActual.setText(_translate("MainWindow", "N/A")) - self.tabWidget.setTabText(self.tabWidget.indexOf(self.navigation), _translate("MainWindow", "Control")) - self.menuScripts.setTitle(_translate("MainWindow", "Scripts")) -from qcustomplot import QCustomPlot diff --git a/groundStation/src/backend/backend.c b/groundStation/src/backend/backend.c index a1588fdd73b8b3a6546b3f69e35925d202532a0b..965f026909d5331a4963cbdf19da29abdda478e6 100644 --- a/groundStation/src/backend/backend.c +++ b/groundStation/src/backend/backend.c @@ -128,15 +128,21 @@ static void sig_exit_handler(int s) { keepRunning = 0; } -// Callback to be ran whenever the tracker receives data. -// Currently doing much more than it should. It will be slimmed down -// in the future. +// Callback to be ran whenever the optitrack camera system tracker receives data. static void cb(struct ucart_vrpn_TrackerData * td) { static int count = 0; sendVrpnPacket(td); count++; } +/** + * @brief Main function of backend. After connecting stuff, runs a loop + * that never returns until the program is killed. + * + * @param argc + * @param argv + * @return int + */ int main(int argc, char **argv) { int activity; @@ -378,6 +384,12 @@ int main(int argc, char **argv) return 0; } +/** + * @brief This function takes in the information given by the optitrack VRPN system + * and sends it to the quadcopter. + * + * @param info Gives tracker data of full pose, xyz, roll, pitch, yaw. + */ void sendVrpnPacket(struct ucart_vrpn_TrackerData *info) { uint8_t packet[64]; struct metadata m; @@ -415,6 +427,11 @@ void sendVrpnPacket(struct ucart_vrpn_TrackerData *info) { } } +/** + * @brief Retrieve tracker data from optitrack VRPN camera system. + * + * @param td + */ void getVRPNPacket(struct ucart_vrpn_TrackerData *td) { int status; #if VRPN_DEBUG_PRINT == 1 @@ -424,27 +441,33 @@ void getVRPNPacket(struct ucart_vrpn_TrackerData *td) { } } #endif - //if ((status = ucart_vrpn_tracker_getData(trackables[i].tracker, td)) < 0) { - // TODO - remove tracker instance if((status = ucart_vrpn_tracker_getData(tracker, td)) < 0) { perror("Error receiving VRPN data from tracker..."); keepRunning = 0; } } -/* void getVRPNPacket(struct ucart_vrpn_TrackerData *td, int index) { - int status; - if ((status = ucart_vrpn_tracker_getData(trackables[i].tracker, td)) < 0) { - perror("Error receiving VRPN data from tracker..."); - keepRunning = 0; - } -} */ - +/** + * @brief Print full pose infromation from optitrack VRPN camera system. + * + * @param td + */ void printVrpnData(struct ucart_vrpn_TrackerData * td) { printf("FPS: %lf Pos (xyz): (%lf %lf %lf) Att (pry): (%lf %lf %lf)\n", td->fps, td->x, td->y, td->z, td->pitch, td->roll, td->yaw); } +/** + * @brief This function handles connecting to the custom quadcopter which + * is zybo fpga based. Connection is either an emulated quadcopter to help + * development, or a wifi based connection. I believe the zybo used to have + * bluetooth based communication but I think this was refactored by sdmay17 + * in order to vastly cut latency. I'm not sure when it was completely commented + * out. + * + * @param index Qaducopter index. + * @return int + */ int connectToZybo(int index) { int sock; int status = -1; @@ -557,6 +580,15 @@ int safe_fd_clr(int fd, fd_set* fds, int* max_fd) { return 0; } +/** + * @brief This function is what actually handles communication with the quad + * or adapter over tcp socket. Setup to be able to work with multiple drones + * so it calls a lower level function writeQuadIndex. + * + * @param buf Data to sent to drone + * @param count Size of the buffer + * @return ssize_t Success or no + */ static ssize_t writeQuad(const uint8_t * buf, size_t count) { ssize_t retval; int index = 0; @@ -578,6 +610,15 @@ static ssize_t writeQuad(const uint8_t * buf, size_t count) { return -1; } +/** + * @brief Safely write to quadcopter. This is where we choose either to + * talk to a custom quad or an adapter, however both are TCP sockets. + * + * @param buf Contains data that will be written to quad + * @param count Size of data + * @param index Index of quadcopter, which one are we talking to. + * @return ssize_t + */ static ssize_t writeQuadIndex(const uint8_t * buf, size_t count, int index) { ssize_t retval; @@ -596,8 +637,10 @@ static ssize_t writeQuadIndex(const uint8_t * buf, size_t count, int index) { if (pthread_mutex_lock(&trackables[index].socket_mutex)) { err(-2, "pthrtead_mutex_lock (%s:%d):", __FILE__, __LINE__); } - - //if (trackables[index].isLocal == 1) { + + // It appears that local comm channel is used in the case of either an + // emulated zybo, or other local connections such as an adapter, however + // it is not being used in order to write to an actual adapter. if (trackables[index].isAdapter == 0 && local_comm_channel) { retval = write(trackables[index].fifo_tx, buf, count); } else { @@ -615,6 +658,14 @@ static ssize_t writeQuadIndex(const uint8_t * buf, size_t count, int index) { return retval; } +/** + * @brief Read data from quadcopter or adapter safely. + * + * @param buf Buffer that data will be written to. + * @param count Size of data to grab from socket. + * @param index Quadcopter index, which one are we talking to + * @return ssize_t + */ static ssize_t readQuad(char * buf, size_t count, int index) { ssize_t retval; if (trackables[index].isAdapter == 0 && getenv(NOQUAD_ENV)) { @@ -632,6 +683,12 @@ static ssize_t readQuad(char * buf, size_t count, int index) { return retval; } +/** + * @brief Open a frontend client connection + * + * @param fd Frontend index + * @return int Success or failure + */ static int new_client(int fd) { ssize_t new_slot = -1; for (ssize_t i = 0; i < MAX_CLIENTS; i++) { @@ -701,6 +758,12 @@ static int clientRemovePendResponses(int fd, uint16_t packet_id) { return -1; } +/** + * @brief Eliminates client from being able to be received from. + * + * @param fd Frontend client index + * @return int Success or failure + */ static int remove_client(int fd) { ssize_t slot = get_client_index(fd); if(slot == -1) @@ -719,6 +782,12 @@ static int remove_client(int fd) { return 0; } +/** + * @brief Thread safely close the frontend client + * + * @param fd Frontend client id + * @param mutexLock Frontend lock + */ static void safe_close_fd(int fd, pthread_mutex_t *mutexLock) { if (pthread_mutex_lock(mutexLock)) { err(-2, "pthrtead_mutex_lock (%s:%d):", __FILE__, __LINE__); @@ -729,6 +798,18 @@ static void safe_close_fd(int fd, pthread_mutex_t *mutexLock) { } } +/** + * @brief Receive data from the client. This function sucks and NEEDS to be + * broken up into smaller chunks. + * + * This is a BIG function, but essentially it receives a command from the frontend + * socket, and then encodes the appropriate information into a command that is then + * written to the quadcopter, be that actual custom quadcopter or adapter. This is + * a function that appears to never return until a command is received or there is an + * error. + * + * @param fd Frontend client index + */ static void client_recv(int fd) { char * buffer; ssize_t len_pre; @@ -866,14 +947,6 @@ static void client_recv(int fd) { ssize_t psize; result = EncodeLogBlockCommand(&m, data, 128, buffer); - /* - result = 1; - int8_t command, id; - - sscanf(buffer, "logblockcommand %hhd %hhd", &command, &id); - data[0] = command; - data[1] = id; - */ if (result < 0) { warnx("Big problems. client_recv. EncodeMetaData"); @@ -920,14 +993,6 @@ static void client_recv(int fd) { ssize_t psize; result = EncodeGetLogfile(&m, data, 128, buffer); - /* - result = 1; - int8_t command, id; - - sscanf(buffer, "logblockcommand %d %d", &command, &id); - data[0] = command; - data[1] = id; - */ if (result < 0) { warnx("Big problems. client_recv. EncodeMetaData"); @@ -940,10 +1005,6 @@ static void client_recv(int fd) { } m.msg_id = currMessageID++; - /* - m.msg_type = MAX_TYPE_ID+2; - m.data_len = 2; - */ if ((psize = EncodePacket(packet, 150, &m, data)) < 0) { warnx("Big problems. client_recv. EncodePacket"); @@ -1043,6 +1104,14 @@ static void client_recv(int fd) { } } +/** + * @brief This function is where data is actually received from the quadcopter. + * This is basically exclusively through the use of log files as most actual data + * received back from the quad is in the form of log files, be it actual log data, + * or parameter data or so on. + * + * @param index Which quad trackable are we talking to + */ static void quad_recv(int index) { static unsigned char respBuf[CMD_MAX_LENGTH]; static size_t respBufLen; @@ -1074,6 +1143,7 @@ static void quad_recv(int index) { while(respBufLen) { datalen = DecodePacket(&m, data, CMD_MAX_LENGTH, respBuf, respBufLen); + // handle bad data if (datalen == -1) { warnx("No start Byte"); for (size_t i = 0; i < respBufLen; ++i) { @@ -1109,6 +1179,8 @@ static void quad_recv(int index) { respBufLen -= packetlen; char * debug_string; + // Depending on the message type, handle differently. A bunch of message types are + // unimplemented though switch (m.msg_type) { case DEBUG_ID: /* in case of debug. Quad send null terminated string in data */ @@ -1135,7 +1207,7 @@ static void quad_recv(int index) { break; case SEND_RT_ID: quadlog_file = fopen("quad_log_data.txt", "w"); - + // TODO why is this commented out??? It looks like this is where the file is actually written as well? //fwrite((char *) formatted_data, sizeof(char), m.data_len, quadlog_file); fclose(quadlog_file); //free(formatted_data); @@ -1163,6 +1235,13 @@ static void quad_recv(int index) { } } +/** + * @brief Called by quad_recv. Decodes data from message and routes + * data back to frontend client. + * + * @param m + * @param data + */ static void handleResponse(struct metadata *m, uint8_t * data) { ssize_t result = 0; @@ -1205,8 +1284,6 @@ static void handleResponse(struct metadata *m, uint8_t * data) return; } - // printf("msg to client = '%s'\n", buffer); - for(int fd = 0; fd <= max_fd; ++fd) { if (get_client_index(fd) > -1) { clientRemovePendResponses(fd, m->msg_id); diff --git a/pycrocart/.gitignore b/pycrocart/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..fa092e1157f8a74972c225495d8a0d9f1680ba57 --- /dev/null +++ b/pycrocart/.gitignore @@ -0,0 +1,5 @@ +venv/ +cache/ +.idea/ +__cache__/ +.qt_for_python/ \ No newline at end of file diff --git a/pycrocart/ControlTab.py b/pycrocart/ControlTab.py new file mode 100644 index 0000000000000000000000000000000000000000..0c6bb3e6b68659eef0676bda97adfbd2e132b63a --- /dev/null +++ b/pycrocart/ControlTab.py @@ -0,0 +1,111 @@ +from PyQt5.QtWidgets import QPushButton, QGridLayout, QWidget, QVBoxLayout +from PyQt5.QtCore import QTimer +from PlottingWindow import PlottingWindow +from LoggingSelectionMenu import LoggingSelectionMenu +from queue import Queue +import queue +from CrazyflieProtoConnection import CrazyflieProtoConnection +from SetpointMenu import SetpointMenu +from SetpointHandler import SetpointHandler +from cfclient.utils.input import JoystickReader + + +class ControlTab(QWidget): + def __init__(self, logging_queue: Queue, + setpoint_handler: SetpointHandler, joystick: JoystickReader, + cf: CrazyflieProtoConnection): + """ + Initialize the control tab. + + :param logging_queue: Holds data coming from crazyflieProtoConnection + that has been logged. This data is handled more by the PlottingWindow + and LoggingSelectionMenu but those are both child widgets of the + Control tab. + :param setpoint_handler: When a setpoint is sent from the control tab, + it is not sent to the crazyflieProtoConnection directly, instead it is + sent to the setpoint_handler which is able to work with timing a bit + more accurately than the GUI should need to. + :param joystick: Connection to any gamepad that is connected. + :param cf: Reference to the crazyflieProtoConnection, used to send + setpoints. + """ + super().__init__() + + self.logging_queue = logging_queue + self.graph_data = False + + self.cf = cf + + layout = QGridLayout() + left_side_vertical_layout = QVBoxLayout() + + self.plotting_window = PlottingWindow() + self.setpoint_menu = SetpointMenu(setpoint_handler, joystick) + self.logging_menu = LoggingSelectionMenu(self.external_press_pause) + + win2 = QWidget() + win2.setLayout(left_side_vertical_layout) + win2.setMaximumWidth(300) + + layout.addWidget(win2, 1, 1) + layout.addWidget(self.plotting_window, 1, 2) + + self.play_or_pause_logging_button = \ + QPushButton("Begin Graphing Logging") + self.play_or_pause_logging_button.clicked.connect( + self.play_pause_button_callback) + + left_side_vertical_layout.addWidget(self.setpoint_menu, 1) + left_side_vertical_layout.addWidget( + self.play_or_pause_logging_button, 2) + left_side_vertical_layout.addWidget(self.logging_menu, 3) + + # Set up the timer to update the plot + self.timer = QTimer() + self.timer.timeout.connect(self.update_plot_outer) + self.timer.start(50) + + # Set the window properties + self.setLayout(layout) + self.setWindowTitle("Sine Wave Plot") + # self.setGeometry(100, 100, 800, 600) + self.show() + + def update_plot_outer(self): + """ Empty out logging queue, and update Plotting Window. """ + + not_empty = True + while not_empty: + try: + value = self.logging_queue.get_nowait() + data = value['data'] + timestamp = value['timestamp'] + + # Uses the logging menu to check if the signal has been + # selected to be graphed or not. If no, axis = None. + axis = self.logging_menu.get_axis_of_signal(value['signal']) + + if axis is not None and self.graph_data: + self.plotting_window.update_plot(data, timestamp, axis) + + except queue.Empty: # done emptying logging queue + not_empty = False + + def external_press_pause(self): + """ Made so the logging selection menu can press the pause button + programatically whenever configurations change. """ + self.play_or_pause_logging_button.click() + + def play_pause_button_callback(self): + """ This function handles getting the logging to stop or to continue + graphing. Called when button is clicked. """ + + if self.graph_data: + self.play_or_pause_logging_button.setText( + "Continue Graphing Logging") + self.graph_data = False + self.cf.stop_logging() + else: + self.play_or_pause_logging_button.setText("Pause Logging") + self.graph_data = True + self.cf.start_logging() diff --git a/pycrocart/CrazyflieProtoConnection.py b/pycrocart/CrazyflieProtoConnection.py new file mode 100644 index 0000000000000000000000000000000000000000..b2a0ed79e8ec13e37bcb832206f0b8122dbba8ab --- /dev/null +++ b/pycrocart/CrazyflieProtoConnection.py @@ -0,0 +1,221 @@ +""" +Crazyflie Proto Connection handles the actual interaction with the crazyflie. +The only reason it exists is to serve as an intermediary between the frontend +and the crazyflie itself so that it can handle all interactions with the +cflib library and that the GUI doesn't have to. + +If one wanted to slot this GUI into the existing microcart infrastructure, +CrazyflieProtoConnection could be rewritten to interface to frontend +commands, and to connect to the backend, and the crazyflie adapter, +and crazyflie groundstation. + +""" + + +from time import time +from typing import List +import time + +import cflib.crtp +from cflib.crazyflie import Crazyflie +from cflib.crazyflie.syncCrazyflie import SyncCrazyflie +from queue import Queue + +from cflib.crazyflie.log import LogConfig + + +class CrazyflieProtoConnection: + """ + Handles all interactions with cflib. + """ + + def __init__(self): + """ + Initialize the start time, the logging queue which is given to the + plotting window, and set the synchronous crazyflie connection by + default to None. This should be set to the crazyflie when connecting + to one. + """ + + # Get the start time, so that the timestamp returned to the user will + # be in seconds since startup. + self.start_time = time.time() + + self.logging_queue = Queue() + + self.scf = None + self.is_connected = False + self.param_callback_count = 0 + self.logging_configs = [] + + # self.timer = QTimer() + # self.timer.timeout.connect(self.update_plot) + # self.timer.start(50) + + def check_link_is_alive_callback(self): + """ Periodically investigate if the link is alive """ + if self.scf: + if not self.scf.is_link_open(): + self.is_connected = False + + def logging_callback(self, _timestamp, data, _logconf): + """ Whenever data comes in from the logging, it is sent here, + which routes it into our specific format for the logging queue. """ + + timestamp1 = time.time() - self.start_time + + for key in data.keys(): + value_pair = {'timestamp': timestamp1, 'data': data[key], + 'signal': key} + self.logging_queue.put(value_pair) + + def param_set_value(self, group: str, name: str, value: float): + """ Set a crazyflie parameter value. """ + + try: + if self.scf.is_link_open(): + full_name = group + "." + name + cf = self.scf.cf + cf.param.add_update_callback(group=group, name=name, + cb=self.done_setting_param_value) + + cf.param.set_value(full_name, value) + + # Don't return until the parameter is done getting set + while self.param_callback_count < 1: + time.sleep(0.01) + + self.param_callback_count = 0 + except AttributeError: + print("Nothing connected") + + def done_setting_param_value(self, *_args): + """ Callback when done setting a parameter value. """ + print("Done setting param") + self.param_callback_count += 1 + + def param_get_value(self, group: str, name: str): + """ Retrieve parameter value from crazyflie toc. """ + try: + if self.scf.is_link_open(): + return self.scf.cf.param.values[group][name] + + except AttributeError: + pass + return -1.234567890 # 1234567890 should be pretty obvious that + # something has gone wrong. + + def get_logging_toc(self): + """ Retrieve entire logging table of contents. Used in order to + display list in logging tab. """ + + try: + if self.scf.is_link_open(): + tocFull = self.scf.cf.log.toc.toc + toc = [] + for key in tocFull.keys(): + for inner_key in tocFull[key].keys(): + # concatenate group name with parameter name. + full_name = key + "." + inner_key + toc.append(full_name) + + return toc + else: + return [] + except AttributeError: + pass + return [] + + def get_param_toc(self): + """ Get the names of all groups available for parameters on the + crazyflie. Used to populate parameter group list on parameter tab. """ + + try: + if self.scf.is_link_open(): + toc = self.scf.cf.param.values + + return toc + except AttributeError: + pass + return {} + + def add_log_config(self, name: str, period: int, variables: List[str]): + """ Add a logging config. Used from logging tab when refreshing + logging variables. Add callback to route logged data to logging + queue. """ + + print("Name: " + name + ", period: " + str(period) + ", variables: " + + str(variables)) + logging_group = LogConfig(name=name, period_in_ms=period) + + for variable in variables: + logging_group.add_variable(variable, 'float') + + self.logging_configs.append(logging_group) + self.logging_configs[-1].data_received_cb.add_callback( + self.logging_callback) + self.scf.cf.log.add_config(self.logging_configs[-1]) + + def clear_logging_configs(self): + """ Stop logging and clear configuration. Used when refreshing + logging to stop anything that has configured to be logged from + logging. """ + + self.stop_logging() + self.logging_configs = [] + # done refreshing toc is a callback function that is triggered when + # refresh toc is done executing. + self.scf.cf.log.refresh_toc( + self.done_refreshing_toc, self.scf.cf.log._toc_cache) + # Blocks until toc is done refreshing. + while self.param_callback_count < 1: + time.sleep(0.01) + self.param_callback_count = 0 + + # grabs new toc values + self.scf.wait_for_params() + + def done_refreshing_toc(self, *_args): + """ Callback for flow control, increments param callback count to + allow exit of while loop. """ + self.param_callback_count = 1 + + def start_logging(self): + """ Begins logging all configured logging blocks. This is used from + the controls tab when hitting begin logging. """ + for i in range(0, len(self.logging_configs)): + self.logging_configs[i].start() + + def stop_logging(self): + """ Stops logging all configured logging blocks. This is used from + the controls tab when hitting pause logging. """ + for i in range(0, len(self.logging_configs)): + self.logging_configs[i].stop() + + def connect(self, uri: str): + """ + Handles connecting to a crazyflie. Bitcraze has excellent + documentation on how to use the synchronous crazyflie object in order + to send setpoints, set parameters or retrieve logging. + :param uri: Radio channel + """ + cflib.crtp.init_drivers() + self.scf = SyncCrazyflie(uri, cf=Crazyflie(rw_cache='./cache')) + self.scf.open_link() + self.scf.wait_for_params() + self.is_connected = True + + def disconnect(self): + """ Disconnect from crazyflie. """ + print("Disconnect quad") + if self.is_connected: + self.scf.close_link() + self.scf = None + self.is_connected = False + + @staticmethod + def list_available_crazyflies(): + """ Lists crazyflies that are on and within range. """ + cflib.crtp.init_drivers() # run this again just in case you plug the + # dongle in + return cflib.crtp.scan_interfaces() diff --git a/pycrocart/Examples/Connection.py b/pycrocart/Examples/Connection.py new file mode 100644 index 0000000000000000000000000000000000000000..047d8996ac5a15205e6284728e799907bfc26212 --- /dev/null +++ b/pycrocart/Examples/Connection.py @@ -0,0 +1,28 @@ +from PyQt5.QtWidgets import QPushButton, QGridLayout, QWidget, QHBoxLayout, QComboBox + + +class Connection(QWidget): + + def __init__(self): + + super().__init__() + + self.dropdown = QComboBox(self) + self.dropdown.addItem("radio://45/") + self.dropdown.addItem("radio://100/") + self.dropdown.setFixedWidth(150) + + self.connect = QPushButton("Connect", self) + self.connect.setFixedWidth(100) + + self.scan = QPushButton("Scan", self) + self.scan.setFixedWidth(100) + + button_layout = QHBoxLayout() + button_layout.addWidget(self.dropdown) + button_layout.addWidget(self.connect) + button_layout.addWidget(self.scan) + button_layout.addStretch(1) + + self.show() + diff --git a/pycrocart/Examples/Readme.md b/pycrocart/Examples/Readme.md new file mode 100644 index 0000000000000000000000000000000000000000..bbda5da7df6cdebc97e35494c1c396f0e8c716ca --- /dev/null +++ b/pycrocart/Examples/Readme.md @@ -0,0 +1,2 @@ +This folder contains several scripts were made in an attempt to learn cflib, +PyQT, or tkinter better along the way in developing pycrocart. diff --git a/pycrocart/Examples/animated_graph_example.py b/pycrocart/Examples/animated_graph_example.py new file mode 100644 index 0000000000000000000000000000000000000000..81aa709c61b98ad00c6fcfeaf3306cc1686a7864 --- /dev/null +++ b/pycrocart/Examples/animated_graph_example.py @@ -0,0 +1,44 @@ +import math +import time +import matplotlib +from matplotlib import pyplot as plt +# matplotlib.use('module://backend_interagg') + +if __name__ == '__main__': + + + print("Hello world!") + + xs = [] + for i in range(0, 100): + xs.append(i) + ys = [0.0] * 100 + y2s = [0.0] * 100 + + plt.ion() + figure, ax = plt.subplots(figsize=(10, 8)) + line1, = ax.plot(xs, ys, '-o') + line2, = ax.plot(xs, y2s, '-o') + # plt.show() + #plt.title("Distance vs. time", fontsize=20) + #plt.xlabel("time") + #plt.ylabel("distance (inches)") + + for i in range(0, 1000): + ys[0:len(ys) - 1] = ys[1:len(ys)] + ys[int(len(ys) - 1)] = i*i*math.sin(i/8) + + y2s[0:len(y2s)-1] = y2s[1:len(y2s)] + y2s[int(len(y2s)-1)] = 0.5*i*i*math.sin(i/4) + + line1.set_xdata(xs) + line1.set_ydata(ys) + line2.set_ydata(y2s) + figure.plotting_window.draw() + figure.plotting_window.flush_events() + if min(ys) != max(ys): + plt.ylim(1.5*min(ys), 1.5*max(ys)) + # time.sleep(0.1) + + + diff --git a/pycrocart/Examples/cflib_tests.py b/pycrocart/Examples/cflib_tests.py new file mode 100644 index 0000000000000000000000000000000000000000..c8bb72815f8beaaceb7407944765469caeb04c94 --- /dev/null +++ b/pycrocart/Examples/cflib_tests.py @@ -0,0 +1,4 @@ +import cflib + + + diff --git a/pycrocart/Examples/control_window_example.py b/pycrocart/Examples/control_window_example.py new file mode 100644 index 0000000000000000000000000000000000000000..b07511353a2343f1d9ec1538440b23d18c4e6a05 --- /dev/null +++ b/pycrocart/Examples/control_window_example.py @@ -0,0 +1,120 @@ +import sys +import numpy as np +from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout, QLabel, \ + QLineEdit, QSlider, QPushButton +from PyQt5.QtGui import QPainter, QPen, QColor +from PyQt5.QtCore import Qt, QTimer +from time import time +import pyqtgraph as pg + + +class MainWindow(QWidget): + def __init__(self): + super().__init__() + + # Set up the layout + layout = QGridLayout() + + # Add the entry boxes + yaw_label = QLabel("Yaw:") + pitch_label = QLabel("Pitch:") + roll_label = QLabel("Roll:") + self.yaw_box = QLineEdit() + self.pitch_box = QLineEdit() + self.roll_box = QLineEdit() + + layout.addWidget(yaw_label, 0, 0) + layout.addWidget(self.yaw_box, 0, 1) + layout.addWidget(pitch_label, 1, 0) + layout.addWidget(self.pitch_box, 1, 1) + layout.addWidget(roll_label, 2, 0) + layout.addWidget(self.roll_box, 2, 1) + + # Add the sliders and thrust label + thrust_label = QLabel("Thrust:") + self.thrust_slider = QSlider(Qt.Horizontal) + self.thrust_slider.setMinimum(0) + self.thrust_slider.setMaximum(100) + + layout.addWidget(thrust_label, 3, 0) + layout.addWidget(self.thrust_slider, 3, 1) + + # Add the print button + self.print_button = QPushButton("Print") + self.print_button.clicked.connect(self.print_entry_boxes) + + layout.addWidget(self.print_button, 4, 0) + + # Add the plot window + self.plot_widget = pg.PlotWidget() + layout.addWidget(self.plot_widget, 0, 2, 5, 1) + + # Set the background color of the plot widget to white + self.plot_widget.setBackground('w') + + # Set up the sine wave data and plot + self.plot_data = np.zeros((100, 5)) + self.plot_curves = [self.plot_widget.plot(self.plot_data[:, i], + pen=pg.mkPen(pg.intColor(i), + width=2)) for i + in range(5)] + + # Set up the timer to update the plot + self.timer = QTimer() + self.timer.timeout.connect(self.update_plot) + self.timer.start(50) + + # Set the layout + self.setLayout(layout) + + def update_plot(self): + # Shift the data and add a new point + self.plot_data[:-1] = self.plot_data[1:] + self.plot_data[-1] = np.sin( + 2 * np.pi * np.array([1, 2, 3, 4, 5]) * time() / 10) + + # Update the plot + for i in range(5): + self.plot_curves[i].setData(self.plot_data[:, i]) + + def print_entry_boxes(self): + # Get the contents of the entry boxes + yaw_text = self.yaw_box.text() + pitch_text = self.pitch_box.text() + roll_text = self.roll_box.text() + + # Attempt to parse the contents as floats + try: + yaw = float(yaw_text) + pitch = float(pitch_text) + roll = float(roll_text) + + # If parsing was successful, print the values to the terminal + print("Yaw:", yaw) + print("Pitch:", pitch) + print("Roll:", roll) + except ValueError: + # If parsing failed, highlight the boxes in red + if not yaw_text.isnumeric(): + self.yaw_box.setStyleSheet("background-color: red;") + if not pitch_text.isnumeric(): + self.pitch_box.setStyleSheet("background-color: red;") + if not roll_text.isnumeric(): + self.roll_box.setStyleSheet("background-color: red;") + else: + # If parsing was successful, reset the box styles and print the + # values + self.yaw_box.setStyleSheet("") + self.pitch_box.setStyleSheet("") + self.roll_box.setStyleSheet("") + print("Yaw:", yaw) + print("Pitch:", pitch) + print("Roll:", roll) + + +# Start the application +if __name__ == '__main__': + app = QApplication(sys.argv) + window = MainWindow() + window.show() + sys.exit(app.exec_()) diff --git a/pycrocart/Examples/crazyflieTestScript.py b/pycrocart/Examples/crazyflieTestScript.py new file mode 100644 index 0000000000000000000000000000000000000000..427d1d848c2007952d3d11c5dd067187d5b4a8ba --- /dev/null +++ b/pycrocart/Examples/crazyflieTestScript.py @@ -0,0 +1,78 @@ +import logging +import time +import cflib.crtp +from cflib.crazyflie import Crazyflie +from cflib.crazyflie.syncCrazyflie import SyncCrazyflie + +# Only output errors from the logging framework +logging.basicConfig(level=logging.ERROR) + + +def simple_connect(): + + print("Yeah, I'm connected! :D") + time.sleep(3) + print("Now I will disconnect :'(") + + +def log_stab_callback(timestamp, data, logconf): + print('[%d][%s]: %s' % (timestamp, logconf.name, data)) + + +def simple_log_async(scf1, logconf): + cf = scf1.cf + cf.log.add_config(logconf) + logconf.data_received_cb.add_callback(log_stab_callback) + logconf.start() + time.sleep(50) + logconf.stop() + + +def get_param_toc(scf1): + + toc = scf1.cf.param.values + print(len(toc)) + + return toc + + +def get_logging_toc(scf1): + + toc = scf1.cf.log.toc.toc + print(len(toc)) + + return toc + + +def set_param(scf1, group: str, name: str, value: float): + + full_name = group + '.' + name + scf1.cf.param.add_update_callback(group=group, name=name, cb=done_set_param) + time.sleep(1) + scf1.cf.param.set_value(full_name, value) + time.sleep(1) + + +def done_set_param(*_args): + print("Done setting param") + + +def find_available_crazyflies(): + cflib.crtp.init_drivers() + + print("Scanning interfaces for Crazyflies...") + available = cflib.crtp.scan_interfaces() + print("Crazyflies found:") + for i in available: + print(i[0]) + + +if __name__ == '__main__': + # Initialize the low-level drivers + cflib.crtp.init_drivers() + uri = 'radio://0/75/2M/E7E7E7E7E7' + scf = SyncCrazyflie(uri, cf=Crazyflie(rw_cache='./cache')) + scf.open_link() + scf.wait_for_params() + + print("Done") diff --git a/pycrocart/Examples/defaultCommanderTestScript.py b/pycrocart/Examples/defaultCommanderTestScript.py new file mode 100644 index 0000000000000000000000000000000000000000..81b7d04d9d0a03fae2946427115cca29d62decce --- /dev/null +++ b/pycrocart/Examples/defaultCommanderTestScript.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# +# || ____ _ __ +# +------+ / __ )(_) /_______________ _____ ___ +# | 0xBC | / __ / / __/ ___/ ___/ __ `/_ / / _ \ +# +------+ / /_/ / / /_/ /__/ / / /_/ / / /_/ __/ +# || || /_____/_/\__/\___/_/ \__,_/ /___/\___/ +# +# Copyright (C) 2017 Bitcraze AB +# +# Crazyflie Nano Quadcopter Client +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +import logging +import sys +import time +from threading import Event + +import cflib.crtp +from cflib.crazyflie import Crazyflie +from cflib.crazyflie.log import LogConfig +from cflib.crazyflie.syncCrazyflie import SyncCrazyflie +from cflib.positioning.motion_commander import MotionCommander +from cflib.crazyflie.commander import Commander +from cflib.utils import uri_helper + +import uCartCommander + +URI = uri_helper.uri_from_env(default='radio://0/75/2M/E7E7E7E7E7') + +DEFAULT_HEIGHT = 0.5 +BOX_LIMIT = 0.5 + + +if __name__ == '__main__': + cflib.crtp.init_drivers() + + with SyncCrazyflie(URI, cf=Crazyflie(rw_cache='./cache')) as scf: + + scf.cf.commander = uCartCommander.Commander(scf.cf) + print("start") + scf.cf.commander.send_setpoint(0, 0, 0, 0) + time.sleep(0.1) + print("setpoint") + scf.cf.commander.send_attitude_rate_setpoint(0, 0, 0, 12000) + print("sleep") + time.sleep(5) + print("stop") + scf.cf.commander.send_stop_setpoint() diff --git a/pycrocart/Examples/gridExample.py b/pycrocart/Examples/gridExample.py new file mode 100644 index 0000000000000000000000000000000000000000..2be0afdd7fc35511ec3c093a235435b1af8fb42e --- /dev/null +++ b/pycrocart/Examples/gridExample.py @@ -0,0 +1,24 @@ +import sys +from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, \ + QGridLayout, QWidget +from PyQt5.QtCore import Qt, QTimer + + +def window(): + app = QApplication(sys.argv) + win = QWidget() + grid = QGridLayout() + + for i in range(1, 5): + for j in range(1, 5): + grid.addWidget(QPushButton("B" + str(i) + str(j)), i, j) + + win.setLayout(grid) + win.setGeometry(100, 100, 200, 100) + win.setWindowTitle("PyQt") + win.show() + sys.exit(app.exec_()) + + +if __name__ == '__main__': + window() \ No newline at end of file diff --git a/pycrocart/Examples/plottingExample.py b/pycrocart/Examples/plottingExample.py new file mode 100644 index 0000000000000000000000000000000000000000..a7e722052dfd5b115cca9673d512a6136970f1c7 --- /dev/null +++ b/pycrocart/Examples/plottingExample.py @@ -0,0 +1,172 @@ +import sys +from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QGridLayout, \ + QWidget, QVBoxLayout, QComboBox +from PyQt5.QtCore import Qt, QTimer +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas +from matplotlib.figure import Figure +import numpy as np +from time import time +import pyqtgraph as pg + + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + + # Create the plot widget + self.canvas = PlotCanvas() + win = QWidget() + win2 = QWidget() + self.setCentralWidget(win) + + layout = QGridLayout() + left_side_vertical_layout = QVBoxLayout() + + self.graph_data = True + + self.print_button = QPushButton("Start") + self.print_button.clicked.connect(self.print_entry_boxes) + left_side_vertical_layout.addWidget(self.print_button, 1) + win2.setLayout(left_side_vertical_layout) + layout.addWidget(win2, 1, 1) + layout.addWidget(self.canvas, 1, 2) + + self.cb1 = QComboBox() + self.line1 = "Sine" + self.cb1.addItems(["Sine", "2Sine", "Ramp", "2Ramp", "Square"]) + left_side_vertical_layout.addWidget(self.cb1, 2) + self.cb1.currentIndexChanged.connect(self.selectionchange) + + self.cb2 = QComboBox() + self.line2 = "Sine" + self.cb2.addItems(["Sine", "2Sine", "Ramp", "2Ramp", "Square"]) + left_side_vertical_layout.addWidget(self.cb2, 3) + + self.cb3 = QComboBox() + self.line3 = "Sine" + self.cb3.addItems(["Sine", "2Sine", "Ramp", "2Ramp", "Square"]) + left_side_vertical_layout.addWidget(self.cb3, 4) + + self.cb4 = QComboBox() + self.line4 = "Sine" + self.cb4.addItems(["Sine", "2Sine", "Ramp", "2Ramp", "Square"]) + left_side_vertical_layout.addWidget(self.cb4, 5) + + self.cb5 = QComboBox() + self.line5 = "Sine" + self.cb5.addItems(["Sine", "2Sine", "Ramp", "2Ramp", "Square"]) + left_side_vertical_layout.addWidget(self.cb5, 6) + + # Set up the timer to update the plot + self.timer = QTimer() + self.timer.timeout.connect(self.update_plot_outer) + self.timer.start(50) + + # Set the window properties + win.setLayout(layout) + self.setWindowTitle("Sine Wave Plot") + self.setGeometry(100, 100, 800, 600) + self.show() + + def update_plot_outer(self): + if self.graph_data: + + if self.cb1.currentText() == "Sine": + data1 = np.sin(2 * np.pi * time() / 10) + elif self.cb1.currentText() == "2Sine": + data1 = 2 * np.sin(2 * np.pi * time() / 10) + elif self.cb1.currentText() == "Ramp": + data1 = (time()) % 10 + elif self.cb1.currentText() == "2Ramp": + data1 = 2 * (time()) % 10 + elif self.cb1.currentText() == "Square": + data1 = (time()) % 2 + + if data1 > 1: + data1 = 1 + else: + data1 = 0 + + + + data2 = np.sin(3 * np.pi * time() / 10) + data3 = np.sin(4 * np.pi * time() / 10) + data4 = np.sin(5 * np.pi * time() / 10) + data5 = np.sin(6 * np.pi * time() / 10) + + data = [data1, data2, data3, data4, data5] + + self.canvas.update_plot(data) + + def selectionchange(self): + print("Change") + + + def print_entry_boxes(self): + print("Hello world!") + + if self.graph_data: + self.print_button.setText("Play") + self.graph_data = False + else: + self.print_button.setText("Pause") + self.graph_data = True + + +class PlotCanvas(pg.PlotWidget): + def __init__(self, parent=None, num_axes=5, width=5, height=4, dpi=100): + super().__init__(parent=parent, width=width, height=height, dpi=dpi) + self.setLabel('bottom', 'Time (s)') + self.setLabel('left', 'Amplitude') + self.setLabel('top', 'Sine Wave Plot') + self._num_axes = num_axes + + self.current_iter = 0 + + # Set the background color of the plot widget to white + self.setBackground('w') + + if num_axes > 0: + # Set up the sine wave data and plot + self.plot_data = np.zeros((0, num_axes)) + self.plot_curves = \ + [self.plot(self.plot_data[:, i], + pen=pg.mkPen(pg.intColor(i), width=2), symbol='o', + symbolSize=5) + for i + in range(num_axes)] + + def update_num_axes(self): + num_axes = 0 + if num_axes < 0: + raise ValueError("num_axes must be greater than 0!") + + if num_axes != self._num_axes: + if num_axes == 0: # turn it all off + self.clear() + else: + pass + + def update_plot(self, input_data: list): + # Shift the data and add a new point + # self.plot_data[:-1] = self.plot_data[1:] + self.plot_data = np.append(self.plot_data, [input_data], axis=0) + + # Update the plot + for i in range(5): + self.plot_curves[i].setData(self.plot_data[:, i]) + + self.current_iter += 1 + + if self.current_iter < 100: + self.setXRange(0, 100, padding=0) + else: + self.setXRange(self.current_iter-100, self.current_iter, padding=0) + + + +# Start the application +if __name__ == '__main__': + app = QApplication(sys.argv) + window = MainWindow() + sys.exit(app.exec_()) diff --git a/pycrocart/Examples/pyQtExample.py b/pycrocart/Examples/pyQtExample.py new file mode 100644 index 0000000000000000000000000000000000000000..37320d6926025e758893fa5bebacb219c4182dc9 --- /dev/null +++ b/pycrocart/Examples/pyQtExample.py @@ -0,0 +1,54 @@ +# importing libraries +from PyQt5.QtWidgets import * +from PyQt5.QtGui import * +from PyQt5.QtCore import * +import sys + + +class Window(QMainWindow): + def __init__(self, parent): + super().__init__() + + # setting title + self.setWindowTitle("Python ") + + # setting geometry + self.setGeometry(100, 100, 600, 400) + + # calling method + self.UiComponents() + + # showing all the widgets + self.show() + + # method for widgets + def UiComponents(self): + + # creating a push button + button = QPushButton("CLICK", self) + + textLabel = QLabel(self) + textLabel.setText("Hello World!") + textLabel.move(200, 85) + + # setting geometry of button + button.setGeometry(200, 50, 100, 40) + + # adding action to a button + button.clicked.connect(self.clickme) + + # action method + def clickme(self): + + # printing pressed + print("pressed") + + +# create pyqt5 app +App = QApplication(sys.argv) + +# create the instance of our Window +window = Window(App) + +# start the app +sys.exit(App.exec()) diff --git a/pycrocart/Examples/tabexample.py b/pycrocart/Examples/tabexample.py new file mode 100644 index 0000000000000000000000000000000000000000..4b0a5c95ed5c45dd9f1a0a2d06d72c88952f899e --- /dev/null +++ b/pycrocart/Examples/tabexample.py @@ -0,0 +1,66 @@ +import sys +from PyQt5.QtWidgets import QMainWindow, QApplication, QPushButton, QWidget, \ + QAction, QTabWidget, QVBoxLayout +from PyQt5.QtGui import QIcon +from PyQt5.QtCore import pyqtSlot + + +class App(QMainWindow): + + def __init__(self): + super().__init__() + self.title = 'PyQt5 tabs - pythonspot.com' + self.left = 0 + self.top = 0 + self.width = 300 + self.height = 200 + self.setWindowTitle(self.title) + self.setGeometry(self.left, self.top, self.width, self.height) + + self.table_widget = MyTableWidget(self) + self.setCentralWidget(self.table_widget) + + self.show() + + +class MyTableWidget(QWidget): + + def __init__(self, parent): + super(QWidget, self).__init__(parent) + self.layout = QVBoxLayout(self) + + # Initialize tab screen + self.tabs = QTabWidget() + self.tab1 = QWidget() + self.tab2 = QWidget() + self.tab3 = QWidget() + self.tabs.resize(300, 200) + + # Add tabs + self.tabs.addTab(self.tab1, "Tab 1") + self.tabs.addTab(self.tab2, "Tab 2") + self.tabs.addTab(self.tab3, "Tab 3") + + # Create first tab + self.tab1.layout = QVBoxLayout(self) + self.pushButton1 = QPushButton("PyQt5 button") + self.tab1.layout.addWidget(self.pushButton1) + self.tab1.setLayout(self.tab1.layout) + + # Add tabs to widget + self.layout.addWidget(self.tabs) + self.setLayout(self.layout) + + @pyqtSlot() + def on_click(self): + print("\n") + for currentQTableWidgetItem in self.tableWidget.selectedItems(): + print(currentQTableWidgetItem.row(), + currentQTableWidgetItem.column(), + currentQTableWidgetItem.text()) + + +if __name__ == '__main__': + app = QApplication(sys.argv) + ex = App() + sys.exit(app.exec_()) diff --git a/pycrocart/GamepadWizardTab.py b/pycrocart/GamepadWizardTab.py new file mode 100644 index 0000000000000000000000000000000000000000..c4e981052c29a94552a5d56554f9319839117f36 --- /dev/null +++ b/pycrocart/GamepadWizardTab.py @@ -0,0 +1,445 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# || ____ _ __ +# +------+ / __ )(_) /_______________ _____ ___ +# | 0xBC | / __ / / __/ ___/ ___/ __ `/_ / / _ \ +# +------+ / /_/ / / /_/ /__/ / / /_/ / / /_/ __/ +# || || /_____/_/\__/\___/_/ \__,_/ /___/\___/ +# +# Copyright (C) 2011-2017 Bitcraze AB +# +# Crazyflie Nano Quadcopter Client +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" +Dialogue used to select and configure an inputdevice. This includes mapping +buttons and axis to match controls for the Crazyflie. +""" +import logging + +import cfclient +from PyQt5.QtCore import QThread +from PyQt5.QtCore import QTimer +from PyQt5.QtCore import pyqtSignal +from PyQt5.QtWidgets import QMessageBox +from cfclient.utils.config_manager import ConfigManager +from PyQt5 import Qt +from PyQt5 import QtWidgets +from PyQt5 import uic # pycharm seems to think this doesn't exist, +# but pycharm is stupid and it does exist, get over it. +from PyQt5.Qt import * # noqa +from cfclient.utils.input import JoystickReader +import sys + +__author__ = 'Bitcraze AB' +__all__ = ['InputConfigDialogue'] + +logger = logging.getLogger(__name__) + +(inputconfig_widget_class, connect_widget_base_class) = ( + uic.loadUiType(cfclient.module_path + '/ui/dialogs/inputconfigdialogue.ui') +) + + +class InputConfigDialogue(QtWidgets.QWidget, inputconfig_widget_class): + + def __init__(self, joystickReader, enable_external_gamepad_func, *args): + super(InputConfigDialogue, self).__init__(*args) + self.setupUi(self) + self._input = joystickReader + self.enable_external_gamepad_func = enable_external_gamepad_func + + self._input_device_reader = DeviceReader(self._input) + self._input_device_reader.start() + + self._input_device_reader.raw_axis_data_signal.connect( + self._detect_axis) + self._input_device_reader.raw_button_data_signal.connect( + self._detect_button) + self._input_device_reader.mapped_values_signal.connect( + self._update_mapped_values) + + self.cancelButton.clicked.connect(self.close) + self.saveButton.clicked.connect(self._save_config) + + self.detectPitch.clicked.connect( + lambda: self._axis_detect( + "pitch", "Pitch axis", + "Center the pitch axis then do max %s pitch", + ["forward", "backward"])) + self.detectRoll.clicked.connect( + lambda: self._axis_detect( + "roll", "Roll axis", + "Center the roll axis and then do max %s roll", + ["right", "left"])) + self.detectYaw.clicked.connect( + lambda: self._axis_detect( + "yaw", "Yaw axis", + "Center the yaw axis and then do max %s yaw", + ["right", "left"])) + self.detectThrust.clicked.connect( + lambda: self._axis_detect( + "thrust", "Thrust axis", + "Center the thrust axis, and then do max thrust")) + self.detectPitchPos.clicked.connect( + lambda: self._button_detect( + "pitchPos", "Pitch Cal Positive", + "Press the button for Pitch postive calibration")) + self.detectPitchNeg.clicked.connect( + lambda: self._button_detect( + "pitchNeg", "Pitch Cal Negative", + "Press the button for Pitch negative calibration")) + self.detectRollPos.clicked.connect( + lambda: self._button_detect( + "rollPos", "Roll Cal Positive", + "Press the button for Roll positive calibration")) + self.detectRollNeg.clicked.connect( + lambda: self._button_detect( + "rollNeg", "Roll Cal Negative", + "Press the button for Roll negative calibration")) + self.detectKillswitch.clicked.connect( + lambda: self._button_detect( + "killswitch", "Killswitch", + "Press the button for the killswitch (will disable motors)")) + self.detectAlt1.clicked.connect( + lambda: self._button_detect( + "alt1", "Alternative function 1", + "The alternative function 1 that will do a callback")) + self.detectAlt2.clicked.connect( + lambda: self._button_detect( + "alt2", "Alternative function 2", + "The alternative function 2 that will do a callback")) + self.detectExitapp.clicked.connect( + lambda: self._button_detect( + "exitapp", "Exit application", + "Press the button for exiting the application")) + self._detect_assisted_control.clicked.connect( + lambda: self._button_detect( + "assistedControl", "Assisted control", + "Press the button for assisted control mode activation " + "(releasing returns to manual mode)")) + self.detectMuxswitch.clicked.connect( + lambda: self._button_detect( + "muxswitch", "Mux Switch", + "Press the button for mux switching")) + + self.configButton.clicked.connect(self._start_configuration) + self.loadButton.clicked.connect(self._load_config_from_file) + self.deleteButton.clicked.connect(self._delete_configuration) + + self._popup = None + self._combined_button = None + self._detection_buttons = [ + self.detectPitch, self.detectRoll, + self.detectYaw, self.detectThrust, + self.detectPitchPos, self.detectPitchNeg, + self.detectRollPos, self.detectRollNeg, + self.detectKillswitch, self.detectExitapp, + self._detect_assisted_control, self.detectAlt1, + self.detectAlt2, self.detectMuxswitch] + + self._button_to_detect = "" + self._axis_to_detect = "" + self.combinedDetection = 0 + self._prev_combined_id = None + + self._maxed_axis = [] + self._mined_axis = [] + + self._buttonindicators = {} + self._axisindicators = {} + self._reset_mapping() + + for d in self._input.available_devices(): + if d.supports_mapping: + self.inputDeviceSelector.addItem(d.name, d.id) + + if len(self._input.available_devices()) > 0: + self.configButton.setEnabled(True) + + self._map = {} + self._saved_open_device = None + + @staticmethod + def _scale(max_value, value): + return (value / max_value) * 100 + + def _reset_mapping(self): + self._buttonindicators = { + "pitchPos": self.pitchPos, + "pitchNeg": self.pitchNeg, + "rollPos": self.rollPos, + "rollNeg": self.rollNeg, + "killswitch": self.killswitch, + "alt1": self.alt1, + "alt2": self.alt2, + "exitapp": self.exitapp, + "assistedControl": self._assisted_control, + "muxswitch": self.muxswitch, + } + + self._axisindicators = { + "pitch": self.pitchAxisValue, + "roll": self.rollAxisValue, + "yaw": self.yawAxisValue, + "thrust": self.thrustAxisValue, + } + + def _cancel_config_popup(self, button): + self._axis_to_detect = "" + self._button_to_detect = "" + + def _show_config_popup(self, caption, message, directions=[]): + self._maxed_axis = [] + self._mined_axis = [] + self._popup = QMessageBox() + self._popup.directions = directions + self._combined_button = QtWidgets.QPushButton('Combined Axis ' + + 'Detection') + self.cancelButton = QtWidgets.QPushButton('Cancel') + self._popup.addButton(self.cancelButton, QMessageBox.DestructiveRole) + self._popup.setWindowTitle(caption) + self._popup.setWindowFlags(Qt.Dialog | Qt.MSWindowsFixedSizeDialogHint) + if len(directions) > 1: + self._popup.originalMessage = message + message = self._popup.originalMessage % directions[0] + self._combined_button.setCheckable(True) + self._combined_button.blockSignals(True) + self._popup.addButton(self._combined_button, + QMessageBox.ActionRole) + self._popup.setText(message) + self._popup.show() + + def _start_configuration(self): + self._input.enableRawReading( + str(self.inputDeviceSelector.currentText())) + self._input_device_reader.start_reading() + self._populate_config_dropdown() + self.profileCombo.setEnabled(True) + for b in self._detection_buttons: + b.setEnabled(True) + + self.enable_external_gamepad_func( + str(self.profileCombo.currentText())) + + def _detect_axis(self, data): + if (len(self._axis_to_detect) > 0): + if (self._combined_button and self._combined_button.isChecked() and + self.combinedDetection == 0): + self._combined_button.setDisabled(True) + self.combinedDetection = 1 + for a in data: + # Axis must go low and high before it's accepted as selected + # otherwise maxed out axis (like gyro/acc) in some controllers + # will always be selected. Not enforcing negative values makes + # it possible to detect split axis (like bumpers on PS3 + # controller) + if a not in self._maxed_axis and abs(data[a]) > 0.8: + self._maxed_axis.append(a) + if a not in self._mined_axis and abs(data[a]) < 0.1: + self._mined_axis.append(a) + if a in self._maxed_axis and a in self._mined_axis and len( + self._axis_to_detect) > 0: + if self.combinedDetection == 0: + if data[a] >= 0: + self._map_axis(self._axis_to_detect, a, 1.0) + else: + self._map_axis(self._axis_to_detect, a, -1.0) + self._axis_to_detect = "" + self._check_and_enable_saving() + if self._popup is not None: + self.cancelButton.click() + elif self.combinedDetection == 2: # finished detection + # not the same axis again ... + if self._prev_combined_id != a: + self._map_axis(self._axis_to_detect, a, -1.0) + self._axis_to_detect = "" + self._check_and_enable_saving() + if (self._popup is not None): + self.cancelButton.click() + self.combinedDetection = 0 + elif self.combinedDetection == 1: + self._map_axis(self._axis_to_detect, a, 1.0) + self._prev_combined_id = a + self.combinedDetection = 2 + message = (self._popup.originalMessage % + self._popup.directions[1]) + self._popup.setText(message) + + def _update_mapped_values(self, mapped_data): + for v in mapped_data.get_all_indicators(): + if v in self._buttonindicators: + if mapped_data.get(v): + self._buttonindicators[v].setChecked(True) + else: + self._buttonindicators[v].setChecked(False) + if v in self._axisindicators: + # The sliders used are set to 0-100 and the values from the + # input-layer is scaled according to the max settings in + # the input-layer. So scale the value and place 0 in the middle + scaled_value = mapped_data.get(v) + if v == "thrust": + scaled_value = InputConfigDialogue._scale( + self._input.max_thrust, scaled_value + ) + if v == "roll" or v == "pitch": + scaled_value = InputConfigDialogue._scale( + self._input.max_rp_angle, scaled_value + ) + if v == "yaw": + scaled_value = InputConfigDialogue._scale( + self._input.max_yaw_rate, scaled_value + ) + self._axisindicators[v].setValue(int(scaled_value)) + + def _map_axis(self, function, key_id, scale): + self._map["Input.AXIS-{}".format(key_id)] = {} + self._map["Input.AXIS-{}".format(key_id)]["id"] = key_id + self._map["Input.AXIS-{}".format(key_id)]["key"] = function + self._map["Input.AXIS-{}".format(key_id)]["scale"] = scale + self._map["Input.AXIS-{}".format(key_id)]["offset"] = 0.0 + self._map["Input.AXIS-{}".format(key_id)]["type"] = "Input.AXIS" + self._input.set_raw_input_map(self._map) + + def _map_button(self, function, key_id): + # Duplicate buttons are not allowed, remove if there's already one + # mapped + prev_button = None + for m in self._map: + if "key" in self._map[m] and self._map[m]["key"] == function: + prev_button = m + if prev_button: + del self._map[prev_button] + + self._map["Input.BUTTON-{}".format(key_id)] = {} + self._map["Input.BUTTON-{}".format(key_id)]["id"] = key_id + self._map["Input.BUTTON-{}".format(key_id)]["key"] = function + self._map["Input.BUTTON-{}".format(key_id)]["scale"] = 1.0 + self._map["Input.BUTTON-{}".format(key_id)]["type"] = "Input.BUTTON" + self._input.set_raw_input_map(self._map) + + def _detect_button(self, data): + if len(self._button_to_detect) > 0: + for b in data: + if data[b] > 0: + self._map_button(self._button_to_detect, b) + self._button_to_detect = "" + self._check_and_enable_saving() + if self._popup is not None: + self._popup.close() + + def _check_and_enable_saving(self): + needed_funcs = ["thrust", "yaw", "roll", "pitch"] + + for m in self._map: + if self._map[m]["key"] in needed_funcs: + needed_funcs.remove(self._map[m]["key"]) + + if len(needed_funcs) == 0: + self.saveButton.setEnabled(True) + + def _populate_config_dropdown(self): + configs = ConfigManager().get_list_of_configs() + if len(configs): + self.loadButton.setEnabled(True) + for c in configs: + self.profileCombo.addItem(c) + + def _axis_detect(self, varname, caption, message, directions=[]): + self._axis_to_detect = varname + self._show_config_popup(caption, message, directions) + + def _button_detect(self, varname, caption, message): + self._button_to_detect = varname + self._show_config_popup(caption, message) + + def _show_error(self, caption, message): + QMessageBox.critical(self, caption, message) + + def _load_config_from_file(self): + print("Loading config") + loaded_map = ConfigManager().get_config( + self.profileCombo.currentText()) + if loaded_map: + self._input.set_raw_input_map(loaded_map) + self._map = loaded_map + else: + logger.warning("Could not load configfile [%s]", + self.profileCombo.currentText()) + self._show_error("Could not load config", + "Could not load config [%s]" % + self.profileCombo.currentText()) + self._check_and_enable_saving() + + def _delete_configuration(self): + logger.warning("deleteConfig not implemented") + + def _save_config(self): + config_name = str(self.profileCombo.currentText()) + ConfigManager().save_config(self._map, config_name) + # self.close() + + def showEvent(self, event): + """Called when dialog is opened""" + # self._saved_open_device = self._input.get_device_name() + # self._input.stop_input() + self._input.pause_input() + + def closeEvent(self, event): + """Called when dialog is closed""" + self._input.stop_raw_reading() + self._input_device_reader.stop_reading() + # self._input.start_input(self._saved_open_device) + self._input.resume_input() + + +class DeviceReader(QThread): + """Used for polling data from the Input layer during configuration""" + raw_axis_data_signal = pyqtSignal(object) + raw_button_data_signal = pyqtSignal(object) + mapped_values_signal = pyqtSignal(object) + + def __init__(self, input): + QThread.__init__(self) + + self._input = input + self._read_timer = QTimer() + self._read_timer.setInterval(25) + + self._read_timer.timeout.connect(self._read_input) + + def stop_reading(self): + """Stop polling data""" + self._read_timer.stop() + + def start_reading(self): + """Start polling data""" + self._read_timer.start() + + def _read_input(self): + [rawaxis, rawbuttons, mapped_values] = self._input.read_raw_values() + self.raw_axis_data_signal.emit(rawaxis) + self.raw_button_data_signal.emit(rawbuttons) + self.mapped_values_signal.emit(mapped_values) + + +if __name__ == "__main__": + app = QApplication(sys.argv) + + win2 = InputConfigDialogue(JoystickReader(), print) + win = QMainWindow() + win.setCentralWidget(win2) + win.show() + sys.exit(app.exec_()) diff --git a/pycrocart/Images/gamepad_tab.png b/pycrocart/Images/gamepad_tab.png new file mode 100644 index 0000000000000000000000000000000000000000..3c8fa2550e0abb3ac9abb45f2f18c6e89f4f18e8 Binary files /dev/null and b/pycrocart/Images/gamepad_tab.png differ diff --git a/pycrocart/Images/gui_main_window.png b/pycrocart/Images/gui_main_window.png new file mode 100644 index 0000000000000000000000000000000000000000..445aae7874a582caf2248a145ce4dea78b497978 Binary files /dev/null and b/pycrocart/Images/gui_main_window.png differ diff --git a/pycrocart/Images/logging_file.png b/pycrocart/Images/logging_file.png new file mode 100644 index 0000000000000000000000000000000000000000..b2abc79e3537c39cf1e77130f1471cb77efc0b2e Binary files /dev/null and b/pycrocart/Images/logging_file.png differ diff --git a/pycrocart/Images/logging_tab.png b/pycrocart/Images/logging_tab.png new file mode 100644 index 0000000000000000000000000000000000000000..d6c362d9e956e36790d2c64c64badafc983cf62e Binary files /dev/null and b/pycrocart/Images/logging_tab.png differ diff --git a/pycrocart/Images/parameter_tab.png b/pycrocart/Images/parameter_tab.png new file mode 100644 index 0000000000000000000000000000000000000000..45ae63bb3bfa2ceabf5289c7277d805c4493d208 Binary files /dev/null and b/pycrocart/Images/parameter_tab.png differ diff --git a/pycrocart/Images/pycrocartArchitecture.png b/pycrocart/Images/pycrocartArchitecture.png new file mode 100644 index 0000000000000000000000000000000000000000..c555ba83b239f087019f6505f7cba9d77a68abce Binary files /dev/null and b/pycrocart/Images/pycrocartArchitecture.png differ diff --git a/pycrocart/LoggingConfigTab.py b/pycrocart/LoggingConfigTab.py new file mode 100644 index 0000000000000000000000000000000000000000..09bb9d5b303ab1a252b66e899658b4923ee839a2 --- /dev/null +++ b/pycrocart/LoggingConfigTab.py @@ -0,0 +1,254 @@ +from PyQt5.QtWidgets import QPushButton, QGridLayout, QWidget, QVBoxLayout, \ + QLabel, QHBoxLayout, QListWidget +from PyQt5.QtCore import Qt +import os +import json +from CrazyflieProtoConnection import CrazyflieProtoConnection + + +class LoggingConfigTab(QWidget): + """ + LoggingConfigTab is the class that configures the logging config tab + inside the gui. Holds the logging table of contents, and a couple buttons to + configure the logging blocks, refresh the logging, and stop it completely. + """ + + def __init__(self, cf: CrazyflieProtoConnection, + logging_selection_menu_update_function): + """ + Initialize all widgets in the logging config tab window. + + :param cf: CrazyflieProtoConnection in order to interface with + drone. + :param logging_selection_menu_update_function: This function is + called whenever the logging blocks are refreshed. The purpose is to + allow the logging selection menu to know that the logging config has + been refreshed and to look for new signals. + """ + super().__init__() + + layout = QHBoxLayout() + self.setLayout(layout) + self.cf = cf + self.logging_selection_menu_update_function = \ + logging_selection_menu_update_function + self.items = [] + self.items_text = [] + + left_widget = QWidget() + right_widget = QWidget() + + left_layout = QVBoxLayout() + right_layout = QGridLayout() + + left_widget.setLayout(left_layout) + right_widget.setLayout(right_layout) + layout.addWidget(left_widget, 1) + layout.addWidget(right_widget, 2) + + # ------------ Left ---------------------------------------------------- + + logging_vars = QLabel("Logging Variables") + logging_vars.setMaximumHeight(20) + left_layout.addWidget(logging_vars, 1) + + self.list_widget = QListWidget() + left_layout.addWidget(self.list_widget, 2) + + # ------------ Right --------------------------------------------------- + + log_block_commands = QLabel("Log Block Commands") + log_block_commands.setMaximumHeight(50) + right_layout.addWidget(log_block_commands, 1, 1, + alignment=Qt.AlignHCenter) + + self.error_label = QLabel() + self.error_label.setMaximumHeight(50) + right_layout.addWidget(self.error_label, 2, 1, + alignment=Qt.AlignHCenter) + + explanation = """ + The logging blocks file is configured to work in logging groups. The + name of the groups does not matter, but what does matter is the + "update_frequency_ms" value as well as the values inside the "vars" + array. Each vars array can be at most 5 values long, otherwise an + error will be thrown. They also must contain a logging variable found in + the valid logging variables, which can be seen in the menu to the left. + The update_frequency_ms can be at most 500ms and at minimum 20 ms. + """ + + log_block_comments = QLabel(explanation) + right_layout.addWidget(log_block_comments, 3, 1, + alignment=Qt.AlignCenter) + + open_log_file_button = QPushButton("Open Logging Block Setup File") + open_log_file_button.clicked.connect(self.open_log_file) + open_log_file_button.setMaximumWidth(500) + right_layout.addWidget(open_log_file_button, 4, 1, + alignment=Qt.AlignCenter) + + horizontal_widget = QWidget() + horizontal_layout = QHBoxLayout() + horizontal_widget.setLayout(horizontal_layout) + right_layout.addWidget(horizontal_widget, 5, 1) + + refresh_logs_button = QPushButton("Refresh Logging Blocks") + refresh_logs_button.clicked.connect(self.on_refresh) + refresh_logs_button.setMaximumWidth(200) + horizontal_layout.addWidget(refresh_logs_button, 1) + + stop_logging_button = QPushButton("Stop Logging") + stop_logging_button.clicked.connect(self.on_stop) + stop_logging_button.setMaximumWidth(200) + horizontal_layout.addWidget(stop_logging_button, 2) + + @staticmethod + def open_log_file(_self): + """ Open logging file using default json file editor, probably + notepad. """ + os.startfile('logging_variables.json') + + def on_refresh(self): + """ Whenever the refresh button is clicked, call this function. """ + + self.cf.clear_logging_configs() + self.set_logging_block_from_file() + + def on_connect(self): + self.update_logging_values() + self.on_refresh() + + def on_disconnect(self): + self.update_logging_values() + + def on_stop(self): + """ Whenever the stop logging button is clicked, call this function. + Arguably unneccesary given the pause button on the controls page. """ + + self.cf.stop_logging() + + def update_logging_values(self): + """ Update the logging parameter values available in the left-hand + menu. """ + + # Clear away the current values + for item in self.items: + self.list_widget.takeItem(self.list_widget.row(item)) + + # Get table of contents. + logging_vars_toc = self.cf.get_logging_toc() + self.list_widget.addItems(logging_vars_toc) + + # Update items list so, we can remove items next time we try. + self.items = [] + self.items_text = [] + for x in range(self.list_widget.count()): + self.items.append(self.list_widget.item(x)) + self.items_text.append(self.list_widget.item(x).text()) + + def set_logging_block_from_file(self): + """ Read logging variables json. Offer feedback to the user if + formatting is incorrect. """ + + with open('./logging_variables.json', 'r') as f: + contents = json.load(f) + # todo offer feedback if file is missing + + blocks_valid = True + blocks = [] + all_vars_logged = [] + + if not contents: + print("No logging blocks") + blocks_valid = False + + for key in contents: + name = key + + try: + period = int(contents[key]['update_frequency_ms']) + + if period <= 20: + error_text = "update_frequency_ms must be greater than 20" + self.error_label.setText( + "<span style='color: red;'>" + error_text + "</span>") + blocks_valid = False + break + elif period >= 500: + error_text = "update_frequency_ms must be less than 500" + self.error_label.setText( + "<span style='color: red;'>" + error_text + "</span>") + blocks_valid = False + break + except ValueError: + error_text = "update_frequency_ms must be an int" + self.error_label.setText("<span style='color: red;'>" + + error_text + "</span>") + blocks_valid = False + break + except KeyError: + error_text = "need an update_frequency_ms key" + self.error_label.setText( + "<span style='color: red;'>" + error_text + "</span>") + blocks_valid = False + break + + bad_var = "" + try: + variables = contents[key]['vars'] + + if len(variables) > 5: + error_text = "logging block " + key + " longer than 5 " \ + "variables" + self.error_label.setText( + "<span style='color: red;'>" + error_text + "</span>") + + blocks_valid = False + break + + # This is where good variables are actually handled, but will + # raise errors if they are bad. + for var in variables: + if var not in self.items_text: + bad_var = var + raise ValueError + if var not in all_vars_logged: + all_vars_logged.append(var) + else: + bad_var = var + raise AssertionError + except KeyError: + error_text = "need a vars key" + self.error_label.setText( + "<span style='color: red;'>" + error_text + "</span>") + + blocks_valid = False + break + except ValueError: + error_text = "logging variable " + str(bad_var) + " not found" + self.error_label.setText( + "<span style='color: red;'>" + error_text + "</span>") + blocks_valid = False + break + except AssertionError: + error_text = "logging variable " + str(bad_var) + \ + " found repeated" + self.error_label.setText( + "<span style='color: red;'>" + error_text + "</span>") + blocks_valid = False + break + # todo add error for if there are over 4 logging groups + + # current logging blocks + blocks.append({'name': name, 'period': period, 'vars': variables}) + + if blocks_valid: + self.error_label.setText("") + variables = [] + for block in blocks: + # add logging block to crazyflie proto connection + self.cf.add_log_config(block['name'], block['period'], + block['vars']) + variables.append(block['vars']) + # call function for menu update on controls page + self.logging_selection_menu_update_function(variables) diff --git a/pycrocart/LoggingSelectionMenu.py b/pycrocart/LoggingSelectionMenu.py new file mode 100644 index 0000000000000000000000000000000000000000..01e05eb13db1c51d1af016724c153d839623b3f9 --- /dev/null +++ b/pycrocart/LoggingSelectionMenu.py @@ -0,0 +1,102 @@ +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QComboBox, QLabel +from PyQt5.QtCore import Qt + + +class LoggingSelectionMenu(QWidget): + """ + Logging selection menu sits within the controls tab, handles selecting + valid logging signals through a series of combo boxes. + """ + + def __init__(self, config_changed_callback): + super().__init__() + + layout = QVBoxLayout() + self.setLayout(layout) + self.config_changed_callback = config_changed_callback # whenever + # the config changes, send a signal to the controls tab to press the + # pause button + + self.available_logging_variables = [] + + self.cbs = [QComboBox(), QComboBox(), QComboBox(), QComboBox(), + QComboBox()] + + # Default logging variables to Logging Variable 1,2,3,4,5 + colors = ['red', 'orange', 'purple', 'green', 'blue'] + + for i in range(0, len(self.cbs)): + lst = ["Logging Variable " + str(i+1)] + lst.extend(self.available_logging_variables) + self.cbs[i].addItems(lst) + layout.addWidget(self.cbs[i], 1+2*i) + + color_label = QLabel("Color Label") + color_label.setStyleSheet("QLabel {color: " + colors[i] + + ";background : " + colors[i] + "}") + color_label.setMaximumHeight(5) + layout.addWidget(color_label, 1+2*i+1, alignment=Qt.AlignHCenter) + + # add callback for when new selection happens + self.cbs[i].currentIndexChanged.connect( + self.selection_change_callback) + # todo add coloring to each combo box text that matches the plotting + # window line color associated with that logging variable + + def get_axis_of_signal(self, signal_name: str): + """ Used for determining which signal is which in plotting window. """ + for i in range(0, len(self.cbs)): + if signal_name == self.cbs[i].currentText(): + return i + + def update_available_logging_variables(self, variables_list: list): + """ This function is called by the logging config tab upon refreshing + the logging variables. If anything changes, the variables list will + be updated. """ + self.available_logging_variables = [] + for variable in variables_list: + for sub_variable in variable: + self.available_logging_variables.append(sub_variable) + self.config_changed_callback() + self.selection_change_callback() + + # todo handle variables that used to be in the list and no longer are + # todo always pause or set back to Logging Variable # whenever the + # old variable selected is no longer valid + + def selection_change_callback(self): + """ Called whenever a signal selection is changed. """ + + selected_signals = [ + self.cbs[0].currentText(), self.cbs[1].currentText(), + self.cbs[2].currentText(), self.cbs[3].currentText(), + self.cbs[4].currentText() + ] + modified_selection_signals = [] + + # find any signals that are different from before. + for i in range(0, len(self.available_logging_variables)): + if not (self.available_logging_variables[i] in selected_signals): + modified_selection_signals.append( + self.available_logging_variables[i]) + + # update logging variable options in each menu, don't show options + # that are selected elsewhere. + for i in range(0, len(self.cbs)): + + self.cbs[i].currentIndexChanged.disconnect( + self.selection_change_callback) + + signal = self.cbs[i].currentText() + + items = ["Logging Variable " + str(i+1)] + items.extend(modified_selection_signals) + if signal != ("Logging Variable " + str(i+1)): + items.append(signal) + + self.cbs[i].clear() + self.cbs[i].addItems(items) + self.cbs[i].setCurrentText(signal) + + self.cbs[i].currentIndexChanged.connect( + self.selection_change_callback) diff --git a/pycrocart/ParameterTab.py b/pycrocart/ParameterTab.py new file mode 100644 index 0000000000000000000000000000000000000000..a76f365829e95cd99d77f1ed4096dca416bf9984 --- /dev/null +++ b/pycrocart/ParameterTab.py @@ -0,0 +1,305 @@ +from PyQt5.QtWidgets import QPushButton, QGridLayout, QWidget, QComboBox, \ + QLabel, QLineEdit, QProgressBar +from PyQt5.QtCore import QTimer +from PyQt5 import QtWidgets +from PyQt5.QtCore import Qt +from PyLine import QHSeparationLine +import os +import json +from queue import Queue +import queue +from CrazyflieProtoConnection import CrazyflieProtoConnection + + +class ParameterTab(QWidget): + """ + The parameter tab is a page that handles sending or getting parameters + from the drone. + """ + + def __init__(self, cf: CrazyflieProtoConnection): + super().__init__() + + layout = QGridLayout() + self.setLayout(layout) + self.cf = cf # Crazyflie proto connection + + self.sending = False + self.sending_queue = Queue() + self.num_to_send = 0 + self.num_sent = 0 + + self.toc = {} # Parameter table of contents + self.timer = QTimer() + + # Add white space around the parameter menus. + horizontal_spacer = QtWidgets.QSpacerItem( + 1, 1, QtWidgets.QSizePolicy.Expanding, + QtWidgets.QSizePolicy.Minimum) + layout.addItem(horizontal_spacer, 1, 2, Qt.AlignRight) + + horizontal_spacer2 = QtWidgets.QSpacerItem( + 1, 1, QtWidgets.QSizePolicy.Expanding, + QtWidgets.QSizePolicy.Minimum) + layout.addItem(horizontal_spacer2, 1, 0, Qt.AlignRight) + + horizontal_spacer3 = QtWidgets.QSpacerItem( + 1, 1, QtWidgets.QSizePolicy.Minimum, + QtWidgets.QSizePolicy.Expanding) + layout.addItem(horizontal_spacer3, 0, 1, Qt.AlignTop) + + # --------------------- Get Param -------------------------------------- + get_paraml = QLabel("Get Parameter") + layout.addWidget(get_paraml, 1, 1) + + get_param_widget = QWidget() + get_param_widget.setMinimumHeight(100) + get_param_widget.setMinimumHeight(100) + layout.addWidget(get_param_widget, 2, 1) + + get_param_layout = QGridLayout() + get_param_widget.setLayout(get_param_layout) + + get_param_group = QLabel("Group") + get_param_layout.addWidget(get_param_group, 1, 1) + + self.get_param_group_cbox = QComboBox() + self.get_param_group_cbox.currentTextChanged.connect( + self.on_get_param_group_changed) + get_param_layout.addWidget(self.get_param_group_cbox, 1, 2) + + get_param = QLabel("Entry") + get_param_layout.addWidget(get_param, 2, 1) + + self.get_param_cbox = QComboBox() + self.get_param_cbox.setMinimumWidth(150) + self.get_param_cbox.setMaximumWidth(150) + get_param_layout.addWidget(self.get_param_cbox, 2, 2) + + value_label = QLabel("Value") + get_param_layout.addWidget(value_label, 3, 1) + + self.value_value_label = QLabel() + self.value_value_label.setMaximumWidth(150) + get_param_layout.addWidget(self.value_value_label, 3, 2) + + self.get_param_button = QPushButton("Get Param") + self.get_param_button.clicked.connect(self.get_param) + layout.addWidget(self.get_param_button, 3, 1, alignment=Qt.AlignHCenter) + self.get_param_button.setMinimumWidth(150) + self.get_param_button.setMaximumWidth(150) + + # ---------------- Line ------------------------------------------------ + line1 = QHSeparationLine() + line2 = QHSeparationLine() + line3 = QHSeparationLine() + layout.addWidget(line1, 4, 1) + layout.addWidget(line2, 4, 2) + layout.addWidget(line3, 4, 0) + + # --------------------- Set Param -------------------------------------- + set_paraml = QLabel("Set Parameter") + layout.addWidget(set_paraml, 6, 1) + + set_param_widget = QWidget() + set_param_widget.setMinimumHeight(100) + set_param_widget.setMinimumHeight(100) + layout.addWidget(set_param_widget, 7, 1) + + set_param_layout = QGridLayout() + set_param_widget.setLayout(set_param_layout) + + set_param_group = QLabel("Group") + set_param_layout.addWidget(set_param_group, 1, 1) + + self.set_param_group_cbox = QComboBox() + self.set_param_group_cbox.currentTextChanged.connect( + self.on_set_param_group_changed) + set_param_layout.addWidget(self.set_param_group_cbox, 1, 2) + + set_param = QLabel("Entry") + set_param_layout.addWidget(set_param, 2, 1) + + self.set_param_cbox = QComboBox() + set_param_layout.addWidget(self.set_param_cbox, 2, 2) + + self.set_value_label = QLabel("Value") + set_param_layout.addWidget(self.set_value_label, 3, 1) + + self.set_param_value = QLineEdit() + self.set_param_value.setMaximumWidth(150) + set_param_layout.addWidget(self.set_param_value, 3, 2) + + self.set_param_button = QPushButton("Set Param") + self.set_param_button.setMinimumWidth(150) + self.set_param_button.setMaximumWidth(150) + self.set_param_button.clicked.connect(self.set_param) + layout.addWidget(self.set_param_button, 8, 1, alignment=Qt.AlignHCenter) + + # -------------------- File interaction -------------------------------- + + file_interaction_widget = QWidget() + file_interaction_layout = QGridLayout() + file_interaction_widget.setLayout(file_interaction_layout) + layout.addWidget(file_interaction_widget, 9, 1) + + self.set_from_file_button = QPushButton("Set Params from Json file") + self.set_from_file_button.setMinimumWidth(200) + self.set_from_file_button.setMaximumWidth(200) + file_interaction_layout.addWidget(self.set_from_file_button, 1, 1) + self.set_from_file_button.clicked.connect(self.on_send) + + self.edit_file_button = QPushButton("Edit") + self.edit_file_button.setMinimumWidth(20) + self.edit_file_button.setMaximumWidth(40) + file_interaction_layout.addWidget(self.edit_file_button, 1, 2) + self.edit_file_button.clicked.connect(self.on_edit) + + file_note = QLabel("Note: This may take a few seconds") + layout.addWidget(file_note, 10, 1, alignment=Qt.AlignHCenter) + + self.complete_label = QLabel("Complete ✓") + layout.addWidget(self.complete_label, 11, 1, alignment=Qt.AlignHCenter) + + self.progress_bar = QProgressBar() + self.progress_bar.setMinimumWidth(100) + self.progress_bar.setMaximumWidth(200) + layout.addWidget(self.progress_bar, 12, 1, alignment=Qt.AlignHCenter) + + def on_connect(self): + """ Whenever connecting to the crazyflie, grab the parameter table of + contents. This is how we actually get what parameters and groups are + available. """ + self.toc = self.cf.get_param_toc() + self.populate_group_menu_options() + + def on_disconnect(self): + self.toc = {} + self.populate_group_menu_options() + + def populate_group_menu_options(self): + """ Remove any current entries from parameter boxes and add the + options for the parameter groups. """ + + self.set_param_group_cbox.clear() + self.get_param_group_cbox.clear() + + self.set_param_group_cbox.addItems(self.toc.keys()) + self.get_param_group_cbox.addItems(self.toc.keys()) + + def on_get_param_group_changed(self): + """ Whenever a parameter group is selected, populate the options for + the specific entries available in each group. """ + + self.get_param_cbox.clear() + group = self.get_param_group_cbox.currentText() + if group != "": # nothing there when cf is disconnected + self.get_param_cbox.addItems(self.toc[group]) + + def on_set_param_group_changed(self): + """ Whenever a parameter group is selected, populate the options for + the specific entries available in each group. """ + + self.set_param_cbox.clear() + group = self.set_param_group_cbox.currentText() + if group != "": # nothing there when cf is disconnected + self.set_param_cbox.addItems(self.toc[group]) + + def get_param(self): + """ Retrieve parameter value from crazyflie proto connection. All + parameter values are always available from it, so this is actually + just retrieving data from the object not from the physical crazyflie + itself. """ + + group = self.get_param_group_cbox.currentText() + entry = self.get_param_cbox.currentText() + value = self.cf.param_get_value(group, entry) + self.value_value_label.setText(value) + + def set_param(self): + """ Set parameter value. If the value isn't a number offer the user + feedback that it is an unacceptable value. Otherwise, set the + parameter utilizing the crazyflie proto connection. """ + + group = self.set_param_group_cbox.currentText() + entry = self.set_param_cbox.currentText() + value = self.set_param_value.text() + + try: + value = float(value) + self.cf.param_set_value(group, entry, value) + + if self.set_value_label.text() != "Value": + self.set_value_label.setText("Value") + + except ValueError: + error_text = "Unaccepted value" + self.set_value_label.setText( + "<span style='color: red;'>" + error_text + "</span>") + + @staticmethod + def on_edit(_self): + """ When editing the mp4params file, launch it in notepad or default + text editor. """ + os.startfile('mp4params.json') + + def on_send(self): + """ Send mp4params file to CrazyflieProtoConnection. """ + + # Don't send if you are already sending + if not self.sending: + + try: + with open('./mp4params.json', 'r') as f: + contents = json.load(f) + + self.sending = True + self.complete_label.setEnabled(False) + self.complete_label.setText("Complete ✓") + + # add all the parameters in the file to a sending queue + for key in contents: + for sub_key in contents[key]: + self.num_to_send += 1 + + self.sending_queue.put( + {'key': key, 'sub_key': sub_key, + 'value': contents[key][sub_key]}) + + # every 300 ms, send the next parameter. Uses a timer to + # retain reactivity of UI. Uses 300ms because it was more + # stable than 100ms. Relatively often at 100ms parameters + # would not get set correctly + self.timer.timeout.connect(self.send_callback) + self.timer.start(300) + except json.decoder.JSONDecodeError: + error_text = "Malformed Json" + # If your json loading fails for some reason this is the + # error to be provided to the user. + self.complete_label.setText( + "<span style='color: red;'>" + error_text + "</span>") + # todo add error for if file isn't present + + def send_callback(self): + """ This function get called whenever a parameter is ready to be + sent from the file, and once all parameters are set it will + disconnect itself from being called. Also updates the progress bar. """ + try: + vals = self.sending_queue.get(block=False) + key = vals['key'] + sub_key = vals['sub_key'] + value = vals['value'] + self.cf.param_set_value(key, sub_key, value) + self.num_sent += 1 + self.progress_bar.setValue( + int((self.num_sent / self.num_to_send) * 100)) + + except queue.Empty: + # stop sending once the queue is empty + self.sending = False + self.timer.timeout.disconnect(self.send_callback) + self.timer.stop() + self.complete_label.setEnabled(True) + self.num_to_send = 0 + self.num_sent = 0 + self.progress_bar.setValue(int(0)) diff --git a/pycrocart/PlottingWindow.py b/pycrocart/PlottingWindow.py new file mode 100644 index 0000000000000000000000000000000000000000..1ec44beb7a7c26e862acd3027792dea345bb379c --- /dev/null +++ b/pycrocart/PlottingWindow.py @@ -0,0 +1,68 @@ +import numpy as np +import pyqtgraph as pg + + +class PlottingWindow(pg.PlotWidget): + """ + Plotting window utilized a pyqtgraph plot widget in order to graph the + logging variables that have been selected by the logging selection menu + from the available logging config. + """ + + def __init__(self, parent=None, num_axes=5, width=5, height=4, dpi=100): + """ + Initialize plotting window, axes, labels, and geometry. + + :param parent: Parent widget + :param num_axes: Number of data axes + :param width: Default width in pixels + :param height: Default height in pixels + :param dpi: + """ + + super().__init__(parent=parent, width=width, height=height, dpi=dpi) + self.setLabel('bottom', 'Time (s)') + self.setLabel('left', 'Amplitude') + self.setLabel('top', 'Logging Variables Plot') + self._num_axes = num_axes + + self.current_iter = 0 + + # Set the background color of the plot widget to white + self.setBackground('w') + + if num_axes > 0: + # Set up the sine wave data and plot + # store 1000 data points for each axis + self.plot_data_ys = np.zeros((1000, num_axes)) + self.plot_data_xs = np.zeros((1000, num_axes)) + + colors = ['red', 'orange', 'purple', 'green', 'blue'] + self.plot_curves = \ + [self.plot(self.plot_data_xs[:, i], self.plot_data_ys[:, i], + pen=pg.mkPen(colors[i], width=2)) + for i in range(num_axes)] + + def update_plot(self, input_data: float, input_timestamp: float, + input_axis: int): + """ Add new data to the plot from the right side. Utilizes a shift + buffer to display data. Constrains the X range to the current time + window. """ + + # Shift the data and add a new point + self.plot_data_ys[:-1, input_axis] = self.plot_data_ys[1:, input_axis] + self.plot_data_ys[-1, input_axis] = input_data + + self.plot_data_xs[:-1, input_axis] = self.plot_data_xs[1:, input_axis] + self.plot_data_xs[-1, input_axis] = input_timestamp + + self.plot_curves[input_axis].setData(self.plot_data_xs[:, input_axis], + self.plot_data_ys[:, input_axis]) + + # don't scroll at first. Display 10 seconds of data. + if max(self.plot_data_xs[-1]) < 10: + self.setXRange(0, 10) # had padding=0 on both these ranges in + # case that's needed + else: + self.setXRange(max(self.plot_data_xs[-1])-10, + max(self.plot_data_xs[-1])) diff --git a/pycrocart/PyCroCart.py b/pycrocart/PyCroCart.py new file mode 100644 index 0000000000000000000000000000000000000000..70fadb3b241efc413ba571dee681a929579dbb15 --- /dev/null +++ b/pycrocart/PyCroCart.py @@ -0,0 +1,208 @@ +""" +PyCroCart is the main script of the pycrocart gui. This includes a master class +PyCroCart that holds the connections from the gui to the crazyflie proto +connection, the setpoint handler, and the joystick reader. + +The __main__ function of this script initializes all components of this +groundstation and launches the gui. +""" + +import sys +from PyQt5.QtWidgets import QApplication, QMainWindow, QTabWidget, \ + QHBoxLayout, QVBoxLayout, QComboBox, QPushButton, QWidget, QLabel, \ + QCheckBox, QButtonGroup +import uCartCommander +from CrazyflieProtoConnection import CrazyflieProtoConnection +from ControlTab import ControlTab +from SetpointHandler import SetpointHandler +from GamepadWizardTab import InputConfigDialogue +from cfclient.utils.input import JoystickReader +from ParameterTab import ParameterTab +from LoggingConfigTab import LoggingConfigTab +import time +import os + + +class PyCroCart(QMainWindow): + """ + PyCroCart holds the connections from the gui to the crazyflie proto + connection, the setpoint handler, and the joystick reader. It also holds + all of the tabs to display in the GUI. PyCroCart IS the gui, but as the + top level of the gui it also is responsible for handling the external + connections like the joystick reader, and the crazyflie. + """ + + def __init__(self, cf: CrazyflieProtoConnection, + setpoint_handler: SetpointHandler): + super().__init__() + + self.cf = cf + self.crazyflie_selection_box = QComboBox(self) + self.crazyflie_selection_box.setFixedWidth(150) + + self.connect = QPushButton("Connect", self) + self.connect.clicked.connect(self.on_connect) + self.connect.setFixedWidth(100) + + self.scan = QPushButton("Scan", self) + self.scan.clicked.connect(self.on_scan) + self.scan.setFixedWidth(100) + + # Some of the gamepad logic needs to different between crazyflie and + # flypi drones because otherwise it's very hard to control the flypi, + # plus whenever we use a TCP connection for the flypi, + # that connection logic will need to be different as well + self.crazyflie_checkbutton = QCheckBox("Crazyflie") + self.flypi_checkbutton = QCheckBox("FlyPi") + self.crazyflie_checkbutton.setChecked(True) # Default to crazyflie + self.drone_selection_group = QButtonGroup() + self.drone_selection_group.addButton(self.crazyflie_checkbutton, 1) + self.drone_selection_group.addButton(self.flypi_checkbutton, 2) + self.drone_selection_group.setExclusive(True) + + self.error_label = QLabel("") + + button_layout = QHBoxLayout() + button_layout.addWidget(self.crazyflie_selection_box) + button_layout.addWidget(self.connect) + button_layout.addWidget(self.scan) + button_layout.addWidget(self.crazyflie_checkbutton) + button_layout.addWidget(self.flypi_checkbutton) + button_layout.addWidget(self.error_label) + button_layout.addStretch(1) + + self.joystick_reader = JoystickReader() + self.setpoint_handler = setpoint_handler + + self.tabs = QTabWidget() + self.control_tab = ControlTab(cf.logging_queue, self.setpoint_handler, + self.joystick_reader, cf) + self.gamepad_tab = InputConfigDialogue( + self.joystick_reader, self.control_tab.setpoint_menu.enableGamepad) + self.parameter_tab = ParameterTab(cf) + self.logging_config_tab = LoggingConfigTab( + cf, self.control_tab.logging_menu.update_available_logging_variables) + + self.setGeometry(100, 200, 600, 600) + + self.tabs.addTab(self.control_tab, "Controls Window") + self.tabs.addTab(self.gamepad_tab, "Gamepad Configuration") + self.tabs.addTab(self.parameter_tab, "Parameter Window") + self.tabs.addTab(self.logging_config_tab, "Logging Window") + + main_layout = QVBoxLayout() + main_layout.addLayout(button_layout) + main_layout.addWidget(self.tabs) + + widget = QWidget() + widget.setLayout(main_layout) + + self.setCentralWidget(widget) + + def on_scan(self): + """ When pressing the scan button, connect to the crazyradio, + and scan for crazyflies. Populate the crazyflie selection box. """ + + cfs = self.cf.list_available_crazyflies() + if cfs: + error_text = "" + self.error_label.setText( + "<span style='color: red;'>" + error_text + "</span>") + + # add crazyflie connections to selection box + self.crazyflie_selection_box.clear() + self.crazyflie_selection_box.addItems(cfs[0][:len(cfs[0])-1]) + else: + error_text = "Either no crazyradio plugged in, or no crazyflies " \ + "detected." + self.error_label.setText( + "<span style='color: red;'>" + error_text + "</span>") + + # clear out selection box + self.crazyflie_selection_box.clear() + + def on_connect(self): + + # check that a crazyflie is selected + self.crazyflie_checkbutton.setEnabled(False) + self.flypi_checkbutton.setEnabled(False) + + uri = self.crazyflie_selection_box.currentText() + "/E7E7E7E7E7" + + # return an error to the user if otherwise + if uri == "/E7E7E7E7E7": + error_text = "No crazyflie selected." + self.error_label.setText( + "<span style='color: red;'>" + error_text + "</span>") + + # attempt to connect to the crazyflie + self.cf.connect(uri) + self.cf.scf.cf.commander = uCartCommander.Commander(cf1.scf.cf) + self.cf.scf.wait_for_params() + + # offer the user a green error label saying connected when connected + error_text = "Connected to drone" + self.error_label.setText( + "<span style='color: green;'>" + error_text + "</span>") + + # change the connect button to a disconnect button, and disconnect this + # function from it and connect to the on_disconnect function + self.connect.clicked.disconnect(self.on_connect) + self.connect.clicked.connect(self.on_disconnect) + self.connect.setText("Disconnect") + + # connect the crazyflie commander to the setpoint handler + # refresh the logging page so that it displays the toc + # refresh the parameter page so that it displays the correct information + self.setpoint_handler.setCommander(self.cf.scf.cf.commander) + + # enable restricting the gamepad input for the flypi to be more + # stable or not + # TODO enable more granular gamepad configuration in gamepad config menu + if self.flypi_checkbutton.isChecked(): + self.control_tab.setpoint_menu.enable_flypi_mode() + else: + self.control_tab.setpoint_menu.disable_flypi_mode() + + self.logging_config_tab.on_connect() + self.parameter_tab.on_connect() + + def on_disconnect(self): + + # terminate the link in crazyflieprotoconnection + self.cf.disconnect() + + self.crazyflie_checkbutton.setEnabled(True) + self.flypi_checkbutton.setEnabled(True) + + error_text = "" + self.error_label.setText( + "<span style='color: green;'>" + error_text + "</span>") + + # change the disconnect button to a connect button, and disconnect this + # function from it and connect the on_connect function + self.connect.clicked.disconnect(self.on_disconnect) + self.connect.clicked.connect(self.on_connect) + self.connect.setText("Connect") + + # disconnect the commander from setpointhandler + self.setpoint_handler.disconnectCommander() + self.logging_config_tab.on_disconnect() + self.parameter_tab.on_disconnect() + + + + +# Start the application +if __name__ == '__main__': + + app = QApplication(sys.argv) + + cf1 = CrazyflieProtoConnection() + setpoint_handler1 = SetpointHandler() + + window = PyCroCart(cf1, setpoint_handler1) + window.show() + + # Janky but it does work + os._exit(app.exec_()) diff --git a/pycrocart/PyLine.py b/pycrocart/PyLine.py new file mode 100644 index 0000000000000000000000000000000000000000..75508e5d426a417aef3875022b9bf303b80fc974 --- /dev/null +++ b/pycrocart/PyLine.py @@ -0,0 +1,32 @@ +""" +This implements either a horizontal line, or a vertical line. Useful for +adding visible separation in windows. +""" + +from PyQt5 import QtWidgets + + +class QHSeparationLine(QtWidgets.QFrame): + + def __init__(self): + super().__init__() + self.setMinimumWidth(1) + self.setFixedHeight(20) + self.setFrameShape(QtWidgets.QFrame.HLine) + self.setFrameShadow(QtWidgets.QFrame.Sunken) + self.setSizePolicy( + QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum) + return + + +class QVSeparationLine(QtWidgets.QFrame): + + def __init__(self): + super().__init__() + self.setFixedWidth(20) + self.setMinimumHeight(1) + self.setFrameShape(QtWidgets.QFrame.VLine) + self.setFrameShadow(QtWidgets.QFrame.Sunken) + self.setSizePolicy(QtWidgets.QSizePolicy.Minimum, + QtWidgets.QSizePolicy.Preferred) + return diff --git a/pycrocart/ReadMe.md b/pycrocart/ReadMe.md new file mode 100644 index 0000000000000000000000000000000000000000..96b6d14adcb7a59e64c9695a2f795d6293125998 --- /dev/null +++ b/pycrocart/ReadMe.md @@ -0,0 +1,196 @@ +PyCroCart +========= + +Pycrocart is a python implemented GUI that rather than interfacing with +the rich Microcart infrastructure of the backend and the rest of the +quads, is connected only to cflib which can be used to connect to +crazyflie based quadcopters. While it is intended to eventually be +slotted into the Microcart infrastructure, it is also able to function +without it. This allows it to be used both as a debugging tool to define +whether a bug exists in the backend, or the GUI. It also eliminates +a many year buildup of technical debt and poor documentation in the GUI. +Furthermore, in case the backend still has lots of issues like it does for +our team (sdmay23), but you still need to run MP4 with crazyflies, you +can use this GUI to act as a more stable version which will hopefully +allow students to complain less about random crashes and more about how +they can't get their PID to work the way they think it should. + +Pycrocart is implemented using PyQT, and it uses cflib in order to +integrate with a crazyflie. The intention is that we will be able to use +the TCP connection example that bitcraze offers in order to connect to +the FlyPi as well. This will be easier to implement than connecting to +the MicroCART backend, which should be the ultimate end goal. + +Furthermore, while the bitcraze vm is nice, this GUI runs 100 million times +faster and silky smooth when not on a VM. Whether this is a linux issue or a VM +issue I'm not sure yet, but I'd highly recommend not running it on the VM. + +Requirements +------------ +Developed with Python 3.10 64-bit on Windows 10 in Pycharm 2022. + +Library requirements can be installed in a venv via requirements.txt. + +pip install -r requirements.txt + +Or you can use Pycharm to install the requirements by browsing into any .py +file. This is what is probably easiest. + +How to run +---------- +Pycrocart was developed in Pycharm and will work best when used in pycharm. +As of right now, in order to use it you must have a crazyradio plugged into +your computer, and a drone turned on and on the same uri as said in the __ main__ +function. The default right now is 120. Hopefully we will have a connect button +soon so that you can start the gui up and connect to a crazyradio dongle and +drone during runtime. + +In order to run the program from the command line enter: + +python ./Pycrocart.py + +This will execute the program and if all went well you should see the GUI +appear. + + + +You can also run it from Pycharm by clicking the run button near the +__ main__ function inside the Pycrocart.py script. + +______________________ + +How to use +---------- +The pycrocart GUI has several windows: the controls window, the gamepad +configuration window, the logging configuration window, and the parameter +window. + +### Controls Tab +Seen above, the controls tab is the first page shown when entering the GUI. +The controls tab features a setpoint menu which allows the user to send +setpoints of thrust, yaw, pitch, and roll. It can do this in rate mode +(yaw rate, pitch rate, roll rate) or attitude mode which controls the raw +angles themselves. When gamepad mode is enabled the setpoints +enter a "mixed" control mode. This lets the pitch and roll angles to be set +directly but the yaw is not. Instead, the user controls the yaw rate, and the +controller will hold the yaw angle it accumulates to. + +Also in the controls tab is a logging selection menu. This allows the user +to select up to 5 logging signals to plot in the plotting window which covers +the entire right-hand portion of the controls tab. + +### Gamepad Configuration Tab + +Seen below, the gamepad tab is used in order to configure the gamepad to detect +each axis of control correctly. The detect button is used in order to detect +when a joystick or button is pressed. This is a copy and paste of the gamepad +configuration tab in cfclient. If you would like more documentation of how to +use it please see the bitcraze wiki. As of right now, in order to use a gamepad, +it must be connected to the computer at launch of pycrocart. The logitech +controller has been saved, at least on my computer as logitech. Theoretically, +other controllers can be connected as well but this has not been tested by me. + +To connect a controller once the program is running, click configure, select +logitech from the combo box at the bottom of the page and click load to load the +logitech controller configuration. You can also save a configuration. If you +do not click load, a different mapping of inputs will be used. If you do not +click configure, no input from the gamepad will be used. + + + +### Parameter Tab + +Seen below, the parameter tab is able to be used in order to either set +individual parameter values, or whole groups of parameters. There is also a +parameter json file which can be opened by clicking edit from the UI. These +parameters will be sent to the crazyflie whenever the set params from json +file is pressed. Due to the amount of data being sent, a slight delay has been +added to ensure that parameter is set successfully. While there are default +groups within the file for attitude rate and attitude PID's, the file is also +capable of setting other parameters and groups. Anything that is able to be set +from the set parameter interaction is able to be set from the file. + + + +### Logging Tab + +Seen below, the logging tab is used in order to configure logging on +the crazyflie. The signals that are logged are configured within the +logging block setup file. These will then be in use whenever the refresh logging +blocks button is pressed. The signals available to be logged are read directly +from the drones' table of contents and change based on what is available from the +firmware of the quad itself. These signals are all listed on the left-hand side +of the tab, which can be useful for knowing what to write in the logging block +setup file. + + + +The file itself is a json file which contains several logging groups. Each of +these groups has an independent update frequency. Due to the way the crazyflie +firmware works, no more than 5 signals are able to be used in a single group, +and no more than 4 groups are able to be used period. If you attempt to, the +GUI will throw an error in the window at you telling you to do something else. +The signals of a group are configured within the vars array. Each group also has +an independent update frequency in milliseconds. The maximum update frequency in +ms is just over 20 ms, which is also the update rate of the controller in the +crazyflie itself. + + + +The logging blocks in the file are what controls what signals are available +in the controls window. + +_______________________ +Architecture +------------ + + +I cannot verify that the architecture of pycrocart is highly optimal, but I can +tell you how it works, because I threw this thing together in a week and a half. +So first things first, take some time to learn PyQT. It IS NOT hard. You just +need to get a little used to how it works. One of the most useful things to know +is timers and callbacks, which are pretty common in other languages but rarely +seen in python. + +Now that you understand PyQT, lets abstract away from PyQT. The GUI handles its +connection to a gamepad. The way the gamepad is connected was 100% stolen from +the cfclient source code, and is utilized as a Joystick reader. This is passed +into the SetpointHandler whenever the gamepad is enabled, and the raw manual +setpoint is passed in whenever the gamepad is not enabled. + +The GUI is connected to a CrazyflieProtoConnection. This class began as a way to +mock a crazyflie connection, and evolved into a way to manage the crazyflie +connection. This is what handles the Synchronous Crazyflie Connection that is +commonly used in cflib example code. Any information the GUI requires, or tries +to manipulate passes through the CrazyflieProtoConnection. + +Cflib is the open source crazyflie control library. It has a ton of already +implemented methods that handle connecting to a crazyradio, and sending commands. +There are more commands cflib is capable of that we do not utilize. We have +implemented code using the low level commander controls, but there exist types of +setpoints in the low level commander that the GUI does not use. There is also a +HighLevelCommander that can do things such as "take off" or "land". This is +commonly used on full positional controllers. One of the guiding reasons for why +this GUI works well is leveraging cflib. My understanding of what the current +CrazyflieGroundStation does is that it acts as an in house total rewrite of +cflib. This makes little sense for why it exists other than being able to say +we did it ourselves. CrazyflieGroundStation should be rewritten so that it +works with the crazyflie adapter to adapt Microcart commands into something +that can be used with cflib. Cflib is open source, supported by a large community, +and continuing development. + +The only thing that was changed with cflib in order to get pycrocart to function +was the implementation of a custom uCartCommander. This simply adds commands +possible to be sent to a crazyflie, while leaving the rest of the cflib +source code unaltered. This is how the set attitude, and set attitude rates are +accomplished. Similarly, these custom setpoints are also added to the firmware +on the crazyflie. These were added to the source code by I believe the sdmay22 +team, and are a great way to allow MP4 to have access to tune a full attitude +controller. + +One important thing to note is that while the GUI is responsive, there is zero +multithreading/processing happening in order to make this the case. Instead, +a system of timers and callback functions have been used in order to accomplish +things in a timely manner. This does not mean that there are not issues when +using shared resources though. For this reason, I've tried to use semaphores +and locks whenever working with something that is accessed via timers. diff --git a/pycrocart/SetpointHandler.py b/pycrocart/SetpointHandler.py new file mode 100644 index 0000000000000000000000000000000000000000..d8f7e56de9b0267d5d64964ae7c54f6448c6a070 --- /dev/null +++ b/pycrocart/SetpointHandler.py @@ -0,0 +1,215 @@ +""" +Setpoint handler holds classes used to send setpoints to the crazyflie proto +connection in an organized fashion. It is used by the setpoint menu found on +the controls tab. + +""" + +from enum import Enum +from PyQt5.QtCore import QTimer +from uCartCommander import Commander +from threading import Semaphore + + +class FlightMode(Enum): + """ Available flight modes found in crazyflie firmware. """ + TYPE_STOP: int = 0 + TYPE_VELOCITY_WORLD: int = 1 + TYPE_ZDISTANCE: int = 2 + altHoldType: int = 4 + TYPE_HOVER: int = 5 + FULL_STATE_TYPE: int = 6 + TYPE_POSITION: int = 7 + # ------ Custom modes added to firmware by uCart ----------- + ATTITUDE_RATE_TYPE: int = 8 + ATTITUDE_TYPE: int = 9 + MIXED_ATTITUDE_TYPE: int = 10 + + +class Setpoint: + """ Used in order to hold setpoint easily. """ + def __init__(self): + self.yaw: float = 0 + self.pitch: float = 0 + self.roll: float = 0 + self.thrust: int = 0 + + +class SetpointHandler: + """ + Setpoint handler is used to send setpoints to the crazyflie proto + connection in an organized manner. It holds direct access to the + crazyflie's commander, which means that whenever connecting to a + crazyflie, that commander must be configured in the setpoint handler. + This commander is intended to by the custom uCartCommander which is a + modified low level setpoint commander. Currently, no modified version of + the high level commander exists. + + """ + + def __init__(self): + """ + Initialize timer, + + :param commander: uCartCommander taken directly from the synchronous + crazyflie. + """ + + self.setpoint = Setpoint() + self.commander = None + + self.setpoint_semaphore = Semaphore(1) + self._flight_mode = FlightMode.TYPE_STOP + + # Send setpoints to crazyflie every 20 ms. + self.timer = QTimer() + self.timer.timeout.connect(self.update) + self.timer.start(20) + + def setCommander(self, commander: Commander): + """ When the crazyflie is not connected, there will be no valid + commander. Set it during runtime. Enables the use of if commander: + on other functions to check if it is valid. """ + self.commander = commander + + def disconnectCommander(self): + """ Set the commander equal to none so that it won't be called when + we are not actually connected to the crazyflie. """ + self.commander = None + + def update(self): + """ If the flight mode is not stopped, send the current setpoint. """ + if self._flight_mode != FlightMode.TYPE_STOP: + self.sendSetpoint() + + def getFlightMode(self): + """ Returns current flight mode. """ + return self._flight_mode + + def setAttitudeMode(self): + """ Safely set to attitude mode. Uses semaphore in case a callback + happens in the middle of the function. """ + self.setpoint_semaphore.acquire() + self._flight_mode = FlightMode.ATTITUDE_TYPE + self.setpoint_semaphore.release() + + def setMixedAttitudeMode(self): + """ Safely set to mixed attitude mode. Uses semaphore in case a + callback happens in the middle of the function. """ + self.setpoint_semaphore.acquire() + self._flight_mode = FlightMode.MIXED_ATTITUDE_TYPE + self.setpoint_semaphore.release() + + def setRateMode(self): + """ Safely set to rate mode. Uses semaphore in case a callback + happens in the middle of the function. """ + self.setpoint_semaphore.acquire() + self._flight_mode = FlightMode.ATTITUDE_RATE_TYPE + self.setpoint_semaphore.release() + + def startFlying(self): + """ Sends an all 0's setpoint which is the convention to tell the + crazyflie to listen for setpoints. """ + if self.commander: + self.commander.start_flying() + + def stopFlying(self): + """ Tells the crazyflie to stop flying. """ + if self.commander: + self.setpoint_semaphore.acquire() + self._flight_mode = FlightMode.TYPE_STOP + + self.commander.send_notify_setpoint_stop(0) + self.setpoint_semaphore.release() + + def setSetpoint(self, yaw: float, pitch: float, roll: float, thrust: float): + """ Safely sets the crazyflie setpoint. Utilizes semaphore to avoid + reading and writing at the same time due to callbacks. """ + self.setpoint_semaphore.acquire() + + self.setpoint.yaw = yaw + self.setpoint.pitch = pitch + self.setpoint.roll = roll + self.setpoint.thrust = thrust + self.sendSetpointUnsafe() + + self.setpoint_semaphore.release() + + def sendSetpoint(self): + """ Uses commander to send setpoints to crazyflie depending upon the + current flight mode. """ + if self.commander: + self.setpoint_semaphore.acquire() + + if self._flight_mode == FlightMode.ATTITUDE_TYPE: + + # scales thrust from 100 for slider control. + thrust = self.setpoint.thrust * 60000 / 100 + print(f"Set attitude: {self.setpoint.yaw}, {self.setpoint.pitch}, " + f"{self.setpoint.roll}, {thrust}") + + self.commander.send_attitude_setpoint( + self.setpoint.yaw, self.setpoint.pitch, self.setpoint.roll, + thrust) + + elif self._flight_mode == FlightMode.ATTITUDE_RATE_TYPE: + + thrust = self.setpoint.thrust * 60000 / 100 + print(f"Set attitude rate: {self.setpoint.yaw}," + f" {self.setpoint.pitch}, " + f"{self.setpoint.roll}, {thrust}") + + self.commander.send_attitude_rate_setpoint( + self.setpoint.yaw, self.setpoint.pitch, self.setpoint.roll, + thrust) + + elif self._flight_mode == FlightMode.MIXED_ATTITUDE_TYPE: + + # scales thrust from 1000 for more fine-grained gamepad control. + thrust = self.setpoint.thrust * 60000 / 1000 + print(f"Set mixed attitude: {self.setpoint.yaw}," + f" {self.setpoint.pitch}, " + f"{self.setpoint.roll}, {thrust}") + + self.commander.send_mixed_attitude_setpoint( + self.setpoint.yaw, self.setpoint.pitch, self.setpoint.roll, + thrust) + + self.setpoint_semaphore.release() + + def sendSetpointUnsafe(self): + """ Exactly the same as send setpoint but no semaphore is used. """ + + print("Unsafe mode activate :)") + if self.commander: + if self._flight_mode == FlightMode.ATTITUDE_TYPE: + # scales thrust from 100 for slider control. + thrust = self.setpoint.thrust * 60000 / 100 + print(f"Set attitude: {self.setpoint.yaw}, {self.setpoint.pitch}, " + f"{self.setpoint.roll}, {self.setpoint.thrust}") + + self.commander.send_attitude_setpoint( + self.setpoint.yaw, self.setpoint.pitch, self.setpoint.roll, + int(thrust)) + + elif self._flight_mode == FlightMode.ATTITUDE_RATE_TYPE: + + thrust = self.setpoint.thrust * 60000 / 100 + print(f"Set attitude rate: {self.setpoint.yaw}," + f" {self.setpoint.pitch}, " + f"{self.setpoint.roll}, {thrust}") + + self.commander.send_attitude_rate_setpoint( + self.setpoint.yaw, self.setpoint.pitch, self.setpoint.roll, + thrust) + + elif self._flight_mode == FlightMode.MIXED_ATTITUDE_TYPE: + # scales thrust from 1000 for more fine-grained gamepad control. + thrust = self.setpoint.thrust * 60000 / 1000 + print(f"Set mixed attitude: {self.setpoint.yaw}," + f" {self.setpoint.pitch}, " + f"{self.setpoint.roll}, {thrust}") + + self.commander.send_mixed_attitude_setpoint( + self.setpoint.yaw, self.setpoint.pitch, self.setpoint.roll, + thrust) diff --git a/pycrocart/SetpointMenu.py b/pycrocart/SetpointMenu.py new file mode 100644 index 0000000000000000000000000000000000000000..775709d5a8fc1ece0218279b398219a8e47a2e2b --- /dev/null +++ b/pycrocart/SetpointMenu.py @@ -0,0 +1,309 @@ +from PyQt5.QtWidgets import QWidget, QGridLayout, QLabel, QLineEdit, QSlider, \ + QPushButton, QButtonGroup, QCheckBox, QVBoxLayout +from PyQt5.QtCore import Qt, QTimer +from SetpointHandler import SetpointHandler, FlightMode, Setpoint +from cfclient.utils.input import JoystickReader +from GamepadWizardTab import DeviceReader +from cfclient.utils.config_manager import ConfigManager +from threading import Semaphore + + +class SetpointMenu(QWidget): + """ Menu in control tab where setpoints are entered. """ + + def __init__(self, setpoint_handler: SetpointHandler, + joystick: JoystickReader, ): + """ + Initialize menu. + + :param setpoint_handler: When a setpoint is sent, it is sent to the + setpoint handler which handles the interaction with the + crazyflieProtoConnection. + :param joystick: When the setpoint is from the gamepad, it needs to + be read from somewhere. This is where it comes from. + """ + super().__init__() + + self.setpoint_handler = setpoint_handler + self.joystick = joystick + self.joystick_reader = DeviceReader(self.joystick) + + self.flypi_mode = False + + layout = QVBoxLayout() + + grid_widget = QWidget() + grid_layout = QGridLayout() + grid_widget.setLayout(grid_layout) + + layout.addWidget(grid_widget, 1) + + self.setLayout(layout) + self.timer = QTimer() + self.setpoint = Setpoint() + self.setpoint_semaphore = Semaphore(1) # Helps for synchronization with + # setpoint handler + + # ------------------- Gamepad Selection -------------------------------- + + self.gamepad_button = QCheckBox("Gamepad Mode") + self.setpoint_button = QCheckBox("Setpoint Mode") + self.setpoint_button.setChecked(True) + self.gamepad_button.toggled.connect(self.toggleSetpointMode) + self.setpoint_button.toggled.connect(self.toggleSetpointMode) + + self.gamepad_or_setpoint = QButtonGroup() # Grouping buttons can allow + # you to only be able to select one or the other. + self.gamepad_or_setpoint.addButton(self.gamepad_button, 1) + self.gamepad_or_setpoint.addButton(self.setpoint_button, 2) + grid_layout.addWidget(self.gamepad_button, 0, 0) + grid_layout.addWidget(self.setpoint_button, 0, 1) + + # ------------------ Setpoints ----------------------------------------- + yaw_label = QLabel("Yaw Setpoint:") + pitch_label = QLabel("Pitch Setpoint:") + roll_label = QLabel("Roll Setpoint:") + self.yaw_box = QLineEdit() + self.yaw_box.setText('0') # Default to 0. + self.pitch_box = QLineEdit() + self.pitch_box.setText('0') + self.roll_box = QLineEdit() + self.roll_box.setText('0') + + grid_layout.addWidget(yaw_label, 1, 0) + grid_layout.addWidget(self.yaw_box, 1, 1) + grid_layout.addWidget(pitch_label, 2, 0) + grid_layout.addWidget(self.pitch_box, 2, 1) + grid_layout.addWidget(roll_label, 3, 0) + grid_layout.addWidget(self.roll_box, 3, 1) + + # ------------------ Thrust -------------------------------------------- + # Add the sliders and thrust label + thrust_label = QLabel("Thrust:") + self.thrust_slider = QSlider(Qt.Horizontal) + + # Thrust is normally between 0 and 100. Scaled correctly to crazyflie + # in the setpoint handler. For flypi, you only want it between 24% and + # 34%, and so instead everything is on a 1000 scale, and you are adding + # the gamepad input to 240 up to 340. + self.thrust_slider.setMinimum(0) + self.thrust_slider.setMaximum(100) + + grid_layout.addWidget(thrust_label, 4, 0) + grid_layout.addWidget(self.thrust_slider, 4, 1) + + # ----------------- Attitude or Rate mode ------------------------------ + + self.b1 = QCheckBox("Attitude Mode") + self.b2 = QCheckBox("Rate Mode") + self.b2.setChecked(True) # Default to rate mode. + self.b1.toggled.connect(self.toggle_flight_mode_while_running) + self.b2.toggled.connect(self.toggle_flight_mode_while_running) + + self.attitude_or_rate = QButtonGroup() + self.attitude_or_rate.addButton(self.b1, 1) + self.attitude_or_rate.addButton(self.b2, 2) + grid_layout.addWidget(self.b1, 5, 0) + grid_layout.addWidget(self.b2, 5, 1) + + # ----------------- Send Setpoint / Stop Flying ------------------------ + + self.valid_label = QLabel("Setpoint Valid") + + self.send_setpoint_button = QPushButton("Send Setpoint") + self.send_setpoint_button.clicked.connect(self.send_setpoint) + + self.stop_flying_button = QPushButton("Stop Flying") + self.stop_flying_button.clicked.connect(self.stop_flying) + + layout.addWidget(self.valid_label, 2) + layout.addWidget(self.send_setpoint_button, 3) + layout.addWidget(self.stop_flying_button, 4) + + def enable_flypi_mode(self): + self.flypi_mode = True + + def disable_flypi_mode(self): + self.flypi_mode = False + + @staticmethod + def cap_max_value(input_value: float, max_value: float, + min_value: float): + """ Saturate an input value """ + if input_value > max_value: + value = max_value + elif input_value < min_value: + value = min_value + else: + value = input_value + + return value + + def send_setpoint(self): + """ + This is a function on a timer whenever we are attempting to send + setpoints. Grabs the setpoints from the setpoint boxes and thrust + slider. It also checks whether we are in gamepad mixed + attitude control mode, rate mode, or attitude mode. + + """ + + yaw = self.yaw_box.text() + pitch = self.pitch_box.text() + roll = self.roll_box.text() + thrust = self.thrust_slider.value() + + # Have the UI give the user feedback on if the setpoints are invalid. + all_valid = True + try: + yaw = float(yaw) + pitch = float(pitch) + roll = float(roll) + thrust = int(thrust) + self.valid_label.setText("Setpoint Valid") + except ValueError: + all_valid = False + self.valid_label.setText("Setpoint Invalid") + + if all_valid: + # if we are only just starting, we need to set the setpoint handler + # into the right control mode, and also tell the crazyflie we are + # about to start flying. + if self.setpoint_handler.getFlightMode() == FlightMode.TYPE_STOP: + if self.setpoint_handler.commander: # check if connected + self.setpoint_handler.commander.start_flying() + + if self.gamepad_button.isChecked(): + self.setpoint_handler.setMixedAttitudeMode() + else: + if self.b1.isChecked(): + self.setpoint_handler.setAttitudeMode() + else: + self.setpoint_handler.setRateMode() + + if self.gamepad_button.isChecked() and self.flypi_mode: + yaw = self.cap_max_value(-yaw, 90, -90) + pitch = self.cap_max_value(-pitch, 5, -5) + roll = self.cap_max_value(-roll, 5, -5) + + # If you are a flypi do this + # todo enable more granular gamepad configuration in gamepad + # menu + thrust = thrust + 270 + thrust = self.cap_max_value(thrust, 370, 270) + elif self.gamepad_button.isChecked(): + # crazyflie shouldn't be so restricted + thrust = thrust*10 + + self.setpoint_handler.setSetpoint(yaw, pitch, roll, thrust) + + def stop_flying(self): + """ Set the setpoints to all 0, which should stop the drone from + flying. """ + self.setpoint_handler.stopFlying() + + def toggle_flight_mode_while_running(self): + """ If the user tries to change the flight mode, let them. This isn't + available in gamepad mode because the buttons are not enabled and + thus cannot be pressed. """ + + if self.setpoint_handler.getFlightMode() != FlightMode.TYPE_STOP: + if self.b1.isChecked(): + self.setpoint_handler.setAttitudeMode() + else: + self.setpoint_handler.setRateMode() + + def getGamepadSetpoint(self, data): + """ This function is called whenever new data from the gamepad comes + in. """ + + # Using a semaphore in case the setpoint is attempted to be read + # before being completely set. + self.setpoint_semaphore.acquire() + + self.setpoint.yaw = data.yaw + self.setpoint.pitch = data.pitch + self.setpoint.roll = data.roll + self.setpoint.thrust = data.thrust*1.25 # max thrust comes in at 80 + + self.setpoint_semaphore.release() + + def enableGamepad(self, current_selection_name: str): + """ Called when the gamepad checkbox is checked. """ + + # Get gamepad configuration from gamepad wizard, and maps it using + # the loaded gamepad map. + loaded_map = ConfigManager().get_config(current_selection_name) + if loaded_map: + self.joystick.set_raw_input_map(loaded_map) + print("Joystick set") + else: + print("error loading config") + + self.joystick_reader.start_reading() + # callback to setting setpoints whenever the gamepad has changing + # inputs. Notice that setpoints are SET whenever new data comes in, + # but are not neccesarilly SENT. + self.joystick_reader.mapped_values_signal.connect( + self.getGamepadSetpoint) + + def toggleSetpointMode(self): + if self.gamepad_button.isChecked(): + # Gray out the setpoint boxes, so you can't change stuff when in + # gamepad mode. + self.yaw_box.setEnabled(False) + self.pitch_box.setEnabled(False) + self.roll_box.setEnabled(False) + self.thrust_slider.setEnabled(False) + self.b1.setEnabled(False) + self.b2.setEnabled(False) + self.send_setpoint_button.setEnabled(False) + self.stop_flying_button.setEnabled(False) + + # Send new gamepad setpoint every 100 ms + self.timer.timeout.connect(self.sendGamepadSetpoint) + self.timer.start(100) + + else: + self.yaw_box.setEnabled(True) + self.pitch_box.setEnabled(True) + self.roll_box.setEnabled(True) + self.thrust_slider.setEnabled(True) + self.b1.setEnabled(True) + self.b2.setEnabled(True) + self.send_setpoint_button.setEnabled(True) + self.stop_flying_button.setEnabled(True) + + self.timer.timeout.disconnect(self.sendGamepadSetpoint) + self.timer.stop() + + self.setpoint_semaphore.acquire() + + self.setpoint.yaw = 0 + self.setpoint.roll = 0 + self.setpoint.pitch = 0 + self.setpoint.thrust = 0 + + self.yaw_box.setText(str(round(self.setpoint.yaw, 2))) + self.roll_box.setText(str(round(self.setpoint.roll, 2))) + self.pitch_box.setText(str(round(self.setpoint.pitch, 2))) + self.thrust_slider.setValue(int(round(self.setpoint.thrust, 2))) + + self.setpoint_semaphore.release() + + self.stop_flying() + + def sendGamepadSetpoint(self): + # Send the gamepad setpoint, but because that is based on the text + # boxes, set them to correct setpoint. + + self.setpoint_semaphore.acquire() + + self.yaw_box.setText(str(round(self.setpoint.yaw, 2))) + self.roll_box.setText(str(round(self.setpoint.roll, 2))) + self.pitch_box.setText(str(round(self.setpoint.pitch, 2))) + self.thrust_slider.setValue(int(round(self.setpoint.thrust, 2))) + + self.setpoint_semaphore.release() + + self.send_setpoint() diff --git a/pycrocart/__pycache__/ControlTab.cpython-310.pyc b/pycrocart/__pycache__/ControlTab.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fcdf6e9397fb6c8bcddafeba92d789dc93df3081 Binary files /dev/null and b/pycrocart/__pycache__/ControlTab.cpython-310.pyc differ diff --git a/pycrocart/__pycache__/ControlTab.cpython-38.pyc b/pycrocart/__pycache__/ControlTab.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ff85ea30bd593f7b7f648ffeef7e77a3e66624e7 Binary files /dev/null and b/pycrocart/__pycache__/ControlTab.cpython-38.pyc differ diff --git a/pycrocart/__pycache__/ControlWindow.cpython-38.pyc b/pycrocart/__pycache__/ControlWindow.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a13f4cbd67a651b27a601507796af84af13c5a45 Binary files /dev/null and b/pycrocart/__pycache__/ControlWindow.cpython-38.pyc differ diff --git a/pycrocart/__pycache__/CrazyflieProtoConnection.cpython-310.pyc b/pycrocart/__pycache__/CrazyflieProtoConnection.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..288946ae53d0f0e8eb3e07f5ff273b7100ebd9f6 Binary files /dev/null and b/pycrocart/__pycache__/CrazyflieProtoConnection.cpython-310.pyc differ diff --git a/pycrocart/__pycache__/CrazyflieProtoConnection.cpython-38.pyc b/pycrocart/__pycache__/CrazyflieProtoConnection.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f38730a95ea1aa9f3f02fd3eeb0a143d6dd0542c Binary files /dev/null and b/pycrocart/__pycache__/CrazyflieProtoConnection.cpython-38.pyc differ diff --git a/pycrocart/__pycache__/GamepadWizard.cpython-38.pyc b/pycrocart/__pycache__/GamepadWizard.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ccac5c9628e41c9568c243f6316ab7a736dfdbdb Binary files /dev/null and b/pycrocart/__pycache__/GamepadWizard.cpython-38.pyc differ diff --git a/pycrocart/__pycache__/GamepadWizardTab.cpython-310.pyc b/pycrocart/__pycache__/GamepadWizardTab.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4585670a35f7430f81617f2d9670bbfdf692e2f5 Binary files /dev/null and b/pycrocart/__pycache__/GamepadWizardTab.cpython-310.pyc differ diff --git a/pycrocart/__pycache__/GamepadWizardTab.cpython-38.pyc b/pycrocart/__pycache__/GamepadWizardTab.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6ebd1d7c9809a01f8266a41de3eb90644f371f55 Binary files /dev/null and b/pycrocart/__pycache__/GamepadWizardTab.cpython-38.pyc differ diff --git a/pycrocart/__pycache__/LoggingConfigTab.cpython-310.pyc b/pycrocart/__pycache__/LoggingConfigTab.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5d15e0ab8c8b0a52b725e42635c135a4c5cb9f56 Binary files /dev/null and b/pycrocart/__pycache__/LoggingConfigTab.cpython-310.pyc differ diff --git a/pycrocart/__pycache__/LoggingConfigTab.cpython-38.pyc b/pycrocart/__pycache__/LoggingConfigTab.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..81747ac14163086f6cdc1f498b789ebf067bed24 Binary files /dev/null and b/pycrocart/__pycache__/LoggingConfigTab.cpython-38.pyc differ diff --git a/pycrocart/__pycache__/LoggingConfigWindow.cpython-38.pyc b/pycrocart/__pycache__/LoggingConfigWindow.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e4d2fc1d5ea0f536ae4109c22dd1807e3a0f9525 Binary files /dev/null and b/pycrocart/__pycache__/LoggingConfigWindow.cpython-38.pyc differ diff --git a/pycrocart/__pycache__/LoggingSelectionMenu.cpython-310.pyc b/pycrocart/__pycache__/LoggingSelectionMenu.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3afb7ffc8fe963d89f71ff7d18a981a283caa261 Binary files /dev/null and b/pycrocart/__pycache__/LoggingSelectionMenu.cpython-310.pyc differ diff --git a/pycrocart/__pycache__/LoggingSelectionMenu.cpython-38.pyc b/pycrocart/__pycache__/LoggingSelectionMenu.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8e5e3ebc84dce8c3c0f669210bd8ab484074d6fa Binary files /dev/null and b/pycrocart/__pycache__/LoggingSelectionMenu.cpython-38.pyc differ diff --git a/pycrocart/__pycache__/ParameterTab.cpython-310.pyc b/pycrocart/__pycache__/ParameterTab.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d397fed2630b4d31cd19027c607b228a9af28db0 Binary files /dev/null and b/pycrocart/__pycache__/ParameterTab.cpython-310.pyc differ diff --git a/pycrocart/__pycache__/ParameterTab.cpython-38.pyc b/pycrocart/__pycache__/ParameterTab.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..86b18dfceee4a1c742ef06719dffcbf9c7083646 Binary files /dev/null and b/pycrocart/__pycache__/ParameterTab.cpython-38.pyc differ diff --git a/pycrocart/__pycache__/ParameterWindow.cpython-38.pyc b/pycrocart/__pycache__/ParameterWindow.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7f221f45a6ce0b13891ab30c98b8ea94e2255fd8 Binary files /dev/null and b/pycrocart/__pycache__/ParameterWindow.cpython-38.pyc differ diff --git a/pycrocart/__pycache__/PlottingWindow.cpython-310.pyc b/pycrocart/__pycache__/PlottingWindow.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d795a170d932bb121181425663ba0a1445b3db89 Binary files /dev/null and b/pycrocart/__pycache__/PlottingWindow.cpython-310.pyc differ diff --git a/pycrocart/__pycache__/PlottingWindow.cpython-38.pyc b/pycrocart/__pycache__/PlottingWindow.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0b18238ad6a376e9e328a04312607b321666975d Binary files /dev/null and b/pycrocart/__pycache__/PlottingWindow.cpython-38.pyc differ diff --git a/pycrocart/__pycache__/PyLine.cpython-310.pyc b/pycrocart/__pycache__/PyLine.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..adbbee48f01b0fd31f75694364cc202ec91870e9 Binary files /dev/null and b/pycrocart/__pycache__/PyLine.cpython-310.pyc differ diff --git a/pycrocart/__pycache__/PyLine.cpython-38.pyc b/pycrocart/__pycache__/PyLine.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..47064fb5bf7b3b20575ca19bf1c6ce1e4ed2843f Binary files /dev/null and b/pycrocart/__pycache__/PyLine.cpython-38.pyc differ diff --git a/pycrocart/__pycache__/SetpointHandler.cpython-310.pyc b/pycrocart/__pycache__/SetpointHandler.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f340d7b9ea58308873ba1e7d6e5acd73f09aaa0f Binary files /dev/null and b/pycrocart/__pycache__/SetpointHandler.cpython-310.pyc differ diff --git a/pycrocart/__pycache__/SetpointHandler.cpython-38.pyc b/pycrocart/__pycache__/SetpointHandler.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..145d67410bccb129d9e6e616fc8a967e0c87b7f4 Binary files /dev/null and b/pycrocart/__pycache__/SetpointHandler.cpython-38.pyc differ diff --git a/pycrocart/__pycache__/SetpointMenu.cpython-310.pyc b/pycrocart/__pycache__/SetpointMenu.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c9339ee211b2b8fa5842a3e4186d696f869b3979 Binary files /dev/null and b/pycrocart/__pycache__/SetpointMenu.cpython-310.pyc differ diff --git a/pycrocart/__pycache__/SetpointMenu.cpython-38.pyc b/pycrocart/__pycache__/SetpointMenu.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..618ed884b9ac957d8f2be7d6d293c6ad9f9d8e21 Binary files /dev/null and b/pycrocart/__pycache__/SetpointMenu.cpython-38.pyc differ diff --git a/pycrocart/__pycache__/uCartCommander.cpython-310.pyc b/pycrocart/__pycache__/uCartCommander.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a7583baa4d9af54bdeb6bf364ac3e2d217072328 Binary files /dev/null and b/pycrocart/__pycache__/uCartCommander.cpython-310.pyc differ diff --git a/pycrocart/__pycache__/uCartCommander.cpython-38.pyc b/pycrocart/__pycache__/uCartCommander.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c02a45944831533fcf0cd01bac697baadc30a149 Binary files /dev/null and b/pycrocart/__pycache__/uCartCommander.cpython-38.pyc differ diff --git a/pycrocart/logging_variables.json b/pycrocart/logging_variables.json new file mode 100644 index 0000000000000000000000000000000000000000..67b220d72ce664a07482685e1acc87efe8116b87 --- /dev/null +++ b/pycrocart/logging_variables.json @@ -0,0 +1,12 @@ +{ + "group1": + { + "update_frequency_ms": 50, + "vars": ["stateEstimate.roll", "stateEstimate.yaw", "stateEstimate.pitch"] + }, + "group3": + { + "update_frequency_ms": 50, + "vars": ["motor.m1", "motor.m2", "motor.m3", "motor.m4"] + } +} \ No newline at end of file diff --git a/pycrocart/mp4params.json b/pycrocart/mp4params.json new file mode 100644 index 0000000000000000000000000000000000000000..d9b6e2e3585a6daa320abf0fb8fff76969085b7b --- /dev/null +++ b/pycrocart/mp4params.json @@ -0,0 +1,30 @@ +{ + "sys": + { + "e_stop":0 + }, + "s_pid_rate": + { + "roll_kp" : 1, + "roll_ki" : 0, + "roll_kd" : 0, + "pitch_kp": 1, + "pitch_ki": 0, + "pitch_kd": 0, + "yaw_kp" : 40, + "yaw_ki" : 0, + "yaw_kd" : 0 + }, + "s_pid_attitude": + { + "roll_kp" : 250, + "roll_ki" : 10, + "roll_kd" : 30, + "pitch_kp": 250, + "pitch_ki": 10, + "pitch_kd": 40, + "yaw_kp" : 10, + "yaw_ki" : 0, + "yaw_kd" : 0.1 + } +} \ No newline at end of file diff --git a/pycrocart/requirements.txt b/pycrocart/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..2f1d7924ea797dd0e123163c732ed632301443bb --- /dev/null +++ b/pycrocart/requirements.txt @@ -0,0 +1,4 @@ +cfclient~=2023.2 +matplotlib~=3.7.1 +pyqtgraph~=0.13.2 + diff --git a/pycrocart/uCartCommander.py b/pycrocart/uCartCommander.py new file mode 100644 index 0000000000000000000000000000000000000000..f74bc7751bddf477c6c75537cc6d6f84a5abb152 --- /dev/null +++ b/pycrocart/uCartCommander.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# || ____ _ __ +# +------+ / __ )(_) /_______________ _____ ___ +# | 0xBC | / __ / / __/ ___/ ___/ __ `/_ / / _ \ +# +------+ / /_/ / / /_/ /__/ / / /_/ / / /_/ __/ +# || || /_____/_/\__/\___/_/ \__,_/ /___/\___/ +# +# Copyright (C) 2011-2013 Bitcraze AB +# +# Crazyflie Nano Quadcopter Client +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +""" +Used for sending control setpoints to the Crazyflie + +- 4/7/2023 - Austin Beinder uCart 23: + modified to support uCart 22's custom packet types + +""" +import struct +import time + +from cflib.crtp.crtpstack import CRTPPacket +from cflib.crtp.crtpstack import CRTPPort +from cflib.crazyflie.commander import Commander + +__author__ = 'Bitcraze AB' +__all__ = ['Commander'] + +SET_SETPOINT_CHANNEL = 0 +META_COMMAND_CHANNEL = 1 + +TYPE_STOP = 0 +TYPE_VELOCITY_WORLD = 1 +TYPE_ZDISTANCE = 2 +altHoldType = 4 +TYPE_HOVER = 5 +FULL_STATE_TYPE = 6 +TYPE_POSITION = 7 +# Custom modes ----------- +ATTITUDE_RATE_TYPE = 8 +ATTITUDE_TYPE = 9 +MIXED_ATTITUDE_TYPE = 10 # Considering the default commander has a mixed +# attitude setpoint sender, it is unclear to me why this exists, unless it +# didn't exist in the version of the firmware this is based upon. +# ------------------------ + +TYPE_META_COMMAND_NOTIFY_SETPOINT_STOP = 0 + + +class Commander: + """ + Used for sending control setpoints to the Crazyflie + """ + + def __init__(self, crazyflie=None): + """ + Initialize the commander object. By default, the commander is in + +-mode (not x-mode). + """ + self._cf = crazyflie + self._x_mode = False + + def set_client_xmode(self, enabled): + """ + Enable/disable the client side X-mode. When enabled this recalculates + the setpoints before sending them to the Crazyflie. + """ + self._x_mode = enabled + + def send_setpoint(self, roll, pitch, yawrate, thrust): + """ + Send a new control setpoint for roll/pitch/yaw_Rate/thrust to the + copter. + The meaning of these values is depended on the mode of the RPYT + commander in the firmware + Default settings are Roll, pitch, yawrate and thrust + roll, pitch are in degrees + yawrate is in degrees/s + thrust is an integer value ranging from 10001 (next to no power) to + 60000 (full power) + """ + if thrust > 0xFFFF or thrust < 0: + raise ValueError('Thrust must be between 0 and 0xFFFF') + + if self._x_mode: + roll, pitch = 0.707 * (roll - pitch), 0.707 * (roll + pitch) + + pk = CRTPPacket() + pk.port = CRTPPort.COMMANDER + pk.data = struct.pack('<fffH', roll, pitch, yawrate, thrust) + self._cf.send_packet(pk) + print() + + def start_flying(self): + self.send_setpoint(0, 0, 0, 0) + + def send_notify_setpoint_stop(self, remain_valid_milliseconds=0): + """ + Sends a packet so that the priority of the current setpoint to the + lowest non-disabled value, + so any new setpoint regardless of source will overwrite it. + """ + pk = CRTPPacket() + pk.port = CRTPPort.COMMANDER_GENERIC + pk.channel = META_COMMAND_CHANNEL + pk.data = struct.pack('<BI', TYPE_META_COMMAND_NOTIFY_SETPOINT_STOP, + remain_valid_milliseconds) + self._cf.send_packet(pk) + + def send_stop_setpoint(self): + """ + Send STOP setpoint, stopping the motors and (potentially) falling. + """ + pk = CRTPPacket() + pk.port = CRTPPort.COMMANDER_GENERIC + pk.data = struct.pack('<B', TYPE_STOP) + self._cf.send_packet(pk) + + def send_velocity_world_setpoint(self, vx, vy, vz, yawrate): + """ + Send Velocity in the world frame of reference setpoint with yawrate + commands + vx, vy, vz are in m/s + yawrate is in degrees/s + """ + pk = CRTPPacket() + pk.port = CRTPPort.COMMANDER_GENERIC + pk.channel = SET_SETPOINT_CHANNEL + pk.data = struct.pack('<Bffff', TYPE_VELOCITY_WORLD, + vx, vy, vz, yawrate) + self._cf.send_packet(pk) + + def send_zdistance_setpoint(self, roll, pitch, yawrate, zdistance): + """ + Control mode where the height is sent as an absolute setpoint (intended + to be the distance to the surface under the Crazflie), + while giving roll, + pitch and yaw rate commands + roll, pitch are in degrees + yawrate is in degrees/s + zdistance is in meters + """ + pk = CRTPPacket() + pk.port = CRTPPort.COMMANDER_GENERIC + pk.channel = SET_SETPOINT_CHANNEL + pk.data = struct.pack('<Bffff', TYPE_ZDISTANCE, + roll, pitch, yawrate, zdistance) + self._cf.send_packet(pk) + + def send_hover_setpoint(self, vx, vy, yawrate, zdistance): + """ + Control mode where the height is sent as an absolute setpoint (intended + to be the distance to the surface under the Crazflie), while giving x, + y velocity + commands in body-fixed coordinates. + vx, vy are in m/s + yawrate is in degrees/s + zdistance is in meters + """ + pk = CRTPPacket() + pk.port = CRTPPort.COMMANDER_GENERIC + pk.channel = SET_SETPOINT_CHANNEL + pk.data = struct.pack('<Bffff', TYPE_HOVER, + vx, vy, yawrate, zdistance) + self._cf.send_packet(pk) + + def send_position_setpoint(self, x, y, z, yaw): + """ + Control mode where the position is sent as absolute (world) x,y,z + coordinate in + meter and the yaw is the absolute orientation. + x, y, z are in m + yaw is in degrees + """ + pk = CRTPPacket() + pk.port = CRTPPort.COMMANDER_GENERIC + pk.channel = SET_SETPOINT_CHANNEL + pk.data = struct.pack('<Bffff', TYPE_POSITION, + x, y, z, yaw) + self._cf.send_packet(pk) + + # ---------------- Custom changes ---------------------------- + def send_attitude_rate_setpoint(self, yawRate: float, pitchRate: float, + rollRate: float, thrust: float) -> None: + """ + Control mode where the attitude rates are set directly. Rates are in + deg/s. + + :param yawRate: Yaw rotational velocity setpoint + :param pitchRate: Pitch rotational velocity setpoint + :param rollRate: Roll rotational velocity setpoint + :param thrust: Directly set thrust, no PID required + :return: None + """ + pk = CRTPPacket() + pk.port = CRTPPort.COMMANDER_GENERIC + pk.channel = SET_SETPOINT_CHANNEL + pk.data = struct.pack('<Bffff', ATTITUDE_RATE_TYPE, rollRate, + pitchRate, + yawRate, thrust) # todo confirm struct is + # right, I tried to format it using H for the int of thrust and f for + # floats otherwise. Kinda looks like what is above + self._cf.send_packet(pk) + + def send_attitude_setpoint(self, yaw: float, pitch: float, roll: float, + thrust: float) -> None: + """ + Control mode where the attitude angles are set directly. Values are + in deg. + + :param yaw: Yaw rotation angle setpoint + :param pitch: Pitch rotation angle setpoint + :param roll: Roll rotation angle setpoint + :param thrust: Directly set thrust, no PID required + :return: None + """ + pk = CRTPPacket() + pk.port = CRTPPort.COMMANDER_GENERIC + pk.channel = SET_SETPOINT_CHANNEL + pk.data = struct.pack('<Bffff', ATTITUDE_TYPE, roll, pitch, yaw, + thrust) + self._cf.send_packet(pk) # todo confirm struct is right , I tried to + # format it using H for the int of thrust and f for + # floats otherwise. Kinda looks like what is above + + def send_mixed_attitude_setpoint(self, yaw: float, pitch: float, + roll: float, thrust: float) -> None: + """ + Control mode where the attitude angles are set directly, but yaw is + controlled via rate. Values are in deg and deg/s. + + :param yaw: Yaw rotation angular velocity setpoint + :param pitch: Pitch rotation angle setpoint + :param roll: Roll rotation angle setpoint + :param thrust: Directly set thrust, no PID required + :return: None + """ + pk = CRTPPacket() + pk.port = CRTPPort.COMMANDER_GENERIC + pk.channel = SET_SETPOINT_CHANNEL + pk.data = struct.pack('<Bffff', MIXED_ATTITUDE_TYPE, roll, pitch, yaw, + thrust) + self._cf.send_packet(pk) # todo confirm struct is right , I tried to + # format it using H for the int of thrust and f for + # floats otherwise. Kinda looks like what is above