Skip to content

Commit

Permalink
Add Python plugin support to webots_ros2_driver (#265)
Browse files Browse the repository at this point in the history
* Add Python <--> C++ test

* Prepare

* Initial prototype

* Add WebotsNode object

* Add properties

* Import the Python lib

* Improve error handling

* Cleaner

* Add node example

* Integrate the new libController

* Delete the test

* Fix copyright

* Explain

* Copyright fix

* Constructor can be private

* Print Python errors

* Fix Windows usage

* Update webots_ros2_driver/src/PythonPlugin.cpp

Co-authored-by: Olivier Michel <[email protected]>

* Update webots_ros2_driver/src/WebotsNode.cpp

Co-authored-by: Olivier Michel <[email protected]>

* Update webots_ros2_turtlebot/webots_ros2_turtlebot/plugin_example.py

Co-authored-by: Olivier Michel <[email protected]>

* Use const

Co-authored-by: Olivier Michel <[email protected]>
  • Loading branch information
lukicdarkoo and omichel authored Aug 13, 2021
1 parent c1a1f5c commit b2e7635
Show file tree
Hide file tree
Showing 12 changed files with 235 additions and 16 deletions.
41 changes: 32 additions & 9 deletions webots_ros2_driver/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)

# ROS2 Packages
find_package(ament_cmake REQUIRED)
find_package(ament_cmake_python REQUIRED)
find_package(rosgraph_msgs REQUIRED)
find_package(rclcpp REQUIRED)
find_package(pluginlib REQUIRED)
Expand All @@ -17,6 +18,7 @@ find_package(vision_msgs REQUIRED)
find_package(webots_ros2_msgs REQUIRED)
find_package(tinyxml2_vendor REQUIRED)
find_package(TinyXML2 REQUIRED)
find_package(PythonLibs 3.8 EXACT REQUIRED)

if (UNIX AND NOT APPLE)
set(WEBOTS_LIB_BASE webots/lib/linux-gnu)
Expand All @@ -32,6 +34,7 @@ include_directories(
include
webots/include/c
webots/include/cpp
${PYTHON_INCLUDE_DIRS}
)
link_directories(${WEBOTS_LIB_BASE})

