Browse Source

Merge pull request #832 from RobotecAI/merging_point-release_24092_to_main

Merge `point-release/24092` to `main` for release
Nicholas Lawson 5 months ago
parent
commit
335b308fdb
45 changed files with 1150 additions and 805 deletions
  1. 1 3
      Gems/ROS2/Code/CMakeLists.txt
  2. 12 1
      Gems/ROS2/Code/Include/ROS2/ROS2GemUtilities.h
  3. 6 5
      Gems/ROS2/Code/Include/ROS2/Utilities/Controllers/PidConfiguration.h
  4. 0 22
      Gems/ROS2/Code/PythonTests/CMakeLists.txt
  5. 0 19
      Gems/ROS2/Code/PythonTests/SmokeTests_Periodic.py
  6. 0 6
      Gems/ROS2/Code/PythonTests/__init__.py
  7. 0 114
      Gems/ROS2/Code/PythonTests/tests/SmokeTests_EnterGameModeWorks.py
  8. 19 1
      Gems/ROS2/Code/Source/Camera/CameraSensor.cpp
  9. 8 1
      Gems/ROS2/Code/Source/Camera/CameraSensor.h
  10. 2 0
      Gems/ROS2/Code/Source/Frame/Conversions/FrameConversion.py
  11. 6 18
      Gems/ROS2/Code/Source/Frame/ROS2FrameComponent.cpp
  12. 0 13
      Gems/ROS2/Code/Source/Manipulation/FollowJointTrajectoryActionServer.cpp
  13. 0 8
      Gems/ROS2/Code/Source/Manipulation/FollowJointTrajectoryActionServer.h
  14. 31 0
      Gems/ROS2/Code/Source/Manipulation/JointUtils.cpp
  15. 19 0
      Gems/ROS2/Code/Source/Manipulation/JointUtils.h
  16. 4 1
      Gems/ROS2/Code/Source/Manipulation/JointsManipulationEditorComponent.cpp
  17. 5 1
      Gems/ROS2/Code/Source/Manipulation/JointsPositionsEditorComponent.cpp
  18. 6 7
      Gems/ROS2/Code/Source/Manipulation/JointsTrajectoryComponent.cpp
  19. 1 1
      Gems/ROS2/Code/Source/Manipulation/JointsTrajectoryComponent.h
  20. 6 1
      Gems/ROS2/Code/Source/Odometry/ROS2OdometrySensorComponent.cpp
  21. 4 4
      Gems/ROS2/Code/Source/Odometry/ROS2OdometrySensorComponent.h
  22. 3 5
      Gems/ROS2/Code/Source/RobotImporter/RobotImporterWidget.cpp
  23. 2 3
      Gems/ROS2/Code/Source/RobotImporter/SDFormat/Hooks/ROS2SkidSteeringModelHook.cpp
  24. 1 0
      Gems/ROS2/Code/Source/Spawner/ROS2SpawnerComponent.cpp
  25. 48 3
      Gems/ROS2/Code/Source/Utilities/Controllers/PidConfiguration.cpp
  26. 195 0
      Gems/ROS2/Code/Tests/PIDTest.cpp
  27. 2 0
      Gems/ROS2/Code/ros2_editor_files.cmake
  28. 1 0
      Gems/ROS2/Code/ros2_tests_files.cmake
  29. 3 3
      Gems/ROS2/gem.json
  30. 1 1
      Gems/RosRobotSample/Assets/ROSbot.prefab
  31. 1 0
      Templates/Ros2FleetRobotTemplate/Template/Gem/gem.json
  32. 2 2
      Templates/Ros2FleetRobotTemplate/template.json
  33. 0 114
      Templates/Ros2ProjectTemplate/Docker/Dockerfile
  34. 0 15
      Templates/Ros2ProjectTemplate/Docker/LaunchNavStack.bash
  35. 0 22
      Templates/Ros2ProjectTemplate/Docker/LaunchSimulation.bash
  36. 0 119
      Templates/Ros2ProjectTemplate/Docker/README.md
  37. 0 43
      Templates/Ros2ProjectTemplate/Docker/cleanup.bash
  38. 1 0
      Templates/Ros2ProjectTemplate/Template/Gem/gem.json
  39. 1 1
      Templates/Ros2ProjectTemplate/Template/Levels/DemoLevel/DemoLevel.prefab
  40. 9 0
      Templates/Ros2ProjectTemplate/Template/Registry/load_level.setreg
  41. 0 1
      Templates/Ros2ProjectTemplate/Template/autoexec.cfg
  42. 2 6
      Templates/Ros2ProjectTemplate/template.json
  43. 1 0
      Templates/Ros2RoboticManipulationTemplate/Template/Gem/gem.json
  44. 2 2
      Templates/Ros2RoboticManipulationTemplate/template.json
  45. 745 239
      repo.json

+ 1 - 3
Gems/ROS2/Code/CMakeLists.txt

@@ -70,8 +70,7 @@ ly_add_target(
             Gem::LmbrCentral.API
 )
 
