diff --git a/CHANGELOG.md b/CHANGELOG.md index 9583ed44..79d65801 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ * Add node to convert a twist to a vehicle control command * Add node carla_spectator_camera * Update carla_waypoint_publisher +* Add carla_ros_scenario_runner +* Add rviz_carla_plugin +* Add carla_ad_agent +* Add carla_ad_demo ## CARLA-ROS-Bridge 0.9.6 diff --git a/README.md b/README.md index a075a57b..5f2dba4c 100644 --- a/README.md +++ b/README.md @@ -48,24 +48,18 @@ For more information about configuring a ROS environment see First run the simulator (see carla documentation: ) - ./CarlaUE4.sh -windowed -ResX=320 -ResY=240 + # run carla in background + SDL_VIDEODRIVER=offscreen ./CarlaUE4.sh -opengl -Wait for the message: +Wait a few seconds, then start the ros bridge (choose one option): - Waiting for the client to connect... - -Then start the ros bridge (choose one option): - - export PYTHONPATH=$PYTHONPATH:/PythonAPI/ + export PYTHONPATH=$PYTHONPATH:/PythonAPI/carla/dist/carla-.egg source ~/carla-ros-bridge/catkin_ws/devel/setup.bash # Option 1: start the ros bridge roslaunch carla_ros_bridge carla_ros_bridge.launch - # Option 2: start the ros bridge together with RVIZ - roslaunch carla_ros_bridge carla_ros_bridge_with_rviz.launch - - # Option 3: start the ros bridge together with an example ego vehicle + # Option 2: start the ros bridge together with an example ego vehicle roslaunch carla_ros_bridge carla_ros_bridge_with_example_ego_vehicle.launch ## Settings @@ -267,17 +261,19 @@ The following markers are supported in 'map'-frame: | ---------------------------------- | ------------------------------------------------------------------------------------------------------ | --------------------------- | | `/carla/debug_marker` (subscriber) | [visualization_msgs.MarkerArray](http://docs.ros.org/api/visualization_msgs/html/msg/MarkerArray.html) | draw markers in CARLA world | -## Carla Ego Vehicle - -`carla_ego_vehicle` provides a generic way to spawn an ego vehicle and attach sensors to it. You can find further documentation [here](carla_ego_vehicle/README.md). - -## Carla Infrastructure Sensors - -`carla_infrastructure` provides a generic way to spawn a set of infrastructure sensors defined in a config file. You can find further documentation [here](carla_infrastructure/README.md). -## Waypoint calculation +## Additional Functionality -To make use of the Carla waypoint calculation a ROS Node is available to get waypoints. You can find further documentation [here](carla_waypoint_publisher/README.md). +| Name | Description | +| --------------------------------- | ------------------------------------------------------------------------------------------------------- | +| [Carla Ego Vehicle](carla_ego_vehicle/README.md) | Provides a generic way to spawn an ego vehicle and attach sensors to it. | +| [Carla Infrastructure](carla_infrastructure/README.md) | Provides a generic way to spawn a set of infrastructure sensors defined in a config file. | +| [Carla Waypoint Publisher](carla_waypoint_publisher/README.md) | Provide routes and access to the Carla waypoint API | +| [Carla ROS Scenario Runner](carla_ros_scenario_runner/README.md) | ROS node that wraps the functionality of the CARLA [scenario runner](https://github.com/carla-simulator/scenario_runner) to execute scenarios. | +| [Carla AD Agent](carla_ad_agent/README.md) | A basic AD agent, that can follow a route and avoid collisions with other vehicles and stop on red traffic lights. | +| [Carla AD Demo](carla_ad_demo/README.md) | A meta package that provides everything to launch a CARLA ROS environment with an AD vehicle. | +| [RVIZ Carla Plugin](rviz_carla_plugin/README.md) | A [RVIZ](http://wiki.ros.org/rviz) plugin to visualize/control CARLA. | +| [RQT Carla Plugin](rqt_carla_plugin/README.md) | A [RQT](http://wiki.ros.org/rqt) plugin to control CARLA. | ## Troubleshooting @@ -285,7 +281,7 @@ To make use of the Carla waypoint calculation a ROS Node is available to get way You're missing Carla Python. Please execute: - export PYTHONPATH=$PYTHONPATH:/PythonAPI/ + export PYTHONPATH=$PYTHONPATH:/PythonAPI/carla/dist/ Please note that you have to put in the complete path to the egg-file including the egg-file itself. Please use the one, that is supported by your Python version. diff --git a/carla_ad_agent/CMakeLists.txt b/carla_ad_agent/CMakeLists.txt new file mode 100644 index 00000000..8f5a7216 --- /dev/null +++ b/carla_ad_agent/CMakeLists.txt @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 2.8.3) +project(carla_ad_agent) + +find_package(catkin REQUIRED COMPONENTS + rospy + roslaunch +) + +catkin_python_setup() + +roslaunch_add_file_check(launch) + +catkin_package( + CATKIN_DEPENDS + rospy +) + +catkin_install_python(PROGRAMS + src/carla_ad_agent/carla_ad_agent.py + src/carla_ad_agent/basic_agent.py + src/carla_ad_agent/agent.py + src/carla_ad_agent/local_planner.py + src/carla_ad_agent/vehicle_pid_controller.py + src/carla_ad_agent/misc.py + src/carla_ad_agent/__init__.py + DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} +) + +install(DIRECTORY launch/ + DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}/launch +) + diff --git a/carla_ad_agent/README.md b/carla_ad_agent/README.md new file mode 100644 index 00000000..63ee8ffd --- /dev/null +++ b/carla_ad_agent/README.md @@ -0,0 +1,13 @@ +# CARLA AD Agent + +An AD agent that can follow a given route. + +It avoids crashs with other vehicles and respects the state of the traffic lights. + +For a more comprehensive solution, have a look at [Autoware](https://www.autoware.ai/). + +## Publications + +| Topic | Type | Description | +| ---------------------------------- | ------------------- | --------------------------- | +| `/carla//vehicle_control_cmd` | [carla_msgs.CarlaEgoVehicleControl](../carla_msgs/msg/CarlaEgoVehicleControl.msg) | Vehicle control command | diff --git a/carla_ad_agent/launch/carla_ad_agent.launch b/carla_ad_agent/launch/carla_ad_agent.launch new file mode 100644 index 00000000..209de6b8 --- /dev/null +++ b/carla_ad_agent/launch/carla_ad_agent.launch @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/carla_ad_agent/package.xml b/carla_ad_agent/package.xml new file mode 100644 index 00000000..d2b3d2a5 --- /dev/null +++ b/carla_ad_agent/package.xml @@ -0,0 +1,18 @@ + + + carla_ad_agent + 0.0.1 + The carla_ad_agent package + CARLA Simulator Team + MIT + catkin + rospy + roslaunch + rospy + rospy + topic_tools + carla_waypoint_types + carla_msgs + + + diff --git a/carla_ad_agent/setup.py b/carla_ad_agent/setup.py new file mode 100644 index 00000000..0af8e3a7 --- /dev/null +++ b/carla_ad_agent/setup.py @@ -0,0 +1,10 @@ +from distutils.core import setup +from catkin_pkg.python_setup import generate_distutils_setup + +d = generate_distutils_setup( + packages=['carla_ad_agent'], + package_dir={'': 'src'} +) + +setup(**d) + diff --git a/carla_ad_agent/src/carla_ad_agent/__init__.py b/carla_ad_agent/src/carla_ad_agent/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/carla_ad_agent/src/carla_ad_agent/agent.py b/carla_ad_agent/src/carla_ad_agent/agent.py new file mode 100644 index 00000000..c1cc8c55 --- /dev/null +++ b/carla_ad_agent/src/carla_ad_agent/agent.py @@ -0,0 +1,290 @@ +#!/usr/bin/env python +# +# Copyright (c) 2018-2020 Intel Corporation +# +# This work is licensed under the terms of the MIT license. +# For a copy, see . +# +""" +Base class for agent +""" + +from enum import Enum +import math +import rospy +from tf.transformations import euler_from_quaternion +from misc import is_within_distance_ahead, compute_magnitude_angle +from carla_msgs.msg import CarlaEgoVehicleControl, CarlaTrafficLightStatus,\ + CarlaTrafficLightStatusList, CarlaWorldInfo +from carla_waypoint_types.srv import GetWaypoint + + +class AgentState(Enum): + """ + AGENT_STATE represents the possible states of a roaming agent + """ + NAVIGATING = 1 + BLOCKED_BY_VEHICLE = 2 + BLOCKED_RED_LIGHT = 3 + + +class Agent(object): + """ + Base class for agent + """ + + def __init__(self, role_name, vehicle_id): + """ + """ + self._proximity_threshold = 10.0 # meters + self._local_planner = None + self._map_name = None + self._vehicle_location = None + self._vehicle_yaw = None + self._vehicle_id = vehicle_id + self._last_traffic_light = None + + rospy.wait_for_service('/carla_waypoint_publisher/{}/get_waypoint'.format(role_name)) + + self._get_waypoint_client = rospy.ServiceProxy( + '/carla_waypoint_publisher/{}/get_waypoint'.format(role_name), GetWaypoint) + + self._traffic_lights = [] + self._traffic_light_status_subscriber = rospy.Subscriber( + "/carla/traffic_lights", CarlaTrafficLightStatusList, self.traffic_lights_updated) + + self._world_info_subscriber = rospy.Subscriber( + "/carla/world_info", CarlaWorldInfo, self.world_info_updated) + + def traffic_lights_updated(self, traffic_lights): + """ + callback on new traffic light list + """ + self._traffic_lights = traffic_lights.traffic_lights + + def world_info_updated(self, world_info): + """ + callback on new world info + """ + self._map_name = world_info.map_name + + def odometry_updated(self, odo): + """ + callback on new odometry + """ + self._vehicle_location = odo.pose.pose.position + quaternion = ( + odo.pose.pose.orientation.x, + odo.pose.pose.orientation.y, + odo.pose.pose.orientation.z, + odo.pose.pose.orientation.w + ) + _, _, self._vehicle_yaw = euler_from_quaternion(quaternion) + + def run_step(self, target_speed): # pylint: disable=no-self-use,unused-argument + """ + Execute one step of navigation. + :return: control + """ + control = CarlaEgoVehicleControl() + return control + + def _is_light_red(self, lights_list): + """ + Method to check if there is a red light affecting us. This version of + the method is compatible with both European and US style traffic lights. + + :param lights_list: list containing TrafficLight objects + :return: a tuple given by (bool_flag, traffic_light), where + - bool_flag is True if there is a traffic light in RED + affecting us and False otherwise + - traffic_light is the object itself or None if there is no + red traffic light affecting us + """ + if self._map_name == 'Town01' or self._map_name == 'Town02': + return self._is_light_red_europe_style(lights_list) + else: + return self._is_light_red_us_style(lights_list) + + def _is_light_red_europe_style(self, lights_list): + """ + This method is specialized to check European style traffic lights. + + :param lights_list: list containing TrafficLight objects + :return: a tuple given by (bool_flag, traffic_light), where + - bool_flag is True if there is a traffic light in RED + affecting us and False otherwise + - traffic_light is the object itself or None if there is no + red traffic light affecting us + """ + ego_vehicle_location = self._vehicle_location + ego_vehicle_waypoint = self.get_waypoint(ego_vehicle_location) + if not ego_vehicle_waypoint: + if not rospy.is_shutdown(): + rospy.logwarn("Could not get waypoint for ego vehicle.") + return (False, None) + + for traffic_light in lights_list: + object_waypoint = traffic_light[1] + if object_waypoint.road_id != ego_vehicle_waypoint.road_id or \ + object_waypoint.lane_id != ego_vehicle_waypoint.lane_id: + continue + if is_within_distance_ahead(object_waypoint.pose.position, ego_vehicle_location, + math.degrees(self._vehicle_yaw), + self._proximity_threshold): + traffic_light_state = CarlaTrafficLightStatus.RED + for status in self._traffic_lights: + if status.id == traffic_light[0]: + traffic_light_state = status.state + break + if traffic_light_state == CarlaTrafficLightStatus.RED: + return (True, traffic_light[0]) + + return (False, None) + + def get_waypoint(self, location): + """ + Helper to get waypoint for location via ros service + """ + if rospy.is_shutdown(): + return None + try: + response = self._get_waypoint_client(location) + return response.waypoint + except (rospy.ServiceException, rospy.ROSInterruptException) as e: + if not rospy.is_shutdown(): + rospy.logwarn("Service call 'get_waypoint' failed: {}".format(e)) + + def _is_light_red_us_style(self, lights_list): # pylint: disable=too-many-branches + """ + This method is specialized to check US style traffic lights. + + :param lights_list: list containing TrafficLight objects + :return: a tuple given by (bool_flag, traffic_light), where + - bool_flag is True if there is a traffic light in RED + affecting us and False otherwise + - traffic_light is the object itself or None if there is no + red traffic light affecting us + """ + ego_vehicle_location = self._vehicle_location + ego_vehicle_waypoint = self.get_waypoint(ego_vehicle_location) + if not ego_vehicle_waypoint: + if not rospy.is_shutdown(): + rospy.logwarn("Could not get waypoint for ego vehicle.") + return (False, None) + + if ego_vehicle_waypoint.is_junction: + # It is too late. Do not block the intersection! Keep going! + return (False, None) + + if self._local_planner.target_waypoint is not None: + if self._local_planner.target_waypoint.is_junction: + min_angle = 180.0 + sel_magnitude = 0.0 # pylint: disable=unused-variable + sel_traffic_light = None + for traffic_light in lights_list: + loc = traffic_light[1] + magnitude, angle = compute_magnitude_angle(loc.pose.position, + ego_vehicle_location, + math.degrees(self._vehicle_yaw)) + if magnitude < 60.0 and angle < min(25.0, min_angle): + sel_magnitude = magnitude + sel_traffic_light = traffic_light[0] + min_angle = angle + + if sel_traffic_light is not None: + # print('=== Magnitude = {} | Angle = {} | ID = {}'.format( + # sel_magnitude, min_angle, sel_traffic_light)) + + if self._last_traffic_light is None: + self._last_traffic_light = sel_traffic_light + + state = None + for status in self._traffic_lights: + if status.id == sel_traffic_light: + state = status.state + break + if state is None: + rospy.logwarn( + "Couldn't get state of traffic light {}".format( + sel_traffic_light)) + return (False, None) + + if state == CarlaTrafficLightStatus.RED: + return (True, self._last_traffic_light) + else: + self._last_traffic_light = None + + return (False, None) + + def _is_vehicle_hazard(self, vehicle_list, objects): + """ + Check if a given vehicle is an obstacle in our way. To this end we take + into account the road and lane the target vehicle is on and run a + geometry test to check if the target vehicle is under a certain distance + in front of our ego vehicle. + + WARNING: This method is an approximation that could fail for very large + vehicles, which center is actually on a different lane but their + extension falls within the ego vehicle lane. + + :param vehicle_list: list of potential obstacle to check + :return: a tuple given by (bool_flag, vehicle), where + - bool_flag is True if there is a vehicle ahead blocking us + and False otherwise + - vehicle is the blocker object itself + """ + + ego_vehicle_location = self._vehicle_location + + ego_vehicle_waypoint = self.get_waypoint(ego_vehicle_location) + if not ego_vehicle_waypoint: + if not rospy.is_shutdown(): + rospy.logwarn("Could not get waypoint for ego vehicle.") + return (False, None) + + for target_vehicle_id in vehicle_list: + # do not account for the ego vehicle + if target_vehicle_id == self._vehicle_id: + continue + + target_vehicle_location = None + for elem in objects: + if elem.id == target_vehicle_id: + target_vehicle_location = elem.pose + break + + if not target_vehicle_location: + rospy.logwarn("Location of vehicle {} not found".format(target_vehicle_id)) + continue + + # if the object is not in our lane it's not an obstacle + target_vehicle_waypoint = self.get_waypoint(target_vehicle_location.position) + if not target_vehicle_waypoint: + if not rospy.is_shutdown(): + rospy.logwarn("Could not get waypoint for target vehicle.") + return (False, None) + if target_vehicle_waypoint.road_id != ego_vehicle_waypoint.road_id or \ + target_vehicle_waypoint.lane_id != ego_vehicle_waypoint.lane_id: + #rospy.loginfo("Vehicle {} is not in our lane".format(target_vehicle_id)) + continue + + if is_within_distance_ahead(target_vehicle_location.position, self._vehicle_location, + math.degrees(self._vehicle_yaw), + self._proximity_threshold): + return (True, target_vehicle_id) + + return (False, None) + + def emergency_stop(self): # pylint: disable=no-self-use + """ + Send an emergency stop command to the vehicle + :return: + """ + control = CarlaEgoVehicleControl() + control.steer = 0.0 + control.throttle = 0.0 + control.brake = 1.0 + control.hand_brake = False + + return control diff --git a/carla_ad_agent/src/carla_ad_agent/basic_agent.py b/carla_ad_agent/src/carla_ad_agent/basic_agent.py new file mode 100644 index 00000000..91ac8ee1 --- /dev/null +++ b/carla_ad_agent/src/carla_ad_agent/basic_agent.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python +# +# Copyright (c) 2018-2020 Intel Corporation +# +# This work is licensed under the terms of the MIT license. +# For a copy, see . +# +""" +BasicAgent implements a basic agent that navigates scenes to reach a given +target destination. This agent respects traffic lights and other vehicles. +""" + +import rospy +from nav_msgs.msg import Odometry +from derived_object_msgs.msg import ObjectArray +from carla_msgs.msg import CarlaActorList +from agent import Agent, AgentState +from local_planner import LocalPlanner +from carla_waypoint_types.srv import GetActorWaypoint + + +class BasicAgent(Agent): + """ + BasicAgent implements a basic agent that navigates scenes to reach a given + target destination. This agent respects traffic lights and other vehicles. + """ + + def __init__(self, role_name, ego_vehicle_id, avoid_risk=True): + """ + """ + super(BasicAgent, self).__init__(role_name, ego_vehicle_id) + + self._proximity_threshold = 10.0 # meters + self._state = AgentState.NAVIGATING + self._avoid_risk = avoid_risk + args_lateral_dict = { + 'K_P': 0.9, + 'K_D': 0.0, + 'K_I': 0.1} + self._local_planner = LocalPlanner(role_name, + opt_dict={'lateral_control_dict': args_lateral_dict}) + + self._vehicle_id_list = [] + self._lights_id_list = [] + self._actors_subscriber = rospy.Subscriber( + "/carla/actor_list", CarlaActorList, self.actors_updated) + self._objects = [] + self._objects_subscriber = rospy.Subscriber( + "/carla/{}/objects".format(role_name), ObjectArray, self.objects_updated) + + self._odometry_subscriber = rospy.Subscriber( + "/carla/{}/odometry".format(role_name), Odometry, self.odometry_updated) + + self._get_actor_waypoint_client = rospy.ServiceProxy( + '/carla_waypoint_publisher/{}/get_actor_waypoint'.format(role_name), GetActorWaypoint) + + def get_actor_waypoint(self, actor_id): + """ + helper method to get waypoint for actor via ros service + """ + try: + response = self._get_actor_waypoint_client(actor_id) + return response.waypoint + except (rospy.ServiceException, rospy.ROSInterruptException) as e: + if not rospy.is_shutdown: + rospy.logwarn("Service call failed: {}".format(e)) + + def odometry_updated(self, odo): + """ + callback on new odometry + """ + self._local_planner.odometry_updated(odo) + super(BasicAgent, self).odometry_updated(odo) + + def actors_updated(self, actors): + """ + callback on new actor list + """ + # retrieve relevant elements for safe navigation, i.e.: traffic lights + # and other vehicles + self._vehicle_id_list = [] + self._lights_id_list = [] + for actor in actors.actors: + if "vehicle" in actor.type: + self._vehicle_id_list.append(actor.id) + elif "traffic_light" in actor.type: + self._lights_id_list.append((actor.id, self.get_actor_waypoint(actor.id))) + + def objects_updated(self, objects): + """ + callback on new objects + """ + self._objects = objects.objects + + def run_step(self, target_speed): + """ + Execute one step of navigation. + :return: carla.VehicleControl + """ + + # is there an obstacle in front of us? + hazard_detected = False + + if self._avoid_risk: + # check possible obstacles + vehicle_state, vehicle = self._is_vehicle_hazard( # pylint: disable=unused-variable + self._vehicle_id_list, self._objects) + if vehicle_state: + #rospy.loginfo('=== Vehicle blocking ahead [{}])'.format(vehicle)) + self._state = AgentState.BLOCKED_BY_VEHICLE + hazard_detected = True + + # check for the state of the traffic lights + light_state, traffic_light = self._is_light_red( # pylint: disable=unused-variable + self._lights_id_list) + if light_state: + #rospy.loginfo('=== Red Light ahead [{}])'.format(traffic_light)) + + self._state = AgentState.BLOCKED_RED_LIGHT + hazard_detected = True + + if hazard_detected: + control = self.emergency_stop() + else: + self._state = AgentState.NAVIGATING + # standard local planner behavior + control = self._local_planner.run_step(target_speed) + + return control diff --git a/carla_ad_agent/src/carla_ad_agent/carla_ad_agent.py b/carla_ad_agent/src/carla_ad_agent/carla_ad_agent.py new file mode 100755 index 00000000..9c3abff8 --- /dev/null +++ b/carla_ad_agent/src/carla_ad_agent/carla_ad_agent.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python +# +# Copyright (c) 2020 Intel Corporation +# +# This work is licensed under the terms of the MIT license. +# For a copy, see . +# +""" +A basic AD agent using CARLA waypoints +""" +import rospy +from nav_msgs.msg import Path +from std_msgs.msg import Float64 +from carla_msgs.msg import CarlaEgoVehicleInfo, CarlaEgoVehicleControl +from basic_agent import BasicAgent + + +class CarlaAdAgent(object): + """ + A basic AD agent using CARLA waypoints + """ + + def __init__(self, role_name, target_speed, avoid_risk): + """ + Constructor + """ + self._route_assigned = False + self._global_plan = None + self._agent = None + self._target_speed = target_speed + rospy.on_shutdown(self.on_shutdown) + + # wait for ego vehicle + vehicle_info = rospy.wait_for_message( + "/carla/{}/vehicle_info".format(role_name), CarlaEgoVehicleInfo) + + self._route_subscriber = rospy.Subscriber( + "/carla/{}/waypoints".format(role_name), Path, self.path_updated) + + self._target_speed_subscriber = rospy.Subscriber( + "/carla/{}/target_speed".format(role_name), Float64, self.target_speed_updated) + + self.vehicle_control_publisher = rospy.Publisher( + "/carla/{}/vehicle_control_cmd".format(role_name), CarlaEgoVehicleControl, queue_size=1) + + self._agent = BasicAgent(role_name, vehicle_info.id, # pylint: disable=no-member + avoid_risk) + + def on_shutdown(self): + """ + callback on shutdown + """ + rospy.loginfo("Shutting down, stopping ego vehicle...") + if self._agent: + self.vehicle_control_publisher.publish(self._agent.emergency_stop()) + + def target_speed_updated(self, target_speed): + """ + callback on new target speed + """ + rospy.loginfo("New target speed received: {}".format(target_speed.data)) + self._target_speed = target_speed.data + + def path_updated(self, path): + """ + callback on new route + """ + rospy.loginfo("New plan with {} waypoints received.".format(len(path.poses))) + if self._agent: + self.vehicle_control_publisher.publish(self._agent.emergency_stop()) + self._global_plan = path + self._route_assigned = False + + def run_step(self): + """ + Execute one step of navigation. + """ + control = CarlaEgoVehicleControl() + control.steer = 0.0 + control.throttle = 0.0 + control.brake = 0.0 + control.hand_brake = False + + if not self._agent: + rospy.loginfo("Waiting for ego vehicle...") + return control + + if not self._route_assigned and self._global_plan: + rospy.loginfo("Assigning plan...") + self._agent._local_planner.set_global_plan( # pylint: disable=protected-access + self._global_plan.poses) + self._route_assigned = True + else: + control = self._agent.run_step(self._target_speed) + + return control + + def run(self): + """ + + Control loop + + :return: + """ + + while not rospy.is_shutdown(): + control = self.run_step() + if control: + control.steer = -control.steer + self.vehicle_control_publisher.publish(control) + + +def main(): + """ + + main function + + :return: + """ + rospy.init_node('carla_ad_agent', anonymous=True) + role_name = rospy.get_param("~role_name", "ego_vehicle") + target_speed = rospy.get_param("~target_speed", 20) + avoid_risk = rospy.get_param("~avoid_risk", True) + controller = CarlaAdAgent(role_name, target_speed, avoid_risk) + try: + controller.run() + finally: + del controller + rospy.loginfo("Done") + + +if __name__ == "__main__": + main() diff --git a/carla_ad_agent/src/carla_ad_agent/local_planner.py b/carla_ad_agent/src/carla_ad_agent/local_planner.py new file mode 100644 index 00000000..ab14dfca --- /dev/null +++ b/carla_ad_agent/src/carla_ad_agent/local_planner.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python +# +# Copyright (c) 2018-2020 Intel Corporation +# +# This work is licensed under the terms of the MIT license. +# For a copy, see . +# +""" +This module contains a local planner to perform +low-level waypoint following based on PID controllers. +""" + +from collections import deque +import math +import rospy +from geometry_msgs.msg import PointStamped +from geometry_msgs.msg import Pose +from tf.transformations import euler_from_quaternion +from carla_waypoint_types.srv import GetWaypoint +from carla_msgs.msg import CarlaEgoVehicleControl +from vehicle_pid_controller import VehiclePIDController +from misc import distance_vehicle + + +class LocalPlanner(object): + """ + LocalPlanner implements the basic behavior of following a trajectory of waypoints that is + generated on-the-fly. The low-level motion of the vehicle is computed by using two PID + controllers, one is used for the lateral control and the other for the longitudinal + control (cruise speed). + + When multiple paths are available (intersections) this local planner makes a random choice. + """ + + # minimum distance to target waypoint as a percentage (e.g. within 90% of + # total distance) + MIN_DISTANCE_PERCENTAGE = 0.9 + + def __init__(self, role_name, opt_dict=None): + """ + :param vehicle: actor to apply to local planner logic onto + :param opt_dict: dictionary of arguments with the following semantics: + + target_speed -- desired cruise speed in Km/h + + sampling_radius -- search radius for next waypoints in seconds: e.g. 0.5 seconds ahead + + lateral_control_dict -- dictionary of arguments to setup the lateral PID controller + {'K_P':, 'K_D':, 'K_I'} + + longitudinal_control_dict -- dictionary of arguments to setup the longitudinal + PID controller + {'K_P':, 'K_D':, 'K_I'} + """ + self._current_waypoint = None + self.target_waypoint = None + self._vehicle_controller = None + self._global_plan = None + self._waypoints_queue = deque(maxlen=20000) + self._buffer_size = 5 + self._waypoint_buffer = deque(maxlen=self._buffer_size) + self._vehicle_yaw = None + self._current_speed = None + self._current_pose = None + + self._target_point_publisher = rospy.Publisher( + "/next_target", PointStamped, queue_size=1) + rospy.wait_for_service('/carla_waypoint_publisher/{}/get_waypoint'.format(role_name)) + + self._get_waypoint_client = rospy.ServiceProxy( + '/carla_waypoint_publisher/{}/get_waypoint'.format(role_name), GetWaypoint) + + # initializing controller + self._init_controller(opt_dict) + + def get_waypoint(self, location): + """ + Helper to get waypoint from a ros service + """ + try: + response = self._get_waypoint_client(location) + return response.waypoint + except (rospy.ServiceException, rospy.ROSInterruptException) as e: + if not rospy.is_shutdown: + rospy.logwarn("Service call failed: {}".format(e)) + + def odometry_updated(self, odo): + """ + Callback on new odometry + """ + self._current_speed = math.sqrt(odo.twist.twist.linear.x ** 2 + + odo.twist.twist.linear.y ** 2 + + odo.twist.twist.linear.z ** 2) * 3.6 + + self._current_pose = odo.pose.pose + quaternion = ( + odo.pose.pose.orientation.x, + odo.pose.pose.orientation.y, + odo.pose.pose.orientation.z, + odo.pose.pose.orientation.w + ) + _, _, self._vehicle_yaw = euler_from_quaternion(quaternion) + + def _init_controller(self, opt_dict): + """ + Controller initialization. + + :param opt_dict: dictionary of arguments. + :return: + """ + # default params + self._current_speed = 0.0 # Km/h + self._current_pose = Pose() + args_lateral_dict = { + 'K_P': 1.95, + 'K_D': 0.01, + 'K_I': 1.4} + args_longitudinal_dict = { + 'K_P': 0.2, + 'K_D': 0.05, + 'K_I': 0.1} + + # parameters overload + if opt_dict: + if 'lateral_control_dict' in opt_dict: + args_lateral_dict = opt_dict['lateral_control_dict'] + if 'longitudinal_control_dict' in opt_dict: + args_longitudinal_dict = opt_dict['longitudinal_control_dict'] + + self._current_waypoint = self.get_waypoint(self._current_pose.position) + self._vehicle_controller = VehiclePIDController(args_lateral=args_lateral_dict, + args_longitudinal=args_longitudinal_dict) + + self._global_plan = False + + def set_global_plan(self, current_plan): + """ + set a global plan to follow + """ + self._waypoints_queue.clear() + self._waypoint_buffer.clear() + for elem in current_plan: + self._waypoints_queue.append(elem.pose) + self._global_plan = True + + def run_step(self, target_speed): + """ + Execute one step of local planning which involves running the longitudinal + and lateral PID controllers to follow the waypoints trajectory. + """ + if not self._waypoints_queue: + control = CarlaEgoVehicleControl() + control.steer = 0.0 + control.throttle = 0.0 + control.brake = 1.0 + control.hand_brake = False + control.manual_gear_shift = False + + return control + + # Buffering the waypoints + if not self._waypoint_buffer: + for i in range(self._buffer_size): + if self._waypoints_queue: + self._waypoint_buffer.append( + self._waypoints_queue.popleft()) + else: + break + + # current vehicle waypoint + self._current_waypoint = self.get_waypoint(self._current_pose.position) + + # target waypoint + target_route_point = self._waypoint_buffer[0] + + # for us redlight-detection + self.target_waypoint = self.get_waypoint(target_route_point.position) + + target_point = PointStamped() + target_point.header.frame_id = "map" + target_point.point.x = target_route_point.position.x + target_point.point.y = target_route_point.position.y + target_point.point.z = target_route_point.position.z + self._target_point_publisher.publish(target_point) + # move using PID controllers + control = self._vehicle_controller.run_step( + target_speed, self._current_speed, self._current_pose, target_route_point) + + # purge the queue of obsolete waypoints + max_index = -1 + + sampling_radius = target_speed * 1 / 3.6 # 1 seconds horizon + min_distance = sampling_radius * self.MIN_DISTANCE_PERCENTAGE + + for i, route_point in enumerate(self._waypoint_buffer): + if distance_vehicle( + route_point, self._current_pose.position) < min_distance: + max_index = i + if max_index >= 0: + for i in range(max_index + 1): + self._waypoint_buffer.popleft() + + return control diff --git a/carla_ad_agent/src/carla_ad_agent/misc.py b/carla_ad_agent/src/carla_ad_agent/misc.py new file mode 100644 index 00000000..9495b815 --- /dev/null +++ b/carla_ad_agent/src/carla_ad_agent/misc.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +# +# Copyright (c) 2018-2020 Intel Corporation +# +# This work is licensed under the terms of the MIT license. +# For a copy, see . +# +""" This module contains helper functions """ + +import math +import numpy as np + + +def is_within_distance_ahead(target_location, current_location, orientation, max_distance): + """ + Check if a target object is within a certain distance in front of a reference object. + + :param target_location: location of the target object + :param current_location: location of the reference object + :param orientation: orientation of the reference object + :param max_distance: maximum allowed distance + :return: True if target object is within max_distance ahead of the reference object + """ + target_vector = np.array([target_location.x - current_location.x, + target_location.y - current_location.y]) + norm_target = np.linalg.norm(target_vector) + + # If the vector is too short, we can simply stop here + if norm_target < 0.001: + return True + + if norm_target > max_distance: + return False + + forward_vector = np.array( + [math.cos(math.radians(orientation)), math.sin(math.radians(orientation))]) + d_angle = math.degrees(math.acos(np.dot(forward_vector, target_vector) / norm_target)) + + return d_angle < 90.0 + + +def compute_magnitude_angle(target_location, current_location, orientation): + """ + Compute relative angle and distance between a target_location and a current_location + + :param target_location: location of the target object + :param current_location: location of the reference object + :param orientation: orientation of the reference object + :return: a tuple composed by the distance to the object and the angle between both objects + """ + target_vector = np.array([target_location.x - current_location.x, + target_location.y - current_location.y]) + norm_target = np.linalg.norm(target_vector) + + forward_vector = np.array([math.cos(math.radians(orientation)), + math.sin(math.radians(orientation))]) + d_angle = math.degrees( + math.acos(np.clip(np.dot(forward_vector, target_vector) / norm_target, -1., 1.))) + + return (norm_target, d_angle) + + +def distance_vehicle(waypoint, vehicle_position): + """ + calculate distance between waypoint and vehicle position + """ + dx = waypoint.position.x - vehicle_position.x + dy = waypoint.position.y - vehicle_position.y + + return math.sqrt(dx * dx + dy * dy) diff --git a/carla_ad_agent/src/carla_ad_agent/vehicle_pid_controller.py b/carla_ad_agent/src/carla_ad_agent/vehicle_pid_controller.py new file mode 100644 index 00000000..6d19936d --- /dev/null +++ b/carla_ad_agent/src/carla_ad_agent/vehicle_pid_controller.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python +# +# Copyright (c) 2018-2020 Intel Corporation +# +# This work is licensed under the terms of the MIT license. +# For a copy, see . +# + +""" This module contains PID controllers to perform lateral and longitudinal control. """ + +from collections import deque +import math +import numpy as np +import rospy +from tf.transformations import euler_from_quaternion +from geometry_msgs.msg import Point +from carla_msgs.msg import CarlaEgoVehicleControl + + +class VehiclePIDController(object): # pylint: disable=too-few-public-methods + """ + VehiclePIDController is the combination of two PID controllers (lateral and longitudinal) + to perform the low level control a vehicle from client side + """ + + def __init__(self, args_lateral=None, args_longitudinal=None): + """ + :param vehicle: actor to apply to local planner logic onto + :param args_lateral: dictionary of arguments to set the lateral PID controller using + the following semantics: + K_P -- Proportional term + K_D -- Differential term + K_I -- Integral term + :param args_longitudinal: dictionary of arguments to set the longitudinal PID + controller using the following semantics: + K_P -- Proportional term + K_D -- Differential term + K_I -- Integral term + """ + if not args_lateral: + args_lateral = {'K_P': 1.0, 'K_D': 0.0, 'K_I': 0.0} + if not args_longitudinal: + args_longitudinal = {'K_P': 1.0, 'K_D': 0.0, 'K_I': 0.0} + + self._lon_controller = PIDLongitudinalController(**args_longitudinal) + self._lat_controller = PIDLateralController(**args_lateral) + self._last_control_time = rospy.get_time() + + def run_step(self, target_speed, current_speed, current_pose, waypoint): + """ + Execute one step of control invoking both lateral and longitudinal + PID controllers to reach a target waypoint at a given target_speed. + + :param target_speed: desired vehicle speed + :param waypoint: target location encoded as a waypoint + :return: distance (in meters) to the waypoint + """ + current_time = rospy.get_time() + dt = current_time-self._last_control_time + if dt == 0.0: + dt = 0.000001 + control = CarlaEgoVehicleControl() + throttle = self._lon_controller.run_step(target_speed, current_speed, dt) + steering = self._lat_controller.run_step(current_pose, waypoint, dt) + self._last_control_time = current_time + control.steer = steering + control.throttle = throttle + control.brake = 0.0 + control.hand_brake = False + control.manual_gear_shift = False + + return control + + +class PIDLongitudinalController(object): # pylint: disable=too-few-public-methods + """ + PIDLongitudinalController implements longitudinal control using a PID. + """ + + def __init__(self, K_P=1.0, K_D=0.0, K_I=0.0): + """ + :param vehicle: actor to apply to local planner logic onto + :param K_P: Proportional term + :param K_D: Differential term + :param K_I: Integral term + """ + self._K_P = K_P + self._K_D = K_D + self._K_I = K_I + self._e_buffer = deque(maxlen=30) + + def run_step(self, target_speed, current_speed, dt): + """ + Estimate the throttle of the vehicle based on the PID equations + + :param target_speed: target speed in Km/h + :param current_speed: current speed of the vehicle in Km/h + :return: throttle control in the range [0, 1] + """ + _e = (target_speed - current_speed) + self._e_buffer.append(_e) + + if len(self._e_buffer) >= 2: + _de = (self._e_buffer[-1] - self._e_buffer[-2]) / dt + _ie = sum(self._e_buffer) * dt + else: + _de = 0.0 + _ie = 0.0 + + return np.clip((self._K_P * _e) + (self._K_D * _de / dt) + (self._K_I * _ie * dt), 0.0, 1.0) + + +class PIDLateralController(object): # pylint: disable=too-few-public-methods + """ + PIDLateralController implements lateral control using a PID. + """ + + def __init__(self, K_P=1.0, K_D=0.0, K_I=0.0): + """ + :param vehicle: actor to apply to local planner logic onto + :param K_P: Proportional term + :param K_D: Differential term + :param K_I: Integral term + """ + self._K_P = K_P + self._K_D = K_D + self._K_I = K_I + self._e_buffer = deque(maxlen=10) + + def run_step(self, current_pose, waypoint, dt): + """ + Estimate the steering angle of the vehicle based on the PID equations + + :param waypoint: target waypoint + :param current_pose: current pose of the vehicle + :return: steering control in the range [-1, 1] + """ + v_begin = current_pose.position + quaternion = ( + current_pose.orientation.x, + current_pose.orientation.y, + current_pose.orientation.z, + current_pose.orientation.w + ) + _, _, yaw = euler_from_quaternion(quaternion) + v_end = Point() + v_end.x = v_begin.x + math.cos(yaw) + v_end.y = v_begin.y + math.sin(yaw) + + v_vec = np.array([v_end.x - v_begin.x, v_end.y - v_begin.y, 0.0]) + w_vec = np.array([waypoint.position.x - + v_begin.x, waypoint.position.y - + v_begin.y, 0.0]) + _dot = math.acos(np.clip(np.dot(w_vec, v_vec) / + (np.linalg.norm(w_vec) * np.linalg.norm(v_vec)), -1.0, 1.0)) + + _cross = np.cross(v_vec, w_vec) + if _cross[2] < 0: + _dot *= -1.0 + + self._e_buffer.append(_dot) + if len(self._e_buffer) >= 2: + _de = (self._e_buffer[-1] - self._e_buffer[-2]) / dt + _ie = sum(self._e_buffer) * dt + else: + _de = 0.0 + _ie = 0.0 + + return np.clip((self._K_P * _dot) + (self._K_D * _de / + dt) + (self._K_I * _ie * dt), -1.0, 1.0) diff --git a/carla_ad_demo/CMakeLists.txt b/carla_ad_demo/CMakeLists.txt new file mode 100644 index 00000000..56b4a771 --- /dev/null +++ b/carla_ad_demo/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 2.8.3) +project(carla_ad_demo) + +find_package(catkin REQUIRED COMPONENTS + roslaunch +) + + +roslaunch_add_file_check(launch) + +catkin_package() + +install(DIRECTORY launch/ + DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}/launch +) + +install(DIRECTORY config/ + DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}/config +) diff --git a/carla_ad_demo/README.md b/carla_ad_demo/README.md new file mode 100644 index 00000000..e6b17f51 --- /dev/null +++ b/carla_ad_demo/README.md @@ -0,0 +1,36 @@ +# CARLA AD Demo + +This meta package provides everything to launch a CARLA ROS environment with an AD vehicle. + + +![CARLA AD Demo](../docs/images/rviz_carla_plugin.png "rviz carla plugin") + + +## Startup + + export PYTHONPATH=$PYTHONPATH:/PythonAPI/carla-.egg:/PythonAPI/carla/ + export SCENARIO_RUNNER_PATH:= + roslaunch carla_ad_demo carla_ad_demo_with_rviz.launch + +### Modes + +#### Following A Random Route + +On startup, an ego vehicle is spawned and follows a random route to a goal. + +You might want to spawn additional vehicles (or pedestrians) by manually executing: + + /PythonAPI/examples/spawn_npc.py + +You can modify start position and goal within the [launch file](launch/carla_ad_demo.launch). The route is currently randomly (regarding the left/right turns) calculated. + +#### Scenario Execution + +If you prefer to execute a predefined scenario, select to example scenario "FollowLeadingVehicle" within the RVIZ Carla Plugin and press "Execute". The ego vehicle gets repositioned and the scenario is processed. + +You can specify your own scenarios by publishing to `/carla/available_scenarios`. See the [launch file](launch/carla_ad_demo_with_rviz.launch) for an example. + + +## Troubleshooting + +If the example scenario fails, please analyze the ros log and follow the scenario runner [Getting Started](https://github.com/carla-simulator/scenario_runner/blob/master/Docs/getting_started.md) to verify that it's working standalone. diff --git a/carla_ad_demo/config/FollowLeadingVehicle.xosc b/carla_ad_demo/config/FollowLeadingVehicle.xosc new file mode 100644 index 00000000..db33827d --- /dev/null +++ b/carla_ad_demo/config/FollowLeadingVehicle.xosc @@ -0,0 +1,270 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ + + + + +
+ + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/carla_ad_demo/config/carla_ad_demo.rviz b/carla_ad_demo/config/carla_ad_demo.rviz new file mode 100644 index 00000000..35fd2f9d --- /dev/null +++ b/carla_ad_demo/config/carla_ad_demo.rviz @@ -0,0 +1,173 @@ +Panels: + - Class: rviz/Displays + Help Height: 78 + Name: Displays + Property Tree Widget: + Expanded: + - /Global Options1 + - /Status1 + Splitter Ratio: 0.5 + Tree Height: 78 + - Class: rviz/Selection + Name: Selection + - Class: rviz/Tool Properties + Expanded: + - /2D Pose Estimate1 + - /2D Nav Goal1 + - /Publish Point1 + Name: Tool Properties + Splitter Ratio: 0.5886790156364441 + - Class: rviz/Views + Expanded: + - /Current View1 + Name: Views + Splitter Ratio: 0.5 + - Class: rviz/Time + Experimental: false + Name: Time + SyncMode: 0 + SyncSource: Camera + - Class: rviz_carla_plugin/CarlaControl + Name: CarlaControl +Preferences: + PromptSaveOnExit: true +Toolbars: + toolButtonStyle: 2 +Visualization Manager: + Class: "" + Displays: + - Alpha: 0.5 + Cell Size: 1 + Class: rviz/Grid + Color: 160; 160; 164 + Enabled: true + Line Style: + Line Width: 0.029999999329447746 + Value: Lines + Name: Grid + Normal Cell Count: 0 + Offset: + X: 0 + Y: 0 + Z: 0 + Plane: XY + Plane Cell Count: 10 + Reference Frame: + Value: true + - Alpha: 1 + Buffer Length: 1 + Class: rviz/Path + Color: 25; 255; 0 + Enabled: true + Head Diameter: 0.30000001192092896 + Head Length: 0.20000000298023224 + Length: 0.30000001192092896 + Line Style: Lines + Line Width: 0.029999999329447746 + Name: Path + Offset: + X: 0 + Y: 0 + Z: 0 + Pose Color: 255; 85; 255 + Pose Style: None + Radius: 0.029999999329447746 + Shaft Diameter: 0.10000000149011612 + Shaft Length: 0.10000000149011612 + Topic: /carla/ego_vehicle/waypoints + Unreliable: false + Value: true + - Class: rviz/Camera + Enabled: true + Image Rendering: background and overlay + Image Topic: /carla/ego_vehicle/camera/rgb/spectator_view/image_color + Name: Camera + Overlay Alpha: 0.5 + Queue Size: 2 + Transport Hint: raw + Unreliable: false + Value: true + Visibility: + Grid: true + Marker: true + Path: true + Value: true + Zoom Factor: 1 + - Class: rviz/Marker + Enabled: true + Marker Topic: /carla/marker + Name: Marker + Namespaces: + "": true + Queue Size: 100 + Value: true + Enabled: true + Global Options: + Background Color: 48; 48; 48 + Default Light: true + Fixed Frame: map + Frame Rate: 30 + Name: root + Tools: + - Class: rviz/Interact + Hide Inactive Objects: true + - Class: rviz/MoveCamera + - Class: rviz/Select + - Class: rviz/FocusCamera + - Class: rviz/Measure + - Class: rviz/SetInitialPose + Theta std deviation: 0.2617993950843811 + Topic: /initialpose + X std deviation: 0.5 + Y std deviation: 0.5 + - Class: rviz/SetGoal + Topic: /move_base_simple/goal + - Class: rviz/PublishPoint + Single click: true + Topic: /clicked_point + Value: true + Views: + Current: + Class: rviz/ThirdPersonFollower + Distance: 13.610654830932617 + Enable Stereo Rendering: + Stereo Eye Separation: 0.05999999865889549 + Stereo Focal Distance: 1 + Swap Stereo Eyes: false + Value: false + Focal Point: + X: 0 + Y: 0 + Z: 0 + Focal Shape Fixed Size: false + Focal Shape Size: 0.05000000074505806 + Invert Z Axis: false + Name: Current View + Near Clip Distance: 0.009999999776482582 + Pitch: 0.49539870023727417 + Target Frame: ego_vehicle + Value: ThirdPersonFollower (rviz) + Yaw: 3.1504008769989014 + Saved: ~ +Window Geometry: + Camera: + collapsed: false + CarlaControl: + collapsed: false + Displays: + collapsed: false + Height: 1050 + Hide Left Dock: false + Hide Right Dock: false + QMainWindow State: 000000ff00000000fd0000000400000000000001fe00000367fc020000000dfb0000001200530065006c0065006300740069006f006e00000001e10000009b0000006800fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afb000000100044006900730070006c0061007900730100000046000000e6000000e600fffffffb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c00000261fb0000000a0049006d00610067006501000002870000001a0000000000000000fb0000000a0049006d00610067006501000002a70000001a0000000000000000fb0000000a0049006d00610067006501000002c70000001a0000000000000000fb0000000c00430061006d0065007200610100000132000001960000001a00fffffffb00000018004300610072006c00610043006f006e00740072006f006c01000002ce000000df000000df00ffffff000000010000010f00000367fc0200000003fb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fb0000000a00560069006500770073010000004600000367000000be00fffffffb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e100000197000000030000077e00000044fc0100000002fb0000000800540069006d006501000000000000077e000002e400fffffffb0000000800540069006d00650100000000000004500000000000000000000004650000036700000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730100000000ffffffff0000000000000000 + Selection: + collapsed: false + Time: + collapsed: false + Tool Properties: + collapsed: false + Views: + collapsed: false + Width: 1918 + X: 0 + Y: 27 diff --git a/carla_ad_demo/config/sensors.json b/carla_ad_demo/config/sensors.json new file mode 100644 index 00000000..3d8fe104 --- /dev/null +++ b/carla_ad_demo/config/sensors.json @@ -0,0 +1,48 @@ +{ + "sensors": [ + { + "type": "sensor.camera.rgb", + "id": "view", + "x": -4.5, "y": 0.0, "z": 2.8, "roll": 0.0, "pitch": -20.0, "yaw": 0.0, + "width": 800, + "height": 600, + "fov": 100, + "sensor_tick": 0.05, + "gamma": 2.2, + "shutter_speed": 60.0, + "iso": 1200.0, + "fstop": 1.4, + "min_fstop": 1.2, + "blade_count": 5, + "exposure_mode": "manual", + "exposure_compensation": 3.0, + "exposure_min_bright": 0.1, + "exposure_max_bright": 2.0, + "exposure_speed_up": 3.0, + "exposure_speed_down": 1.0, + "calibration_constant": 16.0, + "focal_distance": 1000.0, + "blur_amount": 1.0, + "blur_radius": 0.0, + "motion_blur_intensity": 0.45, + "motion_blur_max_distortion": 0.35, + "motion_blur_min_object_screen_size": 0.1, + "slope": 0.88, + "toe": 0.55, + "shoulder": 0.26, + "black_clip": 0.0, + "white_clip": 0.04, + "temp": 6500.0, + "tint": 0.0, + "chromatic_aberration_intensity": 0.0, + "chromatic_aberration_offset": 0.0, + "enable_postprocess_effects": "True", + "lens_circle_falloff": 5.0, + "lens_circle_multiplier": 0.0, + "lens_k": -1.0, + "lens_kcube": 0.0, + "lens_x_size": 0.08, + "lens_y_size": 0.08 + } + ] +} diff --git a/carla_ad_demo/launch/carla_ad_demo.launch b/carla_ad_demo/launch/carla_ad_demo.launch new file mode 100644 index 00000000..c679c7bf --- /dev/null +++ b/carla_ad_demo/launch/carla_ad_demo.launch @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/carla_ad_demo/launch/carla_ad_demo_with_rviz.launch b/carla_ad_demo/launch/carla_ad_demo_with_rviz.launch new file mode 100644 index 00000000..2efff80c --- /dev/null +++ b/carla_ad_demo/launch/carla_ad_demo_with_rviz.launch @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/carla_ad_demo/package.xml b/carla_ad_demo/package.xml new file mode 100644 index 00000000..4d354b9e --- /dev/null +++ b/carla_ad_demo/package.xml @@ -0,0 +1,20 @@ + + + carla_ad_demo + 0.0.1 + The carla_ad_demo package + CARLA Simulator Team + MIT + catkin + carla_ros_bridge + carla_ego_vehicle + carla_waypoint_publisher + carla_ad_agent + carla_manual_control + rviz_carla_plugin + carla_twist_to_control + carla_spectator_camera + carla_ros_scenario_runner + + + diff --git a/carla_ros_scenario_runner/CMakeLists.txt b/carla_ros_scenario_runner/CMakeLists.txt new file mode 100644 index 00000000..877653c6 --- /dev/null +++ b/carla_ros_scenario_runner/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 2.8.3) +project(carla_ros_scenario_runner) + +find_package(catkin REQUIRED COMPONENTS + roslaunch +) + +catkin_python_setup() + +roslaunch_add_file_check(launch) + +catkin_package() + +catkin_install_python(PROGRAMS + src/carla_ros_scenario_runner/carla_ros_scenario_runner.py + src/carla_ros_scenario_runner/application_runner.py + src/carla_ros_scenario_runner/scenario_runner_runner.py + DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} +) + +install(DIRECTORY launch/ + DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}/launch +) diff --git a/carla_ros_scenario_runner/README.md b/carla_ros_scenario_runner/README.md new file mode 100644 index 00000000..ab803b35 --- /dev/null +++ b/carla_ros_scenario_runner/README.md @@ -0,0 +1,45 @@ +# ROS Scenario Runner + +This is a wrapper to execute OpenScenarios with the CARLA [scenario runner](https://github.com/carla-simulator/scenario_runner) via ROS. + +It is best used from within the [rviz_carla_plugin](../rviz_carla_plugin). + +## Setup + +Please follow the scenario runner [Getting Started](https://github.com/carla-simulator/scenario_runner/blob/master/Docs/getting_started.md) and verify, that scenario_runner is working, if started manually. + +Additionally, please execute: + + sudo apt install python-pexpect + +## Startup + +The environment variables are forwarded to scenario_runner, therefore set them to: + + export PYTHONPATH=$PYTHONPATH:/PythonAPI/carla-.egg:/PythonAPI/carla/ + +To run the ROS node: + + roslaunch carla_ros_scenario_runner carla_ros_scenario_runner.launch scenario_runner_path:= + +To run a scenario from commandline: + + rosservice call /scenario_runner/execute_scenario "{ 'scenario': { 'scenario_file': '', 'target_speed': 10.0, 'destination': { 'position': { 'x': 10, 'y': 10, 'z':0 } } } }" + + +## Available services + +| Service | Description | Type | +| ----------------------------------------------------------- | ----------- | -------------------------------------------------------------------- | +| `/scenario_runner/execute_scenario` | Execute a scenario. If another scenario is currently running, it gets stopped. | [carla_ros_scenario_runner_types.ExecuteScenario](../carla_ros_scenario_runner_types/srv/ExecuteScenario.srv) | + + +## Available topics + + +| Topic | Description | Type | +| ------------------------------------- | ----------- | -------------------------------------------------------------------- | +| `/carla//target_speed` | The ego vehicle target speed | [std_msgs.Float64](http://docs.ros.org/api/std_msgs/html/msg/Float64.html) | +| `/carla//goal` | The ego vehicle destination (e.g. used by [carla_waypoint_publisher](../carla_waypoint_publisher)) | [geometry_msgs.PoseStamped](http://docs.ros.org/api/geometry_msgs/html/msg/PoseStamped.html) | +| `/scenario_runner/status` | The current status of the scenario runner execution (e.g. used by the [rviz_carla_plugin](../rviz_carla_plugin)) | [carla_ros_scenario_runner_types.CarlaScenarioRunnerStatus](../carla_ros_scenario_runner_types/msg/CarlaScenarioRunnerStatus.msg) | + diff --git a/carla_ros_scenario_runner/launch/carla_ros_scenario_runner.launch b/carla_ros_scenario_runner/launch/carla_ros_scenario_runner.launch new file mode 100644 index 00000000..def00183 --- /dev/null +++ b/carla_ros_scenario_runner/launch/carla_ros_scenario_runner.launch @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/carla_ros_scenario_runner/package.xml b/carla_ros_scenario_runner/package.xml new file mode 100644 index 00000000..a7701fe1 --- /dev/null +++ b/carla_ros_scenario_runner/package.xml @@ -0,0 +1,19 @@ + + + carla_ros_scenario_runner + 0.0.0 + The carla_ros_scenario_runner package + CARLA Simulator Team + MIT + catkin + rospy + roslaunch + rospy + rospy + carla_msgs + carla_ros_scenario_runner_types + std_msgs + + + + diff --git a/carla_ros_scenario_runner/setup.py b/carla_ros_scenario_runner/setup.py new file mode 100644 index 00000000..1204b037 --- /dev/null +++ b/carla_ros_scenario_runner/setup.py @@ -0,0 +1,13 @@ +""" +Setup for carla_ros_scenario_runner +""" + +from distutils.core import setup +from catkin_pkg.python_setup import generate_distutils_setup + +d = generate_distutils_setup( + packages=['carla_ros_scenario_runner'], + package_dir={'': 'src'} +) + +setup(**d) diff --git a/carla_ros_scenario_runner/src/carla_ros_scenario_runner/__init__.py b/carla_ros_scenario_runner/src/carla_ros_scenario_runner/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/carla_ros_scenario_runner/src/carla_ros_scenario_runner/application_runner.py b/carla_ros_scenario_runner/src/carla_ros_scenario_runner/application_runner.py new file mode 100755 index 00000000..dcff47b1 --- /dev/null +++ b/carla_ros_scenario_runner/src/carla_ros_scenario_runner/application_runner.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python +# +# Copyright (c) 2020 Intel Corporation +# +""" +Execute an application. +""" +import os +from enum import Enum +from threading import Thread, Event +from datetime import datetime, timedelta +import pexpect + + +class ApplicationStatus(Enum): + """ + States of an application + """ + STOPPED = 0 + STARTING = 1 + RUNNING = 2 + SHUTTINGDOWN = 3 + ERROR = 4 + + +class ApplicationRunner(object): + + """ + Execute application + """ + + def __init__(self, status_updated_fct, log_fct, ready_string=""): + """ + Constructor + """ + self._app_thread = None + self._status_updated_fct = status_updated_fct + self._log_fct = log_fct + self._shutdown_requested_event = Event() + self._ready_string = ready_string + + def execute(self, cmdline, env=None, cwd=None): + """ + Starts a thread to execute the application + """ + if self.is_running(): + self._log_fct("Application already running!") + return False + + self._shutdown_requested_event.clear() + self._app_thread = Thread(target=self.start_and_run, args=(cmdline, + env, + cwd, + self._shutdown_requested_event, + self._ready_string, + self._status_updated_fct, + self._log_fct,)) + self._app_thread.start() + + return True + + def start_and_run(self, cmdline, env, cwd, shutdown_requested_event, ready_string, # pylint: disable=too-many-arguments + status_updated_fct, log_fct): + """ + thread function + """ + status_updated_fct(ApplicationStatus.STARTING) + try: + process = self.start_process(cmdline, log_fct, env=env, cwd=cwd) + self.run(process, shutdown_requested_event, ready_string, status_updated_fct, log_fct) + except (KeyError, pexpect.ExceptionPexpect) as e: + self._log_fct("Error while starting process: {}".format(e)) + status_updated_fct(ApplicationStatus.ERROR) + + def is_running(self): + """ + returns if the application is still running + """ + if self._app_thread is None: + return False + + return self._app_thread.is_alive() + + def shutdown(self): + """ + Shut down the application thread + """ + if not self.is_running(): + return + self._log_fct("Requesting shutdown...") + self._status_updated_fct(ApplicationStatus.SHUTTINGDOWN) + self._shutdown_requested_event.set() + if self._app_thread: + self._app_thread.join() + self._log_fct("Shutdown finished.") + + def start_process(self, argument_list, log_fct, env=None, cwd=None): # pylint: disable=no-self-use + """ + Starts a process. + """ + if not argument_list: + raise KeyError("No arguments given!") + executable = argument_list[0] + if not os.path.isfile(executable): + raise KeyError("The executable {} does not exist".format(executable)) + log_fct("Executing: {}".format(" ".join(argument_list))) + process = pexpect.spawn(" ".join(argument_list), env=env, cwd=cwd) + #process.logfile_read = sys.stdout + return process + + def run(self, process, shutdown_requested_event, ready_string, status_updated_fct, log_fct): # pylint: disable=no-self-use,too-many-arguments + """ + Threaded application execution + + :return: + """ + shutting_down_trigger_time = None + signaled_running = False + while True: + if shutdown_requested_event.is_set(): + if shutting_down_trigger_time is None: + shutting_down_trigger_time = datetime.now() + log_fct("Shutdown requested while process is still \ + running. Sending SIGHUP/SIGINT...") + process.terminate(force=False) + else: + if (datetime.now() - shutting_down_trigger_time) > timedelta(seconds=8): + log_fct("Waited 8s for application to exit. Forcing Shutdown. \ + Sending SIGKILL") + process.terminate(force=True) + try: + process.expect(".*\n", timeout=0.1) + log_fct(process.after.strip()) + if not signaled_running: + if process.after.find(ready_string) != -1: + status_updated_fct(ApplicationStatus.RUNNING) + log_fct("Application is ready.") + signaled_running = True + except pexpect.EOF: + # application exited + log_fct(process.before.strip()) + log_fct("Application exited. Exiting run loop") + break + except pexpect.TIMEOUT: + # no output received + pass + + process.close() + if process.exitstatus == 0: + status_updated_fct(ApplicationStatus.STOPPED) + else: + status_updated_fct(ApplicationStatus.ERROR) diff --git a/carla_ros_scenario_runner/src/carla_ros_scenario_runner/carla_ros_scenario_runner.py b/carla_ros_scenario_runner/src/carla_ros_scenario_runner/carla_ros_scenario_runner.py new file mode 100755 index 00000000..77ae976e --- /dev/null +++ b/carla_ros_scenario_runner/src/carla_ros_scenario_runner/carla_ros_scenario_runner.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python +# +# Copyright (c) 2020 Intel Corporation. +# +# This work is licensed under the terms of the MIT license. +# For a copy, see . +# +""" +Execute scenarios via ros service + +Internally, the CARLA scenario runner is executed +""" +import sys +import os +try: + import queue +except ImportError: + import Queue as queue +import rospy +from geometry_msgs.msg import PoseStamped +from std_msgs.msg import Float64 +from carla_ros_scenario_runner_types.srv import ExecuteScenario, ExecuteScenarioResponse +from carla_ros_scenario_runner_types.msg import CarlaScenarioRunnerStatus +from application_runner import ApplicationStatus +from scenario_runner_runner import ScenarioRunnerRunner + +# Check Python dependencies of scenario runner +try: + import carla # pylint: disable=unused-import +except ImportError: + print "ERROR: CARLA Python Egg not available. Please add \ + /PythonAPI/carla/dist/carla--\ + py-linux-x86_64.egg to your PYTHONPATH." + sys.exit(1) + +try: + from agents.navigation.local_planner import LocalPlanner # pylint: disable=unused-import +except ImportError: + print "ERROR: CARLA Python Agents not available. \ + Please add /PythonAPI/carla to your PYTHONPATH." + sys.exit(1) + + +class CarlaRosScenarioRunner(object): + """ + Execute scenarios via ros service + """ + + def __init__(self, role_name, host, scenario_runner_path): + """ + Constructor + """ + self._goal_publisher = rospy.Publisher( + "/carla/{}/goal".format(role_name), PoseStamped, queue_size=1, latch=True) + self._target_speed_publisher = rospy.Publisher( + "/carla/{}/target_speed".format(role_name), Float64, queue_size=1, latch=True) + self._status_publisher = rospy.Publisher( + "/scenario_runner/status", CarlaScenarioRunnerStatus, queue_size=1, latch=True) + self.scenario_runner_status_updated(ApplicationStatus.STOPPED) + self._scenario_runner = ScenarioRunnerRunner( + scenario_runner_path, + host, + self.scenario_runner_status_updated, + self.scenario_runner_log) + self._request_queue = queue.Queue() + self._execute_scenario_service = rospy.Service( + '/scenario_runner/execute_scenario', ExecuteScenario, self.execute_scenario) + + def scenario_runner_log(self, log): # pylint: disable=no-self-use + """ + Callback for application logs + """ + rospy.logwarn("[SC]{}".format(log)) + + def scenario_runner_status_updated(self, status): + """ + Executed from application runner whenever the status changed + """ + rospy.loginfo("Status updated to {}".format(status)) + val = CarlaScenarioRunnerStatus.STOPPED + if status == ApplicationStatus.STOPPED: + val = CarlaScenarioRunnerStatus.STOPPED + elif status == ApplicationStatus.STARTING: + val = CarlaScenarioRunnerStatus.STARTING + elif status == ApplicationStatus.RUNNING: + val = CarlaScenarioRunnerStatus.RUNNING + elif status == ApplicationStatus.SHUTTINGDOWN: + val = CarlaScenarioRunnerStatus.SHUTTINGDOWN + else: + val = CarlaScenarioRunnerStatus.ERROR + status = CarlaScenarioRunnerStatus() + status.status = val + self._status_publisher.publish(status) + + def execute_scenario(self, req): + """ + Execute a scenario + """ + rospy.loginfo("Scenario Execution requested...") + + response = ExecuteScenarioResponse() + response.result = True + if not os.path.isfile(req.scenario.scenario_file): + rospy.logwarn("Requested scenario file not existing {}".format( + req.scenario.scenario_file)) + response.result = False + else: + self._request_queue.put(req.scenario) + return response + + def run(self): + """ + Control loop + + :return: + """ + current_req = None + while not rospy.is_shutdown(): + if current_req: + if self._scenario_runner.is_running(): + rospy.loginfo("Scenario Runner currently running. Shutting down.") + self._scenario_runner.shutdown() + rospy.loginfo("Scenario Runner stopped.") + rospy.loginfo("Executing scenario {}...".format(current_req.name)) + # publish goal + goal = PoseStamped(pose=current_req.destination) + goal.header.stamp = rospy.get_rostime() + goal.header.frame_id = "map" + self._goal_publisher.publish(goal) + # publish target speed + self._target_speed_publisher.publish(Float64(data=current_req.target_speed)) + # execute scenario + scenario_executed = self._scenario_runner.execute_scenario( + current_req.scenario_file) + if not scenario_executed: + rospy.logwarn("Unable to execute scenario.") + current_req = None + else: + try: + current_req = self._request_queue.get(block=True, timeout=0.5) + except queue.Empty: + # no new request + pass + if self._scenario_runner.is_running(): + rospy.loginfo("Scenario Runner currently running. Shutting down.") + self._scenario_runner.shutdown() + + +def main(): + """ + + main function + + :return: + """ + rospy.init_node('carla_ros_scenario_runner', anonymous=True) + role_name = rospy.get_param("~role_name", "ego_vehicle") + scenario_runner_path = rospy.get_param("~scenario_runner_path", "") + host = rospy.get_param("~host", "localhost") + scenario_runner = CarlaRosScenarioRunner(role_name, host, scenario_runner_path) + try: + scenario_runner.run() + finally: + del scenario_runner + rospy.loginfo("Done.") + + +if __name__ == "__main__": + main() diff --git a/carla_ros_scenario_runner/src/carla_ros_scenario_runner/scenario_runner_runner.py b/carla_ros_scenario_runner/src/carla_ros_scenario_runner/scenario_runner_runner.py new file mode 100755 index 00000000..9ff8da61 --- /dev/null +++ b/carla_ros_scenario_runner/src/carla_ros_scenario_runner/scenario_runner_runner.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +# +# Copyright (c) 2020 Intel Corporation +# +""" +Executes scenario runner +""" +import os +from application_runner import ApplicationRunner + + +class ScenarioRunnerRunner(ApplicationRunner): + """ + Executes scenario runner + """ + + def __init__(self, path, host, status_updated_fct, log_fct): + self._path = path + self._host = host + super(ScenarioRunnerRunner, self).__init__( + status_updated_fct, + log_fct, + "ScenarioManager: Running scenario OpenScenario") + + def execute_scenario(self, scenario_file): + """ + Executes scenario + """ + cmdline = ["/usr/bin/python", "{}/scenario_runner.py".format(self._path), + "--openscenario", "{}".format(scenario_file), + "--waitForEgo", + "--host", self._host] + return self.execute(cmdline, env=os.environ) diff --git a/carla_ros_scenario_runner_types/CMakeLists.txt b/carla_ros_scenario_runner_types/CMakeLists.txt new file mode 100644 index 00000000..8184efef --- /dev/null +++ b/carla_ros_scenario_runner_types/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 2.8.3) +project(carla_ros_scenario_runner_types) + +find_package(catkin REQUIRED COMPONENTS + message_generation + geometry_msgs +) + +add_service_files(DIRECTORY srv FILES + ExecuteScenario.srv +) + +add_message_files(DIRECTORY msg FILES + CarlaScenario.msg + CarlaScenarioList.msg + CarlaScenarioRunnerStatus.msg +) + +generate_messages(DEPENDENCIES geometry_msgs) + +catkin_package() diff --git a/carla_ros_scenario_runner_types/msg/CarlaScenario.msg b/carla_ros_scenario_runner_types/msg/CarlaScenario.msg new file mode 100644 index 00000000..bdb90fa3 --- /dev/null +++ b/carla_ros_scenario_runner_types/msg/CarlaScenario.msg @@ -0,0 +1,11 @@ +# +# Copyright (c) 2020 Intel Corporation. +# +# This work is licensed under the terms of the MIT license. +# For a copy, see . +# +string name +string scenario_file +geometry_msgs/Pose destination +float64 target_speed + diff --git a/carla_ros_scenario_runner_types/msg/CarlaScenarioList.msg b/carla_ros_scenario_runner_types/msg/CarlaScenarioList.msg new file mode 100644 index 00000000..8b2df049 --- /dev/null +++ b/carla_ros_scenario_runner_types/msg/CarlaScenarioList.msg @@ -0,0 +1,8 @@ +# +# Copyright (c) 2020 Intel Corporation. +# +# This work is licensed under the terms of the MIT license. +# For a copy, see . +# +CarlaScenario[] scenarios + diff --git a/carla_ros_scenario_runner_types/msg/CarlaScenarioRunnerStatus.msg b/carla_ros_scenario_runner_types/msg/CarlaScenarioRunnerStatus.msg new file mode 100644 index 00000000..cf0039e3 --- /dev/null +++ b/carla_ros_scenario_runner_types/msg/CarlaScenarioRunnerStatus.msg @@ -0,0 +1,14 @@ +# +# Copyright (c) 2020 Intel Corporation. +# +# This work is licensed under the terms of the MIT license. +# For a copy, see . +# +uint8 STOPPED = 0 +uint8 STARTING = 1 +uint8 RUNNING = 2 +uint8 SHUTTINGDOWN = 3 +uint8 ERROR = 4 + +uint8 status + diff --git a/carla_ros_scenario_runner_types/package.xml b/carla_ros_scenario_runner_types/package.xml new file mode 100644 index 00000000..5df0ef5a --- /dev/null +++ b/carla_ros_scenario_runner_types/package.xml @@ -0,0 +1,14 @@ + + + carla_ros_scenario_runner_types + 0.1.0 + The carla_ros_scenario_runner_types package + CARLA Simulator Team + MIT + catkin + message_generation + geometry_msgs + message_runtime + geometry_msgs + + diff --git a/carla_ros_scenario_runner_types/srv/ExecuteScenario.srv b/carla_ros_scenario_runner_types/srv/ExecuteScenario.srv new file mode 100644 index 00000000..d342a73f --- /dev/null +++ b/carla_ros_scenario_runner_types/srv/ExecuteScenario.srv @@ -0,0 +1,10 @@ +# +# Copyright (c) 2020 Intel Corporation. +# +# This work is licensed under the terms of the MIT license. +# For a copy, see . +# +CarlaScenario scenario +--- +bool result + diff --git a/carla_spectator_camera/src/carla_spectator_camera/carla_spectator_camera.py b/carla_spectator_camera/src/carla_spectator_camera/carla_spectator_camera.py index 7877514c..4d1c3201 100755 --- a/carla_spectator_camera/src/carla_spectator_camera/carla_spectator_camera.py +++ b/carla_spectator_camera/src/carla_spectator_camera/carla_spectator_camera.py @@ -24,6 +24,7 @@ # -- CarlaSpectatorCamera ------------------------------------------------------------ # ============================================================================== + class CarlaSpectatorCamera(object): """ Spawns a camera, attached to an ego vehicle. @@ -71,7 +72,7 @@ def get_camera_transform(self): return None if self.pose.header.frame_id != self.role_name: rospy.logwarn("Unsupported frame received. Supported {}, received {}".format( - self.role_name, self.pose.header.frame_id)) + self.role_name, self.pose.header.frame_id)) return None sensor_location = carla.Location(x=self.pose.pose.position.x, y=-self.pose.pose.position.y, @@ -83,7 +84,7 @@ def get_camera_transform(self): self.pose.pose.orientation.w ) roll, pitch, yaw = euler_from_quaternion(quaternion) - #rotate to CARLA + # rotate to CARLA sensor_rotation = carla.Rotation(pitch=math.degrees(roll)-90, roll=math.degrees(pitch), yaw=-math.degrees(yaw)-90) @@ -98,9 +99,9 @@ def create_camera(self, ego_actor): bp = bp_library.find("sensor.camera.rgb") bp.set_attribute('role_name', "spectator_view") rospy.loginfo("Creating camera with resolution {}x{}, fov {}".format( - self.camera_resolution_x, - self.camera_resolution_y, - self.camera_fov)) + self.camera_resolution_x, + self.camera_resolution_y, + self.camera_fov)) bp.set_attribute('image_size_x', str(self.camera_resolution_x)) bp.set_attribute('image_size_y', str(self.camera_resolution_y)) bp.set_attribute('fov', str(self.camera_fov)) @@ -165,7 +166,7 @@ def run(self): self.world = client.get_world() try: - r = rospy.Rate(10) # 10hz + r = rospy.Rate(10) # 10hz while not rospy.is_shutdown(): self.find_ego_vehicle_actor() r.sleep() diff --git a/carla_twist_to_control/README.md b/carla_twist_to_control/README.md new file mode 100644 index 00000000..4fcfd7d0 --- /dev/null +++ b/carla_twist_to_control/README.md @@ -0,0 +1,17 @@ +# Twist to Vehicle Control conversion + +This node converts a [geometry_msgs.Twist](http://docs.ros.org/api/geometry_msgs/html/msg/Twist.html) to [carla_msgs.CarlaEgoVehicleControl](../carla_msgs/msg/CarlaEgoVehicleControl.msg) + + +## Subscriptions + +| Topic | Type | Description | +| ---------------------------------- | ------------------- | --------------------------- | +| `/carla//vehicle_info` | [carla_msgs.CarlaEgoVehicleInfo](../carla_msgs/msg/CarlaEgoVehicleInfo.msg) | Ego vehicle info, to receive max steering angle | +| `/carla//twist` | [geometry_msgs.Twist](http://docs.ros.org/api/geometry_msgs/html/msg/Twist.html) | Twist to convert | + +## Publications + +| Topic | Type | Description | +| ---------------------------------- | ------------------- | --------------------------- | +| `/carla//vehicle_control_cmd` | [carla_msgs.CarlaEgoVehicleControl](../carla_msgs/msg/CarlaEgoVehicleControl.msg) | Converted vehicle control command | diff --git a/carla_twist_to_control/src/carla_twist_to_control/carla_twist_to_control.py b/carla_twist_to_control/src/carla_twist_to_control/carla_twist_to_control.py index cb9bae89..dea8861a 100755 --- a/carla_twist_to_control/src/carla_twist_to_control/carla_twist_to_control.py +++ b/carla_twist_to_control/src/carla_twist_to_control/carla_twist_to_control.py @@ -14,7 +14,8 @@ from geometry_msgs.msg import Twist from carla_msgs.msg import CarlaEgoVehicleControl, CarlaEgoVehicleInfo -class TwistToVehicleControl(object): + +class TwistToVehicleControl(object): # pylint: disable=too-few-public-methods """ receive geometry_nav_msgs::Twist and publish carla_msgs::CarlaEgoVehicleControl @@ -30,11 +31,11 @@ def __init__(self, role_name): rospy.loginfo("Wait for vehicle info...") vehicle_info = rospy.wait_for_message("/carla/{}/vehicle_info".format(role_name), CarlaEgoVehicleInfo) - if not vehicle_info.wheels: + if not vehicle_info.wheels: # pylint: disable=no-member rospy.logerr("Cannot determine max steering angle: Vehicle has no wheels.") sys.exit(1) - - self.max_steering_angle = vehicle_info.wheels[0].max_steer_angle + + self.max_steering_angle = vehicle_info.wheels[0].max_steer_angle # pylint: disable=no-member if not self.max_steering_angle: rospy.logerr("Cannot determine max steering angle: Value is %s", self.max_steering_angle) @@ -53,30 +54,28 @@ def twist_received(self, twist): """ control = CarlaEgoVehicleControl() if twist == Twist(): - #stop + # stop control.throttle = 0. control.brake = 1. control.steer = 0. else: if twist.linear.x > 0: - control.throttle = min(TwistToVehicleControl.MAX_LON_ACCELERATION, twist.linear.x)/ \ - TwistToVehicleControl.MAX_LON_ACCELERATION + control.throttle = min(TwistToVehicleControl.MAX_LON_ACCELERATION, + twist.linear.x) / TwistToVehicleControl.MAX_LON_ACCELERATION else: control.reverse = True - control.throttle = max(-TwistToVehicleControl.MAX_LON_ACCELERATION, twist.linear.x)/ \ - -TwistToVehicleControl.MAX_LON_ACCELERATION + control.throttle = max(-TwistToVehicleControl.MAX_LON_ACCELERATION, + twist.linear.x) / -TwistToVehicleControl.MAX_LON_ACCELERATION if twist.angular.z > 0: - control.steer = -min(self.max_steering_angle, twist.angular.z)/ \ - self.max_steering_angle + control.steer = -min(self.max_steering_angle, twist.angular.z) / \ + self.max_steering_angle else: - control.steer = -max(-self.max_steering_angle, twist.angular.z)/ \ - self.max_steering_angle - #invert steer for reverse case - if control.reverse: - control.steer = -control.steer + control.steer = -max(-self.max_steering_angle, twist.angular.z) / \ + self.max_steering_angle self.pub.publish(control) + def main(): """ main function @@ -93,6 +92,6 @@ def main(): del twist_to_vehicle_control rospy.loginfo("Done") + if __name__ == "__main__": main() - diff --git a/carla_waypoint_publisher/README.md b/carla_waypoint_publisher/README.md index 7419a7ce..a2b756dc 100644 --- a/carla_waypoint_publisher/README.md +++ b/carla_waypoint_publisher/README.md @@ -13,7 +13,7 @@ Additionally, services are provided to query CARLA waypoints. As the waypoint publisher requires some Carla PythonAPI functionality that is not part of the python egg-file, you have to extend your PYTHONPATH. - export PYTHONPATH=$PYTHONPATH:/PythonAPI/carla-.egg:/PythonAPI/carla/ + export PYTHONPATH=$PYTHONPATH:/PythonAPI/carla/dist/carla-.egg:/PythonAPI/carla/ To run it: @@ -40,5 +40,5 @@ The calculated route is published: | Service | Description | Type | | ----------------------------------------------------------- | ----------- | -------------------------------------------------------------------- | -| `/carla_waypoint_publisher//get_waypoint` | Get the waypoint for a specific location | [carla_waypoint_types.GetWaypoint](../carla_waypoint_types/srv/GetWaypoint.msg) | -| `/carla_waypoint_publisher//get_actor_waypoint` | Get the waypoint for the ego vehicle | [carla_waypoint_types.GetActorWaypoint](../carla_waypoint_types/srv/GetActorWaypoint.msg) | +| `/carla_waypoint_publisher//get_waypoint` | Get the waypoint for a specific location | [carla_waypoint_types.GetWaypoint](../carla_waypoint_types/srv/GetWaypoint.srv) | +| `/carla_waypoint_publisher//get_actor_waypoint` | Get the waypoint for an actor id | [carla_waypoint_types.GetActorWaypoint](../carla_waypoint_types/srv/GetActorWaypoint.srv) | diff --git a/carla_waypoint_publisher/src/carla_waypoint_publisher/carla_waypoint_publisher.py b/carla_waypoint_publisher/src/carla_waypoint_publisher/carla_waypoint_publisher.py index 915bc708..95191278 100755 --- a/carla_waypoint_publisher/src/carla_waypoint_publisher/carla_waypoint_publisher.py +++ b/carla_waypoint_publisher/src/carla_waypoint_publisher/carla_waypoint_publisher.py @@ -110,13 +110,16 @@ def get_waypoint(self, req): #rospy.logwarn("Get waypoint {}".format(response.waypoint.pose.position)) return response - def get_actor_waypoint(self, _): + def get_actor_waypoint(self, req): """ Convenience method to get the waypoint for an actor """ + rospy.loginfo("get_actor_waypoint(): Get waypoint of actor {}".format(req.id)) + actor = self.world.get_actors().find(req.id) + response = GetActorWaypointResponse() - if self.ego_vehicle: - carla_waypoint = self.map.get_waypoint(self.ego_vehicle.get_location()) + if actor: + carla_waypoint = self.map.get_waypoint(actor.get_location()) response.waypoint.pose.position.x = carla_waypoint.transform.location.x response.waypoint.pose.position.y = -carla_waypoint.transform.location.y response.waypoint.pose.position.z = carla_waypoint.transform.location.z @@ -125,7 +128,7 @@ def get_actor_waypoint(self, _): response.waypoint.section_id = carla_waypoint.section_id response.waypoint.lane_id = carla_waypoint.lane_id else: - rospy.logwarn("get_actor_waypoint(): Ego vehicle not valid.") + rospy.logwarn("get_actor_waypoint(): Actor {} not valid.".format(req.id)) return response def on_goal(self, goal): diff --git a/carla_waypoint_types/srv/GetActorWaypoint.srv b/carla_waypoint_types/srv/GetActorWaypoint.srv index f7e7dad5..7b9100db 100644 --- a/carla_waypoint_types/srv/GetActorWaypoint.srv +++ b/carla_waypoint_types/srv/GetActorWaypoint.srv @@ -4,5 +4,6 @@ # This work is licensed under the terms of the MIT license. # For a copy, see . # +uint32 id --- carla_waypoint_types/CarlaWaypoint waypoint \ No newline at end of file diff --git a/check.sh b/check.sh index dcf47bb6..b16ec844 100755 --- a/check.sh +++ b/check.sh @@ -4,5 +4,10 @@ autopep8 carla_ackermann_control/src/carla_ackermann_control/*.py --in-place --m autopep8 carla_ego_vehicle/src/carla_ego_vehicle/*.py --in-place --max-line-length=100 autopep8 carla_waypoint_publisher/src/carla_waypoint_publisher/*.py --in-place --max-line-length=100 autopep8 rqt_carla_control/src/rqt_carla_control/*.py --in-place --max-line-length=100 +autopep8 carla_ad_agent/src/carla_ad_agent/*.py --in-place --max-line-length=100 +autopep8 carla_ros_scenario_runner/src/carla_ros_scenario_runner/*.py --in-place --max-line-length=100 +autopep8 carla_twist_to_control/src/carla_twist_to_control/*.py --in-place --max-line-length=100 +autopep8 carla_spectator_camera/src/carla_spectator_camera/*.py --in-place --max-line-length=100 +autopep8 carla_infrastructure/src/carla_infrastructure/*.py --in-place --max-line-length=100 -pylint --rcfile=.pylintrc carla_ackermann_control/src/carla_ackermann_control/ carla_ros_bridge/src/carla_ros_bridge/ carla_ego_vehicle/src/carla_ego_vehicle/ carla_waypoint_publisher/src/carla_waypoint_publisher/ rqt_carla_control/src/rqt_carla_control/ +pylint --rcfile=.pylintrc carla_ackermann_control/src/carla_ackermann_control/ carla_ros_bridge/src/carla_ros_bridge/ carla_ego_vehicle/src/carla_ego_vehicle/ carla_waypoint_publisher/src/carla_waypoint_publisher/ rqt_carla_control/src/rqt_carla_control/ carla_ad_agent/src/carla_ad_agent/ carla_ros_scenario_runner/src/carla_ros_scenario_runner/ carla_twist_to_control/src/carla_twist_to_control/ carla_spectator_camera/src/carla_spectator_camera/ carla_infrastructure/src/carla_infrastructure/ diff --git a/docs/images/rviz_carla_plugin.png b/docs/images/rviz_carla_plugin.png new file mode 100644 index 00000000..f7e8208d Binary files /dev/null and b/docs/images/rviz_carla_plugin.png differ diff --git a/rviz_carla_plugin/CMakeLists.txt b/rviz_carla_plugin/CMakeLists.txt new file mode 100644 index 00000000..ba6c616d --- /dev/null +++ b/rviz_carla_plugin/CMakeLists.txt @@ -0,0 +1,48 @@ +cmake_minimum_required(VERSION 2.8.3) +project(rviz_carla_plugin) + +set(CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_STANDARD 14) + +find_package(catkin REQUIRED COMPONENTS + rviz + carla_msgs + nav_msgs + carla_ros_scenario_runner_types +) +catkin_package() + +include_directories(${catkin_INCLUDE_DIRS}) +link_directories(${catkin_LIBRARY_DIRS}) + +set(CMAKE_AUTOMOC ON) + +message(STATUS "Using Qt5 based on the rviz_QT_VERSION: ${rviz_QT_VERSION}") +find_package(Qt5 ${rviz_QT_VERSION} EXACT REQUIRED Core Widgets) +set(QT_LIBRARIES Qt5::Widgets) + +set(SRC_FILES + src/drive_widget.cpp + src/indicator_widget.cpp + src/carla_control_panel.cpp +) + +qt5_add_resources(SRC_FILES rviz_carla_plugin.qrc) + +add_library(${PROJECT_NAME} ${SRC_FILES}) + +target_link_libraries(${PROJECT_NAME} ${QT_LIBRARIES} ${catkin_LIBRARIES}) + +install(TARGETS + ${PROJECT_NAME} + ARCHIVE DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION} + LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION} + RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} +) + +install(FILES + plugin_description.xml + DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}) + +install(DIRECTORY icons/ + DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}/icons) diff --git a/rviz_carla_plugin/README.md b/rviz_carla_plugin/README.md new file mode 100644 index 00000000..c1db4887 --- /dev/null +++ b/rviz_carla_plugin/README.md @@ -0,0 +1,64 @@ +# RVIZ CARLA Control + +A [RVIZ](http://wiki.ros.org/rviz) plugin to visualize/control CARLA. + +![CARLA AD Demo](../docs/images/rviz_carla_plugin.png "rviz carla plugin") + +This plugin is expecting a ego vehicle named `ego_vehicle`. + +## Features + +### Provide the RVIZ view pose to other nodes + +In combination with [carla_spectator_camera](../carla_spectator_camera), this allows visually moving around in the CARLA world. + +Currently, it is limited to a camera attached to the ego-vehicle. Please set the target frame of the "Current View" to `ego_vehicle`. + +### Visualize the current ego vehicle state + +The current vehicle state is visualized: + +- Vehicle Control +- Position + +### Allows manually overriding the ego vehicle vehicle control + +By using the drive-widget from the [RVIZ Visualization Tutorials](https://github.com/ros-visualization/visualization_tutorials) and a [node to convert from twist to vehicle control](../carla_twist_to_control) it is possible to steer the ego vehicle by mouse. + +### Execute a scenario + +By using [carla_ros_scenario_runner](../carla_ros_scenario_runner), it is possible to trigger scenarios from within RVIZ. + + +### Play/Pause the simulation (if started in synchronous mode) + +Similar to the [rqt CARLA plugin](../rqt_carla_plugin), it's possible to control the CARLA world, if synchronous mode is active. + + + +## Topics + +### Subscriptions + +| Topic | Type | Description | +| ------------------- | ------------------------- | ------------------------------------------------------ | +| `/carla/status` | [carla_msgs.CarlaStatus](../carla_msgs/msg/CarlaStatus.msg) | Read the status of CARLA, to enable/disable the UI | +| `/carla/ego_vehicle/vehicle_status` | [carla_msgs.CarlaEgoVehicleStatus](../carla_msgs/msg/CarlaEgoVehicleStatus.msg) | To display the current state of the ego vehicle | +| `/carla/ego_vehicle/odometry` | [nav_msgs.Odometry](http://docs.ros.org/api/nav_msgs/html/msg/Odometry.html) | To display the current pose of the ego vehicle | +| `/scenario_runner/status` | [carla_ros_scenario_runner_types.CarlaScenarioRunnerStatus](../carla_ros_scenario_runner_types/msg/CarlaScenarioRunnerStatus.msg) | To visualize the scenario runner status | +| `/carla/available_scenarios` | [carla_ros_scenario_runner_types.CarlaScenarioList](../carla_ros_scenario_runner_types/msg/CarlaScenarioList.msg) | For providing a list of scenarios to execute (disabled in combo box) | + +### Publications + +| Topic | Type | Description | +| ------------------- | ------------------------- | ------------------------------------------------------ | +| `/carla/control` | [carla_msgs.CarlaControl](../carla_msgs/msg/CarlaControl.msg) | Start/pause CARLA | +| `/carla/ego_vehicle/spectator_pose` | [geometry_msgs.PoseStamped](http://docs.ros.org/api/geometry_msgs/html/msg/PoseStamped.html) | Publish the current pose of the RVIZ camera view. | +| `/carla/ego_vehicle/vehicle_control_manual_override` | [std_msgs.Bool](http://docs.ros.org/api/std_msgs/html/msg/Bool.html) | To enable/disable the overriding of the vehicle control | +| `/carla/ego_vehicle/twist` | [geometry_msgs.Twist](http://docs.ros.org/api/geometry_msgs/html/msg/Twist.html) | The twist command, created via mouse | + +### Service Client + +| Service | Type | Description | +| ----------------------------------------------------------- | ---- | ------------------------------------------------------------------- | +| `/scenario_runner/execute_scenario` | [carla_ros_scenario_runner_types.ExecuteScenario](../carla_ros_scenario_runner_types/srv/ExecuteScenario.srv) | Execute the selected scenario | diff --git a/rviz_carla_plugin/icons/classes/CarlaControl.png b/rviz_carla_plugin/icons/classes/CarlaControl.png new file mode 100644 index 00000000..4b659faf Binary files /dev/null and b/rviz_carla_plugin/icons/classes/CarlaControl.png differ diff --git a/rviz_carla_plugin/icons/pause.png b/rviz_carla_plugin/icons/pause.png new file mode 100644 index 00000000..f486019f Binary files /dev/null and b/rviz_carla_plugin/icons/pause.png differ diff --git a/rviz_carla_plugin/icons/play.png b/rviz_carla_plugin/icons/play.png new file mode 100644 index 00000000..07078775 Binary files /dev/null and b/rviz_carla_plugin/icons/play.png differ diff --git a/rviz_carla_plugin/icons/step_once.png b/rviz_carla_plugin/icons/step_once.png new file mode 100644 index 00000000..7e85487f Binary files /dev/null and b/rviz_carla_plugin/icons/step_once.png differ diff --git a/rviz_carla_plugin/package.xml b/rviz_carla_plugin/package.xml new file mode 100644 index 00000000..c1a0d791 --- /dev/null +++ b/rviz_carla_plugin/package.xml @@ -0,0 +1,26 @@ + + + rviz_carla_plugin + 0.0.1 + The carla_ros_bridge package + CARLA Simulator Team + MIT + catkin + qtbase5-dev + rviz + carla_msgs + nav_msgs + geometry_msgs + carla_ros_scenario_runner_types + carla_msgs + nav_msgs + geometry_msgs + carla_ros_scenario_runner_types + libqt5-core + libqt5-gui + libqt5-widgets + rviz + + + + diff --git a/rviz_carla_plugin/plugin_description.xml b/rviz_carla_plugin/plugin_description.xml new file mode 100644 index 00000000..42f236a3 --- /dev/null +++ b/rviz_carla_plugin/plugin_description.xml @@ -0,0 +1,9 @@ + + + + A panel widget for controlling carla nodes + + + diff --git a/rviz_carla_plugin/rviz_carla_plugin.qrc b/rviz_carla_plugin/rviz_carla_plugin.qrc new file mode 100644 index 00000000..1ebe3370 --- /dev/null +++ b/rviz_carla_plugin/rviz_carla_plugin.qrc @@ -0,0 +1,7 @@ + + + icons/play.png + icons/pause.png + icons/step_once.png + + \ No newline at end of file diff --git a/rviz_carla_plugin/src/carla_control_panel.cpp b/rviz_carla_plugin/src/carla_control_panel.cpp new file mode 100644 index 00000000..ce436946 --- /dev/null +++ b/rviz_carla_plugin/src/carla_control_panel.cpp @@ -0,0 +1,411 @@ +/* + * Copyright (c) 2020 Intel Corporation + * + * This work is licensed under the terms of the MIT license. + * For a copy, see . + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include "carla_control_panel.h" +#include "drive_widget.h" +#include "indicator_widget.h" + +namespace rviz_carla_plugin { + +CarlaControlPanel::CarlaControlPanel(QWidget *parent) + : rviz::Panel(parent) +{ + QVBoxLayout *layout = new QVBoxLayout; + QHBoxLayout *vehicleLayout = new QHBoxLayout; + + QFormLayout *egoCtrlStatusLayout = new QFormLayout; + + mThrottleBar = new QProgressBar(); + mThrottleBar->setRange(0, 100); + egoCtrlStatusLayout->addRow("Throttle", mThrottleBar); + mBrakeBar = new QProgressBar(); + mBrakeBar->setRange(0, 100); + egoCtrlStatusLayout->addRow("Brake", mBrakeBar); + mSteerBar = new QProgressBar(); + mSteerBar->setRange(-100, 100); + egoCtrlStatusLayout->addRow("Steer", mSteerBar); + vehicleLayout->addLayout(egoCtrlStatusLayout); + + QFormLayout *egoStatusLayout = new QFormLayout; + mPosLabel = new QLineEdit(); + mPosLabel->setDisabled(true); + egoStatusLayout->addRow("Position", mPosLabel); + + mSpeedLabel = new QLineEdit(); + mSpeedLabel->setDisabled(true); + egoStatusLayout->addRow("Speed", mSpeedLabel); + + mHeadingLabel = new QLineEdit(); + mHeadingLabel->setDisabled(true); + egoStatusLayout->addRow("Heading", mHeadingLabel); + + vehicleLayout->addLayout(egoStatusLayout); + + QVBoxLayout *egoCtrlLayout = new QVBoxLayout; + mOverrideVehicleControl = new QCheckBox("Manual control"); + mOverrideVehicleControl->setDisabled(true); + egoCtrlLayout->addWidget(mOverrideVehicleControl); + mDriveWidget = new DriveWidget; + mDriveWidget->setDisabled(true); + egoCtrlLayout->addWidget(mDriveWidget); + connect(mOverrideVehicleControl, SIGNAL(stateChanged(int)), this, SLOT(overrideVehicleControl(int))); + + vehicleLayout->addLayout(egoCtrlLayout); + + layout->addLayout(vehicleLayout); + + QFormLayout *carlaLayout = new QFormLayout; + + // row1 + QHBoxLayout *carlaRow1Layout = new QHBoxLayout; + + mScenarioSelection = new QComboBox(); + carlaRow1Layout->addWidget(mScenarioSelection, 10); + + mTriggerScenarioButton = new QPushButton("Execute"); + carlaRow1Layout->addWidget(mTriggerScenarioButton); + + mIndicatorWidget = new IndicatorWidget(); + carlaRow1Layout->addWidget(mIndicatorWidget); + connect(mTriggerScenarioButton, SIGNAL(released()), this, SLOT(executeScenario())); + + carlaLayout->addRow("Scenario Execution", carlaRow1Layout); + + QHBoxLayout *synchronous_layout = new QHBoxLayout; + QPixmap pixmap(":/icons/play.png"); + QIcon iconPlay(pixmap); + mPlayPauseButton = new QPushButton(iconPlay, ""); + synchronous_layout->addWidget(mPlayPauseButton); + connect(mPlayPauseButton, SIGNAL(released()), this, SLOT(carlaTogglePlayPause())); + + QPixmap pixmapStepOnce(":/icons/step_once.png"); + QIcon iconStepOnce(pixmapStepOnce); + mStepOnceButton = new QPushButton(iconStepOnce, ""); + connect(mStepOnceButton, SIGNAL(released()), this, SLOT(carlaStepOnce())); + + synchronous_layout->addWidget(mStepOnceButton); + carlaLayout->addRow("Carla Control", synchronous_layout); + + layout->addLayout(carlaLayout); + + setLayout(layout); + + QTimer *outputTimer = new QTimer(this); + connect(outputTimer, SIGNAL(timeout()), this, SLOT(sendVel())); + outputTimer->start(100); + + connect(mDriveWidget, SIGNAL(outputVelocity(float, float)), this, SLOT(setVel(float, float))); + mDriveWidget->setEnabled(false); + + setSimulationButtonStatus(false); + setScenarioRunnerStatus(false); + + mCarlaStatusSubscriber = mNodeHandle.subscribe("/carla/status", 1000, &CarlaControlPanel::carlaStatusChanged, this); + mCarlaControlPublisher = mNodeHandle.advertise("/carla/control", 10); + mEgoVehicleStatusSubscriber = mNodeHandle.subscribe( + "/carla/ego_vehicle/vehicle_status", 1000, &CarlaControlPanel::egoVehicleStatusChanged, this); + mEgoVehicleOdometrySubscriber + = mNodeHandle.subscribe("/carla/ego_vehicle/odometry", 1000, &CarlaControlPanel::egoVehicleOdometryChanged, this); + + mCameraPosePublisher + = mNodeHandle.advertise("carla/ego_vehicle/spectator_pose", 10, true); + + mEgoVehicleControlManualOverridePublisher + = mNodeHandle.advertise("/carla/ego_vehicle/vehicle_control_manual_override", 1, true); + + mExecuteScenarioClient + = mNodeHandle.serviceClient("/scenario_runner/execute_scenario"); + mScenarioRunnerStatusSubscriber + = mNodeHandle.subscribe("/scenario_runner/status", 10, &CarlaControlPanel::scenarioRunnerStatusChanged, this); + + mTwistPublisher = mNodeHandle.advertise("/carla/ego_vehicle/twist", 1); + + mScenarioSubscriber + = mNodeHandle.subscribe("/carla/available_scenarios", 1, &CarlaControlPanel::carlaScenariosChanged, this); + + // //initially set the camera + QTimer::singleShot(1000, this, SLOT(updateCameraPos())); +} + +void CarlaControlPanel::cameraPreRenderScene(Ogre::Camera *cam) +{ + double epsilon = 0.001; + if ((std::fabs(cam->getPosition().x - mCameraCurrentPosition.x) > epsilon) + || (std::fabs(cam->getPosition().y - mCameraCurrentPosition.y) > epsilon) + || (std::fabs(cam->getPosition().z - mCameraCurrentPosition.z) > epsilon) + || (std::fabs(cam->getOrientation().x - mCameraCurrentOrientation.x) > epsilon) + || (std::fabs(cam->getOrientation().y - mCameraCurrentOrientation.y) > epsilon) + || (std::fabs(cam->getOrientation().z - mCameraCurrentOrientation.z) > epsilon) + || (std::fabs(cam->getOrientation().w - mCameraCurrentOrientation.w) > epsilon)) + { + mCameraCurrentPosition = cam->getPosition(); + mCameraCurrentOrientation = cam->getOrientation(); + QTimer::singleShot(0, this, SLOT(updateCameraPos())); + } +} + +void CarlaControlPanel::updateCameraPos() +{ + auto frame = mViewController->subProp("Target Frame")->getValue(); + + geometry_msgs::PoseStamped pose; + pose.header.frame_id = frame.toString().toStdString(); + pose.header.stamp = ros::Time::now(); + pose.pose.position.x = mCameraCurrentPosition.x; + pose.pose.position.y = mCameraCurrentPosition.y; + pose.pose.position.z = mCameraCurrentPosition.z; + pose.pose.orientation.x = mCameraCurrentOrientation.x; + pose.pose.orientation.y = mCameraCurrentOrientation.y; + pose.pose.orientation.z = mCameraCurrentOrientation.z; + pose.pose.orientation.w = mCameraCurrentOrientation.w; + + mCameraPosePublisher.publish(pose); +} + +void CarlaControlPanel::onInitialize() +{ + currentViewControllerChanged(); + connect(vis_manager_->getViewManager(), SIGNAL(currentChanged()), this, SLOT(currentViewControllerChanged())); +} + +void CarlaControlPanel::currentViewControllerChanged() +{ + mViewController + = dynamic_cast(vis_manager_->getViewManager()->getCurrent()); + if (!mViewController) + { + ROS_ERROR("Invalid view controller!"); + return; + } + + auto camera = mViewController->getCamera(); + camera->addListener(this); +} + +void CarlaControlPanel::executeScenario() +{ + for (auto const &scenario : mCarlaScenarios->scenarios) + { + if (QString::fromStdString(scenario.name) == mScenarioSelection->currentText()) + { + carla_ros_scenario_runner_types::ExecuteScenario srv; + srv.request.scenario = scenario; + if (!mExecuteScenarioClient.call(srv)) + { + ROS_ERROR("Failed to call service executeScenario"); + mIndicatorWidget->setState(IndicatorWidget::State::Error); + } + break; + } + } +} + +void CarlaControlPanel::overrideVehicleControl(int state) +{ + std_msgs::Bool boolMsg; + if (state == Qt::Checked) + { + boolMsg.data = true; + mDriveWidget->setEnabled(true); + } + else + { + boolMsg.data = false; + mDriveWidget->setEnabled(false); + } + mEgoVehicleControlManualOverridePublisher.publish(boolMsg); +} + +void CarlaControlPanel::scenarioRunnerStatusChanged( + const carla_ros_scenario_runner_types::CarlaScenarioRunnerStatus::ConstPtr &msg) +{ + if (msg->status == carla_ros_scenario_runner_types::CarlaScenarioRunnerStatus::STOPPED) + { + mIndicatorWidget->setState(IndicatorWidget::State::Stopped); + } + else if (msg->status == carla_ros_scenario_runner_types::CarlaScenarioRunnerStatus::STARTING) + { + mIndicatorWidget->setState(IndicatorWidget::State::Starting); + } + else if (msg->status == carla_ros_scenario_runner_types::CarlaScenarioRunnerStatus::RUNNING) + { + mIndicatorWidget->setState(IndicatorWidget::State::Running); + } + else if (msg->status == carla_ros_scenario_runner_types::CarlaScenarioRunnerStatus::SHUTTINGDOWN) + { + mIndicatorWidget->setState(IndicatorWidget::State::ShuttingDown); + } + else + { + mIndicatorWidget->setState(IndicatorWidget::State::Error); + } +} + +void CarlaControlPanel::setScenarioRunnerStatus(bool active) +{ + mScenarioSelection->setEnabled(active); + mTriggerScenarioButton->setEnabled(active); + mIndicatorWidget->setEnabled(active); +} + +void CarlaControlPanel::carlaScenariosChanged(const carla_ros_scenario_runner_types::CarlaScenarioList::ConstPtr &msg) +{ + auto currentSelection = mScenarioSelection->currentText(); + mCarlaScenarios = msg; + mScenarioSelection->clear(); + int idx = 0; + for (auto const &scenario : msg->scenarios) + { + auto name = QString::fromStdString(scenario.name); + mScenarioSelection->addItem(name); + if (name == currentSelection) + { // switch to previously selected item + mScenarioSelection->setCurrentIndex(idx); + } + ++idx; + } + setScenarioRunnerStatus(mScenarioSelection->count() > 0); +} + +void CarlaControlPanel::egoVehicleStatusChanged(const carla_msgs::CarlaEgoVehicleStatus::ConstPtr &msg) +{ + mOverrideVehicleControl->setEnabled(true); + mSteerBar->setValue(msg->control.steer * 100); + mThrottleBar->setValue(msg->control.throttle * 100); + mBrakeBar->setValue(msg->control.brake * 100); + + std::stringstream speedStream; + speedStream << std::fixed << std::setprecision(2) << msg->velocity * 3.6; + mSpeedLabel->setText(speedStream.str().c_str()); +} + +void CarlaControlPanel::egoVehicleOdometryChanged(const nav_msgs::Odometry::ConstPtr &msg) +{ + std::stringstream posStream; + posStream << std::fixed << std::setprecision(2) << msg->pose.pose.position.x << ", " << msg->pose.pose.position.y; + mPosLabel->setText(posStream.str().c_str()); + + std::stringstream headingStream; + headingStream << std::fixed << std::setprecision(2) << tf::getYaw(msg->pose.pose.orientation); + mHeadingLabel->setText(headingStream.str().c_str()); +} + +void CarlaControlPanel::setSimulationButtonStatus(bool active) +{ + if (active) + { + mPlayPauseButton->setDisabled(false); + mPlayPauseButton->setToolTip("Play/Pause the CARLA world."); + mStepOnceButton->setDisabled(false); + mStepOnceButton->setToolTip("Execute on tick within the CARLA world."); + } + else + { + mPlayPauseButton->setDisabled(true); + mPlayPauseButton->setToolTip("Disabled due to CARLA running in non-synchronous mode."); + mStepOnceButton->setDisabled(true); + mStepOnceButton->setToolTip("Disabled due to CARLA running in non-synchronous mode."); + } +} + +void CarlaControlPanel::carlaStatusChanged(const carla_msgs::CarlaStatus::ConstPtr &msg) +{ + mCarlaStatus = msg; + setSimulationButtonStatus(mCarlaStatus->synchronous_mode); + + if (mCarlaStatus->synchronous_mode) + { + if (mCarlaStatus->synchronous_mode_running) + { + QPixmap pixmap(":/icons/pause.png"); + QIcon iconPause(pixmap); + mPlayPauseButton->setIcon(iconPause); + } + else + { + QPixmap pixmap(":/icons/play.png"); + QIcon iconPlay(pixmap); + mPlayPauseButton->setIcon(iconPlay); + } + } +} + +void CarlaControlPanel::carlaStepOnce() +{ + carla_msgs::CarlaControl ctrl; + ctrl.command = carla_msgs::CarlaControl::STEP_ONCE; + mCarlaControlPublisher.publish(ctrl); +} + +void CarlaControlPanel::carlaTogglePlayPause() +{ + if (mCarlaStatus) + { + carla_msgs::CarlaControl ctrl; + if (mCarlaStatus->synchronous_mode_running) + { + ctrl.command = carla_msgs::CarlaControl::PAUSE; + } + else + { + ctrl.command = carla_msgs::CarlaControl::PLAY; + } + mCarlaControlPublisher.publish(ctrl); + } +} + +void CarlaControlPanel::setVel(float linearVelocity, float angularVelocity) +{ + mLinearVelocity = linearVelocity; + mAngularVelocity = angularVelocity; +} + +void CarlaControlPanel::sendVel() +{ + if (ros::ok() && mTwistPublisher && (mOverrideVehicleControl->checkState() == Qt::Checked)) + { + geometry_msgs::Twist msg; + msg.linear.x = mLinearVelocity; + msg.linear.y = 0; + msg.linear.z = 0; + msg.angular.x = 0; + msg.angular.y = 0; + msg.angular.z = mAngularVelocity; + mTwistPublisher.publish(msg); + } +} + +} // end namespace rviz_carla_plugin + +#include +PLUGINLIB_EXPORT_CLASS(rviz_carla_plugin::CarlaControlPanel, rviz::Panel) diff --git a/rviz_carla_plugin/src/carla_control_panel.h b/rviz_carla_plugin/src/carla_control_panel.h new file mode 100644 index 00000000..646b96dd --- /dev/null +++ b/rviz_carla_plugin/src/carla_control_panel.h @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2020 Intel Corporation + * + * This work is licensed under the terms of the MIT license. + * For a copy, see . + */ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +class QLineEdit; +class QPushButton; +class QProgressBar; +class QCheckBox; +class QComboBox; + +namespace rviz { +class ViewController; +class FramePositionTrackingViewController; +} + +namespace rviz_carla_plugin { + +class DriveWidget; +class IndicatorWidget; + +class CarlaControlPanel : public rviz::Panel, public Ogre::Camera::Listener +{ + Q_OBJECT +public: + CarlaControlPanel(QWidget *parent = 0); + +public Q_SLOTS: + void setVel(float linearVelocity, float angularVelocity); + +protected Q_SLOTS: + void sendVel(); + + void carlaStepOnce(); + void carlaTogglePlayPause(); + void overrideVehicleControl(int state); + void executeScenario(); + + void updateCameraPos(); + void currentViewControllerChanged(); + +protected: + virtual void cameraPreRenderScene(Ogre::Camera *cam) override; + + virtual void onInitialize() override; + void setSimulationButtonStatus(bool active); + void setScenarioRunnerStatus(bool active); + + void scenarioRunnerStatusChanged(const carla_ros_scenario_runner_types::CarlaScenarioRunnerStatus::ConstPtr &msg); + void carlaStatusChanged(const carla_msgs::CarlaStatus::ConstPtr &msg); + void egoVehicleStatusChanged(const carla_msgs::CarlaEgoVehicleStatus::ConstPtr &msg); + void egoVehicleOdometryChanged(const nav_msgs::Odometry::ConstPtr &msg); + void carlaScenariosChanged(const carla_ros_scenario_runner_types::CarlaScenarioList::ConstPtr &msg); + carla_msgs::CarlaStatus::ConstPtr mCarlaStatus{nullptr}; + + DriveWidget *mDriveWidget; + QPushButton *mTriggerScenarioButton; + QPushButton *mPlayPauseButton; + QPushButton *mStepOnceButton; + QProgressBar *mThrottleBar; + QProgressBar *mBrakeBar; + QProgressBar *mSteerBar; + QLineEdit *mPosLabel; + QLineEdit *mSpeedLabel; + QLineEdit *mHeadingLabel; + QCheckBox *mOverrideVehicleControl; + QComboBox *mScenarioSelection; + IndicatorWidget *mIndicatorWidget; + ros::Publisher mTwistPublisher; + ros::Publisher mCarlaControlPublisher; + ros::Publisher mEgoVehicleControlManualOverridePublisher; + ros::Subscriber mCarlaStatusSubscriber; + ros::Subscriber mEgoVehicleStatusSubscriber; + ros::Subscriber mEgoVehicleOdometrySubscriber; + ros::ServiceClient mExecuteScenarioClient; + ros::Subscriber mScenarioSubscriber; + ros::Subscriber mScenarioRunnerStatusSubscriber; + ros::Publisher mCameraPosePublisher; + + carla_ros_scenario_runner_types::CarlaScenarioList::ConstPtr mCarlaScenarios; + + ros::NodeHandle mNodeHandle; + + float mLinearVelocity{0.0}; + float mAngularVelocity{0.0}; + bool mVehicleControlManualOverride{false}; + rviz::FramePositionTrackingViewController *mViewController{nullptr}; + Ogre::Vector3 mCameraCurrentPosition; + Ogre::Quaternion mCameraCurrentOrientation; +}; + +} // end namespace rviz_carla_plugin diff --git a/rviz_carla_plugin/src/drive_widget.cpp b/rviz_carla_plugin/src/drive_widget.cpp new file mode 100644 index 00000000..2de48ac2 --- /dev/null +++ b/rviz_carla_plugin/src/drive_widget.cpp @@ -0,0 +1,249 @@ +/* + * Copyright (c) 2011, Willow Garage, Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the Willow Garage, Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include + +#include +#include + +#include "drive_widget.h" + +namespace rviz_carla_plugin { + +// BEGIN_TUTORIAL +// The DriveWidget constructor does the normal Qt thing of +// passing the parent widget to the superclass constructor, then +// initializing the member variables. +DriveWidget::DriveWidget(QWidget *parent) + : QWidget(parent) + , linear_velocity_(0) + , angular_velocity_(0) + , linear_scale_(10) + , angular_scale_(.2) +{ +} + +// This paintEvent() is complex because of the drawing of the two +// arc-arrows representing wheel motion. It is not particularly +// relevant to learning how to make an RViz plugin, so I will kind of +// skim it. +void DriveWidget::paintEvent(QPaintEvent *event) +{ + // The background color and crosshair lines are drawn differently + // depending on whether this widget is enabled or not. This gives a + // nice visual indication of whether the control is "live". + QColor background; + QColor crosshair; + if (isEnabled()) + { + background = Qt::white; + crosshair = Qt::black; + } + else + { + background = Qt::lightGray; + crosshair = Qt::darkGray; + } + + // The main visual is a square, centered in the widget's area. Here + // we compute the size of the square and the horizontal and vertical + // offsets of it. + int w = width(); + int h = height(); + int size = ((w > h) ? h : w) - 1; + int hpad = (w - size) / 2; + int vpad = (h - size) / 2; + + QPainter painter(this); + painter.setBrush(background); + painter.setPen(crosshair); + + // Draw the background square. + painter.drawRect(QRect(hpad, vpad, size, size)); + + // Draw a cross-hair inside the square. + painter.drawLine(hpad, height() / 2, hpad + size, height() / 2); + painter.drawLine(width() / 2, vpad, width() / 2, vpad + size); + + // If the widget is enabled and the velocities are not zero, draw + // some sweet green arrows showing possible paths that the wheels of + // a diff-drive robot would take if it stayed at these velocities. + if (isEnabled() && (angular_velocity_ != 0 || linear_velocity_ != 0)) + { + QPen arrow; + arrow.setWidth(size / 20); + arrow.setColor(Qt::green); + arrow.setCapStyle(Qt::RoundCap); + arrow.setJoinStyle(Qt::RoundJoin); + painter.setPen(arrow); + + // This code steps along a central arc defined by the linear and + // angular velocites. At each step, it computes where the left + // and right wheels would be and collects the resulting points + // in the left_track and right_track arrays. + const int step_count = 100; + QPointF left_track[step_count]; + QPointF right_track[step_count]; + + float half_track_width = size / 4.0; + + float cx = w / 2; + float cy = h / 2; + left_track[0].setX(cx - half_track_width); + left_track[0].setY(cy); + right_track[0].setX(cx + half_track_width); + right_track[0].setY(cy); + float angle = M_PI / 2; + float delta_angle = angular_velocity_ / step_count; + float step_dist = linear_velocity_ * size / 2 / linear_scale_ / step_count; + for (int step = 1; step < step_count; step++) + { + angle += delta_angle / 2; + float next_cx = cx + step_dist * cosf(angle); + float next_cy = cy - step_dist * sinf(angle); + angle += delta_angle / 2; + + left_track[step].setX(next_cx + half_track_width * cosf(angle + M_PI / 2)); + left_track[step].setY(next_cy - half_track_width * sinf(angle + M_PI / 2)); + right_track[step].setX(next_cx + half_track_width * cosf(angle - M_PI / 2)); + right_track[step].setY(next_cy - half_track_width * sinf(angle - M_PI / 2)); + + cx = next_cx; + cy = next_cy; + } + // Now the track arrays are filled, so stroke each with a fat green line. + painter.drawPolyline(left_track, step_count); + painter.drawPolyline(right_track, step_count); + + // Here we decide which direction each arrowhead will point + // (forward or backward). This works by comparing the arc length + // travelled by the center in one step (step_dist) with the arc + // length travelled by the wheel (half_track_width * delta_angle). + int left_arrow_dir = (-step_dist + half_track_width * delta_angle > 0); + int right_arrow_dir = (-step_dist - half_track_width * delta_angle > 0); + + // Use MiterJoin for the arrowheads so we get a nice sharp point. + arrow.setJoinStyle(Qt::MiterJoin); + painter.setPen(arrow); + + // Compute and draw polylines for each arrowhead. This code could + // probably be more elegant. + float head_len = size / 8.0; + QPointF arrow_head[3]; + float x, y; + if (fabsf(-step_dist + half_track_width * delta_angle) > .01) + { + x = left_track[step_count - 1].x(); + y = left_track[step_count - 1].y(); + arrow_head[0].setX(x + head_len * cosf(angle + 3 * M_PI / 4 + left_arrow_dir * M_PI)); + arrow_head[0].setY(y - head_len * sinf(angle + 3 * M_PI / 4 + left_arrow_dir * M_PI)); + arrow_head[1].setX(x); + arrow_head[1].setY(y); + arrow_head[2].setX(x + head_len * cosf(angle - 3 * M_PI / 4 + left_arrow_dir * M_PI)); + arrow_head[2].setY(y - head_len * sinf(angle - 3 * M_PI / 4 + left_arrow_dir * M_PI)); + painter.drawPolyline(arrow_head, 3); + } + if (fabsf(-step_dist - half_track_width * delta_angle) > .01) + { + x = right_track[step_count - 1].x(); + y = right_track[step_count - 1].y(); + arrow_head[0].setX(x + head_len * cosf(angle + 3 * M_PI / 4 + right_arrow_dir * M_PI)); + arrow_head[0].setY(y - head_len * sinf(angle + 3 * M_PI / 4 + right_arrow_dir * M_PI)); + arrow_head[1].setX(x); + arrow_head[1].setY(y); + arrow_head[2].setX(x + head_len * cosf(angle - 3 * M_PI / 4 + right_arrow_dir * M_PI)); + arrow_head[2].setY(y - head_len * sinf(angle - 3 * M_PI / 4 + right_arrow_dir * M_PI)); + painter.drawPolyline(arrow_head, 3); + } + } +} + +// Every mouse move event received here sends a velocity because Qt +// only sends us mouse move events if there was previously a +// mouse-press event while in the widget. +void DriveWidget::mouseMoveEvent(QMouseEvent *event) +{ + sendVelocitiesFromMouse(event->x(), event->y(), width(), height()); +} + +// Mouse-press events should send the velocities too, of course. +void DriveWidget::mousePressEvent(QMouseEvent *event) +{ + sendVelocitiesFromMouse(event->x(), event->y(), width(), height()); +} + +// When the mouse leaves the widget but the button is still held down, +// we don't get the leaveEvent() because the mouse is "grabbed" (by +// default from Qt). However, when the mouse drags out of the widget +// and then other buttons are pressed (or possibly other +// window-manager things happen), we will get a leaveEvent() but not a +// mouseReleaseEvent(). Without catching this event you can have a +// robot stuck "on" without the user controlling it. +void DriveWidget::leaveEvent(QEvent *event) +{ + stop(); +} + +// The ordinary way to stop: let go of the mouse button. +void DriveWidget::mouseReleaseEvent(QMouseEvent *event) +{ + stop(); +} + +// Compute and emit linear and angular velocities based on Y and X +// mouse positions relative to the central square. +void DriveWidget::sendVelocitiesFromMouse(int x, int y, int width, int height) +{ + int size = ((width > height) ? height : width); + int hpad = (width - size) / 2; + int vpad = (height - size) / 2; + + linear_velocity_ = (1.0 - float(y - vpad) / float(size / 2)) * linear_scale_; + angular_velocity_ = (1.0 - float(x - hpad) / float(size / 2)) * angular_scale_; + Q_EMIT outputVelocity(linear_velocity_, angular_velocity_); + + // update() is a QWidget function which schedules this widget to be + // repainted the next time through the main event loop. We need + // this because the velocities have just changed, so the arrows need + // to be redrawn to match. + update(); +} + +// How to stop: emit velocities of 0! +void DriveWidget::stop() +{ + linear_velocity_ = 0; + angular_velocity_ = 0; + Q_EMIT outputVelocity(linear_velocity_, angular_velocity_); + update(); +} +// END_TUTORIAL + +} // end namespace rviz_carla_plugin diff --git a/rviz_carla_plugin/src/drive_widget.h b/rviz_carla_plugin/src/drive_widget.h new file mode 100644 index 00000000..e0a6c7d0 --- /dev/null +++ b/rviz_carla_plugin/src/drive_widget.h @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2011, Willow Garage, Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the Willow Garage, Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +#ifndef DRIVE_WIDGET_H +#define DRIVE_WIDGET_H + +#include + +namespace rviz_carla_plugin { + +// BEGIN_TUTORIAL +// DriveWidget implements a control which translates mouse Y values +// into linear velocities and mouse X values into angular velocities. +// +// For maximum reusability, this class is only responsible for user +// interaction and display inside its widget. It does not make any +// ROS or RViz calls. It communicates its data to the outside just +// via Qt signals. +class DriveWidget : public QWidget +{ + Q_OBJECT +public: + // This class is not instantiated by pluginlib::ClassLoader, so the + // constructor has no restrictions. + DriveWidget(QWidget *parent = 0); + + // We override QWidget::paintEvent() to do custom painting. + virtual void paintEvent(QPaintEvent *event); + + // We override the mouse events and leaveEvent() to keep track of + // what the mouse is doing. + virtual void mouseMoveEvent(QMouseEvent *event); + virtual void mousePressEvent(QMouseEvent *event); + virtual void mouseReleaseEvent(QMouseEvent *event); + virtual void leaveEvent(QEvent *event); + + // Override sizeHint() to give the layout managers some idea of a + // good size for this. + virtual QSize sizeHint() const + { + return QSize(150, 150); + } + + // We emit outputVelocity() whenever it changes. +Q_SIGNALS: + void outputVelocity(float linear, float angular); + + // mouseMoveEvent() and mousePressEvent() need the same math to + // figure the velocities, so I put that in here. +protected: + void sendVelocitiesFromMouse(int x, int y, int width, int height); + + // A function to emit zero velocity. + void stop(); + + // Finally the member variables: + float linear_velocity_; // In m/s + float angular_velocity_; // In radians/s + float linear_scale_; // In m/s + float angular_scale_; // In radians/s +}; +// END_TUTORIAL + +} // end namespace rviz_carla_plugin + +#endif // DRIVE_WIDGET_H diff --git a/rviz_carla_plugin/src/indicator_widget.cpp b/rviz_carla_plugin/src/indicator_widget.cpp new file mode 100644 index 00000000..ae160a42 --- /dev/null +++ b/rviz_carla_plugin/src/indicator_widget.cpp @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2020 Intel Corporation + * + * This work is licensed under the terms of the MIT license. + * For a copy, see . + */ +#include "indicator_widget.h" +#include + +namespace rviz_carla_plugin { + +IndicatorWidget::IndicatorWidget(QWidget *parent) + : QWidget(parent) + , mCurrentState(IndicatorWidget::State::Stopped) +{ + setFixedSize(18, 18); +} + +void IndicatorWidget::paintEvent(QPaintEvent *event) +{ + QPainter painter(this); + painter.setPen(Qt::darkGray); + if (mCurrentState == IndicatorWidget::State::Stopped) + { + painter.setBrush(QBrush(Qt::lightGray, Qt::SolidPattern)); + } + else if (mCurrentState == IndicatorWidget::State::Running) + { + painter.setBrush(QBrush(Qt::green, Qt::SolidPattern)); + } + else if (mCurrentState == IndicatorWidget::State::Error) + { + painter.setBrush(QBrush(Qt::red, Qt::SolidPattern)); + } + else if ((mCurrentState == IndicatorWidget::State::Starting) + || (mCurrentState == IndicatorWidget::State::ShuttingDown)) + { + painter.setBrush(QBrush(Qt::yellow, Qt::SolidPattern)); + } + else + { + painter.setBrush(QBrush(Qt::black, Qt::SolidPattern)); + } + painter.drawEllipse(1, 1, 16, 16); +} + +void IndicatorWidget::setState(IndicatorWidget::State state) +{ + mCurrentState = state; + repaint(); +} + +} // end namespace rviz_carla_plugin diff --git a/rviz_carla_plugin/src/indicator_widget.h b/rviz_carla_plugin/src/indicator_widget.h new file mode 100644 index 00000000..2d5ea19f --- /dev/null +++ b/rviz_carla_plugin/src/indicator_widget.h @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2020 Intel Corporation + * + * This work is licensed under the terms of the MIT license. + * For a copy, see . + */ +#pragma once + +#include + +namespace rviz_carla_plugin { + +class IndicatorWidget : public QWidget +{ + Q_OBJECT +public: + enum class State + { + Stopped, + Starting, + Running, + ShuttingDown, + Error + }; + + IndicatorWidget(QWidget *parent = 0); + + virtual void paintEvent(QPaintEvent *event) override; + + void setState(IndicatorWidget::State state); + +private: + State mCurrentState{State::Stopped}; +}; + +} // end namespace rviz_carla_plugin