Expand All @@ -42,31 +45,35 @@ if (MSVC OR MSYS OR MINGW OR WIN32)
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
file(GLOB CppController_SRC CONFIGURE_DEPENDS "webots/source/cpp/*.cpp")
add_library(
CppController
CppControllerRos
SHARED
${CppController_SRC}
)
target_link_libraries(CppController
target_link_libraries(CppControllerRos
${CMAKE_SHARED_LIBRARY_PREFIX}Controller${CMAKE_SHARED_LIBRARY_SUFFIX}
)
install(TARGETS CppController
install(TARGETS CppControllerRos
LIBRARY DESTINATION lib
)
set(WEBOTS_LIB
${CMAKE_SHARED_LIBRARY_PREFIX}Controller${CMAKE_SHARED_LIBRARY_SUFFIX}
CppController
CppControllerRos
)
ament_export_libraries(CppController)
ament_export_libraries(CppControllerRos)
else()
set(WEBOTS_LIB
${CMAKE_SHARED_LIBRARY_PREFIX}Controller${CMAKE_SHARED_LIBRARY_SUFFIX}
${CMAKE_SHARED_LIBRARY_PREFIX}CppController${CMAKE_SHARED_LIBRARY_SUFFIX}
)
endif()

ament_python_install_package(${PROJECT_NAME}/webots
PACKAGE_DIR ${WEBOTS_LIB_BASE}/python38)

add_executable(driver
src/Driver.cpp
src/WebotsNode.cpp
src/PythonPlugin.cpp
src/plugins/Ros2SensorPlugin.cpp
src/plugins/static/Ros2Lidar.cpp
src/plugins/static/Ros2LED.cpp
Expand Down Expand Up @@ -95,6 +102,7 @@ ament_target_dependencies(driver
)
target_link_libraries(driver
${WEBOTS_LIB}
${PYTHON_LIBRARIES}
)
install(
DIRECTORY include/
Expand Down Expand Up @@ -140,16 +148,30 @@ install(TARGETS ${PROJECT_NAME}_imu
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
)
install(
DIRECTORY ${WEBOTS_LIB_BASE}/
DESTINATION lib/
)
if (MSVC OR MSYS OR MINGW OR WIN32)
# Windows requires the library to be placed with executable
install(
FILES "${WEBOTS_LIB_BASE}/${CMAKE_SHARED_LIBRARY_PREFIX}Controller${CMAKE_SHARED_LIBRARY_SUFFIX}"
DESTINATION lib/${PROJECT_NAME}
)

# Windows requires the C++ library to be placed with the Python module
install(
FILES "${WEBOTS_LIB_BASE}/${CMAKE_SHARED_LIBRARY_PREFIX}CppController${CMAKE_SHARED_LIBRARY_SUFFIX}"
DESTINATION "${PYTHON_INSTALL_DIR}/${PROJECT_NAME}/webots/"
)
else()
install(
DIRECTORY ${WEBOTS_LIB_BASE}/
DESTINATION lib
FILES_MATCHING
PATTERN "*Controller*"
PATTERN "*CppController*"
PATTERN "*car*"
PATTERN "*CppCar*"
PATTERN "*driver*"
PATTERN "*CppDriver*"
)
endif()

# Prevent pluginlib from using boost
Expand All @@ -162,6 +184,7 @@ ament_export_include_directories(include)
ament_export_dependencies(
rclcpp
rclcpp_lifecycle
rclpy
sensor_msgs
std_msgs
tf2_ros
Expand Down
42 changes: 42 additions & 0 deletions webots_ros2_driver/include/webots_ros2_driver/PythonPlugin.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright 1996-2021 Cyberbotics Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#ifndef PYTHON_PLUGIN_HPP
#define PYTHON_PLUGIN_HPP

#include <unordered_map>
#include <Python.h>

#include <webots_ros2_driver/PluginInterface.hpp>
#include <webots_ros2_driver/WebotsNode.hpp>

namespace webots_ros2_driver
{
class PythonPlugin : public PluginInterface
{
public:
void init(webots_ros2_driver::WebotsNode *node, std::unordered_map<std::string, std::string> &parameters) override;
void step() override;

static std::shared_ptr<PythonPlugin> createFromType(const std::string &type);

private:
PythonPlugin(PyObject *pyPlugin);

PyObject *mPyPlugin;
PyObject *getPyWebotsNodeInstance();
};
}

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ namespace webots_ros2_driver
pluginlib::ClassLoader<PluginInterface> mPluginLoader;
tinyxml2::XMLElement *mWebotsXMLElement;
std::shared_ptr<tinyxml2::XMLDocument> mRobotDescriptionDocument;
std::shared_ptr<PluginInterface> loadPlugin(const std::string &type);

rclcpp::Publisher<rosgraph_msgs::msg::Clock>::SharedPtr mClockPublisher;
rosgraph_msgs::msg::Clock mClockMessage;
Expand Down
4 changes: 4 additions & 0 deletions webots_ros2_driver/package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@
<url type="repository">https://github.com/cyberbotics/webots_ros2</url>

<buildtool_depend>ament_cmake</buildtool_depend>
<buildtool_depend>python_cmake_module</buildtool_depend>
<buildtool_depend>ament_cmake_python</buildtool_depend>

<depend>pluginlib</depend>
<depend>rclcpp</depend>
<depend>rclpy</depend>
<depend>rclcpp_lifecycle</depend>
<depend>sensor_msgs</depend>
<depend>std_msgs</depend>
Expand Down
76 changes: 76 additions & 0 deletions webots_ros2_driver/src/PythonPlugin.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#include "webots_ros2_driver/PythonPlugin.hpp"

static PyObject *gPyWebotsNode = NULL;

namespace webots_ros2_driver
{
PythonPlugin::PythonPlugin(PyObject *pyPlugin) : mPyPlugin(pyPlugin){};

void PythonPlugin::init(webots_ros2_driver::WebotsNode *node, std::unordered_map<std::string, std::string> &parameters)
{
PyObject *pyParameters = PyDict_New();
for (const std::pair<std::string, std::string> &parameter : parameters)
PyDict_SetItem(pyParameters, PyUnicode_FromString(parameter.first.c_str()), PyUnicode_FromString(parameter.second.c_str()));

PyObject_CallMethod(mPyPlugin, "init", "OO", getPyWebotsNodeInstance(), pyParameters);
PyErr_Print();
}

void PythonPlugin::step()
{
PyObject_CallMethod(mPyPlugin, "step", "");
PyErr_Print();
}

PyObject *PythonPlugin::getPyWebotsNodeInstance()
{
if (gPyWebotsNode)
return gPyWebotsNode;

PyObject *pyWebotsExtraModuleSource = Py_CompileString(
R"EOT(
from webots_ros2_driver.webots.controller import Supervisor
class WebotsNode:
def __init__(self):
self.robot = Supervisor.internalGetInstance()
)EOT",
"webots_extra", Py_file_input);
if (!pyWebotsExtraModuleSource)
throw std::runtime_error("Error: The Python module with the WebotsNode class cannot be compiled.");

PyObject *pyWebotsExtraModule = PyImport_ExecCodeModule("webots_extra", pyWebotsExtraModuleSource);
if (!pyWebotsExtraModule)
throw std::runtime_error("Error: The Python module with the WebotsNode class cannot be executed.");

PyObject *pyDict = PyModule_GetDict(pyWebotsExtraModule);
PyObject *pyClass = PyDict_GetItemString(pyDict, "WebotsNode");
gPyWebotsNode = PyObject_CallObject(pyClass, nullptr);
return gPyWebotsNode;
}

std::shared_ptr<PythonPlugin> PythonPlugin::createFromType(const std::string &type)
{
const std::string moduleName = type.substr(0, type.find_last_of("."));
const std::string className = type.substr(type.find_last_of(".") + 1);

Py_Initialize();

PyObject *pyName = PyUnicode_FromString(moduleName.c_str());
PyObject *pyModule = PyImport_Import(pyName);
PyErr_Print();

// If the module cannot be found the error should be handled in the upper level (e.g. try loading C++ plugin)
if (!pyModule)
return NULL;

PyObject *pyDict = PyModule_GetDict(pyModule);
PyObject *pyClass = PyDict_GetItemString(pyDict, className.c_str());
PyErr_Print();
if (!pyClass)
throw std::runtime_error("Error in plugin " + type + ": The class " + className + " cannot be found.");

PyObject *pyPlugin = PyObject_CallObject(pyClass, nullptr);
return std::shared_ptr<PythonPlugin>(new PythonPlugin(pyPlugin));
}
}
31 changes: 28 additions & 3 deletions webots_ros2_driver/src/WebotsNode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@
#include <webots_ros2_driver/plugins/static/Ros2LED.hpp>

#include "webots_ros2_driver/PluginInterface.hpp"
#include "webots_ros2_driver/PythonPlugin.hpp"

namespace webots_ros2_driver
{
const char *gDeviceRefferenceAttribute = "reference";
const char *gDeviceReferenceAttribute = "reference";
const char *gDeviceRosTag = "ros";
const char *gPluginInterface = "webots_ros2_driver::PluginInterface";
const char *gPluginInterfaceName = "webots_ros2_driver";
Expand Down Expand Up @@ -88,7 +89,7 @@ namespace webots_ros2_driver
tinyxml2::XMLElement *deviceChild = mWebotsXMLElement->FirstChildElement();
while (deviceChild)
{
if (deviceChild->Attribute(gDeviceRefferenceAttribute) && deviceChild->Attribute(gDeviceRefferenceAttribute) == name)
if (deviceChild->Attribute(gDeviceReferenceAttribute) && deviceChild->Attribute(gDeviceReferenceAttribute) == name)
break;
deviceChild = deviceChild->NextSiblingElement();
}
Expand Down Expand Up @@ -177,7 +178,7 @@ namespace webots_ros2_driver

const std::string type = pluginElement->Attribute("type");

std::shared_ptr<PluginInterface> plugin(mPluginLoader.createUnmanagedInstance(type));
std::shared_ptr<PluginInterface> plugin = loadPlugin(type);
std::unordered_map<std::string, std::string> pluginProperties = getPluginProperties(pluginElement);
plugin->init(this, pluginProperties);
mPlugins.push_back(plugin);
Expand All @@ -186,6 +187,30 @@ namespace webots_ros2_driver
}
}

std::shared_ptr<PluginInterface> WebotsNode::loadPlugin(const std::string &type)
{
// First, we assume the plugin is C++
try
{
std::shared_ptr<PluginInterface> plugin(mPluginLoader.createUnmanagedInstance(type));
return plugin;
}
catch (const pluginlib::LibraryLoadException &e)
{
// It may be a Python plugin
}
catch (const pluginlib::CreateClassException &e)
{
throw std::runtime_error("The " + type + " class cannot be initialized.");
}

std::shared_ptr<PluginInterface> plugin = PythonPlugin::createFromType(type);
if (plugin == NULL)
throw std::runtime_error("The " + type + " plugin cannot be found (C++ or Python).");

return plugin;
}

void WebotsNode::timerCallback()
{
mRobot->step(mStep);
Expand Down
2 changes: 1 addition & 1 deletion webots_ros2_driver/webots
Submodule webots updated 148 files
5 changes: 5 additions & 0 deletions webots_ros2_turtlebot/resource/turtlebot_webots.urdf
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
</plugin>

<plugin type="webots_ros2_control::Ros2Control" />

<!-- type="package_name.file_name.class_name" -->
<plugin type="webots_ros2_turtlebot.plugin_example.PluginExample">
<parameterExample>someValue</parameterExample>
</plugin>
</webots>

<ros2_control name="WebotsControl" type="system">
Expand Down
2 changes: 1 addition & 1 deletion webots_ros2_turtlebot/test/test_copyright.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2015 Open Source Robotics Foundation, Inc.
# Copyright 1996-2021 Cyberbotics Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down
2 changes: 1 addition & 1 deletion webots_ros2_turtlebot/test/test_flake8.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2017 Open Source Robotics Foundation, Inc.
# Copyright 1996-2021 Cyberbotics Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down
2 changes: 1 addition & 1 deletion webots_ros2_turtlebot/test/test_pep257.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2015 Open Source Robotics Foundation, Inc.
# Copyright 1996-2021 Cyberbotics Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down
43 changes: 43 additions & 0 deletions webots_ros2_turtlebot/webots_ros2_turtlebot/plugin_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Copyright 1996-2021 Cyberbotics Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""A simple dummy plugin that demonstrates the usage of Python plugins."""

from webots_ros2_driver.webots.controller import Node
from std_msgs.msg import Float32
import rclpy
import rclpy.node


class PluginExample:
def init(self, webots_node, properties):
print('PluginExample: The init() method is called')
print(' - properties:', properties)

print(' - basic timestep:', int(webots_node.robot.getBasicTimeStep()))
print(' - robot name:', webots_node.robot.getName())
print(' - is robot?', webots_node.robot.getType() == Node.ROBOT)

self.__robot = webots_node.robot

# Unfortunately, we cannot get an instance of the parent ROS node.
# However, we can create a new one.
rclpy.init(args=None)
self.__node = rclpy.node.Node('plugin_node_example')
print('PluginExample: Node created')
self.__publisher = self.__node.create_publisher(Float32, 'custom_time', 1)
print('PluginExample: Publisher created')

def step(self):
self.__publisher.publish(Float32(data=self.__robot.getTime()))

0 comments on commit b2e7635

Please sign in to comment.