Skip to content

Commit

Permalink
Merge pull request #177 from rosflight/vim_fly
Browse files Browse the repository at this point in the history
Vim Fly - A keyboard controller for the rosflight sim
  • Loading branch information
iandareid authored Aug 5, 2024
2 parents 49944da + 851536d commit 7acd3b3
Show file tree
Hide file tree
Showing 3 changed files with 226 additions and 3 deletions.
14 changes: 13 additions & 1 deletion rosflight_sim/launch/multirotor_sim_io_joy.launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,26 @@
"""

import os
import sys

from ament_index_python import get_package_share_directory
from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node


def generate_launch_description():
"""This is a launch file that runs the bare minimum requirements fly a multirotor in gazebo"""

use_vimfly = False

for arg in sys.argv:
if arg.startswith("use_vimfly:="):
use_vimfly = arg.split(":=")[1]
use_vimfly = use_vimfly.lower() == 'true'

# Start simulator
simulator_launch_include = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
Expand All @@ -44,6 +53,9 @@ def generate_launch_description():
executable='rc.py',
remappings=[
('/RC', '/multirotor/RC')
],
parameters=[
{'use_vimfly': use_vimfly}
]
)

Expand Down
17 changes: 15 additions & 2 deletions rosflight_sim/src/rc.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,16 @@

from enum import Enum

from numpy import true_divide
import pygame
import rclpy
from rclpy.node import Node
from std_srvs.srv import Trigger

from rosflight_msgs.msg import RCRaw

from vimfly import VimFly


# define output RC channels
class Channel(Enum):
Expand Down Expand Up @@ -141,8 +144,6 @@ class Channel(Enum):
class RC(Node):
def __init__(self):
super().__init__('rc')
self.rc_publisher = self.create_publisher(RCRaw, 'RC', 10)
self.timer = self.create_timer(1.0 / 50, self.timer_callback)

# Initialize pygame, checking if a joystick is connected
try:
Expand All @@ -153,6 +154,18 @@ def __init__(self):
except:
self.get_logger().info('No joystick (or display) detected, using simulated joystick')
self.transmitter_detected = False

self.declare_parameter('use_vimfly', False)
self.use_vimfly = self.get_parameter('use_vimfly').get_parameter_value().bool_value

# Intercept if using VimFly.
if self.use_vimfly and not self.transmitter_detected:
self.get_logger().info('Using VimFly...')
self.vim_fly = VimFly()
return

self.rc_publisher = self.create_publisher(RCRaw, 'RC', 10)
self.timer = self.create_timer(1.0 / 50, self.timer_callback)

# Transmitter detected, initialize joystick
if self.transmitter_detected:
Expand Down
198 changes: 198 additions & 0 deletions rosflight_sim/src/vimfly.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
#!/usr/bin/env python
"""
vimfly - vim keybindings for your multirotor!
Teleoperated flying from your keyboard.
Keys are mapped to each channel of the RCRaw message.
"""
import pygame
import rclpy
import time
from rosflight_msgs.msg import RCRaw


class VimFly:
def __init__(self):

self.node = rclpy.create_node('vimfly')

# initialize pygame display
pygame.init()
pygame.display.set_caption('vimfly')
self.screen = pygame.display.set_mode((550, 200))
self.font = pygame.font.SysFont("monospace", 18)

# create publisher for RC commands.
self.rc_pub = self.node.create_publisher(RCRaw, 'RC', 10)

# TODO: swap these hardcoded vals to params
self.rate = 30

# self.timer = self.node.create_timer(1/self.rate, self.run)

self.thrust_command = 1000
self.thrust_step = 10
self.THRUST_DEBOUNCE_THRESHOLD = 0.100
self.thrust_debouncing = [False]
self.thrust_start_time = self.node.get_clock().now()

self.armed = 1000 # Unarmed
self.ARM_DEBOUNCE_THRESHOLD = 0.500
self.arm_debouncing = [False]
self.arm_start_time = self.node.get_clock().now()

self.RC_override = 2000 # Start under manual control
self.RC_OVERRIDE_DEBOUNCE_THRESHOLD = 0.500
self.RC_override_debouncing = [False]
self.RC_override_start_time = self.node.get_clock().now()

while(True):
self.keys = pygame.key.get_pressed()
self.run()
time.sleep(1/self.rate)


def run(self):

# initialize command message
msg = RCRaw()
msg.header.stamp = self.node.get_clock().now().to_msg()

# LEFT -- h
if self.keys[pygame.K_h]:
msg.values[0] = 2000

# RIGHT -- l
elif self.keys[pygame.K_l]:
msg.values[0] = 1000

else:
msg.values[0] = 1500

# FORWARD -- k
if self.keys[pygame.K_k]:
msg.values[1] = 2000

# BACKWARD -- j
elif self.keys[pygame.K_j]:
msg.values[1] = 1000

else:
msg.values[1] = 1500

# CCW -- d
if self.keys[pygame.K_d]:
msg.values[3] = 2000

# CW -- f
elif self.keys[pygame.K_f]:
msg.values[3] = 1000

else:
msg.values[3] = 1500

# THRUST LOWER -- s // THRUST HIGHER -- a
if self.keys[pygame.K_a] or self.keys[pygame.K_s]:
self.debounce(self.thrust_debouncing, self.thrust_start_time, self.THRUST_DEBOUNCE_THRESHOLD, self.increment_thrust)
else:
self.thrust_debouncing[0] = False

# Always send an thrust command -- we don't want to drop like a brick!
msg.values[2] = self.thrust_command

# Send arm commands
if self.keys[pygame.K_t]:
self.debounce(self.arm_debouncing, self.arm_start_time, self.ARM_DEBOUNCE_THRESHOLD, self.toggle_arm)
else:
self.arm_debouncing[0] = False

msg.values[4] = self.armed

# Send RC override commands
if self.keys[pygame.K_r]:
self.debounce(self.RC_override_debouncing, self.RC_override_start_time, self.RC_OVERRIDE_DEBOUNCE_THRESHOLD, self.toggle_RC_override)
else:
self.RC_override_debouncing[0] = False

msg.values[5] = self.RC_override

# Pad the message with remaining channels.
msg.values[6] = 1500
msg.values[7] = 1500

# Publish commands
self.rc_pub.publish(msg)

# Update the display with the current commands
self.update_display(msg)

# process event queue and throttle the while loop
pygame.event.pump()


def update_display(self, msg):
self.display_help()

msgText = "roll: {}, pitch: {}, yaw: {}, thrust: {}".format(msg.values[0], msg.values[1], msg.values[3], msg.values[2])

status_info = "armed: {}, RC_override: {}".format(msg.values[4], msg.values[5])
self.render(msgText, (0,140))
self.render(status_info, (100,160))

pygame.display.flip()


def display_help(self):
self.screen.fill((0,0,0))

LINE=20

self.render("vimfly keybindings:", (0,0))
self.render("- a: higher thrust", (0,1*LINE)); self.render("- h: Roll Left", (250,1*LINE))
self.render("- s: lower thrust", (0,2*LINE)); self.render("- j: Pitch Backward", (250,2*LINE))
self.render("- d: yaw CCW", (0,3*LINE)); self.render("- k: Pitch Forward", (250,3*LINE))
self.render("- f: yaw CW", (0,4*LINE)); self.render("- l: Roll Right", (250,4*LINE))
self.render("- t: arm/disarm", (0,5*LINE)); self.render("- r: RC override", (250,5*LINE))


def render(self, text, loc):
txt = self.font.render(text, 1, (255,255,255))
self.screen.blit(txt, loc)

def debounce(self, debounced, debounce_start_time, threshold, key_action):

if not debounced[0]:
debounced[0] = True
debounce_start_time = self.node.get_clock().now()

if (self.node.get_clock().now() - debounce_start_time).nanoseconds / 1e9 > threshold:

# The key has been debounced once, start the process over!
debounced[0] = False

key_action()

def increment_thrust(self):
# Increment the commanded altitude
if self.keys[pygame.K_a] and self.thrust_command < 2000:
self.thrust_command += self.thrust_step

elif self.keys[pygame.K_s] and self.thrust_command > 1000:
self.thrust_command -= self.thrust_step

def toggle_arm(self):
if self.armed == 1000:
self.armed = 2000
else:
self.armed = 1000

def toggle_RC_override(self):
if self.RC_override == 1000:
self.RC_override = 2000
else:
self.RC_override = 1000

if __name__ == '__main__':
rclpy.init()
teleop = VimFly()

0 comments on commit 7acd3b3

Please sign in to comment.