-
-
Notifications
You must be signed in to change notification settings - Fork 613
ROS2 turtlesim joystick
In this example, we'll create an application that uses the NiceGUI joystick to control the turtlesim.
The source code of this example can be found here: turtlesim joystick
Before getting started, make sure you have the following installed:
- ROS2: ROS2 Humble Installation Guide
- Python 3.8 or higher
- NiceGUI: Install NiceGUI using
pip install nicegui
The example consists of a simple ROS2 node that sends the virtual joystick input as a twist message to the turtlesim node.
import rclpy
from rclpy.node import Node
from rclpy.executors import ExternalShutdownException
from geometry_msgs.msg import Twist
from nicegui import app, Client, ui
import threading
from pathlib import Path
We import the necessary libraries, including ROS2, NiceGUI for the user interface, geometry_msgs.msg twist for the message and other required modules.
class SimpleJoystick(Node):
def __init__(self) -> None:
super().__init__('joystick_node')
self.publisher_ = self.create_publisher(Twist, 'turtle1/cmd_vel', 1)
We create a ROS2 node called SimpleJoystick that publishes the twist message on the topic turtle1/cmd_vel
.
We use NiceGUI to create a graphical user interface. The interface includes two ui.card()
elements.
#this is where we add nicegui elements
with globals.index_client:
#create a row
with ui.row().classes('items-stretch'):
#create a card with the joystick in it
with ui.card().classes('w-80 text-center items-center'):
# NOTE: Joystick will be reworked in the future, so this is a temporary workaround for the size.
ui.add_head_html('<style>.my-joystick > div { width: 20em !important; height: 20em !important; }</style>')
ui.joystick(
color='blue',
size=50,
#Note: x has to be inverted. Negative values turn the turtle to the right.
on_move=lambda e: self.publish_speeds(-e.x, e.y),
on_end=lambda _: self.publish_speeds(0.0, 0.0),
).classes('my-joystick')
ui.label('Publish steering commands by dragging your mouse around in the blue field').classes('mt-6')
In the first card, we define the joystick to control the turtlesim. We also add a simple label with instructions.
with ui.card().classes('w-44 text-center items-center'):
ui.label('Speeds').classes('text-2xl')
slider_props = 'readonly selection-color=transparent'
#create a slider & labelfor the linear speed
ui.label('linear velocity').classes('text-xs mb-[-1.8em]')
self.linear = ui.slider(min=-1, max=1, step=0.05, value=0).props(slider_props)
ui.label().bind_text_from(self.linear,'value', backward=lambda value: f'{value:.3f}')
#create a slider & label for the angular speed
ui.label('angular velocity').classes('text-xs mb-[-1.8em]')
self.angular = ui.slider(min=-1, max=1, step=0.05, value=0).props(slider_props)
ui.label().bind_text_from(self.angular,'value', backward=lambda value: f'{value:.3f}')
The second card displays the velocities, with an ui.slider()
and an ui.label()
element for each velocity.
#this function publishes the twist message
def publish_speeds(self, x:float, y:float) -> None:
msg = Twist()
self.get_logger().info('Publishing1-> linear: "%f", angular: "%f"' % (y, x))
#this is for the sliders
self.linear.value = y
self.angular.value = x
#loading values into the message
msg.linear.x = y
msg.angular.z = x
self.publisher_.publish(msg)
Before publishing the twist message, the joystick's values get saved in the value of the sliders.
def ros_main() -> None:
#Standart ROS2 node initialization
print('Starting ROS2...', flush=True)
rclpy.init()
simple_joystick = SimpleJoystick()
try:
rclpy.spin(simple_joystick)
except ExternalShutdownException:
pass
The ROS2 node itself gets controlled by this function. This is basically the contents of your main() function in a normal ROS2 node.
app.on_startup(lambda: threading.Thread(target=ros_main).start())
# We add reload dirs to watch changes in our package
ui.run(title='Turtlesim Joystick',uvicorn_reload_dirs=str(Path(__file__).parent.resolve()))
The starting of the ROS2 node is handled by NiceGUI. The Node will be started with the app.on_startup()
function. The node will be started in its own thread as well, since running NiceGUI and the ROS2 node in the same thread causes one to be blocked by the other.
The second reason we do this is, that the ROS2 node gets restarted every time NiceGUI reloads itself.
The usage is quite simple, just run these commands in two terminals. Afterward, a browser window should have opened with the GUI running on localhost:8080.
Note: This node is not continuously sending. It will send a new twist command on change of the joystick.
Terminal1 :
python3 joystick.py
In this version of this tutorial, we start the script directly with python. In ROS2, you can run nodes directly with python3 because the ROS2 initialization uses standard Python APIs, but you must source your ROS2 distribution beforehand to ensure the environment is correctly set.
Terminal2 :
ros2 run turtlesim turtlesim_node
If you want to start the code with ros2 run
inside of a package, you have to add the following to the code (example code). This is a workaround to let ros2 start the node, but let NiceGUI run it.
You might want to deactivate reload since there is a bug with uvicorn, that will watch more then just your set folder for reloads. The same goes for reload exclude, which will be ignored.
def main():
pass # NOTE: This is originally used as the ROS entry point, but we give the control of the node to NiceGUI.
ui_run.APP_IMPORT_STRING = f'{__name__}:app'