-target_depends_on_ros2_packages(${gem_name}.Static rclcpp builtin_interfaces std_msgs sensor_msgs nav_msgs tf2_ros ackermann_msgs gazebo_msgs)
-target_depends_on_ros2_package(${gem_name}.Static control_toolbox 2.2.0 REQUIRED)
+target_depends_on_ros2_packages(${gem_name}.Static rclcpp builtin_interfaces std_msgs sensor_msgs nav_msgs tf2_ros ackermann_msgs gazebo_msgs control_msgs)
 
 ly_add_target(
     NAME ${gem_name}.API HEADERONLY
@@ -256,4 +255,3 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
     endif()
 endif()
 
-add_subdirectory(PythonTests)

+ 12 - 1
Gems/ROS2/Code/Include/ROS2/ROS2GemUtilities.h

@@ -9,13 +9,25 @@
 
 #include <AzCore/Component/ComponentBus.h>
 #include <AzCore/Component/Entity.h>
+#include <AzCore/Component/EntityUtils.h>
 #ifdef ROS2_EDITOR
 #include <AzToolsFramework/ToolsComponents/GenericComponentWrapper.h>
 #endif
+
 namespace ROS2
 {
     namespace Utils
     {
+        //! Checks whether the entity has a component of the given type
+        //! @param entity pointer to entity
+        //! @param typeId type of the component
+        //! @returns true if entity has component with given type
+        inline bool HasComponentOfType(const AZ::Entity* entity, const AZ::Uuid typeId)
+        {
+            auto components = AZ::EntityUtils::FindDerivedComponents(entity, typeId);
+            return !components.empty();
+        }
+
         /// Create component for a given entity in safe way.
         /// @param entityId entity that will own component
         /// @param componentType Uuid of component to create
@@ -26,7 +38,6 @@ namespace ROS2
         /// We should use that that we are not sure if we access eg ROS2FrameComponent in game mode or from Editor
         /// @param entity pointer to entity eg with GetEntity()
         /// @return pointer to component with type T
-
         template<class ComponentType>
         ComponentType* GetGameOrEditorComponent(const AZ::Entity* entity)
         {

+ 6 - 5
Gems/ROS2/Code/Include/ROS2/Utilities/Controllers/PidConfiguration.h

@@ -8,11 +8,11 @@
 #pragma once
 
 #include <AzCore/Serialization/SerializeContext.h>
-#include <control_toolbox/pid.hpp>
 
 namespace ROS2::Controllers
 {
-    //! A wrapper for ROS 2 control_toolbox pid controller.
+    //! A PID controller.
+    //! Based on a ROS 2 control_toolbox implementation.
     //! @see <a href="https://github.com/ros-controls/control_toolbox">control_toolbox</a>.
     class PidConfiguration
     {
@@ -38,7 +38,7 @@ namespace ROS2::Controllers
             const bool antiWindup,
             const double outputLimit);
 
-        //! Initialize PID using member fields as set by the user.
+        //! Initialize the controller
         void InitializePid();
 
         //! Compute the value of PID command.
@@ -54,8 +54,9 @@ namespace ROS2::Controllers
         double m_iMax = 10.0; //!< maximal allowable integral term.
         double m_iMin = -10.0; //!< minimal allowable integral term.
         bool m_antiWindup = false; //!< prevents condition of integrator overflow in integral action.
+        bool m_initialized = false; //!< is PID initialized.
         double m_outputLimit = 0.0; //!< limit PID output; set to 0.0 to disable.
-
-        control_toolbox::Pid m_pid; //!< PID implementation object from control_toolbox (of ros2_control).
+        double m_previousError = 0.0; //!< previous recorded error.
+        double m_integral = 0.0; //!< integral accumulator.
     };
 } // namespace ROS2::Controllers

+ 0 - 22
Gems/ROS2/Code/PythonTests/CMakeLists.txt

@@ -1,22 +0,0 @@
-# Copyright (c) Contributors to the Open 3D Engine Project.
-# For complete copyright and license terms please see the LICENSE at the root of this distribution.
-#
-# SPDX-License-Identifier: Apache-2.0 OR MIT
-
-if (PAL_TRAIT_BUILD_TESTS_SUPPORTED)
-    if (PAL_TRAIT_TEST_PYTEST_SUPPORTED)
-        # PAL_TRAIT_BUILD macros are used by platform detection.
-        ly_add_pytest(
-                NAME Gem::${gem_name}.PythonTests
-                TEST_SUITE smoke
-                TEST_SERIAL
-                PATH ${CMAKE_CURRENT_LIST_DIR}/SmokeTests_Periodic.py
-                RUNTIME_DEPENDENCIES
-                    Legacy::Editor
-                    AZ::AssetProcessor
-                    ${gem_name}.Editor
-                COMPONENT
-                    SmokeTests
-        )
-    endif ()
-endif ()

+ 0 - 19
Gems/ROS2/Code/PythonTests/SmokeTests_Periodic.py

@@ -1,19 +0,0 @@
-#
-# Copyright (c) Contributors to the Open 3D Engine Project.
-# For complete copyright and license terms please see the LICENSE at the root of this distribution.
-#
-# SPDX-License-Identifier: Apache-2.0 OR MIT
-#
-
-import pytest
-from ly_test_tools.o3de.editor_test import EditorTestSuite, EditorSingleTest
-
-
[email protected]_periodic  # Marks the test suite as being part of a Periodic test suite
[email protected]("launcher_platform", ['linux_editor'])  # This test works on Linux editor
[email protected]("project", ["AutomatedTesting"])  # Use the AutomatedTesting project
-class TestAutomation(EditorTestSuite):
-    # Declaring a class that extends from EditorSingleTest declares a single test.
-    class SmokeTests_EnterGameModeWorks(EditorSingleTest):
-        # This runs a Single Test in a single Editor. For further work check EditorSingleTest siblings
-        from .tests import SmokeTests_EnterGameModeWorks as test_module

+ 0 - 6
Gems/ROS2/Code/PythonTests/__init__.py

@@ -1,6 +0,0 @@
-"""
-Copyright (c) Contributors to the Open 3D Engine Project.
-For complete copyright and license terms please see the LICENSE at the root of this distribution.
-
-SPDX-License-Identifier: Apache-2.0 OR MIT
-"""

+ 0 - 114
Gems/ROS2/Code/PythonTests/tests/SmokeTests_EnterGameModeWorks.py

@@ -1,114 +0,0 @@
-#
-# Copyright (c) Contributors to the Open 3D Engine Project.
-# For complete copyright and license terms please see the LICENSE at the root of this distribution.
-#
-# SPDX-License-Identifier: Apache-2.0 OR MIT
-#
-
-# Test Case Title : Check that entering into game mode works
-
-# List of results that we want to check, this is not 100% necessary but its a good
-# practice to make it easier to debug tests.
-# Here we define a tuple of tests
-
-# Paths to ROS rclpy are added directly as pytest is not accessing PYTHONPATH environment variable
-import sys
-
-sys.path.append('/opt/ros/humble/lib/python3.10/site-packages')
-sys.path.append('/opt/ros/humble/local/lib/python3.10/dist-packages')
-
-
-class Tests:
-    enter_game_mode = ("Entered game mode", "Failed to enter game mode")
-    topics_published = ("Topics were published during game mode", "Failed to publish topics during game mode")
-    topics_not_published_outside_of_game_mode = (
-        "All simulation topics closed", "Failed to close publishers after simulation")
-
-
-def check_result(result, msg):
-    from editor_python_test_tools.utils import Report
-    if not result:
-        Report.result(msg, False)
-        raise Exception(msg + " : FAILED")
-
-
-def check_topics():
-    import rclpy
-    from rclpy.node import Node
-
-    def get_topic_list():
-        node_dummy = Node("_ros2cli_dummy_to_show_topic_list")
-        result = node_dummy.get_topic_names_and_types()
-        node_dummy.destroy_node()
-        return result
-
-    topics = []
-    rclpy.init()
-    topic_list = get_topic_list()
-
-    for info in topic_list:
-        topics.append(info[0])
-    rclpy.shutdown()
-    return topics
-
-
-def SmokeTest_EnterGameModeWorks():
-    # Description: This test checks that entering into game mode works by opening an empty level
-    # and entering into the game mode. The is in game mode state should be changed after doing it
-    # Next it checks if there are some additional ROS topics published during the game mode
-
-    # Import report and test helper utilities
-    from editor_python_test_tools.utils import Report
-    from editor_python_test_tools.utils import TestHelper as helper
-    # All exposed python bindings are in azlmbr
-    import azlmbr.legacy.general as general
-
-    # Required for automated tests
-    helper.init_idle()
-
-    # Open the level called "DefaultLevel".
-    # We use a DefaultLevel level for a smoke test.
-    # ROS2 System Component should publish topics listed below regardless of level
-    # - /tf
-    # - /tf_static
-    # - /clock
-    helper.open_level(level="DefaultLevel", directory='')
-
-    topics_before_game_mode = check_topics()
-
-    # Using the exposed Python API from editor in CryEditPy.py we can enter into game mode this way
-    general.enter_game_mode()
-
-    # The script drives the execution of the test, to return the flow back to the editor,
-    # we will tick it one time
-    general.idle_wait_frames(1)
-
-    # Now we can use the Report.result() to report the state of a result
-    # if the second argument is false, it will mark this test as failed, however it will keep going.
-    Report.result(Tests.enter_game_mode, general.is_in_game_mode())
-
-    topics_in_game_mode = check_topics()
-
-    Report.result(Tests.topics_published, len(topics_in_game_mode) > len(topics_before_game_mode))
-
-    # Instead of using Report.result(), you can also use:
-    # assert is_in_game_mode, "Didn't enter into game mode"
-    # However this would stop the test at this point and not report anything when it succeeds
-
-    # The test will end at this point, is good practice to exit game mode or reset any changed stated
-    # *DO NOT* close the editor, the editor will close automatically and report the error code
-    general.exit_game_mode()
-
-    # this line is needed to update the simulation state
-    general.idle_wait_frames(1)
-
-    topics_after_game_mode = check_topics()
-    Report.result(Tests.topics_not_published_outside_of_game_mode,
-                  len(topics_after_game_mode) == len(topics_before_game_mode))
-
-
-if __name__ == "__main__":
-    # This utility starts up the test and sets up the state for knowing what test is currently being run
-    from editor_python_test_tools.utils import Report
-
-    Report.start_test(SmokeTest_EnterGameModeWorks)

+ 19 - 1
Gems/ROS2/Code/Source/Camera/CameraSensor.cpp

@@ -158,15 +158,33 @@ namespace ROS2
         m_passHierarchy.push_back("CopyToSwapChain");
 
         m_pipeline->SetDefaultView(m_view);
-        const AZ::RPI::ViewPtr targetView = m_scene->GetDefaultRenderPipeline()->GetDefaultView();
+        UpdateViewAlias();
+
+        Camera::CameraNotificationBus::Handler::BusConnect();
+    }
+
+    void CameraSensor::OnCameraRemoved(const AZ::EntityId& cameraEntityId)
+    {
+        UpdateViewAlias();
+    }
+
+    void CameraSensor::OnActiveViewChanged(const AZ::EntityId& cameraEntityId)
+    {
+        UpdateViewAlias();
+    }
+
+    void CameraSensor::UpdateViewAlias()
+    {
         if (auto* fp = m_scene->GetFeatureProcessor<AZ::Render::PostProcessFeatureProcessor>())
         {
+            const AZ::RPI::ViewPtr targetView = m_scene->GetDefaultRenderPipeline()->GetDefaultView();
             fp->SetViewAlias(m_view, targetView);
         }
     }
 
     CameraSensor::~CameraSensor()
     {
+        Camera::CameraNotificationBus::Handler::BusDisconnect();
         if (m_scene)
         {
             if (auto* fp = m_scene->GetFeatureProcessor<AZ::Render::PostProcessFeatureProcessor>())

+ 8 - 1
Gems/ROS2/Code/Source/Camera/CameraSensor.h

@@ -10,6 +10,7 @@
 #include "CameraPublishers.h"
 #include <Atom/Feature/Utils/FrameCaptureBus.h>
 #include <AzCore/std/containers/span.h>
+#include <AzFramework/Components/CameraBus.h>
 #include <ROS2/ROS2GemUtilities.h>
 #include <rclcpp/publisher.hpp>
 #include <sensor_msgs/msg/camera_info.hpp>
@@ -20,7 +21,7 @@ namespace ROS2
 {
     //! Class to create camera sensor using Atom renderer
     //! It creates dedicated rendering pipeline for each camera
-    class CameraSensor
+    class CameraSensor : public Camera::CameraNotificationBus::Handler
     {
     public:
         //! Initializes rendering pipeline for the camera sensor.
@@ -40,6 +41,12 @@ namespace ROS2
         [[nodiscard]] const CameraSensorDescription& GetCameraSensorDescription() const;
 
     private:
+        // CameraNotificationBus overrides
+        void OnCameraRemoved(const AZ::EntityId& cameraEntityId) override;
+        void OnActiveViewChanged(const AZ::EntityId& cameraEntityId) override;
+
+        void UpdateViewAlias();
+
         AZStd::vector<AZStd::string> m_passHierarchy;
         AZ::RPI::ViewPtr m_view;
         AZ::RPI::Scene* m_scene = nullptr;

+ 2 - 0
Gems/ROS2/Code/Source/Frame/Conversions/FrameConversion.py

@@ -47,6 +47,8 @@ def find_and_replace(data):
                     "m_template/Joint Name"
                 ]):
                     item["path"] = item["path"].replace("m_template", "ROS2FrameConfiguration")
+                if item["op"] == "remove" and "ROS2FrameComponent" in item["path"]:
+                    item["path"] = item["path"].replace("ROS2FrameComponent", "ROS2FrameEditorComponent")
 
             for key, value in item.items():
                 search_for_components(value, foundComponents, insidePatches)

+ 6 - 18
Gems/ROS2/Code/Source/Frame/ROS2FrameComponent.cpp

@@ -8,7 +8,6 @@
 
 #include "ROS2FrameSystemComponent.h"
 #include <AzCore/Component/Entity.h>
-#include <AzCore/Component/EntityUtils.h>
 #include <AzCore/RTTI/ReflectContext.h>
 #include <AzCore/Serialization/EditContext.h>
 #include <AzCore/Serialization/EditContextConstants.inl>
@@ -21,6 +20,9 @@
 #include <ROS2/ROS2Bus.h>
 #include <ROS2/ROS2GemUtilities.h>
 #include <ROS2/Utilities/ROS2Names.h>
+#include <Source/ArticulationLinkComponent.h>
+#include <Source/FixedJointComponent.h>
+#include <Source/JointComponent.h>
 #include <rapidjson/document.h>
 #include <rapidjson/stringbuffer.h>
 
@@ -68,17 +70,6 @@ namespace ROS2
             // Found the component!
             return component;
         }
-
-        //! Checks whether the entity has a component of the given type
-        //! @param entity pointer to entity
-        //! @param typeId type of the component
-        //! @returns true if entity has component with given type
-        static bool CheckIfEntityHasComponentOfType(const AZ::Entity* entity, const AZ::Uuid typeId)
-        {
-            auto components = AZ::EntityUtils::FindDerivedComponents(entity, typeId);
-            return !components.empty();
-        }
-
     } // namespace Internal
 
     AZ::JsonSerializationResult::Result JsonFrameComponentConfigSerializer::Load(
@@ -160,12 +151,9 @@ namespace ROS2
             // Otherwise it'll be dynamic when it has joints and it's not a fixed joint.
             else
             {
-                const bool hasJoints = Internal::CheckIfEntityHasComponentOfType(
-                    m_entity, AZ::Uuid("{B01FD1D2-1D91-438D-874A-BF5EB7E919A8}")); // Physx::JointComponent;
-                const bool hasFixedJoints = Internal::CheckIfEntityHasComponentOfType(
-                    m_entity, AZ::Uuid("{02E6C633-8F44-4CEE-AE94-DCB06DE36422}")); // Physx::FixedJointComponent
-                const bool hasArticulations = Internal::CheckIfEntityHasComponentOfType(
-                    m_entity, AZ::Uuid("{48751E98-B35F-4A2F-A908-D9CDD5230264}")); // Physx::ArticulationComponent
+                const bool hasJoints = Utils::HasComponentOfType(m_entity, PhysX::JointComponent::TYPEINFO_Uuid());
+                const bool hasFixedJoints = Utils::HasComponentOfType(m_entity, PhysX::FixedJointComponent::TYPEINFO_Uuid());
+                const bool hasArticulations = Utils::HasComponentOfType(m_entity, PhysX::ArticulationLinkComponent::TYPEINFO_Uuid());
                 m_isDynamic = (hasJoints && !hasFixedJoints) || hasArticulations;
             }
 

+ 0 - 13
Gems/ROS2/Code/Source/Manipulation/FollowJointTrajectoryActionServer.cpp

@@ -23,16 +23,6 @@ namespace ROS2
             AZStd::bind(&FollowJointTrajectoryActionServer::GoalAcceptedCallback, this, AZStd::placeholders::_1));
     }
 
-    JointsTrajectoryRequests::TrajectoryActionStatus FollowJointTrajectoryActionServer::GetGoalStatus() const
-    {
-        return m_goalStatus;
-    }
-
-    void FollowJointTrajectoryActionServer::SetGoalSuccess()
-    {
-        m_goalStatus = JointsTrajectoryRequests::TrajectoryActionStatus::Succeeded;
-    }
-
     void FollowJointTrajectoryActionServer::CancelGoal(std::shared_ptr<FollowJointTrajectory::Result> result)
     {
         AZ_Assert(m_goalHandle, "Invalid goal handle!");
@@ -50,7 +40,6 @@ namespace ROS2
         {
             AZ_Trace("FollowJointTrajectoryActionServer", "Goal succeeded\n");
             m_goalHandle->succeed(result);
-            m_goalStatus = JointsTrajectoryRequests::TrajectoryActionStatus::Succeeded;
         }
     }
 
@@ -102,7 +91,6 @@ namespace ROS2
             return rclcpp_action::CancelResponse::REJECT;
         }
 
