diff --git a/.devcontainer/postCreateCommand.sh b/.devcontainer/postCreateCommand.sh index a94bb633..80618592 100755 --- a/.devcontainer/postCreateCommand.sh +++ b/.devcontainer/postCreateCommand.sh @@ -3,4 +3,5 @@ set -e export PIP_BREAK_SYSTEM_PACKAGES=1 pip install -r ../docs/source/requirements.txt -pip install sphinx-autobuild \ No newline at end of file +pip install sphinx-autobuild +chmod +x serve_sphinx.sh \ No newline at end of file diff --git a/.devcontainer/serve_sphinx.sh b/.devcontainer/serve_sphinx.sh new file mode 100755 index 00000000..d79bf0dc --- /dev/null +++ b/.devcontainer/serve_sphinx.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e + +sphinx-autobuild ../docs/source ../build/html \ No newline at end of file diff --git a/.readthedocs.yaml b/.readthedocs.yaml index b0d7e88e..d19fd641 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -11,9 +11,12 @@ build: tools: python: "3.11" # You can also specify other tool versions: - # nodejs: "19" + nodejs: "19" # rust: "1.64" # golang: "1.19" + jobs: + post_install: + - npm install -g @mermaid-js/mermaid-cli # Build documentation in the docs/ directory with Sphinx sphinx: diff --git a/docs/source/conf.py b/docs/source/conf.py index b914e6bc..c8d12b61 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -9,7 +9,7 @@ # -- Project information -project = 'ROS2 Tutorial' +project = "(Murilo's) ROS2 Tutorial" author = 'Murilo M. Marinho' if this_year_str == '2023': @@ -31,7 +31,8 @@ 'hoverxref.extension', # https://sphinx-hoverxref.readthedocs.io/en/latest/index.html 'sphinx_copybutton', # https://sphinx-copybutton.readthedocs.io/en/latest/ 'sphinx_design', # https://sphinx-design.readthedocs.io/en/latest/get_started.html - "sphinxext.remoteliteralinclude" # https://github.com/wpilibsuite/sphinxext-remoteliteralinclude + 'sphinxext.remoteliteralinclude', # https://github.com/wpilibsuite/sphinxext-remoteliteralinclude + 'sphinxcontrib.mermaid' #https://sphinxcontrib-mermaid-demo.readthedocs.io/en/latest/ ] intersphinx_mapping = { @@ -46,6 +47,12 @@ html_theme = 'sphinx_book_theme' # Tried with `furo` on May 23, 2025, but somehow it didn't look right. In particular the download button wasn't as clear. +html_theme_options = { + "announcement": "The documentation is being updated to Jazzy in this branch. " + "See Humble Docs for the stable ones. " + "Create an issue for inconsistencies.", +} +html_title = project # -- Options for EPUB output epub_show_urls = 'footnote' @@ -53,6 +60,9 @@ # -- Options for hoverxref.extension https://sphinx-hoverxref.readthedocs.io/en/latest/configuration.html hoverxref_auto_ref = True +# https://sphinxcontrib-mermaid-demo.readthedocs.io/en/latest/ +mermaid_params = ['-p' 'puppeteer-config.json'] + # -- Options for latex https://docs.readthedocs.io/en/stable/guides/pdf-non-ascii-languages.html # latex_engine = "xelatex" # By using this, the UTF emojis became question marks in the PDF. diff --git a/docs/source/create_interface_package.rst b/docs/source/create_interface_package.rst index 976433c3..587995f9 100644 --- a/docs/source/create_interface_package.rst +++ b/docs/source/create_interface_package.rst @@ -5,6 +5,10 @@ Creating a dedicated package for custom interfaces Despite this push in ROS2 towards having the users define even the simplest of message types, to define new interfaces in ROS2 we must use an :program:`ament_cmake` package. It **cannot** be done with an :program:`ament_python` package. +.. seealso:: + + The contents of this session were simplified in this version. A more complex example is shown in https://ros2-tutorial.readthedocs.io/en/humble/service_servers_and_clients.html. + All interfaces in ROS2 must be made in an :program:`ament_cmake` package. We have so far not needed it, but for this scenario we cannot escape. Thankfully, for this we don't need to dig too deep into :program:`CMake`, so fear not. Creating the package @@ -28,14 +32,14 @@ which again shows our beloved wall of text, with a few highlighted differences b going to create a new package package name: package_with_interfaces - destination directory: /home/murilo/git/ROS2_Tutorial/ros2_tutorial_workspace/src + destination directory: /root/ros2_tutorial_workspace/src package format: 3 version: 0.0.0 description: TODO: Package description - maintainer: ['murilo '] + maintainer: ['root '] licenses: ['TODO: License declaration'] build type: ament_cmake - dependencies: [geometry_msgs] + dependencies: ['geometry_msgs'] creating folder ./package_with_interfaces creating ./package_with_interfaces/package.xml creating source and include folder @@ -44,7 +48,7 @@ which again shows our beloved wall of text, with a few highlighted differences b creating ./package_with_interfaces/CMakeLists.txt [WARNING]: Unknown license 'TODO: License declaration'. This has been set in the package.xml, but no LICENSE file has been created. - It is recommended to use one of the ament license identitifers: + It is recommended to use one of the ament license identifiers: Apache-2.0 BSL-1.0 BSD-2.0 @@ -63,7 +67,7 @@ The :file:`package.xml` dependencies Whenever the package has any type of interface, the :file:`package.xml` **must** include three specific dependencies. Namely, the ones highlighted below. Edit the :file:`package_with_interfaces/package.xml` like so -:download:`package.xml <../../ros2_tutorial_workspace/src/package_with_interfaces/package.xml>` +:download:`~/ros2_tutorial_workspace/src/package_with_interfaces/package.xml <../../ros2_tutorial_workspace/src/package_with_interfaces/package.xml>` .. literalinclude:: ../../ros2_tutorial_workspace/src/package_with_interfaces/package.xml :language: xml @@ -98,12 +102,28 @@ Let us create a message file to transfer inspirational quotes between Nodes. For There are many ways to represent this, but for the sake of the example let us give each message an :code:`id` and two rather obvious fields. Create a file called :file:`AmazingQuote.msg` in the folder :file:`msg` that we just created with the following contents. -:download:`AmazingQuote.msg <../../ros2_tutorial_workspace/src/package_with_interfaces/msg/AmazingQuote.msg>` +:download:`~/ros2_tutorial_workspace/src/package_with_interfaces/msg/AmazingQuote.msg <../../ros2_tutorial_workspace/src/package_with_interfaces/msg/AmazingQuote.msg>` .. literalinclude:: ../../ros2_tutorial_workspace/src/package_with_interfaces/msg/AmazingQuote.msg :language: yaml :linenos: +Re-using a message from the same package +++++++++++++++++++++++++++++++++++++++++ + +With the :file:`AmazingQuote.msg`, we have seen how to use built-in types. Let's use another message, :file:`AmazingQuoteStamped.msg`, to learn two more possibilities, namely using messages from the same package and messages defined elsewhere. + +:download:`~/ros2_tutorial_workspace/src/package_with_interfaces/msg/AmazingQuoteStamped.msg <../../ros2_tutorial_workspace/src/package_with_interfaces/msg/AmazingQuoteStamped.msg>` + +.. literalinclude:: ../../ros2_tutorial_workspace/src/package_with_interfaces/msg/AmazingQuoteStamped.msg + :language: yaml + :linenos: + +Note that if the message is defined in the same package, the package name does not appear in the message (or service) definition. If the message is defined elsewhere, we have to fully specify the package. + +In many :program:`ROS2` packages, messages with the suffix ``Stamped`` exist. As a rule, those are the same messages but with a additional +``std_msgs/Header`` so that they can be timestamped. + The service folder ------------------ @@ -119,17 +139,18 @@ The convention is to add all services to a folder called :file:`srv`. Let's foll The service file ---------------- -With the :file:`AmazingQuote.msg`, we have seen how to use built-in types. Let's use the service to learn two more possibilities. Let us use a message from the same package and a message from another package. Services cannot be used to define other services. +.. note:: + + Services cannot be used to define other services. -Add the file :file:`WhatIsThePoint.srv` in the :file:`srv` folder with the following contents +Add the file :file:`AddPoints.srv` in the :file:`srv` folder with the following contents -:download:`WhatIsThePoint.srv <../../ros2_tutorial_workspace/src/package_with_interfaces/srv/WhatIsThePoint.srv>` +:download:`~/ros2_tutorial_workspace/src/package_with_interfaces/srv/AddPoints.srv <../../ros2_tutorial_workspace/src/package_with_interfaces/srv/AddPoints.srv>` -.. literalinclude:: ../../ros2_tutorial_workspace/src/package_with_interfaces/srv/WhatIsThePoint.srv +.. literalinclude:: ../../ros2_tutorial_workspace/src/package_with_interfaces/srv/AddPoints.srv :language: yaml :linenos: -Note that if the message is defined in the same package, the package name does not appear in the service or message definition. If the message is defined elsewhere, we have to specify it. The :file:`CMakeLists.txt` directives ------------------------------------- @@ -141,12 +162,12 @@ The :file:`CMakeLists.txt` directives If a package is dedicated to interfaces, there is no need to worry too much about the :program:`CMake` details. We can follow the boilerplate as shown below. Edit the :file:`package_with_interfaces/CMakeLists.txt` like so -:download:`CMakeLists.txt <../../ros2_tutorial_workspace/src/package_with_interfaces/CMakeLists.txt>` +:download:`~/ros2_tutorial_workspace/src/package_with_interfaces/CMakeLists.txt <../../ros2_tutorial_workspace/src/package_with_interfaces/CMakeLists.txt>` .. literalinclude:: ../../ros2_tutorial_workspace/src/package_with_interfaces/CMakeLists.txt :language: cmake :linenos: - :emphasize-lines: 14-36 + :emphasize-lines: 14-37 What to do when adding new interfaces? -------------------------------------- @@ -167,8 +188,8 @@ If additional interfaces are required .. literalinclude:: ../../ros2_tutorial_workspace/src/package_with_interfaces/CMakeLists.txt :language: cmake - :lines: 17-24 - :emphasize-lines: 4, 7 + :lines: 17-25 + :emphasize-lines: 5, 8 .. note:: @@ -196,8 +217,9 @@ returns .. code:: console - package_with_interfaces/srv/WhatIsThePoint - package_with_interfaces/msg/AmazingQuote + package_with_interfaces/msg/AmazingQuote + package_with_interfaces/msg/AmazingQuoteStamped + package_with_interfaces/srv/AddPoints and we can further get more specific info on :file:`AmazingQuote.msg` @@ -210,26 +232,29 @@ which returns .. literalinclude:: ../../ros2_tutorial_workspace/src/package_with_interfaces/msg/AmazingQuote.msg :language: yaml -alternatively, we can do the same for :file:`WhatIsThePoint.srv` +alternatively, we can do the same for :file:`AddPoints.srv` .. code:: console - ros2 interface show package_with_interfaces/srv/WhatIsThePoint + ros2 interface show package_with_interfaces/srv/AddPoints which returns expanded information on each field of the service - .. code:: yaml - # WhatIsThePoint.srv from https://ros2-tutorial.readthedocs.io - # Receives an AmazingQuote and returns what is the point - AmazingQuote quote - int32 id - string quote - string philosopher_name - --- - geometry_msgs/Point point - float64 x - float64 y - float64 z + # AddPoints.srv from https://ros2-tutorial.readthedocs.io + # Adds the values of points `a` and `b` to give the output `result` + geometry_msgs/Point a + float64 x + float64 y + float64 z + geometry_msgs/Point b + float64 x + float64 y + float64 z + --- + geometry_msgs/Point result + float64 x + float64 y + float64 z diff --git a/docs/source/create_packages.rst b/docs/source/create_packages.rst index c79b231f..cff72696 100644 --- a/docs/source/create_packages.rst +++ b/docs/source/create_packages.rst @@ -1,5 +1,5 @@ Create packages (:program:`ros2 pkg create`) ---------------- +-------------------------------------------- ROS2 has a tool to help create package templates. We can get all available options by running @@ -10,43 +10,35 @@ ROS2 has a tool to help create package templates. We can get all available optio which outputs a list of handy options to populate the package template with useful files. Namely, the four emphasized ones. .. code-block:: console - :emphasize-lines: 27, 29, 35, 37 - - usage: ros2 pkg create [-h] [--package-format {2,3}] [--description DESCRIPTION] - [--license LICENSE] - [--destination-directory DESTINATION_DIRECTORY] - [--build-type {cmake,ament_cmake,ament_python}] - [--dependencies DEPENDENCIES [DEPENDENCIES ...]] - [--maintainer-email MAINTAINER_EMAIL] - [--maintainer-name MAINTAINER_NAME] [--node-name NODE_NAME] - [--library-name LIBRARY_NAME] - package_name - - Create a new ROS 2 package - - positional arguments: - package_name The package name - - options: - -h, --help show this help message and exit - --package-format {2,3}, --package_format {2,3} - The package.xml format. - --description DESCRIPTION - The description given in the package.xml - --license LICENSE The license attached to this package; this can be an arbitrary - string, but a LICENSE file will only be generated if it is one - of the supported licenses (pass '?' to get a list) - --destination-directory DESTINATION_DIRECTORY - Directory where to create the package directory - --build-type {cmake,ament_cmake,ament_python} - The build type to process the package with - --dependencies DEPENDENCIES [DEPENDENCIES ...] - list of dependencies - --maintainer-email MAINTAINER_EMAIL - email address of the maintainer of this package - --maintainer-name MAINTAINER_NAME - name of the maintainer of this package - --node-name NODE_NAME - name of the empty executable - --library-name LIBRARY_NAME - name of the empty library + :emphasize-lines: 19,21,27, 29 + + usage: ros2 pkg create [-h] [--package-format {2,3}] [--description DESCRIPTION] [--license LICENSE] [--destination-directory DESTINATION_DIRECTORY] [--build-type {cmake,ament_cmake,ament_python}] + [--dependencies DEPENDENCIES [DEPENDENCIES ...]] [--maintainer-email MAINTAINER_EMAIL] [--maintainer-name MAINTAINER_NAME] [--node-name NODE_NAME] [--library-name LIBRARY_NAME] + package_name + + Create a new ROS 2 package + + positional arguments: + package_name The package name + + options: + -h, --help show this help message and exit + --package-format {2,3}, --package_format {2,3} + The package.xml format. + --description DESCRIPTION + The description given in the package.xml + --license LICENSE The license attached to this package; this can be an arbitrary string, but a LICENSE file will only be generated if it is one of the supported licenses (pass '?' to get a list) + --destination-directory DESTINATION_DIRECTORY + Directory where to create the package directory + --build-type {cmake,ament_cmake,ament_python} + The build type to process the package with + --dependencies DEPENDENCIES [DEPENDENCIES ...] + list of dependencies + --maintainer-email MAINTAINER_EMAIL + email address of the maintainer of this package + --maintainer-name MAINTAINER_NAME + name of the maintainer of this package + --node-name NODE_NAME + name of the empty executable + --library-name LIBRARY_NAME + name of the empty library \ No newline at end of file diff --git a/docs/source/create_python_library.rst b/docs/source/create_python_library.rst index 342818c4..0c92133e 100644 --- a/docs/source/create_python_library.rst +++ b/docs/source/create_python_library.rst @@ -17,11 +17,11 @@ which outputs the forever beautiful wall of text we're now used to, with a minor going to create a new package package name: python_package_with_a_library - destination directory: /home/murilo/git/ROS2_Tutorial/ros2_tutorial_workspace/src + destination directory: /root/ros2_tutorial_workspace/src package format: 3 version: 0.0.0 description: TODO: Package description - maintainer: ['murilo '] + maintainer: ['root '] licenses: ['TODO: License declaration'] build type: ament_python dependencies: [] @@ -43,7 +43,7 @@ which outputs the forever beautiful wall of text we're now used to, with a minor creating ./python_package_with_a_library/python_package_with_a_library/sample_python_library/__init__.py [WARNING]: Unknown license 'TODO: License declaration'. This has been set in the package.xml, but no LICENSE file has been created. - It is recommended to use one of the ament license identitifers: + It is recommended to use one of the ament license identifiers: Apache-2.0 BSL-1.0 BSD-2.0 @@ -58,21 +58,25 @@ The folders/files, Mason, what do they mean? -------------------------------------------- The ROS2 package created from the template has a structure like so. In particular, we can see that :file:`python_package_with_a_library` is repeated twice in a row. This is a common source of error, so don't forget! +The first is the name of the :program:`ROS2` package, and the second is the name of Python package that will be installed by :program:`ROS2`. .. code-block:: console - :emphasize-lines: 1,2 + :emphasize-lines: 1,3 - python_package_with_a_library - └── python_package_with_a_library - └── sample_python_library - __init__.py - __init__.py - └── resource - python_package_with_a_library - └── test - package.xml - setup.cfg - setup.py + python_package_with_a_library/ + |-- package.xml + |-- python_package_with_a_library + | |-- __init__.py + | `-- sample_python_library + | `-- __init__.py + |-- resource + | `-- python_package_with_a_library + |-- setup.cfg + |-- setup.py + `-- test + |-- test_copyright.py + |-- test_flake8.py + `-- test_pep257.py We learned the meaning of most of those in the preamble, namely :ref:`Python Best Practices`. To quickly clarify a few things, see the table below. @@ -105,17 +109,24 @@ Overview of the library For the sake of the example, let us create a library with a Python :code:`function` and another one with a :code:`class`. To guide our next steps, we first draw a quick overview of what our :code:`python_package_with_a_library` will look like. .. code-block:: console - :emphasize-lines: 4,5,6 + :emphasize-lines: 6-8 - python_package_with_a_library - └── python_package_with_a_library - └── sample_python_library - __init__.py - _sample_class.py - _sample_function.py - __init__.py - └── resource - └── test + python_package_with_a_library/ + |-- package.xml + |-- python_package_with_a_library + | |-- __init__.py + | `-- sample_python_library + | |-- __init__.py + | |-- _sample_class.py + | `-- _sample_function.py + |-- resource + | `-- python_package_with_a_library + |-- setup.cfg + |-- setup.py + `-- test + |-- test_copyright.py + |-- test_flake8.py + `-- test_pep257.py With respect to the highlighted files, we will @@ -152,35 +163,6 @@ Create a new file with the following contents and name. :lines: 26- The class is quite simple with a `private data member `_ and a method to retrieve it. - -Modify the :code:`__init__.py` to export the symbols ----------------------------------------------------- - -With the necessary files created and properly organized, the last step is to :code:`import` the function and the class. We modify proper :file:`__init__.py` file with the following contents. - -:download:`~/ros2_tutorial_workspace/src/python_package_with_a_library/python_package_with_a_library/sample_python_library/__init__.py <../../ros2_tutorial_workspace/src/python_package_with_a_library/python_package_with_a_library/sample_python_library/__init__.py>` - -.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_with_a_library/python_package_with_a_library/sample_python_library/__init__.py - :language: python - :linenos: - :lines: 24- - -Modify the :code:`setup.py` to export the packages --------------------------------------------------- - -.. warning:: - This step might be unnecessary after `this fix `_. - -.. note:: - - This is a *one-size-fits-most* solution, which might not work for certain Python package structures. As a generic solution, we will export all Python packages in the ROS2 package excluding the `test` directory. For more information on :program:`setuptools`, see the `official Python packaging docs `_. - -:download:`~/ros2_tutorial_workspace/src/python_package_with_a_library/setup.py <../../ros2_tutorial_workspace/src/python_package_with_a_library/setup.py>` - -.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_with_a_library/setup.py - :language: python - :linenos: - :emphasize-lines: 1,8 Build and source ---------------- diff --git a/docs/source/create_python_node_from_scratch.rst b/docs/source/create_python_node_from_scratch.rst index 85457968..cf45cc17 100644 --- a/docs/source/create_python_node_from_scratch.rst +++ b/docs/source/create_python_node_from_scratch.rst @@ -70,10 +70,11 @@ By now, this should be enough for you to be able to run the node in :program:`Py .. code :: console - [INFO] [1683009340.877110693] [print_forever]: Printed 0 times. - [INFO] [1683009341.336559942] [print_forever]: Printed 1 times. - [INFO] [1683009341.836334639] [print_forever]: Printed 2 times. - [INFO] [1683009342.336555088] [print_forever]: Printed 3 times. + [INFO] [1753518435.025424000] [print_forever]: Printed 0 times. + [INFO] [1753518435.509299083] [print_forever]: Printed 1 times. + [INFO] [1753518436.009566292] [print_forever]: Printed 2 times. + [INFO] [1753518436.509644292] [print_forever]: Printed 3 times. + [INFO] [1753518437.009296251] [print_forever]: Printed 4 times. To finish, press the :guilabel:`Stop` button or press :kbd:`CTRL+F2` on :program:`PyCharm`. The node will exit gracefully with @@ -124,9 +125,10 @@ which will output, as expected .. code :: console - [INFO] [1683010987.130432622] [print_forever]: Printed 0 times. - [INFO] [1683010987.622780292] [print_forever]: Printed 1 times. - [INFO] [1683010988.122731296] [print_forever]: Printed 2 times. - [INFO] [1683010988.622735422] [print_forever]: Printed 3 times. + [INFO] [1753518652.646459087] [print_forever]: Printed 0 times. + [INFO] [1753518653.131078795] [print_forever]: Printed 1 times. + [INFO] [1753518653.632436004] [print_forever]: Printed 2 times. + [INFO] [1753518654.132090212] [print_forever]: Printed 3 times. + [INFO] [1753518654.630924338] [print_forever]: Printed 4 times. To stop, press :kbd:`CTRL+C` on the terminal and the Node will return gracefully. diff --git a/docs/source/create_python_node_with_template.rst b/docs/source/create_python_node_with_template.rst index f4360545..ac927920 100644 --- a/docs/source/create_python_node_with_template.rst +++ b/docs/source/create_python_node_with_template.rst @@ -14,11 +14,7 @@ Let us use the template for creating a package with a Node, as follows. Which will output many things in common with the prior example, but with two major differences. -#. It generates a template Node - - .. code-block:: console - creating ./python_package_with_a_node/python_package_with_a_node/sample_python_node.py - +#. It generates a template Node #. The :file:`setup.py` has information about the Node. .. code-block:: bash @@ -26,11 +22,11 @@ Which will output many things in common with the prior example, but with two maj going to create a new package package name: python_package_with_a_node - destination directory: ~/ros2_tutorial_workspace/src + destination directory: /root/ros2_tutorial_workspace/src package format: 3 version: 0.0.0 description: TODO: Package description - maintainer: ['murilo '] + maintainer: ['root '] licenses: ['TODO: License declaration'] build type: ament_python dependencies: [] @@ -51,7 +47,7 @@ Which will output many things in common with the prior example, but with two maj creating ./python_package_with_a_node/python_package_with_a_node/sample_python_node.py [WARNING]: Unknown license 'TODO: License declaration'. This has been set in the package.xml, but no LICENSE file has been created. - It is recommended to use one of the ament license identitifers: + It is recommended to use one of the ament license identifiers: Apache-2.0 BSL-1.0 BSD-2.0 @@ -73,18 +69,9 @@ which will result in going through the package we created in the prior example a .. code :: console - Starting >>> python_package_with_a_node - Starting >>> the_simplest_python_package - --- stderr: python_package_with_a_node - /usr/lib/python3/dist-packages/setuptools/command/install.py:34: SetuptoolsDeprecationWarning: setup.py install is deprecated. Use build and pip and other standards-based tools. - warnings.warn( - --- - Finished <<< python_package_with_a_node [1.16s] - --- stderr: the_simplest_python_package - /usr/lib/python3/dist-packages/setuptools/command/install.py:34: SetuptoolsDeprecationWarning: setup.py install is deprecated. Use build and pip and other standards-based tools. - warnings.warn( - --- - Finished <<< the_simplest_python_package [1.17s] + Starting >>> python_package_with_a_node + Starting >>> the_simplest_python_package + Finished <<< the_simplest_python_package [0.56s] + Finished <<< python_package_with_a_node [0.56s] - Summary: 2 packages finished [1.36s] - 2 packages had stderr output: python_package_with_a_node the_simplest_python_package + Summary: 2 packages finished [0.62s] diff --git a/docs/source/create_python_package.rst b/docs/source/create_python_package.rst index 48b5920c..176006f9 100644 --- a/docs/source/create_python_package.rst +++ b/docs/source/create_python_package.rst @@ -29,11 +29,11 @@ which will result in the output below, meaning the package has been generated su going to create a new package package name: the_simplest_python_package - destination directory: /home/murilo/ros2_tutorial_workspace/src + destination directory: /root/ros2_tutorial_workspace/src package format: 3 version: 0.0.0 description: TODO: Package description - maintainer: ['murilo '] + maintainer: ['root '] licenses: ['TODO: License declaration'] build type: ament_python dependencies: [] @@ -52,7 +52,7 @@ which will result in the output below, meaning the package has been generated su creating ./the_simplest_python_package/test/test_pep257.py [WARNING]: Unknown license 'TODO: License declaration'. This has been set in the package.xml, but no LICENSE file has been created. - It is recommended to use one of the ament license identitifers: + It is recommended to use one of the ament license identifiers: Apache-2.0 BSL-1.0 BSD-2.0 @@ -75,24 +75,8 @@ which will now output .. code :: console Starting >>> the_simplest_python_package - --- stderr: the_simplest_python_package - /usr/lib/python3/dist-packages/setuptools/command/install.py:34: SetuptoolsDeprecationWarning: setup.py install is deprecated. Use build and pip and other standards-based tools. - warnings.warn( - --- - Finished <<< the_simplest_python_package [1.72s] + Finished <<< the_simplest_python_package [0.49s] - Summary: 1 package finished [1.89s] - 1 package had stderr output: the_simplest_python_package + Summary: 1 package finished [0.55s] meaning that :program:`colcon` successfully built the example package. - -.. warning:: - - In this version of ROS2, all :program:`ament_python` packages will output a :code:`SetuptoolsDeprecationWarning`. - This is related to `this issue on Github `_. Until that is fixed, just ignore it. - - - - - - diff --git a/docs/source/editing_python_source.rst b/docs/source/editing_python_source.rst index 6261265a..688d823e 100644 --- a/docs/source/editing_python_source.rst +++ b/docs/source/editing_python_source.rst @@ -21,9 +21,10 @@ With the project correctly configured, you can 1. move to :menuselection:`src --> python_package_with_a_node --> python_package_with_a_node`. 2. double (left) click :program:`sample_python_node.py` to open the source code, showing the contents of the Node. It is minimal to the point that it doesn't have anything related to :program:`ROS` at all. -.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_with_a_node/sample_python_node.py +.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_with_a_node/python_package_with_a_node/sample_python_node.py :language: python :linenos: + 3. right click :program:`sample_python_node.py` and choose :menuselection:`Debug sample_python_node` It will output in :program:`PyCharm`'s console diff --git a/docs/source/index.rst b/docs/source/index.rst index cff4805b..76ae10db 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -11,7 +11,7 @@ 📖 **About this tutorial** -`ROS2 Humble `_ tutorials by `Murilo M. Marinho `_, focusing on Ubuntu 22.04 x64 LTS and the programming practices of successful state-of-the-art robotics implementations such as the `SmartArmStack `_ and the `AISciencePlatform `_. +`ROS2 Jazzy `_ tutorials by `Murilo M. Marinho `_, focusing on Ubuntu 24.04 x64 LTS and the programming practices of successful state-of-the-art robotics implementations such as the `SmartArmStack `_ also used in the `AISciencePlatform `_. 🤟 **Using this tutorial** diff --git a/docs/source/inspecting_parameters.rst b/docs/source/inspecting_parameters.rst index a23f5694..cde271a6 100644 --- a/docs/source/inspecting_parameters.rst +++ b/docs/source/inspecting_parameters.rst @@ -1,5 +1,3 @@ -.. include:: the_topic_is_under_heavy_construction.rst - Inspecting parameters (:program:`ros2 param`) ============================================= @@ -26,7 +24,7 @@ which returns Commands: delete Delete parameter describe Show descriptive information about declared parameters - dump Dump the parameters of a node to a yaml file + dump Show all of the parameters of a node in a YAML file format get Get parameter list Output a list of available parameters load Load parameter file for a node @@ -34,10 +32,6 @@ which returns Call `ros2 param -h` for more detailed usage. -.. note:: - - By the time you try this out, the documentation of :program:`ros2 param dump` might have changed. See `ros2/ros2cli/#835 `_. - As shown in the emphasized lines above, the :program:`ros2 param` tool has a large number of useful commands to interact with parameters. Launching the Node with parameters @@ -161,10 +155,6 @@ Changing parameters is not instantaneous and, after the change becomes visible i Save parameters to a file with :program:`ros2 param dump` --------------------------------------------------------- -.. warning:: - - At the time I was writing this part of the tutorial, `the description `_ of :program:`ros2 param dump` was outdated. By the time you try this out, it might have been corrected. See `ros2/ros2cli/#836 `_ for more info. - Words are sometimes little happy accidents. This usage of the word dump has no relation whatsoever to, for example, `Peter got dumped by Sarah and went to Hawaii `_. Dump files are usually related to `crashes and unresponsive programs `_, so this name puzzles me since ROS: the first. While we wait for someone to come and correct me on my claims above, just think about this as a weird name for :program:`ros2 param print_to_screen_as_yaml`. It prints the parameters in the terminal with a YAML file format. It is nice because it gives a bit more info than :program:`ros2 param list`, but not so useful as-is. The trick is that we can put all that nicely formatted content into a file with diff --git a/docs/source/inspecting_services.rst b/docs/source/inspecting_services.rst index 063385ec..e732530c 100644 --- a/docs/source/inspecting_services.rst +++ b/docs/source/inspecting_services.rst @@ -1,7 +1,7 @@ Inspecting services (:program:`ros2 service`) ============================================= -ROS2 has a tool to help us inspect services. It is just as helpful as the tools for topics. +:program:`ROS2` has a tool to help us inspect services. It is just as helpful as the tools for topics. .. code:: console @@ -12,23 +12,23 @@ which outputs the detailed information of the tool, as shown below. In particula .. code-block:: console :emphasize-lines: 13,15 - usage: ros2 service [-h] [--include-hidden-services] - Call `ros2 service -h` for more - detailed usage. ... - + usage: ros2 service [-h] [--include-hidden-services] Call `ros2 service -h` for more detailed usage. ... + Various service related sub-commands - + options: -h, --help show this help message and exit --include-hidden-services Consider hidden services as well - + Commands: call Call a service + echo Echo a service find Output a list of available services of a given type + info Print information about a service list Output a list of available services type Output a service's type - + Call `ros2 service -h` for more detailed usage. Start a service server @@ -42,7 +42,7 @@ Similar to the discussion about topics, it is good to be able to test service se .. code:: console - ros2 run python_package_that_uses_the_services what_is_the_point_service_server_node + ros2 run python_package_that_uses_the_services add_points_service_server_node Getting all services with :program:`ros2 service list` ------------------------------------------------------ @@ -58,13 +58,14 @@ which, in this case, outputs .. code-block:: console :emphasize-lines: 1 - /what_is_the_point - /what_is_the_point_service_server/describe_parameters - /what_is_the_point_service_server/get_parameter_types - /what_is_the_point_service_server/get_parameters - /what_is_the_point_service_server/list_parameters - /what_is_the_point_service_server/set_parameters - /what_is_the_point_service_server/set_parameters_atomically + /add_points + /add_points_service_server/describe_parameters + /add_points_service_server/get_parameter_types + /add_points_service_server/get_parameters + /add_points_service_server/get_type_description + /add_points_service_server/list_parameters + /add_points_service_server/set_parameters + /add_points_service_server/set_parameters_atomically To everyone's surprise, there are a lot of services beyond the one we created. We can address those when we talk about ROS2 parameters, for now, we ignore them. @@ -76,25 +77,29 @@ Back to our example, we can do .. code-block:: console - ros2 service call /what_is_the_point \ - package_with_interfaces/srv/WhatIsThePoint \ + ros2 service call /add_points \ + package_with_interfaces/srv/AddPoints \ '{ - quote: { - id: 1994, - quote: So you’re telling me there’s a chance, - philosopher_name: Lloyd - } + a: { + x: 10, + y: 11, + z: 12 + }, + b: { + x: -10, + y: -10, + z: 22 + } }' which results in .. code-block:: console - waiting for service to become available... - requester: making request: package_with_interfaces.srv.WhatIsThePoint_Request(quote=package_with_interfaces.msg.AmazingQuote(id=1994, quote='So you’re telling me there’s a chance', philosopher_name='Lloyd')) - + requester: making request: package_with_interfaces.srv.AddPoints_Request(a=geometry_msgs.msg.Point(x=10.0, y=11.0, z=12.0), b=geometry_msgs.msg.Point(x=-10.0, y=-10.0, z=22.0)) + response: - package_with_interfaces.srv.WhatIsThePoint_Response(point=geometry_msgs.msg.Point(x=8.327048266159165, y=95.97987946924988, z=67.03878311627777)) + package_with_interfaces.srv.AddPoints_Response(result=geometry_msgs.msg.Point(x=0.0, y=1.0, z=34.0)) Testing your service clients??? ------------------------------- diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 45585f66..30866275 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -4,7 +4,7 @@ ROS2 Installation ================= .. note:: - This tutorial is an abridged version of the original `ROS 2 Documentation `_. This tutorial considers a fresh Ubuntu Desktop (not Server) 22.04 LTS x64 (not arm64) installation, that you have super user access and common sense. It might work in other cases, but those have not been tested in this tutorial. + This tutorial is an abridged version of the original `ROS 2 Documentation `_. This tutorial considers a fresh Ubuntu Desktop (not Server) 24.04 LTS installation, that you have super user access and common sense. It might work in other cases, but those have not been tested in this tutorial. .. warning:: All commands must be followed to the letter, in the precise order described herein. Any deviation from what is described might cause unspecified problems and not all of them are easily solvable. @@ -47,8 +47,9 @@ The following commands will do all that magic. .. code-block:: console sudo add-apt-repository universe - sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg - echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(. /etc/os-release && echo $UBUNTU_CODENAME) main" | sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null + export ROS_APT_SOURCE_VERSION=$(curl -s https://api.github.com/repos/ros-infrastructure/ros-apt-source/releases/latest | grep -F "tag_name" | awk -F\" '{print $4}') + curl -L -o /tmp/ros2-apt-source.deb "https://github.com/ros-infrastructure/ros-apt-source/releases/download/${ROS_APT_SOURCE_VERSION}/ros2-apt-source_${ROS_APT_SOURCE_VERSION}.$(. /etc/os-release && echo $VERSION_CODENAME)_all.deb" # If using Ubuntu derivates use $UBUNTU_CODENAME + sudo dpkg -i /tmp/ros2-apt-source.deb sudo apt update && sudo apt upgrade -y Install ROS2 packages @@ -58,7 +59,7 @@ There are plenty of ways to install ROS2, the following will suffice for now. .. code-block:: console - sudo apt install -y ros-humble-desktop ros-dev-tools + sudo apt install -y ros-jazzy-desktop ros-dev-tools Set up system environment to find ROS2 ------------------------------------- @@ -71,8 +72,8 @@ The :code:`~/.bashrc` file can be used for that exact purpose as, in Ubuntu, tha .. code-block:: console - echo "# Source ROS2 Humble, as instructed in https://ros2-tutorial.readthedocs.io" >> ~/.bashrc - echo "source /opt/ros/humble/setup.bash" >> ~/.bashrc + echo "# Source ROS2 Jazzy, as instructed in https://ros2-tutorial.readthedocs.io" >> ~/.bashrc + echo "source /opt/ros/jazzy/setup.bash" >> ~/.bashrc source ~/.bashrc Check if it works @@ -88,18 +89,14 @@ outputs something similar to what is shown below, then it worked! Otherwise, it .. code-block:: console - usage: ros2 [-h] [--use-python-default-buffering] - Call `ros2 -h` for more detailed usage. ... + usage: ros2 [-h] [--use-python-default-buffering] Call `ros2 -h` for more detailed usage. ... ros2 is an extensible command-line tool for ROS 2. options: -h, --help show this help message and exit --use-python-default-buffering - Do not force line buffering in stdout and instead use - the python default buffering, which might be affected - by PYTHONUNBUFFERED/-u and depends on whatever stdout - is interactive or not + Do not force line buffering in stdout and instead use the python default buffering, which might be affected by PYTHONUNBUFFERED/-u and depends on whatever stdout is interactive or not Commands: action Various action related sub-commands @@ -122,8 +119,6 @@ outputs something similar to what is shown below, then it worked! Otherwise, it Call `ros2 -h` for more detailed usage. - - .. _software-properties-common: https://askubuntu.com/questions/1000118/what-is-software-properties-common .. _curl: https://curl.se/ .. _terminator: https://manpages.ubuntu.com/manpages/bionic/man1/terminator.1.html diff --git a/docs/source/launch_configurable_nodes.rst b/docs/source/launch_configurable_nodes.rst index 77138061..44936564 100644 --- a/docs/source/launch_configurable_nodes.rst +++ b/docs/source/launch_configurable_nodes.rst @@ -1,5 +1,3 @@ -.. include:: the_topic_is_under_heavy_construction.rst - Launch configurable Nodes (:program:`ros2 launch`) -------------------------------------------------- diff --git a/docs/source/messages.rst b/docs/source/messages.rst index 81605260..f789cbfb 100644 --- a/docs/source/messages.rst +++ b/docs/source/messages.rst @@ -1,16 +1,16 @@ Messages and Services (:program:`ros2 interface`) ================================================= -If by now you haven't particularly fallen in love with ROS2, fear not. Indeed, we haven't done much so far that couldn't be achieved more easily by other means. +If by now you haven't particularly fallen in love with :program:`ROS2`, fear not. Indeed, we haven't done much so far that couldn't be achieved more easily by other means. -ROS2 begins to shine most in its interprocess communication, through what are called `ROS2 interfaces `_. In particular, the fact that we can easily interface Nodes written in Python and C++ is a strong selling point. +:program:`ROS2` begins to shine most in its interprocess communication, through what are called `ROS2 interfaces `_. In particular, the fact that we can easily interface Nodes written in Python and C++ is a strong selling point. :code:`Messages` are one of the three types of ROS2 interfaces. This will most likely be the standard of communication between Nodes in your packages. We will also see the bidirectional :code:`Services` now. The last type of interface, :code:`Actions`, is left for another section. Description ----------- -In ROS2, interfaces are files written in the ROS2 :abbr:`IDL (Interface Description Language)`. Each type of interface is described in a :file:`.msg` file (or :file:`.srv` file), which is then built by :program:`colcon` into libraries that can be imported into your Python programs. +In :program:`ROS2`, interfaces are files written in the ROS2 :abbr:`IDL (Interface Description Language)`. Each type of interface is described in a :file:`.msg` file (or :file:`.srv` file), which is then built by :program:`colcon` into libraries that can be imported into your Python programs. When dealing with common robotics concepts such as geometric and sensor messages, it is good practice to use interfaces that already exist in ROS2, instead of creating new ones that serve the exact same purpose. In addition, for complicated interfaces, we can combine existing ones for simplicity. @@ -52,7 +52,7 @@ This shows that with :program:`ros2 interface list` we can get a list of all int to get the list of packages with interfaces available, which returns something similar to .. code-block:: console - :emphasize-lines: 8, 19 + :emphasize-lines: 8, 21 action_msgs action_tutorials_interfaces @@ -66,13 +66,16 @@ to get the list of packages with interfaces available, which returns something s logging_demo map_msgs nav_msgs + package_with_interfaces pcl_msgs pendulum_msgs rcl_interfaces rmw_dds_common rosbag2_interfaces rosgraph_msgs + sas_msgs sensor_msgs + service_msgs shape_msgs statistics_msgs std_msgs @@ -81,6 +84,7 @@ to get the list of packages with interfaces available, which returns something s tf2_msgs trajectory_msgs turtlesim + type_description_interfaces unique_identifier_msgs visualization_msgs @@ -101,39 +105,39 @@ which returns .. code:: console + example_interfaces/msg/UInt16 + example_interfaces/msg/Empty + example_interfaces/action/Fibonacci example_interfaces/msg/String - example_interfaces/srv/AddTwoInts - example_interfaces/srv/SetBool - example_interfaces/msg/UInt8 - example_interfaces/msg/Int64MultiArray - example_interfaces/msg/Byte - example_interfaces/msg/Float32 - example_interfaces/msg/Int64 + example_interfaces/msg/Int32 example_interfaces/msg/UInt32MultiArray - example_interfaces/msg/Int32MultiArray - example_interfaces/msg/Empty + example_interfaces/msg/Float64MultiArray example_interfaces/msg/Float32MultiArray + example_interfaces/srv/AddTwoInts + example_interfaces/msg/UInt8MultiArray + example_interfaces/msg/Int8 example_interfaces/msg/Int16MultiArray - example_interfaces/action/Fibonacci - example_interfaces/msg/UInt16MultiArray + example_interfaces/msg/UInt32 + example_interfaces/srv/SetBool + example_interfaces/msg/Int64 + example_interfaces/msg/MultiArrayDimension example_interfaces/msg/Int8MultiArray - example_interfaces/msg/Bool example_interfaces/msg/ByteMultiArray - example_interfaces/msg/MultiArrayLayout - example_interfaces/msg/UInt8MultiArray - example_interfaces/msg/UInt16 + example_interfaces/msg/Int32MultiArray + example_interfaces/srv/Trigger + example_interfaces/msg/Int64MultiArray + example_interfaces/msg/Float64 + example_interfaces/msg/Byte example_interfaces/msg/Int16 - example_interfaces/msg/Int8 - example_interfaces/msg/MultiArrayDimension + example_interfaces/msg/UInt16MultiArray + example_interfaces/msg/UInt64MultiArray example_interfaces/msg/Char - example_interfaces/msg/Float64 - example_interfaces/srv/Trigger + example_interfaces/msg/UInt8 + example_interfaces/msg/Bool example_interfaces/msg/UInt64 example_interfaces/msg/WString - example_interfaces/msg/Int32 - example_interfaces/msg/Float64MultiArray - example_interfaces/msg/UInt64MultiArray - example_interfaces/msg/UInt32 + example_interfaces/msg/MultiArrayLayout + example_interfaces/msg/Float32 Messages -------- diff --git a/docs/source/parameters_and_launch.rst b/docs/source/parameters_and_launch.rst index 53c28845..ea1c1f04 100644 --- a/docs/source/parameters_and_launch.rst +++ b/docs/source/parameters_and_launch.rst @@ -1,5 +1,3 @@ -.. include:: the_topic_is_under_heavy_construction.rst - Parameters: creating configurable Nodes ======================================= diff --git a/docs/source/peppeteer-config.json b/docs/source/peppeteer-config.json new file mode 100644 index 00000000..dce67b8e --- /dev/null +++ b/docs/source/peppeteer-config.json @@ -0,0 +1,3 @@ +{ + "args": ["--no-sandbox"] +} \ No newline at end of file diff --git a/docs/source/preamble.rst b/docs/source/preamble.rst deleted file mode 100644 index 7f273692..00000000 --- a/docs/source/preamble.rst +++ /dev/null @@ -1,8 +0,0 @@ -Preamble -======== - -.. toctree:: - - preamble/python_basics - preamble/python_best_practices - diff --git a/docs/source/preamble/python/python_best_practices.rst b/docs/source/preamble/python/python_best_practices.rst index 656e3844..aefbef51 100644 --- a/docs/source/preamble/python/python_best_practices.rst +++ b/docs/source/preamble/python/python_best_practices.rst @@ -162,7 +162,7 @@ because our file does not have the permission to run as an executable. To give i and now we can run it properly with -.. code-block:: commandline +.. code-block:: console cd ~/ros2_tutorials_preamble/python/minimalist_package/minimalist_package ./minimalist_script.py @@ -448,14 +448,14 @@ For a quick jolt of instant gratification, let's run the tests before we proceed There are many ways to run tests written with :code:`unittest`. The following will run all tests found in the folder :file:`test` -.. code-block:: commandLine +.. code-block:: console cd ~/ros2_tutorials_preamble/python/minimalist_package python -m unittest discover -v test which will output -.. code-block:: commandLine +.. code-block:: console test_attribute (test_minimalist_class.TestMinimalistClass) ... ok test_get_set_private_attribute (test_minimalist_class.TestMinimalistClass) ... ok diff --git a/docs/source/publishers_and_subscribers.rst b/docs/source/publishers_and_subscribers.rst index b84e50e2..a2edc87a 100644 --- a/docs/source/publishers_and_subscribers.rst +++ b/docs/source/publishers_and_subscribers.rst @@ -5,17 +5,30 @@ Publishers and Subscribers: using messages Except for the particulars of the :file:`setup.py` file, the way that publishers and subscribers in ROS2 work in Python, i.e. the explanation in this section, does not depend on :program:`ament_python` or :program:`ament_cmake`. -Finally, we reached the point where ROS2 becomes appealing. As you saw in the last section, we can easily create complex interface types using an easy and generic description. +Finally, we reached the point where :program:`ROS2` becomes appealing. As you saw in the last section, we can easily create complex interface types using an easy and generic description. We can use those to provide `interprocess communication `_, i.e. two different programs talking to each other, which otherwise can be error-prone and very difficult to implement. -ROS2 works on a model in which any number of processes can communicate over a :code:`Topic` that only accepts one message type. Each topic is uniquely identified by a string. +:program:`ROS2` messages work on a model in which any number of processes can communicate over a :code:`Topic` that only accepts one message type. Each topic is uniquely identified by a string. Then -- A program that sends (publishes) information to the topic has a :code:`Publisher`. -- A program that reads (subscribes) information from a topic has a :code:`Subscriber`. +- A program that sends (publishes) information to the topic has one or more :code:`Publisher` \(s). +- A program that reads (subscribes) information from a topic has one or more :code:`Subscriber` \(s). -Each Node can have any number of :code:`Publishers` and :code:`Subscribers` and a combination thereof, connecting to an arbitrary number of Nodes. This forms part of the connections in the so-called `ROS graph `_. +Each Node can have any number of :code:`Publishers` and :code:`Subscribers` and a combination thereof, connecting to an arbitrary number of Nodes. This forms part of the connections in the so-called `ROS graph `_. An example is shown below. + +.. mermaid:: + + %%{init: { "theme" : "dark" }}%% + graph LR; + A[Publisher #1] --> B[Topic] + C[Publisher #2] --> B + B --> D[Subscriber #1] + +.. note:: + + This is an abstraction. As long as the information flows in this manner, it does not mean that an entity called ``topic`` must exist. + In :program:`ROS2`, this type of communication happens, in fact, peer-to-peer. Create the package ------------------ @@ -29,6 +42,46 @@ First, let us create an :program:`ament_python` package that depends on our newl --build-type ament_python \ --dependencies rclpy package_with_interfaces +.. dropdown:: ros2 pkg create output + + .. code :: console + + going to create a new package + package name: python_package_that_uses_the_messages + destination directory: /root/ros2_tutorial_workspace/src + package format: 3 + version: 0.0.0 + description: TODO: Package description + maintainer: ['root '] + licenses: ['TODO: License declaration'] + build type: ament_python + dependencies: ['rclpy', 'package_with_interfaces'] + creating folder ./python_package_that_uses_the_messages + creating ./python_package_that_uses_the_messages/package.xml + creating source folder + creating folder ./python_package_that_uses_the_messages/python_package_that_uses_the_messages + creating ./python_package_that_uses_the_messages/setup.py + creating ./python_package_that_uses_the_messages/setup.cfg + creating folder ./python_package_that_uses_the_messages/resource + creating ./python_package_that_uses_the_messages/resource/python_package_that_uses_the_messages + creating ./python_package_that_uses_the_messages/python_package_that_uses_the_messages/__init__.py + creating folder ./python_package_that_uses_the_messages/test + creating ./python_package_that_uses_the_messages/test/test_copyright.py + creating ./python_package_that_uses_the_messages/test/test_flake8.py + creating ./python_package_that_uses_the_messages/test/test_pep257.py + + [WARNING]: Unknown license 'TODO: License declaration'. This has been set in the package.xml, but no LICENSE file has been created. + It is recommended to use one of the ament license identifiers: + Apache-2.0 + BSL-1.0 + BSD-2.0 + BSD-2-Clause + BSD-3-Clause + GPL-3.0-only + LGPL-3.0-only + MIT + MIT-0 + Overview -------- @@ -134,7 +187,7 @@ For the subscriber Node, create a file in :file:`python_package_that_uses_the_me :language: python :linenos: :lines: 24- - :emphasize-lines: 3, 11-15, 17-30 + :emphasize-lines: 3, 11-15, 17-31 Similarly to the publisher, in the subscriber, we start by importing the message in question @@ -155,7 +208,7 @@ where the only difference with respect to the publisher is the third argument, n .. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_messages/python_package_that_uses_the_messages/amazing_quote_subscriber_node.py :language: python :lines: 40-53 - :emphasize-lines: 7,11,13 + :emphasize-lines: 8,12,14 That callback method will be automatically called by ROS2, as one of the tasks performed by :code:`rclpy.spin(Node)`. Depending on the :code:`qos_profile`, it will not necessarily be the latest message. @@ -207,41 +260,30 @@ which outputs .. code:: console - [INFO] [1684215672.344532584] [amazing_quote_subscriber_node]: - I have received the most amazing of quotes. - It says - - 'Use the force, Pikachu!' - - And was thought by the following genius - - -- Uncle Ben - - This latest quote had the id=3. + [INFO] [1753664072.638312553] [amazing_quote_subscriber_node]: + I have received the most amazing of quotes. + It says - [INFO] [1684215672.844618237] [amazing_quote_subscriber_node]: - I have received the most amazing of quotes. - It says + 'Use the force, Pikachu!' - 'Use the force, Pikachu!' + And was thought by the following genius - And was thought by the following genius + -- Uncle Ben - -- Uncle Ben + This latest quote had the id=37. - This latest quote had the id=4. + [INFO] [1753664073.121886428] [amazing_quote_subscriber_node]: + I have received the most amazing of quotes. + It says - [INFO] [1684215673.344514856] [amazing_quote_subscriber_node]: - I have received the most amazing of quotes. - It says + 'Use the force, Pikachu!' - 'Use the force, Pikachu!' + And was thought by the following genius - And was thought by the following genius + -- Uncle Ben - -- Uncle Ben + This latest quote had the id=38. - This latest quote had the id=5. .. note:: diff --git a/docs/source/python_node_explained.rst b/docs/source/python_node_explained.rst index 6ee6ef24..0155d628 100644 --- a/docs/source/python_node_explained.rst +++ b/docs/source/python_node_explained.rst @@ -76,7 +76,9 @@ Have a :code:`try-catch` block for :code:`KeyboardInterrupt` You can see more about this topic at :ref:`Python try catch`, in the preamble. -In the current version of the `official ROS2 examples `_, for reasons beyond my comprehension, this step is not followed. +.. important:: + + In the current version of the `official ROS2 examples `_, for reasons beyond my comprehension, this step is not followed. However, when running Nodes either in the terminal or in :program:`PyCharm`, catching a :code:`KeyboardInterrupt` is the only reliable way to finish the Nodes cleanly. A :code:`KeyboardInterrupt` is emitted at a terminal by pressing :kbd:`CTRL+C`, whereas it is emitted by :program:`PyCharm` when pressing :guilabel:`Stop`. @@ -87,6 +89,20 @@ That is particularly important when real robots need to be gracefully shut down :lines: 43-58 :emphasize-lines: 7,13 +.. important:: + + Despite what is shown in the official ROS2 examples, do not add these to your code. This will cause the node to crash when + shutting down and not return 0 as expected by anything automatic running your nodes. This can cause many confusing behaviours. + + .. code-block:: python + + # Destroy the node explicitly + # (optional - otherwise it will be done automatically + # when the garbage collector destroys the node object) + minimal_publisher.destroy_node() + rclpy.shutdown() + + Document your code with Docstrings ---------------------------------- diff --git a/docs/source/requirements.txt b/docs/source/requirements.txt index afb6a04d..fe98e99e 100644 --- a/docs/source/requirements.txt +++ b/docs/source/requirements.txt @@ -4,3 +4,4 @@ sphinx-design sphinx-rtd-theme sphinxext-remoteliteralinclude sphinx-book-theme +sphinxcontrib-mermaid \ No newline at end of file diff --git a/docs/source/running_node.rst b/docs/source/running_node.rst index fd06a666..c4436bbd 100644 --- a/docs/source/running_node.rst +++ b/docs/source/running_node.rst @@ -27,8 +27,7 @@ which returns the most relevant arguments :code:`package_name` and :code:`execut options: -h, --help show this help message and exit - --prefix PREFIX Prefix command, which should go before the executable. Command must be wrapped - in quotes if it contains spaces (e.g. --prefix 'gdb -ex run --args'). + --prefix PREFIX Prefix command, which should go before the executable. Command must be wrapped in quotes if it contains spaces (e.g. --prefix 'gdb -ex run --args'). Back to our example, with a properly sourced terminal, the example node can be executed with diff --git a/docs/source/service_servers_and_clients.rst b/docs/source/service_servers_and_clients.rst index b7da5d54..81131b30 100644 --- a/docs/source/service_servers_and_clients.rst +++ b/docs/source/service_servers_and_clients.rst @@ -6,6 +6,10 @@ At your Service: Servers and Clients Except for the particulars of the :file:`setup.py` file, the way that services in ROS2 work in Python, i.e. the explanation in this section, does not depend on :program:`ament_python` or :program:`ament_cmake`. +.. seealso:: + + The contents of this session were simplified in this version. A more complex example is shown in https://ros2-tutorial.readthedocs.io/en/humble/service_servers_and_clients.html. + In some cases, we need means of communication in which each command has an associated response. That is where :code:`Services` come into play. Create the package @@ -20,6 +24,46 @@ We start by creating a package to use the :code:`Service` we first created in :r --build-type ament_python \ --dependencies rclpy package_with_interfaces +.. dropdown:: ros2 pkg create output + + .. code :: console + + going to create a new package + package name: python_package_that_uses_the_services + destination directory: /root/ros2_tutorial_workspace/src + package format: 3 + version: 0.0.0 + description: TODO: Package description + maintainer: ['root '] + licenses: ['TODO: License declaration'] + build type: ament_python + dependencies: ['rclpy', 'package_with_interfaces'] + creating folder ./python_package_that_uses_the_services + creating ./python_package_that_uses_the_services/package.xml + creating source folder + creating folder ./python_package_that_uses_the_services/python_package_that_uses_the_services + creating ./python_package_that_uses_the_services/setup.py + creating ./python_package_that_uses_the_services/setup.cfg + creating folder ./python_package_that_uses_the_services/resource + creating ./python_package_that_uses_the_services/resource/python_package_that_uses_the_services + creating ./python_package_that_uses_the_services/python_package_that_uses_the_services/__init__.py + creating folder ./python_package_that_uses_the_services/test + creating ./python_package_that_uses_the_services/test/test_copyright.py + creating ./python_package_that_uses_the_services/test/test_flake8.py + creating ./python_package_that_uses_the_services/test/test_pep257.py + + [WARNING]: Unknown license 'TODO: License declaration'. This has been set in the package.xml, but no LICENSE file has been created. + It is recommended to use one of the ament license identifiers: + Apache-2.0 + BSL-1.0 + BSD-2.0 + BSD-2-Clause + BSD-3-Clause + GPL-3.0-only + LGPL-3.0-only + MIT + MIT-0 + Overview -------- @@ -45,30 +89,30 @@ Create the Node with a Service Server #. Add the new Node to :file:`setup.py` -Let's start by creating a :file:`what_is_the_point_service_server_node.py` in :file:`~/ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services` with the following contents +Let's start by creating a :file:`add_points_service_server_node.py` in :file:`~/ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services` with the following contents -:download:`what_is_the_point_service_server_node.py <../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/what_is_the_point_service_server_node.py>` +:download:`add_points_service_server_node.py <../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/add_points_service_server_node.py>` -.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/what_is_the_point_service_server_node.py +.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/add_points_service_server_node.py :language: python :lines: 24- The code begins with an import to the service we created. No surprise here. -.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/what_is_the_point_service_server_node.py +.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/add_points_service_server_node.py :language: python :lines: 24-29 :emphasize-lines: 6 The Service Server must be initialised with the :code:`create_service()`, as follows, with parameters that should by now be quite obvious to us. -.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/what_is_the_point_service_server_node.py +.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/add_points_service_server_node.py :language: python :lines: 38-41 -The Service Server receives a :code:`WhatIsThePoint.Request` and returns a :code:`WhatIsThePoint.Response`. +The Service Server receives a :code:`AddPoints.Request` and returns a :code:`AddPoints.Response`. -.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/what_is_the_point_service_server_node.py +.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/add_points_service_server_node.py :language: python :lines: 45-52 :emphasize-lines: 2,4 @@ -78,18 +122,17 @@ The Service Server receives a :code:`WhatIsThePoint.Request` and returns a :code The API for the Service Server callback is a bit weird in that needs the :code:`Response` as an argument. This API `might change `_, but for now this is what we got. -We play around with the :code:`WhatIsThePoint.Request` a bit and use that result to populate a :code:`WhatIsThePoint.Response`, as follows +We use the members of :code:`AddPoints.Request` to calculate and populate the :code:`AddPoints.Response`, as follows -.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/what_is_the_point_service_server_node.py +.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/add_points_service_server_node.py :language: python - :lines: 66-69 + :lines: 53-55 -At the end of the callback, we must return that :code:`WhatIsThePoint.Request`, like so +At the end of the callback, we must return that :code:`AddPoints.Request`, like so -.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/what_is_the_point_service_server_node.py +.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/add_points_service_server_node.py :language: python - :lines: 85 - :emphasize-lines: 2 + :lines: 57 The Service Server was quite painless, but it doesn't do much. The Service Client might be a bit more on the painful side for the uninitiated. @@ -123,11 +166,11 @@ The Node This implementation shown herein uses a callback and :code:`rclpy.spin()`. It has many practical applications, but it's no *panacea*. -We start by adding a :file:`what_is_the_point_service_client_node.py` at :file:`python_package_that_uses_the_services/python_package_that_uses_the_services` with the following contents. +We start by adding a :file:`add_points_service_client_node.py` at :file:`python_package_that_uses_the_services/python_package_that_uses_the_services` with the following contents. -:download:`what_is_the_point_service_client_node.py <../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/what_is_the_point_service_client_node.py>` +:download:`add_points_service_client_node.py <../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/add_points_service_client_node.py>` -.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/what_is_the_point_service_client_node.py +.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/add_points_service_client_node.py :language: python :linenos: :lines: 24- @@ -137,7 +180,7 @@ Imports To have access to the service, we import it with :code:`from .srv import `. -.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/what_is_the_point_service_client_node.py +.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/add_points_service_client_node.py :language: python :lines: 31 @@ -146,7 +189,7 @@ Instantiate a Service Client We instantiate a Service Client with :code:`Node.create_client()`. The values of :code:`srv_type` and :code:`srv_name` must match the ones used in the Service Server. -.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/what_is_the_point_service_client_node.py +.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/add_points_service_client_node.py :language: python :lines: 40-42 @@ -158,7 +201,7 @@ We instantiate a Service Client with :code:`Node.create_client()`. The values of In many cases, having the result of the service is of particular importance (hence the use of a service and not messages). In that case, we have to wait until :code:`service_client.wait_for_service()`, as shown below. -.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/what_is_the_point_service_client_node.py +.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/add_points_service_client_node.py :language: python :lines: 44,45 @@ -167,7 +210,7 @@ Instantiate a :code:`Future` as a class attribute As part of the :code:`async` framework, we instantiate a :code:`Future` (`More info `_). In this example it is important to have it as an attribute of the class so that we do not lose the reference to it after the callback. -.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/what_is_the_point_service_client_node.py +.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/add_points_service_client_node.py :language: python :lines: 47 @@ -176,13 +219,13 @@ Instantiate a Timer Whenever periodic work must be done, it is recommended to use a :code:`Timer`, as we already learned in :ref:`Use a Timer for periodic work`. -.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/what_is_the_point_service_client_node.py +.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/add_points_service_client_node.py :language: python :lines: 49-52 The need for a callback for the :code:`Timer`, should also be no surprise. -.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/what_is_the_point_service_client_node.py +.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/add_points_service_client_node.py :language: python :lines: 54-55 @@ -191,7 +234,7 @@ Service Clients use :code:`.Request()` Given that services work in a request-response model, the Service Client must instantiate a suitable :code:`.Request()` and populate its fields before making the service call, as shown below. To make the example more interesting, it randomly switches between two possible quotes. -.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/what_is_the_point_service_client_node.py +.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/add_points_service_client_node.py :language: python :lines: 57-65 @@ -210,14 +253,14 @@ There are many ways to address the use of a :code:`Future`. One of them, special The benefit of this is that the callback will not block our resources until the response is ready. When the response is ready, and the ROS2 executor gets to processing :code:`Future` callbacks, our callback will be called *automagically*. -.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/what_is_the_point_service_client_node.py +.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/add_points_service_client_node.py :language: python :lines: 67-72 :emphasize-lines: 5,6 Given that we are periodically calling the service, before replace the class :code:`Future` with the next service call, we can check if the service call was done with :code:`Future.done()`. If it is not done, we can use :code:`Future.cancel()` so that our callback can handle this case as well. For instance, if the Service Server has been shutdown, the :code:`Future` would never be done. -.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/what_is_the_point_service_client_node.py +.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/add_points_service_client_node.py :language: python :lines: 67-72 :emphasize-lines: 1-4 @@ -229,7 +272,7 @@ The callback for the :code:`Future` must receive a :code:`Future` as an argument The result of the :code:`Future` is obtained using :code:`Future.result()`. The response might be :code:`None` in some cases, so we must check it before trying to use the result, otherwise we will get a nasty exception. -.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/what_is_the_point_service_client_node.py +.. literalinclude:: ../../ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/add_points_service_client_node.py :language: python :lines: 74-88 :emphasize-lines: 1,3,4 @@ -258,73 +301,33 @@ Testing Service Server and Client .. code:: console - ros2 run python_package_that_uses_the_services what_is_the_point_service_client_node + ros2 run python_package_that_uses_the_services add_points_service_client_node when running the client Node, the server is still not active. In that case, the client node will keep waiting for it, as follows .. code:: console - [INFO] [1684293008.888276849] [what_is_the_point_service_client]: service /what_is_the_point not available, waiting... - [INFO] [1684293009.890589539] [what_is_the_point_service_client]: service /what_is_the_point not available, waiting... - [INFO] [1684293010.892778194] [what_is_the_point_service_client]: service /what_is_the_point not available, waiting... + [INFO] [1753667386.959416097] [add_points_service_client]: service /add_points not available, waiting... + [INFO] [1753667387.967904375] [add_points_service_client]: service /add_points not available, waiting... + [INFO] [1753667388.978200250] [add_points_service_client]: service /add_points not available, waiting... -In another terminal, we run the :program:`what_is_the_point_service_server_node`, as follows +In another terminal, we run the :program:`add_points_service_server_node`, as follows .. code:: console - ros2 run python_package_that_uses_the_services what_is_the_point_service_server_node + ros2 run python_package_that_uses_the_services add_points_service_server_node -The server Node will then output, periodically, +The server Node will output nothing, whereas the client Node will output, periodically, .. code:: console - [INFO] [1684485151.608507798] [what_is_the_point_service_server]: - This is the call number 1 to this Service Server. - The analysis of the AmazingQuote below is complete. - - [...] your living... it is always potatoes. I dream of potatoes. - - -- a young Maltese potato farmer - - The point has been sent back to the client. - - [INFO] [1684485152.092508332] [what_is_the_point_service_server]: - This is the call number 2 to this Service Server. - The analysis of the AmazingQuote below is complete. - - I wonder about the Ultimate Question of Life, the Universe, and Everything. - - -- Creators of Deep Thought - - The point has been sent back to the client. - - [INFO] [1684485152.592516148] [what_is_the_point_service_server]: - This is the call number 3 to this Service Server. - The analysis of the AmazingQuote below is complete. - - I wonder about the Ultimate Question of Life, the Universe, and Everything. - - -- Creators of Deep Thought - - The point has been sent back to the client. - -and the client Node will output, periodically, - -.. code:: console - - [INFO] [1684485151.609611689] [what_is_the_point_service_client]: - We have thus received the point of our quote. - - (18.199457100225292, 33.14595477433704, 52.65262570058381) - - [INFO] [1684485152.093228181] [what_is_the_point_service_client]: - We have thus received the point of our quote. - - (11.17170193214362, 9.384897014549527, 21.443401053306854) - - [INFO] [1684485152.593294259] [what_is_the_point_service_client]: - We have thus received the point of our quote. - - (16.58535176162403, 0.6180505400411676, 24.796597698334804) + [INFO] [1753667415.876223138] [add_points_service_client]: The result was (853.122385593111, 613.3399959983066, 722.6376752208978) + [INFO] [1753667416.373657638] [add_points_service_client]: The result was (645.418992882397, 560.9466217293334, 874.7214190239486) + [INFO] [1753667416.875945305] [add_points_service_client]: The result was (1270.7448356640075, 345.69676803639936, 953.6879012399689) + [INFO] [1753667417.376013639] [add_points_service_client]: The result was (1203.944887411107, 733.5131783020975, 927.902266740569) + [INFO] [1753667417.872921291] [add_points_service_client]: The result was (671.9458297091917, 1210.490009902154, 545.6078440547075) + [INFO] [1753667418.371451541] [add_points_service_client]: The result was (629.0766519110047, 872.0699525880541, 581.7396957576223) + [INFO] [1753667418.873213958] [add_points_service_client]: The result was (579.0485532702639, 1714.0365146695003, 396.1743388215037) + [INFO] [1753667419.375147834] [add_points_service_client]: The result was (545.2849740451343, 1629.1832720438556, 945.1871456532875) diff --git a/docs/source/using_python_library.rst b/docs/source/using_python_library.rst index e3e09191..3ecfdaa4 100644 --- a/docs/source/using_python_library.rst +++ b/docs/source/using_python_library.rst @@ -22,11 +22,11 @@ resulting in yet another version of our favorite wall of text going to create a new package package name: python_package_that_uses_the_library - destination directory: /home/murilo/ros2_tutorial_workspace/src + destination directory: /root/ros2_tutorial_workspace/src package format: 3 version: 0.0.0 description: TODO: Package description - maintainer: ['murilo '] + maintainer: ['root '] licenses: ['TODO: License declaration'] build type: ament_python dependencies: ['rclpy', 'python_package_with_a_library'] @@ -47,7 +47,7 @@ resulting in yet another version of our favorite wall of text creating ./python_package_that_uses_the_library/python_package_that_uses_the_library/node_that_uses_the_library.py [WARNING]: Unknown license 'TODO: License declaration'. This has been set in the package.xml, but no LICENSE file has been created. - It is recommended to use one of the ament license identitifers: + It is recommended to use one of the ament license identifiers: Apache-2.0 BSL-1.0 BSD-2.0 @@ -105,10 +105,12 @@ Which outputs something similar to the shown below, but with different numbers a .. code :: console - [INFO] [1683598288.149167944] [node_that_uses_the_library]: sample_function_for_square_of_sum(0.19395834493833486,1.3891603395040568) returned 2.506264769030609. - [INFO] [1683598288.149643378] [node_that_uses_the_library]: sample_class_with_random_name.get_name() returned qyOXLBEtzZ. - [INFO] [1683598288.616095880] [node_that_uses_the_library]: sample_function_for_square_of_sum(0.7387236329957096,1.7650481260672202) returned 6.2688730214810775. - [INFO] [1683598288.616604769] [node_that_uses_the_library]: sample_class_with_random_name.get_name() returned LCFNFyzwhk. - [INFO] [1683598289.116050219] [node_that_uses_the_library]: sample_function_for_square_of_sum(0.003813494022560704,1.7056916575839387) returned 2.9224078633691604. - [INFO] [1683598289.116553899] [node_that_uses_the_library]: sample_class_with_random_name.get_name() returned wrtTlOdanZ. + [INFO] [1753585839.509922172] [node_that_uses_the_library]: sample_function_for_square_of_sum(0.9787232004970391,1.7320908702316369) returned 7.348512926060575. + [INFO] [1753585839.510400755] [node_that_uses_the_library]: sample_class_with_random_name.get_name() returned GQkUZgSkje. + [INFO] [1753585839.993999505] [node_that_uses_the_library]: sample_function_for_square_of_sum(0.7637556876478347,1.0377535114838756) returned 3.2454353945561767. + [INFO] [1753585839.994351922] [node_that_uses_the_library]: sample_class_with_random_name.get_name() returned EztAWYuFMB. + [INFO] [1753585840.494895006] [node_that_uses_the_library]: sample_function_for_square_of_sum(0.26638758438777976,1.1809445792770386) returned 2.0947703919786846. + [INFO] [1753585840.495950422] [node_that_uses_the_library]: sample_class_with_random_name.get_name() returned ITLIHPOMgv. + [INFO] [1753585840.994468589] [node_that_uses_the_library]: sample_function_for_square_of_sum(0.5244531764161572,1.7524376840394509) returned 5.184231990426279. + [INFO] [1753585840.994695797] [node_that_uses_the_library]: sample_class_with_random_name.get_name() returned LGtybBngKv. diff --git a/docs/source/workspace_setup.rst b/docs/source/workspace_setup.rst index 4b30d918..758a2c03 100644 --- a/docs/source/workspace_setup.rst +++ b/docs/source/workspace_setup.rst @@ -36,22 +36,27 @@ for which the output will be something similar to .. code :: console - Summary: 0 packages finished [0.17s] + Summary: 0 packages finished [0.08s] given that we have an empty workspace, no surprise here. The folders :code:`build`, :code:`install`, and :code:`log` have been generated automatically by :program:`colcon`. The project structure becomes as follows. .. code-block:: console - :emphasize-lines: 3,4,5 + :emphasize-lines: 2-4 ros2_tutorial_workspace/ - └── src/ - └── build/ - └── install/ - └── log/ + |-- build + |-- install + |-- log + `-- src -Inside the :code:`install` folder lie all programs etc generated by the project that can be accessed by the users. +Inside the :code:`install` folder lie everything in the project that can be accessed by the users. + +.. note:: + + An easy way to understand if something is not accessible via :program:`ROS2` commands is if they cannot be found + inside your `install` folder. Do the following just once, so that all terminal windows automatically source this new workspace for you. diff --git a/ros2_tutorial_workspace/src/package_with_interfaces/CMakeLists.txt b/ros2_tutorial_workspace/src/package_with_interfaces/CMakeLists.txt index 10812801..5b12b6ae 100644 --- a/ros2_tutorial_workspace/src/package_with_interfaces/CMakeLists.txt +++ b/ros2_tutorial_workspace/src/package_with_interfaces/CMakeLists.txt @@ -17,9 +17,10 @@ find_package(rosidl_default_generators REQUIRED) set(interface_files # Messages "msg/AmazingQuote.msg" + "msg/AmazingQuoteStamped.msg" # Services - "srv/WhatIsThePoint.srv" + "srv/AddPoints.srv" ) diff --git a/ros2_tutorial_workspace/src/package_with_interfaces/msg/AmazingQuoteStamped.msg b/ros2_tutorial_workspace/src/package_with_interfaces/msg/AmazingQuoteStamped.msg new file mode 100644 index 00000000..57e37150 --- /dev/null +++ b/ros2_tutorial_workspace/src/package_with_interfaces/msg/AmazingQuoteStamped.msg @@ -0,0 +1,4 @@ +# AmazingQuoteStamped.msg from https://ros2-tutorial.readthedocs.io +# An AmazingQuote.msg with a header +std_msgs/Header header +AmazingQuote quote \ No newline at end of file diff --git a/ros2_tutorial_workspace/src/package_with_interfaces/srv/AddPoints.srv b/ros2_tutorial_workspace/src/package_with_interfaces/srv/AddPoints.srv new file mode 100644 index 00000000..c5ec6dde --- /dev/null +++ b/ros2_tutorial_workspace/src/package_with_interfaces/srv/AddPoints.srv @@ -0,0 +1,6 @@ +# AddPoints.srv from https://ros2-tutorial.readthedocs.io +# Adds the values of points `a` and `b` to give the output `result` +geometry_msgs/Point a +geometry_msgs/Point b +--- +geometry_msgs/Point result \ No newline at end of file diff --git a/ros2_tutorial_workspace/src/package_with_interfaces/srv/WhatIsThePoint.srv b/ros2_tutorial_workspace/src/package_with_interfaces/srv/WhatIsThePoint.srv deleted file mode 100644 index 1fb07b05..00000000 --- a/ros2_tutorial_workspace/src/package_with_interfaces/srv/WhatIsThePoint.srv +++ /dev/null @@ -1,5 +0,0 @@ -# WhatIsThePoint.srv from https://ros2-tutorial.readthedocs.io -# Receives an AmazingQuote and returns what is the point -AmazingQuote quote ---- -geometry_msgs/Point point \ No newline at end of file diff --git a/ros2_tutorial_workspace/src/python_package_that_uses_the_messages/setup.py b/ros2_tutorial_workspace/src/python_package_that_uses_the_messages/setup.py index 3f8c58fb..fe716f73 100644 --- a/ros2_tutorial_workspace/src/python_package_that_uses_the_messages/setup.py +++ b/ros2_tutorial_workspace/src/python_package_that_uses_the_messages/setup.py @@ -1,11 +1,11 @@ -from setuptools import setup +from setuptools import find_packages, setup package_name = 'python_package_that_uses_the_messages' setup( name=package_name, version='0.0.0', - packages=[package_name], + packages=find_packages(exclude=['test']), data_files=[ ('share/ament_index/resource_index/packages', ['resource/' + package_name]), @@ -13,8 +13,8 @@ ], install_requires=['setuptools'], zip_safe=True, - maintainer='murilo', - maintainer_email='murilomarinho@ieee.org', + maintainer='root', + maintainer_email='murilo.marinho@manchester.ac.uk', description='TODO: Package description', license='TODO: License declaration', tests_require=['pytest'], diff --git a/ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/what_is_the_point_service_client_node.py b/ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/add_points_service_client_node.py similarity index 65% rename from ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/what_is_the_point_service_client_node.py rename to ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/add_points_service_client_node.py index fea7026c..a8f23c6c 100644 --- a/ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/what_is_the_point_service_client_node.py +++ b/ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/add_points_service_client_node.py @@ -1,7 +1,7 @@ """ MIT LICENSE -Copyright (C) 2023 Murilo Marques Marinho (www.murilomarinho.info) +Copyright (C) 2023-2025 Murilo Marques Marinho (www.murilomarinho.info) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -28,18 +28,18 @@ from rclpy.task import Future from rclpy.node import Node -from package_with_interfaces.srv import WhatIsThePoint +from package_with_interfaces.srv import AddPoints -class WhatIsThePointServiceClientNode(Node): - """A ROS2 Node with a Service Client for WhatIsThePoint.""" +class AddPointsServiceClientNode(Node): + """A ROS2 Node with a Service Client for AddPoints.""" def __init__(self): - super().__init__('what_is_the_point_service_client') + super().__init__('add_points_service_client') self.service_client = self.create_client( - srv_type=WhatIsThePoint, - srv_name='/what_is_the_point') + srv_type=AddPoints, + srv_name='/add_points') while not self.service_client.wait_for_service(timeout_sec=1.0): self.get_logger().info(f'service {self.service_client.srv_name} not available, waiting...') @@ -54,19 +54,19 @@ def __init__(self): def timer_callback(self): """Method that is periodically called by the timer.""" - request = WhatIsThePoint.Request() - if random.uniform(0, 1) < 0.5: - request.quote.quote = "I wonder about the Ultimate Question of Life, the Universe, and Everything." - request.quote.philosopher_name = "Creators of Deep Thought" - request.quote.id = 1979 - else: - request.quote.quote = """[...] your living... it is always potatoes. I dream of potatoes.""" - request.quote.philosopher_name = "a young Maltese potato farmer" - request.quote.id = 2013 + request = AddPoints.Request() + + request.a.x = random.uniform(0, 1000) + request.a.y = random.uniform(0, 1000) + request.a.y = random.uniform(0, 1000) + + request.b.x = random.uniform(0, 1000) + request.b.y = random.uniform(0, 1000) + request.b.z = random.uniform(0, 1000) if self.future is not None and not self.future.done(): self.future.cancel() # Cancel the future. The callback will be called with Future.result == None. - self.get_logger().info("Service Future canceled. The Node took too long to process the service call." + self.get_logger().warn("Service Future canceled. The Node took too long to process the service call." "Is the Service Server still alive?") self.future = self.service_client.call_async(request) self.future.add_done_callback(self.process_response) @@ -75,15 +75,9 @@ def process_response(self, future: Future): """Callback for the future, that will be called when it is done""" response = future.result() if response is not None: - self.get_logger().info(dedent(f""" - We have thus received the point of our quote. - - {(response.point.x, response.point.y, response.point.z)} - """)) + self.get_logger().info(f"The result was {(response.result.x, response.result.y, response.result.z)}") else: - self.get_logger().info(dedent(""" - The response was None. :( - """)) + self.get_logger().info("The response was None.") def main(args=None): @@ -95,9 +89,9 @@ def main(args=None): try: rclpy.init(args=args) - what_is_the_point_service_client_node = WhatIsThePointServiceClientNode() + add_points_service_client_node = AddPointsServiceClientNode() - rclpy.spin(what_is_the_point_service_client_node) + rclpy.spin(add_points_service_client_node) except KeyboardInterrupt: pass except Exception as e: diff --git a/ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/add_points_service_server_node.py b/ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/add_points_service_server_node.py new file mode 100644 index 00000000..fca79648 --- /dev/null +++ b/ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/add_points_service_server_node.py @@ -0,0 +1,79 @@ +""" +MIT LICENSE + +Copyright (C) 2023-25 Murilo Marques Marinho (www.murilomarinho.info) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" +import random +from textwrap import dedent # https://docs.python.org/3/library/textwrap.html#textwrap.dedent + +import rclpy +from rclpy.node import Node +from package_with_interfaces.srv import AddPoints + + +class AddPointsServiceServerNode(Node): + """A ROS2 Node with a Service Server for AddPoints.""" + + def __init__(self): + super().__init__('add_points_service_server') + + self.service_server = self.create_service( + srv_type=AddPoints, + srv_name='/add_points', + callback=self.add_points_service_callback) + + self.service_server_call_count: int = 0 + + def add_points_service_callback(self, + request: AddPoints.Request, + response: AddPoints.Response + ) -> AddPoints.Response: + """ + Adds the two points `a` and `b` in the request and returns the `result`. + """ + + response.result.x = request.a.x + request.b.x + response.result.y = request.a.y + request.b.y + response.result.z = request.a.z + request.b.z + + return response + + +def main(args=None): + """ + The main function. + :param args: Not used directly by the user, but used by ROS2 to configure + certain aspects of the Node. + """ + try: + rclpy.init(args=args) + + add_points_service_server_node = AddPointsServiceServerNode() + + rclpy.spin(add_points_service_server_node) + except KeyboardInterrupt: + pass + except Exception as e: + print(e) + + +if __name__ == '__main__': + main() diff --git a/ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/what_is_the_point_service_server_node.py b/ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/what_is_the_point_service_server_node.py deleted file mode 100644 index 3bc3d4cd..00000000 --- a/ros2_tutorial_workspace/src/python_package_that_uses_the_services/python_package_that_uses_the_services/what_is_the_point_service_server_node.py +++ /dev/null @@ -1,107 +0,0 @@ -""" -MIT LICENSE - -Copyright (C) 2023 Murilo Marques Marinho (www.murilomarinho.info) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" -import random -from textwrap import dedent # https://docs.python.org/3/library/textwrap.html#textwrap.dedent - -import rclpy -from rclpy.node import Node -from package_with_interfaces.srv import WhatIsThePoint - - -class WhatIsThePointServiceServerNode(Node): - """A ROS2 Node with a Service Server for WhatIsThePoint.""" - - def __init__(self): - super().__init__('what_is_the_point_service_server') - - self.service_server = self.create_service( - srv_type=WhatIsThePoint, - srv_name='/what_is_the_point', - callback=self.what_is_the_point_service_callback) - - self.service_server_call_count: int = 0 - - def what_is_the_point_service_callback(self, - request: WhatIsThePoint.Request, - response: WhatIsThePoint.Response - ) -> WhatIsThePoint.Response: - """Analyses an AmazingQuote and returns what is the point. - If the quote contains 'life', it returns a point whose sum of coordinates is 42. - Otherwise, it returns a random point whose sum of coordinates is not 42. - """ - - # Generate the x,y,z of the point - if "life" in request.quote.quote.lower(): - x: float = random.uniform(0, 42) - y: float = random.uniform(0, 42 - x) - z: float = 42 - (x + y) - else: - x: float = random.uniform(0, 100) - y: float = random.uniform(0, 100) - z: float = random.uniform(0, 100) - if x + y + z == 42: # So you’re telling me there’s a chance? Yes! - x = x + 1 # Not anymore :( - - # Assign to the response - response.point.x = x - response.point.y = y - response.point.z = z - - # Increase the call count - self.service_server_call_count = self.service_server_call_count + 1 - - self.get_logger().info(dedent(f""" - This is the call number {self.service_server_call_count} to this Service Server. - The analysis of the AmazingQuote below is complete. - - {request.quote.quote} - - -- {request.quote.philosopher_name} - - The point has been sent back to the client. - """)) - - return response - - -def main(args=None): - """ - The main function. - :param args: Not used directly by the user, but used by ROS2 to configure - certain aspects of the Node. - """ - try: - rclpy.init(args=args) - - what_is_the_point_service_server_node = WhatIsThePointServiceServerNode() - - rclpy.spin(what_is_the_point_service_server_node) - except KeyboardInterrupt: - pass - except Exception as e: - print(e) - - -if __name__ == '__main__': - main() diff --git a/ros2_tutorial_workspace/src/python_package_that_uses_the_services/setup.py b/ros2_tutorial_workspace/src/python_package_that_uses_the_services/setup.py index 316a7fec..09e7d6c9 100644 --- a/ros2_tutorial_workspace/src/python_package_that_uses_the_services/setup.py +++ b/ros2_tutorial_workspace/src/python_package_that_uses_the_services/setup.py @@ -20,10 +20,10 @@ tests_require=['pytest'], entry_points={ 'console_scripts': [ - 'what_is_the_point_service_client_node = ' - 'python_package_that_uses_the_services.what_is_the_point_service_client_node:main', - 'what_is_the_point_service_server_node = ' - 'python_package_that_uses_the_services.what_is_the_point_service_server_node:main' + 'add_points_service_client_node = ' + 'python_package_that_uses_the_services.add_points_service_client_node:main', + 'add_points_service_server_node = ' + 'python_package_that_uses_the_services.add_points_service_server_node:main' ], }, ) diff --git a/ros2_tutorial_workspace/src/python_package_with_a_node/setup.py b/ros2_tutorial_workspace/src/python_package_with_a_node/setup.py index 6eb24b24..490f1e83 100644 --- a/ros2_tutorial_workspace/src/python_package_with_a_node/setup.py +++ b/ros2_tutorial_workspace/src/python_package_with_a_node/setup.py @@ -1,11 +1,11 @@ -from setuptools import setup +from setuptools import find_packages, setup package_name = 'python_package_with_a_node' setup( name=package_name, version='0.0.0', - packages=[package_name], + packages=find_packages(exclude=['test']), data_files=[ ('share/ament_index/resource_index/packages', ['resource/' + package_name]),