Skip to content

Commit 0faa487

Browse files
Merge pull request #10 from NVIDIA-ISAAC-ROS/hotfix-release-3.0-1
Add Nova Carter Docking package
2 parents 44b542a + fccf2d6 commit 0faa487

15 files changed

+1260
-0
lines changed

nova_carter_docking/CMakeLists.txt

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES
2+
# Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
16+
# SPDX-License-Identifier: Apache-2.0
17+
cmake_minimum_required(VERSION 3.5)
18+
project(nova_carter_docking)
19+
20+
find_package(ament_cmake REQUIRED)
21+
find_package(ament_cmake_python REQUIRED)
22+
find_package(angles REQUIRED)
23+
find_package(rclcpp REQUIRED)
24+
find_package(rclpy REQUIRED)
25+
find_package(rclcpp_action REQUIRED)
26+
find_package(rclcpp_lifecycle REQUIRED)
27+
find_package(rclcpp_components REQUIRED)
28+
find_package(std_msgs REQUIRED)
29+
find_package(sensor_msgs REQUIRED)
30+
find_package(nav2_msgs REQUIRED)
31+
find_package(nav_msgs REQUIRED)
32+
find_package(geometry_msgs REQUIRED)
33+
find_package(tf2_geometry_msgs REQUIRED)
34+
find_package(tf2_ros REQUIRED)
35+
find_package(pluginlib REQUIRED)
36+
find_package(opennav_docking_core REQUIRED)
37+
find_package(isaac_ros_apriltag_interfaces REQUIRED)
38+
39+
# potentially replace with nav2_common, nav2_package()
40+
set(CMAKE_CXX_STANDARD 17)
41+
add_compile_options(-Wall -Wextra -Wpedantic -Werror -Wdeprecated -fPIC -Wshadow -Wnull-dereference)
42+
43+
set(dependencies
44+
angles
45+
rclcpp
46+
rclcpp_action
47+
rclcpp_lifecycle
48+
rclcpp_components
49+
std_msgs
50+
sensor_msgs
51+
nav2_msgs
52+
nav_msgs
53+
geometry_msgs
54+
tf2_ros
55+
tf2_geometry_msgs
56+
pluginlib
57+
opennav_docking_core
58+
isaac_ros_apriltag_interfaces
59+
)
60+
61+
add_executable(dock_pose_publisher src/dock_pose_publisher.cpp)
62+
ament_target_dependencies(dock_pose_publisher ${dependencies})
63+
64+
install(TARGETS
65+
dock_pose_publisher
66+
DESTINATION lib/${PROJECT_NAME})
67+
68+
# Install Python modules
69+
ament_python_install_package(${PROJECT_NAME})
70+
# Install Python executables
71+
install(PROGRAMS
72+
scripts/fp_dock_pose_publisher.py
73+
DESTINATION lib/${PROJECT_NAME}
74+
)
75+
76+
install(DIRECTORY launch DESTINATION share/${PROJECT_NAME})
77+
install(DIRECTORY params DESTINATION share/${PROJECT_NAME})
78+
79+
ament_export_include_directories(include)
80+
ament_export_dependencies(${dependencies})
81+
ament_package()

