Something went wrong on our end
SetpointMenu.py 10.71 KiB
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()