diff --git a/docs/pages/Ros2-Singleton.rst b/docs/pages/Ros2-Singleton.rst index 8214313..a1c2e8d 100644 --- a/docs/pages/Ros2-Singleton.rst +++ b/docs/pages/Ros2-Singleton.rst @@ -27,18 +27,31 @@ As described in the API documentation for :cpp:func:`Ros2.init `` +to make it compatible with QML. +Also check out the ``graph_queries.qml`` example in the repo's examples folder. + +Additionally, for topics three convenience methods are also provided: * | ``QStringList queryTopics( const QString &datatype = QString())`` | Queries a list of topics with the given datatype or all topics if no type provided. * | ``QList queryTopicInfo()`` - | Retrieves a list of all advertised topics including their datatype. See :cpp:class:`TopicInfo` -* | ``QString queryTopicType( const QString &name )`` - | Retrieves the datatype for a given topic. + | Retrieves a list of all advertised topics including their datatypes. See :cpp:class:`TopicInfo` +* | ``QString queryTopicTypes( const QString &name )`` + | Retrieves the datatypes for a given topic. Example: @@ -50,15 +63,15 @@ Example: var cameraTopics = [] var topics = Ros2.queryTopicInfo() for (var i = 0; i < topics.length; ++i) { - if (topics[i].datatype == "sensor_msgs/msg/Image") cameraTopics.push(topics[i].name) + if (topics[i].datatypes.includes("sensor_msgs/msg/Image")) cameraTopics.push(topics[i].name) } - // The type of a specific topic can be retrieved as follows - var datatype = Ros2.queryTopicType("/topic/that/i/care/about") + // The types of a specific topic can be retrieved as follows + var datatypes = Ros2.queryTopicTypes("/topic/that/i/care/about") // Using this we can make an even worse implementation of the same functionality var cameraTopics = [] var topics = Ros2.queryTopics() // Gets all topics for (var i = 0; i < topics.length; ++i) { - if (Ros2.queryTopicType(topics[i]) == "sensor_msgs/msg/Image") cameraTopics.push(topics[i]) + if (Ros2.queryTopicTypes(topics[i]).includes("sensor_msgs/msg/Image")) cameraTopics.push(topics[i]) } Create Empty Message diff --git a/examples/graph_queries.qml b/examples/graph_queries.qml new file mode 100644 index 0000000..583577a --- /dev/null +++ b/examples/graph_queries.qml @@ -0,0 +1,82 @@ +import QtQuick 2.6 +import QtQuick.Controls 2.2 +import QtQuick.Controls.Material 2.2 +import QtQuick.Layouts 1.0 +import Ros2 1.0 + +ApplicationWindow { + id: page + width: 620 + height: 400 + + // This connection makes sure the application exits if this ROS node is requested to shutdown + Connections { + target: Ros2 + function onShutdown() { + Qt.quit() + } + } + property var topics: ({}) + property var services: ({}) + property var actions: ({}) + + function update() { + Ros2.info("Updating.") + topics = Ros2.getTopicNamesAndTypes() + services = Ros2.getServiceNamesAndTypes() + actions = Ros2.getActionNamesAndTypes() + } + function printNamesAndTypes(map) { + let result = "" + for (let key in map) { + result += key + ": " + map[key] + "\n" + } + return result + } + + ScrollView { + anchors.fill: parent + + ColumnLayout { + anchors.margins: 12 + + + Button { + Layout.columnSpan: 3 + text: "Update" + onClicked: page.update() + } + + Text { + text: "Topics:" + } + Text { + leftPadding: 12 + text: printNamesAndTypes(topics) + } + Text { + text: "Services:" + } + Text { + leftPadding: 12 + text: printNamesAndTypes(services) + } + Text { + text: "Actions:" + } + Text { + leftPadding: 12 + text: printNamesAndTypes(actions) + } + } + } + + Component.onCompleted: { + // Initialize ROS with the given name. The command line args are passed by the plugin + // Optionally, you can call init with a string list ["arg1", "arg2"] after the name to use those + // args instead of the ones supplied by the command line. + Ros2.init("qml_graph_queries_demo") + update() + } + +} diff --git a/include/qml_ros2_plugin/ros2.hpp b/include/qml_ros2_plugin/ros2.hpp index 8673cde..b901282 100644 --- a/include/qml_ros2_plugin/ros2.hpp +++ b/include/qml_ros2_plugin/ros2.hpp @@ -80,6 +80,26 @@ class Ros2Qml : public QObject */ QStringList queryTopicTypes( const QString &name ) const; + /*! + * Queries the internal node for its known topics and their types. + * See rclcpp::Node::get_topic_names_and_types() for more information. + * @return A map with the topic names as keys and the types as values. + */ + QMap getTopicNamesAndTypes() const; + + /*! + * Queries the internal node for its known services and their types. + * See rclcpp::Node::get_service_names_and_types() for more information. + * @return A map with the service names as keys and the types as values. + */ + QMap getServiceNamesAndTypes() const; + + /*! + * Queries the internal node for its known actions and their types. + * @return A map with the action names as keys and the types as values. + */ + QMap getActionNamesAndTypes() const; + /*! * Creates an empty message for the given message type, e.g., "geometry_msgs/Point". * If the message type is known, an empty message with all members set to their default is returned. @@ -115,9 +135,7 @@ class Ros2Qml : public QObject //! Emitted when this ROS node was shut down and it is time to exit. void shutdown(); - private: - std::thread executor_thread_; std::shared_ptr context_; std::shared_ptr node_; @@ -169,6 +187,15 @@ class Ros2QmlSingletonWrapper : public QObject //! @copydoc Ros2Qml::queryTopicTypes Q_INVOKABLE QStringList queryTopicTypes( const QString &name ) const; + //! @copydoc Ros2Qml::getTopicNamesAndTypes + Q_INVOKABLE QVariantMap getTopicNamesAndTypes() const; + + //! @copydoc Ros2Qml::getServiceNamesAndTypes + Q_INVOKABLE QVariantMap getServiceNamesAndTypes() const; + + //! @copydoc Ros2Qml::getActionNamesAndTypes + Q_INVOKABLE QVariantMap getActionNamesAndTypes() const; + //! @copydoc Ros2Qml::createEmptyMessage Q_INVOKABLE QVariant createEmptyMessage( const QString &datatype ) const; diff --git a/src/ros2.cpp b/src/ros2.cpp index bbe1cc2..4e774de 100644 --- a/src/ros2.cpp +++ b/src/ros2.cpp @@ -12,6 +12,7 @@ #include #include +#include #include namespace qml_ros2_plugin @@ -89,8 +90,7 @@ QStringList Ros2Qml::queryTopics( const QString &datatype ) const QML_ROS2_PLUGIN_ERROR( "Tried to query topics before node was initialized!" ); return {}; } - std::map> topics_and_types = - node_->get_topic_names_and_types(); + auto topics_and_types = node_->get_topic_names_and_types(); QStringList result; std::string std_datatype = toFullyQualifiedDatatype( datatype ); for ( const auto &[topic, types] : topics_and_types ) { @@ -104,15 +104,14 @@ QStringList Ros2Qml::queryTopics( const QString &datatype ) const QList Ros2Qml::queryTopicInfo() const { - std::map> topics_and_types = - node_->get_topic_names_and_types(); + auto topics_and_types = node_->get_topic_names_and_types(); QList result; - for ( const auto &topic : topics_and_types ) { - QStringList types; - types.reserve( static_cast( topic.second.size() ) ); - std::transform( topic.second.begin(), topic.second.end(), std::back_inserter( types ), + for ( const auto &[topic, types] : topics_and_types ) { + QStringList result_types; + result_types.reserve( static_cast( types.size() ) ); + std::transform( types.begin(), types.end(), std::back_inserter( result_types ), QString::fromStdString ); - result.append( { QString::fromStdString( topic.first ), types } ); + result.append( { QString::fromStdString( topic ), result_types } ); } return result; } @@ -121,21 +120,76 @@ QStringList Ros2Qml::queryTopicTypes( const QString &name ) const { if ( name.isEmpty() ) return {}; - std::map> topics_and_types = - node_->get_topic_names_and_types(); + auto topics_and_types = node_->get_topic_names_and_types(); std::string std_name = name.toStdString(); - for ( const auto &topic : topics_and_types ) { - if ( std_name != topic.first ) + for ( const auto &[topic, types] : topics_and_types ) { + if ( std_name != topic ) continue; - QStringList types; - types.reserve( static_cast( topic.second.size() ) ); - std::transform( topic.second.begin(), topic.second.end(), std::back_inserter( types ), - QString::fromStdString ); - return types; + QStringList result; + result.reserve( static_cast( types.size() ) ); + std::transform( types.begin(), types.end(), std::back_inserter( result ), QString::fromStdString ); + return result; } return {}; } +QMap Ros2Qml::getTopicNamesAndTypes() const +{ + auto topics_and_types = node_->get_topic_names_and_types(); + QMap result; + for ( const auto &[topic, types] : topics_and_types ) { + QStringList result_types; + result_types.reserve( static_cast( types.size() ) ); + std::transform( types.begin(), types.end(), std::back_inserter( result_types ), + QString::fromStdString ); + result.insert( QString::fromStdString( topic ), result_types ); + } + return result; +} + +QMap Ros2Qml::getServiceNamesAndTypes() const +{ + auto service_names_and_types = node_->get_service_names_and_types(); + QMap result; + for ( const auto &[service_name, types] : service_names_and_types ) { + QStringList result_types; + result_types.reserve( static_cast( types.size() ) ); + std::transform( types.begin(), types.end(), std::back_inserter( result_types ), + QString::fromStdString ); + result.insert( QString::fromStdString( service_name ), result_types ); + } + return result; +} + +QMap Ros2Qml::getActionNamesAndTypes() const +{ + rcl_names_and_types_t names_and_types = rcl_get_zero_initialized_names_and_types(); + const rcl_node_s *node_handle = node_->get_node_base_interface()->get_rcl_node_handle(); + rcl_allocator_t allocator = rcutils_get_default_allocator(); + if ( rcl_ret_t ret = rcl_action_get_names_and_types( node_handle, &allocator, &names_and_types ); + ret != RCL_RET_OK ) { + QML_ROS2_PLUGIN_ERROR( "Failed to get action names and types: %s", rcl_get_error_string().str ); + return {}; + } + QMap result; + for ( size_t i = 0; i < names_and_types.names.size; ++i ) { + const std::string name = names_and_types.names.data[i]; + const auto &types = names_and_types.types[i]; + QStringList result_types; + result_types.reserve( static_cast( types.size ) ); + for ( size_t j = 0; j < types.size; ++j ) { + result_types.append( QString::fromStdString( types.data[j] ) ); + } + result.insert( QString::fromStdString( name ), result_types ); + } + if ( rcl_names_and_types_fini( &names_and_types ) != RCL_RET_OK ) { + QML_ROS2_PLUGIN_ERROR( + "Failed to free action names and types: '%s'. Might have lost some memory.", + rcl_get_error_string().str ); + } + return result; +} + QVariant Ros2Qml::createEmptyMessage( const QString &datatype ) const { try { @@ -254,6 +308,36 @@ QStringList Ros2QmlSingletonWrapper::queryTopicTypes( const QString &name ) cons return Ros2Qml::getInstance().queryTopicTypes( name ); } +QVariantMap Ros2QmlSingletonWrapper::getTopicNamesAndTypes() const +{ + QVariantMap result; + QMap topic_names_and_types = Ros2Qml::getInstance().getTopicNamesAndTypes(); + for ( auto it = topic_names_and_types.begin(); it != topic_names_and_types.end(); ++it ) { + result.insert( it.key(), QVariant( it.value() ) ); + } + return result; +} + +QVariantMap Ros2QmlSingletonWrapper::getServiceNamesAndTypes() const +{ + QVariantMap result; + QMap topic_names_and_types = Ros2Qml::getInstance().getServiceNamesAndTypes(); + for ( auto it = topic_names_and_types.begin(); it != topic_names_and_types.end(); ++it ) { + result.insert( it.key(), QVariant( it.value() ) ); + } + return result; +} + +QVariantMap Ros2QmlSingletonWrapper::getActionNamesAndTypes() const +{ + QVariantMap result; + QMap topic_names_and_types = Ros2Qml::getInstance().getActionNamesAndTypes(); + for ( auto it = topic_names_and_types.begin(); it != topic_names_and_types.end(); ++it ) { + result.insert( it.key(), QVariant( it.value() ) ); + } + return result; +} + QVariant Ros2QmlSingletonWrapper::createEmptyMessage( const QString &datatype ) const { return Ros2Qml::getInstance().createEmptyMessage( datatype );