nova_carter_docking/demo.py

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
#! /usr/bin/env python3
2+
# Copyright 2024 Open Navigation LLC
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
from enum import Enum
17+
import time
18+
19+
from action_msgs.msg import GoalStatus
20+
from geometry_msgs.msg import PoseStamped
21+
from lifecycle_msgs.srv import GetState
22+
from opennav_docking_msgs.action import DockRobot, UndockRobot
23+
import rclpy
24+
from rclpy.action import ActionClient
25+
from rclpy.duration import Duration
26+
from rclpy.node import Node
27+
28+
29+
class TaskResult(Enum):
30+
UNKNOWN = 0
31+
SUCCEEDED = 1
32+
CANCELED = 2
33+
FAILED = 3
34+
35+
36+
class DockingTester(Node):
37+
38+
def __init__(self):
39+
super().__init__(node_name='docking_tester')
40+
self.goal_handle = None
41+
self.result_future = None
42+
self.status = None
43+
self.feedback = None
44+
45+
self.docking_client = ActionClient(self, DockRobot,
46+
'dock_robot')
47+
self.undocking_client = ActionClient(self, UndockRobot,
48+
'undock_robot')
49+
50+
def destroy_node(self):
51+
self.docking_client.destroy()
52+
self.undocking_client.destroy()
53+
super().destroy_node()
54+
55+
def dockRobot(self, dock_pose, dock_type = ""):
56+
"""Send a `DockRobot` action request."""
57+
print("Waiting for 'DockRobot' action server")
58+
while not self.docking_client.wait_for_server(timeout_sec=1.0):
59+
print('"DockRobot" action server not available, waiting...')
60+
61+
goal_msg = DockRobot.Goal()
62+
goal_msg.use_dock_id = False
63+
# goal_msg.dock_id = dock_id # if wanting to use ID instead
64+
goal_msg.dock_pose = dock_pose
65+
goal_msg.dock_type = dock_type
66+
# goal_msg.navigate_to_staging_pose = True # if want to navigate before staging
67+
68+
print('Docking at pose: ' + str(dock_pose) + '...')
69+
send_goal_future = self.docking_client.send_goal_async(goal_msg,
70+
self._feedbackCallback)
71+
rclpy.spin_until_future_complete(self, send_goal_future)
72+
self.goal_handle = send_goal_future.result()
73+
74+
if not self.goal_handle.accepted:
75+
print('Docking request was rejected!')
76+
return False
77+
78+
self.result_future = self.goal_handle.get_result_async()
79+
return True
80+
81+
def undockRobot(self, dock_type):
82+
"""Send a `UndockRobot` action request."""
83+
print("Waiting for 'UndockRobot' action server")
84+
while not self.undocking_client.wait_for_server(timeout_sec=1.0):
85+
print('"UndockRobot" action server not available, waiting...')
86+
87+
goal_msg = UndockRobot.Goal()
88+
goal_msg.dock_type = dock_type
89+
90+
print('Undocking from dock of type: ' + str(dock_type) + '...')
91+
send_goal_future = self.undocking_client.send_goal_async(goal_msg,
92+
self._feedbackCallback)
93+
rclpy.spin_until_future_complete(self, send_goal_future)
94+
self.goal_handle = send_goal_future.result()
95+
96+
if not self.goal_handle.accepted:
97+
print('Undocking request was rejected!')
98+
return False
99+
100+
self.result_future = self.goal_handle.get_result_async()
101+
return True
102+
103+
def isTaskComplete(self):
104+
"""Check if the task request of any type is complete yet."""
105+
if not self.result_future:
106+
# task was cancelled or completed
107+
return True
108+
rclpy.spin_until_future_complete(self, self.result_future, timeout_sec=0.10)
109+
if self.result_future.result():
110+
self.status = self.result_future.result().status
111+
if self.status != GoalStatus.STATUS_SUCCEEDED:
112+
print(f'Task with failed with status code: {self.status}')
113+
return True
114+
else:
115+
# Timed out, still processing, not complete yet
116+
return False
117+
118+
print('Task succeeded!')
119+
return True
120+
121+
def _feedbackCallback(self, msg):
122+
self.feedback = msg.feedback
123+
return
124+
125+
def cancelAction(self):
126+
self.goal_handle.cancel_goal()
127+
128+
def getFeedback(self):
129+
"""Get the pending action feedback message."""
130+
return self.feedback
131+
132+
def getResult(self):
133+
"""Get the pending action result message."""
134+
if self.status == GoalStatus.STATUS_SUCCEEDED:
135+
return TaskResult.SUCCEEDED
136+
elif self.status == GoalStatus.STATUS_ABORTED:
137+
return TaskResult.FAILED
138+
elif self.status == GoalStatus.STATUS_CANCELED:
139+
return TaskResult.CANCELED
140+
else:
141+
return TaskResult.UNKNOWN
142+
143+
def startup(self, node_name='docking_server'):
144+
# Waits for the node within the tester namespace to become active
145+
print(f'Waiting for {node_name} to become active..')
146+
node_service = f'{node_name}/get_state'
147+
state_client = self.create_client(GetState, node_service)
148+
while not state_client.wait_for_service(timeout_sec=1.0):
149+
print(f'{node_service} service not available, waiting...')
150+
151+
req = GetState.Request()
152+
state = 'unknown'
153+
while state != 'active':
154+
print(f'Getting {node_name} state...')
155+
future = state_client.call_async(req)
156+
rclpy.spin_until_future_complete(self, future)
157+
if future.result() is not None:
158+
state = future.result().current_state.label
159+
print(f'Result of get_state: {state}')
160+
time.sleep(2)
161+
return
162+
163+
164+
def main():
165+
rclpy.init()
166+
167+
tester = DockingTester()
168+
tester.startup()
169+
170+
while True:
171+
time.sleep(1)
172+
173+
# Some example dock 40 cm in front of itself.
174+
# The same as the staging distance to dock in mid-air
175+
dock_pose = PoseStamped()
176+
dock_pose.header.stamp = tester.get_clock().now().to_msg()
177+
dock_pose.header.frame_id = "base_link"
178+
dock_pose.pose.position.x = 0.7
179+
dock_pose.pose.position.y = 0.0
180+
tester.dockRobot(dock_pose)
181+
182+
# Test cancel action
183+
# time.sleep(0.5)
184+
# tester.cancelAction()
185+
186+
i = 0
187+
while not tester.isTaskComplete():
188+
i = i + 1
189+
if i % 5 == 0:
190+
print('Docking in progress...')
191+
time.sleep(1)
192+
193+
# Do something depending on the return code
194+
result = tester.getResult()
195+
if result == TaskResult.SUCCEEDED:
196+
print('Docking succeeded!')
197+
elif result == TaskResult.CANCELED:
198+
print('Docking canceled!')
199+
elif result == TaskResult.FAILED:
200+
print('Docking failed!')
201+
else:
202+
print('Docking has an invalid return status!')
203+
204+
time.sleep(3)
205+
206+
# Undock from this dock
207+
dock_type = "nova_carter_dock"
208+
tester.undockRobot(dock_type)
209+
210+
i = 0
211+
while not tester.isTaskComplete():
212+
i = i + 1
213+
if i % 5 == 0:
214+
print('Undocking in progress...')
215+
time.sleep(1)
216+
217+
# Do something depending on the return code
218+
result = tester.getResult()
219+
if result == TaskResult.SUCCEEDED:
220+
print('Undock succeeded!')
221+
elif result == TaskResult.CANCELED:
222+
print('Undock canceled!')
223+
elif result == TaskResult.FAILED:
224+
print('Undock failed!')
225+
else:
226+
print('Undock has an invalid return status!')
227+
228+
229+
if __name__ == '__main__':
230+
main()

0 commit comments

Comments
 (0)