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.
+
+![./Images/gui_main_window.png](./Images/gui_main_window.png)
+
+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.
+
+![./Images/gamepad_tab.png](./Images/gamepad_tab.png)
+
+### 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.
+
+![./Images/parameter_tab.png](./Images/parameter_tab.png)
+
+### 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.
+
+![./Images/logging_tab.png](./Images/logging_tab.png)
+
+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.
+
+![./Images/logging_file.png](./Images/logging_file.png)
+
+The logging blocks in the file are what controls what signals are available
+in the controls window.
+
+_______________________
+Architecture
+------------
+![./Images/pycrocartArchitecture.png](./Images/pycrocartArchitecture.png)
+
+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