-        m_goalStatus = JointsTrajectoryRequests::TrajectoryActionStatus::Cancelled;
         return rclcpp_action::CancelResponse::ACCEPT;
     }
 
@@ -149,6 +137,5 @@ namespace ROS2
 
         m_goalHandle = goalHandle;
         // m_goalHandle->execute(); // No need to call this, as we are already executing the goal due to ACCEPT_AND_EXECUTE
-        m_goalStatus = JointsTrajectoryRequests::TrajectoryActionStatus::Executing;
     }
 } // namespace ROS2

+ 0 - 8
Gems/ROS2/Code/Source/Manipulation/FollowJointTrajectoryActionServer.h

@@ -32,17 +32,10 @@ namespace ROS2
         //! server documentation </a>
         FollowJointTrajectoryActionServer(const AZStd::string& actionName, const AZ::EntityId& entityId);
 
-        //! Return trajectory action status.
-        //! @return Status of the trajectory execution.
-        JointsTrajectoryRequests::TrajectoryActionStatus GetGoalStatus() const;
-
         //! Cancel the current goal.
         //! @param result Result to be passed to through action server to the client.
         void CancelGoal(std::shared_ptr<FollowJointTrajectory::Result> result);
 
-        //! Sets the goal status to success
-        void SetGoalSuccess();
-
         //! Report goal success to the action server.
         //! @param result Result which contains success code.
         void GoalSuccess(std::shared_ptr<FollowJointTrajectory::Result> result);
@@ -56,7 +49,6 @@ namespace ROS2
         using TrajectoryActionStatus = JointsTrajectoryRequests::TrajectoryActionStatus;
 
         AZ::EntityId m_entityId;
-        TrajectoryActionStatus m_goalStatus = TrajectoryActionStatus::Idle;
         rclcpp_action::Server<FollowJointTrajectory>::SharedPtr m_actionServer;
         std::shared_ptr<GoalHandle> m_goalHandle;
 

+ 31 - 0
Gems/ROS2/Code/Source/Manipulation/JointUtils.cpp

@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include "JointUtils.h"
+
+#include <ROS2/ROS2GemUtilities.h>
+#include <Source/EditorArticulationLinkComponent.h>
+#include <Source/EditorBallJointComponent.h>
+#include <Source/EditorFixedJointComponent.h>
+#include <Source/EditorHingeJointComponent.h>
+#include <Source/EditorPrismaticJointComponent.h>
+
+namespace ROS2::JointUtils
+{
+
+    bool HasNonFixedJoints(const AZ::Entity* entity)
+    {
+        const bool hasPrismaticJoints = Utils::HasComponentOfType(entity, PhysX::EditorPrismaticJointComponent::TYPEINFO_Uuid());
+        const bool hasBallJoints = Utils::HasComponentOfType(entity, PhysX::EditorBallJointComponent::TYPEINFO_Uuid());
+        const bool hasHingeJoints = Utils::HasComponentOfType(entity, PhysX::EditorHingeJointComponent::TYPEINFO_Uuid());
+        const bool hasArticulations = Utils::HasComponentOfType(entity, PhysX::EditorArticulationLinkComponent::TYPEINFO_Uuid());
+        const bool hasJoints = hasPrismaticJoints || hasBallJoints || hasHingeJoints || hasArticulations;
+
+        return hasJoints;
+    }
+} // namespace ROS2::Utils

+ 19 - 0
Gems/ROS2/Code/Source/Manipulation/JointUtils.h

@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <AzCore/Component/Entity.h>
+
+namespace ROS2::JointUtils
+{
+    //! Check if the entity has any of the non fixed joint or articulation components.
+    //! @param entity Entity to check.
+    //! @return True if the entity has any of the joint or articulation components.
+    bool HasNonFixedJoints(const AZ::Entity* entity);
+} // namespace ROS2::Utils

+ 4 - 1
Gems/ROS2/Code/Source/Manipulation/JointsManipulationEditorComponent.cpp

@@ -7,6 +7,7 @@
  */
 
 #include "JointsManipulationEditorComponent.h"
+#include "JointUtils.h"
 #include "JointsManipulationComponent.h"
 #include <AzCore/Component/ComponentApplicationBus.h>
 #include <AzCore/Component/TransformBus.h>
