diff --git a/.github/thirdparty.repos b/.github/thirdparty.repos
index d3c41654..16256309 100644
--- a/.github/thirdparty.repos
+++ b/.github/thirdparty.repos
@@ -1,7 +1,7 @@
repositories:
ThirdParty/EasyNavigation:
type: git
- url: https://github.com/EasyNavigation/EasyNavigation.git
+ url: https://github.com/juanscelyg/EasyNavigation.git
version: humble
ThirdParty/yaets:
type: git
@@ -9,5 +9,5 @@ repositories:
version: humble
ThirdParty/NavMap:
type: git
- url: https://github.com/EasyNavigation/NavMap.git
+ url: https://github.com/juanscelyg/NavMap.git
version: humble
diff --git a/.github/workflows/humble.yaml b/.github/workflows/humble.yaml
index 1bc35f70..f805b91e 100644
--- a/.github/workflows/humble.yaml
+++ b/.github/workflows/humble.yaml
@@ -7,33 +7,24 @@ on:
push:
branches:
- humble
- schedule:
- - cron: '0 0 * * 6'
+ workflow_dispatch:
jobs:
build-and-test:
- runs-on: ${{ matrix.os }}
- strategy:
- matrix:
- os: [ubuntu-22.04]
- fail-fast: false
+ runs-on: ubuntu-22.04
+ container:
+ image: ubuntu:jammy
steps:
- name: Repo checkout
uses: actions/checkout@v4
- with:
- ref: humble
- name: Setup ROS 2
uses: ros-tooling/setup-ros@0.7.15
- with:
- required-ros-distributions: humble
- - name: Upgrade typing_extensions
- run: python3 -m pip install --upgrade "typing_extensions>=4.7"
- name: build and test
uses: ros-tooling/action-ros-ci@0.4.5
with:
- package-name: easynav_common easynav_controller easynav_core easynav_interfaces easynav_localizer easynav_maps_manager easynav_planner easynav_sensors easynav_support_py easynav_system easynav_tools easynav_bonxai_maps_manager easynav_costmap_common easynav_costmap_localizer easynav_costmap_maps_manager easynav_costmap_planner easynav_gps_localizer easynav_mppi_controller easynav_navmap_localizer easynav_navmap_maps_manager easynav_navmap_planner easynav_serest_controller easynav_simple_common easynav_simple_controller easynav_simple_localizer easynav_simple_maps_manager easynav_simple_planner easynav_vff_controller
+ package-name: easynav_bonxai_maps_manager easynav_costmap_common easynav_costmap_localizer easynav_costmap_maps_manager easynav_costmap_planner easynav_gps_localizer easynav_mpc_controller easynav_mppi_controller easynav_navmap_localizer easynav_navmap_maps_manager easynav_navmap_planner easynav_octomap_maps_manager easynav_regulated_pp_controller easynav_routes_maps_manager easynav_serest_controller easynav_simple_common easynav_simple_controller easynav_simple_localizer easynav_simple_maps_manager easynav_simple_planner easynav_vff_controller
target-ros2-distro: humble
- vcs-repo-file-url: ${GITHUB_WORKSPACE}/.github/thirdparty.repos
- skip-tests: true
+ vcs-repo-file-url: ${{ github.workspace }}/.github/thirdparty.repos
+ skip-tests: false
colcon-defaults: |
{
"build": {
diff --git a/.github/workflows/jazzy.yaml b/.github/workflows/jazzy.yaml
index 33198952..89695fac 100644
--- a/.github/workflows/jazzy.yaml
+++ b/.github/workflows/jazzy.yaml
@@ -7,15 +7,12 @@ on:
push:
branches:
- jazzy
- schedule:
- - cron: '0 0 * * 6'
+ workflow_dispatch:
jobs:
build-and-test:
- runs-on: ${{ matrix.os }}
- strategy:
- matrix:
- os: [ubuntu-24.04]
- fail-fast: false
+ runs-on: ubuntu-24.04
+ container:
+ image: ubuntu:noble
steps:
- name: Repo checkout
uses: actions/checkout@v4
@@ -23,16 +20,13 @@ jobs:
ref: jazzy
- name: Setup ROS 2
uses: ros-tooling/setup-ros@0.7.15
- with:
- required-ros-distributions: jazzy
-
- name: build and test
uses: ros-tooling/action-ros-ci@0.4.5
with:
- package-name: easynav_common easynav_controller easynav_core easynav_interfaces easynav_localizer easynav_maps_manager easynav_planner easynav_sensors easynav_support_py easynav_system easynav_tools easynav_bonxai_maps_manager easynav_costmap_common easynav_costmap_localizer easynav_costmap_maps_manager easynav_costmap_planner easynav_gps_localizer easynav_mppi_controller easynav_navmap_localizer easynav_navmap_maps_manager easynav_navmap_planner easynav_serest_controller easynav_simple_common easynav_simple_controller easynav_simple_localizer easynav_simple_maps_manager easynav_simple_planner easynav_vff_controller
+ package-name: easynav_bonxai_maps_manager easynav_costmap_common easynav_costmap_localizer easynav_costmap_maps_manager easynav_costmap_planner easynav_gps_localizer easynav_fusion_localizer easynav_mppi_controller easynav_navmap_localizer easynav_navmap_maps_manager easynav_navmap_planner easynav_serest_controller easynav_simple_common easynav_simple_controller easynav_simple_localizer easynav_simple_maps_manager easynav_simple_planner easynav_vff_controller easynav_mpc_controller
target-ros2-distro: jazzy
vcs-repo-file-url: ${GITHUB_WORKSPACE}/.github/thirdparty.repos
- skip-tests: true
+ skip-tests: false
colcon-defaults: |
{
"build": {
diff --git a/.github/workflows/jazzy_cron.yaml b/.github/workflows/jazzy_cron.yaml
new file mode 100644
index 00000000..4fbccdb1
--- /dev/null
+++ b/.github/workflows/jazzy_cron.yaml
@@ -0,0 +1,44 @@
+name: jazzy
+
+on:
+ schedule:
+ - cron: '0 0 * * 6'
+jobs:
+ build-and-test:
+ runs-on: ubuntu-24.04
+ container:
+ image: ubuntu:noble
+ steps:
+ - name: Repo checkout
+ uses: actions/checkout@v4
+ with:
+ ref: jazzy
+ - name: Setup ROS 2
+ uses: ros-tooling/setup-ros@0.7.15
+ - name: build and test
+ uses: ros-tooling/action-ros-ci@0.4.5
+ with:
+ package-name: easynav_bonxai_maps_manager easynav_costmap_common easynav_costmap_localizer easynav_costmap_maps_manager easynav_costmap_planner easynav_gps_localizer easynav_fusion_localizer easynav_mppi_controller easynav_navmap_localizer easynav_navmap_maps_manager easynav_navmap_planner easynav_serest_controller easynav_simple_common easynav_simple_controller easynav_simple_localizer easynav_simple_maps_manager easynav_simple_planner easynav_vff_controller easynav_mpc_controller
+ target-ros2-distro: jazzy
+ vcs-repo-file-url: ${GITHUB_WORKSPACE}/.github/thirdparty.repos
+ skip-tests: false
+ colcon-defaults: |
+ {
+ "build": {
+ "packages-up-to": true,
+ "mixin": ["coverage-gcc"]
+ },
+ "test": {
+ "parallel-workers" : 1
+ }
+ }
+ colcon-mixin-repository: https://raw.githubusercontent.com/colcon/colcon-mixin-repository/master/index.yaml
+
+ - name: Codecov
+ uses: codecov/codecov-action@v5.4.0
+ with:
+ files: ros_ws/lcov/total_coverage.info
+ flags: unittests
+ name: codecov-umbrella
+ # yml: ./codecov.yml
+ fail_ci_if_error: false
diff --git a/.github/workflows/kilted.yaml b/.github/workflows/kilted.yaml
index f699529f..ee4356be 100644
--- a/.github/workflows/kilted.yaml
+++ b/.github/workflows/kilted.yaml
@@ -7,15 +7,12 @@ on:
push:
branches:
- kilted
- schedule:
- - cron: '0 0 * * 6'
+ workflow_dispatch:
jobs:
build-and-test:
- runs-on: ${{ matrix.os }}
- strategy:
- matrix:
- os: [ubuntu-24.04]
- fail-fast: false
+ runs-on: ubuntu-24.04
+ container:
+ image: ubuntu:noble
steps:
- name: Repo checkout
uses: actions/checkout@v4
@@ -23,16 +20,12 @@ jobs:
ref: kilted
- name: Setup ROS 2
uses: ros-tooling/setup-ros@0.7.15
- with:
- required-ros-distributions: kilted
-
- name: build and test
uses: ros-tooling/action-ros-ci@0.4.5
with:
- package-name: easynav_common easynav_controller easynav_core easynav_interfaces easynav_localizer easynav_maps_manager easynav_planner easynav_sensors easynav_support_py easynav_system easynav_tools easynav_bonxai_maps_manager easynav_costmap_common easynav_costmap_localizer easynav_costmap_maps_manager easynav_costmap_planner easynav_gps_localizer easynav_mppi_controller easynav_navmap_localizer easynav_navmap_maps_manager easynav_navmap_planner easynav_serest_controller easynav_simple_common easynav_simple_controller easynav_simple_localizer easynav_simple_maps_manager easynav_simple_planner easynav_vff_controller
+ package-name: easynav_bonxai_maps_manager easynav_costmap_common easynav_costmap_localizer easynav_costmap_maps_manager easynav_costmap_planner easynav_gps_localizer easynav_fusion_localizer easynav_mppi_controller easynav_navmap_localizer easynav_navmap_maps_manager easynav_navmap_planner easynav_serest_controller easynav_simple_common easynav_simple_controller easynav_simple_localizer easynav_simple_maps_manager easynav_simple_planner easynav_vff_controller easynav_mpc_controller
target-ros2-distro: kilted
- vcs-repo-file-url: ${GITHUB_WORKSPACE}/.github/thirdparty.repos
- skip-tests: true
+ skip-tests: false
colcon-defaults: |
{
"build": {
diff --git a/.github/workflows/kilted_cron.yaml b/.github/workflows/kilted_cron.yaml
new file mode 100644
index 00000000..7a245495
--- /dev/null
+++ b/.github/workflows/kilted_cron.yaml
@@ -0,0 +1,44 @@
+name: kilted
+
+on:
+ schedule:
+ - cron: '0 0 * * 6'
+jobs:
+ build-and-test:
+ runs-on: ubuntu-24.04
+ container:
+ image: ubuntu:noble
+ steps:
+ - name: Repo checkout
+ uses: actions/checkout@v4
+ with:
+ ref: kilted
+ - name: Setup ROS 2
+ uses: ros-tooling/setup-ros@0.7.15
+ - name: build and test
+ uses: ros-tooling/action-ros-ci@0.4.5
+ with:
+ package-name: easynav_bonxai_maps_manager easynav_costmap_common easynav_costmap_localizer easynav_costmap_maps_manager easynav_costmap_planner easynav_gps_localizer easynav_fusion_localizer easynav_mppi_controller easynav_navmap_localizer easynav_navmap_maps_manager easynav_navmap_planner easynav_serest_controller easynav_simple_common easynav_simple_controller easynav_simple_localizer easynav_simple_maps_manager easynav_simple_planner easynav_vff_controller easynav_mpc_controller
+ target-ros2-distro: kilted
+ vcs-repo-file-url: ${GITHUB_WORKSPACE}/.github/thirdparty.repos
+ skip-tests: false
+ colcon-defaults: |
+ {
+ "build": {
+ "packages-up-to": true,
+ "mixin": ["coverage-gcc"]
+ },
+ "test": {
+ "parallel-workers" : 1
+ }
+ }
+ colcon-mixin-repository: https://raw.githubusercontent.com/colcon/colcon-mixin-repository/master/index.yaml
+
+ - name: Codecov
+ uses: codecov/codecov-action@v5.4.0
+ with:
+ files: ros_ws/lcov/total_coverage.info
+ flags: unittests
+ name: codecov-umbrella
+ # yml: ./codecov.yml
+ fail_ci_if_error: false
diff --git a/.github/workflows/lyrical.yaml b/.github/workflows/lyrical.yaml
new file mode 100644
index 00000000..8443a2fb
--- /dev/null
+++ b/.github/workflows/lyrical.yaml
@@ -0,0 +1,49 @@
+name: lyrical
+
+on:
+ pull_request:
+ branches:
+ - lyrical
+ push:
+ branches:
+ - lyrical
+ schedule:
+ - cron: '0 0 * * 6'
+ workflow_dispatch:
+jobs:
+ build-and-test:
+ runs-on: ubuntu-26.04
+ container:
+ image: ghcr.io/ros-tooling/setup-ros-docker/setup-ros-docker-ubuntu-resolute-ros-lyrical-ros-base:master
+ steps:
+ - name: Repo checkout
+ uses: actions/checkout@v6
+ with:
+ ref: lyrical
+ - name: build and test
+ uses: ros-tooling/action-ros-ci@0.4.8
+ with:
+ package-name: easynav_common easynav_controller easynav_core easynav_interfaces easynav_localizer easynav_maps_manager easynav_planner easynav_sensors easynav_support_py easynav_system easynav_tools easynav_bonxai_maps_manager easynav_costmap_common easynav_costmap_localizer easynav_costmap_maps_manager easynav_costmap_planner easynav_gps_localizer easynav_fusion_localizer easynav_mppi_controller easynav_navmap_localizer easynav_navmap_maps_manager easynav_navmap_planner easynav_serest_controller easynav_simple_common easynav_simple_controller easynav_simple_localizer easynav_simple_maps_manager easynav_simple_planner easynav_vff_controller easynav_mpc_controller
+ target-ros2-distro: lyrical
+ vcs-repo-file-url: ${GITHUB_WORKSPACE}/.github/thirdparty.repos
+ skip-tests: false
+ colcon-defaults: |
+ {
+ "build": {
+ "packages-up-to": true,
+ "mixin": ["coverage-gcc"]
+ },
+ "test": {
+ "parallel-workers" : 1
+ }
+ }
+ colcon-mixin-repository: https://raw.githubusercontent.com/colcon/colcon-mixin-repository/master/index.yaml
+
+ - name: Codecov
+ uses: codecov/codecov-action@v5.4.0
+ with:
+ files: ros_ws/lcov/total_coverage.info
+ flags: unittests
+ name: codecov-umbrella
+ # yml: ./codecov.yml
+ fail_ci_if_error: false
diff --git a/.github/workflows/rolling.yaml b/.github/workflows/rolling.yaml
index 30cd778d..cafefba5 100644
--- a/.github/workflows/rolling.yaml
+++ b/.github/workflows/rolling.yaml
@@ -8,31 +8,25 @@ on:
branches:
- rolling
schedule:
- - cron: '0 0 * * 6'
+ - cron: '0 0 * * 6'
+ workflow_dispatch:
jobs:
build-and-test:
- runs-on: ${{ matrix.os }}
- strategy:
- matrix:
- os: [ubuntu-24.04]
- fail-fast: false
+ runs-on: ubuntu-26.04
+ container:
+ image: ghcr.io/ros-tooling/setup-ros-docker/setup-ros-docker-ubuntu-resolute-ros-rolling-ros-base:master
steps:
- name: Repo checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
with:
ref: rolling
- - name: Setup ROS 2
- uses: ros-tooling/setup-ros@0.7.15
- with:
- required-ros-distributions: rolling
-
- name: build and test
- uses: ros-tooling/action-ros-ci@0.4.5
+ uses: ros-tooling/action-ros-ci@0.4.8
with:
- package-name: easynav_common easynav_controller easynav_core easynav_interfaces easynav_localizer easynav_maps_manager easynav_planner easynav_sensors easynav_support_py easynav_system easynav_tools easynav_bonxai_maps_manager easynav_costmap_common easynav_costmap_localizer easynav_costmap_maps_manager easynav_costmap_planner easynav_gps_localizer easynav_mppi_controller easynav_navmap_localizer easynav_navmap_maps_manager easynav_navmap_planner easynav_serest_controller easynav_simple_common easynav_simple_controller easynav_simple_localizer easynav_simple_maps_manager easynav_simple_planner easynav_vff_controller
+ package-name: easynav_common easynav_controller easynav_core easynav_interfaces easynav_localizer easynav_maps_manager easynav_planner easynav_sensors easynav_support_py easynav_system easynav_tools easynav_bonxai_maps_manager easynav_costmap_common easynav_costmap_localizer easynav_costmap_maps_manager easynav_costmap_planner easynav_gps_localizer easynav_fusion_localizer easynav_mppi_controller easynav_navmap_localizer easynav_navmap_maps_manager easynav_navmap_planner easynav_serest_controller easynav_simple_common easynav_simple_controller easynav_simple_localizer easynav_simple_maps_manager easynav_simple_planner easynav_vff_controller easynav_mpc_controller
target-ros2-distro: rolling
vcs-repo-file-url: ${GITHUB_WORKSPACE}/.github/thirdparty.repos
- skip-tests: true
+ skip-tests: false
colcon-defaults: |
{
"build": {
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..46e553ed
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+# VS Code stuff
+/.vscode/**
+**/__pycache__/
+
+# ROS 2 build files
+build/
+install/
+log/
diff --git a/LICENSE b/LICENSE
index f288702d..261eeb9e 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,674 +1,201 @@
- GNU GENERAL PUBLIC LICENSE
- Version 3, 29 June 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc.
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
- Preamble
-
- The GNU General Public License is a free, copyleft license for
-software and other kinds of works.
-
- The licenses for most software and other practical works are designed
-to take away your freedom to share and change the works. By contrast,
-the GNU General Public License is intended to guarantee your freedom to
-share and change all versions of a program--to make sure it remains free
-software for all its users. We, the Free Software Foundation, use the
-GNU General Public License for most of our software; it applies also to
-any other work released this way by its authors. You can apply it to
-your programs, too.
-
- When we speak of free software, we are referring to freedom, not
-price. Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-them if you wish), that you receive source code or can get it if you
-want it, that you can change the software or use pieces of it in new
-free programs, and that you know you can do these things.
-
- To protect your rights, we need to prevent others from denying you
-these rights or asking you to surrender the rights. Therefore, you have
-certain responsibilities if you distribute copies of the software, or if
-you modify it: responsibilities to respect the freedom of others.
-
- For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must pass on to the recipients the same
-freedoms that you received. You must make sure that they, too, receive
-or can get the source code. And you must show them these terms so they
-know their rights.
-
- Developers that use the GNU GPL protect your rights with two steps:
-(1) assert copyright on the software, and (2) offer you this License
-giving you legal permission to copy, distribute and/or modify it.
-
- For the developers' and authors' protection, the GPL clearly explains
-that there is no warranty for this free software. For both users' and
-authors' sake, the GPL requires that modified versions be marked as
-changed, so that their problems will not be attributed erroneously to
-authors of previous versions.
-
- Some devices are designed to deny users access to install or run
-modified versions of the software inside them, although the manufacturer
-can do so. This is fundamentally incompatible with the aim of
-protecting users' freedom to change the software. The systematic
-pattern of such abuse occurs in the area of products for individuals to
-use, which is precisely where it is most unacceptable. Therefore, we
-have designed this version of the GPL to prohibit the practice for those
-products. If such problems arise substantially in other domains, we
-stand ready to extend this provision to those domains in future versions
-of the GPL, as needed to protect the freedom of users.
-
- Finally, every program is threatened constantly by software patents.
-States should not allow patents to restrict development and use of
-software on general-purpose computers, but in those that do, we wish to
-avoid the special danger that patents applied to a free program could
-make it effectively proprietary. To prevent this, the GPL assures that
-patents cannot be used to render the program non-free.
-
- The precise terms and conditions for copying, distribution and
-modification follow.
-
- TERMS AND CONDITIONS
-
- 0. Definitions.
-
- "This License" refers to version 3 of the GNU General Public License.
-
- "Copyright" also means copyright-like laws that apply to other kinds of
-works, such as semiconductor masks.
-
- "The Program" refers to any copyrightable work licensed under this
-License. Each licensee is addressed as "you". "Licensees" and
-"recipients" may be individuals or organizations.
-
- To "modify" a work means to copy from or adapt all or part of the work
-in a fashion requiring copyright permission, other than the making of an
-exact copy. The resulting work is called a "modified version" of the
-earlier work or a work "based on" the earlier work.
-
- A "covered work" means either the unmodified Program or a work based
-on the Program.
-
- To "propagate" a work means to do anything with it that, without
-permission, would make you directly or secondarily liable for
-infringement under applicable copyright law, except executing it on a
-computer or modifying a private copy. Propagation includes copying,
-distribution (with or without modification), making available to the
-public, and in some countries other activities as well.
-
- To "convey" a work means any kind of propagation that enables other
-parties to make or receive copies. Mere interaction with a user through
-a computer network, with no transfer of a copy, is not conveying.
-
- An interactive user interface displays "Appropriate Legal Notices"
-to the extent that it includes a convenient and prominently visible
-feature that (1) displays an appropriate copyright notice, and (2)
-tells the user that there is no warranty for the work (except to the
-extent that warranties are provided), that licensees may convey the
-work under this License, and how to view a copy of this License. If
-the interface presents a list of user commands or options, such as a
-menu, a prominent item in the list meets this criterion.
-
- 1. Source Code.
-
- The "source code" for a work means the preferred form of the work
-for making modifications to it. "Object code" means any non-source
-form of a work.
-
- A "Standard Interface" means an interface that either is an official
-standard defined by a recognized standards body, or, in the case of
-interfaces specified for a particular programming language, one that
-is widely used among developers working in that language.
-
- The "System Libraries" of an executable work include anything, other
-than the work as a whole, that (a) is included in the normal form of
-packaging a Major Component, but which is not part of that Major
-Component, and (b) serves only to enable use of the work with that
-Major Component, or to implement a Standard Interface for which an
-implementation is available to the public in source code form. A
-"Major Component", in this context, means a major essential component
-(kernel, window system, and so on) of the specific operating system
-(if any) on which the executable work runs, or a compiler used to
-produce the work, or an object code interpreter used to run it.
-
- The "Corresponding Source" for a work in object code form means all
-the source code needed to generate, install, and (for an executable
-work) run the object code and to modify the work, including scripts to
-control those activities. However, it does not include the work's
-System Libraries, or general-purpose tools or generally available free
-programs which are used unmodified in performing those activities but
-which are not part of the work. For example, Corresponding Source
-includes interface definition files associated with source files for
-the work, and the source code for shared libraries and dynamically
-linked subprograms that the work is specifically designed to require,
-such as by intimate data communication or control flow between those
-subprograms and other parts of the work.
-
- The Corresponding Source need not include anything that users
-can regenerate automatically from other parts of the Corresponding
-Source.
-
- The Corresponding Source for a work in source code form is that
-same work.
-
- 2. Basic Permissions.
-
- All rights granted under this License are granted for the term of
-copyright on the Program, and are irrevocable provided the stated
-conditions are met. This License explicitly affirms your unlimited
-permission to run the unmodified Program. The output from running a
-covered work is covered by this License only if the output, given its
-content, constitutes a covered work. This License acknowledges your
-rights of fair use or other equivalent, as provided by copyright law.
-
- You may make, run and propagate covered works that you do not
-convey, without conditions so long as your license otherwise remains
-in force. You may convey covered works to others for the sole purpose
-of having them make modifications exclusively for you, or provide you
-with facilities for running those works, provided that you comply with
-the terms of this License in conveying all material for which you do
-not control copyright. Those thus making or running the covered works
-for you must do so exclusively on your behalf, under your direction
-and control, on terms that prohibit them from making any copies of
-your copyrighted material outside their relationship with you.
-
- Conveying under any other circumstances is permitted solely under
-the conditions stated below. Sublicensing is not allowed; section 10
-makes it unnecessary.
-
- 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-
- No covered work shall be deemed part of an effective technological
-measure under any applicable law fulfilling obligations under article
-11 of the WIPO copyright treaty adopted on 20 December 1996, or
-similar laws prohibiting or restricting circumvention of such
-measures.
-
- When you convey a covered work, you waive any legal power to forbid
-circumvention of technological measures to the extent such circumvention
-is effected by exercising rights under this License with respect to
-the covered work, and you disclaim any intention to limit operation or
-modification of the work as a means of enforcing, against the work's
-users, your or third parties' legal rights to forbid circumvention of
-technological measures.
-
- 4. Conveying Verbatim Copies.
-
- You may convey verbatim copies of the Program's source code as you
-receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice;
-keep intact all notices stating that this License and any
-non-permissive terms added in accord with section 7 apply to the code;
-keep intact all notices of the absence of any warranty; and give all
-recipients a copy of this License along with the Program.
-
- You may charge any price or no price for each copy that you convey,
-and you may offer support or warranty protection for a fee.
-
- 5. Conveying Modified Source Versions.
-
- You may convey a work based on the Program, or the modifications to
-produce it from the Program, in the form of source code under the
-terms of section 4, provided that you also meet all of these conditions:
-
- a) The work must carry prominent notices stating that you modified
- it, and giving a relevant date.
-
- b) The work must carry prominent notices stating that it is
- released under this License and any conditions added under section
- 7. This requirement modifies the requirement in section 4 to
- "keep intact all notices".
-
- c) You must license the entire work, as a whole, under this
- License to anyone who comes into possession of a copy. This
- License will therefore apply, along with any applicable section 7
- additional terms, to the whole of the work, and all its parts,
- regardless of how they are packaged. This License gives no
- permission to license the work in any other way, but it does not
- invalidate such permission if you have separately received it.
-
- d) If the work has interactive user interfaces, each must display
- Appropriate Legal Notices; however, if the Program has interactive
- interfaces that do not display Appropriate Legal Notices, your
- work need not make them do so.
-
- A compilation of a covered work with other separate and independent
-works, which are not by their nature extensions of the covered work,
-and which are not combined with it such as to form a larger program,
-in or on a volume of a storage or distribution medium, is called an
-"aggregate" if the compilation and its resulting copyright are not
-used to limit the access or legal rights of the compilation's users
-beyond what the individual works permit. Inclusion of a covered work
-in an aggregate does not cause this License to apply to the other
-parts of the aggregate.
-
- 6. Conveying Non-Source Forms.
-
- You may convey a covered work in object code form under the terms
-of sections 4 and 5, provided that you also convey the
-machine-readable Corresponding Source under the terms of this License,
-in one of these ways:
-
- a) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by the
- Corresponding Source fixed on a durable physical medium
- customarily used for software interchange.
-
- b) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by a
- written offer, valid for at least three years and valid for as
- long as you offer spare parts or customer support for that product
- model, to give anyone who possesses the object code either (1) a
- copy of the Corresponding Source for all the software in the
- product that is covered by this License, on a durable physical
- medium customarily used for software interchange, for a price no
- more than your reasonable cost of physically performing this
- conveying of source, or (2) access to copy the
- Corresponding Source from a network server at no charge.
-
- c) Convey individual copies of the object code with a copy of the
- written offer to provide the Corresponding Source. This
- alternative is allowed only occasionally and noncommercially, and
- only if you received the object code with such an offer, in accord
- with subsection 6b.
-
- d) Convey the object code by offering access from a designated
- place (gratis or for a charge), and offer equivalent access to the
- Corresponding Source in the same way through the same place at no
- further charge. You need not require recipients to copy the
- Corresponding Source along with the object code. If the place to
- copy the object code is a network server, the Corresponding Source
- may be on a different server (operated by you or a third party)
- that supports equivalent copying facilities, provided you maintain
- clear directions next to the object code saying where to find the
- Corresponding Source. Regardless of what server hosts the
- Corresponding Source, you remain obligated to ensure that it is
- available for as long as needed to satisfy these requirements.
-
- e) Convey the object code using peer-to-peer transmission, provided
- you inform other peers where the object code and Corresponding
- Source of the work are being offered to the general public at no
- charge under subsection 6d.
-
- A separable portion of the object code, whose source code is excluded
-from the Corresponding Source as a System Library, need not be
-included in conveying the object code work.
-
- A "User Product" is either (1) a "consumer product", which means any
-tangible personal property which is normally used for personal, family,
-or household purposes, or (2) anything designed or sold for incorporation
-into a dwelling. In determining whether a product is a consumer product,
-doubtful cases shall be resolved in favor of coverage. For a particular
-product received by a particular user, "normally used" refers to a
-typical or common use of that class of product, regardless of the status
-of the particular user or of the way in which the particular user
-actually uses, or expects or is expected to use, the product. A product
-is a consumer product regardless of whether the product has substantial
-commercial, industrial or non-consumer uses, unless such uses represent
-the only significant mode of use of the product.
-
- "Installation Information" for a User Product means any methods,
-procedures, authorization keys, or other information required to install
-and execute modified versions of a covered work in that User Product from
-a modified version of its Corresponding Source. The information must
-suffice to ensure that the continued functioning of the modified object
-code is in no case prevented or interfered with solely because
-modification has been made.
-
- If you convey an object code work under this section in, or with, or
-specifically for use in, a User Product, and the conveying occurs as
-part of a transaction in which the right of possession and use of the
-User Product is transferred to the recipient in perpetuity or for a
-fixed term (regardless of how the transaction is characterized), the
-Corresponding Source conveyed under this section must be accompanied
-by the Installation Information. But this requirement does not apply
-if neither you nor any third party retains the ability to install
-modified object code on the User Product (for example, the work has
-been installed in ROM).
-
- The requirement to provide Installation Information does not include a
-requirement to continue to provide support service, warranty, or updates
-for a work that has been modified or installed by the recipient, or for
-the User Product in which it has been modified or installed. Access to a
-network may be denied when the modification itself materially and
-adversely affects the operation of the network or violates the rules and
-protocols for communication across the network.
-
- Corresponding Source conveyed, and Installation Information provided,
-in accord with this section must be in a format that is publicly
-documented (and with an implementation available to the public in
-source code form), and must require no special password or key for
-unpacking, reading or copying.
-
- 7. Additional Terms.
-
- "Additional permissions" are terms that supplement the terms of this
-License by making exceptions from one or more of its conditions.
-Additional permissions that are applicable to the entire Program shall
-be treated as though they were included in this License, to the extent
-that they are valid under applicable law. If additional permissions
-apply only to part of the Program, that part may be used separately
-under those permissions, but the entire Program remains governed by
-this License without regard to the additional permissions.
-
- When you convey a copy of a covered work, you may at your option
-remove any additional permissions from that copy, or from any part of
-it. (Additional permissions may be written to require their own
-removal in certain cases when you modify the work.) You may place
-additional permissions on material, added by you to a covered work,
-for which you have or can give appropriate copyright permission.
-
- Notwithstanding any other provision of this License, for material you
-add to a covered work, you may (if authorized by the copyright holders of
-that material) supplement the terms of this License with terms:
-
- a) Disclaiming warranty or limiting liability differently from the
- terms of sections 15 and 16 of this License; or
-
- b) Requiring preservation of specified reasonable legal notices or
- author attributions in that material or in the Appropriate Legal
- Notices displayed by works containing it; or
-
- c) Prohibiting misrepresentation of the origin of that material, or
- requiring that modified versions of such material be marked in
- reasonable ways as different from the original version; or
-
- d) Limiting the use for publicity purposes of names of licensors or
- authors of the material; or
-
- e) Declining to grant rights under trademark law for use of some
- trade names, trademarks, or service marks; or
-
- f) Requiring indemnification of licensors and authors of that
- material by anyone who conveys the material (or modified versions of
- it) with contractual assumptions of liability to the recipient, for
- any liability that these contractual assumptions directly impose on
- those licensors and authors.
-
- All other non-permissive additional terms are considered "further
-restrictions" within the meaning of section 10. If the Program as you
-received it, or any part of it, contains a notice stating that it is
-governed by this License along with a term that is a further
-restriction, you may remove that term. If a license document contains
-a further restriction but permits relicensing or conveying under this
-License, you may add to a covered work material governed by the terms
-of that license document, provided that the further restriction does
-not survive such relicensing or conveying.
-
- If you add terms to a covered work in accord with this section, you
-must place, in the relevant source files, a statement of the
-additional terms that apply to those files, or a notice indicating
-where to find the applicable terms.
-
- Additional terms, permissive or non-permissive, may be stated in the
-form of a separately written license, or stated as exceptions;
-the above requirements apply either way.
-
- 8. Termination.
-
- You may not propagate or modify a covered work except as expressly
-provided under this License. Any attempt otherwise to propagate or
-modify it is void, and will automatically terminate your rights under
-this License (including any patent licenses granted under the third
-paragraph of section 11).
-
- However, if you cease all violation of this License, then your
-license from a particular copyright holder is reinstated (a)
-provisionally, unless and until the copyright holder explicitly and
-finally terminates your license, and (b) permanently, if the copyright
-holder fails to notify you of the violation by some reasonable means
-prior to 60 days after the cessation.
-
- Moreover, your license from a particular copyright holder is
-reinstated permanently if the copyright holder notifies you of the
-violation by some reasonable means, this is the first time you have
-received notice of violation of this License (for any work) from that
-copyright holder, and you cure the violation prior to 30 days after
-your receipt of the notice.
-
- Termination of your rights under this section does not terminate the
-licenses of parties who have received copies or rights from you under
-this License. If your rights have been terminated and not permanently
-reinstated, you do not qualify to receive new licenses for the same
-material under section 10.
-
- 9. Acceptance Not Required for Having Copies.
-
- You are not required to accept this License in order to receive or
-run a copy of the Program. Ancillary propagation of a covered work
-occurring solely as a consequence of using peer-to-peer transmission
-to receive a copy likewise does not require acceptance. However,
-nothing other than this License grants you permission to propagate or
-modify any covered work. These actions infringe copyright if you do
-not accept this License. Therefore, by modifying or propagating a
-covered work, you indicate your acceptance of this License to do so.
-
- 10. Automatic Licensing of Downstream Recipients.
-
- Each time you convey a covered work, the recipient automatically
-receives a license from the original licensors, to run, modify and
-propagate that work, subject to this License. You are not responsible
-for enforcing compliance by third parties with this License.
-
- An "entity transaction" is a transaction transferring control of an
-organization, or substantially all assets of one, or subdividing an
-organization, or merging organizations. If propagation of a covered
-work results from an entity transaction, each party to that
-transaction who receives a copy of the work also receives whatever
-licenses to the work the party's predecessor in interest had or could
-give under the previous paragraph, plus a right to possession of the
-Corresponding Source of the work from the predecessor in interest, if
-the predecessor has it or can get it with reasonable efforts.
-
- You may not impose any further restrictions on the exercise of the
-rights granted or affirmed under this License. For example, you may
-not impose a license fee, royalty, or other charge for exercise of
-rights granted under this License, and you may not initiate litigation
-(including a cross-claim or counterclaim in a lawsuit) alleging that
-any patent claim is infringed by making, using, selling, offering for
-sale, or importing the Program or any portion of it.
-
- 11. Patents.
-
- A "contributor" is a copyright holder who authorizes use under this
-License of the Program or a work on which the Program is based. The
-work thus licensed is called the contributor's "contributor version".
-
- A contributor's "essential patent claims" are all patent claims
-owned or controlled by the contributor, whether already acquired or
-hereafter acquired, that would be infringed by some manner, permitted
-by this License, of making, using, or selling its contributor version,
-but do not include claims that would be infringed only as a
-consequence of further modification of the contributor version. For
-purposes of this definition, "control" includes the right to grant
-patent sublicenses in a manner consistent with the requirements of
-this License.
-
- Each contributor grants you a non-exclusive, worldwide, royalty-free
-patent license under the contributor's essential patent claims, to
-make, use, sell, offer for sale, import and otherwise run, modify and
-propagate the contents of its contributor version.
-
- In the following three paragraphs, a "patent license" is any express
-agreement or commitment, however denominated, not to enforce a patent
-(such as an express permission to practice a patent or covenant not to
-sue for patent infringement). To "grant" such a patent license to a
-party means to make such an agreement or commitment not to enforce a
-patent against the party.
-
- If you convey a covered work, knowingly relying on a patent license,
-and the Corresponding Source of the work is not available for anyone
-to copy, free of charge and under the terms of this License, through a
-publicly available network server or other readily accessible means,
-then you must either (1) cause the Corresponding Source to be so
-available, or (2) arrange to deprive yourself of the benefit of the
-patent license for this particular work, or (3) arrange, in a manner
-consistent with the requirements of this License, to extend the patent
-license to downstream recipients. "Knowingly relying" means you have
-actual knowledge that, but for the patent license, your conveying the
-covered work in a country, or your recipient's use of the covered work
-in a country, would infringe one or more identifiable patents in that
-country that you have reason to believe are valid.
-
- If, pursuant to or in connection with a single transaction or
-arrangement, you convey, or propagate by procuring conveyance of, a
-covered work, and grant a patent license to some of the parties
-receiving the covered work authorizing them to use, propagate, modify
-or convey a specific copy of the covered work, then the patent license
-you grant is automatically extended to all recipients of the covered
-work and works based on it.
-
- A patent license is "discriminatory" if it does not include within
-the scope of its coverage, prohibits the exercise of, or is
-conditioned on the non-exercise of one or more of the rights that are
-specifically granted under this License. You may not convey a covered
-work if you are a party to an arrangement with a third party that is
-in the business of distributing software, under which you make payment
-to the third party based on the extent of your activity of conveying
-the work, and under which the third party grants, to any of the
-parties who would receive the covered work from you, a discriminatory
-patent license (a) in connection with copies of the covered work
-conveyed by you (or copies made from those copies), or (b) primarily
-for and in connection with specific products or compilations that
-contain the covered work, unless you entered into that arrangement,
-or that patent license was granted, prior to 28 March 2007.
-
- Nothing in this License shall be construed as excluding or limiting
-any implied license or other defenses to infringement that may
-otherwise be available to you under applicable patent law.
-
- 12. No Surrender of Others' Freedom.
-
- If conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License. If you cannot convey a
-covered work so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you may
-not convey it at all. For example, if you agree to terms that obligate you
-to collect a royalty for further conveying from those to whom you convey
-the Program, the only way you could satisfy both those terms and this
-License would be to refrain entirely from conveying the Program.
-
- 13. Use with the GNU Affero General Public License.
-
- Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
-under version 3 of the GNU Affero General Public License into a single
-combined work, and to convey the resulting work. The terms of this
-License will continue to apply to the part which is the covered work,
-but the special requirements of the GNU Affero General Public License,
-section 13, concerning interaction through a network will apply to the
-combination as such.
-
- 14. Revised Versions of this License.
-
- The Free Software Foundation may publish revised and/or new versions of
-the GNU General Public License from time to time. Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
- Each version is given a distinguishing version number. If the
-Program specifies that a certain numbered version of the GNU General
-Public License "or any later version" applies to it, you have the
-option of following the terms and conditions either of that numbered
-version or of any later version published by the Free Software
-Foundation. If the Program does not specify a version number of the
-GNU General Public License, you may choose any version ever published
-by the Free Software Foundation.
-
- If the Program specifies that a proxy can decide which future
-versions of the GNU General Public License can be used, that proxy's
-public statement of acceptance of a version permanently authorizes you
-to choose that version for the Program.
-
- Later license versions may give you additional or different
-permissions. However, no additional obligations are imposed on any
-author or copyright holder as a result of your choosing to follow a
-later version.
-
- 15. Disclaimer of Warranty.
-
- THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
-HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
-OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
-IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
-ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
- 16. Limitation of Liability.
-
- IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
-THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
-USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
-DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
-EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGES.
-
- 17. Interpretation of Sections 15 and 16.
-
- If the disclaimer of warranty and limitation of liability provided
-above cannot be given local legal effect according to their terms,
-reviewing courts shall apply local law that most closely approximates
-an absolute waiver of all civil liability in connection with the
-Program, unless a warranty or assumption of liability accompanies a
-copy of the Program in return for a fee.
-
- END OF TERMS AND CONDITIONS
-
- How to Apply These Terms to Your New Programs
-
- If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
- To do so, attach the following notices to the program. It is safest
-to attach them to the start of each source file to most effectively
-state the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
-
- Copyright (C)
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see .
-
-Also add information on how to contact you by electronic and paper mail.
-
- If the program does terminal interaction, make it output a short
-notice like this when it starts in an interactive mode:
-
- Copyright (C)
- This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
- This is free software, and you are welcome to redistribute it
- under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License. Of course, your program's commands
-might be different; for a GUI interface, you would use an "about box".
-
- You should also get your employer (if you work as a programmer) or school,
-if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU GPL, see
-.
-
- The GNU General Public License does not permit incorporating your program
-into proprietary programs. If your program is a subroutine library, you
-may consider it more useful to permit linking proprietary applications with
-the library. If this is what you want to do, use the GNU Lesser General
-Public License instead of this License. But first, please read
-.
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/README.md b/README.md
index 1f2c8957..15cc761c 100644
--- a/README.md
+++ b/README.md
@@ -2,11 +2,15 @@
[](https://github.com/EasyNavigation/easynav_plugins/actions/workflows/doxygen-doc.yml)
[](https://github.com/EasyNavigation/easynav_plugins/actions/workflows/rolling.yaml)
+[](https://github.com/EasyNavigation/easynav_plugins/actions/workflows/lyrical.yaml)
[](https://github.com/EasyNavigation/easynav_plugins/actions/workflows/kilted.yaml)
[](https://github.com/EasyNavigation/easynav_plugins/actions/workflows/jazzy.yaml)
[](https://github.com/EasyNavigation/easynav_plugins/actions/workflows/humble.yaml)
+📋 Roadmap Project: [RoadMap](https://github.com/EasyNavigation/EasyNavigation/blob/rolling/ROADMAP.md)
+
## Description
+
**EasyNav Plugins** provides the official collection of plugins for the [Easy Navigation (EasyNav)](https://github.com/EasyNavigation) framework.
These plugins extend the navigation core with planners, controllers, map managers, and localizers compatible with ROS 2.
@@ -17,6 +21,7 @@ Each plugin resides in its own ROS 2 package and is registered via `pluginlib`,
## Repository Structure
### 🧭 Planners
+
Path planning plugins implementing A*, costmap, or NavMap–based methods.
| Package | Description | Link |
@@ -28,6 +33,7 @@ Path planning plugins implementing A*, costmap, or NavMap–based methods.
---
### ⚙️ Controllers
+
Motion controllers for trajectory tracking and reactive behaviors.
| Package | Description | Link |
@@ -36,10 +42,13 @@ Motion controllers for trajectory tracking and reactive behaviors.
| `easynav_mppi_controller` | Model Predictive Path Integral (MPPI) controller. | [README](./controllers/easynav_mppi_controller/README.md) |
| `easynav_simple_controller` | Simple proportional controller for testing. | [README](./controllers/easynav_simple_controller/README.md) |
| `easynav_serest_controller` | SeReST (Safe Reactive Steering) controller. | [README](./controllers/easynav_serest_controller/README.md) |
+| `easynav_mpc_controller` | Model Predictive Controller (MPC). | [README](./controllers/easynav_mpc_controller/README.md) |
+| `easynav_regulated_pp_controller` | Regulated Pure Pursuit controller, with optional Dynamic Window (DWPP) extension. | [README](./controllers/easynav_regulated_pp_controller/README.md) |
---
### 🗺️ Maps Managers
+
Map management plugins that provide, update, and store different environment representations.
| Package | Description | Link |
@@ -53,6 +62,7 @@ Map management plugins that provide, update, and store different environment rep
---
### 📍 Localizers
+
Localization plugins based on different map types and sensors.
| Package | Description | Link |
@@ -61,9 +71,10 @@ Localization plugins based on different map types and sensors.
| `easynav_simple_localizer` | Basic localizer for SimpleMap–based setups. | [README](./localizers/easynav_simple_localizer/README.md) |
| `easynav_navmap_localizer` | AMCL-like localizer operating on NavMap meshes. | [README](./localizers/easynav_navmap_localizer/README.md) |
| `easynav_costmap_localizer` | AMCL-like localizer using Costmap2D. | [README](./localizers/easynav_costmap_localizer/README.md) |
+| `easynav_fusion_localizer` | Multi-sensor fusion localizer (e.g., GPS + odometry + map). | [README](./localizers/easynav_fusion_localizer/README.md) |
---
## License
-All packages in this repository are released under **GPL-3.0-only** unless stated otherwise in the individual package.
+All packages in this repository are released under **Apache-2.0** unless stated otherwise in the individual package.
diff --git a/common/easynav_costmap_common/CHANGELOG.rst b/common/easynav_costmap_common/CHANGELOG.rst
index 924b909c..5b9c3369 100644
--- a/common/easynav_costmap_common/CHANGELOG.rst
+++ b/common/easynav_costmap_common/CHANGELOG.rst
@@ -2,6 +2,22 @@
Changelog for package easynav_costmap_common
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+0.4.0 (2026-07-26)
+------------------
+* Add missing easynav_sensors deps
+* Any change in the costmap set it as modified
+* Adding timestamp to costmap
+* GPLv3 -> Apache 2.0
+* Contributors: Francisco Martín Rico, Juan S. Cely G., Miguel, migueldm
+
+0.2.1 (2026-02-27)
+------------------
+* 0.2.0
+* GPLv3 -> Apache 2.0
+* Remove C++20/C++23 features and update to new MethodBase interface
+* Cleanup unused headers
+* Contributors: Francisco Martín Rico, Francisco Miguel Moreno, Juan S. Cely
+
0.0.2 (2025-10-12)
------------------
* Reorganization initial
diff --git a/common/easynav_costmap_common/CMakeLists.txt b/common/easynav_costmap_common/CMakeLists.txt
index fd5578d7..de103729 100644
--- a/common/easynav_costmap_common/CMakeLists.txt
+++ b/common/easynav_costmap_common/CMakeLists.txt
@@ -12,6 +12,9 @@ set(CMAKE_CXX_EXTENSIONS OFF)
find_package(ament_cmake REQUIRED)
find_package(easynav_common REQUIRED)
find_package(nav_msgs REQUIRED)
+find_package(rclcpp REQUIRED)
+find_package(geometry_msgs REQUIRED)
+find_package(tf2_geometry_msgs REQUIRED)
add_library(${PROJECT_NAME} SHARED
@@ -24,6 +27,9 @@ target_include_directories(${PROJECT_NAME} PUBLIC
target_link_libraries(${PROJECT_NAME} PUBLIC
easynav_common::easynav_common
${nav_msgs_TARGETS}
+ rclcpp::rclcpp
+ ${geometry_msgs_TARGETS}
+ tf2_geometry_msgs::tf2_geometry_msgs
)
install(
@@ -56,5 +62,8 @@ ament_export_dependencies(
easynav_common
# pluginlib
nav_msgs
+ rclcpp
+ geometry_msgs
+ tf2_geometry_msgs
)
ament_package()
diff --git a/common/easynav_costmap_common/include/easynav_costmap_common/costmap_2d.hpp b/common/easynav_costmap_common/include/easynav_costmap_common/costmap_2d.hpp
index d141f071..e0750d4a 100644
--- a/common/easynav_costmap_common/include/easynav_costmap_common/costmap_2d.hpp
+++ b/common/easynav_costmap_common/include/easynav_costmap_common/costmap_2d.hpp
@@ -38,15 +38,13 @@
#ifndef EASYNAV_COSTMAP_COMMON__COSTMAP_2D_HPP_
#define EASYNAV_COSTMAP_COMMON__COSTMAP_2D_HPP_
-#include
-#include
-#include
+#include
#include
#include
-#include
+#include
#include
-#include
#include
+#include "rclcpp/time.hpp"
#include "geometry_msgs/msg/point.hpp"
#include "nav_msgs/msg/occupancy_grid.hpp"
@@ -129,10 +127,29 @@ class Costmap2D
*
* The resulting OccupancyGrid message contains metadata such as resolution, size, and origin,
* and its data field is populated with cost values from the costmap. Cells with a value of
- * NO_INFORMATION are mapped to -1; other values are cast to int8_t directly.
+ * NO_INFORMATION are mapped to -1; other values are cast to int8_t directly.
+ *
+ * The output message header stamp is set to the internal last-modified timestamp.
*/
void toOccupancyGridMsg(nav_msgs::msg::OccupancyGrid & msg) const;
+ /**
+ * @brief Get timestamp of last costmap modification.
+ *
+ * Set by constructors and copy/assignment, and may be updated by some mutating operations.
+ * For high-frequency per-cell updates (e.g., many `setCost()` calls in a loop), prefer calling
+ * `touch()` once after the batch update.
+ */
+ rclcpp::Time getLastModifiedStamp() const;
+
+ /**
+ * @brief Mark the costmap as modified.
+ *
+ * This bumps the internal last-modified stamp by 1 nanosecond while preserving the clock type.
+ * It is intended to be called after bulk updates to avoid per-cell overhead.
+ */
+ void touch();
+
/**
* @brief Copies the (x0,y0)..(xn,yn) window from source costmap into a current costmap
@param source Source costmap where the window will be copied from
@@ -583,6 +600,10 @@ class Costmap2D
unsigned char * costmap_;
unsigned char default_value_;
+ // Timestamp indicating when the costmap was last modified.
+ // Set by constructors and copy/assignment, and bumped by some mutating operations.
+ rclcpp::Time last_modified_;
+
// *INDENT-OFF* Uncrustify doesn't handle indented public/private labels
class MarkCell
{
diff --git a/common/easynav_costmap_common/include/easynav_costmap_common/geometry_utils.hpp b/common/easynav_costmap_common/include/easynav_costmap_common/geometry_utils.hpp
index e7f4b640..744f724a 100644
--- a/common/easynav_costmap_common/include/easynav_costmap_common/geometry_utils.hpp
+++ b/common/easynav_costmap_common/include/easynav_costmap_common/geometry_utils.hpp
@@ -16,7 +16,6 @@
#define EASYNAV_COSTMAP_COMMON__GEOMETRY_UTILS_HPP_
#include
-#include
#include "geometry_msgs/msg/pose.hpp"
#include "geometry_msgs/msg/pose_stamped.hpp"
diff --git a/common/easynav_costmap_common/include/easynav_costmap_common/occ_grid_values.hpp b/common/easynav_costmap_common/include/easynav_costmap_common/occ_grid_values.hpp
index a563202e..9834ef89 100644
--- a/common/easynav_costmap_common/include/easynav_costmap_common/occ_grid_values.hpp
+++ b/common/easynav_costmap_common/include/easynav_costmap_common/occ_grid_values.hpp
@@ -35,6 +35,7 @@
#ifndef EASYNAV_COSTMAP_COMMON__OCC_GRID_VALUES_HPP_
#define EASYNAV_COSTMAP_COMMON__OCC_GRID_VALUES_HPP_
+#include
namespace easynav
{
diff --git a/common/easynav_costmap_common/package.xml b/common/easynav_costmap_common/package.xml
index 55db3489..cf01089e 100644
--- a/common/easynav_costmap_common/package.xml
+++ b/common/easynav_costmap_common/package.xml
@@ -2,7 +2,7 @@
easynav_costmap_common
- 0.0.2
+ 0.4.0
Easy Navigation: Simple Common package.
Francisco Martín Rico
@@ -11,7 +11,7 @@
Juan Sebastián Cely Gutiérrez
José Miguel Guerrero Hernández
- GPL-3.0-only
+ Apache-2.0
Francisco Martín Rico
Francisco Miguel Moreno Olivo
@@ -21,7 +21,10 @@
easynav_common
nav_msgs
+ rclcpp
ament_index_cpp
+ geometry_msgs
+ tf2_geometry_msgs
ament_lint_auto
ament_lint_common
diff --git a/common/easynav_costmap_common/src/easynav_costmap_common/costmap_2d.cpp b/common/easynav_costmap_common/src/easynav_costmap_common/costmap_2d.cpp
index aeb8b088..1b7a5f14 100644
--- a/common/easynav_costmap_common/src/easynav_costmap_common/costmap_2d.cpp
+++ b/common/easynav_costmap_common/src/easynav_costmap_common/costmap_2d.cpp
@@ -39,11 +39,7 @@
#include
#include
-#include
#include
-#include
-#include
-#include
#include "easynav_costmap_common/cost_values.hpp"
#include "easynav_costmap_common/occ_grid_values.hpp"
@@ -55,7 +51,7 @@ Costmap2D::Costmap2D(
unsigned int cells_size_x, unsigned int cells_size_y, double resolution,
double origin_x, double origin_y, unsigned char default_value)
: resolution_(resolution), origin_x_(origin_x),
- origin_y_(origin_y), costmap_(NULL), default_value_(default_value)
+ origin_y_(origin_y), costmap_(NULL), default_value_(default_value), last_modified_(0)
{
access_ = new mutex_t();
@@ -65,7 +61,7 @@ Costmap2D::Costmap2D(
}
Costmap2D::Costmap2D(const nav_msgs::msg::OccupancyGrid & map)
-: default_value_(FREE_SPACE)
+: default_value_(FREE_SPACE), last_modified_(map.header.stamp)
{
access_ = new mutex_t();
@@ -130,6 +126,9 @@ void Costmap2D::resetMaps()
{
std::unique_lock lock(*access_);
memset(costmap_, default_value_, size_x_ * size_y_ * sizeof(unsigned char));
+
+ const int64_t new_stamp_ns = last_modified_.nanoseconds() + 1;
+ last_modified_ = rclcpp::Time(new_stamp_ns, last_modified_.get_clock_type());
}
void Costmap2D::resetMap(unsigned int x0, unsigned int y0, unsigned int xn, unsigned int yn)
@@ -145,6 +144,9 @@ void Costmap2D::resetMapToValue(
for (unsigned int y = y0 * size_x_ + x0; y < yn * size_x_ + x0; y += size_x_) {
memset(costmap_ + y, value, len * sizeof(unsigned char));
}
+
+ const int64_t new_stamp_ns = last_modified_.nanoseconds() + 1;
+ last_modified_ = rclcpp::Time(new_stamp_ns, last_modified_.get_clock_type());
}
bool Costmap2D::copyCostmapWindow(
@@ -183,6 +185,11 @@ bool Costmap2D::copyCostmapWindow(
map.costmap_, lower_left_x, lower_left_y, map.size_x_, costmap_, 0, 0, size_x_,
size_x_,
size_y_);
+
+ {
+ std::unique_lock lock(*access_);
+ last_modified_ = map.getLastModifiedStamp();
+ }
return true;
}
@@ -206,6 +213,15 @@ bool Costmap2D::copyWindow(
source.costmap_, sx0, sy0, source.size_x_,
costmap_, dx0, dy0, size_x_,
sz_x, sz_y);
+
+ {
+ const auto source_stamp = source.getLastModifiedStamp();
+ std::unique_lock lock(*access_);
+ const int64_t new_stamp_ns = std::max(
+ last_modified_.nanoseconds(),
+ source_stamp.nanoseconds()) + 1;
+ last_modified_ = rclcpp::Time(new_stamp_ns, last_modified_.get_clock_type());
+ }
return true;
}
@@ -214,6 +230,10 @@ Costmap2D::toOccupancyGridMsg(nav_msgs::msg::OccupancyGrid & msg) const
{
std::lock_guard lock(*access_);
+ const int64_t stamp_ns = last_modified_.nanoseconds();
+ msg.header.stamp.sec = static_cast(stamp_ns / 1000000000LL);
+ msg.header.stamp.nanosec = static_cast(stamp_ns % 1000000000LL);
+
msg.info.width = size_x_;
msg.info.height = size_y_;
msg.info.resolution = resolution_;
@@ -235,6 +255,19 @@ Costmap2D::toOccupancyGridMsg(nav_msgs::msg::OccupancyGrid & msg) const
}
}
+rclcpp::Time Costmap2D::getLastModifiedStamp() const
+{
+ std::lock_guard lock(*access_);
+ return last_modified_;
+}
+
+void Costmap2D::touch()
+{
+ std::lock_guard lock(*access_);
+ const int64_t new_stamp_ns = last_modified_.nanoseconds() + 1;
+ last_modified_ = rclcpp::Time(new_stamp_ns, last_modified_.get_clock_type());
+}
+
Costmap2D & Costmap2D::operator=(const Costmap2D & map)
{
// check for self assignment
@@ -251,6 +284,7 @@ Costmap2D & Costmap2D::operator=(const Costmap2D & map)
origin_x_ = map.origin_x_;
origin_y_ = map.origin_y_;
default_value_ = map.default_value_;
+ last_modified_ = map.last_modified_;
// initialize our various maps
initMaps(size_x_, size_y_);
@@ -262,7 +296,7 @@ Costmap2D & Costmap2D::operator=(const Costmap2D & map)
}
Costmap2D::Costmap2D(const Costmap2D & map)
-: costmap_(NULL)
+: costmap_(NULL), last_modified_(0)
{
access_ = new mutex_t();
*this = map;
@@ -270,7 +304,8 @@ Costmap2D::Costmap2D(const Costmap2D & map)
// just initialize everything to NULL by default
Costmap2D::Costmap2D()
-: size_x_(0), size_y_(0), resolution_(0.0), origin_x_(0.0), origin_y_(0.0), costmap_(NULL)
+: size_x_(0), size_y_(0), resolution_(0.0), origin_x_(0.0), origin_y_(0.0),
+ costmap_(NULL), last_modified_(0)
{
access_ = new mutex_t();
}
@@ -309,7 +344,16 @@ unsigned char Costmap2D::getCost(unsigned int undex) const
void Costmap2D::setCost(unsigned int mx, unsigned int my, unsigned char cost)
{
- costmap_[getIndex(mx, my)] = cost;
+ std::lock_guard lock(*access_);
+ const unsigned int index = getIndex(mx, my);
+
+ if (costmap_[index] == cost) {
+ return;
+ }
+
+ costmap_[index] = cost;
+ const int64_t new_stamp_ns = last_modified_.nanoseconds() + 1;
+ last_modified_ = rclcpp::Time(new_stamp_ns, last_modified_.get_clock_type());
}
void Costmap2D::mapToWorld(unsigned int mx, unsigned int my, double & wx, double & wy) const
diff --git a/common/easynav_costmap_common/tests/costmap_2d_tests.cpp b/common/easynav_costmap_common/tests/costmap_2d_tests.cpp
index 70e969d9..81983326 100644
--- a/common/easynav_costmap_common/tests/costmap_2d_tests.cpp
+++ b/common/easynav_costmap_common/tests/costmap_2d_tests.cpp
@@ -1,26 +1,22 @@
// Copyright 2025 Intelligent Robotics Lab
//
// This file is part of the project Easy Navigation (EasyNav in short)
-// licensed under the GNU General Public License v3.0.
-// See for details.
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
//
-// Easy Navigation program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
+// http://www.apache.org/licenses/LICENSE-2.0
//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
#include
#include "easynav_costmap_common/costmap_2d.hpp"
-#include "easynav_costmap_common/cost_values.hpp"
#include "nav_msgs/msg/occupancy_grid.hpp"
+#include "rclcpp/time.hpp"
using easynav::Costmap2D;
@@ -121,8 +117,88 @@ TEST_F(Costmap2DTest, OccupancyGridConversion)
};
for (size_t i = 0; i < grid.data.size(); ++i) {
- bool expected = std::find(expected_indices.begin(),
+ bool expected = std::find(
+ expected_indices.begin(),
expected_indices.end(), i) != expected_indices.end();
EXPECT_EQ(grid.data[i], expected ? 100 : 0);
}
}
+
+TEST_F(Costmap2DTest, TimestampFromOccupancyGridConstructor)
+{
+ nav_msgs::msg::OccupancyGrid in;
+ in.header.stamp.sec = 123;
+ in.header.stamp.nanosec = 456u;
+
+ in.info.width = 2u;
+ in.info.height = 3u;
+ in.info.resolution = 0.5;
+ in.info.origin.position.x = 1.0;
+ in.info.origin.position.y = -2.0;
+ in.data.assign(in.info.width * in.info.height, 0);
+
+ Costmap2D map(in);
+ const int64_t expected_ns = 123LL * 1000000000LL + 456LL;
+ EXPECT_EQ(map.getLastModifiedStamp().nanoseconds(), expected_ns);
+
+ nav_msgs::msg::OccupancyGrid out;
+ map.toOccupancyGridMsg(out);
+
+ EXPECT_EQ(out.header.stamp.sec, in.header.stamp.sec);
+ EXPECT_EQ(out.header.stamp.nanosec, in.header.stamp.nanosec);
+}
+
+TEST_F(Costmap2DTest, TimestampFromCopyConstructor)
+{
+ nav_msgs::msg::OccupancyGrid in;
+ in.header.stamp.sec = 10;
+ in.header.stamp.nanosec = 20u;
+ in.info.width = 1u;
+ in.info.height = 1u;
+ in.info.resolution = 1.0;
+ in.info.origin.position.x = 0.0;
+ in.info.origin.position.y = 0.0;
+ in.data.assign(1u, 0);
+
+ Costmap2D original(in);
+ Costmap2D copy(original);
+
+ const int64_t expected_ns = 10LL * 1000000000LL + 20LL;
+ EXPECT_EQ(copy.getLastModifiedStamp().nanoseconds(), expected_ns);
+
+ nav_msgs::msg::OccupancyGrid out;
+ copy.toOccupancyGridMsg(out);
+ EXPECT_EQ(out.header.stamp.sec, in.header.stamp.sec);
+ EXPECT_EQ(out.header.stamp.nanosec, in.header.stamp.nanosec);
+}
+
+TEST_F(Costmap2DTest, TimestampUpdatedBySetCost)
+{
+ nav_msgs::msg::OccupancyGrid in;
+ in.header.stamp.sec = 7;
+ in.header.stamp.nanosec = 8u;
+ in.info.width = 3u;
+ in.info.height = 3u;
+ in.info.resolution = 1.0;
+ in.info.origin.position.x = 0.0;
+ in.info.origin.position.y = 0.0;
+ in.data.assign(in.info.width * in.info.height, 0);
+
+ Costmap2D map(in);
+ const int64_t expected_ns = 7LL * 1000000000LL + 8LL;
+
+ map.setCost(1, 1, 100);
+ EXPECT_EQ(map.getLastModifiedStamp().nanoseconds(), expected_ns + 1);
+
+ // Setting the same value should not change the stamp.
+ map.setCost(1, 1, 100);
+ EXPECT_EQ(map.getLastModifiedStamp().nanoseconds(), expected_ns + 1);
+
+ nav_msgs::msg::OccupancyGrid out;
+ map.toOccupancyGridMsg(out);
+
+ const int64_t out_ns =
+ static_cast(out.header.stamp.sec) * 1000000000LL +
+ static_cast(out.header.stamp.nanosec);
+ EXPECT_EQ(out_ns, expected_ns + 1);
+}
diff --git a/common/easynav_simple_common/CHANGELOG.rst b/common/easynav_simple_common/CHANGELOG.rst
index 7d5ee0d0..715de44e 100644
--- a/common/easynav_simple_common/CHANGELOG.rst
+++ b/common/easynav_simple_common/CHANGELOG.rst
@@ -2,6 +2,22 @@
Changelog for package easynav_simple_common
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+0.4.0 (2026-07-26)
+------------------
+* Sync with Rolling branch
+* GPLv3 -> Apache 2.0
+* Contributors: Francisco Martín Rico, Juan S. Cely G., Miguel, migueldm
+
+0.2.1 (2026-02-27)
+------------------
+* 0.2.0
+* GPLv3 -> Apache 2.0
+* Rolling updates
+* TFInfo in RTTFBuffer
+* Fix downsample in Simple
+* Cleanup unused headers
+* Contributors: Francisco Martín Rico, Francisco Miguel Moreno, Juan S. Cely G., Miguel
+
0.0.2 (2025-10-12)
------------------
* Reorganization initial
diff --git a/common/easynav_simple_common/include/easynav_simple_common/SimpleMap.hpp b/common/easynav_simple_common/include/easynav_simple_common/SimpleMap.hpp
index 331a820c..4039c221 100644
--- a/common/easynav_simple_common/include/easynav_simple_common/SimpleMap.hpp
+++ b/common/easynav_simple_common/include/easynav_simple_common/SimpleMap.hpp
@@ -1,21 +1,17 @@
// Copyright 2025 Intelligent Robotics Lab
//
// This file is part of the project Easy Navigation (EasyNav in short)
-// licensed under the GNU General Public License v3.0.
-// See for details.
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
//
-// Easy Navigation program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
+// http://www.apache.org/licenses/LICENSE-2.0
//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
/// \file
/// \brief Declaration of the SimpleMap type.
@@ -24,11 +20,7 @@
#define EASYNAV_PLANNER__SIMPLEMAP_HPP_
#include
-#include
-#include
#include
-#include
-#include
#include "nav_msgs/msg/occupancy_grid.hpp"
diff --git a/common/easynav_simple_common/package.xml b/common/easynav_simple_common/package.xml
index 63b5bae1..0838a159 100644
--- a/common/easynav_simple_common/package.xml
+++ b/common/easynav_simple_common/package.xml
@@ -2,10 +2,10 @@
easynav_simple_common
- 0.0.2
+ 0.4.0
Easy Navigation: Simple Common package.
Francisco Martín Rico
- GPL-3.0-only
+ Apache-2.0
ament_cmake
diff --git a/common/easynav_simple_common/src/easynav_simple_common/SimpleMap.cpp b/common/easynav_simple_common/src/easynav_simple_common/SimpleMap.cpp
index e469b9c1..e52eeaec 100644
--- a/common/easynav_simple_common/src/easynav_simple_common/SimpleMap.cpp
+++ b/common/easynav_simple_common/src/easynav_simple_common/SimpleMap.cpp
@@ -1,22 +1,17 @@
// Copyright 2025 Intelligent Robotics Lab
//
// This file is part of the project Easy Navigation (EasyNav in short)
-// licensed under the GNU General Public License v3.0.
-// See for details.
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
//
-// Easy Navigation program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
+// http://www.apache.org/licenses/LICENSE-2.0
//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
-
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
#include
#include
@@ -296,7 +291,8 @@ SimpleMap::downsample_factor(int factor) const
}
}
- new_map->at(x, y) = (occupied_count > (factor * factor / 2));
+ // Mark cell as occupied if any source cell in the block is occupied
+ new_map->at(x, y) = (occupied_count > 0);
}
}
diff --git a/common/easynav_simple_common/tests/simple_maps_tests.cpp b/common/easynav_simple_common/tests/simple_maps_tests.cpp
index d3f97cb6..b51b2d2b 100644
--- a/common/easynav_simple_common/tests/simple_maps_tests.cpp
+++ b/common/easynav_simple_common/tests/simple_maps_tests.cpp
@@ -1,28 +1,22 @@
// Copyright 2025 Intelligent Robotics Lab
//
// This file is part of the project Easy Navigation (EasyNav in short)
-// licensed under the GNU General Public License v3.0.
-// See for details.
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
//
-// Easy Navigation program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
+// http://www.apache.org/licenses/LICENSE-2.0
//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
#include
#include "easynav_simple_common/SimpleMap.hpp"
-#include
-
/// \brief Fixture for SimpleMap tests (minimal)
class SimpleMapTest : public ::testing::Test
{
@@ -127,7 +121,8 @@ TEST_F(SimpleMapTest, OccupancyGridConversion)
};
for (std::size_t i = 0; i < grid_msg.data.size(); ++i) {
- bool is_occupied = std::find(expected_indices.begin(),
+ bool is_occupied = std::find(
+ expected_indices.begin(),
expected_indices.end(), i) != expected_indices.end();
EXPECT_EQ(grid_msg.data[i], is_occupied ? 100 : 0);
}
@@ -174,7 +169,7 @@ TEST_F(SimpleMapTest, DownsampleIntegerFactor)
EXPECT_TRUE(downsampled->at(0, 0));
EXPECT_FALSE(downsampled->at(1, 0));
EXPECT_FALSE(downsampled->at(0, 1));
- EXPECT_FALSE(downsampled->at(1, 1));
+ EXPECT_TRUE(downsampled->at(1, 1));
}
/// \brief Downsampling to new resolution
@@ -197,9 +192,9 @@ TEST_F(SimpleMapTest, DownsampleToResolution)
EXPECT_DOUBLE_EQ(downsampled->origin_x(), -1.0);
EXPECT_DOUBLE_EQ(downsampled->origin_y(), -1.0);
- EXPECT_FALSE(downsampled->at(0, 0));
- EXPECT_FALSE(downsampled->at(1, 1));
- EXPECT_FALSE(downsampled->at(2, 2));
+ EXPECT_TRUE(downsampled->at(0, 0));
+ EXPECT_TRUE(downsampled->at(1, 1));
+ EXPECT_TRUE(downsampled->at(2, 2));
}
/// \brief Downsample that results in cropped edges
diff --git a/controllers/easynav_mpc_controller/CHANGELOG.rst b/controllers/easynav_mpc_controller/CHANGELOG.rst
new file mode 100644
index 00000000..b77f4bbc
--- /dev/null
+++ b/controllers/easynav_mpc_controller/CHANGELOG.rst
@@ -0,0 +1,60 @@
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Changelog for package easynav_mpc_controller
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+0.4.0 (2026-07-26)
+------------------
+* Add missing easynav_sensors deps
+* Adaptations to `#94 `_
+* Update plugins to new sensors API
+* GPLv3 -> Apache 2.0
+* Contributors: Francisco Martín Rico, Francisco Miguel Moreno, Juan S. Cely G., Miguel, migueldm
+
+* Add missing easynav_sensors deps
+* Add missing easynav_sensors deps
+* Sync with Rolling branch
+* Sync with rolling branch
+* Merge pull request `#61 `_ from Butakus/rolling
+ Update plugins to new sensors API
+* Adaptations to `#94 `_
+* Update plugins to new sensors API
+* 0.3.1
+* update changelogs
+* Merge branch 'rolling' of github.com:midemig/easynav_plugins into rolling
+* Merge branch 'EasyNavigation:rolling' into rolling
+* Merge pull request `#51 `_ from EasyNavigation/license-kilted
+ GPLv3 -> Apache 2.0
+* GPLv3 -> Apache 2.0
+* Merge pull request `#50 `_ from EasyNavigation/license
+ GPLv3 -> Apache 2.0
+* Merge pull request `#49 `_ from juanscelyg/rolling
+ Latest Updates and Corrected CI
+* Merge pull request `#48 `_ from juanscelyg/kilted
+ Kilted
+* GPLv3 -> Apache 2.0
+* Latest changes were added
+* Dependencies were corrected
+* Contributors: Francisco Martín Rico, Francisco Miguel Moreno, Juan S. Cely G., Miguel, migueldm
+
+0.3.1 (2026-02-27)
+------------------
+* GPLv3 -> Apache 2.0
+* Remove C++20/C++23 features and update to new MethodBase interface
+* TFInfo in RTTFBuffer
+* Refactor to use TFInfo
+* Trim path for MPC
+* Stop controllers at IDLE
+* Obstacles are detected
+* Methods were added to access to private attributes
+* Optimizer header file was added
+* Dependencies were fixed
+* MPC with differential model is working
+* MPC Controller minimizes path following
+* Visualization Marker was added
+* CI updated
+* MPC controller is working
+* Optimizer was changed
+* Contributors: Francisco Martín Rico, Francisco Miguel Moreno, José Miguel Guerrero, Juan S. Cely G., Miguel
+
+0.0.2 (2025-10-12)
+------------------
diff --git a/controllers/easynav_mpc_controller/CMakeLists.txt b/controllers/easynav_mpc_controller/CMakeLists.txt
new file mode 100644
index 00000000..a8ff1d0b
--- /dev/null
+++ b/controllers/easynav_mpc_controller/CMakeLists.txt
@@ -0,0 +1,107 @@
+cmake_minimum_required(VERSION 3.20)
+project(easynav_mpc_controller)
+
+if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
+ add_compile_options(-Wall -Wextra -Wpedantic)
+endif()
+
+find_package(ament_cmake REQUIRED)
+find_package(easynav_common REQUIRED)
+find_package(easynav_core REQUIRED)
+find_package(easynav_system REQUIRED)
+find_package(easynav_sensors REQUIRED)
+find_package(pluginlib REQUIRED)
+find_package(geometry_msgs REQUIRED)
+find_package(tf2_ros REQUIRED)
+find_package(tf2 REQUIRED)
+find_package(nav_msgs REQUIRED)
+find_package(sensor_msgs REQUIRED)
+find_package(Eigen3 REQUIRED NO_MODULE)
+find_package(PCL REQUIRED COMPONENTS common io)
+find_package(pcl_conversions REQUIRED)
+find_package(NLopt QUIET)
+if(NOT NLopt_FOUND)
+ find_path(NLOPT_INCLUDE_DIR nlopt.hpp)
+ find_library(NLOPT_LIBRARY nlopt)
+ if(NLOPT_INCLUDE_DIR AND NLOPT_LIBRARY)
+ set(NLopt_FOUND TRUE)
+ set(NLopt_INCLUDE_DIRS ${NLOPT_INCLUDE_DIR})
+ set(NLopt_LIBRARIES ${NLOPT_LIBRARY})
+ endif()
+endif()
+if(NOT NLopt_FOUND)
+ message(FATAL_ERROR "NLopt not found. Install libnlopt-dev or set NLopt_DIR/CMAKE_PREFIX_PATH.")
+endif()
+
+add_library(${PROJECT_NAME} SHARED
+ src/easynav_mpc_controller/MPCController.cpp
+ src/easynav_mpc_controller/MPCOptimizer.cpp
+)
+target_include_directories(${PROJECT_NAME} PUBLIC
+ $
+ $
+ ${PCL_INCLUDE_DIRS}
+ ${pcl_conversions_INCLUDE_DIRS}
+)
+target_link_libraries(${PROJECT_NAME} PUBLIC
+ easynav_common::easynav_common
+ easynav_core::easynav_core
+ easynav_system::easynav_system
+ easynav_sensors::easynav_sensors
+ tf2_ros::tf2_ros
+ tf2::tf2
+ pluginlib::pluginlib
+ Eigen3::Eigen
+ nlopt
+ ${geometry_msgs_TARGETS}
+ ${nav_msgs_TARGETS}
+ ${sensor_msgs_TARGETS}
+ ${PCL_LIBRARIES}
+)
+
+install(
+ DIRECTORY include/
+ DESTINATION include/${PROJECT_NAME}
+)
+
+install(TARGETS
+ ${PROJECT_NAME}
+ EXPORT export_${PROJECT_NAME}
+ ARCHIVE DESTINATION lib
+ LIBRARY DESTINATION lib
+ RUNTIME DESTINATION lib/${PROJECT_NAME}
+)
+
+if(BUILD_TESTING)
+ find_package(ament_lint_auto REQUIRED)
+ set(ament_cmake_copyright_FOUND TRUE)
+ set(ament_cmake_cpplint_FOUND TRUE)
+ ament_lint_auto_find_test_dependencies()
+
+ find_package(ament_cmake_gtest REQUIRED)
+ # add_subdirectory(tests)
+endif()
+
+ament_export_include_directories("include/${PROJECT_NAME}")
+ament_export_libraries(${PROJECT_NAME})
+ament_export_targets(export_${PROJECT_NAME})
+
+# Register the planning plugins
+pluginlib_export_plugin_description_file(easynav_core easynav_mpc_controller_plugins.xml)
+
+ament_export_dependencies(
+ easynav_common
+ easynav_core
+ easynav_sensors
+ pluginlib
+ tf2_ros
+ tf2
+ geometry_msgs
+ nav_msgs
+ sensor_msgs
+ pcl_conversions
+ eigen
+ nlopt
+)
+
+ament_package()
diff --git a/controllers/easynav_mpc_controller/README.md b/controllers/easynav_mpc_controller/README.md
new file mode 100644
index 00000000..9d6fbbb3
--- /dev/null
+++ b/controllers/easynav_mpc_controller/README.md
@@ -0,0 +1,64 @@
+# easynav_mpc_controller
+
+[](#)
+
+
+## Description
+A Model Predictive Controller (MPC) implementation for Easy Navigation.
+
+## Authors and Maintainers
+- **Authors:** Intelligent Robotics Lab
+- **Maintainers:** Juan S. Cely G.
+
+## Supported ROS 2 Distributions
+| Distribution | Status |
+|---|---|
+| rolling |  |
+
+## Plugin (pluginlib)
+- **Plugin Name:** `easynav_mpc_controller/MPCController`
+- **Type:** `easynav::MPCController`
+- **Base Class:** `easynav::ControllerMethodBase`
+- **Library:** `easynav_mpc_controller`
+- **Description:** A Model Predictive Controller (MPC) implementation for Easy Navigation.
+
+## Parameters
+All parameters are declared under the plugin namespace, i.e., `//easynav_mpc_controller/MPCController/...`.
+
+| Name | Type | Default | Description |
+|---|---|---:|---|
+| `.horizon_steps` | `int` | `5` | Number of time steps in the prediction horizon. |
+| `.dt` | `double` | `0.1` | Integration time step (seconds). |
+| `.safety_radius` | `double` | `0.35` | Safety radius to check possible collisions. |
+| `.max_linear_velocity` | `double` | `1.5` | Maximum linear velocity (m/s). |
+| `.max_angular_velocity` | `double` | `1.5` | Maximum angular velocity (rad/s). |
+| `.verbose` | `bool` | `false` | Show data on terminal about Optimization. |
+
+
+## Interfaces (Topics and Services)
+
+### Subscriptions and Publications
+| Direction | Topic | Type | Purpose | QoS |
+|---|---|---|---|---|
+| Publisher | `/mpc/path` | `nav_msgs/msg/path` | MPC trajectories generate by Predictive Component. | QoS depth=10 |
+| Publisher | `/mpc/detection` | `sensor_msgs::msg::PointCloud2` | Detections used by collision checker. | QoS depth=10 |
+
+
+### Services
+This package does not create service servers or clients.
+
+
+## NavState Keys
+| Key | Type | Access | Notes |
+|---|---|---|---|
+| `path` | `nav_msgs::msg::Path` | **Read** | Target path to track. |
+| `robot_pose` | `nav_msgs::msg::Odometry` | **Read** | Current robot pose/state. |
+| `cmd_vel` | `geometry_msgs::msg::TwistStamped` | **Read** | Last commanded velocity (if provided in state). |
+| `points` | `PointPerceptions` | **Read** | Perception point cloud(s) used for costs. |
+
+
+## TF Frames
+This controller does not explicitly publish or require TF frames in code.
+
+## License
+Apache-2.0
diff --git a/controllers/easynav_mpc_controller/easynav_mpc_controller_plugins.xml b/controllers/easynav_mpc_controller/easynav_mpc_controller_plugins.xml
new file mode 100644
index 00000000..cebf350e
--- /dev/null
+++ b/controllers/easynav_mpc_controller/easynav_mpc_controller_plugins.xml
@@ -0,0 +1,9 @@
+
+
+
+
+ A MPC implementation for a Controller.
+
+
+
+
diff --git a/controllers/easynav_mpc_controller/include/easynav_mpc_controller/MPCController.hpp b/controllers/easynav_mpc_controller/include/easynav_mpc_controller/MPCController.hpp
new file mode 100644
index 00000000..acef3630
--- /dev/null
+++ b/controllers/easynav_mpc_controller/include/easynav_mpc_controller/MPCController.hpp
@@ -0,0 +1,105 @@
+// Copyright 2025 Intelligent Robotics Lab
+//
+// This file is part of the project Easy Navigation (EasyNav in short)
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// #pragma once
+#ifndef EASYNAV_MPC_CONTROLLER__MPCCONTROLLER_HPP_
+#define EASYNAV_MPC_CONTROLLER__MPCCONTROLLER_HPP_
+
+#include
+#include
+#include
+#include
+
+#include
+
+#include "geometry_msgs/msg/pose.hpp"
+#include "geometry_msgs/msg/pose_stamped.hpp"
+#include "geometry_msgs/msg/twist_stamped.hpp"
+#include "nav_msgs/msg/odometry.hpp"
+#include "nav_msgs/msg/path.hpp"
+
+#include
+#include
+
+#include "sensor_msgs/msg/point_cloud2.hpp"
+
+#include "easynav_core/ControllerMethodBase.hpp"
+#include "easynav_common/types/NavState.hpp"
+#include "easynav_sensors/types/PointPerception.hpp"
+
+#include "easynav_mpc_controller/MPCOptimizer.hpp"
+
+namespace easynav
+{
+
+/// \brief A MPC Controller.
+class MPCController : public ControllerMethodBase
+{
+public:
+ MPCController();
+
+ /// \brief Destructor.
+ ~MPCController() override;
+
+ /// \brief Initializes parameters and MPC controller.
+ /// \throws std::runtime_error if initialization fails.
+ void on_initialize() override;
+
+ /// \brief Updates the controller using the given NavState.
+ /// \param nav_state Current navigation state, including odometry and planned path.
+ void update_rt(NavState & nav_state) override;
+
+ /// \brief Publishes the selected path by MPC controller
+ /// \param data MPCParameters object as data pointer
+ /// \param best_path Vector of velocities generated by MPC
+ /// \param path Path obtained from navstate
+ void publish_mpc_path(
+ void * data, const std::vector & best_path,
+ nav_msgs::msg::Path path);
+
+ /// \brief Check if there is a possible collision
+ /// \param data MPCParameters object as data pointer
+ /// \param u Vector of velocities generated by MPC
+ void collision_checker(void * data, std::vector & u);
+
+protected:
+ int horizon_steps_{5}; ///< Prediction horizon for MPC.
+ double dt_{0.1}; ///< Time step for MPC.
+ double safety_radius_{0.35}; ///< Safety radius to avoid obstacles
+ double max_lin_vel_{1.5}; ///< Maximum linear velocity for MPC.
+ double max_ang_vel_{1.5}; ///< Maximum angular velocity for MPC.
+ bool verbose_{false}; ///< Value to debug on terminal
+ double last_v_{0.0}; ///< Last value for linear velocity before than collision
+ double last_w_{0.0}; ///< Last value for angular velocity before than collision
+ bool collision_state_{false}; ///< Collision state flag
+ double collision_factor_{0.618033}; ///< Collision avoidance for recalculate velocities
+
+ // Fallback goal tolerances if GoalManager does not publish them
+ double fallback_goal_pos_tol_{0.05}; ///< Default positional tolerance (meters).
+ double fallback_goal_yaw_tol_{0.05}; ///< Default angular tolerance (radians).
+
+ std::unique_ptr optimizer_; ///< MPC optimizer
+
+ rclcpp::Publisher::SharedPtr mpc_path_pub_; ///< Publisher for MPC path.
+ rclcpp::Publisher::SharedPtr detection_pub_; ///< Publisher for MPC obstacles.
+
+private:
+ geometry_msgs::msg::TwistStamped cmd_vel_; ///< Current velocity command.
+
+};
+
+} // namespace easynav
+
+#endif // EASYNAV_MPC_CONTROLLER__MPCCONTROLLER_HPP_
diff --git a/controllers/easynav_mpc_controller/include/easynav_mpc_controller/MPCOptimizer.hpp b/controllers/easynav_mpc_controller/include/easynav_mpc_controller/MPCOptimizer.hpp
new file mode 100644
index 00000000..40ca6ce6
--- /dev/null
+++ b/controllers/easynav_mpc_controller/include/easynav_mpc_controller/MPCOptimizer.hpp
@@ -0,0 +1,130 @@
+// Copyright 2025 Intelligent Robotics Lab
+//
+// This file is part of the project Easy Navigation (EasyNav in short)
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// #pragma once
+#ifndef EASYNAV_MPC_CONTROLLER__MPCOPTIMIZER_HPP_
+#define EASYNAV_MPC_CONTROLLER__MPCOPTIMIZER_HPP_
+
+#include "geometry_msgs/msg/pose.hpp"
+#include "nav_msgs/msg/path.hpp"
+#include "pcl/point_cloud.h"
+#include "pcl/point_types.h"
+
+namespace easynav
+{
+
+/// \brief A MPC parameters class.
+class MPCParameters
+{
+public:
+ MPCParameters(
+ Eigen::Vector2d goal,
+ Eigen::Vector3d x0,
+ Eigen::Vector3d theta0,
+ const pcl::PointCloud & points,
+ int N,
+ double dt);
+
+ /// \brief Destructor.
+ ~MPCParameters();
+
+ Eigen::Vector2d goal; ///< Goal pose (x,y) to optimizer.
+ Eigen::Vector3d x0; ///< Init pos (x,y,z).
+ Eigen::Vector3d theta0; ///< Init orientation (roll,pitch,yaw).
+ const pcl::PointCloud & points; ///< Filtered Point Cloud to detect collisions.
+
+ /// \brief Get the number of horizont steps
+ /// \return integer value of amount of horizont step used in optimization
+ int get_steps();
+
+ /// \brief Get the differential time used by integrator in kinematic model
+ /// \return double value of time in seconds
+ double get_timestep();
+
+ /// \brief Get angular cost used in angle optimization
+ /// \return double value of angular cost
+ double get_angular_tracking_cost();
+
+ /// \brief Get effort cost matrix used in optimization
+ /// \return Eigen::Matrix2d as effort cost matrix
+ Eigen::Matrix2d get_effort_cost();
+
+ /// \brief Get tracking cost matrix used in optimization
+ /// \return Eigen::Matrix2d as tracking cost matrix
+ Eigen::Matrix2d get_tracking_cost();
+
+ /// \brief Get smooth cost matrix used in optimization
+ /// \return Eigen::Matrix2d as smooth cost matrix
+ Eigen::Matrix2d get_smooth_cost();
+
+private:
+ int N_ {5}; ///< Horizont Step to optimize.
+ double dt_ {0.1}; ///< Differential time to integrate.
+ Eigen::Matrix2d Q_ {{4.0, 0.0}, {0.0, 4.0}}; ///< Tracking Cost Matrix
+ Eigen::Matrix2d R_ {{0.1, 0.0}, {0.0, 0.1}}; ///< Effort Cost Matrix
+ Eigen::Matrix2d Rd_ {{0.1, 0.0}, {0.0, 0.1}}; ///< Smooth Cost Matrix
+ double qtheta_ {3.0}; ///< Angular cost value.
+
+};
+
+/// \brief A MPC Optimizer class.
+class MPCOptimizer
+{
+public:
+ MPCOptimizer();
+
+ /// \brief Destructor.
+ ~MPCOptimizer();
+
+ /// \brief Kinematic model for a particle used by optmizer to stimate final position
+ /// \param x Eigen::Vector3d Initial position for robot (x,y,z)
+ /// \param q Eigen::Vector3d Initial angel for robot (roll, pitch , yaw)
+ /// \param v double lineal velocity
+ /// \param w double angular velocity
+ /// \param dt double differential time used for integration
+ /// \return Eigen::Vector3d Final state for robot (x,y,yaw)
+ Eigen::Vector3d kinematic_model(
+ const Eigen::Vector3d & x,
+ const Eigen::Vector3d & q, double v, double w, double dt);
+
+ /// \brief Wrap for real cost function
+ /// \param u std::vector Velocity vector to be optimized
+ /// \param grad std::vector gradient values for optimizer. It is NOT used for this implementation.
+ /// \param data MPCParameter pointer with parameters used by optimizer
+ /// \return double cost value used by nlOpt in internal callback
+ double cost_function(const std::vector & u, [[maybe_unused]]
+ std::vector & grad, void * data);
+
+ /// \brief Real cost function with static propierties
+ /// \param u std::vector Velocity vector to be optimized
+ /// \param grad std::vector gradient values for optimizer. It is NOT used for this implementation.
+ /// \param data MPCParameter pointer with parameters used by optimizer
+ /// \return double cost value used by nlOpt in internal callback
+ static double nlopt_cost_callback(
+ const std::vector & x,
+ std::vector & grad, void * data);
+
+};
+
+/// \brief Struct used as element in callback.
+struct NLoptCallbackData
+{
+ MPCOptimizer * optimizer; ///< Pointer to optimizer.
+ MPCParameters * params; ///< Pointer to parameter for optimizer.
+};
+
+} // namespace easynav
+
+#endif // EASYNAV_MPC_CONTROLLER__MPCOPTIMIZER_HPP_
diff --git a/controllers/easynav_mpc_controller/package.xml b/controllers/easynav_mpc_controller/package.xml
new file mode 100644
index 00000000..0a102524
--- /dev/null
+++ b/controllers/easynav_mpc_controller/package.xml
@@ -0,0 +1,38 @@
+
+
+
+ easynav_mpc_controller
+ 0.4.0
+ Easy Navigation: MPC Controller package.
+ Juan S. Cely G.
+ Apache-2.0
+
+ ament_cmake
+ libnlopt-cxx-dev
+ libnlopt-dev
+ libnlopt0
+
+ easynav_common
+ easynav_core
+ easynav_system
+ easynav_sensors
+ pluginlib
+ tf2_ros
+ tf2
+ geometry_msgs
+ nav_msgs
+ sensor_msgs
+ pcl_conversions
+ eigen
+
+ rclcpp_lifecycle
+ easynav_simple_common
+ std_srvs
+ ament_lint_auto
+ ament_lint_common
+ ament_cmake_gtest
+
+
+ ament_cmake
+
+
diff --git a/controllers/easynav_mpc_controller/src/easynav_mpc_controller/MPCController.cpp b/controllers/easynav_mpc_controller/src/easynav_mpc_controller/MPCController.cpp
new file mode 100644
index 00000000..114382e5
--- /dev/null
+++ b/controllers/easynav_mpc_controller/src/easynav_mpc_controller/MPCController.cpp
@@ -0,0 +1,361 @@
+// Copyright 2025 Intelligent Robotics Lab
+//
+// This file is part of the project Easy Navigation (EasyNav in short)
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/// \file
+/// \brief Implementation of the MPCController class.
+
+#include "easynav_mpc_controller/MPCController.hpp"
+#include "easynav_system/GoalManager.hpp"
+
+#include "easynav_common/RTTFBuffer.hpp"
+
+namespace easynav
+{
+
+MPCController::MPCController() {}
+
+MPCController::~MPCController() = default;
+
+void
+MPCController::on_initialize()
+{
+ auto node = get_node();
+ const auto & plugin_name = get_plugin_name();
+
+ node->declare_parameter(plugin_name + ".horizon_steps", horizon_steps_);
+ node->declare_parameter(plugin_name + ".dt", dt_);
+ node->declare_parameter(plugin_name + ".safety_radius", safety_radius_);
+ node->declare_parameter(plugin_name + ".max_linear_velocity", max_lin_vel_);
+ node->declare_parameter(plugin_name + ".max_angular_velocity", max_ang_vel_);
+ node->declare_parameter(plugin_name + ".verbose", verbose_);
+
+ node->declare_parameter(plugin_name + ".fallback_goal_pos_tol", fallback_goal_pos_tol_);
+ node->declare_parameter(plugin_name + ".fallback_goal_yaw_tol", fallback_goal_yaw_tol_);
+
+ node->get_parameter(plugin_name + ".horizon_steps", horizon_steps_);
+ node->get_parameter(plugin_name + ".dt", dt_);
+ node->get_parameter(plugin_name + ".safety_radius", safety_radius_);
+ node->get_parameter(plugin_name + ".max_linear_velocity", max_lin_vel_);
+ node->get_parameter(plugin_name + ".max_angular_velocity", max_ang_vel_);
+ node->get_parameter(plugin_name + ".verbose", verbose_);
+
+ node->get_parameter(plugin_name + ".fallback_goal_pos_tol", fallback_goal_pos_tol_);
+ node->get_parameter(plugin_name + ".fallback_goal_yaw_tol", fallback_goal_yaw_tol_);
+
+ optimizer_ = std::make_unique();
+
+ mpc_path_pub_ =
+ node->create_publisher("/mpc/path", 10);
+
+ detection_pub_ =
+ node->create_publisher("/mpc/detection", 10);
+}
+
+void
+MPCController::publish_mpc_path(
+ void * data, const std::vector & best_vel,
+ nav_msgs::msg::Path path)
+{
+ MPCParameters * params = reinterpret_cast(data);
+ nav_msgs::msg::Path mpc_path_;
+
+ if (best_vel.size() > 0) {
+ mpc_path_.header.stamp = get_node()->now();
+ mpc_path_.header.frame_id = path.header.frame_id;
+ for (size_t i = 0; i + 1 < best_vel.size(); i += 2) {
+ geometry_msgs::msg::PoseStamped pose_stamped;
+ double v = best_vel[i];
+ double w = best_vel[i + 1];
+ pose_stamped.header.frame_id = path.header.frame_id;
+ pose_stamped.header.stamp = path.header.stamp;
+ auto state = optimizer_->kinematic_model(params->x0, params->theta0, v, w, dt_);
+ pose_stamped.pose.position.x = state[0];
+ pose_stamped.pose.position.y = state[1];
+ mpc_path_.poses.push_back(pose_stamped);
+ }
+ mpc_path_pub_->publish(mpc_path_);
+
+ }
+}
+
+void
+MPCController::collision_checker(void * data, std::vector & u)
+{
+ MPCParameters * params = reinterpret_cast(data);
+ double x_m = 0.0, y_m = 0.0, dist = 0.0, angle = 0.0;
+ size_t real_points = 0;
+ for (const auto & point : params->points) {
+ if (!std::isnan(point.x) || !std::isnan(point.y)) {
+ x_m += (point.x - params->x0[0]);
+ y_m += (point.y - params->x0[1]);
+ real_points++;
+ }
+ }
+ if (real_points != 0) {
+ x_m /= real_points;
+ y_m /= real_points;
+ dist = std::hypot(x_m, y_m);
+ angle = std::atan2(y_m, x_m) - params->theta0[2];
+ if (dist < safety_radius_) {
+ if (!collision_state_) {
+ collision_state_ = true;
+ last_v_ = u[0];
+ last_w_ = u[1];
+ }
+ RCLCPP_WARN(
+ get_node()->get_logger(),
+ "[COLLISION] Collision detected at: [%f] m and Theta: [%f] degrees", dist,
+ (angle * 180.00 / M_PI) );
+ last_v_ = collision_factor_ * last_v_ * safety_radius_ / dist;
+ last_w_ = collision_factor_ * last_w_ * safety_radius_ / dist;
+ u[0] = -last_v_;
+ u[1] = -last_w_;
+ } else {
+ collision_state_ = false;
+ }
+ }
+}
+
+void
+MPCController::update_rt(NavState & nav_state)
+{
+ // If navigation is IDLE, force zero velocity
+ if (nav_state.has("navigation_state")) {
+ const auto nav_state_val = nav_state.get("navigation_state");
+ if (nav_state_val == easynav::GoalManager::State::IDLE) {
+ cmd_vel_.header.stamp = get_node()->now();
+ cmd_vel_.twist.linear.x = 0.0;
+ cmd_vel_.twist.angular.z = 0.0;
+ nav_state.set("cmd_vel", cmd_vel_);
+ return;
+ }
+ }
+
+ const auto & perceptions = nav_state.get_no_group();
+
+ if (!nav_state.has("path") || !nav_state.has("robot_pose") || perceptions.empty()) {
+ if (verbose_) {
+ std::cout << "No Path, No Points or No Robot Pose" << std::endl;
+ }
+ return;
+ }
+
+ nav_msgs::msg::Path path = nav_state.get("path");
+ if (path.poses.empty()) {
+ // If the path is empty, stop the robot
+ cmd_vel_.header.frame_id = path.header.frame_id;
+ cmd_vel_.header.stamp = get_node()->now();
+ cmd_vel_.twist.linear.x = 0.0;
+ cmd_vel_.twist.angular.z = 0.0;
+ nav_state.set("cmd_vel", cmd_vel_);
+ return;
+ }
+
+ // Build a local path that:
+ // 1) keeps only the segment that brings the robot closer to the goal, and
+ // 2) prepends a short straight segment from the robot pose to that segment.
+ const auto & robot_pose_msg = nav_state.get("robot_pose");
+ const auto & robot_p = robot_pose_msg.pose.pose.position;
+
+ // Goal is the last point of the planner path
+ const auto & goal_p = path.poses.back().pose.position;
+
+ nav_msgs::msg::Path local_path;
+ local_path.header = path.header;
+ local_path.poses.clear();
+
+ // 1) Find the first index that actually brings us closer to the goal than our current pose
+ const double d_robot_goal = std::hypot(goal_p.x - robot_p.x, goal_p.y - robot_p.y);
+ std::size_t start_idx = 0;
+ for (std::size_t i = 0; i < path.poses.size(); ++i) {
+ const auto & pi = path.poses[i].pose.position;
+ const double d_pi_goal = std::hypot(goal_p.x - pi.x, goal_p.y - pi.y);
+ if (d_pi_goal <= d_robot_goal) {
+ start_idx = i;
+ break;
+ }
+ }
+
+ // 2) Prepend a point at the robot position to ensure continuity from the current pose
+ geometry_msgs::msg::PoseStamped robot_ps;
+ robot_ps.header = path.header;
+ robot_ps.pose.position = robot_p;
+ robot_ps.pose.orientation = path.poses[start_idx].pose.orientation;
+ local_path.poses.push_back(robot_ps);
+
+ // 3) Copy the remaining points from start_idx to the goal
+ for (std::size_t i = start_idx; i < path.poses.size(); ++i) {
+ local_path.poses.push_back(path.poses[i]);
+ }
+
+ // Use the local path from now on
+ path = local_path;
+
+ int num_elements = path.poses.size();
+ size_t local_horizon;
+ if (num_elements > horizon_steps_) {
+ local_horizon = horizon_steps_;
+ } else {
+ local_horizon = num_elements - 1;
+ }
+ const auto & last_pose = path.poses[local_horizon].pose.position;
+ const auto & tf_info = RTTFBuffer::getInstance()->get_tf_info();
+
+ const auto & filtered = PointPerceptionsOpsView(perceptions)
+ .filter({-2.0, -0.35, -1.0}, {0.0, 0.35, 1.0})
+ .fuse(tf_info.map_frame)
+ .filter({NAN, NAN, 0.1}, {NAN, NAN, NAN})
+ .collapse({NAN, NAN, 0.1})
+ .downsample(0.1)
+ .as_points();
+
+ sensor_msgs::msg::PointCloud2 cloud_out;
+ pcl::toROSMsg(filtered, cloud_out);
+ cloud_out.header.frame_id = path.header.frame_id;
+ cloud_out.header.stamp = get_node()->now();
+ detection_pub_->publish(cloud_out);
+
+ const auto pose = nav_state.get("robot_pose").pose.pose;
+ double roll_, pitch_, yaw_;
+ tf2::Quaternion q(
+ pose.orientation.x,
+ pose.orientation.y,
+ pose.orientation.z,
+ pose.orientation.w);
+ tf2::Matrix3x3 m(q);
+ m.getRPY(roll_, pitch_, yaw_);
+
+ // MPC Code
+ double minf;
+ std::vector u(2 * horizon_steps_, 0.0);
+
+ auto params = MPCParameters(
+ Eigen::Vector2d(static_cast(last_pose.x), static_cast(last_pose.y)),
+ {pose.position.x, pose.position.y, pose.position.z},
+ {roll_, pitch_, yaw_},
+ filtered,
+ static_cast(horizon_steps_),
+ dt_);
+
+ NLoptCallbackData cbdata{optimizer_.get(), ¶ms};
+
+ nlopt::opt opt(nlopt::LN_COBYLA, static_cast(u.size()));
+ opt.set_min_objective(easynav::MPCOptimizer::nlopt_cost_callback, &cbdata);
+
+ std::vector lb(2 * horizon_steps_);
+ std::vector ub(2 * horizon_steps_);
+ for (int k = 0; k < horizon_steps_; k++) {
+ lb[2 * k] = -max_lin_vel_;
+ lb[2 * k + 1] = -max_ang_vel_;
+ ub[2 * k] = max_lin_vel_;
+ ub[2 * k + 1] = max_ang_vel_;
+ }
+ opt.set_lower_bounds(lb);
+ opt.set_upper_bounds(ub);
+ opt.set_xtol_rel(1e-3);
+ opt.set_ftol_rel(1e-3);
+ // opt.set_maxeval(1000);
+
+ try {
+ nlopt::result result = opt.optimize(u, minf);
+ if (verbose_) {
+ if (result > 0) {
+ std::cerr << "Optimization Successful " << std::endl;
+ std::cout << "Result: " << result << std::endl;
+ } else {
+ std::cerr << "Optimization Unsuccessful " << std::endl;
+ }
+ }
+
+ } catch (std::exception & e) {
+ std::cerr << "Optimization Error: " << e.what() << std::endl;
+ }
+
+ if (ControllerMethodBase::collision_checker_active_) {
+ collision_checker(¶ms, u);
+ }
+
+ // Final alignment phase with hysteresis on distance:
+ // - Enter when dist_to_goal <= 0.5 * pos_tol
+ // - Stay in this phase (even if dist grows slightly) until dist_to_goal > pos_tol
+ {
+ const auto & goal_pose = path.poses.back().pose;
+
+ double pos_tol = fallback_goal_pos_tol_;
+ double yaw_tol = fallback_goal_yaw_tol_;
+
+ if (nav_state.has("goal_tolerance.position")) {
+ pos_tol = nav_state.get("goal_tolerance.position");
+ }
+ if (nav_state.has("goal_tolerance.yaw")) {
+ yaw_tol = nav_state.get("goal_tolerance.yaw");
+ }
+
+ const double dx_g = goal_pose.position.x - pose.position.x;
+ const double dy_g = goal_pose.position.y - pose.position.y;
+ const double dist_to_goal = std::hypot(dx_g, dy_g);
+
+ const double yaw_goal = std::atan2(
+ 2.0 * (goal_pose.orientation.w * goal_pose.orientation.z +
+ goal_pose.orientation.x * goal_pose.orientation.y),
+ 1.0 - 2.0 * (goal_pose.orientation.y * goal_pose.orientation.y +
+ goal_pose.orientation.z * goal_pose.orientation.z));
+ double e_theta_goal = std::atan2(std::sin(yaw_ - yaw_goal), std::cos(yaw_ - yaw_goal));
+
+ const double enter_dist = 0.5 * pos_tol;
+
+ // Hysteresis based on distance to goal: start aligning when we are
+ // well inside the goal radius (enter_dist) and keep aligning until
+ // we move clearly outside (dist_to_goal > pos_tol).
+ const bool inside_hysteresis_band = (dist_to_goal <= pos_tol);
+ const bool should_enter_alignment = (dist_to_goal <= enter_dist);
+
+ if ((inside_hysteresis_band && std::fabs(e_theta_goal) > yaw_tol) ||
+ should_enter_alignment)
+ {
+ // Stay in place and rotate towards the goal orientation using a simple P controller
+ const double k_align = 1.0;
+ double vlin = 0.0;
+ double vrot = -k_align * e_theta_goal;
+
+ vrot = std::clamp(vrot, -max_ang_vel_, max_ang_vel_);
+
+ cmd_vel_.header.frame_id = path.header.frame_id;
+ cmd_vel_.header.stamp = get_node()->now();
+ cmd_vel_.twist.linear.x = vlin;
+ cmd_vel_.twist.angular.z = vrot;
+
+ nav_state.set("cmd_vel", cmd_vel_);
+ publish_mpc_path(¶ms, u, path);
+ return;
+ }
+ }
+
+ // Publish the computed velocity command
+ cmd_vel_.header.frame_id = path.header.frame_id;
+ cmd_vel_.header.stamp = get_node()->now();
+ cmd_vel_.twist.linear.x = u[0];
+ cmd_vel_.twist.angular.z = u[1];
+
+ nav_state.set("cmd_vel", cmd_vel_);
+
+ // Publish the path
+ publish_mpc_path(¶ms, u, path);
+}
+
+} // namespace easynav
+
+#include
+PLUGINLIB_EXPORT_CLASS(easynav::MPCController, easynav::ControllerMethodBase)
diff --git a/controllers/easynav_mpc_controller/src/easynav_mpc_controller/MPCOptimizer.cpp b/controllers/easynav_mpc_controller/src/easynav_mpc_controller/MPCOptimizer.cpp
new file mode 100644
index 00000000..067b1495
--- /dev/null
+++ b/controllers/easynav_mpc_controller/src/easynav_mpc_controller/MPCOptimizer.cpp
@@ -0,0 +1,150 @@
+// Copyright 2025 Intelligent Robotics Lab
+//
+// This file is part of the project Easy Navigation (EasyNav in short)
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/// \file
+/// \brief Implementation of MPCParameters and MPCOptimizer classes.
+
+#include "easynav_mpc_controller/MPCOptimizer.hpp"
+
+namespace easynav
+{
+
+MPCParameters::MPCParameters(
+ Eigen::Vector2d goal,
+ Eigen::Vector3d x0,
+ Eigen::Vector3d theta0,
+ const pcl::PointCloud & points,
+ int N,
+ double dt)
+: goal(goal), x0(x0), theta0(theta0), points(points), N_(N), dt_(dt) {}
+
+MPCParameters::~MPCParameters() = default;
+
+int
+MPCParameters::get_steps()
+{
+ return N_;
+}
+
+double
+MPCParameters::get_timestep()
+{
+ return dt_;
+}
+
+double
+MPCParameters::get_angular_tracking_cost()
+{
+ return qtheta_;
+}
+
+Eigen::Matrix2d
+MPCParameters::get_effort_cost()
+{
+ return R_;
+}
+
+Eigen::Matrix2d
+MPCParameters::get_tracking_cost()
+{
+ return Q_;
+}
+
+Eigen::Matrix2d
+MPCParameters::get_smooth_cost()
+{
+ return Rd_;
+}
+
+MPCOptimizer::MPCOptimizer() {}
+
+MPCOptimizer::~MPCOptimizer() = default;
+
+Eigen::Vector3d
+MPCOptimizer::kinematic_model(
+ const Eigen::Vector3d & x, const Eigen::Vector3d & q, double v,
+ double w, double dt)
+{
+ Eigen::Vector3d x_k1;
+ x_k1[0] = x[0] + v * cos(q[2]) * dt;
+ x_k1[1] = x[1] + v * sin(q[2]) * dt;
+ x_k1[2] = q[2] + w * dt;
+ return x_k1;
+}
+
+double
+MPCOptimizer::cost_function(
+ const std::vector & u,
+ [[maybe_unused]] std::vector & grad, void * data)
+{
+ MPCParameters * params = reinterpret_cast(data);
+
+ Eigen::Vector3d position = params->x0;
+ Eigen::Vector3d orientation = params->theta0;
+ Eigen::Vector3d state;
+ int N = params->get_steps();
+ double dt = params->get_timestep();
+ double qtheta = params->get_angular_tracking_cost();
+ double cost = 0.0;
+
+ Eigen::Matrix2d R = params->get_effort_cost();
+ Eigen::Matrix2d Q = params->get_tracking_cost();
+ Eigen::Matrix2d Rd = params->get_smooth_cost();
+
+ for (int i = 0; i < N; ++i) {
+ double v = u[2 * i];
+ double w = u[2 * i + 1];
+ double dv, dw;
+ if (i < (N - 1)) {
+ dv = u[2 * (i + 1)] - u[2 * i];
+ dw = u[2 * (i + 1) + 1] - u[2 * i + 1];
+ } else {
+ dv = 0.0;
+ dw = 0.0;
+ }
+
+ state = MPCOptimizer::kinematic_model(position, orientation, v, w, dt);
+
+ Eigen::Vector2d pos = state.head<2>();
+ Eigen::Vector2d error = params->goal - pos;
+ double error_theta = (atan2((error[1]), (error[0]))) - state[2];
+ Eigen::Vector2d uk(v, w);
+ Eigen::Vector2d duk(dv, dw);
+
+ // Tracking cost
+ cost += Q(0, 0) * error[0] * error[0] + Q(
+ 1,
+ 1) * error[1] * error[1] + qtheta * error_theta * error_theta;
+ // Effort Cost
+ cost += R(0, 0) * v * v + R(1, 1) * w * w;
+ // Smooth Cost
+ cost += Rd(0, 0) * dv * dv + Rd(1, 1) * dw * dw;
+ }
+
+ return cost;
+
+}
+
+double
+MPCOptimizer::nlopt_cost_callback(
+ const std::vector & x,
+ std::vector & grad,
+ void * data)
+{
+ auto * cbdata = static_cast(data);
+ return cbdata->optimizer->cost_function(x, grad, cbdata->params);
+}
+
+} // namespace easynav
diff --git a/controllers/easynav_mppi_controller/CHANGELOG.rst b/controllers/easynav_mppi_controller/CHANGELOG.rst
index 7c4dda88..dd3b7b6d 100644
--- a/controllers/easynav_mppi_controller/CHANGELOG.rst
+++ b/controllers/easynav_mppi_controller/CHANGELOG.rst
@@ -2,6 +2,27 @@
Changelog for package easynav_mppi_controller
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+0.4.0 (2026-07-26)
+------------------
+* Add missing easynav_sensors deps
+* Update plugins to new sensors API
+* GPLv3 -> Apache 2.0
+* Contributors: Francisco Martín Rico, Francisco Miguel Moreno, Juan S. Cely G., Miguel, migueldm
+
+0.2.1 (2026-02-27)
+------------------
+* 0.2.0
+* GPLv3 -> Apache 2.0
+* Documentation was corrected
+* Remove C++20/C++23 features and update to new MethodBase interface
+* Merge branch 'set_robot_frame' into frames-fix-pr-40
+* TFInfo in RTTFBuffer
+* Refactor to use TFInfo
+* Include package in cmake and package.xml
+* Fix link error when running MPPI in Realease
+* Stop controllers at IDLE
+* Contributors: Francisco Martín Rico, Francisco Miguel Moreno, José Miguel Guerrero, Juan S. Cely G., Miguel, estherag
+
0.0.2 (2025-10-12)
------------------
* Reorganization initial
diff --git a/controllers/easynav_mppi_controller/CMakeLists.txt b/controllers/easynav_mppi_controller/CMakeLists.txt
index d702c4f5..19b6bcdd 100644
--- a/controllers/easynav_mppi_controller/CMakeLists.txt
+++ b/controllers/easynav_mppi_controller/CMakeLists.txt
@@ -5,16 +5,16 @@ if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
-set(CMAKE_CXX_STANDARD 23)
-set(CMAKE_CXX_STANDARD_REQUIRED ON)
-set(CMAKE_CXX_EXTENSIONS OFF)
-
find_package(ament_cmake REQUIRED)
find_package(easynav_common REQUIRED)
find_package(easynav_core REQUIRED)
+find_package(easynav_system REQUIRED)
+find_package(easynav_sensors REQUIRED)
find_package(pluginlib REQUIRED)
find_package(geometry_msgs REQUIRED)
find_package(tf2_ros REQUIRED)
+find_package(tf2 REQUIRED)
+find_package(tf2_geometry_msgs REQUIRED)
find_package(nav_msgs REQUIRED)
find_package(visualization_msgs REQUIRED)
find_package(PCL REQUIRED COMPONENTS common io)
@@ -32,7 +32,11 @@ target_include_directories(${PROJECT_NAME} PUBLIC
target_link_libraries(${PROJECT_NAME} PUBLIC
easynav_common::easynav_common
easynav_core::easynav_core
+ easynav_system::easynav_system
+ easynav_sensors::easynav_sensors
tf2_ros::tf2_ros
+ tf2::tf2
+ tf2_geometry_msgs::tf2_geometry_msgs
pluginlib::pluginlib
${geometry_msgs_TARGETS}
${nav_msgs_TARGETS}
@@ -73,8 +77,11 @@ pluginlib_export_plugin_description_file(easynav_core easynav_mppi_controller_pl
ament_export_dependencies(
easynav_common
easynav_core
+ easynav_sensors
pluginlib
tf2_ros
+ tf2
+ tf2_geometry_msgs
geometry_msgs
nav_msgs
visualization_msgs
diff --git a/controllers/easynav_mppi_controller/README.md b/controllers/easynav_mppi_controller/README.md
index 655e8568..238d8b44 100644
--- a/controllers/easynav_mppi_controller/README.md
+++ b/controllers/easynav_mppi_controller/README.md
@@ -2,22 +2,27 @@
[](#) [](#) [](#)
-
## Description
+
A Model Predictive Path Integral (MPPI) controller implementation for Easy Navigation.
## Authors and Maintainers
+
- **Authors:** Intelligent Robotics Lab
- **Maintainers:** Jose Miguel Guerrero Hernandez
## Supported ROS 2 Distributions
+
| Distribution | Status |
|---|---|
+| humble |  |
+| jazzy |  |
| kilted |  |
| rolling |  |
| jazzy |  |
## Plugin (pluginlib)
+
- **Plugin Name:** `easynav_mppi_controller/MPPIController`
- **Type:** `easynav::MPPIController`
- **Base Class:** `easynav::ControllerMethodBase`
@@ -25,8 +30,12 @@ A Model Predictive Path Integral (MPPI) controller implementation for Easy Navig
- **Description:** A Model Predictive Path Integral (MPPI) controller implementation for Easy Navigation.
## Parameters
+
All parameters are declared under the plugin namespace, i.e., `//easynav_mppi_controller/MPPIController/...`.
+> This plugin derives from [`easynav::ControllerMethodBase`](https://github.com/EasyNavigation/EasyNavigation/tree/rolling/easynav_core#easynavcontrollermethodbase). \
+> See that section for shared collision-checking parameters and debug markers common to all controllers.
+
| Name | Type | Default | Description |
|---|---|---:|---|
| `.num_samples` | `int` | `100` | Number of trajectory rollouts per iteration. |
@@ -40,21 +49,21 @@ All parameters are declared under the plugin namespace, i.e., `//easyn
| `.fov` | `double` | `M_PI/2.0` | Field of view used in trajectory sampling (radians). |
| `.safety_radius` | `double` | `0.6` | Safety radius around the robot (meters). |
-
## Interfaces (Topics and Services)
### Subscriptions and Publications
+
| Direction | Topic | Type | Purpose | QoS |
|---|---|---|---|---|
| Publisher | `/mppi/candidates` | `visualization_msgs/msg/MarkerArray` | MPPI candidate trajectories as markers. | QoS depth=10 |
| Publisher | `/mppi/optimal_path` | `visualization_msgs/msg/MarkerArray` | Optimal MPPI trajectory as markers. | QoS depth=10 |
-
### Services
-This package does not create service servers or clients.
+This package does not create service servers or clients.
## NavState Keys
+
| Key | Type | Access | Notes |
|---|---|---|---|
| `path` | `nav_msgs::msg::Path` | **Read** | Target path to track. |
@@ -62,9 +71,10 @@ This package does not create service servers or clients.
| `points` | `PointPerceptions` | **Read** | Perception point cloud(s) used for costs. |
| `cmd_vel` | `geometry_msgs::msg::TwistStamped` | **Read** | Last commanded velocity (if provided in state). |
-
## TF Frames
+
This controller does not explicitly publish or require TF frames in code.
## License
-GPL-3.0-only
+
+Apache-2.0
diff --git a/controllers/easynav_mppi_controller/include/easynav_mppi_controller/MPPIController.hpp b/controllers/easynav_mppi_controller/include/easynav_mppi_controller/MPPIController.hpp
index ac53bfe2..0f3f03c9 100644
--- a/controllers/easynav_mppi_controller/include/easynav_mppi_controller/MPPIController.hpp
+++ b/controllers/easynav_mppi_controller/include/easynav_mppi_controller/MPPIController.hpp
@@ -1,29 +1,32 @@
// Copyright 2025 Intelligent Robotics Lab
//
// This file is part of the project Easy Navigation (EasyNav in short)
-// licensed under the GNU General Public License v3.0.
-// See for details.
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
#ifndef EASYNAV_MPPI_CONTROLLER__MPPICONTROLLER_HPP_
#define EASYNAV_MPPI_CONTROLLER__MPPICONTROLLER_HPP_
#include
-#include
#include
-#include "geometry_msgs/msg/pose_stamped.hpp"
-#include "geometry_msgs/msg/point.hpp"
#include "geometry_msgs/msg/twist_stamped.hpp"
-#include "nav_msgs/msg/path.hpp"
#include "easynav_core/ControllerMethodBase.hpp"
#include "easynav_common/types/NavState.hpp"
#include "easynav_mppi_controller/MPPIOptimizer.hpp"
-#include "visualization_msgs/msg/marker.hpp"
#include "visualization_msgs/msg/marker_array.hpp"
-#include "sensor_msgs/msg/point_cloud2.hpp"
namespace easynav
{
@@ -38,8 +41,8 @@ class MPPIController : public ControllerMethodBase
~MPPIController() override;
/// \brief Initializes parameters and MPPI controller.
- /// \return std::expected success or error message.
- std::expected on_initialize() override;
+ /// \throws std::runtime_error if initialization fails.
+ void on_initialize() override;
/// \brief Updates the controller using the given NavState.
/// \param nav_state Current navigation state, including odometry and planned path.
diff --git a/controllers/easynav_mppi_controller/include/easynav_mppi_controller/MPPIOptimizer.hpp b/controllers/easynav_mppi_controller/include/easynav_mppi_controller/MPPIOptimizer.hpp
index ba9ba858..d900e166 100644
--- a/controllers/easynav_mppi_controller/include/easynav_mppi_controller/MPPIOptimizer.hpp
+++ b/controllers/easynav_mppi_controller/include/easynav_mppi_controller/MPPIOptimizer.hpp
@@ -1,8 +1,17 @@
// Copyright 2025 Intelligent Robotics Lab
//
// This file is part of the project Easy Navigation (EasyNav in short)
-// licensed under the GNU General Public License v3.0.
-// See for details.
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
// #pragma once
#ifndef EASYNAV_MPPI_CONTROLLER__MPPIOPTIMIZER_HPP_
diff --git a/controllers/easynav_mppi_controller/package.xml b/controllers/easynav_mppi_controller/package.xml
index 7e9ee33a..de1542b9 100644
--- a/controllers/easynav_mppi_controller/package.xml
+++ b/controllers/easynav_mppi_controller/package.xml
@@ -2,17 +2,21 @@
easynav_mppi_controller
- 0.0.2
+ 0.4.0
Easy Navigation: MPPI Controller package.
Jose Miguel Guerrero Hernandez
- GPL-3.0-only
+ Apache-2.0
ament_cmake
easynav_common
easynav_core
+ easynav_system
+ easynav_sensors
pluginlib
tf2_ros
+ tf2
+ tf2_geometry_msgs
geometry_msgs
nav_msgs
visualization_msgs
diff --git a/controllers/easynav_mppi_controller/src/easynav_mppi_controller/MPPIController.cpp b/controllers/easynav_mppi_controller/src/easynav_mppi_controller/MPPIController.cpp
index 42d45c38..9947db91 100644
--- a/controllers/easynav_mppi_controller/src/easynav_mppi_controller/MPPIController.cpp
+++ b/controllers/easynav_mppi_controller/src/easynav_mppi_controller/MPPIController.cpp
@@ -1,31 +1,26 @@
// Copyright 2025 Intelligent Robotics Lab
//
// This file is part of the project Easy Navigation (EasyNav in short)
-// licensed under the GNU General Public License v3.0.
-// See for details.
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
//
-// Easy Navigation program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
+// http://www.apache.org/licenses/LICENSE-2.0
//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
/// \file
/// \brief Implementation of the MPPIController class.
-#include
-#include "tf2/utils.hpp"
-
#include "easynav_mppi_controller/MPPIController.hpp"
-#include "easynav_common/types/Perceptions.hpp"
-#include "easynav_common/types/PointPerception.hpp"
+#include "easynav_sensors/types/PointPerception.hpp"
+#include "easynav_common/RTTFBuffer.hpp"
+
+#include "easynav_system/GoalManager.hpp"
#include "nav_msgs/msg/odometry.hpp"
@@ -36,7 +31,7 @@ MPPIController::MPPIController() {}
MPPIController::~MPPIController() = default;
-std::expected
+void
MPPIController::on_initialize()
{
auto node = get_node();
@@ -64,21 +59,21 @@ MPPIController::on_initialize()
node->get_parameter(plugin_name + ".fov", fov_);
node->get_parameter(plugin_name + ".safety_radius", safety_radius_);
- optimizer_ = std::make_unique(num_samples_, horizon_steps_, dt_, lambda_,
+ optimizer_ = std::make_unique(
+ num_samples_, horizon_steps_, dt_, lambda_,
max_lin_vel_, max_ang_vel_, fov_, safety_radius_);
mppi_candidates_pub_ =
node->create_publisher("/mppi/candidates", 10);
mppi_optimal_pub_ =
node->create_publisher("/mppi/optimal_path", 10);
-
- return {};
}
void MPPIController::publish_mppi_markers(
const std::vector>> & all_trajs,
const std::vector> & best_traj)
{
+ const auto & tf_info = RTTFBuffer::getInstance()->get_tf_info();
visualization_msgs::msg::MarkerArray candidates;
visualization_msgs::msg::MarkerArray optimal;
int id = 0;
@@ -86,7 +81,7 @@ void MPPIController::publish_mppi_markers(
// Candidates in blue
for (const auto & traj : all_trajs) {
visualization_msgs::msg::Marker marker;
- marker.header.frame_id = "map";
+ marker.header.frame_id = tf_info.map_frame;
marker.header.stamp = rclcpp::Clock().now();
marker.ns = "mppi_candidates";
marker.id = id++;
@@ -111,7 +106,7 @@ void MPPIController::publish_mppi_markers(
// Best trajectory in red
visualization_msgs::msg::Marker best_marker;
- best_marker.header.frame_id = "map";
+ best_marker.header.frame_id = tf_info.map_frame;
best_marker.header.stamp = rclcpp::Clock().now();
best_marker.ns = "mppi_optimal_path";
best_marker.id = id++;
@@ -142,11 +137,32 @@ void MPPIController::publish_mppi_markers(
void
MPPIController::update_rt(NavState & nav_state)
{
+ // If navigation is IDLE, force zero velocity
+ if (nav_state.has("navigation_state")) {
+ const auto nav_state_val = nav_state.get("navigation_state");
+ if (nav_state_val == easynav::GoalManager::State::IDLE) {
+ twist_stamped_.header.stamp = get_node()->now();
+ twist_stamped_.twist.linear.x = 0.0;
+ twist_stamped_.twist.angular.z = 0.0;
+ nav_state.set("cmd_vel", twist_stamped_);
+
+ // Also clear visualization markers when idle
+ visualization_msgs::msg::MarkerArray clear_markers;
+ visualization_msgs::msg::Marker delete_all;
+ delete_all.action = visualization_msgs::msg::Marker::DELETEALL;
+ clear_markers.markers.push_back(delete_all);
+
+ mppi_candidates_pub_->publish(clear_markers);
+ mppi_optimal_pub_->publish(clear_markers);
+ return;
+ }
+ }
+
if (!nav_state.has("path") || !nav_state.has("robot_pose")) {
return;
}
- const auto path = nav_state.get("path");
+ const auto & path = nav_state.get("path");
if (path.poses.empty()) {
// If the path is empty, stop the robot and clear markers
@@ -166,20 +182,21 @@ MPPIController::update_rt(NavState & nav_state)
return;
}
- const auto pose = nav_state.get("robot_pose").pose.pose;
- const auto perceptions = nav_state.get("points");
-
+ const auto & pose = nav_state.get("robot_pose").pose.pose;
+ const auto & perceptions = nav_state.get_no_group();
+ const auto & tf_info = RTTFBuffer::getInstance()->get_tf_info();
const auto & filtered = PointPerceptionsOpsView(perceptions)
.filter({-1.0, -1.0, -1.0}, {1.0, 1.0, 1.0})
- .fuse("map")
- ->filter({NAN, NAN, 0.1}, {NAN, NAN, NAN})
+ .fuse(tf_info.map_frame)
+ .filter({NAN, NAN, 0.1}, {NAN, NAN, NAN})
.collapse({NAN, NAN, 0.1})
- ->downsample(0.1)
+ .downsample(0.1)
.as_points();
if (filtered.empty()) {
- RCLCPP_WARN(get_node()->get_logger(),
- "No valid points available for MPPI optimization, using the path only.");
+ RCLCPP_WARN(
+ get_node()->get_logger(),
+ "No valid points available for MPPI optimization, using the path only.");
}
// Compute the control using MPPI with points
diff --git a/controllers/easynav_mppi_controller/src/easynav_mppi_controller/MPPIOptimizer.cpp b/controllers/easynav_mppi_controller/src/easynav_mppi_controller/MPPIOptimizer.cpp
index 8205169f..2b03cca1 100644
--- a/controllers/easynav_mppi_controller/src/easynav_mppi_controller/MPPIOptimizer.cpp
+++ b/controllers/easynav_mppi_controller/src/easynav_mppi_controller/MPPIOptimizer.cpp
@@ -1,13 +1,10 @@
#include "easynav_mppi_controller/MPPIOptimizer.hpp"
#include "tf2/utils.hpp"
+#include "tf2_geometry_msgs/tf2_geometry_msgs.hpp"
#include
#include
#include
-#include
-
-#include "easynav_common/types/Perceptions.hpp"
-#include "easynav_common/types/PointPerception.hpp"
namespace easynav
{
@@ -46,8 +43,9 @@ MPPIOptimizer::simulate_trajectory(
if (has_path) {
// Calculate the index in the path based on the current step
- size_t path_idx = std::min(static_cast((i * path.poses.size()) / horizon_steps),
- path.poses.size() - 1);
+ size_t path_idx = std::min(
+ static_cast((i * path.poses.size()) / horizon_steps),
+ path.poses.size() - 1);
double target_x = path.poses[path_idx].pose.position.x;
double target_y = path.poses[path_idx].pose.position.y;
@@ -103,12 +101,14 @@ double MPPIOptimizer::compute_cost(
double dist = std::hypot(dx, dy);
// Heading error is calculated based on the initial yaw
- double heading_penalty = heading_error(initial_yaw, path.poses[idx].pose.position.x,
- path.poses[idx].pose.position.y, x, y);
+ double heading_penalty = heading_error(
+ initial_yaw, path.poses[idx].pose.position.x,
+ path.poses[idx].pose.position.y, x, y);
// FOV penalty: discourage trajectories outside robot's view
- double angle_to_goal = heading_error(initial_yaw, trajectory.back().first,
- trajectory.back().second, x, y);
+ double angle_to_goal = heading_error(
+ initial_yaw, trajectory.back().first,
+ trajectory.back().second, x, y);
double fov_penalty = std::pow(std::max(0.0, angle_to_goal - fov_), 2);
// Accumulate penalties
@@ -245,8 +245,9 @@ MPPIResult MPPIOptimizer::compute_control(
}
// Softmin: Find minimum cost among samples
- auto best_sample_it = std::min_element(samples.begin(), samples.end(),
- [](const auto & a, const auto & b) {return a.cost < b.cost;});
+ auto best_sample_it = std::min_element(
+ samples.begin(), samples.end(),
+ [](const auto & a, const auto & b) {return a.cost < b.cost;});
// Best trajectory and cost
best_traj = all_trajs[std::distance(samples.begin(), best_sample_it)];
@@ -269,7 +270,7 @@ MPPIResult MPPIOptimizer::compute_control(
denom += sample.cost;
}
- // Weighted average of velocities
+ // Weighted average of velocities
double vlin = 0.0, vrot = 0.0;
for (const auto & sample : samples) {
vlin += sample.v * sample.cost / denom;
diff --git a/controllers/easynav_regulated_pp_controller/CHANGELOG.rst b/controllers/easynav_regulated_pp_controller/CHANGELOG.rst
new file mode 100644
index 00000000..1b79b819
--- /dev/null
+++ b/controllers/easynav_regulated_pp_controller/CHANGELOG.rst
@@ -0,0 +1,20 @@
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Changelog for package easynav_regulated_pp_controller
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+0.4.0 (2026-07-26)
+------------------
+* Add missing easynav_sensors deps
+* Fix overshoot when DWPP is activated
+* Fix license and copyright
+* Port of Regulated Pure Pursuit. Initial commit
+* Contributors: Francisco Martín Rico
+
+0.3.1 (2026-02-27 17:53)
+------------------------
+
+0.2.1 (2026-02-27 14:48)
+------------------------
+
+0.0.2 (2025-10-12)
+------------------
diff --git a/controllers/easynav_regulated_pp_controller/CMakeLists.txt b/controllers/easynav_regulated_pp_controller/CMakeLists.txt
new file mode 100644
index 00000000..8f54c652
--- /dev/null
+++ b/controllers/easynav_regulated_pp_controller/CMakeLists.txt
@@ -0,0 +1,86 @@
+cmake_minimum_required(VERSION 3.20)
+project(easynav_regulated_pp_controller)
+
+if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
+ add_compile_options(-Wall -Wextra -Wpedantic)
+endif()
+
+find_package(ament_cmake REQUIRED)
+find_package(easynav_common REQUIRED)
+find_package(easynav_core REQUIRED)
+find_package(easynav_system REQUIRED)
+find_package(easynav_sensors REQUIRED)
+find_package(pluginlib REQUIRED)
+find_package(geometry_msgs REQUIRED)
+find_package(tf2_ros REQUIRED)
+find_package(tf2 REQUIRED)
+find_package(tf2_geometry_msgs REQUIRED)
+find_package(nav_msgs REQUIRED)
+find_package(std_msgs REQUIRED)
+
+
+add_library(${PROJECT_NAME} SHARED
+ src/easynav_regulated_pp_controller/RegulatedPurePursuitController.cpp
+)
+target_include_directories(${PROJECT_NAME} PUBLIC
+ $
+ $
+)
+target_link_libraries(${PROJECT_NAME} PUBLIC
+ easynav_common::easynav_common
+ easynav_core::easynav_core
+ easynav_system::easynav_system
+ easynav_sensors::easynav_sensors
+ tf2_ros::tf2_ros
+ tf2::tf2
+ tf2_geometry_msgs::tf2_geometry_msgs
+ pluginlib::pluginlib
+ ${geometry_msgs_TARGETS}
+ ${nav_msgs_TARGETS}
+ ${std_msgs_TARGETS}
+)
+
+install(
+ DIRECTORY include/
+ DESTINATION include/${PROJECT_NAME}
+)
+
+install(TARGETS
+ ${PROJECT_NAME}
+ EXPORT export_${PROJECT_NAME}
+ ARCHIVE DESTINATION lib
+ LIBRARY DESTINATION lib
+ RUNTIME DESTINATION lib/${PROJECT_NAME}
+)
+
+if(BUILD_TESTING)
+ find_package(ament_lint_auto REQUIRED)
+ set(ament_cmake_copyright_FOUND TRUE)
+ set(ament_cmake_cpplint_FOUND TRUE)
+ ament_lint_auto_find_test_dependencies()
+
+ find_package(ament_cmake_gtest REQUIRED)
+ add_subdirectory(tests)
+endif()
+
+ament_export_include_directories("include/${PROJECT_NAME}")
+ament_export_libraries(${PROJECT_NAME})
+ament_export_targets(export_${PROJECT_NAME})
+
+# Register the controller plugin
+pluginlib_export_plugin_description_file(easynav_core easynav_regulated_pp_controller_plugins.xml)
+
+ament_export_dependencies(
+ easynav_common
+ easynav_core
+ easynav_system
+ easynav_sensors
+ pluginlib
+ tf2_ros
+ tf2
+ tf2_geometry_msgs
+ geometry_msgs
+ nav_msgs
+ std_msgs
+)
+ament_package()
diff --git a/controllers/easynav_regulated_pp_controller/README.md b/controllers/easynav_regulated_pp_controller/README.md
new file mode 100644
index 00000000..03be5d46
--- /dev/null
+++ b/controllers/easynav_regulated_pp_controller/README.md
@@ -0,0 +1,170 @@
+# easynav_regulated_pp_controller
+
+Port of the [Nav2 Regulated Pure Pursuit Controller](https://docs.nav2.org/configuration/packages/configuring-regulated-pp.html)
+to the EasyNav plugin architecture (`easynav::ControllerMethodBase`), including its optional
+Dynamic Window Pure Pursuit (DWPP) extension.
+
+- S. Macenski, S. Singh, F. Martin, J. Gines, [**Regulated Pure Pursuit for Robot Path Tracking**](https://arxiv.org/abs/2305.20026). Autonomous Robots, 2023.
+- Fumiya Ohnishi and Masaki Takahashi, **DWPP: Dynamic Window Pure Pursuit Considering Velocity and Acceleration Constraints**, arXiv:2601.15006, 2026.
+
+```bibtex
+@article{macenski2023regulated,
+ title={Regulated Pure Pursuit for Robot Path Tracking},
+ author={Steve Macenski and Shrijit Singh and Francisco Martin and Jonatan Gines},
+ year={2023},
+ journal = {Autonomous Robots}
+}
+```
+
+## Algorithm
+
+The controller finds a lookahead ("carrot") point on the path at a given distance from the
+robot and drives towards it, following the classic Pure Pursuit geometry: the curvature needed
+to reach the carrot point is `k = 2y / (x^2+y^2)` in the robot's local frame. On top of that, it
+adds the *regulation* terms from the paper above:
+
+- **Velocity-scaled lookahead distance** (Adaptive Pure Pursuit): the lookahead distance grows
+ with speed, clamped between `min_lookahead_dist` and `max_lookahead_dist`.
+- **Curvature regulation**: linear velocity is reduced on sharp turns (small turning radius).
+- **Obstacle-proximity regulation**: linear velocity is reduced close to obstacles.
+- **Approach-to-goal regulation**: linear velocity is reduced smoothly as the robot nears the
+ end of the path.
+- **Rotate to rough heading / rotate to goal heading**: the robot rotates in place first if the
+ carrot (or the final goal) is at a large angle from the current heading.
+- **Dynamic Window Pure Pursuit (optional)**: instead of directly applying `angular_vel = linear_vel * curvature`,
+ it searches the reachable (velocity, angular velocity) space under the configured
+ acceleration/deceleration limits for the point that best matches the desired curvature.
+
+## Adaptations from the Nav2 version
+
+EasyNav's design differs from Nav2's `controller_server` in ways that require some adaptations:
+
+- **No costmap.** EasyNav controllers only have access to the robot pose, the path, and fused
+ point-cloud perceptions (`PointPerception`) via the `NavState` blackboard — there is no
+ costmap or inflation layer to query. The cost-based linear velocity regulation term
+ (`use_cost_regulated_linear_velocity_scaling` in Nav2) is therefore replaced by an equivalent
+ term, `use_obstacle_regulated_linear_velocity_scaling`, driven by the distance to the nearest
+ point in the fused perception cloud within the lookahead corridor, rather than by inflated
+ costmap cost (see `heuristics::obstacleConstraint` in
+ [`regulation_functions.hpp`](include/easynav_regulated_pp_controller/regulation_functions.hpp)).
+- **No per-controller collision-arc checking.** Nav2's RPP throws a `NoValidControl` exception
+ when it predicts a collision along a forward-simulated arc. EasyNav already performs this kind
+ of safety stop uniformly for every controller in `ControllerMethodBase` (see the shared
+ `colision_checker.*` parameters), so this controller does not duplicate it.
+- **No separate goal-checker plugin.** Nav2 relies on an independent `GoalChecker` plugin to
+ decide when the robot has reached the goal. This controller checks the distance/angle to the
+ last path pose directly, using `goal_tolerance.position` / `goal_tolerance.yaw` from the
+ `NavState` if `GoalManager` has published them, or the `xy_goal_tolerance` /
+ `yaw_goal_tolerance` parameters otherwise — consistent with how other EasyNav controllers
+ (e.g. `easynav_simple_controller`, `easynav_mpc_controller`) handle it.
+- **No TF-based path transform.** EasyNav controllers assume `path` and `robot_pose` are already
+ expressed in the same frame (see `easynav_simple_controller`, `easynav_mpc_controller`), so
+ there is no `transformPathInTargetFrame` step. The path-to-robot-frame conversion needed by the
+ Pure Pursuit geometry (curvature is only meaningful with the robot at the local origin facing
+ +x) is done analytically from the robot pose instead of via `tf2`.
+- **No plan pruning upstream.** Nav2's `controller_server` prunes the global plan to a local
+ window starting at (or near) the robot before handing it to the controller. EasyNav's `path` in
+ `NavState` is the full, un-pruned plan, so the carrot search (`getLookAheadPoint`) explicitly
+ starts from the path pose closest to the robot (`findClosestPoseIndex`) rather than from index 0
+ of the array. Skipping this step is a real trap, not just a style choice: once the robot has
+ travelled more than a lookahead distance away from the path's start pose, that start pose itself
+ would satisfy "farther than the lookahead distance", so a naive search from index 0 keeps
+ returning it as the carrot for the rest of the path — freezing the carrot behind the robot,
+ producing bogus large curvature/angle values, and making the controller rotate-in-place far more
+ than it should.
+- **No `setSpeedLimit` API.** EasyNav has no equivalent to Nav2's dynamic speed-limiting
+ interface, so it is not ported.
+
+## Parameters
+
+| Parameter | Description |
+|-----|----|
+| `max_linear_vel` | Maximum linear velocity. |
+| `min_linear_vel` | Minimum linear velocity, used when `use_dynamic_window` is `true`. |
+| `max_angular_vel` / `min_angular_vel` | Angular velocity bounds, used when `use_dynamic_window` is `true`. |
+| `max_linear_accel` / `max_linear_decel` | Linear acceleration/deceleration bounds, used when `use_dynamic_window` is `true`. |
+| `max_angular_accel` / `max_angular_decel` | Angular acceleration/deceleration bounds; also used by `rotate_to_heading`. |
+| `lookahead_dist` | Fixed lookahead distance to find the carrot point. |
+| `min_lookahead_dist` / `max_lookahead_dist` | Bounds for the velocity-scaled lookahead distance. |
+| `lookahead_time` | Gain used to scale the lookahead distance by the current speed. |
+| `use_velocity_scaled_lookahead_dist` | Use velocity-scaled lookahead distance instead of the fixed `lookahead_dist`. |
+| `rotate_to_heading_angular_vel` | Angular velocity used while rotating in place. |
+| `use_rotate_to_heading` | Enable rotate-in-place behaviors (rough path heading and final goal heading). |
+| `rotate_to_heading_min_angle` | Angle to the carrot beyond which the robot rotates in place first. |
+| `use_regulated_linear_velocity_scaling` | Enable curvature-based velocity regulation. |
+| `regulated_linear_scaling_min_radius` | Turning radius below which curvature regulation kicks in. |
+| `regulated_linear_scaling_min_speed` | Minimum velocity kept under regulation. |
+| `use_fixed_curvature_lookahead` | Use a separate, fixed lookahead distance to compute the regulation curvature. |
+| `curvature_lookahead_dist` | Distance of the fixed curvature lookahead, if enabled. |
+| `interpolate_curvature_after_goal` | Extrapolate the curvature carrot past the goal to avoid end-of-path oscillation. |
+| `use_obstacle_regulated_linear_velocity_scaling` | Enable obstacle-proximity velocity regulation (EasyNav adaptation of Nav2's cost-based term). |
+| `obstacle_scaling_dist` | Distance below which obstacle regulation is triggered. |
+| `obstacle_scaling_gain` | Gain (`<= 1.0`) applied when scaling down the velocity near obstacles. |
+| `min_approach_linear_velocity` | Minimum velocity while approaching the goal. |
+| `approach_velocity_scaling_dist` | Remaining path distance at which approach-to-goal slow-down starts. |
+| `allow_reversing` | Allow driving backwards when the carrot point is behind the robot. |
+| `use_dynamic_window` | Enable the Dynamic Window Pure Pursuit (DWPP) extension. |
+| `xy_goal_tolerance` / `yaw_goal_tolerance` | Fallback goal tolerances used if `GoalManager` has not published `goal_tolerance.*` in the `NavState`. |
+
+## Topics
+
+| Topic | Type | Description |
+|-----|----|----|
+| `lookahead_point` | `geometry_msgs/PointStamped` | The current carrot point on the path. |
+| `curvature_lookahead_point` | `geometry_msgs/PointStamped` | The fixed curvature lookahead point, when `use_fixed_curvature_lookahead` is enabled. |
+| `is_rotating_to_heading` | `std_msgs/Bool` | Whether the controller is currently rotating in place. |
+
+## Example configuration
+
+```yaml
+controller:
+ ros__parameters:
+ controller_plugin: "easynav_regulated_pp_controller/RegulatedPurePursuitController"
+ RegulatedPurePursuitController:
+ max_linear_vel: 0.5
+ min_linear_vel: -0.5
+ max_angular_vel: 2.5
+ min_angular_vel: -2.5
+ max_linear_accel: 2.5
+ max_linear_decel: 2.5
+ max_angular_accel: 3.2
+ max_angular_decel: 3.2
+ lookahead_dist: 0.6
+ min_lookahead_dist: 0.3
+ max_lookahead_dist: 0.9
+ lookahead_time: 1.5
+ use_velocity_scaled_lookahead_dist: false
+ rotate_to_heading_angular_vel: 1.8
+ use_rotate_to_heading: true
+ rotate_to_heading_min_angle: 0.785
+ use_regulated_linear_velocity_scaling: true
+ regulated_linear_scaling_min_radius: 0.9
+ regulated_linear_scaling_min_speed: 0.25
+ use_fixed_curvature_lookahead: false
+ curvature_lookahead_dist: 1.0
+ interpolate_curvature_after_goal: false
+ use_obstacle_regulated_linear_velocity_scaling: false
+ obstacle_scaling_dist: 0.3
+ obstacle_scaling_gain: 1.0
+ min_approach_linear_velocity: 0.05
+ approach_velocity_scaling_dist: 1.0
+ allow_reversing: false
+ use_dynamic_window: false
+ xy_goal_tolerance: 0.25
+ yaw_goal_tolerance: 0.25
+```
+
+## Tuning notes
+
+Same guidance as upstream Nav2 applies:
+
+- For Adaptive Pure Pursuit behavior, disable all boolean regulation parameters except
+ `use_velocity_scaled_lookahead_dist`, and tune `lookahead_time`, `min_lookahead_dist` and
+ `max_lookahead_dist`.
+- For plain Pure Pursuit behavior, disable all boolean parameters and set
+ `approach_velocity_scaling_dist: 0.0` to disable approach-to-goal slow-down, then tune
+ `lookahead_dist`.
+- `use_obstacle_regulated_linear_velocity_scaling` can over-trigger in tightly cluttered
+ environments (every point in a narrow corridor is "close"); tune `obstacle_scaling_dist` and
+ `obstacle_scaling_gain` for the platform's environment, the same way `cost_scaling_dist` /
+ `cost_scaling_gain` are tuned in Nav2.
diff --git a/controllers/easynav_regulated_pp_controller/easynav_regulated_pp_controller_plugins.xml b/controllers/easynav_regulated_pp_controller/easynav_regulated_pp_controller_plugins.xml
new file mode 100644
index 00000000..c5c47a8c
--- /dev/null
+++ b/controllers/easynav_regulated_pp_controller/easynav_regulated_pp_controller_plugins.xml
@@ -0,0 +1,9 @@
+
+
+
+
+ EasyNav port of the Nav2 Regulated Pure Pursuit Controller, with optional Dynamic Window Pure Pursuit (DWPP) extension.
+
+
+
+
diff --git a/controllers/easynav_regulated_pp_controller/include/easynav_regulated_pp_controller/RegulatedPurePursuitController.hpp b/controllers/easynav_regulated_pp_controller/include/easynav_regulated_pp_controller/RegulatedPurePursuitController.hpp
new file mode 100644
index 00000000..607e2075
--- /dev/null
+++ b/controllers/easynav_regulated_pp_controller/include/easynav_regulated_pp_controller/RegulatedPurePursuitController.hpp
@@ -0,0 +1,201 @@
+// Copyright (c) 2020 Shrijit Singh
+// Copyright (c) 2020 Samsung Research America
+// Copyright 2026 Intelligent Robotics Lab
+//
+// This file is part of the project Easy Navigation (EasyNav in short)
+// and is a derivative work of nav2_regulated_pure_pursuit_controller,
+// ported to the EasyNav plugin architecture.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef EASYNAV_REGULATED_PP_CONTROLLER__REGULATEDPUREPURSUITCONTROLLER_HPP_
+#define EASYNAV_REGULATED_PP_CONTROLLER__REGULATEDPUREPURSUITCONTROLLER_HPP_
+
+#include
+
+#include "geometry_msgs/msg/point.hpp"
+#include "geometry_msgs/msg/point_stamped.hpp"
+#include "geometry_msgs/msg/pose.hpp"
+#include "geometry_msgs/msg/twist.hpp"
+#include "geometry_msgs/msg/twist_stamped.hpp"
+#include "nav_msgs/msg/odometry.hpp"
+#include "nav_msgs/msg/path.hpp"
+#include "std_msgs/msg/bool.hpp"
+#include "std_msgs/msg/header.hpp"
+
+#include "easynav_core/ControllerMethodBase.hpp"
+#include "easynav_common/types/NavState.hpp"
+#include "easynav_sensors/types/PointPerception.hpp"
+
+namespace easynav
+{
+
+/// \brief Port of nav2_regulated_pure_pursuit_controller to EasyNav.
+///
+/// Implements the Regulated Pure Pursuit algorithm described in:
+/// S. Macenski, S. Singh, F. Martin, J. Gines, "Regulated Pure Pursuit for Robot Path Tracking",
+/// Autonomous Robots, 2023 (https://arxiv.org/abs/2305.20026), plus the optional Dynamic Window
+/// Pure Pursuit (DWPP) extension. See the package README for the adaptations made to fit
+/// EasyNav's design (no costmap, no separate goal-checker/TF-transform plugins).
+class RegulatedPurePursuitController : public ControllerMethodBase
+{
+public:
+ RegulatedPurePursuitController();
+
+ /// \brief Destructor.
+ ~RegulatedPurePursuitController() override;
+
+ /// \brief Declares and reads parameters.
+ /// \throws std::runtime_error on initialization failure.
+ void on_initialize() override;
+
+ /// \brief Computes the next velocity command from the current NavState.
+ /// \param nav_state Current navigation state, including odometry and planned path.
+ void update_rt(NavState & nav_state) override;
+
+protected:
+ // --- Lookahead ---
+ double lookahead_dist_{0.6}; ///< Fixed lookahead distance (m).
+ double min_lookahead_dist_{0.3}; ///< Minimum velocity-scaled lookahead distance (m).
+ double max_lookahead_dist_{0.9}; ///< Maximum velocity-scaled lookahead distance (m).
+ double lookahead_time_{1.5}; ///< Lookahead gain: seconds projected at current speed.
+ bool use_velocity_scaled_lookahead_dist_{false}; ///< Use velocity-scaled lookahead distance.
+
+ // --- Velocity / acceleration limits ---
+ double max_linear_vel_{0.5}; ///< Maximum linear velocity (m/s).
+ double min_linear_vel_{-0.5}; ///< Minimum linear velocity, used when use_dynamic_window is true (m/s).
+ double max_angular_vel_{2.5}; ///< Maximum angular velocity (rad/s).
+ double min_angular_vel_{-2.5}; ///< Minimum angular velocity, used when use_dynamic_window is true (rad/s).
+ double max_linear_accel_{2.5}; ///< Maximum linear acceleration (m/s^2).
+ double max_linear_decel_{2.5}; ///< Maximum linear deceleration (m/s^2).
+ double max_angular_accel_{3.2}; ///< Maximum angular acceleration (rad/s^2).
+ double max_angular_decel_{3.2}; ///< Maximum angular deceleration (rad/s^2).
+ bool use_dynamic_window_{false}; ///< Use the Dynamic Window Pure Pursuit (DWPP) extension.
+ bool allow_reversing_{false}; ///< Allow driving backwards when the carrot is behind the robot.
+
+ // --- Rotate to heading ---
+ bool use_rotate_to_heading_{true}; ///< Enable rotate-in-place behaviors.
+ double rotate_to_heading_angular_vel_{1.8}; ///< Angular velocity used while rotating in place.
+ double rotate_to_heading_min_angle_{0.785}; ///< Angle to path beyond which to rotate in place.
+
+ // --- Curvature regulation ---
+ bool use_regulated_linear_velocity_scaling_{true}; ///< Enable curvature-based regulation.
+ double regulated_linear_scaling_min_radius_{0.9}; ///< Turning radius that triggers regulation.
+ double regulated_linear_scaling_min_speed_{0.25}; ///< Minimum speed kept under regulation.
+ bool use_fixed_curvature_lookahead_{false}; ///< Use a separate lookahead for curvature.
+ double curvature_lookahead_dist_{1.0}; ///< Distance of the fixed curvature lookahead.
+ bool interpolate_curvature_after_goal_{false}; ///< Extrapolate curvature carrot past the goal.
+
+ // --- Obstacle-proximity regulation (adapts the costmap-based term to EasyNav's perception) ---
+ bool use_obstacle_regulated_linear_velocity_scaling_{false}; ///< Enable obstacle-proximity regulation.
+ double obstacle_scaling_dist_{0.3}; ///< Distance below which obstacle regulation is triggered (m).
+ double obstacle_scaling_gain_{1.0}; ///< Gain (<=1.0) applied when scaling down the velocity.
+
+ // --- Approach to goal ---
+ double min_approach_linear_velocity_{0.05}; ///< Minimum linear velocity while approaching goal.
+ double approach_velocity_scaling_dist_{1.0}; ///< Remaining-path distance at which to start slowing.
+
+ // --- Goal tolerances (fallback values if "goal_tolerance.*" is not present in NavState) ---
+ double xy_goal_tolerance_{0.25}; ///< Fallback positional tolerance to the goal (m).
+ double yaw_goal_tolerance_{0.25}; ///< Fallback angular tolerance to the goal (rad).
+
+ rclcpp::Publisher::SharedPtr lookahead_point_pub_;
+ rclcpp::Publisher::SharedPtr curvature_lookahead_point_pub_;
+ rclcpp::Publisher::SharedPtr is_rotating_to_heading_pub_;
+
+ rclcpp::Time last_update_ts_; ///< Timestamp of the last control update.
+ double last_linear_vel_{0.0}; ///< Last commanded linear velocity (open-loop feedback).
+ double last_angular_vel_{0.0}; ///< Last commanded angular velocity (open-loop feedback).
+ bool is_rotating_to_heading_{false}; ///< Whether the controller is currently rotating in place.
+ geometry_msgs::msg::TwistStamped cmd_vel_; ///< Current velocity command.
+
+ /// \brief Publishes a zero-velocity command and resets the open-loop velocity feedback.
+ void stop(NavState & nav_state, const std_msgs::msg::Header & header);
+
+ /// \brief Computes the lookahead distance to search for the carrot pose.
+ /// \param linear_vel Linear velocity used to scale the lookahead distance, if enabled.
+ double getLookAheadDistance(double linear_vel) const;
+
+ /// \brief Index of the path pose closest to \p robot_position.
+ ///
+ /// Since EasyNav controllers receive the full, un-pruned global path (see the README), the
+ /// search for the carrot pose must start from this index instead of from the beginning of the
+ /// path: otherwise, once the robot has advanced more than a lookahead distance away from the
+ /// path's start pose, that start pose itself would (incorrectly) be picked up as "farther than
+ /// the lookahead distance" and returned as the carrot, even though it lies behind the robot.
+ static std::size_t findClosestPoseIndex(
+ const nav_msgs::msg::Path & path,
+ const geometry_msgs::msg::Point & robot_position);
+
+ /// \brief Finds the pose on the path at (approximately) \p lookahead_dist from \p robot_position.
+ ///
+ /// Walks the path (in the same global frame as the robot pose), starting from the pose closest
+ /// to the robot (see \ref findClosestPoseIndex), looking for the first pose from there onwards
+ /// that is farther than \p lookahead_dist from the robot, then interpolates the exact carrot
+ /// position on the segment leading to it via a circle/segment intersection. If the remainder of
+ /// the path is closer than \p lookahead_dist, the path's last pose is returned (extrapolated one
+ /// extra segment if \p interpolate_after_end is true and the path has at least two poses).
+ static geometry_msgs::msg::Point getLookAheadPoint(
+ const nav_msgs::msg::Path & path,
+ const geometry_msgs::msg::Point & robot_position,
+ double lookahead_dist,
+ bool interpolate_after_end = false);
+
+ /// \brief Intersection of the segment [p1, p2] with the circle of radius \p r centered at \p center.
+ /// \return The intersection point closer to \p p2, or \p p2 itself if there is no real intersection.
+ static geometry_msgs::msg::Point circleSegmentIntersection(
+ const geometry_msgs::msg::Point & p1,
+ const geometry_msgs::msg::Point & p2,
+ double r,
+ const geometry_msgs::msg::Point & center);
+
+ /// \brief Expresses a global-frame point in the robot's local frame (robot at origin, facing +x).
+ static geometry_msgs::msg::Point toRobotFrame(
+ const geometry_msgs::msg::Point & point,
+ const geometry_msgs::msg::Pose & robot_pose,
+ double robot_yaw);
+
+ /// \brief Whether the robot should rotate in place towards \p angle_to_path.
+ bool shouldRotateToPath(double angle_to_path) const;
+
+ /// \brief Computes a kinematically-feasible rotate-in-place command towards \p angle_to_target.
+ void rotateToHeading(
+ double & linear_vel, double & angular_vel, double angle_to_target, double dt) const;
+
+ /// \brief Applies curvature and obstacle-proximity regulation, then approach-to-goal scaling.
+ void applyConstraints(
+ double curvature,
+ double min_obstacle_distance,
+ double remaining_path_distance,
+ double euclidean_dist_to_goal,
+ double & linear_vel) const;
+
+ /// \brief Distance (m) from the robot to the nearest fused perception point in front of it,
+ /// within \p max_range. Returns +infinity if there is no perception data or no nearby point.
+ double computeMinObstacleDistance(NavState & nav_state, double max_range) const;
+
+ /// \brief Integrated path length from the robot position to the end of \p path.
+ static double remainingPathDistance(
+ const nav_msgs::msg::Path & path,
+ const geometry_msgs::msg::Point & robot_position);
+
+ /// \brief Publishes a PointStamped marker for a carrot pose, if the publisher has subscribers.
+ void publishCarrot(
+ const rclcpp::Publisher::SharedPtr & pub,
+ const geometry_msgs::msg::Point & carrot,
+ const std_msgs::msg::Header & header) const;
+};
+
+} // namespace easynav
+
+#endif // EASYNAV_REGULATED_PP_CONTROLLER__REGULATEDPUREPURSUITCONTROLLER_HPP_
diff --git a/controllers/easynav_regulated_pp_controller/include/easynav_regulated_pp_controller/dynamic_window_pure_pursuit_functions.hpp b/controllers/easynav_regulated_pp_controller/include/easynav_regulated_pp_controller/dynamic_window_pure_pursuit_functions.hpp
new file mode 100644
index 00000000..3579ac33
--- /dev/null
+++ b/controllers/easynav_regulated_pp_controller/include/easynav_regulated_pp_controller/dynamic_window_pure_pursuit_functions.hpp
@@ -0,0 +1,328 @@
+// Copyright (c) 2025 Fumiya Ohnishi
+// Copyright 2026 Intelligent Robotics Lab
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/// \file
+/// \brief Dynamic Window Pure Pursuit (DWPP) helper functions, ported from
+/// nav2_regulated_pure_pursuit_controller. These are plain math functions over velocities and
+/// accelerations, so they carry over to EasyNav unchanged.
+///
+/// Fumiya Ohnishi and Masaki Takahashi, "DWPP: Dynamic Window Pure Pursuit Considering Velocity
+/// and Acceleration Constraints", arXiv:2601.15006, 2026.
+
+#ifndef EASYNAV_REGULATED_PP_CONTROLLER__DYNAMIC_WINDOW_PURE_PURSUIT_FUNCTIONS_HPP_
+#define EASYNAV_REGULATED_PP_CONTROLLER__DYNAMIC_WINDOW_PURE_PURSUIT_FUNCTIONS_HPP_
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "geometry_msgs/msg/twist.hpp"
+
+namespace easynav
+{
+
+namespace dynamic_window_pure_pursuit
+{
+
+struct DynamicWindowBounds
+{
+ double max_linear_vel;
+ double min_linear_vel;
+ double max_angular_vel;
+ double min_angular_vel;
+};
+
+/**
+ * @brief Compute the dynamic window (feasible velocity bounds) based on
+ * the current speed and the given velocity and acceleration constraints.
+ * @param current_speed Current linear and angular velocity of the robot
+ * @param max_linear_vel Maximum allowable linear velocity
+ * @param min_linear_vel Minimum allowable linear velocity
+ * @param max_angular_vel Maximum allowable angular velocity
+ * @param min_angular_vel Minimum allowable angular velocity
+ * @param max_linear_accel Maximum allowable linear acceleration
+ * @param max_linear_decel Maximum allowable linear deceleration
+ * @param max_angular_accel Maximum allowable angular acceleration
+ * @param max_angular_decel Maximum allowable angular deceleration
+ * @param dt Control duration
+ * @return Computed dynamic window's velocity bounds
+ */
+inline DynamicWindowBounds computeDynamicWindow(
+ const geometry_msgs::msg::Twist & current_speed,
+ const double & max_linear_vel,
+ const double & min_linear_vel,
+ const double & max_angular_vel,
+ const double & min_angular_vel,
+ const double & max_linear_accel,
+ const double & max_linear_decel,
+ const double & max_angular_accel,
+ const double & max_angular_decel,
+ const double & dt
+)
+{
+ DynamicWindowBounds dynamic_window;
+ constexpr double Eps = 1e-3;
+
+ // function to compute dynamic window for a single dimension
+ auto compute_window = [&](const double & current_vel, const double & max_vel,
+ const double & min_vel, const double & max_accel, const double & max_decel)
+ {
+ double candidate_max_vel = 0.0;
+ double candidate_min_vel = 0.0;
+
+ if (current_vel > Eps) {
+ // if the current velocity is positive, acceleration means an increase in speed
+ candidate_max_vel = current_vel + max_accel * dt;
+ candidate_min_vel = current_vel - max_decel * dt;
+ } else if (current_vel < -Eps) {
+ // if the current velocity is negative, acceleration means a decrease in speed
+ candidate_max_vel = current_vel + max_decel * dt;
+ candidate_min_vel = current_vel - max_accel * dt;
+ } else {
+ // if the current velocity is zero, allow acceleration in both directions.
+ candidate_max_vel = current_vel + max_accel * dt;
+ candidate_min_vel = current_vel - max_accel * dt;
+ }
+
+ // clip to max/min velocity limits
+ double dynamic_window_max_vel = std::min(candidate_max_vel, max_vel);
+ double dynamic_window_min_vel = std::max(candidate_min_vel, min_vel);
+ return std::make_tuple(dynamic_window_max_vel, dynamic_window_min_vel);
+ };
+
+ // linear velocity
+ std::tie(dynamic_window.max_linear_vel, dynamic_window.min_linear_vel) =
+ compute_window(
+ current_speed.linear.x, max_linear_vel, min_linear_vel,
+ max_linear_accel, max_linear_decel);
+
+ // angular velocity
+ std::tie(dynamic_window.max_angular_vel, dynamic_window.min_angular_vel) =
+ compute_window(
+ current_speed.angular.z, max_angular_vel, min_angular_vel,
+ max_angular_accel, max_angular_decel);
+
+ return dynamic_window;
+}
+
+/**
+ * @brief Apply regulated linear velocity to the dynamic window
+ * @param regulated_linear_vel Regulated linear velocity
+ * @param dynamic_window Dynamic window to be regulated
+ */
+inline void applyRegulationToDynamicWindow(
+ const double & regulated_linear_vel,
+ DynamicWindowBounds & dynamic_window)
+{
+ // Create regulated bounds [0, v_reg] or [v_reg, 0]
+ double v_reg_min = std::min(0.0, regulated_linear_vel);
+ double v_reg_max = std::max(0.0, regulated_linear_vel);
+
+ // Intersect the dynamic window with the regulated bounds
+ dynamic_window.min_linear_vel = std::max(dynamic_window.min_linear_vel, v_reg_min);
+ dynamic_window.max_linear_vel = std::min(dynamic_window.max_linear_vel, v_reg_max);
+
+ // If min > max, collapse to the nearest boundary
+ if (dynamic_window.min_linear_vel > dynamic_window.max_linear_vel) {
+ if (dynamic_window.min_linear_vel > v_reg_max) {
+ dynamic_window.max_linear_vel = dynamic_window.min_linear_vel;
+ } else {
+ dynamic_window.min_linear_vel = dynamic_window.max_linear_vel;
+ }
+ }
+}
+
+/**
+ * @brief Compute the optimal velocity to follow the path within the dynamic window
+ * @param dynamic_window Dynamic window defining feasible velocity bounds
+ * @param curvature Curvature of the path to follow
+ * @param sign Velocity sign (forward or backward)
+ * @return Optimal linear and angular velocity
+ */
+inline std::tuple computeOptimalVelocityWithinDynamicWindow(
+ const DynamicWindowBounds & dynamic_window,
+ const double & curvature,
+ const double & sign
+)
+{
+ double optimal_linear_vel;
+ double optimal_angular_vel;
+
+ // consider linear_vel - angular_vel space (horizontal and vertical axes respectively)
+ // Select the closest point to the line
+ // angular_vel = curvature * linear_vel within the dynamic window.
+ // If multiple points are equally close, select the one with the largest linear_vel.
+
+ // When curvature == 0, the line is angular_vel = 0
+ if (std::abs(curvature) < 1e-3) {
+ // linear velocity
+ if (sign >= 0.0) {
+ // If moving forward, select the max linear vel
+ optimal_linear_vel = dynamic_window.max_linear_vel;
+ } else {
+ // If moving backward, select the min linear vel
+ optimal_linear_vel = dynamic_window.min_linear_vel;
+ }
+
+ // angular velocity
+ // If the line angular_vel = 0 intersects the dynamic window,angular_vel = 0.0
+ if (dynamic_window.min_angular_vel <= 0.0 && 0.0 <= dynamic_window.max_angular_vel) {
+ optimal_angular_vel = 0.0;
+ } else {
+ // If not, select angular vel within dynamic window closest to 0
+ if (std::abs(dynamic_window.min_angular_vel) <= std::abs(dynamic_window.max_angular_vel)) {
+ optimal_angular_vel = dynamic_window.min_angular_vel;
+ } else {
+ optimal_angular_vel = dynamic_window.max_angular_vel;
+ }
+ }
+ return std::make_tuple(optimal_linear_vel, optimal_angular_vel);
+ }
+
+ // When the dynamic window and the line angular_vel = curvature * linear_vel intersect,
+ // select the intersection point that yields the highest linear velocity.
+
+ // List the four candidate intersection points
+ std::pair candidates[] = {
+ {dynamic_window.min_linear_vel, curvature * dynamic_window.min_linear_vel},
+ {dynamic_window.max_linear_vel, curvature * dynamic_window.max_linear_vel},
+ {dynamic_window.min_angular_vel / curvature, dynamic_window.min_angular_vel},
+ {dynamic_window.max_angular_vel / curvature, dynamic_window.max_angular_vel}
+ };
+
+ double best_linear_vel = -std::numeric_limits::max() * sign;
+ double best_angular_vel = 0.0;
+
+ for (auto [linear_vel, angular_vel] : candidates) {
+ // Check whether the candidate lies within the dynamic window
+ if (linear_vel >= dynamic_window.min_linear_vel &&
+ linear_vel <= dynamic_window.max_linear_vel &&
+ angular_vel >= dynamic_window.min_angular_vel &&
+ angular_vel <= dynamic_window.max_angular_vel)
+ {
+ // Select the candidate with the largest linear velocity (considering moving direction)
+ if (linear_vel * sign > best_linear_vel * sign) {
+ best_linear_vel = linear_vel;
+ best_angular_vel = angular_vel;
+ }
+ }
+ }
+
+ // If best_linear_vel was updated, it means that a valid intersection exists
+ if (best_linear_vel != -std::numeric_limits::max() * sign) {
+ optimal_linear_vel = best_linear_vel;
+ optimal_angular_vel = best_angular_vel;
+ return std::make_tuple(optimal_linear_vel, optimal_angular_vel);
+ }
+
+ // When the dynamic window and the line angular_vel = curvature * linear_vel have no intersection,
+ // select the point within the dynamic window that is closest to the line.
+
+ // Because the dynamic window is a convex region,
+ // the closest point must be one of its four corners.
+ const std::array, 4> corners = {
+ {
+ {dynamic_window.min_linear_vel, dynamic_window.min_angular_vel},
+ {dynamic_window.min_linear_vel, dynamic_window.max_angular_vel},
+ {dynamic_window.max_linear_vel, dynamic_window.min_angular_vel},
+ {dynamic_window.max_linear_vel, dynamic_window.max_angular_vel}
+ }};
+
+ // Compute the distance from a point (linear_vel, angular_vel)
+ // to the line angular_vel = curvature * linear_vel
+ const double denom = std::sqrt(curvature * curvature + 1.0);
+ auto compute_dist = [&](const std::array & corner) -> double {
+ return std::abs(curvature * corner[0] - corner[1]) / denom;
+ };
+
+ double closest_dist = std::numeric_limits::max();
+ best_linear_vel = -std::numeric_limits::max() * sign;
+ best_angular_vel = 0.0;
+
+ for (const auto & corner : corners) {
+ const double dist = compute_dist(corner);
+ // Update if this corner is closer to the line,
+ // or equally close but has a larger linear velocity (considering moving direction)
+ if (dist < closest_dist ||
+ (std::abs(dist - closest_dist) <= 1e-3 && corner[0] * sign > best_linear_vel * sign))
+ {
+ closest_dist = dist;
+ best_linear_vel = corner[0];
+ best_angular_vel = corner[1];
+ }
+ }
+
+ optimal_linear_vel = best_linear_vel;
+ optimal_angular_vel = best_angular_vel;
+
+ return std::make_tuple(optimal_linear_vel, optimal_angular_vel);
+}
+
+/**
+ * @brief Compute velocity commands using Dynamic Window Pure Pursuit method
+ * @param current_speed Current linear and angular velocity of the robot
+ * @param max_linear_vel Maximum allowable linear velocity
+ * @param min_linear_vel Minimum allowable linear velocity
+ * @param max_angular_vel Maximum allowable angular velocity
+ * @param min_angular_vel Minimum allowable angular velocity
+ * @param max_linear_accel Maximum allowable linear acceleration
+ * @param max_linear_decel Maximum allowable linear deceleration
+ * @param max_angular_accel Maximum allowable angular acceleration
+ * @param max_angular_decel Maximum allowable angular deceleration
+ * @param regulated_linear_vel Regulated linear velocity
+ * @param curvature Curvature of the path to follow
+ * @param sign Velocity sign (forward or backward)
+ * @param dt Control duration
+ * @return Optimal linear and angular velocity
+ */
+inline std::tuple computeDynamicWindowVelocities(
+ const geometry_msgs::msg::Twist & current_speed,
+ const double & max_linear_vel,
+ const double & min_linear_vel,
+ const double & max_angular_vel,
+ const double & min_angular_vel,
+ const double & max_linear_accel,
+ const double & max_linear_decel,
+ const double & max_angular_accel,
+ const double & max_angular_decel,
+ const double & regulated_linear_vel,
+ const double & curvature,
+ const double & sign,
+ const double & dt
+)
+{
+ // compute Dynamic Window
+ DynamicWindowBounds dynamic_window = computeDynamicWindow(
+ current_speed, max_linear_vel, min_linear_vel, max_angular_vel, min_angular_vel,
+ max_linear_accel, max_linear_decel, max_angular_accel, max_angular_decel, dt);
+
+ // apply regulation to Dynamic Window
+ applyRegulationToDynamicWindow(regulated_linear_vel, dynamic_window);
+
+ // compute optimal velocity within Dynamic Window
+ auto [linear_vel, angular_vel] = computeOptimalVelocityWithinDynamicWindow(
+ dynamic_window, curvature, sign);
+
+ return std::make_tuple(linear_vel, angular_vel);
+}
+
+} // namespace dynamic_window_pure_pursuit
+
+} // namespace easynav
+
+#endif // EASYNAV_REGULATED_PP_CONTROLLER__DYNAMIC_WINDOW_PURE_PURSUIT_FUNCTIONS_HPP_
diff --git a/controllers/easynav_regulated_pp_controller/include/easynav_regulated_pp_controller/regulation_functions.hpp b/controllers/easynav_regulated_pp_controller/include/easynav_regulated_pp_controller/regulation_functions.hpp
new file mode 100644
index 00000000..babc2859
--- /dev/null
+++ b/controllers/easynav_regulated_pp_controller/include/easynav_regulated_pp_controller/regulation_functions.hpp
@@ -0,0 +1,153 @@
+// Copyright (c) 2020 Shrijit Singh
+// Copyright (c) 2020 Samsung Research America
+// Copyright 2026 Intelligent Robotics Lab
+//
+// This file is part of the project Easy Navigation (EasyNav in short)
+// and is a derivative work of nav2_regulated_pure_pursuit_controller,
+// ported to the EasyNav plugin architecture.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/// \file
+/// \brief Pure regulation heuristics for the Regulated Pure Pursuit controller.
+///
+/// These are the same regulation terms described in:
+/// S. Macenski, S. Singh, F. Martin, J. Gines, "Regulated Pure Pursuit for Robot Path Tracking",
+/// Autonomous Robots, 2023 (https://arxiv.org/abs/2305.20026), ported from
+/// nav2_regulated_pure_pursuit_controller. Since EasyNav controllers have no costmap available,
+/// the cost-based regulation term is replaced by an equivalent one driven by the distance to the
+/// nearest point in the fused point-cloud perception (see \ref easynav::RegulatedPurePursuitController).
+
+#ifndef EASYNAV_REGULATED_PP_CONTROLLER__REGULATION_FUNCTIONS_HPP_
+#define EASYNAV_REGULATED_PP_CONTROLLER__REGULATION_FUNCTIONS_HPP_
+
+#include
+#include
+#include
+
+namespace easynav
+{
+
+namespace heuristics
+{
+
+/// \brief Curvature of the arc from the robot (at the local origin, facing +x) to a lookahead point.
+/// \param lookahead_x X coordinate of the lookahead point in the robot frame.
+/// \param lookahead_y Y coordinate of the lookahead point in the robot frame.
+/// \return Signed curvature (1 / turning radius) of the arc.
+inline double calculateCurvature(double lookahead_x, double lookahead_y)
+{
+ const double carrot_dist2 = lookahead_x * lookahead_x + lookahead_y * lookahead_y;
+ if (carrot_dist2 > 0.001) {
+ return 2.0 * lookahead_y / carrot_dist2;
+ } else {
+ return 0.0;
+ }
+}
+
+/// \brief Apply curvature-based regulation on the linear velocity.
+///
+/// Slows the robot down when the path curvature radius is smaller than \p min_radius,
+/// so sharp turns are taken more conservatively.
+/// \param raw_linear_vel Desired linear velocity before regulation.
+/// \param curvature Curvature of the path at the regulation lookahead point.
+/// \param min_radius Turning radius below which regulation kicks in.
+/// \return Regulated linear velocity.
+inline double curvatureConstraint(
+ const double raw_linear_vel, const double curvature, const double min_radius)
+{
+ const double radius = std::fabs(1.0 / curvature);
+ if (radius < min_radius) {
+ return raw_linear_vel * (1.0 - (std::fabs(radius - min_radius) / min_radius));
+ } else {
+ return raw_linear_vel;
+ }
+}
+
+/// \brief Apply obstacle-proximity regulation on the linear velocity.
+///
+/// EasyNav controllers do not have access to a costmap, so this heuristic replaces
+/// nav2_regulated_pure_pursuit_controller's cost-based regulation with an equivalent term based on
+/// the distance to the closest obstacle point observed by the robot's perception.
+/// \param raw_linear_vel Desired linear velocity before regulation.
+/// \param min_obstacle_distance Distance (m) to the closest known obstacle ahead of the robot.
+/// \param obstacle_scaling_dist Distance below which the regulation is triggered.
+/// \param obstacle_scaling_gain Gain (<= 1.0) applied when scaling down the velocity.
+/// \return Regulated linear velocity.
+inline double obstacleConstraint(
+ const double raw_linear_vel,
+ const double min_obstacle_distance,
+ const double obstacle_scaling_dist,
+ const double obstacle_scaling_gain)
+{
+ if (!std::isfinite(min_obstacle_distance)) {
+ return raw_linear_vel;
+ }
+
+ if (min_obstacle_distance < obstacle_scaling_dist) {
+ const double clamped_dist = std::max(0.0, min_obstacle_distance);
+ return raw_linear_vel * (obstacle_scaling_gain * clamped_dist / obstacle_scaling_dist);
+ }
+
+ return raw_linear_vel;
+}
+
+/// \brief Compute the scale factor to apply for linear velocity regulation on approach to goal.
+/// \param remaining_path_distance Integrated distance left to travel along the path.
+/// \param euclidean_dist_to_goal Straight-line distance from the robot to the goal.
+/// \param approach_velocity_scaling_dist Distance away from goal at which to apply the heuristic.
+/// \return A scale factor in [0.0, 1.0].
+inline double approachVelocityScalingFactor(
+ const double remaining_path_distance,
+ const double euclidean_dist_to_goal,
+ const double approach_velocity_scaling_dist)
+{
+ // Waiting to apply the threshold based on integrated distance ensures we don't erroneously
+ // apply approach scaling on curvy paths still far from the goal in a straight line.
+ if (remaining_path_distance < approach_velocity_scaling_dist) {
+ return euclidean_dist_to_goal / approach_velocity_scaling_dist;
+ } else {
+ return 1.0;
+ }
+}
+
+/// \brief Velocity on approach to goal heuristic regulation term.
+/// \param constrained_linear_vel Linear velocity already constrained by other heuristics.
+/// \param remaining_path_distance Integrated distance left to travel along the path.
+/// \param euclidean_dist_to_goal Straight-line distance from the robot to the goal.
+/// \param min_approach_velocity Minimum velocity to use on approach to goal.
+/// \param approach_velocity_scaling_dist Distance away from goal at which to apply the heuristic.
+/// \return Velocity after regulation via approach-to-goal slow-down.
+inline double approachVelocityConstraint(
+ const double constrained_linear_vel,
+ const double remaining_path_distance,
+ const double euclidean_dist_to_goal,
+ const double min_approach_velocity,
+ const double approach_velocity_scaling_dist)
+{
+ const double velocity_scaling = approachVelocityScalingFactor(
+ remaining_path_distance, euclidean_dist_to_goal, approach_velocity_scaling_dist);
+ double approach_vel = constrained_linear_vel * velocity_scaling;
+
+ if (approach_vel < min_approach_velocity) {
+ approach_vel = min_approach_velocity;
+ }
+
+ return std::min(constrained_linear_vel, approach_vel);
+}
+
+} // namespace heuristics
+
+} // namespace easynav
+
+#endif // EASYNAV_REGULATED_PP_CONTROLLER__REGULATION_FUNCTIONS_HPP_
diff --git a/controllers/easynav_regulated_pp_controller/package.xml b/controllers/easynav_regulated_pp_controller/package.xml
new file mode 100644
index 00000000..7aa5f5f9
--- /dev/null
+++ b/controllers/easynav_regulated_pp_controller/package.xml
@@ -0,0 +1,35 @@
+
+
+
+ easynav_regulated_pp_controller
+ 0.4.0
+ Easy Navigation: port of the Nav2 Regulated Pure Pursuit Controller.
+ Francisco Martín Rico
+ Apache-2.0
+
+ Steve Macenski
+ Shrijit Singh
+
+ ament_cmake
+
+ easynav_common
+ easynav_core
+ easynav_system
+ easynav_sensors
+ pluginlib
+ tf2
+ tf2_geometry_msgs
+ tf2_ros
+ geometry_msgs
+ nav_msgs
+ std_msgs
+
+ rclcpp_lifecycle
+ ament_lint_auto
+ ament_lint_common
+ ament_cmake_gtest
+
+
+ ament_cmake
+
+
diff --git a/controllers/easynav_regulated_pp_controller/src/easynav_regulated_pp_controller/RegulatedPurePursuitController.cpp b/controllers/easynav_regulated_pp_controller/src/easynav_regulated_pp_controller/RegulatedPurePursuitController.cpp
new file mode 100644
index 00000000..7d78d1a5
--- /dev/null
+++ b/controllers/easynav_regulated_pp_controller/src/easynav_regulated_pp_controller/RegulatedPurePursuitController.cpp
@@ -0,0 +1,542 @@
+// Copyright (c) 2020 Shrijit Singh
+// Copyright (c) 2020 Samsung Research America
+// Copyright 2026 Intelligent Robotics Lab
+//
+// This file is part of the project Easy Navigation (EasyNav in short)
+// and is a derivative work of nav2_regulated_pure_pursuit_controller,
+// ported to the EasyNav plugin architecture.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/// \file
+/// \brief Implementation of the RegulatedPurePursuitController class.
+
+#include
+#include
+#include
+
+#include "tf2/utils.hpp"
+#include "tf2_geometry_msgs/tf2_geometry_msgs.hpp"
+
+#include "easynav_regulated_pp_controller/RegulatedPurePursuitController.hpp"
+#include "easynav_regulated_pp_controller/regulation_functions.hpp"
+#include "easynav_regulated_pp_controller/dynamic_window_pure_pursuit_functions.hpp"
+
+#include "easynav_common/RTTFBuffer.hpp"
+#include "easynav_system/GoalManager.hpp"
+
+namespace easynav
+{
+
+namespace
+{
+double normalizeAngle(double angle)
+{
+ while (angle > M_PI) {angle -= 2.0 * M_PI;}
+ while (angle < -M_PI) {angle += 2.0 * M_PI;}
+ return angle;
+}
+} // namespace
+
+RegulatedPurePursuitController::RegulatedPurePursuitController() {}
+
+RegulatedPurePursuitController::~RegulatedPurePursuitController() = default;
+
+void
+RegulatedPurePursuitController::on_initialize()
+{
+ auto node = get_node();
+ const auto & plugin_name = get_plugin_name();
+
+ auto declare_and_get = [&node, &plugin_name](const std::string & name, auto & value) {
+ node->declare_parameter(plugin_name + "." + name, value);
+ node->get_parameter(plugin_name + "." + name, value);
+ };
+
+ declare_and_get("lookahead_dist", lookahead_dist_);
+ declare_and_get("min_lookahead_dist", min_lookahead_dist_);
+ declare_and_get("max_lookahead_dist", max_lookahead_dist_);
+ declare_and_get("lookahead_time", lookahead_time_);
+ declare_and_get("use_velocity_scaled_lookahead_dist", use_velocity_scaled_lookahead_dist_);
+
+ declare_and_get("max_linear_vel", max_linear_vel_);
+ declare_and_get("min_linear_vel", min_linear_vel_);
+ declare_and_get("max_angular_vel", max_angular_vel_);
+ declare_and_get("min_angular_vel", min_angular_vel_);
+ declare_and_get("max_linear_accel", max_linear_accel_);
+ declare_and_get("max_linear_decel", max_linear_decel_);
+ declare_and_get("max_angular_accel", max_angular_accel_);
+ declare_and_get("max_angular_decel", max_angular_decel_);
+ declare_and_get("use_dynamic_window", use_dynamic_window_);
+ declare_and_get("allow_reversing", allow_reversing_);
+
+ declare_and_get("use_rotate_to_heading", use_rotate_to_heading_);
+ declare_and_get("rotate_to_heading_angular_vel", rotate_to_heading_angular_vel_);
+ declare_and_get("rotate_to_heading_min_angle", rotate_to_heading_min_angle_);
+
+ declare_and_get("use_regulated_linear_velocity_scaling", use_regulated_linear_velocity_scaling_);
+ declare_and_get("regulated_linear_scaling_min_radius", regulated_linear_scaling_min_radius_);
+ declare_and_get("regulated_linear_scaling_min_speed", regulated_linear_scaling_min_speed_);
+ declare_and_get("use_fixed_curvature_lookahead", use_fixed_curvature_lookahead_);
+ declare_and_get("curvature_lookahead_dist", curvature_lookahead_dist_);
+ declare_and_get("interpolate_curvature_after_goal", interpolate_curvature_after_goal_);
+
+ declare_and_get(
+ "use_obstacle_regulated_linear_velocity_scaling",
+ use_obstacle_regulated_linear_velocity_scaling_);
+ declare_and_get("obstacle_scaling_dist", obstacle_scaling_dist_);
+ declare_and_get("obstacle_scaling_gain", obstacle_scaling_gain_);
+
+ declare_and_get("min_approach_linear_velocity", min_approach_linear_velocity_);
+ declare_and_get("approach_velocity_scaling_dist", approach_velocity_scaling_dist_);
+
+ declare_and_get("xy_goal_tolerance", xy_goal_tolerance_);
+ declare_and_get("yaw_goal_tolerance", yaw_goal_tolerance_);
+
+ lookahead_point_pub_ = node->create_publisher(
+ "lookahead_point", 10);
+ curvature_lookahead_point_pub_ = node->create_publisher(
+ "curvature_lookahead_point", 10);
+ is_rotating_to_heading_pub_ = node->create_publisher(
+ "is_rotating_to_heading", 10);
+
+ last_update_ts_ = node->now();
+}
+
+void
+RegulatedPurePursuitController::stop(NavState & nav_state, const std_msgs::msg::Header & header)
+{
+ cmd_vel_.header = header;
+ cmd_vel_.twist.linear.x = 0.0;
+ cmd_vel_.twist.angular.z = 0.0;
+ last_linear_vel_ = 0.0;
+ last_angular_vel_ = 0.0;
+ is_rotating_to_heading_ = false;
+ last_update_ts_ = get_node()->now();
+ nav_state.set("cmd_vel", cmd_vel_);
+}
+
+double
+RegulatedPurePursuitController::getLookAheadDistance(double linear_vel) const
+{
+ double dist = lookahead_dist_;
+ if (use_velocity_scaled_lookahead_dist_) {
+ dist = std::fabs(linear_vel) * lookahead_time_;
+ dist = std::clamp(dist, min_lookahead_dist_, max_lookahead_dist_);
+ }
+ return dist;
+}
+
+geometry_msgs::msg::Point
+RegulatedPurePursuitController::circleSegmentIntersection(
+ const geometry_msgs::msg::Point & p1,
+ const geometry_msgs::msg::Point & p2,
+ double r,
+ const geometry_msgs::msg::Point & center)
+{
+ const double x1 = p1.x - center.x;
+ const double y1 = p1.y - center.y;
+ const double x2 = p2.x - center.x;
+ const double y2 = p2.y - center.y;
+
+ const double dx = x2 - x1;
+ const double dy = y2 - y1;
+ const double dr2 = dx * dx + dy * dy;
+
+ geometry_msgs::msg::Point result;
+
+ if (dr2 < 1e-9) {
+ result = p2;
+ return result;
+ }
+
+ const double D = x1 * y2 - x2 * y1;
+ const double discriminant = r * r * dr2 - D * D;
+
+ if (discriminant < 0.0) {
+ // No real intersection (can happen with degenerate/noisy paths): fall back to p2.
+ result = p2;
+ return result;
+ }
+
+ const double sqrt_term = std::sqrt(discriminant);
+ const double sign_dy = (dy < 0.0) ? -1.0 : 1.0;
+
+ const double sol1_x = (D * dy + sign_dy * dx * sqrt_term) / dr2;
+ const double sol1_y = (-D * dx + std::fabs(dy) * sqrt_term) / dr2;
+ const double sol2_x = (D * dy - sign_dy * dx * sqrt_term) / dr2;
+ const double sol2_y = (-D * dx - std::fabs(dy) * sqrt_term) / dr2;
+
+ // Choose the solution further along the segment direction (closer to p2).
+ const double t1 = ((sol1_x - x1) * dx + (sol1_y - y1) * dy) / dr2;
+ const double t2 = ((sol2_x - x1) * dx + (sol2_y - y1) * dy) / dr2;
+
+ if (t1 >= t2) {
+ result.x = sol1_x + center.x;
+ result.y = sol1_y + center.y;
+ } else {
+ result.x = sol2_x + center.x;
+ result.y = sol2_y + center.y;
+ }
+ result.z = 0.0;
+ return result;
+}
+
+std::size_t
+RegulatedPurePursuitController::findClosestPoseIndex(
+ const nav_msgs::msg::Path & path,
+ const geometry_msgs::msg::Point & robot_position)
+{
+ std::size_t closest_idx = 0;
+ double closest_dist = std::numeric_limits::max();
+ for (std::size_t i = 0; i < path.poses.size(); ++i) {
+ const auto & p = path.poses[i].pose.position;
+ const double d = std::hypot(p.x - robot_position.x, p.y - robot_position.y);
+ if (d < closest_dist) {
+ closest_dist = d;
+ closest_idx = i;
+ }
+ }
+ return closest_idx;
+}
+
+geometry_msgs::msg::Point
+RegulatedPurePursuitController::getLookAheadPoint(
+ const nav_msgs::msg::Path & path,
+ const geometry_msgs::msg::Point & robot_position,
+ double lookahead_dist,
+ bool interpolate_after_end)
+{
+ auto dist_to_robot = [&robot_position](const geometry_msgs::msg::Point & p) {
+ return std::hypot(p.x - robot_position.x, p.y - robot_position.y);
+ };
+
+ // Only look forward from the pose closest to the robot: the controller receives the full,
+ // un-pruned global path, so searching from index 0 would (incorrectly) match the path's start
+ // pose as soon as the robot has travelled more than a lookahead distance away from it, freezing
+ // the carrot behind the robot for the rest of the path.
+ const std::size_t start_idx = findClosestPoseIndex(path, robot_position);
+ const auto begin_it = path.poses.begin() + static_cast(start_idx);
+
+ auto it = std::find_if(
+ begin_it, path.poses.end(),
+ [&](const geometry_msgs::msg::PoseStamped & ps) {
+ return dist_to_robot(ps.pose.position) >= lookahead_dist;
+ });
+
+ if (it == path.poses.end()) {
+ const auto & goal = path.poses.back().pose.position;
+ if (interpolate_after_end && path.poses.size() >= 2 && dist_to_robot(goal) < lookahead_dist) {
+ const auto & prev = path.poses[path.poses.size() - 2].pose.position;
+ const double seg_len = std::hypot(goal.x - prev.x, goal.y - prev.y);
+ if (seg_len > 1e-6) {
+ geometry_msgs::msg::Point extended;
+ extended.x = goal.x + (goal.x - prev.x) / seg_len * lookahead_dist;
+ extended.y = goal.y + (goal.y - prev.y) / seg_len * lookahead_dist;
+ return circleSegmentIntersection(goal, extended, lookahead_dist, robot_position);
+ }
+ }
+ return goal;
+ }
+
+ if (it == begin_it) {
+ return it->pose.position;
+ }
+
+ const auto prev_it = std::prev(it);
+ return circleSegmentIntersection(
+ prev_it->pose.position, it->pose.position, lookahead_dist, robot_position);
+}
+
+geometry_msgs::msg::Point
+RegulatedPurePursuitController::toRobotFrame(
+ const geometry_msgs::msg::Point & point,
+ const geometry_msgs::msg::Pose & robot_pose,
+ double robot_yaw)
+{
+ const double dx = point.x - robot_pose.position.x;
+ const double dy = point.y - robot_pose.position.y;
+ const double c = std::cos(-robot_yaw);
+ const double s = std::sin(-robot_yaw);
+
+ geometry_msgs::msg::Point local;
+ local.x = c * dx - s * dy;
+ local.y = s * dx + c * dy;
+ local.z = 0.0;
+ return local;
+}
+
+bool
+RegulatedPurePursuitController::shouldRotateToPath(double angle_to_path) const
+{
+ return use_rotate_to_heading_ && std::fabs(angle_to_path) > rotate_to_heading_min_angle_;
+}
+
+void
+RegulatedPurePursuitController::rotateToHeading(
+ double & linear_vel, double & angular_vel, double angle_to_target, double dt) const
+{
+ linear_vel = 0.0;
+ const double sign = angle_to_target > 0.0 ? 1.0 : -1.0;
+ angular_vel = sign * rotate_to_heading_angular_vel_;
+
+ // Braking (magnitude decreasing) is bounded by max_angular_decel_, speeding up
+ // (magnitude increasing) by max_angular_accel_; which bound applies to which side
+ // of the window depends on the current direction of rotation.
+ const double decel_bound = last_angular_vel_ >= 0.0 ? max_angular_decel_ : max_angular_accel_;
+ const double accel_bound = last_angular_vel_ >= 0.0 ? max_angular_accel_ : max_angular_decel_;
+ const double min_feasible = last_angular_vel_ - decel_bound * dt;
+ const double max_feasible = last_angular_vel_ + accel_bound * dt;
+ angular_vel = std::clamp(angular_vel, min_feasible, max_feasible);
+
+ // Slow down to avoid overshooting the target angle.
+ const double decel = std::max(max_angular_decel_, 1e-9);
+ const double max_vel_to_stop = std::sqrt(2.0 * decel * std::fabs(angle_to_target));
+ if (std::fabs(angular_vel) > max_vel_to_stop) {
+ angular_vel = sign * max_vel_to_stop;
+ }
+}
+
+void
+RegulatedPurePursuitController::applyConstraints(
+ double curvature,
+ double min_obstacle_distance,
+ double remaining_path_distance,
+ double euclidean_dist_to_goal,
+ double & linear_vel) const
+{
+ double curvature_vel = linear_vel;
+ double obstacle_vel = linear_vel;
+
+ if (use_regulated_linear_velocity_scaling_) {
+ curvature_vel = heuristics::curvatureConstraint(
+ linear_vel, curvature, regulated_linear_scaling_min_radius_);
+ }
+
+ if (use_obstacle_regulated_linear_velocity_scaling_) {
+ obstacle_vel = heuristics::obstacleConstraint(
+ linear_vel, min_obstacle_distance, obstacle_scaling_dist_, obstacle_scaling_gain_);
+ }
+
+ linear_vel = std::min(curvature_vel, obstacle_vel);
+ linear_vel = std::max(linear_vel, regulated_linear_scaling_min_speed_);
+
+ linear_vel = heuristics::approachVelocityConstraint(
+ linear_vel, remaining_path_distance, euclidean_dist_to_goal,
+ min_approach_linear_velocity_, approach_velocity_scaling_dist_);
+}
+
+double
+RegulatedPurePursuitController::computeMinObstacleDistance(
+ NavState & nav_state, double max_range) const
+{
+ const auto & perceptions = nav_state.get_by_type();
+ if (perceptions.empty()) {
+ return std::numeric_limits::infinity();
+ }
+
+ const auto & tf_info = RTTFBuffer::getInstance()->get_tf_info();
+ const auto & robot_frame = tf_info.robot_frame;
+
+ auto view = PointPerceptionsOpsView(perceptions);
+ view.fuse(robot_frame)
+ .filter(
+ {0.0, -robot_radius_ - safety_margin_, z_min_filter_},
+ {std::max(max_range, 0.0), robot_radius_ + safety_margin_, robot_height_});
+
+ const auto & cloud = view.as_points();
+
+ double min_dist = std::numeric_limits::infinity();
+ for (const auto & p : cloud.points) {
+ if (!std::isfinite(p.x) || !std::isfinite(p.y)) {continue;}
+ const double d = std::hypot(p.x, p.y) - robot_radius_;
+ if (d < min_dist) {min_dist = d;}
+ }
+
+ return min_dist;
+}
+
+double
+RegulatedPurePursuitController::remainingPathDistance(
+ const nav_msgs::msg::Path & path,
+ const geometry_msgs::msg::Point & robot_position)
+{
+ if (path.poses.empty()) {return 0.0;}
+
+ const std::size_t closest_idx = findClosestPoseIndex(path, robot_position);
+ const auto & closest_p = path.poses[closest_idx].pose.position;
+ double distance = std::hypot(closest_p.x - robot_position.x, closest_p.y - robot_position.y);
+ for (std::size_t i = closest_idx + 1; i < path.poses.size(); ++i) {
+ const auto & prev = path.poses[i - 1].pose.position;
+ const auto & cur = path.poses[i].pose.position;
+ distance += std::hypot(cur.x - prev.x, cur.y - prev.y);
+ }
+
+ return distance;
+}
+
+void
+RegulatedPurePursuitController::publishCarrot(
+ const rclcpp::Publisher::SharedPtr & pub,
+ const geometry_msgs::msg::Point & carrot,
+ const std_msgs::msg::Header & header) const
+{
+ if (!pub || pub->get_subscription_count() == 0) {return;}
+ geometry_msgs::msg::PointStamped msg;
+ msg.header = header;
+ msg.point = carrot;
+ msg.point.z = 0.05;
+ pub->publish(msg);
+}
+
+void
+RegulatedPurePursuitController::update_rt(NavState & nav_state)
+{
+ if (nav_state.has("navigation_state")) {
+ const auto goal_state = nav_state.get("navigation_state");
+ if (goal_state == easynav::GoalManager::State::IDLE) {
+ std_msgs::msg::Header header;
+ header.stamp = get_node()->now();
+ stop(nav_state, header);
+ return;
+ }
+ }
+
+ if (!nav_state.has("path") || !nav_state.has("robot_pose")) {return;}
+
+ const auto & path = nav_state.get("path");
+
+ std_msgs::msg::Header header;
+ header.frame_id = path.header.frame_id;
+ header.stamp = get_node()->now();
+
+ if (path.poses.empty()) {
+ stop(nav_state, header);
+ return;
+ }
+
+ const auto & robot_pose = nav_state.get("robot_pose").pose.pose;
+ const double robot_yaw = tf2::getYaw(robot_pose.orientation);
+
+ const auto & goal_pose = path.poses.back().pose;
+
+ double xy_tol = xy_goal_tolerance_;
+ double yaw_tol = yaw_goal_tolerance_;
+ if (nav_state.has("goal_tolerance.position")) {
+ xy_tol = nav_state.get("goal_tolerance.position");
+ }
+ if (nav_state.has("goal_tolerance.yaw")) {
+ yaw_tol = nav_state.get("goal_tolerance.yaw");
+ }
+
+ const double dist_to_goal = std::hypot(
+ goal_pose.position.x - robot_pose.position.x,
+ goal_pose.position.y - robot_pose.position.y);
+ const double yaw_goal = tf2::getYaw(goal_pose.orientation);
+ const double e_theta_goal = normalizeAngle(yaw_goal - robot_yaw);
+
+ const bool at_goal_xy = dist_to_goal <= xy_tol;
+
+ if (at_goal_xy && std::fabs(e_theta_goal) <= yaw_tol) {
+ stop(nav_state, header);
+ return;
+ }
+
+ double dt = (get_node()->now() - last_update_ts_).seconds();
+ if (dt <= 0.0 || dt > 1.0) {dt = 0.05;}
+ last_update_ts_ = get_node()->now();
+
+ double linear_vel = max_linear_vel_;
+ double angular_vel = 0.0;
+
+ if (use_rotate_to_heading_ && at_goal_xy) {
+ is_rotating_to_heading_ = true;
+ rotateToHeading(linear_vel, angular_vel, e_theta_goal, dt);
+ } else {
+ const double lookahead_dist = getLookAheadDistance(last_linear_vel_);
+ const auto carrot_global = getLookAheadPoint(path, robot_pose.position, lookahead_dist);
+ const auto carrot_local = toRobotFrame(carrot_global, robot_pose, robot_yaw);
+ publishCarrot(lookahead_point_pub_, carrot_global, header);
+
+ auto curvature_local = carrot_local;
+ if (use_fixed_curvature_lookahead_) {
+ const auto curvature_carrot_global = getLookAheadPoint(
+ path, robot_pose.position, curvature_lookahead_dist_, interpolate_curvature_after_goal_);
+ curvature_local = toRobotFrame(curvature_carrot_global, robot_pose, robot_yaw);
+ publishCarrot(curvature_lookahead_point_pub_, curvature_carrot_global, header);
+ }
+
+ double x_vel_sign = 1.0;
+ if (allow_reversing_) {
+ x_vel_sign = carrot_local.x >= 0.0 ? 1.0 : -1.0;
+ }
+
+ double angle_to_path = std::atan2(curvature_local.y, curvature_local.x);
+ if (x_vel_sign < 0.0) {
+ angle_to_path = normalizeAngle(angle_to_path + M_PI);
+ }
+
+ const double regulation_curvature = heuristics::calculateCurvature(
+ curvature_local.x, curvature_local.y);
+
+ if (shouldRotateToPath(angle_to_path)) {
+ is_rotating_to_heading_ = true;
+ rotateToHeading(linear_vel, angular_vel, angle_to_path, dt);
+ } else {
+ is_rotating_to_heading_ = false;
+
+ const double min_obstacle_distance = use_obstacle_regulated_linear_velocity_scaling_ ?
+ computeMinObstacleDistance(nav_state, lookahead_dist) :
+ std::numeric_limits::infinity();
+ const double remaining_path_distance = remainingPathDistance(path, robot_pose.position);
+
+ applyConstraints(
+ regulation_curvature, min_obstacle_distance, remaining_path_distance, dist_to_goal,
+ linear_vel);
+ linear_vel = x_vel_sign * std::clamp(std::fabs(linear_vel), 0.0, max_linear_vel_);
+
+ if (!use_dynamic_window_) {
+ angular_vel = linear_vel * regulation_curvature;
+ } else {
+ geometry_msgs::msg::Twist current_speed;
+ current_speed.linear.x = last_linear_vel_;
+ current_speed.angular.z = last_angular_vel_;
+ std::tie(linear_vel, angular_vel) =
+ dynamic_window_pure_pursuit::computeDynamicWindowVelocities(
+ current_speed, max_linear_vel_, min_linear_vel_, max_angular_vel_, min_angular_vel_,
+ max_linear_accel_, max_linear_decel_, max_angular_accel_, max_angular_decel_,
+ linear_vel, regulation_curvature, x_vel_sign, dt);
+ }
+ }
+ }
+
+ last_linear_vel_ = linear_vel;
+ last_angular_vel_ = angular_vel;
+
+ if (is_rotating_to_heading_pub_ && is_rotating_to_heading_pub_->get_subscription_count() > 0) {
+ std_msgs::msg::Bool msg;
+ msg.data = is_rotating_to_heading_;
+ is_rotating_to_heading_pub_->publish(msg);
+ }
+
+ cmd_vel_.header = header;
+ cmd_vel_.twist.linear.x = linear_vel;
+ cmd_vel_.twist.angular.z = angular_vel;
+ nav_state.set("cmd_vel", cmd_vel_);
+}
+
+} // namespace easynav
+
+#include
+PLUGINLIB_EXPORT_CLASS(
+ easynav::RegulatedPurePursuitController, easynav::ControllerMethodBase)
diff --git a/controllers/easynav_regulated_pp_controller/tests/CMakeLists.txt b/controllers/easynav_regulated_pp_controller/tests/CMakeLists.txt
new file mode 100644
index 00000000..7c7c867f
--- /dev/null
+++ b/controllers/easynav_regulated_pp_controller/tests/CMakeLists.txt
@@ -0,0 +1,4 @@
+ament_add_gtest(regulated_pp_controller_tests regulated_pp_controller_tests.cpp)
+target_link_libraries(regulated_pp_controller_tests
+ ${PROJECT_NAME}
+)
diff --git a/controllers/easynav_regulated_pp_controller/tests/regulated_pp_controller_tests.cpp b/controllers/easynav_regulated_pp_controller/tests/regulated_pp_controller_tests.cpp
new file mode 100644
index 00000000..e77fb865
--- /dev/null
+++ b/controllers/easynav_regulated_pp_controller/tests/regulated_pp_controller_tests.cpp
@@ -0,0 +1,257 @@
+// Copyright 2026 Intelligent Robotics Lab
+//
+// This file is part of the project Easy Navigation (EasyNav in short)
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include
+
+#include
+#include
+
+#include "easynav_regulated_pp_controller/regulation_functions.hpp"
+#include "easynav_regulated_pp_controller/dynamic_window_pure_pursuit_functions.hpp"
+#include "easynav_regulated_pp_controller/RegulatedPurePursuitController.hpp"
+
+using easynav::heuristics::approachVelocityConstraint;
+using easynav::heuristics::calculateCurvature;
+using easynav::heuristics::curvatureConstraint;
+using easynav::heuristics::obstacleConstraint;
+
+TEST(RegulationFunctions, CalculateCurvatureStraightAhead)
+{
+ // A lookahead point directly ahead has zero lateral offset -> zero curvature.
+ EXPECT_NEAR(calculateCurvature(1.0, 0.0), 0.0, 1e-6);
+}
+
+TEST(RegulationFunctions, CalculateCurvatureLateralOffset)
+{
+ // k = 2y / (x^2+y^2)
+ const double k = calculateCurvature(1.0, 0.5);
+ EXPECT_NEAR(k, 2.0 * 0.5 / (1.0 * 1.0 + 0.5 * 0.5), 1e-9);
+}
+
+TEST(RegulationFunctions, CalculateCurvatureDegenerate)
+{
+ EXPECT_NEAR(calculateCurvature(0.0, 0.0), 0.0, 1e-9);
+}
+
+TEST(RegulationFunctions, CurvatureConstraintNoRegulationOnGentleTurn)
+{
+ // Radius = 1/curvature = 10 m, well above the 0.9 m min radius: no regulation applied.
+ const double v = curvatureConstraint(1.0, 0.1, 0.9);
+ EXPECT_NEAR(v, 1.0, 1e-9);
+}
+
+TEST(RegulationFunctions, CurvatureConstraintSlowsOnSharpTurn)
+{
+ // Radius = 1/2.0 = 0.5 m, below the 1.0 m min radius: velocity should be reduced.
+ const double v = curvatureConstraint(1.0, 2.0, 1.0);
+ EXPECT_LT(v, 1.0);
+ EXPECT_GT(v, 0.0);
+}
+
+TEST(RegulationFunctions, ObstacleConstraintNoObstacleNearby)
+{
+ const double v = obstacleConstraint(1.0, 5.0, 0.3, 1.0);
+ EXPECT_NEAR(v, 1.0, 1e-9);
+}
+
+TEST(RegulationFunctions, ObstacleConstraintSlowsNearObstacle)
+{
+ const double v = obstacleConstraint(1.0, 0.1, 0.3, 1.0);
+ EXPECT_LT(v, 1.0);
+ EXPECT_GT(v, 0.0);
+}
+
+TEST(RegulationFunctions, ObstacleConstraintIgnoresInfiniteDistance)
+{
+ const double v = obstacleConstraint(1.0, std::numeric_limits::infinity(), 0.3, 1.0);
+ EXPECT_NEAR(v, 1.0, 1e-9);
+}
+
+TEST(RegulationFunctions, ApproachVelocityConstraintFarFromGoal)
+{
+ const double v = approachVelocityConstraint(1.0, 10.0, 10.0, 0.05, 1.0);
+ EXPECT_NEAR(v, 1.0, 1e-9);
+}
+
+TEST(RegulationFunctions, ApproachVelocityConstraintNearGoal)
+{
+ const double v = approachVelocityConstraint(1.0, 0.5, 0.5, 0.05, 1.0);
+ EXPECT_LT(v, 1.0);
+ EXPECT_GE(v, 0.05);
+}
+
+TEST(DynamicWindowPurePursuit, ComputeDynamicWindowClampsToAccelLimits)
+{
+ geometry_msgs::msg::Twist current_speed;
+ current_speed.linear.x = 0.0;
+ current_speed.angular.z = 0.0;
+
+ const auto window = easynav::dynamic_window_pure_pursuit::computeDynamicWindow(
+ current_speed, /*max_linear_vel=*/ 1.0, /*min_linear_vel=*/ -1.0,
+ /*max_angular_vel=*/ 1.0, /*min_angular_vel=*/ -1.0,
+ /*max_linear_accel=*/ 2.0, /*max_linear_decel=*/ 2.0,
+ /*max_angular_accel=*/ 2.0, /*max_angular_decel=*/ 2.0, /*dt=*/ 0.1);
+
+ EXPECT_NEAR(window.max_linear_vel, 0.2, 1e-9);
+ EXPECT_NEAR(window.min_linear_vel, -0.2, 1e-9);
+}
+
+TEST(DynamicWindowPurePursuit, ComputeOptimalVelocityZeroCurvatureForward)
+{
+ easynav::dynamic_window_pure_pursuit::DynamicWindowBounds window;
+ window.max_linear_vel = 1.0;
+ window.min_linear_vel = -1.0;
+ window.max_angular_vel = 1.0;
+ window.min_angular_vel = -1.0;
+
+ const auto [lin, ang] =
+ easynav::dynamic_window_pure_pursuit::computeOptimalVelocityWithinDynamicWindow(
+ window, /*curvature=*/ 0.0, /*sign=*/ 1.0);
+
+ EXPECT_NEAR(lin, 1.0, 1e-9);
+ EXPECT_NEAR(ang, 0.0, 1e-9);
+}
+
+class FriendRegulatedPurePursuitController : public easynav::RegulatedPurePursuitController
+{
+public:
+ using easynav::RegulatedPurePursuitController::getLookAheadPoint;
+ using easynav::RegulatedPurePursuitController::circleSegmentIntersection;
+ using easynav::RegulatedPurePursuitController::toRobotFrame;
+ using easynav::RegulatedPurePursuitController::remainingPathDistance;
+ using easynav::RegulatedPurePursuitController::findClosestPoseIndex;
+};
+
+TEST(RegulatedPurePursuitControllerHelpers, LookAheadPointInterpolatesOnSegment)
+{
+ nav_msgs::msg::Path path;
+ geometry_msgs::msg::PoseStamped p0, p1, p2;
+ p0.pose.position.x = 0.0;
+ p1.pose.position.x = 1.0;
+ p2.pose.position.x = 2.0;
+ path.poses = {p0, p1, p2};
+
+ geometry_msgs::msg::Point robot;
+ robot.x = 0.0;
+ robot.y = 0.0;
+
+ const auto carrot = FriendRegulatedPurePursuitController::getLookAheadPoint(
+ path, robot, /*lookahead_dist=*/ 1.5);
+
+ EXPECT_NEAR(carrot.x, 1.5, 1e-6);
+ EXPECT_NEAR(carrot.y, 0.0, 1e-6);
+}
+
+TEST(RegulatedPurePursuitControllerHelpers, LookAheadPointIgnoresPathBehindRobot)
+{
+ // Regression test: the controller receives the full, un-pruned global path, so the carrot
+ // search must start from the pose closest to the robot, not from the path's start pose. A path
+ // start pose that is far behind the robot must never be picked up as the carrot.
+ nav_msgs::msg::Path path;
+ for (double x = 0.0; x <= 10.0; x += 0.1) {
+ geometry_msgs::msg::PoseStamped p;
+ p.pose.position.x = x;
+ path.poses.push_back(p);
+ }
+
+ // Robot has already travelled most of the path.
+ geometry_msgs::msg::Point robot;
+ robot.x = 9.0;
+ robot.y = 0.0;
+
+ const auto carrot = FriendRegulatedPurePursuitController::getLookAheadPoint(
+ path, robot, /*lookahead_dist=*/ 0.5);
+
+ // The carrot must be ahead of the robot (close to x=9.5), never back at the path's start (x=0).
+ EXPECT_NEAR(carrot.x, 9.5, 1e-6);
+ EXPECT_NEAR(carrot.y, 0.0, 1e-6);
+}
+
+TEST(RegulatedPurePursuitControllerHelpers, FindClosestPoseIndexPicksNearestNotFirst)
+{
+ nav_msgs::msg::Path path;
+ for (double x = 0.0; x <= 10.0; x += 1.0) {
+ geometry_msgs::msg::PoseStamped p;
+ p.pose.position.x = x;
+ path.poses.push_back(p);
+ }
+
+ geometry_msgs::msg::Point robot;
+ robot.x = 7.1;
+ robot.y = 0.0;
+
+ const auto idx = FriendRegulatedPurePursuitController::findClosestPoseIndex(path, robot);
+ EXPECT_EQ(idx, 7u);
+}
+
+TEST(RegulatedPurePursuitControllerHelpers, LookAheadPointClampsToPathEnd)
+{
+ nav_msgs::msg::Path path;
+ geometry_msgs::msg::PoseStamped p0, p1;
+ p0.pose.position.x = 0.0;
+ p1.pose.position.x = 1.0;
+ path.poses = {p0, p1};
+
+ geometry_msgs::msg::Point robot;
+ robot.x = 0.0;
+ robot.y = 0.0;
+
+ const auto carrot = FriendRegulatedPurePursuitController::getLookAheadPoint(
+ path, robot, /*lookahead_dist=*/ 5.0);
+
+ EXPECT_NEAR(carrot.x, 1.0, 1e-6);
+}
+
+TEST(RegulatedPurePursuitControllerHelpers, ToRobotFrameRotatesAndTranslates)
+{
+ geometry_msgs::msg::Point global_point;
+ global_point.x = 1.0;
+ global_point.y = 1.0;
+
+ geometry_msgs::msg::Pose robot_pose;
+ robot_pose.position.x = 1.0;
+ robot_pose.position.y = 0.0;
+
+ // Robot facing +90 degrees (looking along +y): the point straight ahead in the global
+ // frame (0, 1) relative to the robot should map to local +x.
+ const auto local = FriendRegulatedPurePursuitController::toRobotFrame(
+ global_point, robot_pose, M_PI_2);
+
+ EXPECT_NEAR(local.x, 1.0, 1e-6);
+ EXPECT_NEAR(local.y, 0.0, 1e-6);
+}
+
+TEST(RegulatedPurePursuitControllerHelpers, RemainingPathDistanceFromClosestPoint)
+{
+ nav_msgs::msg::Path path;
+ geometry_msgs::msg::PoseStamped p0, p1, p2;
+ p0.pose.position.x = 0.0;
+ p1.pose.position.x = 1.0;
+ p2.pose.position.x = 2.0;
+ path.poses = {p0, p1, p2};
+
+ geometry_msgs::msg::Point robot;
+ robot.x = 1.0;
+ robot.y = 0.0;
+
+ const double dist = FriendRegulatedPurePursuitController::remainingPathDistance(path, robot);
+ EXPECT_NEAR(dist, 1.0, 1e-6);
+}
+
+int main(int argc, char ** argv)
+{
+ testing::InitGoogleTest(&argc, argv);
+ return RUN_ALL_TESTS();
+}
diff --git a/controllers/easynav_serest_controller/CHANGELOG.rst b/controllers/easynav_serest_controller/CHANGELOG.rst
index 0c86bc2a..d57cbc21 100644
--- a/controllers/easynav_serest_controller/CHANGELOG.rst
+++ b/controllers/easynav_serest_controller/CHANGELOG.rst
@@ -2,6 +2,35 @@
Changelog for package easynav_serest_controller
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+0.4.0 (2026-07-26)
+------------------
+* Add missing easynav_sensors deps
+* Navstate key among filters are always map, not an arbitrary key
+* Adaptations to `#94 `_
+* Update plugins to new sensors API
+* Fix segfault in some cases and reduce extrapolation to the future
+* GPLv3 -> Apache 2.0
+* Sync to current EasyNavigation
+* Contributors: Francisco Martín Rico, Francisco Miguel Moreno, Juan S. Cely G., Miguel, migueldm
+
+0.2.1 (2026-02-27)
+------------------
+* 0.2.0
+* GPLv3 -> Apache 2.0
+* Documentation was corrected
+* Remove C++20/C++23 features and update to new MethodBase interface
+* Merge branch 'set_robot_frame' into frames-fix-pr-40
+* TFInfo in RTTFBuffer
+* Refactor to use TFInfo
+* Sync to current EasyNavigation
+* Translate comments
+* Referencing base class if ot void
+* Merge remote-tracking branch 'upstream/rolling' into rolling
+* Fix Link error in controller
+* Optimize execution
+* Cleanup unused headers
+* Contributors: Francisco Martín Rico, Francisco Miguel Moreno, José Miguel Guerrero, Juan S. Cely G., Miguel, estherag
+
0.0.2 (2025-10-12)
------------------
* Reorganization initial
diff --git a/controllers/easynav_serest_controller/CMakeLists.txt b/controllers/easynav_serest_controller/CMakeLists.txt
index 26541250..b0b5b0e4 100644
--- a/controllers/easynav_serest_controller/CMakeLists.txt
+++ b/controllers/easynav_serest_controller/CMakeLists.txt
@@ -5,16 +5,16 @@ if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
-set(CMAKE_CXX_STANDARD 23)
-set(CMAKE_CXX_STANDARD_REQUIRED ON)
-set(CMAKE_CXX_EXTENSIONS OFF)
-
find_package(ament_cmake REQUIRED)
find_package(easynav_common REQUIRED)
find_package(easynav_core REQUIRED)
+find_package(easynav_sensors REQUIRED)
+find_package(rclcpp REQUIRED)
find_package(pluginlib REQUIRED)
find_package(geometry_msgs REQUIRED)
find_package(tf2_ros REQUIRED)
+find_package(tf2 REQUIRED)
+find_package(tf2_geometry_msgs REQUIRED)
find_package(nav_msgs REQUIRED)
@@ -28,7 +28,11 @@ target_include_directories(${PROJECT_NAME} PUBLIC
target_link_libraries(${PROJECT_NAME} PUBLIC
easynav_common::easynav_common
easynav_core::easynav_core
+ easynav_sensors::easynav_sensors
+ rclcpp::rclcpp
tf2_ros::tf2_ros
+ tf2::tf2
+ tf2_geometry_msgs::tf2_geometry_msgs
pluginlib::pluginlib
${geometry_msgs_TARGETS}
${nav_msgs_TARGETS}
@@ -66,8 +70,12 @@ pluginlib_export_plugin_description_file(easynav_core easynav_serest_controller_
ament_export_dependencies(
easynav_common
easynav_core
+ easynav_sensors
+ rclcpp
pluginlib
tf2_ros
+ tf2
+ tf2_geometry_msgs
geometry_msgs
nav_msgs
)
diff --git a/controllers/easynav_serest_controller/README.md b/controllers/easynav_serest_controller/README.md
index f5d6fa49..9546f28e 100644
--- a/controllers/easynav_serest_controller/README.md
+++ b/controllers/easynav_serest_controller/README.md
@@ -4,20 +4,26 @@
## Description
+
A SeReST (Smooth Error-Responsive Speed and Turning) controller for path tracking.
## Authors and Maintainers
+
- **Authors:** Intelligent Robotics Lab
- **Maintainers:** Francisco Martín Rico
## Supported ROS 2 Distributions
+
| Distribution | Status |
|---|---:|
+| humble |  |
+| jazzy |  |
| kilted |  |
| rolling |  |
| jazzy |  |
## Plugin (pluginlib)
+
- **Plugin Name:** `easynav_serest_controller/SerestController`
- **Type:** `easynav::SerestController`
- **Base Class:** `easynav::ControllerMethodBase`
@@ -25,8 +31,12 @@ A SeReST (Smooth Error-Responsive Speed and Turning) controller for path trackin
- **Description:** A SeReST (Smooth Error-Responsive Speed and Turning) controller for path tracking.
## Parameters
+
All parameters are declared under the plugin namespace, i.e., `//easynav_serest_controller/SerestController/...`.
+> This plugin derives from [`easynav::ControllerMethodBase`](https://github.com/EasyNavigation/EasyNavigation/tree/rolling/easynav_core#easynavcontrollermethodbase). \
+> See that section for shared collision-checking parameters and debug markers common to all controllers.
+
| Name | Type | Default | Description |
|---|---|---:|---|
| `.a_acc` | `double` | `0.8` | Comfortable forward acceleration (m/s²). |
@@ -68,17 +78,18 @@ All parameters are declared under the plugin namespace, i.e., `//easyn
| `.v_progress_min` | `double` | `0.05` | Minimum forward speed to ensure progress (m/s). |
| `.v_ref` | `double` | `0.6` | Nominal reference speed (m/s). |
-
## Interfaces (Topics and Services)
### Subscriptions and Publications
-This controller communicates through `NavState` (no direct ROS topics in this plugin).
+This controller communicates through `NavState` (no direct ROS topics in this plugin).
### Services
+
This package does not create service servers or clients.
## NavState Keys
+
| Key | Type | Access | Notes |
|---|---|---|---|
| `path` | `nav_msgs::msg::Path` | **Read** | Reference path. |
@@ -100,9 +111,10 @@ This package does not create service servers or clients.
| `serest.debug.goal.in_final_align` | `double/int` | **Write** | Debug metric |
| `serest.debug.goal.arrived` | `double/int` | **Write** | Debug metric |
-
## TF Frames
+
This controller reads pose from `nav_msgs/Odometry` (NavState key `robot_pose`). TF is not directly used in this plugin.
## License
-GPL-3.0-only
+
+Apache-2.0
diff --git a/controllers/easynav_serest_controller/include/easynav_serest_controller/SerestController.hpp b/controllers/easynav_serest_controller/include/easynav_serest_controller/SerestController.hpp
index 9a22d258..c854e88c 100644
--- a/controllers/easynav_serest_controller/include/easynav_serest_controller/SerestController.hpp
+++ b/controllers/easynav_serest_controller/include/easynav_serest_controller/SerestController.hpp
@@ -1,35 +1,28 @@
// Copyright 2025 Intelligent Robotics Lab
//
// This file is part of the project Easy Navigation (EasyNav in short)
-// licensed under the GNU General Public License v3.0.
-// See for details.
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
//
-// Easy Navigation program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
+// http://www.apache.org/licenses/LICENSE-2.0
//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
#ifndef EASYNAV_SEREST_CONTROLLER__SERESTCONTROLLER_HPP_
#define EASYNAV_SEREST_CONTROLLER__SERESTCONTROLLER_HPP_
-#include
#include
#include
-#include
-#include "rclcpp/rclcpp.hpp"
+#include "rclcpp/time.hpp"
#include "nav_msgs/msg/odometry.hpp"
#include "nav_msgs/msg/path.hpp"
#include "geometry_msgs/msg/twist_stamped.hpp"
-#include "tf2/utils.hpp"
#include "easynav_core/ControllerMethodBase.hpp"
#include "easynav_common/types/NavState.hpp"
@@ -65,9 +58,9 @@ class SerestController : public ControllerMethodBase
/**
* @brief Initialize parameters and internal state.
- * @return std::expected Empty on success; error message otherwise.
+ * @throws std::runtime_error on initialization error.
*/
- std::expected on_initialize() override;
+ void on_initialize() override;
/**
* @brief Real-time control update (called ~20–30 Hz).
@@ -123,7 +116,7 @@ class SerestController : public ControllerMethodBase
{
size_t seg_idx{0}; ///< Current segment index [i, i+1].
double s_star{0.0}; ///< Arc-length at the closest point.
- Vec2 closest{0.0, 0.0}; ///< Closest point coordinates on the path.
+ Vec2 closest{0.0, 0.0}; ///< Closest point coordinates on the path.
double t{0.0}; ///< Segment parameter in [0,1].
};
@@ -185,7 +178,7 @@ class SerestController : public ControllerMethodBase
* @return double Minimum obstacle distance (m). Infinity if none is found.
*/
double closest_obstacle_distance(
- const NavState & nav_state) const;
+ const NavState & nav_state);
/**
* @brief Compute a safe linear speed bound from obstacle distance and slope.
@@ -229,7 +222,7 @@ class SerestController : public ControllerMethodBase
void safety_limits(
const NavState & nav_state,
const RefKinematics & rk,
- double & d_closest, double & v_safe, double & v_curv) const;
+ double & d_closest, double & v_safe, double & v_curv);
// Aplica corner‑guard: ajusta v_prog_ref y obtiene omega_boost + término ápice.
void apply_corner_guard(
@@ -266,7 +259,7 @@ class SerestController : public ControllerMethodBase
// --- Parameters ---
/// @brief Allow reverse motion (if false, forward-only).
- bool allow_reverse_{false};
+ bool allow_reverse_{false};
/// @brief Minimal forward progress (m/s) when reasonably aligned.
double v_progress_min_{0.05};
/// @brief Max fraction of forward progress that -k_s * e_y can cancel.
@@ -328,7 +321,7 @@ class SerestController : public ControllerMethodBase
double final_align_wmax_{0.6};
/// @brief Enable corner-guard behavior in tight turns.
- bool corner_guard_enable_{true};
+ bool corner_guard_enable_{true};
/// @brief Corner-guard weight for outside lateral error (e_y_out) in speed reduction.
double corner_gain_ey_{1.5};
/// @brief Corner-guard weight for |e_theta| in speed reduction.
@@ -357,6 +350,8 @@ class SerestController : public ControllerMethodBase
double last_vrot_{0.0};
/// @brief Last update timestamp.
rclcpp::Time last_update_ts_;
+ /// @brief Last input timestamp (max of path and odom).
+ rclcpp::Time last_input_ts_;
/// @brief Output TwistStamped buffer.
geometry_msgs::msg::TwistStamped twist_stamped_;
};
diff --git a/controllers/easynav_serest_controller/package.xml b/controllers/easynav_serest_controller/package.xml
index 0e39b126..cd44e27d 100644
--- a/controllers/easynav_serest_controller/package.xml
+++ b/controllers/easynav_serest_controller/package.xml
@@ -2,16 +2,20 @@
easynav_serest_controller
- 0.0.2
+ 0.4.0
Easy Navigation: SeReST Controller package.
Francisco Martín Rico
- GPL-3.0-only
+ Apache-2.0
ament_cmake
easynav_common
easynav_core
+ easynav_sensors
+ rclcpp
pluginlib
+ tf2
+ tf2_geometry_msgs
tf2_ros
geometry_msgs
nav_msgs
diff --git a/controllers/easynav_serest_controller/src/easynav_serest_controller/SerestController.cpp b/controllers/easynav_serest_controller/src/easynav_serest_controller/SerestController.cpp
index 669a2d2d..30ea299b 100644
--- a/controllers/easynav_serest_controller/src/easynav_serest_controller/SerestController.cpp
+++ b/controllers/easynav_serest_controller/src/easynav_serest_controller/SerestController.cpp
@@ -1,21 +1,17 @@
// Copyright 2025 Intelligent Robotics Lab
//
// This file is part of the project Easy Navigation (EasyNav in short)
-// licensed under the GNU General Public License v3.0.
-// See for details.
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
//
-// Easy Navigation program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
+// http://www.apache.org/licenses/LICENSE-2.0
//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
/// \file
/// \brief Implementation of the SimpleController class.
@@ -24,8 +20,11 @@
#include
#include
-#include "easynav_common/types/Perceptions.hpp"
-#include "easynav_common/types/PointPerception.hpp"
+#include "tf2/utils.hpp"
+#include "tf2_geometry_msgs/tf2_geometry_msgs.hpp"
+
+#include "easynav_sensors/types/PointPerception.hpp"
+#include "easynav_common/RTTFBuffer.hpp"
#include "easynav_serest_controller/SerestController.hpp"
#include "pluginlib/class_list_macros.hpp"
@@ -37,13 +36,13 @@ namespace easynav
SerestController::SerestController() = default;
SerestController::~SerestController() = default;
-std::expected
+void
SerestController::on_initialize()
{
auto node = get_node();
const auto & ns = get_plugin_name();
- // Máximos y límites básicos
+ // Maximums and basic limits
node->declare_parameter(ns + ".allow_reverse", allow_reverse_);
node->declare_parameter(ns + ".v_progress_min", v_progress_min_);
node->declare_parameter(ns + ".k_s_share_max", k_s_share_max_);
@@ -53,7 +52,7 @@ SerestController::on_initialize()
node->declare_parameter(ns + ".max_linear_acc", max_linear_acc_);
node->declare_parameter(ns + ".max_angular_acc", max_angular_acc_);
- // Seguimiento
+ // Tracking
node->declare_parameter(ns + ".k_s", k_s_);
node->declare_parameter(ns + ".k_theta", k_theta_);
node->declare_parameter(ns + ".k_y", k_y_);
@@ -61,7 +60,7 @@ SerestController::on_initialize()
node->declare_parameter(ns + ".v_ref", v_ref_);
node->declare_parameter(ns + ".eps", eps_);
- // Seguridad
+ // Safety
node->declare_parameter(ns + ".a_acc", a_acc_);
node->declare_parameter(ns + ".a_brake", a_brake_);
node->declare_parameter(ns + ".a_lat_max", a_lat_max_);
@@ -70,7 +69,7 @@ SerestController::on_initialize()
node->declare_parameter(ns + ".d_hard", d_hard_);
node->declare_parameter(ns + ".t_emerg", t_emerg_);
- // Blend en vértices
+ // Blend at vertices
node->declare_parameter(ns + ".blend_base", blend_base_);
node->declare_parameter(ns + ".blend_k_per_v", blend_k_per_v_);
node->declare_parameter(ns + ".kappa_max", kappa_max_);
@@ -145,8 +144,6 @@ SerestController::on_initialize()
last_vlin_ = 0.0;
last_vrot_ = 0.0;
last_update_ts_ = node->now();
-
- return {};
}
SerestController::PathData
@@ -214,31 +211,31 @@ SerestController::ref_heading_and_curvature(
if (i + 1 >= pd.pts.size()) {return v2(1, 0);}
return normalize(pd.pts[i + 1] - pd.pts[i]);
};
- auto atan2dir = [](const Vec2 & t){return std::atan2(t.y, t.x);};
+ auto atan2dir = [](const Vec2 & t) {return std::atan2(t.y, t.x);};
- // Índices de segmentos relevante: i-1, i, i + 1
+ // Relevant segment indices: i-1, i, i + 1
const size_t i = prj.seg_idx;
const Vec2 Ti = seg_dir(i);
double psi_i = atan2dir(Ti);
- // Mezcla local con previsualización
+ // Local blend with look-ahead
double b = std::max(0.1, blend_base_ + blend_k_per_v_ * std::fabs(v_prev));
- // Determina si estamos cerca del comienzo o fin de un segmento
+ // Determine if we are near the beginning or end of a segment
double s_i = pd.s_acc[i];
double s_ip1 = pd.s_acc[i + 1];
double s = prj.s_star;
- // Por defecto: rumbo constante del segmento, kappa = 0
+ // Default: constant segment heading, kappa = 0
rk.psi_ref = psi_i;
rk.kappa_hat = 0.0;
- // Blend cerca del inicio del segmento i (con i-1)
+ // Blend near the beginning of segment i (with i-1)
if (i > 0 && (s - s_i) < b) {
Vec2 Tim1 = seg_dir(i - 1);
double psi_im1 = atan2dir(Tim1);
- double w = 1.0 / (1.0 + std::exp(-( (s - s_i) / b ))); // sigmoide en [s_i, s_i+b]
- // Interpolación circular de rumbos
+ double w = 1.0 / (1.0 + std::exp(-( (s - s_i) / b ))); // sigmoid in [s_i, s_i+b]
+ // Circular interpolation of headings
double sx = (1.0 - w) * std::cos(psi_im1) + w * std::cos(psi_i);
double sy = (1.0 - w) * std::sin(psi_im1) + w * std::sin(psi_i);
rk.psi_ref = std::atan2(sy, sx);
@@ -249,12 +246,12 @@ SerestController::ref_heading_and_curvature(
rk.kappa_hat = std::clamp(dpsi * sigma_prime, -kappa_max_, kappa_max_);
}
- // Blend cerca del final del segmento i (hacia i + 1)
+ // Blend near the end of segment i (towards i + 1)
if (i + 1 < pd.pts.size() - 1 && (s_ip1 - s) < b) {
Vec2 Tip1 = seg_dir(i + 1);
double psi_ip1 = atan2dir(Tip1);
- double w = 1.0 / (1.0 + std::exp(-( (s_ip1 - s) / b ))); // simétrico
- // Mezcla entre i y i + 1
+ double w = 1.0 / (1.0 + std::exp(-( (s_ip1 - s) / b ))); // symmetric
+ // Blend between i and i + 1
double sx = (1.0 - w) * std::cos(psi_i) + w * std::cos(psi_ip1);
double sy = (1.0 - w) * std::sin(psi_i) + w * std::sin(psi_ip1);
rk.psi_ref = std::atan2(sy, sx);
@@ -262,7 +259,7 @@ SerestController::ref_heading_and_curvature(
double dpsi = std::atan2(std::sin(psi_ip1 - psi_i), std::cos(psi_ip1 - psi_i));
double sigma_prime = (w * (1 - w)) / b;
double kappa2 = std::clamp(dpsi * sigma_prime, -kappa_max_, kappa_max_);
- // Si hay dos blends solapados, combinamos suavemente (promedio)
+ // If there are two overlapping blends, combine smoothly (average)
rk.kappa_hat = 0.5 * (rk.kappa_hat + kappa2);
}
@@ -275,40 +272,53 @@ SerestController::frenet_errors(
const Projection & prj, double psi_ref,
double & e_y, double & e_theta) const
{
- // Vector normal a la derecha de la tangente
+ // Normal vector to the right of the tangent
Vec2 T = v2(std::cos(psi_ref), std::sin(psi_ref));
- Vec2 N = v2(-T.y, T.x); // 90º a la izquierda (convención)
+ Vec2 N = v2(-T.y, T.x); // 90º to the left (convention)
Vec2 err = robot_xy - prj.closest;
- e_y = dot(err, N); // distancia lateral con signo
+ e_y = dot(err, N); // signed lateral distance
e_theta = std::atan2(std::sin(robot_yaw - psi_ref), std::cos(robot_yaw - psi_ref));
}
double
SerestController::closest_obstacle_distance(
- const NavState & nav_state) const
+ const NavState & nav_state)
{
- // 1) Preferir medición directa si existe
+ // 1) Prefer direct measurement if it exists
if (nav_state.has("closest_obstacle_distance")) {
try {
return nav_state.get