From 00c4e3f439007ec36f1ac516ff8c7ce94fff2352 Mon Sep 17 00:00:00 2001 From: abeinder <abeinder@iastate.edu> Date: Wed, 26 Apr 2023 11:30:07 -0500 Subject: [PATCH] added rest of documentation --- pycrocart/.idea/workspace.xml | 36 +++++++++---- pycrocart/CrazyflieProtoConnection.py | 78 ++++++++++++++++++++++++--- pycrocart/LoggingConfigTab.py | 38 ++++++++++++- pycrocart/LoggingSelectionMenu.py | 23 +++++++- pycrocart/ParameterTab.py | 45 ++++++++++++++-- pycrocart/PlottingWindow.py | 38 ++++++++----- pycrocart/PyCroCart.py | 2 +- pycrocart/PyLine.py | 8 ++- pycrocart/SetpointHandler.py | 76 +++++++++++++++++++------- pycrocart/SetpointMenu.py | 4 +- 10 files changed, 287 insertions(+), 61 deletions(-) diff --git a/pycrocart/.idea/workspace.xml b/pycrocart/.idea/workspace.xml index 20ebf6909..d2d50689a 100644 --- a/pycrocart/.idea/workspace.xml +++ b/pycrocart/.idea/workspace.xml @@ -1,17 +1,16 @@ <?xml version="1.0" encoding="UTF-8"?> <project version="4"> <component name="ChangeListManager"> - <list default="true" id="279c12a8-b571-4465-a118-8f46f6730b05" name="Changes" comment="add working gamepad control and ReadMe.md"> + <list default="true" id="279c12a8-b571-4465-a118-8f46f6730b05" name="Changes" comment="added comments"> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/ControlWindow.py" beforeDir="false" afterPath="$PROJECT_DIR$/ControlTab.py" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/Examples/animated_graph_example.py" beforeDir="false" afterPath="$PROJECT_DIR$/Examples/animated_graph_example.py" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/GamepadWizard.py" beforeDir="false" afterPath="$PROJECT_DIR$/GamepadWizardTab.py" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/LoggingConfigWindow.py" beforeDir="false" afterPath="$PROJECT_DIR$/LoggingConfigTab.py" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/ParameterWindow.py" beforeDir="false" afterPath="$PROJECT_DIR$/ParameterTab.py" afterDir="false" /> + <change beforePath="$PROJECT_DIR$/CrazyflieProtoConnection.py" beforeDir="false" afterPath="$PROJECT_DIR$/CrazyflieProtoConnection.py" afterDir="false" /> + <change beforePath="$PROJECT_DIR$/LoggingConfigTab.py" beforeDir="false" afterPath="$PROJECT_DIR$/LoggingConfigTab.py" afterDir="false" /> + <change beforePath="$PROJECT_DIR$/LoggingSelectionMenu.py" beforeDir="false" afterPath="$PROJECT_DIR$/LoggingSelectionMenu.py" afterDir="false" /> + <change beforePath="$PROJECT_DIR$/ParameterTab.py" beforeDir="false" afterPath="$PROJECT_DIR$/ParameterTab.py" afterDir="false" /> <change beforePath="$PROJECT_DIR$/PlottingWindow.py" beforeDir="false" afterPath="$PROJECT_DIR$/PlottingWindow.py" afterDir="false" /> <change beforePath="$PROJECT_DIR$/PyCroCart.py" beforeDir="false" afterPath="$PROJECT_DIR$/PyCroCart.py" afterDir="false" /> + <change beforePath="$PROJECT_DIR$/PyLine.py" beforeDir="false" afterPath="$PROJECT_DIR$/PyLine.py" afterDir="false" /> <change beforePath="$PROJECT_DIR$/SetpointHandler.py" beforeDir="false" afterPath="$PROJECT_DIR$/SetpointHandler.py" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/SetpointMenu.py" beforeDir="false" afterPath="$PROJECT_DIR$/SetpointMenu.py" afterDir="false" /> </list> <option name="SHOW_DIALOG" value="false" /> <option name="HIGHLIGHT_CONFLICTS" value="true" /> @@ -33,6 +32,14 @@ </option> <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$/.." /> </component> + <component name="GitSEFilterConfiguration"> + <file-type-list> + <filtered-out-file-type name="LOCAL_BRANCH" /> + <filtered-out-file-type name="REMOTE_BRANCH" /> + <filtered-out-file-type name="TAG" /> + <filtered-out-file-type name="COMMIT_BY_MESSAGE" /> + </file-type-list> + </component> <component name="MarkdownSettingsMigration"> <option name="stateVersion" value="1" /> </component> @@ -256,7 +263,8 @@ <workItem from="1682362425838" duration="5156000" /> <workItem from="1682441377494" duration="2792000" /> <workItem from="1682445089720" duration="8546000" /> - <workItem from="1682476048756" duration="8000" /> + <workItem from="1682476048756" duration="137000" /> + <workItem from="1682521516348" duration="4679000" /> </task> <task id="LOCAL-00001" summary="initial push"> <created>1681663716970</created> @@ -300,7 +308,14 @@ <option name="project" value="LOCAL" /> <updated>1682454191112</updated> </task> - <option name="localTasksCounter" value="7" /> + <task id="LOCAL-00007" summary="added comments"> + <created>1682479884093</created> + <option name="number" value="00007" /> + <option name="presentableId" value="LOCAL-00007" /> + <option name="project" value="LOCAL" /> + <updated>1682479884093</updated> + </task> + <option name="localTasksCounter" value="8" /> <servers /> </component> <component name="TypeScriptGeneratedFilesManager"> @@ -323,7 +338,8 @@ <MESSAGE value="removed a ton of warnings" /> <MESSAGE value="current working version" /> <MESSAGE value="add working gamepad control and ReadMe.md" /> - <option name="LAST_COMMIT_MESSAGE" value="add working gamepad control and ReadMe.md" /> + <MESSAGE value="added comments" /> + <option name="LAST_COMMIT_MESSAGE" value="added comments" /> </component> <component name="XDebuggerManager"> <breakpoint-manager> diff --git a/pycrocart/CrazyflieProtoConnection.py b/pycrocart/CrazyflieProtoConnection.py index 0b1098c73..cdb898145 100644 --- a/pycrocart/CrazyflieProtoConnection.py +++ b/pycrocart/CrazyflieProtoConnection.py @@ -1,3 +1,17 @@ +""" +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 @@ -11,8 +25,20 @@ from cflib.crazyflie.log import LogConfig class CrazyflieProtoConnection: + """ + Handles all interactions with cflib. + """ + def __init__(self): - # Set up the timer to update the plot + """ + 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() @@ -23,6 +49,8 @@ class CrazyflieProtoConnection: self.logging_configs = [] 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 @@ -32,6 +60,8 @@ class CrazyflieProtoConnection: 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 @@ -50,36 +80,45 @@ class CrazyflieProtoConnection: 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 + 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 {} + return [] except AttributeError: pass - return {} + 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 @@ -90,6 +129,10 @@ class CrazyflieProtoConnection: 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) @@ -103,40 +146,61 @@ class CrazyflieProtoConnection: 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): - + 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() def disconnect(self): + """ Disconnect from crazyflie. """ if self.is_connected: self.scf.close_link() @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/LoggingConfigTab.py b/pycrocart/LoggingConfigTab.py index 19845fd87..e3a76e084 100644 --- a/pycrocart/LoggingConfigTab.py +++ b/pycrocart/LoggingConfigTab.py @@ -7,8 +7,24 @@ 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() @@ -88,26 +104,37 @@ class LoggingConfigTab(QWidget): @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): - print("refresh") + """ Whenever the refresh button is clicked, call this function. """ + self.cf.clear_logging_configs() self.update_logging_values() self.set_logging_block_from_file() 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()): @@ -115,9 +142,12 @@ class LoggingConfigTab(QWidget): 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 = [] @@ -171,6 +201,8 @@ class LoggingConfigTab(QWidget): 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 @@ -200,14 +232,18 @@ class LoggingConfigTab(QWidget): "<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 index 72657f28b..4d19a48ac 100644 --- a/pycrocart/LoggingSelectionMenu.py +++ b/pycrocart/LoggingSelectionMenu.py @@ -2,6 +2,11 @@ from PyQt5.QtWidgets import QWidget, QVBoxLayout, QComboBox 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): super().__init__() @@ -13,28 +18,40 @@ class LoggingSelectionMenu(QWidget): self.cbs = [QComboBox(), QComboBox(), QComboBox(), QComboBox(), QComboBox()] + # Default logging variables to Logging Variable 1,2,3,4,5 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], i+1) + # 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+1 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.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(), @@ -44,12 +61,14 @@ class LoggingSelectionMenu(QWidget): 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]) - # cb1 + # 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( diff --git a/pycrocart/ParameterTab.py b/pycrocart/ParameterTab.py index 86de41fd5..ab80f04fe 100644 --- a/pycrocart/ParameterTab.py +++ b/pycrocart/ParameterTab.py @@ -22,7 +22,7 @@ class ParameterTab(QWidget): layout = QGridLayout() self.setLayout(layout) - self.cf = cf # Crayzflie proto connection + self.cf = cf # Crazyflie proto connection self.sending = False self.sending_queue = Queue() @@ -166,14 +166,19 @@ class ParameterTab(QWidget): self.progress_bar.setMaximumWidth(200) layout.addWidget(self.progress_bar, 12, 1, alignment=Qt.AlignHCenter) + # for connecting during runtime this would need to be moved self.on_connect() def on_connect(self): - """ Whenever connecting to the crazyflie, """ + """ 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 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() @@ -182,22 +187,37 @@ class ParameterTab(QWidget): 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() 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() 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() @@ -216,10 +236,14 @@ class ParameterTab(QWidget): @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: @@ -230,22 +254,33 @@ class ParameterTab(QWidget): 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]}) + 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'] diff --git a/pycrocart/PlottingWindow.py b/pycrocart/PlottingWindow.py index 74a369437..520c40c8a 100644 --- a/pycrocart/PlottingWindow.py +++ b/pycrocart/PlottingWindow.py @@ -3,11 +3,27 @@ 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', 'Sine Wave Plot') + self.setLabel('top', 'Logging Variables Plot') self._num_axes = num_axes self.current_iter = 0 @@ -17,26 +33,21 @@ class PlottingWindow(pg.PlotWidget): 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)) self.plot_curves = \ [self.plot(self.plot_data_xs[:, i], self.plot_data_ys[:, i], - pen=pg.mkPen(pg.intColor(i), width=2)) 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 + pen=pg.mkPen(pg.intColor(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 @@ -47,6 +58,7 @@ class PlottingWindow(pg.PlotWidget): 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 diff --git a/pycrocart/PyCroCart.py b/pycrocart/PyCroCart.py index 07af7b577..5ea6ea8ec 100644 --- a/pycrocart/PyCroCart.py +++ b/pycrocart/PyCroCart.py @@ -66,7 +66,7 @@ if __name__ == '__main__': cf1.scf.cf.commander = uCartCommander.Commander(cf1.scf.cf) time.sleep(1) - setpoint_handler1 = SetpointHandler(cf1.scf.cf.commander, cf1) + setpoint_handler1 = SetpointHandler(cf1.scf.cf.commander) window = PyCroCart(cf1, setpoint_handler1) window.tab4.on_refresh() diff --git a/pycrocart/PyLine.py b/pycrocart/PyLine.py index 94ad9b0af..75508e5d4 100644 --- a/pycrocart/PyLine.py +++ b/pycrocart/PyLine.py @@ -1,3 +1,8 @@ +""" +This implements either a horizontal line, or a vertical line. Useful for +adding visible separation in windows. +""" + from PyQt5 import QtWidgets @@ -22,5 +27,6 @@ class QVSeparationLine(QtWidgets.QFrame): self.setMinimumHeight(1) self.setFrameShape(QtWidgets.QFrame.VLine) self.setFrameShadow(QtWidgets.QFrame.Sunken) - self.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred) + self.setSizePolicy(QtWidgets.QSizePolicy.Minimum, + QtWidgets.QSizePolicy.Preferred) return diff --git a/pycrocart/SetpointHandler.py b/pycrocart/SetpointHandler.py index 79dcba6ee..7e6e8f8ac 100644 --- a/pycrocart/SetpointHandler.py +++ b/pycrocart/SetpointHandler.py @@ -1,12 +1,18 @@ -from enum import Enum +""" +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 PyQt5.QtCore import QTimer +""" +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 @@ -14,14 +20,14 @@ class FlightMode(Enum): TYPE_HOVER: int = 5 FULL_STATE_TYPE: int = 6 TYPE_POSITION: int = 7 - # Custom modes ----------- + # ------ 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 @@ -30,48 +36,73 @@ class Setpoint: 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, commander: Commander, cf): + """ + + def __init__(self, commander: Commander): + """ + Initialize timer, + + :param commander: uCartCommander taken directly from the synchronous + crazyflie. + """ self.setpoint = Setpoint() self.commander = commander - self.cf = cf self.setpoint_semaphore = Semaphore(1) self._flight_mode = FlightMode.TYPE_STOP - # Set up the timer to update the plot + # Send setpoints to crazyflie every 20 ms. self.timer = QTimer() self.timer.timeout.connect(self.update) - self.timer.start(500) + self.timer.start(20) 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. """ self.commander.start_flying() def stopFlying(self): - + """ Tells the crazyflie to stop flying. """ self.setpoint_semaphore.acquire() self._flight_mode = FlightMode.TYPE_STOP @@ -80,7 +111,8 @@ class SetpointHandler: print("Stop flying") 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 @@ -92,12 +124,14 @@ class SetpointHandler: self.setpoint_semaphore.release() def sendSetpoint(self): - + """ Uses commander to send setpoints to crazyflie depending upon the + current flight mode. """ self.setpoint_semaphore.acquire() if self._flight_mode == FlightMode.ATTITUDE_TYPE: - thrust = self.setpoint.thrust * 65000 / 1000 + # scales thrust from 100 for slider control. + thrust = self.setpoint.thrust * 65000 / 100 print(f"Set attitude: {self.setpoint.yaw}, {self.setpoint.pitch}, " f"{self.setpoint.roll}, {thrust}") @@ -107,7 +141,7 @@ class SetpointHandler: elif self._flight_mode == FlightMode.ATTITUDE_RATE_TYPE: - thrust = self.setpoint.thrust * 65000 / 1000 + thrust = self.setpoint.thrust * 65000 / 100 print(f"Set attitude rate: {self.setpoint.yaw}," f" {self.setpoint.pitch}, " f"{self.setpoint.roll}, {thrust}") @@ -118,21 +152,25 @@ class SetpointHandler: elif self._flight_mode == FlightMode.MIXED_ATTITUDE_TYPE: + # scales thrust from 1000 for more fine-grained gamepad control. thrust = self.setpoint.thrust * 65000 / 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.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._flight_mode == FlightMode.ATTITUDE_TYPE: - - thrust = self.setpoint.thrust * 65000 / 1000 + # scales thrust from 100 for slider control. + thrust = self.setpoint.thrust * 65000 / 100 print(f"Set attitude: {self.setpoint.yaw}, {self.setpoint.pitch}, " f"{self.setpoint.roll}, {self.setpoint.thrust}") @@ -142,7 +180,7 @@ class SetpointHandler: elif self._flight_mode == FlightMode.ATTITUDE_RATE_TYPE: - thrust = self.setpoint.thrust * 65000 / 1000 + thrust = self.setpoint.thrust * 65000 / 100 print(f"Set attitude rate: {self.setpoint.yaw}," f" {self.setpoint.pitch}, " f"{self.setpoint.roll}, {thrust}") @@ -152,7 +190,7 @@ class SetpointHandler: thrust) elif self._flight_mode == FlightMode.MIXED_ATTITUDE_TYPE: - + # scales thrust from 1000 for more fine-grained gamepad control. thrust = self.setpoint.thrust * 65000 / 1000 print(f"Set mixed attitude: {self.setpoint.yaw}," f" {self.setpoint.pitch}, " diff --git a/pycrocart/SetpointMenu.py b/pycrocart/SetpointMenu.py index 7123ebeb1..a4a3580e8 100644 --- a/pycrocart/SetpointMenu.py +++ b/pycrocart/SetpointMenu.py @@ -83,7 +83,7 @@ class SetpointMenu(QWidget): # 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 + # 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) @@ -136,7 +136,7 @@ class SetpointMenu(QWidget): """ 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 or not we are in gamepad mixed + slider. It also checks whether we are in gamepad mixed attitude control mode, rate mode, or attitude mode. """ -- GitLab