@@ -108,8 +109,10 @@ namespace ROS2
                 azrtti_cast<ROS2::ROS2FrameEditorComponent*>(Utils::GetGameOrEditorComponent<ROS2::ROS2FrameEditorComponent>(entity));
             AZ_Assert(frameEditorComponent, "ROS2FrameEditorComponent does not exist!");
 
+            const bool hasNonFixedJoints = JointUtils::HasNonFixedJoints(entity);
+
             AZStd::string jointName(frameEditorComponent->GetJointName().GetCStr());
-            if (!jointName.empty())
+            if (!jointName.empty() && hasNonFixedJoints)
             {
                 m_initialPositions.emplace_back(AZStd::make_pair(jointName, configBackup[jointName]));
             }

+ 5 - 1
Gems/ROS2/Code/Source/Manipulation/JointsPositionsEditorComponent.cpp

@@ -9,6 +9,7 @@
 
 #include "JointsPositionsEditorComponent.h"
 #include "JointPositionsSubscriptionHandler.h"
+#include "JointUtils.h"
 #include "JointsPositionsComponent.h"
 
 #include <AzCore/Serialization/EditContext.h>
@@ -76,14 +77,17 @@ namespace ROS2
 
     AZ::Crc32 JointsPositionsEditorComponent::FindAllJoints()
     {
+        m_jointNames.clear();
         AZStd::function<void(const AZ::Entity* entity)> getAllJointsHierarchy = [&](const AZ::Entity* entity)
         {
             auto* frameEditorComponent =
                 azrtti_cast<ROS2::ROS2FrameEditorComponent*>(Utils::GetGameOrEditorComponent<ROS2::ROS2FrameEditorComponent>(entity));
             AZ_Assert(frameEditorComponent, "ROS2FrameEditorComponent does not exist!");
 
+            const bool hasNonFixedJoints = JointUtils::HasNonFixedJoints(entity);
+
             AZStd::string jointName(frameEditorComponent->GetJointName().GetCStr());
-            if (!jointName.empty())
+            if (!jointName.empty() && hasNonFixedJoints)
             {
                 m_jointNames.emplace_back(jointName);
             }

+ 6 - 7
Gems/ROS2/Code/Source/Manipulation/JointsTrajectoryComponent.cpp

@@ -12,8 +12,8 @@
 #include <ROS2/Frame/ROS2FrameComponent.h>
 #include <ROS2/Manipulation/JointsManipulationRequests.h>
 #include <ROS2/ROS2Bus.h>
-#include <ROS2/Utilities/ROS2Names.h>
 #include <ROS2/Utilities/ROS2Conversions.h>
+#include <ROS2/Utilities/ROS2Names.h>
 
 namespace ROS2
 {
@@ -92,7 +92,7 @@ namespace ROS2
     AZ::Outcome<void, JointsTrajectoryComponent::TrajectoryResult> JointsTrajectoryComponent::StartTrajectoryGoal(
         TrajectoryGoalPtr trajectoryGoal)
     {
-        if (m_trajectoryInProgress)
+        if (m_goalStatus == JointsTrajectoryRequests::TrajectoryActionStatus::Executing)
         {
             auto result = JointsTrajectoryComponent::TrajectoryResult();
             result.error_code = JointsTrajectoryComponent::TrajectoryResult::INVALID_GOAL;
@@ -107,7 +107,7 @@ namespace ROS2
         }
         m_trajectoryGoal = *trajectoryGoal;
         m_trajectoryExecutionStartTime = rclcpp::Time(ROS2::ROS2Interface::Get()->GetROSTimestamp());
-        m_trajectoryInProgress = true;
+        m_goalStatus = JointsTrajectoryRequests::TrajectoryActionStatus::Executing;
         return AZ::Success();
     }
 
@@ -186,13 +186,13 @@ namespace ROS2
     AZ::Outcome<void, AZStd::string> JointsTrajectoryComponent::CancelTrajectoryGoal()
     {
         m_trajectoryGoal.trajectory.points.clear();
-        m_trajectoryInProgress = false;
+        m_goalStatus = JointsTrajectoryRequests::TrajectoryActionStatus::Cancelled;
         return AZ::Success();
     }
 
     JointsTrajectoryRequests::TrajectoryActionStatus JointsTrajectoryComponent::GetGoalStatus()
     {
-        return m_followTrajectoryServer->GetGoalStatus();
+        return m_goalStatus;
     }
 
     void JointsTrajectoryComponent::FollowTrajectory(const uint64_t deltaTimeNs)
@@ -205,7 +205,6 @@ namespace ROS2
             result->error_string = "User Cancelled";
             result->error_code = FollowJointTrajectoryActionServer::FollowJointTrajectory::Result::SUCCESSFUL;
             m_followTrajectoryServer->CancelGoal(result);
-            m_followTrajectoryServer->SetGoalSuccess();
             return;
         }
 
@@ -219,7 +218,7 @@ namespace ROS2
             AZ_TracePrintf("JointsManipulationComponent", "Goal Concluded: all points reached\n");
             auto successResult = std::make_shared<control_msgs::action::FollowJointTrajectory::Result>(); //!< Empty defaults to success.
             m_followTrajectoryServer->GoalSuccess(successResult);
-            m_trajectoryInProgress = false;
+            m_goalStatus = JointsTrajectoryRequests::TrajectoryActionStatus::Succeeded;
             return;
         }
 

+ 1 - 1
Gems/ROS2/Code/Source/Manipulation/JointsTrajectoryComponent.h

@@ -64,9 +64,9 @@ namespace ROS2
         AZStd::string m_followTrajectoryActionName{ "arm_controller/follow_joint_trajectory" };
         AZStd::unique_ptr<FollowJointTrajectoryActionServer> m_followTrajectoryServer;
         TrajectoryGoal m_trajectoryGoal;
+        TrajectoryActionStatus m_goalStatus = TrajectoryActionStatus::Idle;
         rclcpp::Time m_trajectoryExecutionStartTime;
         ManipulationJoints m_manipulationJoints;
-        bool m_trajectoryInProgress{ false };
         builtin_interfaces::msg::Time m_lastTickTimestamp; //!< ROS 2 Timestamp during last OnTick call
     };
 } // namespace ROS2

+ 6 - 1
Gems/ROS2/Code/Source/Odometry/ROS2OdometrySensorComponent.cpp

@@ -6,10 +6,10 @@
  *
  */
 
+#include "ROS2OdometrySensorComponent.h"
 #include <AzFramework/Physics/PhysicsScene.h>
 #include <AzFramework/Physics/RigidBodyBus.h>
 #include <AzFramework/Physics/SimulatedBodies/RigidBody.h>
-#include "ROS2OdometrySensorComponent.h"
 #include <ROS2/Utilities/ROS2Conversions.h>
 #include <ROS2/Utilities/ROS2Names.h>
 
@@ -55,6 +55,11 @@ namespace ROS2
         required.push_back(AZ_CRC_CE("ROS2Frame"));
     }
 
+    void ROS2OdometrySensorComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible)
+    {
+        incompatible.push_back(AZ_CRC("ArticulationLinkService"));
+    }
+
     void ROS2OdometrySensorComponent::OnOdometryEvent(AzPhysics::SceneHandle sceneHandle)
     {
         if (m_bodyHandle == AzPhysics::InvalidSimulatedBodyHandle)

+ 4 - 4
Gems/ROS2/Code/Source/Odometry/ROS2OdometrySensorComponent.h

@@ -10,10 +10,10 @@
 #include <AzCore/Math/Quaternion.h>
 #include <AzCore/Math/Transform.h>
 #include <AzCore/Math/Vector3.h>
-#include <nav_msgs/msg/odometry.hpp>
-#include <rclcpp/publisher.hpp>
 #include <ROS2/Sensor/Events/PhysicsBasedSource.h>
 #include <ROS2/Sensor/ROS2SensorComponentBase.h>
+#include <nav_msgs/msg/odometry.hpp>
+#include <rclcpp/publisher.hpp>
 
 namespace ROS2
 {
@@ -21,14 +21,14 @@ namespace ROS2
     //! It constructs and publishes an odometry message, which contains information about vehicle velocity and position in space.
     //! This is a ground truth "sensor", which can be helpful for development and machine learning.
     //! @see <a href="https://index.ros.org/p/nav_msgs/"> nav_msgs package. </a>
-    class ROS2OdometrySensorComponent
-        : public ROS2SensorComponentBase<PhysicsBasedSource>
+    class ROS2OdometrySensorComponent : public ROS2SensorComponentBase<PhysicsBasedSource>
     {
     public:
         AZ_COMPONENT(ROS2OdometrySensorComponent, "{61387448-63AA-4563-AF87-60C72B05B863}", SensorBaseType);
         ROS2OdometrySensorComponent();
         ~ROS2OdometrySensorComponent() = default;
         static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required);
+        static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible);
         static void Reflect(AZ::ReflectContext* context);
         //////////////////////////////////////////////////////////////////////////
         // Component overrides

+ 3 - 5
Gems/ROS2/Code/Source/RobotImporter/RobotImporterWidget.cpp

@@ -656,7 +656,6 @@ namespace ROS2
     {
         const auto filePath = m_robotDescriptionPage->GetModifiedUrdfName();
         const auto& streamData = m_modifiedUrdfWindow->GetUrdfData();
-        bool success = false;
         AZ::IO::FileIOBase* fileIo = AZ::IO::FileIOBase::GetInstance();
         AZ::IO::FixedMaxPathString resolvedPath;
         if (fileIo == nullptr || !fileIo->ResolvePath(filePath.c_str(), resolvedPath.data(), resolvedPath.capacity() + 1))
@@ -667,11 +666,10 @@ namespace ROS2
                 resolvedPath.c_str(),
                 AZ::IO::SystemFile::SF_OPEN_CREATE | AZ::IO::SystemFile::SF_OPEN_CREATE_PATH | AZ::IO::SystemFile::SF_OPEN_WRITE_ONLY))
         {
-            AZ::IO::SizeType bytesWritten = fileHandle.Write(streamData.data(), streamData.size());
-            success = (bytesWritten == streamData.size());
+            [[maybe_unused]] const AZ::IO::SizeType bytesWritten = fileHandle.Write(streamData.data(), streamData.size());
+            AZ_Warning(
+                "onSaveModifiedUrdfPressed", (bytesWritten == streamData.size()), "Cannot save the output file %s", filePath.c_str());
         }
