|
| 1 | +""" |
| 2 | +MAVSDK Offboard Control - Attitude Rate Control Sender |
| 3 | +====================================================== |
| 4 | +
|
| 5 | +This script provides an interface for controlling a drone's attitude rates (roll, pitch, yaw rates) and thrust |
| 6 | +through keyboard inputs, utilizing MAVSDK over UDP. It features an interactive GUI built with Pygame |
| 7 | +for real-time control and feedback, enabling dynamic adjustment of the drone's flight parameters. |
| 8 | +
|
| 9 | +Overview: |
| 10 | +--------- |
| 11 | +- Sends control packets to command drone attitude rates and thrust in local body coordinates. |
| 12 | +- Offers two modes of operation: 'Instant Reset' and 'Incremental Control', toggled by pressing 'M'. |
| 13 | +- Provides a graphical interface to visualize and control the drone's attitude rates and thrust. |
| 14 | +
|
| 15 | +Setup Requirements: |
| 16 | +------------------- |
| 17 | +- A MAVSDK-compatible drone or a SITL setup running and accessible on the network. |
| 18 | +- The receiver node (`receiver.py`) must be operational to handle and execute the commands sent from this script. |
| 19 | +- Ensure that the receiver and this sender script are configured to communicate over the specified IP and port. |
| 20 | +
|
| 21 | +Key Functionalities: |
| 22 | +-------------------- |
| 23 | +- **Attitude Rate Control**: Use W, S, A, D for adjusting pitch and roll rates. |
| 24 | + - W: Decrease pitch rate (nose down) |
| 25 | + - S: Increase pitch rate (nose up) |
| 26 | + - A: Decrease roll rate (left down) |
| 27 | + - D: Increase roll rate (right down) |
| 28 | +- **Thrust Adjustment**: Up and Down arrow keys adjust thrust. |
| 29 | +- **Yaw Rate Control**: Left and Right arrow keys adjust yaw rate. |
| 30 | +- **Mode Switching**: Press 'M' to toggle between 'Instant Reset' and 'Incremental Control' modes. |
| 31 | +- **Control Enable/Disable**: 'E' to enable sending commands, 'C' to cancel and send a stop command. |
| 32 | +- **Emergency Hold**: Press 'H' to immediately hold the current attitude rates and thrust, effectively stopping any adjustments. |
| 33 | +- **Application Exit**: Press 'Q' to safely exit the application, ensuring all movements are halted. |
| 34 | +
|
| 35 | +Usage Instructions: |
| 36 | +------------------- |
| 37 | +1. Ensure your MAVSDK setup (either SITL or a real drone) is operational and that `receiver.py` is running. |
| 38 | +2. Start this script in a Python environment where Pygame is installed. The script's GUI will display on your screen. |
| 39 | +3. Use the keyboard controls as outlined to command the drone. Ensure you start command transmission by pressing 'E' and can stop it anytime with 'H' or 'C'. |
| 40 | +
|
| 41 | +Safety Notice: |
| 42 | +-------------- |
| 43 | +- When operating with a real drone, ensure you are in a safe, open environment to avoid any accidents. |
| 44 | +- Always be prepared to take manual control of the drone if necessary. |
| 45 | +
|
| 46 | +Author: |
| 47 | +------- |
| 48 | +- Alireza Ghaderi |
| 49 | +- GitHub: alireza787b |
| 50 | +- Date: May 2024 |
| 51 | +
|
| 52 | +Dependencies: |
| 53 | +------------- |
| 54 | +- Pygame for GUI operations. |
| 55 | +- MAVSDK for drone control interfacing. |
| 56 | +- Python's `socket` library for UDP communication. |
| 57 | +- `control_packet.py` for formatting control commands. |
| 58 | +
|
| 59 | +The code is designed to be clear and modifiable for different use cases, allowing adjustments to IP settings, control rates, and more directly within the script. |
| 60 | +
|
| 61 | +""" |
| 62 | +import socket |
| 63 | +import pygame |
| 64 | +import sys |
| 65 | +from control_packet import ControlPacket, SetpointMode |
| 66 | + |
| 67 | +# Constants for communication and control |
| 68 | +UDP_IP = "127.0.0.1" |
| 69 | +UDP_PORT = 5005 |
| 70 | +SEND_RATE = 0.1 # Packet send rate in seconds (10 Hz) |
| 71 | +ROLL_PITCH_RATE_STEP = 2.0 # degrees per second step for roll and pitch rate |
| 72 | +YAW_RATE_STEP = 5.0 # degrees per second step for yaw rate |
| 73 | +THRUST_STEP = 0.02 # thrust step |
| 74 | +INCREMENTAL_MODE = False # False for instant reset, True for incremental control |
| 75 | + |
| 76 | +# Initialize Pygame and set up the display |
| 77 | +pygame.init() |
| 78 | +screen = pygame.display.set_mode((800, 600)) |
| 79 | +pygame.display.set_caption('MAVSDK Offboard Control - Attitude Rate Control') |
| 80 | + |
| 81 | +# Colors, fonts, and initial settings |
| 82 | +BACKGROUND_COLOR = (30, 30, 30) |
| 83 | +TEXT_COLOR = (255, 255, 255) |
| 84 | +FONT = pygame.font.Font(None, 36) |
| 85 | +SMALL_FONT = pygame.font.Font(None, 24) |
| 86 | +GREEN = (0, 255, 0) |
| 87 | +RED = (255, 0, 0) |
| 88 | + |
| 89 | +# Setup UDP socket for sending commands |
| 90 | +sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
| 91 | + |
| 92 | +def send_attitude_rate(roll_rate, pitch_rate, yaw_rate, thrust): |
| 93 | + """Send an attitude rate command to the drone.""" |
| 94 | + packet = ControlPacket( |
| 95 | + mode=SetpointMode.ATTITUDE_RATE_CONTROL, |
| 96 | + enable_flag=True, |
| 97 | + yaw_control_flag=True, |
| 98 | + position=(0, 0, 0), # Not used in attitude rate mode |
| 99 | + velocity=(0, 0, 0), # Not used in attitude rate mode |
| 100 | + acceleration=(0, 0, 0), # Not used in attitude rate mode |
| 101 | + attitude=(0, 0, 0, thrust), # Thrust only |
| 102 | + attitude_rate=(roll_rate, pitch_rate, yaw_rate) |
| 103 | + ) |
| 104 | + packed_data = packet.pack() |
| 105 | + sock.sendto(packed_data, (UDP_IP, UDP_PORT)) |
| 106 | + |
| 107 | +def display_text(message, position, font=FONT, color=TEXT_COLOR): |
| 108 | + """Displays text on the Pygame screen at the given position.""" |
| 109 | + text = font.render(message, True, color) |
| 110 | + screen.blit(text, position) |
| 111 | + |
| 112 | +class Button: |
| 113 | + """Button class to create interactive GUI buttons.""" |
| 114 | + def __init__(self, text, position, size, action, release_action=None): |
| 115 | + self.text = text |
| 116 | + self.position = position |
| 117 | + self.size = size |
| 118 | + self.action = action |
| 119 | + self.release_action = release_action |
| 120 | + self.color = (100, 100, 100) |
| 121 | + self.active = False |
| 122 | + |
| 123 | + def draw(self, screen): |
| 124 | + color = (150, 150, 150) if self.active else self.color |
| 125 | + pygame.draw.rect(screen, color, (*self.position, *self.size)) |
| 126 | + text_surface = SMALL_FONT.render(self.text, True, TEXT_COLOR) |
| 127 | + text_rect = text_surface.get_rect(center=(self.position[0] + self.size[0] // 2, self.position[1] + self.size[1] // 2)) |
| 128 | + screen.blit(text_surface, text_rect) |
| 129 | + |
| 130 | + def click(self): |
| 131 | + self.active = True |
| 132 | + self.action() |
| 133 | + |
| 134 | + def release(self): |
| 135 | + self.active = False |
| 136 | + if self.release_action: |
| 137 | + self.release_action() |
| 138 | + |
| 139 | + def is_clicked(self, mouse_pos): |
| 140 | + x, y = mouse_pos |
| 141 | + return (self.position[0] <= x <= self.position[0] + self.size[0]) and (self.position[1] <= y <= self.position[1] + self.size[1]) |
| 142 | + |
| 143 | +# Movement control variables |
| 144 | +roll_rate, pitch_rate, yaw_rate, thrust = 0, 0, 0, 0.5 # Start with a neutral thrust value |
| 145 | +enabled = False |
| 146 | + |
| 147 | +# Button actions |
| 148 | +def enable_control(): |
| 149 | + global enabled |
| 150 | + enabled = True |
| 151 | + |
| 152 | +def disable_control(): |
| 153 | + global enabled |
| 154 | + enabled = False |
| 155 | + send_attitude_rate(0, 0, 0, 0) |
| 156 | + |
| 157 | +def reset_control(): |
| 158 | + global roll_rate, pitch_rate, yaw_rate, thrust |
| 159 | + roll_rate, pitch_rate, yaw_rate, thrust = 0, 0, 0, 0.5 # Reset to neutral thrust |
| 160 | + |
| 161 | +def adjust_pitch_rate_up(): |
| 162 | + global pitch_rate |
| 163 | + pitch_rate += ROLL_PITCH_RATE_STEP if INCREMENTAL_MODE else ROLL_PITCH_RATE_STEP |
| 164 | + |
| 165 | +def adjust_pitch_rate_down(): |
| 166 | + global pitch_rate |
| 167 | + pitch_rate -= ROLL_PITCH_RATE_STEP if INCREMENTAL_MODE else ROLL_PITCH_RATE_STEP |
| 168 | + |
| 169 | +def adjust_roll_rate_left(): |
| 170 | + global roll_rate |
| 171 | + roll_rate -= ROLL_PITCH_RATE_STEP if INCREMENTAL_MODE else ROLL_PITCH_RATE_STEP |
| 172 | + |
| 173 | +def adjust_roll_rate_right(): |
| 174 | + global roll_rate |
| 175 | + roll_rate += ROLL_PITCH_RATE_STEP if INCREMENTAL_MODE else ROLL_PITCH_RATE_STEP |
| 176 | + |
| 177 | +def increase_thrust(): |
| 178 | + global thrust |
| 179 | + thrust = min(thrust + THRUST_STEP, 1.0) # Ensure thrust does not exceed 1 |
| 180 | + |
| 181 | +def decrease_thrust(): |
| 182 | + global thrust |
| 183 | + thrust = max(thrust - THRUST_STEP, 0) # Ensure thrust does not go below 0 |
| 184 | + |
| 185 | +def yaw_rate_left(): |
| 186 | + global yaw_rate |
| 187 | + yaw_rate -= YAW_RATE_STEP if not INCREMENTAL_MODE else (yaw_rate - YAW_RATE_STEP) |
| 188 | + |
| 189 | +def yaw_rate_right(): |
| 190 | + global yaw_rate |
| 191 | + yaw_rate += YAW_RATE_STEP if not INCREMENTAL_MODE else (yaw_rate + YAW_RATE_STEP) |
| 192 | + |
| 193 | +def toggle_mode(): |
| 194 | + global INCREMENTAL_MODE |
| 195 | + INCREMENTAL_MODE = not INCREMENTAL_MODE |
| 196 | + |
| 197 | +# Reset functions for instant return mode |
| 198 | +def reset_roll_rate(): |
| 199 | + global roll_rate |
| 200 | + roll_rate = 0 |
| 201 | + |
| 202 | +def reset_pitch_rate(): |
| 203 | + global pitch_rate |
| 204 | + pitch_rate = 0 |
| 205 | + |
| 206 | +def reset_thrust(): |
| 207 | + # global thrust |
| 208 | + # thrust = 0.5 # Reset to mid thrust |
| 209 | + pass |
| 210 | + |
| 211 | +def reset_yaw_rate(): |
| 212 | + global yaw_rate |
| 213 | + yaw_rate = 0 |
| 214 | + |
| 215 | +# Button actions |
| 216 | +def check_enabled(action): |
| 217 | + """Decorator-like function to execute the action only if controls are enabled.""" |
| 218 | + def wrapper(): |
| 219 | + if enabled: |
| 220 | + action() |
| 221 | + return wrapper |
| 222 | + |
| 223 | +# Wrapper function to reset only if incremental mode is not active |
| 224 | +def check_and_reset(action): |
| 225 | + """Decorator-like function to reset only if incremental mode is not active.""" |
| 226 | + def wrapper(): |
| 227 | + if not INCREMENTAL_MODE: |
| 228 | + action() |
| 229 | + return wrapper |
| 230 | + |
| 231 | +# Button initialization with joystick-style layout |
| 232 | +buttons = [ |
| 233 | + Button('Enable', (50, 150), (100, 50), enable_control), |
| 234 | + Button('Disable', (50, 220), (100, 50), disable_control), |
| 235 | + Button('Hold', (50, 290), (100, 50), reset_control), |
| 236 | + Button('Mode', (50, 360), (100, 50), toggle_mode), # Mode toggle button |
| 237 | + Button('Pitch Up', (600, 290), (100, 50), check_enabled(adjust_pitch_rate_up), check_and_reset(reset_pitch_rate)), |
| 238 | + Button('Pitch Down', (600, 150), (100, 50), check_enabled(adjust_pitch_rate_down), check_and_reset(reset_pitch_rate)), |
| 239 | + Button('Roll Left', (500, 220), (100, 50), check_enabled(adjust_roll_rate_left), check_and_reset(reset_roll_rate)), |
| 240 | + Button('Roll Right', (700, 220), (100, 50), check_enabled(adjust_roll_rate_right), check_and_reset(reset_roll_rate)), |
| 241 | + Button('Increase Thrust', (275, 150), (150, 50), check_enabled(increase_thrust), check_and_reset(reset_thrust)), |
| 242 | + Button('Decrease Thrust', (275, 290), (150, 50), check_enabled(decrease_thrust), check_and_reset(reset_thrust)), |
| 243 | + Button('Yaw Left', (200, 220), (100, 50), check_enabled(yaw_rate_left), check_and_reset(reset_yaw_rate)), |
| 244 | + Button('Yaw Right', (375, 220), (100, 50), check_enabled(yaw_rate_right), check_and_reset(reset_yaw_rate)) |
| 245 | +] |
| 246 | + |
| 247 | +def main(): |
| 248 | + """Main function to handle keyboard and mouse inputs for drone attitude rate control.""" |
| 249 | + global INCREMENTAL_MODE, roll_rate, pitch_rate, yaw_rate, thrust |
| 250 | + running = True |
| 251 | + clock = pygame.time.Clock() |
| 252 | + |
| 253 | + while running: |
| 254 | + screen.fill(BACKGROUND_COLOR) |
| 255 | + display_text("MAVSDK Offboard Control: Attitude Rate Control", (50, 20), font=FONT) |
| 256 | + display_text("Press 'E' to enable, 'C' to cancel, 'M' to toggle mode, 'H' to hold, 'Q' to quit", (50, 50), font=SMALL_FONT) |
| 257 | + mode_text = "Incremental" if INCREMENTAL_MODE else "Instant Reset" |
| 258 | + display_text(f"Mode: {mode_text}", (50, 80), font=SMALL_FONT) |
| 259 | + if enabled: |
| 260 | + display_text("Status: Enabled", (50, 100), font=SMALL_FONT, color=GREEN) |
| 261 | + else: |
| 262 | + display_text("Status: Disabled", (50, 100), font=SMALL_FONT, color=RED) |
| 263 | + display_text(f"Current Command: Roll Rate={roll_rate:.2f}, Pitch Rate={pitch_rate:.2f}, Yaw Rate={yaw_rate:.2f}, Thrust={thrust:.2f}", (50, 500), font=SMALL_FONT) |
| 264 | + display_text(f"IP: {UDP_IP}, Port: {UDP_PORT}, Rate: {SEND_RATE}s", (50, 550), font=SMALL_FONT) |
| 265 | + |
| 266 | + for event in pygame.event.get(): |
| 267 | + if event.type == pygame.QUIT: |
| 268 | + running = False |
| 269 | + elif event.type == pygame.KEYDOWN: |
| 270 | + if event.key == pygame.K_q: |
| 271 | + send_attitude_rate(0, 0, 0, 0) # Safety stop |
| 272 | + running = False |
| 273 | + elif event.key == pygame.K_e: |
| 274 | + enable_control() |
| 275 | + elif event.key == pygame.K_c: |
| 276 | + disable_control() |
| 277 | + elif event.key == pygame.K_m: |
| 278 | + toggle_mode() |
| 279 | + elif event.key == pygame.K_h: |
| 280 | + reset_control() |
| 281 | + |
| 282 | + if enabled: |
| 283 | + if event.key == pygame.K_w: |
| 284 | + adjust_pitch_rate_up() |
| 285 | + elif event.key == pygame.K_s: |
| 286 | + adjust_pitch_rate_down() |
| 287 | + elif event.key == pygame.K_a: |
| 288 | + adjust_roll_rate_left() |
| 289 | + elif event.key == pygame.K_d: |
| 290 | + adjust_roll_rate_right() |
| 291 | + elif event.key == pygame.K_UP: |
| 292 | + increase_thrust() |
| 293 | + elif event.key == pygame.K_DOWN: |
| 294 | + decrease_thrust() |
| 295 | + elif event.key == pygame.K_LEFT: |
| 296 | + yaw_rate_left() |
| 297 | + elif event.key == pygame.K_RIGHT: |
| 298 | + yaw_rate_right() |
| 299 | + |
| 300 | + elif event.type == pygame.KEYUP: |
| 301 | + if not INCREMENTAL_MODE: |
| 302 | + if event.key in [pygame.K_w, pygame.K_s]: |
| 303 | + reset_pitch_rate() |
| 304 | + elif event.key in [pygame.K_a, pygame.K_d]: |
| 305 | + reset_roll_rate() |
| 306 | + elif event.key in [pygame.K_UP, pygame.K_DOWN]: |
| 307 | + reset_thrust() |
| 308 | + elif event.key in [pygame.K_LEFT, pygame.K_RIGHT]: |
| 309 | + reset_yaw_rate() |
| 310 | + |
| 311 | + elif event.type == pygame.MOUSEBUTTONDOWN: |
| 312 | + mouse_pos = pygame.mouse.get_pos() |
| 313 | + for button in buttons: |
| 314 | + if button.is_clicked(mouse_pos): |
| 315 | + button.click() |
| 316 | + |
| 317 | + elif event.type == pygame.MOUSEBUTTONUP: |
| 318 | + for button in buttons: |
| 319 | + button.release() |
| 320 | + |
| 321 | + if enabled: |
| 322 | + send_attitude_rate(roll_rate, pitch_rate, yaw_rate, thrust) |
| 323 | + |
| 324 | + for button in buttons: |
| 325 | + button.draw(screen) |
| 326 | + |
| 327 | + pygame.display.flip() |
| 328 | + clock.tick(1 / SEND_RATE) |
| 329 | + |
| 330 | + sock.close() |
| 331 | + pygame.quit() |
| 332 | + |
| 333 | +if __name__ == "__main__": |
| 334 | + main() |
0 commit comments