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) 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.b1.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) @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: 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() yaw = self.cap_max_value(yaw, 20, -20) pitch = self.cap_max_value(pitch, 5, -5) roll = self.cap_max_value(roll, 5, -5) # If you are a flypi do this thrust = thrust + 100 thrust = self.cap_max_value(thrust, 340, 240) # otherwise don't..... but I'm not changing that rn 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 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.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()