-
-        AZ_Warning("onSaveModifiedUrdfPressed", success, "Cannot save the output file %s", filePath.c_str());
     }
 
     void RobotImporterWidget::onShowModifiedUrdfPressed()

+ 2 - 3
Gems/ROS2/Code/Source/RobotImporter/SDFormat/Hooks/ROS2SkidSteeringModelHook.cpp

@@ -103,7 +103,6 @@ namespace ROS2::SDFormat
             if (element->HasElement("wheel_separation") && element->HasElement("wheel_diameter"))
             {
                 // ROS 2 version of either libgazebo_ros_skid_steer_drive.so or libgazebo_ros_diff_drive.so
-                const auto wheelPairs = element->Get<int>("num_wheel_pairs", 1).first;
                 int dataCount = 0;
 
                 auto wheelSeparation = element->GetElement("wheel_separation");
@@ -120,10 +119,9 @@ namespace ROS2::SDFormat
                     }
                     else
                     {
-                        constexpr float epsilon = 0.001f;
                         AZ_Warning(
                             "CreateVehicleConfiguration",
-                            AZ::IsClose(configuration.m_wheelbase, wheelSeparation->Get<float>(), epsilon),
+                            AZ::IsClose(configuration.m_wheelbase, wheelSeparation->Get<float>()),
                             "Different wheel separation distances in one model are not supported.");
                     }
 
@@ -138,6 +136,7 @@ namespace ROS2::SDFormat
                     wheelSeparation = wheelSeparation->GetNextElement("wheel_separation");
                     wheelDiameter = wheelDiameter->GetNextElement("wheel_diameter");
                 }
