Controlling the Drone with a Joystick
In my previous project, I demonstrated drone navigation using keyboard inputs. In this follow-up, I showcase how to control a DJI Tello drone using a joystick. This project leverages the djitellopy
library for drone communication, pygame
for joystick input, and opencv
for video streaming. The code is organized into two main components: a main script and a custom joystick handler class.
Code Explanation
main.py
The main script handles core operations: connecting to the drone, managing joystick input, and displaying the camera feed.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import cv2
import pygame
from djitellopy import Tello
from joystickhandler import JoystickHandler
def main():
# Initialize the drone and joystick handler
uav = Tello()
uav.connect()
print(f"Battery: {uav.get_battery()}%")
uav.streamon()
joystick_handler = JoystickHandler()
img = None
while True:
# Check for Pygame events
for event in pygame.event.get():
if event.type == pygame.QUIT:
uav.land()
pygame.quit()
return
# Retrieve joystick input and update drone controls
vals = joystick_handler.get_joystick_input()
uav.send_rc_control(vals[0], vals[1], vals[2], vals[3])
joystick_handler.handle_joystick_buttons(uav)
# Capture and display the camera feed
img = uav.get_frame_read().frame
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = cv2.resize(img, (360, 240))
cv2.imshow("Image", img)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cv2.destroyAllWindows()
if __name__ == '__main__':
main()
- Initialization: Connects to the Tello drone and sets up the joystick handler.
- Main Loop: Continuously checks for Pygame events, updates drone controls based on joystick input, captures the camera feed, and displays it.
- Exiting: Handles the quit event by landing the drone, quitting Pygame, and closing OpenCV windows.
joystickhandler.py
This file defines the JoystickHandler
class, encapsulating joystick functionality and managing both joystick axis inputs and button events.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import pygame
import time
import cv2
class JoystickHandler:
def __init__(self):
pygame.init()
pygame.joystick.init()
self._initialize_joystick()
def _initialize_joystick(self):
"""Initializes the joystick and raises an exception if none is detected."""
if pygame.joystick.get_count() > 0:
self._joystick = pygame.joystick.Joystick(0)
self._joystick.init()
else:
raise RuntimeError("No joystick detected")
def get_joystick_input(self):
"""Retrieves joystick axis values and scales them for drone control."""
speed = 50 # Maximum speed value for the drone
lr = int(self._joystick.get_axis(2) * speed) # Axis 2 for left-right
fb = -int(self._joystick.get_axis(3) * speed) # Axis 3 for forward-backward
ud = -int(self._joystick.get_axis(1) * speed) # Axis 1 for up-down
yv = int(self._joystick.get_axis(0) * speed) # Axis 0 for yaw
return [lr, fb, ud, yv]
def handle_joystick_buttons(self, uav):
"""Handles joystick button presses to perform actions on the UAV."""
if self._joystick.get_button(3): # Button 3 for takeoff
uav.takeoff()
elif self._joystick.get_button(0): # Button 0 for land
uav.land()
elif self._joystick.get_button(2): # Button 2 for capturing an image
self._capture_image(uav)
@staticmethod
def _capture_image(uav):
"""Captures an image from the UAV's camera and saves it."""
img = uav.get_frame_read().frame
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
cv2.imwrite(f'Resources/Images/{time.time()}.jpg', img)
time.sleep(0.3)
- Initialization: Initializes the joystick and raises an exception if no joystick is detected.
- Joystick Input: The
get_joystick_input
method retrieves and scales joystick axis values for drone control. - Button Handling: The
handle_joystick_buttons
method performs actions based on button presses, such as takeoff, landing, or capturing an image.
OOP Principles Followed: Encapsulation and Abstraction
I find that organizing and clearly expressing information helps me understand it better. Therefore, I want to share an explanation of the custom JoystickHandler class in terms of Object-Oriented Programming (OOP) principles.
Python’s Philosophy: Python follows the principle of “we are all consenting adults here,” relying on conventions rather than enforced access control.
Encapsulation: Encapsulation involves bundling the data (attributes) and methods (functions) that operate on the data into a single unit or class. In the JoystickHandler
class:
- Data Hiding: Attributes and methods prefixed with an underscore (e.g.,
_joystick
,_initialize_joystick
) are intended to be private to the class. This convention signals that these are internal details not meant to be accessed directly from outside the class. This helps protect the internal state and implementation details, ensuring they are not inadvertently modified by external code. - Controlled Access: Public methods like
get_joystick_input
andhandle_joystick_buttons
provide controlled access to the joystick’s functionality, making the code easier to maintain and reducing the risk of errors.
Abstraction: Abstraction hides complex implementation details and exposes only the necessary features of an object:
- Simplified Interface: The
JoystickHandler
class abstracts the complexity of joystick interaction. Users interact with the joystick through simplified methods without needing to understand the underlying details. - Modularity: By isolating joystick functionality into its own class, the code adheres to modularity. Changes to joystick handling will not affect other parts of the code, enhancing maintainability and readability.