+                [[maybe_unused]] const auto wheelPairs = element->Get<int>("num_wheel_pairs", 1).first;
                 AZ_Warning(
                     "CreateVehicleConfiguration",
                     wheelPairs == configuration.m_axles.size(),

+ 1 - 0
Gems/ROS2/Code/Source/Spawner/ROS2SpawnerComponent.cpp

@@ -107,6 +107,7 @@ namespace ROS2
         {
             response->model_names.emplace_back(spawnable.first.c_str());
         }
+        response->success = true;
     }
 
     void ROS2SpawnerComponent::SpawnEntity(

+ 48 - 3
Gems/ROS2/Code/Source/Utilities/Controllers/PidConfiguration.cpp

@@ -6,6 +6,7 @@
  *
  */
 
+#include <AzCore/Math/MathUtils.h>
 #include <AzCore/Serialization/EditContext.h>
 #include <AzCore/Serialization/EditContextConstants.inl>
 #include <AzCore/Serialization/SerializeContext.h>
@@ -70,15 +71,59 @@ namespace ROS2::Controllers
 
     void PidConfiguration::InitializePid()
     {
-        m_pid.initPid(m_p, m_i, m_d, m_iMax, m_iMin, m_antiWindup);
+        if (m_iMin > m_iMax)
+        {
+            AZ_Error("PidConfiguration", false, "Invalid PID configuration.");
+        }
+        else
+        {
+            m_initialized = true;
+        }
     }
 
     double PidConfiguration::ComputeCommand(double error, uint64_t deltaTimeNanoseconds)
     {
-        double output = m_pid.computeCommand(error, deltaTimeNanoseconds);
+        const double dt = aznumeric_cast<double>(deltaTimeNanoseconds) / 1.e9;
+
+        if (!m_initialized)
+        {
+            AZ_Error("PidConfiguration", false, "PID not initialized, ignoring.");
+            return 0.0;
+        }
+
+        if (dt <= 0.0 || !azisfinite(error))
+        {
+            AZ_Warning("PidConfiguration", false, "Invalid PID conditions.");
+            return 0.0;
+        }
+
+        const double proportionalTerm = m_p * error;
+
+        m_integral += error * dt;
+
+        if (m_antiWindup && m_i != 0)
+        {
+            AZStd::pair<double, double> bounds = AZStd::minmax<double>(m_iMin / m_i, m_iMax / m_i);
+            m_integral = AZStd::clamp<double>(m_integral, bounds.first, bounds.second);
+        }
+
+        double integralTerm = m_i * m_integral;
+
+        if (m_antiWindup)
+        {
+            integralTerm = AZStd::clamp<double>(integralTerm, m_iMin, m_iMax);
+        }
+
+        const double derivative = (error - m_previousError) / dt;
+        const double derivativeTerm = m_d * derivative;
+
+        m_previousError = error;
+
+        double output = proportionalTerm + integralTerm + derivativeTerm;
+
         if (m_outputLimit > 0.0)
         {
-            output = AZStd::clamp<float>(output, -m_outputLimit, m_outputLimit);
+            output = AZStd::clamp<double>(output, 0.0, m_outputLimit);
         }
         return output;
     }

+ 195 - 0
Gems/ROS2/Code/Tests/PIDTest.cpp

@@ -0,0 +1,195 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <AzCore/UnitTest/TestTypes.h>
+#include <AzTest/AzTest.h>
+
+#include <ROS2/Utilities/Controllers/PidConfiguration.h>
+
+namespace UnitTest
+{
+    static const uint64_t secToNanosec = 1e9;
+
+    class PIDTest : public LeakDetectionFixture
+    {
+    };
+
+    TEST_F(PIDTest, iClampAntiwindup)
+    {
+        double iGain = 1.0;
+        double iMin = -1.0;
+        double iMax = 1.0;
+
+        ROS2::Controllers::PidConfiguration pid(0.0, iGain, 0.0, iMax, iMin, true, 1.0);
+        pid.InitializePid();
+
+        double output = 0.0;
+
+        output = pid.ComputeCommand(-10.0, 1 * secToNanosec);
+        EXPECT_EQ(0.0, output);
+
+        output = pid.ComputeCommand(30.0, 1 * secToNanosec);
+        EXPECT_EQ(1.0, output);
+    }
+
+    TEST_F(PIDTest, iClampNoGain)
+    {
+        double iGain = 0.0;
+        double iMin = -1.0;
+        double iMax = 1.0;
+
+        ROS2::Controllers::PidConfiguration pid(0.0, iGain, 0.0, iMax, iMin, false, 0.0);
+        pid.InitializePid();
+
+        double output = 0.0;
+
+        output = pid.ComputeCommand(-1.0, 1 * secToNanosec);
+        EXPECT_LE(iMin, output);
+        EXPECT_LE(output, iMax);
+        EXPECT_EQ(0.0, output);
+
+        output = pid.ComputeCommand(-1.0, 1 * secToNanosec);
+        EXPECT_LE(iMin, output);
+        EXPECT_LE(output, iMax);
+        EXPECT_EQ(0.0, output);
+    }
+
+    TEST_F(PIDTest, iAntiwindup)
+    {
+        double iGain = 2.0;
+        double iMin = -1.0;
+        double iMax = 1.0;
+
+        ROS2::Controllers::PidConfiguration pid(0.0, iGain, 0.0, iMax, iMin, true, 0.0);
+        pid.InitializePid();
+
+        double output = 0.0;
+
+        output = pid.ComputeCommand(-1.0, 1 * secToNanosec);
+        EXPECT_EQ(-1.0, output);
+
+        output = pid.ComputeCommand(-1.0, 1 * secToNanosec);
+        EXPECT_EQ(-1.0, output);
+
+        output = pid.ComputeCommand(0.5, 1 * secToNanosec);
+        EXPECT_EQ(0.0, output);
+
+        output = pid.ComputeCommand(-1.0, 1 * secToNanosec);
+        EXPECT_EQ(-1.0, output);
+    }
+
+    TEST_F(PIDTest, negativeIAntiwindup)
+    {
+        double iGain = -2.5;
+        double iMin = -0.2;
+        double iMax = 0.5;
+
+        ROS2::Controllers::PidConfiguration pid(0.0, iGain, 0.0, iMax, iMin, true, 0.0);
+        pid.InitializePid();
+
+        double output = 0.0;
+
+        output = pid.ComputeCommand(0.1, 1 * secToNanosec);
+        EXPECT_EQ(-0.2, output);
+
+        output = pid.ComputeCommand(0.1, 1 * secToNanosec);
+        EXPECT_EQ(-0.2, output);
+
+        output = pid.ComputeCommand(-0.05, 1 * secToNanosec);
+        EXPECT_EQ(-0.075, output);
+
+        output = pid.ComputeCommand(0.1, 1 * secToNanosec);
+        EXPECT_EQ(-0.2, output);
+    }
+
+    TEST_F(PIDTest, pOnly)
+    {
+        ROS2::Controllers::PidConfiguration pid(1.0, 0.0, 0.0, 0.0, 0.0, false, 0.0);
+        pid.InitializePid();
+
+        double output = 0.0;
+
+        output = pid.ComputeCommand(-0.5, 1 * secToNanosec);
+        EXPECT_EQ(-0.5, output);
+
+        output = pid.ComputeCommand(-0.5, 1 * secToNanosec);
+        EXPECT_EQ(-0.5, output);
+
+        output = pid.ComputeCommand(-1.0, 1 * secToNanosec);
+        EXPECT_EQ(-1.0, output);
+
+        output = pid.ComputeCommand(0.5, 1 * secToNanosec);
+        EXPECT_EQ(0.5, output);
+    }
+
+    TEST_F(PIDTest, iOnly)
+    {
+        ROS2::Controllers::PidConfiguration pid(0.0, 1.0, 0.0, 5.0, -5.0, false, 0.0);
+        pid.InitializePid();
+
+        double output = 0.0;
+
+        output = pid.ComputeCommand(-0.5, 1 * secToNanosec);
+        EXPECT_EQ(-0.5, output);
+
+        output = pid.ComputeCommand(-0.5, 1 * secToNanosec);
+        EXPECT_EQ(-1.0, output);
+
+        output = pid.ComputeCommand(0.0, 1 * secToNanosec);
+        EXPECT_EQ(-1.0, output);
+
+        output = pid.ComputeCommand(0.0, 1 * secToNanosec);
+        EXPECT_EQ(-1.0, output);
+
+        output = pid.ComputeCommand(1.0, 1 * secToNanosec);
+        EXPECT_EQ(0.0, output);
+
+        output = pid.ComputeCommand(-0.5, 1 * secToNanosec);
+        EXPECT_EQ(-0.5, output);
+    }
+
+    TEST_F(PIDTest, dOnly)
+    {
+        ROS2::Controllers::PidConfiguration pid(0.0, 0.0, 1.0, 0.0, 0.0, false, 0.0);
+        pid.InitializePid();
+
+        double output = 0.0;
+
+        output = pid.ComputeCommand(-0.5, 1 * secToNanosec);
+        EXPECT_EQ(-0.5, output);
+
+        output = pid.ComputeCommand(-0.5, 1 * secToNanosec);
+        EXPECT_EQ(0.0, output);
+
+        output = pid.ComputeCommand(-0.5, 0 * secToNanosec);
+        EXPECT_EQ(0.0, output);
+
+        output = pid.ComputeCommand(-1.0, 1 * secToNanosec);
+        EXPECT_EQ(-0.5, output);
+
+        output = pid.ComputeCommand(-0.5, 1 * secToNanosec);
+        EXPECT_EQ(0.5, output);
+    }
+
+    TEST_F(PIDTest, completePID)
+    {
+        ROS2::Controllers::PidConfiguration pid(1.0, 1.0, 1.0, 5.0, -5.0, false, 0.0);
+        pid.InitializePid();
+
+        double output = 0.0;
+
+        output = pid.ComputeCommand(-0.5, 1 * secToNanosec);
+        EXPECT_EQ(-1.5, output);
+
+        output = pid.ComputeCommand(-0.5, 1 * secToNanosec);
+        EXPECT_EQ(-1.5, output);
+
+        output = pid.ComputeCommand(-1.0, 1 * secToNanosec);
+        EXPECT_EQ(-3.5, output);
+    }
+} // namespace UnitTest

+ 2 - 0
Gems/ROS2/Code/ros2_editor_files.cmake

@@ -18,6 +18,8 @@ set(FILES
     Source/Manipulation/JointsPositionsEditorComponent.h
     Source/Manipulation/JointsManipulationEditorComponent.cpp
     Source/Manipulation/JointsManipulationEditorComponent.h
+    Source/Manipulation/JointUtils.cpp
+    Source/Manipulation/JointUtils.h
     Source/RobotImporter/FixURDF/FixURDF.cpp
     Source/RobotImporter/FixURDF/FixURDF.h
     Source/RobotImporter/Pages/ModifiedURDFWindow.cpp

+ 1 - 0
Gems/ROS2/Code/ros2_tests_files.cmake

@@ -6,4 +6,5 @@
 set(FILES
     Tests/ROS2Test.cpp
     Tests/GNSSTest.cpp
+    Tests/PIDTest.cpp
 )

+ 3 - 3
Gems/ROS2/gem.json

@@ -1,6 +1,6 @@
 {
     "gem_name": "ROS2",
-    "version": "3.2.2",
+    "version": "3.2.3",
     "platforms": [
         "Linux"
     ],
@@ -23,7 +23,7 @@
         "o3de>=2.3.0"
     ],
     "icon_path": "preview.png",
-    "requirements": "Requires ROS 2 installation (supported distributions: Humble). Source your workspace before building the Gem",
+    "requirements": "Requires ROS 2 installation (supported distributions: Humble, Jazzy). Source your workspace before building the Gem",
     "documentation_url": "https://docs.o3de.org/docs/user-guide/gems/reference/robotics/ros2/",
     "dependencies": [
         "Atom_RPI",
@@ -36,5 +36,5 @@
     ],
     "restricted": "ROS2",
     "repo_uri": "https://raw.githubusercontent.com/o3de/o3de-extras/development",
-    "download_source_uri": "https://github.com/o3de/o3de-extras/releases/download/2.0/ros2-3.2.2-gem.zip"
+    "download_source_uri": "https://github.com/o3de/o3de-extras/releases/download/2.0/ros2-3.2.3-gem.zip"
 }

+ 1 - 1
Gems/RosRobotSample/Assets/ROSbot.prefab

@@ -2091,4 +2091,4 @@
             }
         }
     }
-}
+}

+ 1 - 0
Templates/Ros2FleetRobotTemplate/Template/Gem/gem.json

@@ -21,6 +21,7 @@
     "requirements": "",
     "documentation_url": "",
     "dependencies": [
+        "ROS2>=3.1.0"
     ],
     "compatible_engines": [
         "o3de-sdk>=1.2.0",

+ 2 - 2
Templates/Ros2FleetRobotTemplate/template.json

@@ -1,6 +1,6 @@
 {
     "template_name": "Ros2FleetRobotTemplate",
-    "version": "2.0.2",
+    "version": "2.0.3",
     "origin": "Open 3D Engine Extras",
     "origin_url": "https://github.com/o3de/o3de-extras",
     "repo_uri": "https://raw.githubusercontent.com/o3de/o3de-extras/development",
@@ -493,5 +493,5 @@
             "dir": "Examples/ros2_ws/src/o3de_fleet_nav/test"
         }
     ],
-    "download_source_uri": "https://github.com/o3de/o3de-extras/releases/download/2.0/ros2fleetrobottemplate-2.0.2-template.zip"
+    "download_source_uri": "https://github.com/o3de/o3de-extras/releases/download/2.0/ros2fleetrobottemplate-2.0.3-template.zip"
 }

+ 0 - 114
Templates/Ros2ProjectTemplate/Docker/Dockerfile

@@ -1,114 +0,0 @@
-# Copyright (c) Contributors to the Open 3D Engine Project.
-# For complete copyright and license terms please see the LICENSE at the root of this distribution.
-#
-# SPDX-License-Identifier: Apache-2.0 OR MIT
-#
-
-ARG ROS_VERSION=jazzy
-
-FROM ros:${ROS_VERSION}-ros-base
-
-# Argument to determining the image type ('simulation' or 'navstack')
-ARG IMAGE_TYPE=simulation  # Default to 'simulation'
-
-# Arguments for the source repos needed for the Ros2Template sample docker
-ARG O3DE_REPO=https://github.com/o3de/o3de.git
-ARG O3DE_BRANCH=main
-
-ARG O3DE_EXTRAS_REPO=https://github.com/o3de/o3de-extras.git
-ARG O3DE_EXTRAS_BRANCH=main
-
-# Additional argument to control build concurrency
-ARG CMAKE_JOBS=8
-
-ENV WORKSPACE=/data/workspace
-
-ENV PROJECT_PATH=/data/workspace/Ros2Project
-
-WORKDIR $WORKSPACE
-
-RUN apt-get update && apt-get upgrade -y
-
-# Install packages needed for cloning and building from the source repos
-RUN apt-get install -y --no-install-recommends \
-    git \
-    git-lfs \
-    clang \
-    ninja-build \
-    cmake \
-    libglu1-mesa-dev \
-    libxcb-xinerama0 \
-    libxcb-xinput0 \
-    libxcb-xinput-dev \
-    libxcb-xfixes0-dev \
-    libxcb-xkb-dev \
-    libxkbcommon-dev \
-    libxkbcommon-x11-dev \
-    libfontconfig1-dev \
-    libcurl4-openssl-dev \
-    libsdl2-dev \
-    zlib1g-dev \
-    mesa-common-dev \
-    libssl-dev libxcb-icccm4 \
-    libxcb-image0 \
-    libxcb-keysyms1 \
-    libxcb-render-util0 \
-    libxcb-randr0 \
-    libnvidia-gl-470 \
-    ufw \
-    ros-${ROS_DISTRO}-slam-toolbox \
-    ros-${ROS_DISTRO}-navigation2 \
-    ros-${ROS_DISTRO}-nav2-bringup \
-    ros-${ROS_DISTRO}-pointcloud-to-laserscan \
-    ros-${ROS_DISTRO}-gazebo-msgs \
-    ros-${ROS_DISTRO}-ackermann-msgs \
-    ros-${ROS_DISTRO}-rmw-cyclonedds-cpp \
-    ros-${ROS_DISTRO}-control-toolbox \
-    ros-${ROS_DISTRO}-nav-msgs \
-    ros-${ROS_DISTRO}-desktop
-
-
-# Copy script used to remove build artifacts
-COPY cleanup.bash /data/workspace/cleanup.bash
-
-# Clone and register O3DE repos 
-RUN if [ "${IMAGE_TYPE}" = "simulation" ]; then \
-        cd $WORKSPACE && \
-        git clone --recursive $O3DE_REPO && \
-        git -C $WORKSPACE/o3de checkout $O3DE_BRANCH &&\
-        git -C $WORKSPACE/o3de lfs install && \
-        git -C $WORKSPACE/o3de lfs pull && \
-        $WORKSPACE/o3de/python/get_python.sh && \
-        $WORKSPACE/o3de/scripts/o3de.sh register -ep $WORKSPACE/o3de && \
-        git clone $O3DE_EXTRAS_REPO && \
-        git -C $WORKSPACE/o3de-extras checkout $O3DE_EXTRAS_BRANCH && \
-        $WORKSPACE/o3de/scripts/o3de.sh register -gp $WORKSPACE/o3de-extras/Gems/ROS2 && \
-        $WORKSPACE/o3de/scripts/o3de.sh register -gp $WORKSPACE/o3de-extras/Gems/RosRobotSample && \
-        $WORKSPACE/o3de/scripts/o3de.sh register -gp $WORKSPACE/o3de-extras/Gems/WarehouseAssets && \
-        $WORKSPACE/o3de/scripts/o3de.sh register -gp $WORKSPACE/o3de-extras/Gems/WarehouseSample; \
-    elif [  "${IMAGE_TYPE}" = "navstack" ]; then \
-        cd $WORKSPACE && \
-        git clone $O3DE_EXTRAS_REPO && \
-        git -C $WORKSPACE/o3de-extras checkout $O3DE_EXTRAS_BRANCH; \
-    else \
-        echo "Unsupported IMAGE_TYPE: ${IMAGE_TYPE}" && exit 1; \
-    fi
-
-# Build and cleanup in the same layer to reduce the size
-RUN if [ "${IMAGE_TYPE}" = "simulation" ]; then \
-        . /opt/ros/${ROS_DISTRO}/setup.sh && \
-        $WORKSPACE/o3de/scripts/o3de.sh create-project --project-path $PROJECT_PATH --template-path $WORKSPACE/o3de-extras/Templates/Ros2ProjectTemplate && \
-        cmake -B $PROJECT_PATH/build/linux -S $PROJECT_PATH -G "Ninja Multi-Config" -DLY_STRIP_DEBUG_SYMBOLS=TRUE -DLY_DISABLE_TEST_MODULES=ON && \
-        cmake --build $PROJECT_PATH/build/linux --config profile --target Ros2Project Editor Ros2Project.Assets Ros2Project.GameLauncher -j $CMAKE_JOBS && \
-        $WORKSPACE/cleanup.bash ; \
-    fi
-
-COPY LaunchSimulation.bash /data/workspace/LaunchSimulation.bash
-COPY LaunchNavStack.bash /data/workspace/LaunchNavStack.bash
-
-ENV RMW_IMPLEMENTATION=rmw_cyclonedds_cpp 
-ENV LAUNCH_FULLSCREEN_OPT=0
-ENV NVIDIA_VISIBLE_DEVICES all
-ENV NVIDIA_DRIVER_CAPABILITIES all
-
-ENTRYPOINT ["/bin/bash"]

+ 0 - 15
Templates/Ros2ProjectTemplate/Docker/LaunchNavStack.bash

@@ -1,15 +0,0 @@
-#!/bin/bash
-
-# Copyright (c) Contributors to the Open 3D Engine Project.
-# For complete copyright and license terms please see the LICENSE at the root of this distribution.
-#
-# SPDX-License-Identifier: Apache-2.0 OR MIT
-#
-
-unset LD_LIBRARY_PATH
-
-source /opt/ros/$ROS_DISTRO/setup.bash
-
-cd /data/workspace/o3de-extras/Templates/Ros2ProjectTemplate/Template/Examples/slam_navigation/launch
-
-ros2 launch navigation.launch.py

+ 0 - 22
Templates/Ros2ProjectTemplate/Docker/LaunchSimulation.bash

@@ -1,22 +0,0 @@
-#!/bin/bash
-
-# Copyright (c) Contributors to the Open 3D Engine Project.
-# For complete copyright and license terms please see the LICENSE at the root of this distribution.
-#
-# SPDX-License-Identifier: Apache-2.0 OR MIT
-#
-
-unset LD_LIBRARY_PATH
-
-source /opt/ros/$ROS_DISTRO/setup.bash
-
-export LD_LIBRARY_PATH=/data/workspace/Ros2Project/build/linux/bin/profile:$LD_LIBRARY_PATH
-
-if [ -d /data/workspace/Ros2Project/build/linux/bin/profile ]
-then
-    cd /data/workspace/Ros2Project/build/linux/bin/profile
-    ./Ros2Project.GameLauncher -r_fullscreen=$LAUNCH_FULLSCREEN_OPT -bg_ConnectToAssetProcessor=0 > /data/workspace/simulation_launch.log 2>&1
-else
-    echo "Simulation not installed on this image"
-fi
-

+ 0 - 119
Templates/Ros2ProjectTemplate/Docker/README.md

@@ -1,119 +0,0 @@
-# Docker scripts for running the O3DE Ros2Project based on O3DE Ros2ProjectTemplate
-
-The following Dockerfiles defined in this path will prepare the appropriate ROS2 package (Ubuntu 20.04/Focal + Galactic or Ubuntu 22.04/Jammy + Iron) based environment and build the components necessary to run the O3DE demo project simulator through the O3DE engine using the Ros2Project template.
-
-## Prerequisites
-
-* [Hardware requirements of o3de](https://www.o3de.org/docs/welcome-guide/requirements/)
-* Ubuntu 20.04 (Focal) or 22.04 (Jammy)
-* At least 60 GB of free disk space
-* Docker 24.0 installed and configured
-  * **Note** It is recommended to have Docker installed correctly and in a secure manner so that the docker commands in this guide do not require elevated privileges (sudo) to run them. See [Docker Engine post-installation steps](https://docs.docker.com/engine/install/linux-postinstall/) for more details.
-* [NVidia container toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html#docker)
-
-## Building the Docker Image
-
-By default, the docker script provided in this template will build a docker image to run the Ros2Project sample using Ubuntu 22.04 (jammy) with the ROS2 Iron distribution. All obsolete source code, git artifacts, and build artifacts will be removed by the 'cleanup.bash' script when completed. 
-
-Run the following command to build the docker image with a default configuration:
-
-```
-docker build -t o3de_ros2project:latest -f Dockerfile .
-```
-
-**Note** 
-The above command example tags specific commits for O3DE, and the ROS2 gem repos based on known working commits. See the Advanced Options section below for more information.
-
-This will create a docker image named 'o3de_ros2project' with the tag 'latest' that contains both the simulation launcher and the navigation stack. 
-
-It will also contain helper scripts that will launch either the simulation (LaunchSimulation.bash) or the RViz2 (LaunchNavStack.bash).
-
-You can also create a separate docker image that only contains the navigation stack and RViz2 by supplying the argument `IMAGE_TYPE` and setting it to 'navstack':
-
-```
-docker build --build-arg IMAGE_TYPE=navstack -t o3de_ros2project_nav:latest .
-```
-
-ROS2 allows for communication across multiple docker images running on the same host, provided that they specify the 'bridge' network type when launching.
-
-## Running the Docker Image
-
-Launching O3DE applications in a Docker container requires GPU acceleration support. (Make sure that the [nvidia-docker 2](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html#docker) is installed.)
-
-### Direct Access to the X Server
-The simulation docker image should be launched before bringing up the robot application. To run the robot application, first, allow the container root user to access the running X server for display
-
-```
-xhost +local:root
-```
-
-Then launch the built simulation docker image with the following command
-
-```
-docker run --rm --network="bridge" --gpus all -e DISPLAY=:1 -v /tmp/.X11-unix:/tmp/.X11-unix -it o3de_ros2project:latest /data/workspace/LaunchSimulation.bash
-```
-
-Once the simulation is up and running, launch the navigation stack inside the simulation docker image, which will bring up RViz to control the robot.
-
-```
-docker run --rm --network="bridge" --gpus all -e DISPLAY=:1 -v /tmp/.X11-unix:/tmp/.X11-unix -it o3de_ros2project:latest /data/workspace/LaunchNavStack.bash
-
-```
-
-If you created a separate docker image 'o3de_ros2project_nav:latest', which only contains the navigation stack and RViz2, you can launch it using that image, provided that the simulation docker image 'o3de_ros2project' is running.
-
-```
-docker run --rm --network="bridge" --gpus all -e DISPLAY=:1 -v /tmp/.X11-unix:/tmp/.X11-unix -it o3de_ros2project_nav:latest /data/workspace/LaunchNavStack.bash
-```
-Make sure to revoke access to the X server when the simulation ends.
-
-```
-xhost -local:root
-```
-
-### Running using Rocker
-
-Alternatively, you can use [Rocker](https://github.com/osrf/rocker) to run a GPU-accelerated docker image. 
-
-Launch the built simulation docker image with the following rocker command
-
-```
-rocker --x11 --nvidia o3de_ros2project:latest /data/workspace/LaunchSimulation.bash
-```
-
-Once the simulation is up and running, launch the robot application docker image, which will bring up RViz to control the robot.
-
-```
-rocker --x11 --nvidia o3de_ros2project:latest /data/workspace/LaunchNavStack.bash
-```
-
-## Advanced Options
-
-### Target ROS2 Distribution
-The Docker script defaults to building an image based on Ubuntu 22.04 (jammy) and the ROS2 Iron distribution. This can be overridden with a combination if the `ROS_VERSION` and `UBUNTU_VERSION` arguments.
-
-| ROS2 Distro   | Repository                                |
-|---------------|-------------------------------------------|
-| galactic      | ROS_VERSION=galactic UBUNTU_VERSION=focal |
-| humble        | ROS_VERSION=humble   UBUNTU_VERSION=jammy |
-| iron          | ROS_VERSION=iron     UBUNTU_VERSION=jammy |
-
-### Custom source repos and branches
-
-The Dockerscripts use the following arguments to determine the repository to pull the source: 
-
-| Argument              | Repository                 | Default                                      |
-|-----------------------|----------------------------|----------------------------------------------|
-| O3DE_REPO             | O3DE                       | https://github.com/o3de/o3de.git             |
-| O3DE_EXTRAS_REPO      | O3DE Extras                | https://github.com/o3de/o3de-extras.git      |
-
-In addition to repositories, the following arguments target the branch, commit, or tag to pull from their corresponding repository:
-
-| Argument                | Repository                       | Default     |
-|-------------------------|----------------------------------|-------------|
-| O3DE_BRANCH             | O3DE                             | 2305.0      |
-| O3DE_EXTRAS_BRANCH      | O3DE Extras                      | 2305.0      |
-
-### Optimizing the build process ###
-The docker script provides a cmake-specific argument override to control the number of parallel jobs that can be used during the build of the docker image. `CMAKE_JOBS` sets the maximum number of concurrent jobs cmake will run during its build process and defaults to 8 jobs. This number can be adjusted to better suit the hardware which is running the docker image build.
-

+ 0 - 43
Templates/Ros2ProjectTemplate/Docker/cleanup.bash

@@ -1,43 +0,0 @@
-#!/bin/bash
-
-# Copyright (c) Contributors to the Open 3D Engine Project.
-# For complete copyright and license terms please see the LICENSE at the root of this distribution.
-#
-# SPDX-License-Identifier: Apache-2.0 OR MIT
-#
-
-# Delete obsolete files after the build of the code and assets are complete
-
-DELETE_LIST=(~/.o3de/3rdParty/ \
-             o3de/.git \
-             o3de/AutomatedTesting \
-             o3de/python/downloaded_packages \
-             o3de/Code \
-             o3de/Gems \
-             Ros2Project/build/linux/Azcg/ \
-             Ros2Project/build/linux/CMake \
-             Ros2Project/build/linux/CMakeFiles/ \
-             Ros2Project/build/linux/External/ \
-             Ros2Project/build/linux/Testing/ \
-             Ros2Project/build/linux/_deps/ \
-             Ros2Project/build/linux/cmake \
-             Ros2Project/build/linux/lib/ \
-             Ros2Project/build/linux/o3de/ \
-             Ros2Project/build/linux/packages/ \
-             Ros2Project/build/linux/runtime_dependencies/ 
-             Ros2Project/build/linux/bin/profile/EditorPlugins \
-             Ros2Project/build/linux/bin/profile/Editor \
-             Ros2Project/build/linux/bin/profile/AssetProcessor \
-             Ros2Project/build/linux/bin/profile/AssetProcessorBatch \
-             Ros2Project/build/linux/bin/profile/MaterialEditor \
-             Ros2Project/build/linux/bin/profile/AssetBuilder \
-             Ros2Project/build/linux/bin/profile/MaterialCanvas )
-
-for i in ${DELETE_LIST[@]}
-do
-   echo "Deleting /data/workspace/$i"
-   rm -rf $i
-done
-
-exit 0
-

+ 1 - 0
Templates/Ros2ProjectTemplate/Template/Gem/gem.json

@@ -24,5 +24,6 @@
     ],
     "documentation_url": "",
     "dependencies": [
+        "ROS2>=3.1.0"
     ]
 }

+ 1 - 1
Templates/Ros2ProjectTemplate/Template/Levels/DemoLevel/DemoLevel.prefab

@@ -268,7 +268,7 @@
                     "$type": "GenericComponentWrapper",
                     "Id": 2318932367744128274,
                     "m_template": {
-                        "$type": "R2PTSampleComponent",
+                        "$type": "${SanitizedCppName}SampleComponent",
                         "goals": [
                             "Entity_[549827034797]",
                             "Entity_[545532067501]"

+ 9 - 0
Templates/Ros2ProjectTemplate/Template/Registry/load_level.setreg

@@ -0,0 +1,9 @@
+{
+   "O3DE": {
+        "Autoexec": {
+            "ConsoleCommands": {
+                "LoadLevel": "demolevel"
+            }
+        }
+    }
+}

+ 0 - 1
Templates/Ros2ProjectTemplate/Template/autoexec.cfg

@@ -1 +0,0 @@
-LoadLevel levels/demolevel/demolevel.spawnable

+ 2 - 6
Templates/Ros2ProjectTemplate/template.json

@@ -1,6 +1,6 @@
 {
     "template_name": "Ros2ProjectTemplate",
-    "version": "2.0.2",
+    "version": "2.0.3",
     "origin": "Open 3D Engine Extras",
     "origin_url": "https://github.com/o3de/o3de-extras",
     "license": "https://github.com/o3de/o3de-extras/tree/development/Templates/Ros2ProjectTemplate/Template/LICENSE.txt",
@@ -402,10 +402,6 @@
             "file": "ShaderLib/viewsrg.srgi",
             "isTemplated": true
         },
-        {
-            "file": "autoexec.cfg",
-            "isTemplated": false
-        },
         {
             "file": "game.cfg",
             "isTemplated": false
@@ -561,5 +557,5 @@
         }
     ],
     "repo_uri": "https://raw.githubusercontent.com/o3de/o3de-extras/development",
-    "download_source_uri": "https://github.com/o3de/o3de-extras/releases/download/2.0/ros2projecttemplate-2.0.2-template.zip"
+    "download_source_uri": "https://github.com/o3de/o3de-extras/releases/download/2.0/ros2projecttemplate-2.0.3-template.zip"
 }

+ 1 - 0
Templates/Ros2RoboticManipulationTemplate/Template/Gem/gem.json

@@ -21,6 +21,7 @@
     "requirements": "",
     "documentation_url": "",
     "dependencies": [
+        "ROS2>=3.1.0"
     ],
     "compatible_engines": [
     ],

+ 2 - 2
Templates/Ros2RoboticManipulationTemplate/template.json

@@ -1,6 +1,6 @@
 {
     "template_name": "Ros2RoboticManipulationTemplate",
-    "version": "2.0.1",
+    "version": "2.0.2",
     "origin": "https://github.com/o3de/o3de-extras",
     "license": "https://github.com/o3de/o3de-extras/tree/development/Templates/Ros2RoboticManipulationTemplate/Template/LICENSE.txt",
     "display_name": "ROS2 Robotic Manipulation",
@@ -834,5 +834,5 @@
         }
     ],
     "repo_uri": "https://raw.githubusercontent.com/o3de/o3de-extras/development",
-    "download_source_uri": "https://github.com/o3de/o3de-extras/releases/download/2.0/ros2roboticmanipulationtemplate-2.0.1-template.zip"
+    "download_source_uri": "https://github.com/o3de/o3de-extras/releases/download/2.0/ros2roboticmanipulationtemplate-2.0.2-template.zip"
 }

File diff suppressed because it is too large
+ 745 - 239
repo.json


Some files were not shown because too many files changed in this diff